Vim のキーマップを簡単に切り替えられるプラグイン、 vim-keymaps を作りました。
下のように、複数のキーマップを設定することができ、簡単に切り替えることが出来ます。下では <C-k>
でキーマップを切り替えています。
今すぐダウンロード!
使い方
各キーマップを示すディクショナリを入れた配列を g:keymaps
に設定し、後はキーマップを変更するためのキーを適当に割り当てる or コマンドを打つと使えます。詳しくは README.md
で。
初めは接続されているキーボードでなんとか判別できないかなと考えましたが、 ssh 先でキーボードの配列を読むのは不可能なので手動で変更するようにしました。
経緯
私は普段、コーディングするアプリケーション上では数字と記号を入れ替えて使っています。例えば 1
は !
になり、 !
は 1
になります。
瞬時に括弧などが打てるので非常に便利に使っていたのですが、キーボードによって数字と記号の関係は異なるので、JIS 配列に合わせたマッピングを書いていると US キーボードでは崩壊してしまいます。記号だけではなく数字も入力できなくなるので非常に困る。
しかし、いろいろあって先月から US 配列の mac を使うことになりました。
mac のキーボードのみ使うなら .vimrc
を書き直して US にマッピングし直せば良いのですが、普段接続して使っている HHKB はもちろん JIS 配列。本体キーボードで使う時だけ paste モードで使う運用をしていました。
しかし paste モードのままでは tab で(空白ではなく) tab が入力されたり、数字記号以外の各種マッピングが効かないなど困ります。
これはもうキーマップをキー配列ごとに変更するしかない。 せっかくの機会なのでプラグインを自分で作りました。
作成
vim のプラグインを書くことは初めてでした。まともに vimscript も書いたこと無かったし。
書くときは下のサイトを参考にしたり、有名なプラグインのコードを見たりしながら書きました。
vim のドキュメントも結構読みました。読みやすい英語だったし、充実していて良かった。
vimscript, はじめは変数のスコープ定義が特殊だったりしてとっつきにくいですが、書けてくるとなかなか楽しいです*1。
内部の話
設定ファイル
設定ファイルをどのように記述しようか迷って、結局 vimscript の配列を使うことにしました。ただ、 vimscript で配列や辞書型を複数行に記述しようとすると割りと面倒で、行の最初に \
を書くことで行の継続としなければなりません。あくまで行の継続なので、途中にコメントを書くことは出来ません…。( vim は一行ごとのコメントしかできない ) README を書いてる時にその事実に気づいて修正しました…。
マッピングの数は結構多いので、.vimrc
に長大な配列を記述することになります。以下は自分の設定 を半分ぐらい省略したもの です。
let g:keymaps = [ \ { \ 'name': 'JP', \ 'keymap': { \ 'noremap!': { \ '1': '!', \ '3': '#', \ '4': '$', \ '5': '%', \ '6': '&', \ '!': '1', \ '"': '2', \ '#': '3', \ '$': '4', \ '%': '5', \ '&': '6', \ "'": '7', \ '(': '8', \ ')': '9', \ }, \ 'imap': { \ '2': '<Plug>delimitMate"', \ '7': "<Plug>delimitMate'", \ '8': '<Plug>delimitMate(', \ '9': '<Plug>delimitMate)', \ }, \ 'cnoremap': { \ '2': '"', \ '7': "'", \ '8': '(', \ '9': ')', \ }, \ }, \ }, \ { \ 'name': 'PASTE', \ 'paste': 1 \ }, \]
dein のように toml や json に分けれればなあと思ったのですが、 vimscript では標準ライブラリでシュッとパースすることはできません*2。Python バインドでも使えば良いのですが、標準では入っていないことが多いので諦めました*3。
map 方法
この g:keymap
からマッピングするのは非常に素朴で、コマンド、左辺、右辺をスペースでつなげて放り込んでいます。なのでオプションを適当に入れても動くし、他のコマンドも打てちゃいます(キーマップ削除する時にエラー出ると思いますが)。
execute(l:cmd . ' ' . pair[0] . ' ' . pair[1])
特に立派な API は無かったのでこうしました。スペース等も自分で書くのと同じようにエスケープすれば動きます。
unmap 方法
デフォルト設定ではキーマップ変更時に以前のマッピングを削除しています。よって、マッピングと対になるアンマッピングコマンドを生成する必要があります。
これも素朴に1文字目と2文字目をパースしています。
function! s:create_unmap(origin) let l:first = a:origin[0] if l:first is 'n' if a:origin[1] is 'o' " noremap let l:cmd = 'unmap' else let l:cmd = 'nunmap' endif elseif l:first is 'm' " unmap let l:cmd = 'unmap' else let l:cmd = l:first . 'unmap' endif if a:origin =~ '!' let l:cmd .= '!' endif return l:cmd endfunction
paste モード関連
このプラグインは paste モードをサポートしているのですが、paste モードに入ってしまうとマッピングが全て外れるのでキーマップの切り替えができません。
set pastetoggle=<同じキー>
とすることで vim の機能によって paste モードを抜けることができるのですが、vim の機能で抜けているので次のキーマップには移動されず paste 用キーマップのままになってしまいます。
結局、autocmd
を使うことによって paste モードの変更を検知して、フック上でキーマップを変更することにしました。
augroup keymaps autocmd! autocmd OptionSet paste call s:on_change_paste() augroup END function! s:on_change_paste() " sometimes v:option_new not working if g:keymaps_paste_auto_rotate && !&paste \ && get(keymaps#get_current_keymap(), 'paste', 0) " exit paste mode -> rotate call keymaps#rotate_keymap() endif endfunction
lightline との連携
keymaps#get_current_keymap_name
で現在のキーマップ名を取れるようにしたので、これを lightline に表示すれば現在のキーマップを簡単に見れます。最初に示した gif もそうしています。
paste モード変更時にもうまくするには lightline も paste を検知できるように autocmd
を新しく設定する必要があります。
augroup local autocmd! autocmd OptionSet paste call lightline#update() augroup END
その他
<SID>
をキーマップに使っているとその<SID>
は実行時のスクリプトに依存するのでプラグイン用のスコープになってしまう。- 同じような理由から、
<expr>
をキーマップに使って関数を実行したい場合、呼び出す関数はグローバルでなければならなかったりする。
- 同じような理由から、
autoload/
に多くを分割したのだけど、最初のキーマップを設定するために起動時に読み込まれるので逆効果になっているかもしれない。- まあ重いスクリプトではないので大丈夫だと思うけど。
- vimscript 、
endfor
はend
にできるがendfunction
はend
に省略できなかったりして面白い。- 結局全部フルで書いた。
l:
をちゃんと書いてたけど実は暗黙的に設定されるっぽい。- vimscript の文法、だいぶ知れたのでこれからは
.vimrc
サクサク書けそう。 - US キーボード、
'
が一瞬で打てるのが良いですね。でも数字と記号を入れ替えていると0
に記号が割り当てられてしまって不便だったりする。
まとめ
これで今まで通り記号が簡単に入力できるようになりました。 「paste モードではないが記号のマップは削除したキーマップ」なども設定できて快適です。