読者です 読者をやめる 読者になる 読者になる

submode.vim とその設定例なんかを紹介

vim

Vim Advent Calendar 2012 の 62 日目の記事です。

私が毎日のようにお世話になっているプラグインの1つに、kana さん作の submode.vim と言うのがあります。結構昔からあり、かなり便利なのにあまり知られていないような気がします。これはもったいない!と言うことで今回はこの submode.vim について書こうと思います。

submode.vim とは

https://github.com/kana/vim-submode

submode.vim は、ユーザが自由にサブモードを定義できるプラグインです。…と言っても何のことやらさっぱりですね。
Vim には、複数のキー、つまりキーシーケンスによって成立するコマンドがたくさんあります。例えば <C-w>+ は現在のウィンドウの高さを変更します。そして、この複数のキー入力が必要な割に、連続で入力したいコマンドもかなりあります。さきほどの <C-w>+ なんかはまさにそれですね。数字を前に付けてもいいですが、大抵はちょっとずつずらして調整すると思います。しかし、そのために <C-w>+<C-w>+<C-w>+<C-w>+<C-w>+ あ、行きすぎた、<C-w>- ... などとやるのはあまりに面倒です。
このために、よく使うコマンドについては1つのキーに割り当てて連続で実行できるようにしている人も多いでしょう。しかしキーの数には限りがあり、毎度割り当てていると使えるキーはあっと言う間に底を突きます。


そこで submode.vim ですよ!


submode.vim を使うと、例えばこの場合だと「ウィンドウサイズ変更モード」と言うのが作れます。<C-w>+ を押すとこのモードに入り、以後は + や - だけでウィンドウサイズの変更が可能です。つまり、<C-w>+++++- です。サブモード中で割り当てていないキーや終了に設定したキーなどを押すとサブモードを抜けます。
このように、submode.vim を使うと単独のキーを潰さずに連続で行うような操作を実行することが可能になります。この例と似たような事例はたくさんあり、応用範囲はかなり広いです。

設定例

以下で、私がやっている設定の例を紹介します。
まずは先ほど挙げたウィンドウサイズの例。

call submode#enter_with('winsize', 'n', '', '<C-w>>', '<C-w>>')
call submode#enter_with('winsize', 'n', '', '<C-w><', '<C-w><')
call submode#enter_with('winsize', 'n', '', '<C-w>+', '<C-w>+')
call submode#enter_with('winsize', 'n', '', '<C-w>-', '<C-w>-')
call submode#map('winsize', 'n', '', '>', '<C-w>>')
call submode#map('winsize', 'n', '', '<', '<C-w><')
call submode#map('winsize', 'n', '', '+', '<C-w>+')
call submode#map('winsize', 'n', '', '-', '<C-w>-')

各関数の最初の引数は、サブモードの名前です。
第2引数はモードの種類。n = ノーマルモード、i = 挿入モードと言った感じで、ni のように複数同時に指定可能。
第3引数は map のオプションで、b = <buffer> e = <expr> と言った具合。
そして第4引数と第5引数が map の {lhs} と {rhs} です。
submode#enter_with() でサブモードに入るためのキーマッピング、および、入った際に同時に行うキーを設定します。
submode#map() で、サブモード中に使えるキーマッピングを設定います。
今回は使ってませんが、submode#leave_with() で抜けるためのキーを設定できます。何も設定しなかった場合はデフォルトのキー(<ESC>)が自動で設定されます。
また、先ほど説明したように割り当てていないキー、この場合は <>+- 以外のキーを押した場合や、一定時間放置した場合などにもサブモードを抜けます。細かいところは help を参照してください。


他の例をいくつか紹介します。次のはタブページの切り替え gt/gT を gttt... でできるようにします。

call submode#enter_with('changetab', 'n', '', 'gt', 'gt')
call submode#enter_with('changetab', 'n', '', 'gT', 'gT')
call submode#map('changetab', 'n', '', 't', 'gt')
call submode#map('changetab', 'n', '', 'T', 'gT')

時系列で undo/redo を辿る g+/g- の例。これは submode.vim の help の冒頭でも紹介されています。

call submode#enter_with('undo/redo', 'n', '', 'g-', 'g-')
call submode#enter_with('undo/redo', 'n', '', 'g+', 'g+')
call submode#map('undo/redo', 'n', '', '-', 'g-')
call submode#map('undo/redo', 'n', '', '+', 'g+')

私は以下の設定で、<Space>gttt... でタブページを移動できるようにしています。右端に来たら左端に来る親切設計です。

function! s:SIDP()
  return '<SNR>' . matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SIDP$') . '_'
endfunction
function! s:movetab(nr)
  execute 'tabmove' g:V.modulo(tabpagenr() + a:nr - 1, tabpagenr('$'))
endfunction
let s:movetab = ':<C-u>call ' . s:SIDP() . 'movetab(%d)<CR>'
call submode#enter_with('movetab', 'n', '', '<Space>gt', printf(s:movetab, 1))
call submode#enter_with('movetab', 'n', '', '<Space>gT', printf(s:movetab, -1))
call submode#map('movetab', 'n', '', 't', printf(s:movetab, 1))
call submode#map('movetab', 'n', '', 'T', printf(s:movetab, -1))
unlet s:movetab

g:V.modulo は vital.vim の Math モジュールに含まれている こんな感じ の関数です。必要ならコピーしてください。

他のプラグインとの組み合わせも有効です。例えば以下は id:tyru さんの nextfile.vim との組み合わせです。nextfile.vim は、同じディレクトリ内のファイルを順に移動するためのプラグインです。

call submode#enter_with('nextfile', 'n', 'r', '<Leader>j', '<Plug>(nextfile-next)')
call submode#enter_with('nextfile', 'n', 'r', '<Leader>k', '<Plug>(nextfile-previous)')
call submode#map('nextfile', 'n', 'r', 'j', '<Plug>(nextfile-next)')
call submode#map('nextfile', 'n', 'r', 'k', '<Plug>(nextfile-previous)')

この設定で、\jjjj... でディレクトリ内のファイルを順に参照できます。


こんな感じで、他にも change-list の移動(g; g,)とか、色々使えると思います。

改造版

そんなこんなでとても便利な submode.vim なんですが、使ってて不満点も出てきたので、手元で改造した版を使ってました。が、まあ手元に置いておくのもアレだし管理も面倒だったので、今回を機に先日 fork して上げてみました。

https://github.com/thinca/vim-submode/tree/my-master

変更点は主に2つ。

任意のキーでサブモードから抜ける際に押したキーを有効にできるように

サブモードでマッピングしてないキーを押すとサブモードから抜けるのは上で説明した通りですが、この時押したキーは submode.vim によって無効化されます。私は gtttt とかで移動した後に j とかでいきなり移動したかったので、無効化されないようなオプションを加えました。

let g:submode_leave_with_key = 1

…オプション名いいの思い付かなかったので名前が微妙です。

内部で使ってるキーマッピングを短くした

内部で管理用にさまざまなキーマッピングが行われているのですが、これによって {lhs} が長くなってしまう場合があります。実は Vim には {lhs} は最大50文字という制限があり、ちょっと長いサブモード名なんかを付けるとこの50文字に引っ掛かることがありました。そこで、内部で使っているキーマッピングを短くして(と言ってもenterをeにしたりとか、単語を削っただけ)、制限に引っ掛かりにくくなるようにしました。
根本な解決ではないけど、まあマシになったということで。



どちらの変更もオプション名微妙だったりそもそも dirty な変更だったりするので Pull Request するか迷い中。今度ダメ元で送ってみようかな。

おまけ: 仕組みの解説

私が作ったわけではないので解説するのもおこがましいですが、最初にこの仕組みを知ったときに、よく思い付いたな!と甚く感動したので、ちょっと解説してみます。ちなみに Vim のキーマッピングの挙動、特にキー待ちについては前提知識とします。
このプラグイン、ちょっと聞いただけだと、getchar() でがんばっているのかな、とか思いますが、getchar() はサブモードを抜ける際に入力したキーを握り潰すのに使っているだけで、それ以外の場所では一切使ってません。全てキーマッピングの設定で実現されています。すごい!

まず、サブモードに入るキーマッピングは以下のようになります。

map {key-to-enter}
\   <Plug>(submode-before-entering:{submode}:with:{key-to-enter})
   \<Plug>(submode-before-entering:{submode})
   \<Plug>(submode-enter:{submode})

最初のがサブモードに入るためのキーシーケンスです。
残りの3つは連続で入力されるもので、{submode} はサブモード名です。
最初のはサブモードに入った際に実行されるキーシーケンスです。submode#enter_with() の {rhs} ですね。
2 番目のはサブモードに入った際のオプション設定などです。'timeout' なんかの設定がここで行われます。
そして最後のはサブモードに設定した{lhs}が入力できるようになる前に実行されるものです。これは更に以下に展開されます。

map <Plug>(submode-enter:{submode})
\   <Plug>(submode-before-action:{submode})
   \<Plug>(submode-prefix:{submode})

before-action は、サブモードの状態をメッセージ領域に表示するためのものです。実際に submode.vim を使ってみるとわかりますが、サブモードの実行中はメッセージ領域に "-- Submode: undo/redo --" みたいな表示が出ます。これをここで出しています。
<Plug>(submode-prefix:{submode}) がキモです。これは名前の通り prefix で、これを prefix にしてサブモード内の {lhs} が設定されています。例えば以下のような感じです。

map <Plug>(submode-prefix:{submode})  <Plug>(submode-leave:{submode})
map <Plug>(submode-prefix:{submode})+ <Plug>(submode-rhs:{submode}:for:+)<Plug>(submode-enter:{submode})
map <Plug>(submode-prefix:{submode})- <Plug>(submode-rhs:{submode}:for:-)<Plug>(submode-enter:{submode})

見ての通り、<Plug>(submode-prefix:{submode}) までが共通で複数のキーマッピングが定義されています。これにより、Vim はキーマッピングの確定待ちの状態に入ります!
ここで定義されている + や - 以外のキーを押すと、一番上の leave に確定するので、サブモードを抜けます。
+ や - を押すとそれぞれ下のキーマッピングに確定します。ここで定義されていた {rhs} が実行された後、<Plug>(submode-enter:{submode}) が自動で入力されて再びキー確定待ちの状態に戻ります。
leave では、最初に変更した 'timeout' なんかのオプション値を復元したり、サブモードを抜ける際に入力した余計なキーを getchar() で握り潰したりしてます。



解説してみたものの、改めてすごい…。こういう動作が可能な Vim も、それを使いこなしてこんなプラグインを作ってしまう kana さんもほんとすごいです。


明日は @ さんです。ついに2月突入!