Vimのdoautoall
コマンドはいろいろな問題のあるコマンドで、私にとって悩みの多い機能でした。
しかし 8.2.2596 の変更によって問題のすべてが解決しました。
本記事ではこのコマンドの問題点とこのパッチの意味について解説します。
なおコミットメッセージは:doautocmd
となっていますが、:doautoall
が正解です。
この記事は、バッファとウィンドウ、タブ(ページ)、イベントに関して理解していることを前提としています。 これらについてのヘルプは以下を参照してください。 vim-jp.org vim-jp.org
doautoall
コマンドは、すべてのバッファに対して指定したイベントを発火するコマンドです。
例えばdoautoall FileType
はすべてのバッファにFileType
コマンドを発火します。
このコマンドの困る挙動の例を挙げましょう。
例えばWinEnter
イベントで、ウィンドウ一覧を取得してそのバッファ名を (デバッグの場所として) statusline
に表示してみましょう。
autocmd WinEnter * let &statusline = join( \ map( \ range(1, winnr('$')), \ 'v:val . ": " . bufname(winbufnr(v:val))' \ ), ' | ')
タブで他のファイルを開いたときの挙動を見てみましょう。
edit foo.txt | vnew bar.txt | tabe baz.txt | tabp doautoall WinEnter
8.2.2596よりも古いVimでは、以下のようになるはずです。
1: baz.txt | 2: bar.txt | 3: foo.txt
現在のタブには2つしかウィンドウがありませんから、baz.txt
が表示されるのは奇妙です。
もう一つ困る挙動の例を挙げましょう。
先程のコマンドを改善して、アクティブウィンドウがわかるようにしてみます。
現在のウィンドウ番号はwinnr()
で取得できます。
autocmd WinEnter * let &statusline = '%t / ' . join( \ map( \ range(1, winnr('$')), \ 'v:val . ": " . (v:val == winnr() ? "[" : "") .' . \ 'bufname(winbufnr(v:val)) . (v:val == winnr() ? "]" : "")' \ ), ' | ')
今度は現在のタブで3つのウィンドウを開いてみます。
edit foo.txt | vnew bar.txt | vnew baz.txt | wincmd p doautoall WinEnter
古いVimでは以下のようになるでしょう。
bar.txt / 1: [baz.txt] | 2: bar.txt | 3: foo.txt
今のバッファはbar.txt
ですから、baz.txt
がアクティブに見えているのは奇妙です。
以上の挙動は、statusline
やWinEnter
に限った話ではありません。
doautoall
コマンドは、発火できるイベントに制限がありませんから (例えばdoautoall WinEnter
の意味を考えているとおかしなコマンドに思われますが)、実際にユーザーはどんなイベントでも発火することができます。
なので何らかのイベントのトリガーでwinnr()
で現在のウィンドウを記録している、bufname()
で現在のバッファ名を取得している、winrestcmd()
やwinsaveview()
を記録している場合など広いユースケースで問題になります。
意味のわからないコマンドだとしても、Vimが実行を許している以上はVimプラグインで考慮する必要があります。
echomsg
ではなくstatusline
を使ったのは、最終的なウィンドウの状態がユーザーから見たときの挙動を左右するからです。
doautoall
の問題点を大きく分けるとautocommand window
(用語は:h win_gettype()
より) とバッファの発火順があります。
このコマンドがイベントを発火するときに、現在のバッファやウィンドウを変更し、イベントハンドラの中でどのバッファかわかるようになっています。
あるバッファが現在のタブのどのウィンドウにも表示されていない場合は、autocommand window
という一時的なウィンドウ (コード上はaucmd_win
) にバッファを紐付けて、それをアクティブウィンドウとした上でイベントを発火しています。
多くのイベントハンドラはこの特殊なウィンドウで実行されることを意図していないでしょう。
Vim 8.2.0996 (正確には8.2.0991からですが返り値に統一感がないということで変更された) より win_gettype()
という関数でautocommand window
かどうかを判別できるようになっています。
この特殊なウィンドウが、1つ目の困る挙動で裏のタブで開いているバッファ名が表示された理由です。
2つ目の問題は発火順です。
doautoall
は素朴にバッファ番号順にイベントを発火していました。
イベントハンドラは、イベントがdoautoall
で発火されたことを検知できません。
autocommand window
は特殊なウィンドウなので区別する手段が作られましたが、最後のバッファが現在のタブの非アクティブウィンドウに表示されている場合は、やはりアクティブウィンドウを勘違いしてしまいます。
この問題を解決するのが、8.2.2596で取り込まれた以下の修正です。
解決方法は簡単で、順番を入れ替えて、現在のウィンドウに表示されているバッファのイベントを最後に実行するというものです。
これによってdoautoall
が引き起こすあらゆる問題が解決しました。
イベント発火の順序の入れ替えという少し不安になる変更でしたが、Bramはあっさりと受け入れてくれました。
パッチがmergeされてから一週間が経ち、特に問題になっていないようですので大丈夫でしょう。
github.com
私は lightline.vim というプラグインを作ってメンテナをしており、このdoautoall
コマンドによってアクティブウィンドウのstatusline
が正しく表示されないという問題には随分悩まされてきました (#435, #444, #447, #448, #556)。
特にセッションファイルにdoautoall SessionLoadPost
というコマンドが含まれていることから、セッションの文脈でバグ報告が上がることが多い現象です。
しかし問題の本質はdoautoall
コマンドの挙動だったので、トリッキーなworkaroundを入れる (例: カーソル移動で更新とかタイマーで再チェックとか) ことなく根本から修正できてよかったです。
autocommand window
はwin_gettype()
によって検知できるようになりました (8.2.0996, 8.2.0991)。
現在のウィンドウに表示されているバッファのイベント発火は最後に行われるようになりました (8.2.2596)。
この2つの修正によってすべての悩みが解決しました。
以上がこのdoautoall
コマンドの問題点とそれに対する修正に関するすべてになります。
この修正によってlightline.vimのみならず、他の多くのプラグインが助かっているかと思います。
実際に手でdoautoall
を打つことはめったにないですが、Vimプラグインのメンテナをしているとそういう機能とも真面目に向き合う必要があります。
ソフトウェアの問題への修正の多くはincrementalであり、dirty workaroundを行うのが容易いことが多いでしょう。
しかし、起きている現象の本質を考えることで、ソフトウェアを本当に良いものにしていけるのだと思います。
本質を捉えるのは難しいこともありますが、これができるプログラマになりたいものです。