Author Archives: AKIYAMA

2022-12-26

Windows上のEmacsで初期化を速くする即効性のある方法

Windows上のEmacsは起動もかなり遅く私もこれまでに色々試したのですが、今回はその中で最も効果的だったload-path解決の高速化をご紹介したいと思います。

何が遅いのか

Windows環境でプロファイルをするとすぐに見つかるのが locate-library が遅いということだと思います。つまり、 (locate-library "magit") などとしたときにこれに平気で数十ミリ秒も持って行かれたりします。

試しにやってみましょう。

(car (benchmark-run 1 (locate-library "magit")))
0.06533

65msかかりました。

今私の手元では (length load-path) は 218 を返してきます。つまり、load-pathに218のディレクトリが設定されているわけです(無駄なディレクトリ多すぎ)。

load-pathの中で最も最後にあるのが (Emacsのインストールディレクトリ)/share/emacs/28.2/lisp/obsolete ですが、その最後のファイルであるyow.elを探してみましょう。

(car (benchmark-run 1 (locate-library "yow")))
0.113833

なんと113.8msもかかりました。

一方で一番最初のディレクトリにあったのは all-the-icons-dired.el でした。

(car (benchmark-run 1 (locate-library "all-the-icons-dired")))
0.001121

こちらは1.1msで済んでいます。

load-path上の順番によって処理速度が大幅に変わっていることが分かります。

locate-library よりもやや気がつきにくいのですが、実は require も同じだけかかっています。 (require 'magit) とすれば初回は当然パスを解決するだけで65msかかってしまいます。trampを読み込むだけで1秒以上持って行かれるのには参りました。その中でも分かりやすかったのがring.el。見てみれば分かりますがとてもシンプルなelispですが、やはり60msくらいrequireで時間を消費していました。こんな他のファイルに依存していない小さなファイルの読み込みがそんなにかかるわけがありません。読み込み以前のパスの解決でそれだけかかっているのです。

なぜ遅いのか

知りません。なぜ遅いのかを知るにはC言語のコードに遡って処理を理解する必要がありますが面倒くさいので見ていません。

また、手元のVirtualBoxに入れたUbuntu(Emacs 27.1)で同様の試験をした結果……

(benchmark-run 1 (locate-library "yow"))
0.007467886

(length load-path) が165で、一番最後にあるyowのパスを特定するのに7msかかりました。文字通り桁が違います。

というわけでWindows版のEmacsに特有の現象である可能性が高そうです。Windowsのファイル処理が遅いのか、それともWin32APIからEmacs Lispまでの間に何かあるのか、調べてみなければ確かなことは分かりません。まぁ、おそらくその両方でしょう。それにしても100msはとんでもない時間だとは思いますが。Win32で同じ処理を直書きして比較してみたいものです。

詳しい原因は分かりませんが、load-path上の順番で処理時間が大幅に変わることから、毎回ディレクトリを検索している可能性が高そうです。

高速化の方法

であれば解決方法は全ファイルの位置をキャッシュしてしまうことでしょう。

もちろんload-pathとその下にあるファイルが変わらないという前提が必要です。幸い私が利用しているパッケージには少なくとも初期化中にload-pathを書き替えるものはありませんでした。新しくelispを生成する物も無し。強いて言えばpackage.elですが、package-enable-at-startupがtなのでinit.elの前にload-pathが設定されます。未インストールのパッケージを自動的にインストールするようにしているとそのタイミングでload-pathが変わることはあるでしょう。自分用にload-pathを追加しているところもあります。しかし、それ以降は変わることはありません。ある時点から初期化終了まではload-pathが変わらないので、少なくともその期間は問題なくキャッシュ出来るでしょう。

というわけで作成したのが次のコードです。

;;;; 高速ライブラリパス解決

(defvar my-locate-library-list nil
  "ファイルのベース名をシンボル化したもののリスト。
後でリセットするためのもの。")

(defvar my-locate-library-load-path nil
  "build時点でのload-path。変更を(簡易的に)検出するためのもの。")

(defconst my-locate-library-file-extensions
  (get-load-suffixes)
  "ロード対象の拡張子リスト")
(defconst my-locate-library-file-regexp
  (concat "\\`\\(.*\\)\\(" (mapconcat #'regexp-quote my-locate-library-file-extensions "\\|") "\\)\\'")
  "ロード対象のファイルにマッチし、ベース名と拡張子を取り出す正規表現。")

(defun my-locate-library-build ()
  "`my-locate-library'関数用のデータを構築する。

`load-path'が確定したら呼び出すこと。"
  ;; load-pathの変更を検出するために構築時のload-pathを保存する。
  ;; my-locate-libraryのたびに厳密な検査は時間がかかりすぎてやりたくないが
  ;; せめて先頭の比較くらいはしたい。
  ;; pushやpopしたくらいなら変更に気づけるので。
  ;; 厳密に判定したいなら、copy-sequenceしておいてequalで判定するくらいか?
  ;; もちろんそれでもファイルが増えたことには気づけない。
  (setq my-locate-library-load-path load-path)

  ;; load-path上の全てのディレクトリを走査する。
  ;; 先頭、つまり優先するものから走査する。
  (dolist (dir load-path)
    ;; 存在するディレクトリであること。
    (when (file-directory-p dir)
      (let (files) ;; filesは(ベース名 . 拡張子)のリスト。
        ;; dirの下にあるロード対象ファイルをリストアップする。
        ;; ファイルのベース名と最も優先する拡張子を求める。
        ;; 例: (foo.el foo.elc bar.el bar.txt aaa.txt) => (foo.elc bar.el)
        ;; directory-filesはソートされたリストを返すので、ベース名が一致
        ;; するファイルは隣接することを利用する。
        (dolist (file (directory-files dir))
          ;; 有効な拡張子を持つファイルであること。
          (when (string-match my-locate-library-file-regexp file)
            ;; ベース名と拡張子を取り出す。
            (let ((curr-base (match-string 1 file))
                  (curr-ext (match-string 2 file)))
              ;; 一つ前のベース名と比較する
              (if (equal (car (car files)) curr-base)
                  ;; 一つ前と同じベース名の場合 (e.g. foo.el and foo.elc)
                  ;; 拡張子の優先順位を比較する
                  ;;@todo .elcのタイムスタンプを考慮すべき?
                  (if (< (my-locate-library-ext-priority curr-ext)
                         (my-locate-library-ext-priority (cdr (car files))))
                      ;; 現在のを取る
                      (setcdr (car files) curr-ext)
                    ;; 一つ前のを取る
                    nil)
                ;; 違うベース名の場合
                (push (cons curr-base curr-ext) files)))))
        ;; シンボルを作りそのプロパティにパスを設定する。
        (dolist (base-ext files)
          (my-locate-library-set-path dir (car base-ext) (cdr base-ext)))))))

(defun my-locate-library-ext-priority (extension)
  "EXTENSIONの優先順位を示す整数値を返す。
例えば.elcの方が.elよりも小さな値を返す。"
  (seq-position my-locate-library-file-extensions extension))

(defun my-locate-library-set-path (dir base ext)
  "BASEをシンボル化し、それにファイルへのパス(DIR/BASE EXT)をプロパティとして設定する。"
  (let ((sym (intern base)))
    (unless (get sym 'my-locate-library-path) ;;上書きするとload-pathで後にある方が優先されてしまうので注意。
      (let ((path (file-name-concat dir (concat base ext))))
        ;;(message "library %s path=%s" sym path)
        (put sym 'my-locate-library-path path)
        (push sym my-locate-library-list)))))

(defun my-locate-library-clean ()
  (dolist (sym my-locate-library-list)
    (put sym 'my-locate-library-path nil))
  (setq my-locate-library-list nil))

(defun my-locate-library-rebuild ()
  (my-locate-library-clean)
  (my-locate-library-build))

(defun my-locate-library (file)
  "FILEで指定したファイルがあればそのパスを返す。

`locate-library'は非常に時間がかかるがこれは短時間でチェックできる。

FILEはシンボルでも良く、文字列を指定するよりも速い。"
  (unless (eq load-path my-locate-library-load-path)
    (warn "load-path change detected on (my-locate-library %s)" file)
    (my-locate-library-rebuild))
  ;; 私の手元ではlocate-libraryに拡張子が付いたファイル名や相対パスを指定するコードは無かったので以下は省略。
  ;;@todo 拡張子を考慮 例:(locate-library "tramp.el.gz")
  ;;@todo ディレクトリ名(相対パス指定)を考慮 例:(locate-library "net/tramp")
  ;; (when (stringp file)
  ;;   (when (file-name-directory file)
  ;;     (warn "directory specified on (my-locate-library %s)" file))
  ;;   (when (file-name-extension file)
  ;;     (warn "extension specified on (my-locate-library %s)" file)))
  (get (if (stringp file) (intern file) file) 'my-locate-library-path))

;; 以下、adviceでlocate-library、require、loadを書き替える。

(defun my-locate-library-advice (orig-fun
                                 library &optional
                                 nosuffix path interactive-call)
  (if (or (not (stringp library))
          (file-name-extension library) ;;2022-12-26:追加 動かないケースを除外する
          (file-name-directory library) ;;2022-12-26:追加 動かないケースを除外する
          nosuffix path interactive-call)
      ;; 想定していない使い方の場合はオリジナルを呼び出す。
      (funcall orig-fun library nosuffix path interactive-call)
    (my-locate-library (intern library))))

(defun my-locate-library-require-advice (orig-fun
                                         feature &optional filename noerror)
  (unless filename
    ;; ファイル名(パス)を補う。
    (setq filename (my-locate-library feature)))
  (funcall orig-fun feature filename noerror))

(defconst my-locate-library-load-suffixes-with-nil
  (cons nil (get-load-suffixes)))

(defun my-locate-library-load-advice (orig-fun
                                      file &optional
                                      noerror nomessage nosuffix must-suffix)
  (funcall orig-fun
           (or (and (stringp file)
                    (not nosuffix)
                    (not must-suffix)
                    (not (file-name-directory file))
                    (member (file-name-extension file)
                            my-locate-library-load-suffixes-with-nil)
                    (my-locate-library (file-name-base file)))
               file)
           noerror nomessage nosuffix must-suffix))

(defun my-locate-library-enable ()
  "`locate-library'や`require'、`load'のパス解決を高速化する。

あらかじめ`load-path'が確定した段階で`my-locate-library-build'を
実行しておくこと。

`my-locate-library-disable'で元に戻せる。"
  (advice-add #'locate-library :around #'my-locate-library-advice)
  (advice-add #'require :around #'my-locate-library-require-advice)
  (advice-add #'load :around #'my-locate-library-load-advice))

(defun my-locate-library-disable ()
  (advice-remove #'locate-library #'my-locate-library-advice)
  (advice-remove #'require #'my-locate-library-require-advice)
  (advice-remove #'load #'my-locate-library-load-advice))

locate-libraryには拡張子が付いたファイル名や相対パスも指定出来るようですが上のコードはそれらには対応していません。そのようなコードがある場合は自分で修正して下さい。

使い方は、load-pathが確定した段階で次のようにします。

(my-locate-library-build)
(my-locate-library-enable)

これでlocate-library、require、loadのよく使われる呼び出し形式が速くなります。ディレクトリを指定したり、ファイルの拡張子を指定したりする一部の呼び出し形式は速くなりません。速くならないどころか正しく動作しない場合もあるので注意して下さい(手抜きです)。

また、使用が終わったら次のようにします。

(my-locate-library-disable)

いつload-pathが変わるか分かりませんし、locate-libraryやrequire、loadに対して私が考慮していない引数を渡すコードがいつ実行されるかも分かりません。初期化が終わったら念のためdisableしておく方が良いでしょう。

効果の確認

(car (benchmark-run 1 (my-locate-library "yow")))
5e-06

桁が違うどころではありませんね(笑)

通常のlocate-libraryと違いシンボルも受け付けます。こちらの方がinternしなくて良いので若干早くなります。

(car (benchmark-run 1 (my-locate-library 'yow)))
3e-06

my-locate-library-enableしておけば通常のlocate-libraryも速くなります。

(my-locate-library-build)
(my-locate-library-enable)
(prog1 (car (benchmark-run 1 (locate-library "yow")))
  (my-locate-library-disable))
4e-06

requireも速くなります。例えば私のload-pathに218個のディレクトリが指定されている環境で、初期化の最初で動作を止めて (require 'tramp)を実行してみましょう。

(car (benchmark-run 1 (require 'tramp)))
1.269366

1.269秒(笑)

上のコードを評価しつつ有効化した後だと

(car (benchmark-run 1 (require 'tramp)))
0.197264

197msと大幅に短くなりました。(それでもかなり長いですが)

ちなみにemacs -Q環境だとload-pathの長さは24で (require 'tramp)は366msほどでした。load-pathに登録されているディレクトリが少なく検索する時間があまりかからないケースでは効果も薄くなります。

キャッシュの構築にかかる時間ですが

(car (benchmark-run 1 (my-locate-library-build)))
0.070408

実行毎にかなりバラツキがあるのですが、70msくらいのことが多いようです。平均的には90msくらい。元のlocate-libraryの1回分と大差ありません。Emacsで全ディレクトリを走査するとそのくらいかかるということなのでしょうね。

初期化プロセス全体だと大量のrequireが発生するのでこれだけで何秒も変わるほどのインパクトがあります。

Emacsの起動時間の短縮はelispの読み込みを遅延するのが王道ですが、Windowsでどうしてもある程度以上短くならないとお悩みの方は試してみてはいかがでしょうか。細かい注意点がいくつかあるので、よく読んだ上でご利用下さい。

まぁ、あとディレクトリも減らした方がいいですね。使っていないの多すぎ。整理しないと。

2022-12-21 ,

Emacsの中で動く作図ツール 最近の変更点

最近またEmacsの中で動く作図ツールをいじっています。

misohena/el-easydraw: Embedded drawing tool for Emacs

作成した図形をカスタムシェイプに登録して使用する様子(gifのため色数少ない)
図1: 作成した図形をカスタムシェイプに登録して使用する様子(gifのため色数少ない)

(↑のgifアニメですが、C-u クリックで既存のアンカーポイントに接続しないでアンカーポイントを追加しています。つまり、一筆書きで描いています。2ストロークに分けた方が自然かもしれません)

最近の変更点:

グループ化機能の改善
最低限実用になる(グループ化を解除できる、つまり使うのをやめられるw)程度まで実装しました。いくつか問題は残っています。特に変形。
opacity属性対応
グループ全体の不透明度を変えたかったので。fill-opacityやstroke-opacityとは別に全体の不透明度を指定出来ます。
カスタムシェイプツール追加

あらかじめ定義済みの図形を追加する仕組みです。追加するだけなら簡単なのですが、シェイプピッカーと呼んでいる図形一覧を表示するバッファの作成にとにかく時間がかかりました。非常にカスタマイズ性がある仕組みになっています。org-modeをシェイプピッカーにしてしまおうというアイデアもあったのですが、それはそのうち。

カスタムシェイプツールを使用しているところ
図2: カスタムシェイプツールを使用しているところ
数値入力での拡大縮小・回転機能追加
お天気マークの太陽を描くのに回転機能が必要だったので。問題多し。
全選択・選択解除機能追加
Aでトグルします。
コピー、カット、ペーストのキーを変更
これまでコピーはC-c C-x M-wとかいう複雑怪奇なキー割り当てだったのですが、久しぶりに使ったら全く覚えていなかったので単純にM-w([remap kill-ring-save])にしました。 これに限らず、キー操作をEmacsに似せて良いのかは悩み所です。作図エディタ内の操作はバッファに対する操作とは独立しているので分けた方が良いかなと思っているのですが、作図エディタ操作中は作図エディタの中に集中しているのでバッファに対する操作はしないと考えると極力Emacsの操作体系に似せた方が使いやすいのかなとも思いますがどうなんでしょうね。UndoとRedoはzとZなのですが、慣れていないとついC-/を押してしまうことがあるので迷うところです(よく使う操作なのですぐに慣れてzを押すようになります)。
高解像度環境下でカラーピッカーの座標がずれる問題の修正
Emacsの(というかcreate-image関数の)自動スケーリングを画質の観点からSVG内部で再現しているのにもかかわらず、カラーピッカーだけ画像の自動スケーリングを無効化し忘れていました。つまり自動スケーリングによる拡大が二回分かかっていたことになります。おそらくかなり初期の頃から問題はあったと思います。結局誰も使っていないと言うことでしょう。
カラーピッカーに色無し(none)ボタンを追加
キーボードでnoneと打たなければならなかったので地味に不便でした。
スクロール・ズーム機能

カスタムシェイプを作成するときに欲しかったので。カスタムシェイプは細かい図形が多くなりますし、原点(0,0)に図形の中心を置くとクリックした位置と配置される位置の関係が分かりやすかったりするのでズームとスクロールが必要でした。ズームがC-ホイール、スクロールが中ドラッグでできる他、SPCでインタラクティブなスクロール・ズームモードに入ります。C-ホイールは単にホイールだけにしようか迷いました。中ボタンは使えない人もいるかもしれないので、そういう場合はSPCを使って下さい。小さなサイズのSVGではズームしたときに編集領域(ビュー)自体も大きくなるようにしました。

ズームして小さなアイコンを編集している様子
図3: ズームして小さなアイコンを編集している様子
viewBox属性指定機能追加
SVG要素のviewBox属性を最低限文字列で指定出来るだけです。現状では編集には一切影響が無く、編集が終わった後の表示にのみ効果があります。
画像ツール追加(image要素対応)

jpgやpngといった画像をSVG内に埋め込めるようになりました。data URIは直接的には対応していませんが自分で変換してプロパティエディタからhref属性に指定すれば使えるとは思います。ただ、あまり容量が大きい物をdataで埋め込むのもどうかなと。Windows等で画像が表示されない場合はgdk-pixbufがらみのファイルを確認しましょう。librsvgはgdk-pixbufを使用して画像を描画するので。

画像ツールで画像を配置した例
図4: 画像ツールで画像を配置した例
内部での数値の持ち方やSVG出力時の数値の形式を改善
.0を出さないようにしたり、内部的な構造を少し見直したり。
プロパティエディタの改善
作図エディタ終了時に自動的に閉じるようにしたり、入力中の数字が微妙に変わってしまう問題(100.00が100になったり100.01から100.009999になったり)を修正しました。
edrawリンクの右クリックメニューを改善

これまでインライン画像に対する右クリックメニュー(コンテキストメニュー)にはEditだけしかありませんでしたが、便利な機能をいくつか追加しました。図形の中身を作図エディタを開かずコピーして他の作図エディタへペーストできたり、SVGのコードを表示したり、data=形式とfile=形式の相互変換が出来たりします。

インライン画像化されたedrawリンクを右クリックしたときの様子
図5: インライン画像化されたedrawリンクを右クリックしたときの様子
fileリンク対応
[[file:somefile.edraw.svg]] のようなリンクをその場で編集するコマンドを追加しました。 [[edraw:file=somefile.edraw.svg]] の方が使い勝手が良いとは思うのですが、エクスポータがらみで通常のリンクにしたい場合は有用です。
rectとellipseをpathへ変換する機能を追加
rectやellipseは座標軸に沿った矩形や楕円しか表現できないので、回転するならtransform属性を使用するかpathへ変換する必要があります。transform属性は拡大縮小時に線の太さも変わってしまうので、それを回避したければpathへ変換するのが手っ取り早いです。
latexエクスポータを追加
私はあまり使わないのですが一応対応。

今後の予定:

変形まわりを何とかしたい
アンカーポイント座標のみの変形と図形全体の変形(transform属性)が現状でごっちゃになっています。グループだけ最初からtransform属性で変形しています。他の要素はtransform属性がある場合はそれに追加する形で変形していて、無い場合はアンカーポイント座標のみで変形しています。一貫性がありません。どちらの方式にも利点があるのでどう切り替えるか。また、GUIで変形したいです。
カラーピッカーやプロパティエディタ、シェイプピッカーは別フレームで表示したい
親フレームからはみ出せる子フレームって作れるのかな。

大きな物はこのくらいでしょうか。Emacsに最低限の作図ツールをもたらすという観点から言えば残っている物はそれほど多くはありません。

必要は最大のモチベーション、ということで自分が必要だと思う物を気ままに作っていくだけです。

2022-11-25 ,

phscrollの修正

org-modernと組み合わせたときにいくつか問題が目に付いたので修正しました。ついでに修正した点もいくつか。

misohena/phscroll: Enable partial horizontal scroll in Emacs

主な修正点:

  • phscroll-use-fringeをdefvarからdefcustomへ変更
  • 左右スクロールコマンドでポイント位置を動かすオプションを追加
  • 左右スクロールコマンドでスクロールする方向を反転するオプションを追加
  • Shift+マウスホイールでのスクロールに対応
  • orgテーブルの直後を余分にスクロール領域にしてしまうミスを修正
  • フィールドテキストがあるときに正しく動作しない問題を修正
  • org-phscroll使用時はmodification-hooksでは更新せずfontify時に更新するように変更
  • font-lockへの登録方法を修正
  • ピクセル単位で幅計算するオプションを追加(実験的)

左右スクロールコマンドが使いづらいという指摘があって私も同感で使っていないのですが、ポイントも一緒に動くようにしたりして少しはマシになりました。元々Emacs標準のscroll-left(C-x <)、scroll-right(C-x >)を真似た物でしたが、それ自体使いづらいですからね。

ついでにマウスのホイールに対応してみました。プラットフォームによってホイールのイベント名は変わるそうですね? 知りませんでした。mouse-wheel-up-eventやmouse-wheel-down-eventという変数にシンボルが格納されているのでそれを使うのだとか(Misc Events (GNU Emacs Lisp Reference Manual), mwheel.el)。

font-lockのキーワードまわりをあまりよく理解していなかったので必要な部分だけ少し勉強しました。font-lock処理(fontify? highlight?)(font-lock-fontify-keywords-regionを参照)はキーワードリストを上から順に処理していきますが、一つのキーワードで対象範囲の最初から最後までを処理してから次のキーワードをまた最初から処理する流れになっています。何となく複数のキーワードをまぜこぜに処理していくような気がしていたのですがそんなわけはありませんでした。一つの関数でマッチからハイライトまでをやってしまう場合、いくつか注意すべき点があります。基本的にmatcherの関数はre-search-forwardの代わりに呼ばれているので、tを返す場合はmatch-dataも有効でなければなりません。nilを返すのであればその限りにあらず。どちらにせよ一度に一箇所しか処理してはいけないという制約はありません。範囲内全てを一度に処理することは可能です。ただしmultilineや無限ループ回避のコードには注意が必要。

orgやorg-modernのfont-lock処理が終わってからでないと正しいテキスト幅が計算できないという問題に気がつきました。phscrollではテキストの幅を正しく計算することが求められます。これまではオーバーレイのmodification-hooksでテキストの変更を検出して更新処理を行っていましたが、それでは不十分でした。orgがリンクのパス部分を非表示にする(invisibleテキストプロパティにシンボルorg-linkを設定する)とテキストの変更無しに幅が縮まります。org-modernがテーブルの縦線を細くしてもテキストの(ピクセル)幅は縮まります。phscrollはその直後に水平スクロールに必要な幅の計算をしなければなりませんでした。

これまで幅の計算は文字数単位で行っていましたが、org-modernがテーブルの縦線を細くしてしまうと文字数は変わらないのに全体のピクセル数は小さくなってしまいます。すると縦線(テーブルの列)が沢山あるほど右側に無意味なスペースが空くことになっていました。これはピクセル単位で幅の計算をしなければ解決できません。

ピクセル単位での幅の計算は window-text-pixel-size 関数を使用しました。自分でテキストプロパティやオーバーレイを解析して計算しても良いのですが、なかなか完璧には出来ないので。

window-text-pixel-size 関数を使うにしても色々とやっかいな点があります。一番やっかいだったのは、折りたたまれて非表示になっているテキストに対してfont-lock処理が働く場合があることです。非表示になっているので window-text-pixel-size で計算しても幅は0になってしまいます。この問題に対しては、折りたたみ部分を隠すためのオーバーレイ(invisible=(outline . t)が設定されている)を一時的に表示状態(invisible=nil)にすることで解決しました。そんなことをして大丈夫なのか自信が無かったのですが、とりあえず動いています。最初は buffer-invisibility-specからoutlineを抜けば良いと思ったのですが、それだと他の非表示部分(リンクのパス部分など)が全て表示された状態で幅の計算をしてしまいます。テキストプロパティがどうであろうと、上に乗っかっているオーバーレイの非nilなinvisibleプロパティが優先されるようです。オーバーレイのinvisibleプロパティがoutlineである以上、その範囲内は全てinvisible=outlineであり、buffer-invisibility-specからoutlineを消した以上全て表示されてしまうのです。何はともあれ、この方法で解決して良かったです。ダメならそれこそ自分で幅の計算(というかもはや推測)をしなければいけないところでした。また、指定のピクセル幅を超えるテキスト位置を求める必要がありましたがそのような機能はどこにも無いため二分探索で何とかしました。

一応ピクセル単位での幅計算はオプションでデフォルト無効にしてあります。ちょっと重いような気もするので。

というわけでorg-modern下でもそれなりの見た目が実現出来ました。

2022-11-25-fix-phscroll-20221125.gif

私はこのプロジェクトがあまり良いものだとは思っていません。一応実用にはなるのですが、やり方はかなり強引ですし、同じ場所を幅の違う複数のウィンドウから見たら破綻するという根本的な問題も抱えています。理想的には、Emacsに折り返しを制御するような特殊なテキストプロパティを追加するのが良さそうに思えます。line-prefixやwrap-prefixと似たようなものです。いつかEmacsにそのような機能が追加されるのを夢見つつ、それまでのつなぎとして作っています。