本稿はVim Advent Calendar 2016の12日目の記事です。
今年の9月にVim 8.0がリリースされました。これに伴って追加されたVimの新機能は数多くありますが、その中に:smileコマンドというものがあることをご存知でしょうか。私はYokohama.vim #8に参加した際に知りました。このコマンドは、実行すると次のような「スマイル」が表示されます。

:smileのヘルプを見ると“make the user happy(ユーザを幸せにする)”と解説されています。確かに、これは大変愉快で素晴らしい新機能だと思いました。
しかし、惜しいことにこの機能はユーザ(Vimmer)に対する考察がほんのわずかに足りていないようにも思われます。すなわち「あるVimmerがunhappyなとき、そのVimmerに:smileコマンドを叩く余裕があるだろうか」ということに関する考察が不十分な気がするのです。本当にunhappyなVimmerには:smileコマンドを叩く余裕などないのではないでしょうか。
そこで、本稿では Vimmerがunhappyなときはどんなときかについての深い考察を行い、そうしたケースで先ほどの「スマイル」が自動的に表示されるようなvimrcの設定を考えてみたいと思います。
不浄キーを押してしまうとき
よく知られているようにVimでのカーソル移動は(QWERTY配列において)ホームポジションから動かずに押すことのできるhjklで行うのが基本です。デフォルトではいわゆる「矢印キー」でも同様の移動を行うことが可能ですが、実際にこれらが使用されることは非常に稀で、一部のVimmerの間では「不浄キー」などと呼ばれているぐらいです1。
不浄キーは人によって(<Nop>にマップすることによって)無効化していたり、もっと過激な例では「開いているバッファの内容を全消去の上、それを保存してVimを終了する」というような設定になっていたり2します(いずれもvimrc読書会で見かけたことがあります)。
おそらくVimmerが不浄キーを押してしまうなどというのはよほど疲れているときに違いありません。つまり、不浄キーはVimmerがunhappyなときに押されるキーと言えます。
ということで、上下左右4種の不浄キーを押すと:smileコマンドが実行されるようなキーマップを設定してみましょう。noremapを用いるとノーマルモードとヴィジュアルモードのキーマップ、inoremapでインサートモードでのキーマップを設定できるので、例えば.vimrcに以下のように記述すれば、上記3モードすべてにおいて不浄キーを押すと例のスマイルが表示されるようになります。
noremap <Left> :<C-u>smile<CR>
noremap <Right> :<C-u>smile<CR>
noremap <Up> :<C-u>smile<CR>
noremap <Down> :<C-u>smile<CR>
inoremap <Left> <Esc>:smile<CR>
inoremap <Right> <Esc>:smile<CR>
inoremap <Up> <Esc>:smile<CR>
inoremap <Down> <Esc>:smile<CR>
動作確認のためにインサートモードで不浄キーを押してみます。

なんというか、すごくバカにされている感じがしますね……
一定時間入力がないとき
Vimは十分に習熟すれば思考のスピードで編集が行える強力なエディタです。熟練のVimmerがVimを用いてコーディングしているとき、その手が止まるなどということは基本的にあり得ません。とはいえ、Vimmerも人間ですから、時にはよいアルゴリズムが思い浮かばず、詰まってしまうこともあるかもしれません。これは間違いなくunhappyなことと言えるでしょう。
そこでコーディングのリズムが止まったとき、すなわちインサートモードに入った状態でユーザが一定時間何のキーも入力もしなかった場合に:smileコマンドが実行されるような設定を考えます。
Vimにおいて、コマンドを自動的に行いたい場合にはautocommandという機能が使えます。そのトリガに使えるイベントには「新しいバッファの編集を始めたとき」(BufRead)や「バッファ全体をファイルに書き込むとき」(BufWrite)など数多くの種類があります。
今回の「インサートモードに入った状態でユーザが一定時間何のキーも押さないとき」は、イベントCursorHoldIによってとらえることもできそうですが、このイベントにおける “一定時間” を変更するためにはupdatetimeという「スワップファイルがディスクに書き込まれる時間」を制御するためのオプション値を変更しなければなりません。これは何となくバッドノウハウのような気がするので、今回はVim 8で新たに導入されたタイマー機能3を用いて実装してみることにします。
" “一定時間”の指定(単位はミリ秒)
let s:smile_time = 10000
" 文字が入力された場合にタイマーをリセットする関数
function! UpdateSmileTimer(timer)
call timer_stop(a:timer)
let s:smile_timer = timer_start(s:smile_time, 'ShowSmile')
endfunction
" スマイルコマンドを実行する関数
function! ShowSmile(timer)
smile
endfunction
augroup smile
autocmd!
" インサートモードに入ってs:smile_timeが経過したらShowSmile() を実行
autocmd InsertEnter * let s:smile_timer = timer_start(s:smile_time, 'ShowSmile')
" 入力があったらUpdateSmileTimer() を実行
autocmd InsertCharPre * call UpdateSmileTimer(s:smile_timer)
augroup END
上の設定ではインサートモード中で10秒間文字の入力をしなかった場合:smileコマンドが実行されるようになっています。実用上は10秒でもかなりシビアだと思いますが、デモにはそれでも長すぎるのでs:smile_timeをもっと短くしたときの動作例を以下に示します4。

この顔、かなり煽リティが高い気がします……
Vimを終了するとき
Vimに別れを告げるとき、それは言うまでもなくVimmerにとっては不幸なときです。それでもせめて“Good Bye”の一言は笑顔で言いたいものです。
というわけで、最後はVim終了時にスマイルを表示させてみましょう。上で紹介したautocmdのトリガに使えるイベントの中にはVim終了時をとらえるものもありますが、ここでは:quitコマンドの上書きによって実現することにします。
デフォルトのVimでは、ユーザは小文字から始まるコマンドを定義することができないため(必ず大文字で始める必要があります):quitコマンドを上書きすることができません。しかしvim-altercmdというプラグインを導入すると小文字から始まるコマンドを定義できるようになります。
プラグインのインストールは各自お好きな方法でなさると良いでしょう。私はdein.vimユーザなので次のようにしました。
call dein#add('tyru/vim-altercmd')
さて、準備は整ったので:quitの上書きをしていきます。vim-altercmdを用いてコマンドを定義するためにはAlterCommandを使用します。このコマンドを.vimrcで使用する場合、それよりも前にcall altercmd#load()によってプラグインを有効化する必要があるようです。AlterCommandによるコマンド定義において[]表記を利用すると省略可能なコマンドを定義することもできます。
call altercmd#load()
AlterCommand q[uit] smile

あれれ……これではVimが終了できないじゃないか!
おわりに
本当は今日までにすごいプラグインを作って自慢する記事を書きたかったのですが、進捗がなかったためこのような非常にくだらない記事になってしまいました……すみません。
とはいえ、なんだかんだ言いつつもvimrcに書ける程度の手軽さで、種々のイベントを検知して望みの処理をフックする方法については私の知る限りを網羅する内容になっています。何かの際に参考にしてもらえれば幸いです。
Happy Vimming!
-
私の知る限り、この語の初出は第3回vimrc読書会です。 ↩︎
-
もっとも私はundo履歴を半永久化するundo-persistenceを有効にしているため、この設定を行っても大して恐怖感はありませんが。 ↩︎
-
Vim 8のタイマー機能についてはVim 8.0 Advent Calender 5日目の記事にthincaさんによる詳しい説明があります。 ↩︎
-
インサートモード中で
:smileコマンドを実行する場合、その出力行数を陽に指定する方法はないようです(もしご存知の方がいらしたら教えてください)。そのため、実際にこの設定を行っても、頭の数行しか表示されない場合があります(筆者の環境では実行の度に表示行数が変わりました)。 ↩︎
