反省はしても後悔はしない

Vim とか備忘録とか。それと関数型言語勉強中

lexima.vim をアップデートしました

ここ数年いろいろあって*1あまり活動できてなかったのですが、最近やっぱりこのままじゃまずいなと思い始めて仕事に関係ないプログラミングの時間を積極的に作っています。

その中で拙作の Vim プラグインである lexima.vim (https://github.com/cohama/lexima.vim) にいくつか機能追加をしました。

lexima.vim は対応する括弧 () [] {} "" '' や括弧以外のいろいろ def ~ end など を自動入力するためのプラグインです。似たようなプラグインはたくさんある (e.g. auto-pairs, delimitMate) のですが、こいつは . キーによるリピート (ドットリピート) が常に可能という点で他と異なります。(他にも柔軟な設定とかが可能というのもあります)

今回一応バージョンとしては v2.0 ということで内部的には大きな変更を実施したので変更点とかを紹介したいと思います。

アンドゥが途切れにくくなった

lexima.vim は括弧の対応等を補完しつつ、常にドットリピート可能なように作られています。当時はカーソルを動かすだけでアンドゥ履歴が途切れたりしていたのを setline() 関数呼び出しを使った黒魔術で気合でドットリピートできるような実装になっていました。しかし、これによってアンドゥが括弧等を入力するたびに途切れてしまい、1つの入力をやり直すのに何回も u を押下しなければならずストレスフルでした。

新バージョンではこれを改善して、少なくとも改行を入力するまではアンドゥが途切れないようになります。実装としては改行するまでは対応する括弧を補完するのに ()<C-g>U<Left> などを使っているというだけです。何のことかわからない方は Vim のヘルプを参照ください。(:help i_CTRL-G_U) 改行を入力するたびにアンドゥが途切れてしまう問題は依然として残っています。これについては今のところ改善の方法がないのでどうしようもないです。

アンドゥがについては前述のとおりなのですが lexima.vim はドットリピート対応したいがために作ったプラグインなのでもちろん複数行入力だろうが1行だろうが (そのようにルールを設定する限り) ドットリピート可能である点に変更はありません。

この変更、だいぶ内部の処理に手を入れており既存のルールが壊れてしまわないか不安もありました。そこで開発版としてまずリリースし、人柱デバッグvim-jp の Slack にいる方たちに手伝ってもらいました。結果的には特に大きな問題はなさそうだったのでそのままリリースすることが出来ました。

vim-jp Slack でデバッグに協力してくれた皆様にこの場で感謝の意を表明します。どうもありがとうございました。

leavedeleteinput/input_after を同時に指定できるようになった

分かる人には分かる変更です。表題のとおりなんですがそもそも leave/delete ってなんだ?という方もいるかもしれません。そういえばブログとかで紹介したことがなかったのでここで軽く触れておきたいと思います。

leave について

(|) というときに ) を入力すると ()| のように実際に閉じ括弧を入力するのではなく単に入力済みの閉じ括弧を抜ける機能です。 lexima.vim のデフォルトルールで以下のように定義されています。

call lexima#add_rule({'char': ')', 'at': '\%#)', 'leave': 1})

上のルールの読み方はカーソル (\%#) の右側に ) があるときに ) を入力すると1文字だけ右に移動する、となります。「右に抜けるのになんでわざわざ leave なんてあるんだ、<Right> でよくない?」みたいに思われるかもしれませんがこれはドットリピートのためです。<Right> はドットリピートを途切れさせてしまいます。lexima.vim が出来た後に <C-g>U の機能が Vim に追加されたので今となっては <C-g>U<Right> とほとんど同じですが、一応 leave だと複数行入力したときでもうまくドットリピートさせるための処理の兼ね合いで leave でないとだめなケースがあります。

ただし、leave で移動できるのはこの挿入モード内で lexima.vim が自動入力したものに限ります。このあたりは好みが分かれる部分かもしれませんが少なくともデフォルトルールでは基本的には常にドットリピート可能なルールのみ設定されています。複数行入力でのドットリピートを諦める代わりに常に移動したいのであれば以下のようにユーザ側で定義することも出来ます。

call lexima#add_rule({'char': ')', 'at': '\%#)', 'input': '<C-g>U<Right>'})

あと、leave は改行やインデントも含めて抜けられるので以下のように設定すると

call lexima#add_rule({'char': '}', 'at': '\%#$\n\s*}', 'leave': '}'})

下みたいなときに改行された後の } の右までジャンプできたりします。ちなみに leave には数字だと文字数分だけ移動、文字列だと指定した文字が最初に見つかるまで移動します。(いずれも lexima.vim が自動入力したものに限ります) これはデフォルトにはないので気に入ったら設定してみてください。もちろんこれもドットリピート可能です。

void foo() {
  |
}

詳細は本体のドキュメントの方を参照してください。:help lexima-rules-leave

delete について

delete<Del> の入力に相当するものです。leave は自動入力された閉じ括弧などを抜けるためのものですが、こちらは抜ける代わりにその文字列を削除します。ただし、今となっては delete はほとんどの場合 <Del> と同じです*2<Del><Del>... と書くよりも "delete": 10 と書くほうが簡単みたいな使い方になります。

あと、後述するように input_after と同時に指定した際の削除の順番に一応関係します。

改めて、leave/deleteinput/input_after を同時に指定できるようになったことについて

ここまで leavedelete の紹介をしましたが、以前までのバージョンでは leavedeleteinput はいずれか1つしか指定できませんでした。と言ってもこれは実は単に実装上のミスというか考慮漏れで特にドキュメント等にも同時に使えないというのは書いていなかったのですが。。。今回改めてちゃんと仕様として定めて正しく動くように実装しました。

これでカーソルを移動しつつ追加で文字を入力するのようなことが可能になります。

これがどういう場合で嬉しいかというと例えば以下のルールを追加すると

call lexima#add_rule({
  \ 'char': '<CR>',
  \ 'at': '{ \%# }',
  \ 'delete': 1,
  \ 'input': '<BS><CR>',
  \ 'input_after': '<CR>'
  \ })

hoge { | } の状態で <CR> を押下すると前後のスペースを削除しつつ以下のように改行してくれます。

hoge {
  |
}

入力は input_after -> delete -> leave -> input の順で処理されます。なのでこの場合 input<Del><BS><CR> としても既に input_after によって改行が入力された後なので <Del> で削除されるのはスペースではなく改行になってしまいます。

もう1つ例を紹介します。

call lexima#add_rule({
  \ 'char': '>',
  \ 'at': '([a-zA-Z, ]*\%#)',
  \ 'leave': ')',
  \ 'input': ' => {',
  \ 'input_after': '}'
  \ })

これを設定すると typescript などの匿名関数を簡単に入力するためのスニペットのようなことができます。(foo|) のときに > を押下することで (foo) => {|} に展開されます。 こちらは 無人島に持っていく(Neo)vimプラグイン10選 (TS開発環境編) の記事で紹介されていたものをドットリピート対応にしたものです。

このように leavedeleteinput input_after を同時に指定することで以前はドットリピート対応にするのが難しかったルールも書けるようになりました。

\1 などの部分マッチを入力に使えるようになった

HTML の閉じタグを自動で入力させる場合、つまり <div|>> を押下すると <div></div> のようのしたいときはどうすると良いでしょう。 以前のバージョンではこれはかなり困難でした。閉じタグにもタグ名を書く必要がありますが、前方で入力したタグ名を input_after 等で利用する方法がなかったからです。

これを解決するために with_submatch というキーを新たに設けました。以下のようなルールを設定すると先程の閉じタグの補完ができるようになります。 もちろんドットリピート可能です。

call lexima#add_rule({
  \ 'char': '>',
  \ 'at': '<\(\w\+\)\%#>',
  \ 'leave': '>',
  \ 'input_after': '</\1>',
  \ 'with_submatch': 1
  \ })

ポイントは at\(\) を使って部分マッチを指定し、それを input_after では \1 で指定できることです。

他にも例えば tex\begin{foo}\end{foo} の対応を自動で入力させる場合などにも使えます。

この機能の注意点として、まず inputinput_after に指定した文字列は置換用の文字列として扱われるので通常の場合とは差異があります。具体的には \\\ にする必要があるなどです。詳しくは :help sub-replace-special を参照してください。 あとは、追加したばかりの機能なのでまだバグがあるかもしれません。with_submatch を指定しない限りは使われない機能なので既存のユーザには影響ありませんが、もし利用したい場合はデバッグのつもりで使ってもらえるといいかもしれません。

この機能、頑張れば HTML のタグを閉じる系のプラグイン (e..g https://github.com/alvan/vim-closetag) を代替、しかもドットリピート可能にできます。とはいえ単純なものなら上にあるルールでできるんですが、<br> は閉じタグを入れない、とか <div class="hoge"> みたいな例外を考え出すと結構面倒くさいので標準で入るのはまだ先になりそうです。(誰かが PR くれれば取り込みたい...)

なお、この機能は Pull Request で頂いた機能を少し修正して取り込んだものになります。感謝。

おわりに

以上、lexima.vim の最新の機能追加の紹介でした。

今後の展望としては、内部構造のリファクタリングを引き続き進めていこうと思います。今回紹介しておいてなんですが正直 leave とかいう概念難しいんじゃないかと思っていて、このあたりを直感的に設定できるようにしたいみたいな思いがあります。(できるかどうかはともかく)

あと、at とか filetype とか syntax 等の発動条件まわりももう少し柔軟にしたいです。この手のプラグインだと対応する括弧の釣り合いが取れているかどうかで自動入力するしないを変えるものもあり、そのような設定が可能なようにする機構を考えています。

まぁでもそういう大工事はまとまった時間が取れたときにやって、とりあえず直近は溜まった Issue をひとつずつ片付けていこうかと思っています。 直近は nvim-treesitter で不具合が出る問題とかですかね。

バグ報告、機能追加要望等ありましたら遠慮なく GitHubIssue までよろしくお願いします。

*1:以前は仕事のストレスを趣味開発のエネルギーにしてた説。転職してから仕事上でのストレスがほぼなくなりました。

*2:確か昔は <Del> でドットリピート途切れていた気がするんですが気づいたら大丈夫になっていました