<expr> で Vim Script のドットリピート対応

自作のvim scriptを.でリピート(dot repeat, single-repeat)出来るようにしたい場合、キーマップを当てるときに<expr>を使うと上手くいくことがある模様。

単純な動作を実行するスクリプトであれば、<expr>のおかげでリピート用にrepeat.vimのようなプラグインを入れなくても済むかもしれません。

特に「デフォルト機能をちょっとだけ変えて実行している」ぐらいのときにぴったり当てはまることが多いのではないでしょうか。

3日位悩んだ末にようやく解決できて嬉しいので記録に残しておこうと思います。

<expr>を使ってドットリピートを実現する例

df.dt.で 日本語の「。」も認識してくれるスクリプトを作りました。

文字の判定のために途中まではあれこれやるものの、最終的にはvimのデフォルトコマンドを実行するシンプルなスクリプトです。

このようなコマンドをexecute normalで実行するとドットリピートできませんが、<expr>を使えば可能になります。

例1) ✕ 1回なら上手くいくがドットリピートは不可能

nnoremap dt. :<C-u>call <SID>Delete_dot_or_maru(pattern))

let pattern = ['.', '。']
function! s:Delete_dot_or_maru(pattern)
   " ...引数 a:pattern この場合は "." を使ってあれこれ処理
   " 現在のカーソル位置から「.」と「。」のどちらか近い方を返す
    if 「.」の場合
        execute "normal! dt."
    else if 「。」の場合
        execute "normal! dt。"
    endif
endfunction

dt.と打てば、カーソルの先に「.」か「。」があれば期待通りに手前まで消去(カット)してくれます。

ct.なら「.」か「。」の手前まで削除して挿入モードに入ります。

しかし、.でリピートできません。非常に残念です。

なぜなら、こんなスクリプトを使わず通常通りにdt.dt。と打ったは場合はリピート可能だからです。便利になったはずなのにこれでは半歩後退、いやむしろマイナスかもしれません。

例2) ◯ <expr>のおかげでドットリピート可能

nnoremap <expr> dt. <SID>Delete_dot_or_maru(pattern)

let pattern = ['.', '。']
function! s:Delete_dot_or_maru(pattern)
   " ...引数 a:pattern この場合は "." を使ってあれこれ処理
   " 現在のカーソル位置から「.」と「。」のどちらか近い方を返す
    if 「.」の場合
        return "dt."
    else if「。」の場合
        return "dt。"
    endif
endfunction

<expr>を使えば、 dt.に当てられるkeymapは関数Delete_dot_or_manu()

ちなみにこのようなスクリプトを実際に作って使うならnnoremapではなくonoremapにし、更にdを省いてtのみをマッピングするといいと思います。そうすれば、cyでも使い回せるからです。

何が違うのか

悲しいことに詳細はよくわかりませんが現時点での考察。

例1の場合は、直前に実行されたのは コマンドラインでSelectChar()を呼んだこと、なのでリピート不可。(※ヘルプにはコマンドラインの動作はリピートしないと書いてあります。)

h: single-repeat

例2の場合は、直前に実行されたのは dt.(またはdt。)と認識されるのでリピート可。

という感じ?

exprとは。先人の知恵を享受する

参考: incsearch.vimでVimの検索体験をリッチにする

昨今私は日本語の移動関係を便利にするプラグインを作っていたので、移動系プラグイン作者のブログなどは参考になるだろうと思って検索していたら見事にヒットしました。

「4. Development」のところで解説されております。詳しく知りたい方は読んでみて下さい。

いろいろいじった上で最終的にデフォルトのマッピングを返して実行! ということができるのでデフォルトのモーションを拡張する際にとても便利

そう、まさにそれなんだ!!それを求めていたんだ!!と、見つけた時は小躍りしました。

<expr>を使ったこの方法は、あくまでもデフォルトの機能を邪魔したくないという私のニーズにぴったりです。

上記の記事を読んで改めてヘルプを読むとちょっと理解が深まった気がしないでもないような…。

式が評価され、その値が {rhs} として使われる…?

<expr>を使うと {rhs}つまりマッピングするときの右側ののほうは式で書けるということになる。

関数を呼ぶのではなく、関数の結果が例えばdiwみたいな文字列だったらそのままマッピングされる、ということらしい。らしいというかそうなった。

そのおかげで 今回作ったスクリプトのdt.は関数を呼んでいるのではなく、dt.を実行したとみなされたので.でリピート可能、ということなのだと思われます。

<expr>の副作用

これも例によって半分も理解できず想像の世界。試すにもどうやって試せばよいのかいまいちわからない…。

:normalが不可というのはやってみてわかりました。動きません。

要はkeymappingの右側で使う式の中で上の4つの動作はできませんよ、ってことなのだと思います。

とりあえずこの副作用は当初の目的であるドットリピートととぶつかることはなさそうなので今は放置。あらためて検証。

自分の身に降りかからないと覚えられない

exprとは一体何なのか。ヘルプや、導入しているプラグインのコードで見かけた記憶はあったのですが、なんのために記述されているのかさっぱりわかっていませんでした。

自分が求めているものが何なのか理解できていないと検索することすらできません。毎度感じるこのもどかしさ。

しかし、それを乗り越えるのがまた快感だったりもします。

ドットリピートは長い間の懸念事項だったので今回のexprはかなりの衝撃でした。