Vim のドットリピートとテスト
2018/8/29 追記
以下に書いている問題は v8.0.0548
以降解消されています。
たまに Vim の拡張としてオペレータを書くのですが、テストを書こうとすると困ることがあります。例として、オペレータを書いてみました。指定された領域の先端に a
を、末端に b
を入力するオペレータです。
function! OperatorInputAB(motionwise) abort let head = getpos("'[") let tail = getpos("']") call setpos('.', tail) normal! ab call setpos('.', head) normal! ia endfunction nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@
今回は、ビジュアルモードは考えません。またカウント、レジスター、 operatorfunc
に渡される引数も使いません。詳しくは、 :help opfunc
及び :help map-operator
を参照願います。
さて、簡単で、なんら役に立たないものではありますが、ひとまずオペレータができました。しかし、このオペレータのドットコマンド時の挙動をテストしようと思うとなかなかうまくいきません。 vim-themis を例にとりますが、次のようなテストを想定しています。
let g:assert = themis#helper('assert') let s:suite = themis#suite('test: ') function! s:suite.before() abort function! OperatorInputAB(motionwise) abort let head = getpos("'[") let tail = getpos("']") call setpos('.', tail) normal! ab call setpos('.', head) normal! ia endfunction nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@ endfunction function! s:suite.dotrepeat() abort call setline('.', 'foo') " test 1 normal 0\aiw call g:assert.equals(getline('.'), 'afoob', 'failed after an operator command') " test 2 normal . call g:assert.equals(getline('.'), 'aafoobb', 'failed after a dot-repeating') endfunction
実行した結果を見ると、
not ok 1 - test: dotrepeat # The equivalent values were expected, but it was not the case. # # expected: 'aafoobb' # got: 'aafoob' # # failed after a dot-repeating # tests 1 # passes 0 # fails 1
test 2
のところでこけていますね。なぜか、 b
が最後に入力されていません。詳しくはわからないのですが、どうも要約するとドットリピートで繰り返される単位の更新のタイミングの問題のようです。次の二つのスクリプトを :source
してみると、一見同じことをしているように見えて別の結果が得られます。
- a.vim
function! OperatorInputAB(motionwise) abort let head = getpos("'[") let tail = getpos("']") call setpos('.', tail) normal! ab call setpos('.', head) normal! ia endfunction nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@ call setline('.', 'foo') normal \aiw normal .
- b.vim
function! OperatorInputAB(motionwise) abort let head = getpos("'[") let tail = getpos("']") call setpos('.', tail) normal! ab call setpos('.', head) normal! ia endfunction nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@ function! s:operation() normal \aiw normal . endfunction call setline('.', 'foo') call s:operation()
ほぼ同じことをしているように見えますが、 a.vim
の結果は意図通りの aafoobb
、しかし b.vim
の結果は最後の b
が欠けて aafoob
です。違いは a.vim
が関数の外でドットリピートしているのに対し、 b.vim
は関数の中でドットリピートをしている点です。どうやら、ドットリピートの単位はすべての関数を抜ける時にまとめて更新されるらしく、何らかの関数の中でドットリピートした場合は OperatorInputAB()
の中の最後の編集 normal! ia
のみがドットリピートの対象になっているみたいです。
と、いうことは逆に考えると a.vim
をもう少し工夫するとドットリピート用のテストが書けて、あとはバッチなりシェルスクリプトなりを用意すれば大勝利、となるわけですけど未だ肝心のテストはかけてないです。書かないと、はー。