INFO

この記事は Vim駅伝 の 2023-09-06 の記事です。

導入

Vim は複数のモードを切り替えてファイル編集を行う、いわゆるモーダル UI を持っているテキストエディタである。使いこなすまでには習熟が必要だが、慣れてくると必要なタイプ数が少なくなるのが魅力である。

しかし、Vim の要であるノーマルモードは、日本語 IME とたいへん食い合わせが悪い。ノーマルモードに Esc キーで戻っても、IME をオフにしておかないと、変換窓が開いてしまってノーマルモードコマンドを入力することができない。また、逆に ia などでインサートモードに入ったときも、いちいち IME をオンにし直さないと日本語を入力することができない。日本語が主体の文章を入力しているときには、これが地味に面倒くさい。

少し考えてみると、この問題は「システムの IME が」「モード移動の時」「自動でオンオフされない」ことに分解できることがわかる。そのどれかを解決してやれば良いため、同じく 3 つの解決策が考えられる。

  1. システムの IME を使わない
  2. ノーマルモードに(できるかぎり)戻らない
  3. モード変更の際、自動で IME のオンオフを切り替える

最もラディカルな解決策である 1.を行う場合、たとえば Vim 自体に日本語 IME の機能を代行させることになる。これを実現しているものの例として、Vim 上で SKK を動かすプラグインである eskk.vim や、kuuote さんによるその denops 実装である skkeleton がある。

1.の解決策で満足できる場合、この記事を読む必要はない。しかし、SKK はさすがにストイックすぎるという人や、使い慣れた IME を Vim でも使いたいという人もいるだろう(自分がそう)。そんな人のために、この記事では自分が使っている 2.と 3.の方針での設定を紹介する。

ノーマルモードに戻らない

まず、ノーマルモードに戻る回数を減らす、という 2 の解決策について。インサートモードでのカーソル移動機能を強化することで、そもそもノーマルモードに戻らなくてもテキストの編集がしやすいようにしてしまえばよい。
Vim と並ぶ古典的なエディタである Emacs では、 <C-x> (Control キーと他のキーの同時押し)や <M-x> (同じく、Meta キー)のようにキーを組み合わせてカーソルを移動するので、このアイデアを拝借する。

Meta キーに使える都合の良いキーが自分の環境にはないので、自分は <C-x> を使ったマッピングのみを行っている。そのためこの記事では、それら Control キーのマッピングについてのみ解説するが、Meta キーも使えるならマッピングの幅は大いに広まるだろう。

Control キーを使ったマッピングをする場合、キーボードの設定を変更して Control キーが A キーの左にくるようにするのを強く推奨する(マッピング関係なくこの設定はおすすめ)。US キーボードなどではキーボードの左下にこのキーがあるが、カーソル移動のたびにここに指を伸ばしていると小指がつる。普通 Caps Lock キーが A キーの左にあるが、日本人はほとんど使わないのだし、潰すか入れ替えてしまえば良い。

標準のキーの確認

インサートモードにおける <C-x> のうち、すでに便利な機能にマッピングされているものを潰してしまうともったいないので、いらないものを使うようにしたい。

標準のマッピングを下の表にまとめている 1 。この表を作った結果、 <C-a> <C-e> <C-f> <C-b> <C-n> <C-p> などがマッピングできそうだ、という感想を抱いた。その感想に同意してくれるならこの表を読む必要はない。

クリックして表を展開

使う? の列には、そのキーバインドを(自分が)よく使うかどうかを書いている。これが いらない だったら、他のコマンドに再利用できそうだということ。

入力効果使う?
<C-a>一つ前のインサートモードで入力したものを再度入力いらない
<C-b>割当なしいらない
<C-c>インサートモードを quit
<C-d>インデント削る
<C-e>一行下の文字を入力いらない
<C-f>この行をインデントし直す= があるのでいらない
<C-g><C-g>g など、組み合わせて使う
<C-h><BS> (バックスペース)する
<C-i>Tab におなじ
<C-j>改行する
<C-k>マルチバイト文字を入力
<C-l>割当なしいらない
<C-m>改行する<C-j> とどっちかでいい気はする
<C-n>次の変換候補補完窓が出てないならいらない
<C-o>インサートモードから一旦抜ける
<C-p>前の変換候補補完窓が出てないならいらない
<C-q><C-v> と同じいらないが押しにくい
<C-r>レジスタから入力
<C-s>割当なしいらない
<C-t>インデント追加
<C-u>行頭まで削除
<C-v>「文字通り」入力
<C-w>単語を削除
<C-x>主に補完ができるサブモードに移行押しにくい
<C-y>一行上の文字を入力いらない
<C-z>Vim をサスペンド
<C-[>Esc に同じ
<C-]>その場で abbreviation を発動
<C-_>ヘブライ語入力(右から入力)に切り替え英字キーボードでは押しにくい
<C-^>言語設定の切り替え押しにくい

これをもとに いらない ものにマッピングしていく。

マッピング

表を眺めた結果、 <C-a> <C-e> <C-f> <C-b> <C-n> <C-p> などの機能はいらなかったので、潰して自前のマッピングに使ってよさそうだ。Emacs のマッピングと似た構成にできるので嬉しい。

行頭、行末への移動

Emacs と同じく、 <C-a><C-e> で行頭・行末へ移動できるようにする。

" 行頭へ移動
cnoremap <C-a> <Home>
inoremap <C-a> <Home>
" 行末へ移動
cnoremap <C-e> <End>
inoremap <C-e> <End>

単語移動

<C-f><C-b> で単語単位で前後に移動できるようにする。Emacs では <C-f><C-b> だと一文字移動、 <M-f><M-b> で単語単位移動になっているが、単語単位のほうが便利なのでこちらを採用。

inoremap <silent> <C-b> <Cmd>normal! b<CR>
inoremap <silent> <C-f> <Cmd>normal! w<CR>

行移動

Emacs と同じく、 <C-n><C-p> で一行先・一行前へ移動できるようにする。

inoremap <silent> <C-p> <Cmd>normal! gk<CR>
inoremap <silent> <C-n> <Cmd>normal! gj<CR>

保存

ファイルの保存もノーマルモードに戻らないと行えないので、マッピングを作ってしまう。 latexmkjekyll serve,zola serve のような、ソースファイルを監視してプレビューを自動更新してくれるプログラムを使っている場合は特に有用。空いているキーならどれでもいいが、自分は <C-l> を採用している。

inoremap <C-l> <Cmd>update<CR>

マウス移動

邪道だが、カーソルを大きく動かしたい場合はマウスを使ってしまうのも手。当然だが、マウス/スクロールはモードを切り替えることなく使用できる。

set mouse+=i

自動で IME をオンオフする

もう一つの方針である 3 の解決策の例として、Vim の内部から IME を操作するコマンドを叩いて、モード切替時に自動で IME のオンオフを行わせるというものが考えられる。Vim の場合 imactivatefunc あたりの機能で制御できるというが、neovim にはそれらの機能は ポートされていないらしい 2 。ただ、やりたいこと自体は単純なので、自前で作ってしまう。

IME を操作するコマンドには、例えば以下が使える:

  • Linux で fcitx5 を使っている場合、 fcitx5-remote
  • macOS を使っている場合、 im-select
  • Windows を使っている場合、 im-select または zenhan.exe

これらを使って、自分は以下のような設定で自動 IME オンオフを行っている:

クリックして展開
let g:is_macos = has('mac')
nnoremap <silent><expr> <F2> IME_toggle()
inoremap <silent><expr> <F2> IME_toggle()
 
augroup IME_autotoggle
  autocmd!
  autocmd InsertEnter * if get(b:, 'IME_autoenable', v:false) | call Enable() | endif
  autocmd InsertLeave * call Disable()
  autocmd CmdLineEnter /,\? if get(b:, 'IME_autoenable', v:false) | cnoremap <CR> <Plug>(kensaku-search-replace)<CR>| endif
  autocmd CmdLineEnter /,\? if !get(b:, 'IME_autoenable', v:false) | silent! cunmap <CR> | endif
augroup END
 
function! IME_toggle() abort
  let b:IME_autoenable = !get(b:, 'IME_autoenable', v:false)
  if b:IME_autoenable ==# v:true
    echomsg '日本語入力モードON'
    if mode() == 'i'
      call Enable()
    endif
  else
    echo '日本語入力モードOFF'
    if mode() == 'i'
      call Disable()
    endif
  endif
  return ''
endfunction
 
function! Enable() abort
  if g:is_macos
    call system('/path/to/im-select com.justsystems.inputmethod.atok33.Japanese')
  else
    call system('fcitx5-remote -o')
  endif
endfunction
 
function! Disable() abort
  if g:is_macos
    call system('/path/to/im-select com.apple.keylayout.ABC')
  else
    call system('fcitx5-remote -c')
  endif
endfunction

このような設定をした上で <F2> を押すと日本語入力設定が起動し、インサートモードに入ると自動で IME を使ってくれるようになる。いわゆる 半角/全角 キーのような感覚で <F2> キーを使えばよい。

この設定をコピペして使う場合は Enable / Disable 関数の内容を自分の環境に合わせて修正する必要があることに注意。以下で詳しく説明する。

IME を自動でオフにする

最も簡単な例として、インサートモードを抜けたときに自動で IME をオフにする設定を考える。これには autocmd を使えばよい:

autocmd InsertLeave * call Disable()

関数 Disable は IME をオフにする自作関数。使っている OS や IME などによって実装は異なるだろうが、自分の場合以下のようにしている:

function! Disable() abort
  if g:is_macos
    call system('/path/to/im-select com.apple.keylayout.ABC')
  else
    call system('fcitx5-remote -c')
  endif
endfunction

自分は普段 macOS と linux を使っており、共通の設定ファイルを使っているので、OS に合わせて呼び出すコマンドを切り替えたい。そのためこの関数の中では、変数 g:is_macos によって呼び出すコマンドの分岐を行っている。 g:is_macos

let g:is_macos = has('mac')

のようにして事前に設定しておく。

IME を自動でオンにする

同様にして IME を有効化する関数 Enable も作れる:

function! Enable() abort
  if g:is_macos
    " atok33を使う場合
    call system('/path/to/im-select com.justsystems.inputmethod.atok33.Japanese')
    " ことえりを使う場合
    call system('/path/to/im-select com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese')
    " それ以外の場合、im-selectを引数なしで使えば現在のIMEの名前が出るので、それを引数とすればよい
  else
    call system('fcitx5-remote -o')
  endif
endfunction

あとは同様に、 autocmd を使ってインサートモードに入るたびにこれを呼び出せばよい。

IME のオンオフ機能をトグルする

ただ、 Enable 関数については、常に呼び出せば良いというものではない。例えばプログラムファイルなど、日本語をほとんど入力しないファイルを編集しているときに毎回 IME が起動するのはかえってわずらわしい。これを防ぐためには、IME を起動するかどうかをバッファ変数 b:IME_autoenable で管理するようにし、 autocmd の中で if 文を使って分岐すればよい:

autocmd InsertEnter * if get(b:, 'IME_autoenable', v:false) | call Enable() | endif

変数 b:IME_autoenable をどう設定するかは自由だが、一例として自分が用いているトグル関数を紹介する:

function! IME_toggle() abort
  let b:IME_autoenable = !get(b:, 'IME_autoenable', v:false)
  if b:IME_autoenable == v:true
    echo '日本語入力モードON'
    if mode() == 'i'
      call Enable()
    endif
  else
    echo '日本語入力モードOFF'
    if mode() == 'i'
      call Disable()
    endif
  endif
  return ''
endfunction

最後に、この関数を呼び出すマッピングを作って終了。自分は <F2> にしている:

nnoremap <silent><expr> <F2> IME_toggle()
inoremap <silent><expr> <F2> IME_toggle()

日本語の文章のファイルを開いたら <F2> を押すようにすれば、日本語入力設定が起動し、IME の切り替えが自動で行われるようになる。

日本語で検索を行う

また、日本語の文章を入力している場合、多くの場合検索したい文字列も日本語である。そのため、検索モードに入ったときにも IME を起動し、検索を終えてノーマルモードに戻るときには解除したい。これには以下のように設定すればよい:

autocmd CmdLineEnter /,\? if get(b:, 'IME_autoenable', v:false) | call Enable() | endif
autocmd CmdlineLeave /,\? call Disable()

もしくは、ローマ字で検索したい内容を入力したら、自動的に「ありえそうな日本語への変換」すべてを生成し、それらにマッチするように検索してくれるととても便利だ。まさにこれを行ってくれるのが lambdalisue さんの kensaku-search.vim である。こちらを使いたい場合、

autocmd CmdLineEnter /,\? if get(b:, 'IME_autoenable', v:false) | cnoremap <CR> <Plug>(kensaku-search-replace)<CR>| endif
autocmd CmdLineEnter /,\? if !get(b:, 'IME_autoenable', v:false) | silent! cunmap <CR>| endif

とすればよい 3

まとめ

あまり使わないキーの組み合わせや autocmd などの機能を活用すると、ノーマルモードへの移動をそもそも減らしたり、移動した際に自動で IME をトグルしたりできる。このような比較的簡単な設定だけでも、素の Vim でかなり快適に日本語を入力できるようになる。例えば、この記事を含め、自分は日本語の文章も常に Vim で書いている。

Footnotes

  1. iminsert というオプションで制御することになっており、ヘルプ 曰くこの値を 2 にすると IME を制御できる、とあるがこれは嘘。その機能が実装されていない現状の neovim だと、実際は そもそも0か1にしか設定できない
    ……と思っていたのだが、この理解には正確ではないところがあった。 iminsert の歴史や目的について 丁寧な補足記事 を kaoriya さんに書いていただいたので、詳しくはこちらを参照のこと。kaoriya さん、ありがとうございます。

  2. kensaku-search.vim を使いたい場合、これ自体に加え、依存先である kensaku.vim と、その依存先である denops もインストールする必要がある