VimConf2016の延長線で出来たネタ - derisの日記をよみました。また、 Vim で operator 実行時にカーソルを移動させないような operator をつくった - Secret Garden(Instrumental)というのも読んだことがあります。とても、便利ですね。
オペレータによっては「カーソルを保持」の意味について考える必要があるので、デフォルトの挙動に納得してはいます。 (例えば行の中ほどで d0
したときにカーソルの絶対位置が保持されても変ですよね?) ただ、いくつかのオペレータでカーソル位置を保持したいというのは、私も思ったことがあります。その時ぶつかった問題がありまして、ドットコマンドの時の場合です。
最初から見ていくために簡単なオペレータを作ってみましょう。特に難しいことはしないので、いろいろ省略していたり、拡張性は考えられていません。なお、 vim-operator-userを使うともっと簡単です。
function! MyOperatorNo1(motionwise) abort
let cursor = getcurpos()
echohl MyOperatorNo1Green
echom'このおれさまのオペレータがせかいでいちばん!つよいってことなんだよ!'
echohl NONEcall setpos('.', cursor)endfunctionfunction! MyOperatorNo1Pre() abort
let&operatorfunc='MyOperatorNo1'return''endfunctionhighlight default MyOperatorNo1Green ctermfg=2 guifg=#008800noremap<expr><SID>(MyOperatorNo1Pre) MyOperatorNo1Pre()nnoremap<silent><script> M <SID>(MyOperatorNo1Pre)g@
xnoremap <silent><script> M <SID>(MyOperatorNo1Pre)g@
onoremap <silent> M g@
できました。実行するとメッセージを表示するだけのオペレータです。色付きです、すごいですね。オペレータ関数の先頭でカーソル位置を保存して最後にカーソルを戻しています。
おや?カーソル位置が長い長いチャンピオン○ードの先頭に戻ってしまいました。これは実はオペレータ関数 MyOperatorNo1()
が呼ばれた時にはすでに Vim がテキストオブジェクト対象範囲の先頭へカーソルを動かした後であるためです。
ではカーソル位置は事前に取得しておきましょう。
function! MyOperatorNo1(motionwise) abort
echohl MyOperatorNo1Green
echom'このおれさまのオペレータがせかいでいちばん!つよいってことなんだよ!'
echohl NONEcall setpos('.',s:cursor)endfunctionfunction! MyOperatorNo1Pre() abort
lets:cursor = getcurpos()let&operatorfunc='MyOperatorNo1'return''endfunctionhighlight default MyOperatorNo1Green ctermfg=2 guifg=#008800noremap<expr><SID>(MyOperatorNo1Pre) MyOperatorNo1Pre()nnoremap<silent><script> M <SID>(MyOperatorNo1Pre)g@
xnoremap <silent><script> M <SID>(MyOperatorNo1Pre)g@
onoremap <silent> M g@
カーソル位置を保持に成功しました。しかし、ドットリピートをしてみると?
カーソルが最初にオペレータを実行した位置に戻ってしまいますね。 MyOperatorNo1Pre()
はドットリピートの時実行されないためです。困りましたね。
さて、今となっては明らかですが、要するにドットコマンドを押した瞬間のカーソル位置を取得する方法がオペレータ関数にないのです。そこで、ドットコマンドの前に実行されるオートコマンドを追加するプラグインを書きました。
これを使用していると、ユーザー定義オートコマンドイベント DotCommandPre
が使えます。また、いくつかのドットコマンドを実行する直前の情報を g:DotCommandPre
に保存します。カーソル位置や画面の状態もこれに含まれるので、これを使ってみましょう。
function! MyOperatorNo1(motionwise) abort
echohl MyOperatorNo1Green
echom'このおれさまのオペレータがせかいでいちばん!つよいってことなんだよ!'
echohl NONEifs:dotcommand
" dot repeatif exists('g:DotCommandPre')call winrestview(g:DotCommandPre.view)endifelse " first actioncall winrestview(s:view)endiflets:dotcommand =1endfunctionfunction! MyOperatorNo1Pre() abort
lets:dotcommand =0lets:view= winsaveview()let&operatorfunc='MyOperatorNo1'return''endfunctionhighlight default MyOperatorNo1Green ctermfg=2 guifg=#008800noremap<expr><SID>(MyOperatorNo1Pre) MyOperatorNo1Pre()nnoremap<silent><script> M <SID>(MyOperatorNo1Pre)g@
xnoremap <silent><script> M <SID>(MyOperatorNo1Pre)g@
onoremap <silent> M g@
lets:dotcommand =1
ドットコマンドでもカーソル位置を保持できるようになりました。s:dotcommand
は最初の実行の時だけ 0 で、ドットリピートの時は常に 1 です。カーソル位置の復元に winsaveview()
、 winrestview()
を使うようにしています。とても便利な関数です。
気が付けばオートコマンドはいらなくなっていますが、まあ、いいでしょう。今回のこれはこういうものがあればいいなという提案で、私の作ったこれでなくてもいいので Vim のプロがもっといいものを作って普及させてくれないかなー、と思っています。
ところで、 vim-event-DotCommandPre を書いているときに気が付いたんですが、ユーザー定義イベントに対するオートコマンド定義が存在するかどうかを
if exists('#User#DotCommandPre') " fooendif
で取得できるみたいですね。これって意図された挙動なんでしょうか?
echo exists('#User#DotCommandPre')" 0autocmdUser DotCommandPre echo 'before dot!'
echo exists('#User#DotCommandPre')" 1