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にそのような機能が追加されるのを夢見つつ、それまでのつなぎとして作っています。