VimConf2016の延長線で出来たネタ

本記事はVim Advent Calandar 2016の13日目の記事です。

前置き

まずはこの動画を見てください(※私の動画ではないです)。

この動画は先日行われたVimConf2016でt9mdさんが発表されたときの動画です。


vim-mode-plus for Atom editor

私はVimConf2016に参加して、リアルタイムに発表を拝見していたのですが、 発表中「すげー」という単語しか出てこないくらいすごかったです。

言葉でうまく表現するのが難しいですが、すごかったところをざっくり言うと、

  • Vimの、特にtextobjects、operator周りの考察が深い。
  • AtomVim風に操作できるvim-modeというpackage(Vimでいうplugin)をforkしてvim-mode-plusで、Vimの機能を移植するのみならず、様々な独自機能を追加され、完全に独自進化を遂げている。
  • そして、そのvim-mode-plusで独自追加された機能が、Vimについて深く考えていないと作れないようなもので、どれも面白く便利そう。

vim-mode-plusの機能について説明するのが本記事の目的ではないので 詳しく知りたい方は、上の動画およびt9mdさんの発表slideを是非見てみてください。

この発表を見てVimを初めた当初の様々な新しい機能を覚えていくときや、新しいVim pluginを思いついたときと似たような、 最近忘れていたVimへの気持ちの高まりというか、なんというか不思議な感覚を覚えました。

そして、VimConf2016が終わっても熱は冷めやらず Twitter上で何人かのVimmervim-mode-plusについての議論になりました。

そして議論の延長で、

と、kaoriyaさんがマルチ選択のVimのissueを立てられたり、

と、haya14busaさんが爆速でvim-mode-plusのoccurence機能と似たような機能を持つ(ちょっと違うけど便利) Vim pluginを実装したり

vim-mode-plusに触発されて色々な話がでてきて盛り上がりました。

で、やっと本題になるんですが、以下のhaya14busaさんのツイート

を、ここ1ヶ月ほど放置していたのを、ちょうど先週Vim Advent Calendarネタに困っていたときに 思い出したので、これをネタにしようという運びとなりました。(ヒドイ)

haya14busaさん、そしてt9mdさんありがとうございました。

以下今回のネタですが、vim-mode-plusの機能である、Keep cursor position by operator (operatorでの操作時カーソル位置を移動しない機能 & operatorの操作対象を一瞬ハイライトする機能) 相当の機能をVim pluginでできるようにしましたという話です。

Keep cursor position by operator

Vimで例えばoperatorのヤンクを使用すると、 カーソル位置がヤンク対象の textobject の先頭に移動します。 この動作はVimの設定で変えられるものではなく、 状況によっては使いづらいケースがあります。

例えば、大きめの段落をテキストオブジェクトでヤンクした際や、 textobj-entireでファイル全体をヤンクした際など、 「あれ?さっきいたところどこだっけ?」となることがあります。

本記事では上記ケースを解決するために「ヤンクした時にカーソル移動しない」機能を 実現するためにoperator-userを forkして実験的にフック機能をつけてみました。

なお、実験的なのでexperiment/hookというブランチを切ってそちらで実装しています。 時間がなくI/Fが練られていなかったり、テストがなかったり、ドキュメントが雑だったりの状態なので、使用は自己責任でお願いします。

(インストール方法も、使用方法もあまり詳しく載せていないのは決して時間がなかったからではない。はず。。。)

整理してそのうち本家にPRを送りたいですが、いつになるかは不明です。。。

(どなたか代わりにやりたいという方がいたらお願いしたいです)

operator-userのfork版 実験ブランチのフック機能

以下のようなフックを追加する関数を用意しています。 (なお実験ブランチなのでI/Fは変更される可能性があります。ご了承ください。)

" フックを登録する。
" {hook}で登録した関数を、{when}で指定したタイミングで呼び出すことができる。
"
" {hook}は文字列stringまたは関数参照Funcrefで指定。
" 文字列で指定する場合、定義された関数名である必要がある。
" フック関数が取る引数は{when}の指定によって異なる。
"   'before'  Hook(operator_name)
"   'after'   Hook(operator_name, motionwise)
" operator_nameは、operator#user#define()または
" operator#user#define_ex_command()の第1引数
" motionwiseはoperatorで選択された範囲が行単位か、文字単位か、ブロック単位か
" を表す。
"   'line'  行単位
"   'char'  文字単位
"   'block' ブロック単位
"
" {when}には、どのタイミングでフックするかを指定。
" 'before'と'after'を指定可能。それぞれ意味は
"   'before'  operatorが実行される前
"   'after'   operatorが実行された後
"
" 戻り値はフックのID。後述するフック削除関数で該当フックを削除できる。
call operator#user#add_hook({hook}, {when})

" 登録されているフックを削除する。
" {id}で削除対象のフックを指定。
" {id}はoperator#user#add_hook()関数の戻り値を指定。
call operator#user#delete_hook({id})

" 登録されているすべてのフックを削除する。
" {when}は省略可能。
" {when}が省略された場合、登録されているすべてのフックを削除する。
" {when}に'before'が指定された場合、登録時に'before'で登録したすべてのフックを削除する。
" {when}に'after'が指定された場合、登録時に'after'で登録したすべてのフックを削除する。
call operator#user#delete_hook([{when}])

上記フック関数で、前述の「ヤンクした時にカーソル移動しない」および、「ヤンク対象をハイライト」 を実現してみたいと思います。

フック関数はoperator-userの実験ブランチで実装しているので、 ヤンク時にフック関数を呼び出すために、operator-userを使用して、オペレータでヤンクするoperator-yankを作りました。 (当たり前ですが、operator-userのフック機能は、operator-userをベースとしたoperatorでしか使えません。Vim標準のoperatorでは残念ながらフックは使えません。)

operator-yankと、上記フック関数を利用してまずは「ヤンクした時にカーソル移動しない」機能の 設定を書いてみます。

" operator操作前のカーソル位置を保存
function! MySavePosition(opename)
  let s:save_cursor = getcurpos()
endfunction

" operator-yank操作後に、操作前のカーソル位置に移動
function! MyRestorePosition(opename, motionwise)
  " a:openameをチェックして、operator-yankの操作時のみ移動するようにしている
  if a:opename ==# 'yank'
    call setpos('.', s:save_cursor)
  endif
endfunction

call operator#user#add_hook('MySavePosition', 'before')
call operator#user#add_hook('MyRestorePosition', 'after')

operator操作前にカーソル位置を保存するというフック関数と operator操作後に保存したカーソル位置に移動するというフック関数を用意し、 これら関数をそれぞれフックとして登録しています。

1点だけ特徴的なのが、フック関数の第1引数は、operatorの名前が渡されるため、 これを判定して、特定のoperator(ここではoperator-yank)のみ動作させるフックを記述しています。 上記例では、operator-yankの名前は'yank'で登録しているため、 'yank'で判定することで、operator-yankでの操作時にのみ operator操作後のカーソル位置移動を行っています。

次に、「ヤンク対象をハイライト」機能の設定を書いてみます。

" ハイライト時間
let s:highlight_time = 500
" ハイライトグループ(デモで見やすいようにErrorMsgに)
let s:highlight_group = 'ErrorMsg'

" ハイライトパターンのIDs
let s:matched_ids = []

" ハイライトパターンを削除する
function! MyHighlightDelete(timer)
  for mm in s:matched_ids
    call matchdelete(mm)
  endfor
  let s:matched_ids = []
endfunction

" operator対象を規定時間ハイライトする
function! MyHighlightOperatorTarget(opename, motionwise)
  " operator-yankでの操作時のみハイライト
  if a:opename !=# 'yank'
    return
  endif
  let start = getpos("'[")
  let end = getpos("']")
  if a:motionwise == 'line'
    if start[1] == end[1]
      call add(s:matched_ids, matchadd(s:highlight_group, '\%'.start[1].'l'.'.*'))
    else
      call add(s:matched_ids, matchadd(s:highlight_group, '\%'.start[1].'l'.'\_.*\%'.end[1].'l'))
    endif
  elseif a:motionwise == 'char'
    if start[1] == end[1]  " マッチ行が1行内
      call add(s:matched_ids, matchadd(s:highlight_group, '\%'.start[1].'l'.'\%'.start[2].'c'.'.*\%'.(end[2]+1).'c'))
    elseif start[1] < end[1]  " マッチ行が2行以上
      " マッチした先頭行をハイライト
      let lastscol = col([start[1], '$'])
      call add(s:matched_ids, matchadd(s:highlight_group, '\%'.start[1].'l'.'\%'.start[2].'c'.'.*\%'.lastscol.'c'))
      " マッチした最終行をハイライト
      call add(s:matched_ids, matchadd(s:highlight_group, '\%'.end[1].'l'.'^.*\%'.end[2].'c'))

      if end[1] - start[1] == 2 " マッチ行が3行
        call add(s:matched_ids, matchadd(s:highlight_group, '\%'.(start[1]+1).'l'.'.*'))
      else " マッチ行が4行以上
        call add(s:matched_ids, matchadd(s:highlight_group, '\%'.(start[1]+1).'l'.'\_.*\%'.(end[1]-1).'l'))
      endif
    endif
  endif
  call timer_start(s:highlight_time, 'MyHighlightDelete', {'repeat': 1})
endfunction

" バッファ移動時、ハイライトパターンをすべて削除
augroup MyHighlightDeleteGroup
  autocmd!
  autocmd BufLeave * call MyHighlightDelete(0)
augroup END

call operator#user#add_hook('MyHighlightOperatorTarget', 'after')

ヤンク対象を正規表現で記述しているのと、motionwiseごとの処理 行数によりパターンの切り分けをしており若干読みづらいですが、 気持ちで感じてください。 matchaddにより、ハイライトしています。

規定時間たったらハイライトを消すために、地味にVim8.0のtimer機能を使っていたりします。

BufLeaveイベント時にハイライトを消しているのは、 こうしておかないとハイライト中に別バッファに切り替えたとき timerのコールバックでmatchdelete()によりハイライトが削除しようとしますが matchadd()はウィンドウ単位であり、別ウィンドウでハイライトを削除しようとしてエラーになります。

上記によって、行単位および文字単位での操作対象をハイライトできるようになっています。 なお、ブロック単位でのハイライトはめんどうなので実装していません。

最後に実際に試したデモを載せておきます。

f:id:deris:20161213001117g:plain

ヤンクしてもカーソル位置が移動していないのがわかると思います。 また、ヤンク対象が赤背景で一瞬ハイライトされるのもわかると思います。

類似Vim plugin

vim-operator-flashy

ヤンクした際に規定時間、ヤンク対象をハイライトできるVim pluginです。

今回私が作った、operator-userのfork版の実験ブランチを使わなくても、 operator-userを入れていれば、今すぐ気軽に試せるので便利です。

ちなみに私は先週このpluginに気づいたのですが、これを見たときに「えっ、haya14busaさん既にflash on operate作ってるやん」としばらく呆然としてました。

ただ、こちらはヤンクのみが対象なので、ネタがボツにならなくてすみました。(ホッ

まとめ

以上が、VimConf2016の延長線でできたネタでした。

それではVim Advent Calendar 2016の13日目の記事を終わります。

Happy Vim Life!