ドットコマンドでもカーソル位置を保持したい

[VimConf2016の延長線で出来たネタ - derisの日記](http://deris.hatenablog.jp/entry/2016/12/13/082317) をよみました。また、 [Vim で operator 実行時にカーソルを移動させないような operator をつくった - Secret Garden(Instrumental)](http://secret-garden.hatenablog.com/entry/2015/03/18/232847) というのも読んだことがあります。とても、便利ですね。

オペレータによっては「カーソルを保持」の意味について考える必要があるので、デフォルトの挙動に納得してはいます。 (例えば行の中ほどで `d0` したときにカーソルの絶対位置が保持されても変ですよね？) ただ、いくつかのオペレータでカーソル位置を保持したいというのは、私も思ったことがあります。その時ぶつかった問題がありまして、ドットコマンドの時の場合です。

最初から見ていくために簡単なオペレータを作ってみましょう。特に難しいことはしないので、いろいろ省略していたり、拡張性は考えられていません。なお、 [vim-operator-user](https://github.com/kana/vim-operator-user/) を使うともっと簡単です。

```vim
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-Demo1](http://imgur.com/xhXrBCl.gif)

おや？カーソル位置が長い長いチャンピオン○ードの先頭に戻ってしまいました。これは実はオペレータ関数 `MyOperatorNo1()` が呼ばれた時にはすでに Vim がテキストオブジェクト対象範囲の先頭へカーソルを動かした後であるためです。

![MyOperatorNo1-CursorPosition](http://imgur.com/vPtDMzW.png)

ではカーソル位置は事前に取得しておきましょう。

```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@
```

![MyOperatorNo1-Demo2](http://imgur.com/yZwULWv.gif)

カーソル位置を保持に成功しました。しかし、ドットリピートをしてみると？

![MyOperatorNo1-Demo3](http://imgur.com/Md7BCfA.gif)

カーソルが最初にオペレータを実行した位置に戻ってしまいますね。 `MyOperatorNo1Pre()` はドットリピートの時実行されないためです。困りましたね。

---

さて、今となっては明らかですが、要するにドットコマンドを押した瞬間のカーソル位置を取得する方法がオペレータ関数にないのです。そこで、ドットコマンドの前に実行されるオートコマンドを追加するプラグインを書きました。

[vim-event-DotCommandPre](https://github.com/machakann/vim-event-DotCommandPre)

これを使用していると、ユーザー定義オートコマンドイベント `DotCommandPre` が使えます。また、いくつかのドットコマンドを実行する直前の情報を `g:DotCommandPre` に保存します。カーソル位置や画面の状態もこれに含まれるので、これを使ってみましょう。

```vim
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
```

![MyOperatorNo1-Demo4](http://imgur.com/DQTysrz.gif)

ドットコマンドでもカーソル位置を保持できるようになりました。`s:dotcommand` は最初の実行の時だけ 0 で、ドットリピートの時は常に 1 です。カーソル位置の復元に `winsaveview()` 、 `winrestview()` を使うようにしています。とても便利な関数です。

気が付けばオートコマンドはいらなくなっていますが、まあ、いいでしょう。今回のこれはこういうものがあればいいなという提案で、私の作ったこれでなくてもいいので Vim のプロがもっといいものを作って普及させてくれないかなー、と思っています。

---

ところで、 vim-event-DotCommandPre を書いているときに気が付いたんですが、ユーザー定義イベントに対するオートコマンド定義が存在するかどうかを

```vim
if exists('#User#DotCommandPre')
    " foo
endif
```

で取得できるみたいですね。これって意図された挙動なんでしょうか？

```vim
echo exists('#User#DotCommandPre')  " 0
autocmd User DotCommandPre echo 'before dot!'
echo exists('#User#DotCommandPre')  " 1
```