VimConf2016の延長線で出来たネタ - derisの日記 をよみました。また、 Vim で operator 実行時にカーソルを移動させないような operator をつくった - Secret Garden(Instrumental) というのも読んだことがあります。とても、便利ですね。
オペレータによっては「カーソルを保持」の意味について考える必要があるので、デフォルトの挙動に納得してはいます。 (例えば行の中ほどで d0
したときにカーソルの絶対位置が保持されても変ですよね?) ただ、いくつかのオペレータでカーソル位置を保持したいというのは、私も思ったことがあります。その時ぶつかった問題がありまして、ドットコマンドの時の場合です。
最初から見ていくために簡単なオペレータを作ってみましょう。特に難しいことはしないので、いろいろ省略していたり、拡張性は考えられていません。なお、 vim-operator-user を使うともっと簡単です。
function! MyOperatorNo1(motionwise) abort
let cursor = getcurpos()
echohl MyOperatorNo1Green
echom 'このおれさまのオペレータがせかいでいちばん!つよいってことなんだよ!'
echohl NONE
call setpos('.', cursor)
endfunction
function! MyOperatorNo1Pre() abort
let &operatorfunc = 'MyOperatorNo1'
return ''
endfunction
highlight default MyOperatorNo1Green ctermfg=2 guifg=#008800
noremap <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 NONE
call setpos('.', s:cursor)
endfunction
function! MyOperatorNo1Pre() abort
let s:cursor = getcurpos()
let &operatorfunc = 'MyOperatorNo1'
return ''
endfunction
highlight default MyOperatorNo1Green ctermfg=2 guifg=#008800
noremap <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 NONE
if s:dotcommand
" dot repeat
if exists('g:DotCommandPre')
call winrestview(g:DotCommandPre.view)
endif
else
" first action
call winrestview(s:view)
endif
let s:dotcommand = 1
endfunction
function! MyOperatorNo1Pre() abort
let s:dotcommand = 0
let s:view = winsaveview()
let &operatorfunc = 'MyOperatorNo1'
return ''
endfunction
highlight default MyOperatorNo1Green ctermfg=2 guifg=#008800
noremap <expr> <SID>(MyOperatorNo1Pre) MyOperatorNo1Pre()
nnoremap <silent><script> M <SID>(MyOperatorNo1Pre)g@
xnoremap <silent><script> M <SID>(MyOperatorNo1Pre)g@
onoremap <silent> M g@
let s:dotcommand = 1
ドットコマンドでもカーソル位置を保持できるようになりました。s:dotcommand
は最初の実行の時だけ 0 で、ドットリピートの時は常に 1 です。カーソル位置の復元に winsaveview()
、 winrestview()
を使うようにしています。とても便利な関数です。
気が付けばオートコマンドはいらなくなっていますが、まあ、いいでしょう。今回のこれはこういうものがあればいいなという提案で、私の作ったこれでなくてもいいので Vim のプロがもっといいものを作って普及させてくれないかなー、と思っています。
ところで、 vim-event-DotCommandPre を書いているときに気が付いたんですが、ユーザー定義イベントに対するオートコマンド定義が存在するかどうかを
if exists('#User#DotCommandPre')
" foo
endif
で取得できるみたいですね。これって意図された挙動なんでしょうか?
echo exists('#User#DotCommandPre') " 0
autocmd User DotCommandPre echo 'before dot!'
echo exists('#User#DotCommandPre') " 1