Unyablog.

のにれんのブログ

vim-keymaps でキーマップを簡単に切り替える

Vim のキーマップを簡単に切り替えられるプラグイン vim-keymaps を作りました。

github.com

下のように、複数のキーマップを設定することができ、簡単に切り替えることが出来ます。下では <C-k> でキーマップを切り替えています。

f:id:nonylene:20170322005931g:plain

今すぐダウンロード!

使い方

各キーマップを示すディクショナリを入れた配列を g:keymaps に設定し、後はキーマップを変更するためのキーを適当に割り当てる or コマンドを打つと使えます。詳しくは README.md で。

初めは接続されているキーボードでなんとか判別できないかなと考えましたが、 ssh 先でキーボードの配列を読むのは不可能なので手動で変更するようにしました。

経緯

私は普段、コーディングするアプリケーション上では数字と記号を入れ替えて使っています。例えば 1! になり、 !1 になります。

瞬時に括弧などが打てるので非常に便利に使っていたのですが、キーボードによって数字と記号の関係は異なるので、JIS 配列に合わせたマッピングを書いていると US キーボードでは崩壊してしまいます。記号だけではなく数字も入力できなくなるので非常に困る。

しかし、いろいろあって先月から US 配列の mac を使うことになりました。

mac のキーボードのみ使うなら .vimrc を書き直して US にマッピングし直せば良いのですが、普段接続して使っている HHKB はもちろん JIS 配列。本体キーボードで使う時だけ paste モードで使う運用をしていました。

しかし paste モードのままでは tab で(空白ではなく) tab が入力されたり、数字記号以外の各種マッピングが効かないなど困ります。

これはもうキーマップをキー配列ごとに変更するしかない。 せっかくの機会なのでプラグインを自分で作りました。

作成

vimプラグインを書くことは初めてでした。まともに vimscript も書いたこと無かったし。

書くときは下のサイトを参考にしたり、有名なプラグインのコードを見たりしながら書きました。

mattn.kaoriya.net

thinca.hatenablog.com

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 では標準ライブラリでシュッとパースすることはできません*2Python バインドでも使えば良いのですが、標準では入っていないことが多いので諦めました*3

map 方法

この g:keymap からマッピングするのは非常に素朴で、コマンド、左辺、右辺をスペースでつなげて放り込んでいます。なのでオプションを適当に入れても動くし、他のコマンドも打てちゃいます(キーマップ削除する時にエラー出ると思いますが)。

execute(l:cmd . ' ' . pair[0] . ' ' . pair[1])

特に立派な API は無かったのでこうしました。スペース等も自分で書くのと同じようにエスケープすれば動きます。

unmap 方法

デフォルト設定ではキーマップ変更時に以前のマッピングを削除しています。よって、マッピングと対になるアンマッピングコマンドを生成する必要があります。

map - Vim日本語ドキュメント

これも素朴に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 、 endforend にできるが endfunctionend に省略できなかったりして面白い。
    • 結局全部フルで書いた。
  • l: をちゃんと書いてたけど実は暗黙的に設定されるっぽい。
  • vimscript の文法、だいぶ知れたのでこれからは .vimrc サクサク書けそう。
  • US キーボード、' が一瞬で打てるのが良いですね。でも数字と記号を入れ替えていると 0 に記号が割り当てられてしまって不便だったりする。

まとめ

これで今まで通り記号が簡単に入力できるようになりました。 「paste モードではないが記号のマップは削除したキーマップ」なども設定できて快適です。

Vim 最高!みんなも今すぐプラグイン書こう!!

*1:perl っぽいですね(?)

*2:雑に eval すると大体動くとは思う

*3:実は go とか使うとよかったりするのかな