LoginSignup
3
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-17

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-Demo1

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

MyOperatorNo1-CursorPosition

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

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

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

MyOperatorNo1-Demo3

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


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

vim-event-DotCommandPre

これを使用していると、ユーザー定義オートコマンドイベント 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

MyOperatorNo1-Demo4

ドットコマンドでもカーソル位置を保持できるようになりました。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
3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0