VimとIMEの仲を取り持つ

Vimで快適に日本語入力したい!

この記事は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.の方針での設定を紹介する。

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

インサートモードでのカーソル移動機能を強化することで、そもそもノーマルモードに戻らなくてもテキストの編集がしやすいようにしてしまう。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をオンオフする

もう一つの方針として、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などによって実装は異なるだろうが、自分の場合以下のようにしている:

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を使ってインサートモードに入るたびにこれを呼び出せばよい。

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

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

この変数を変更するための設定は後述する。

日本語で検索を行う

また、日本語の文章を入力している場合、多くの場合検索したい文字列も日本語である。そのため、検索モードに入ったときにも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

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

上で用いられている、IME自動オンを制御している変数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の切り替えが自動で行われるようになる。

まとめ

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

脚注

2

iminsertというオプションで制御することになっており、ヘルプ曰くこの値を2にするとIMEを制御できる……とあるがこれは嘘。その機能が実装されていない現状のneovimだと、実際はそもそも0か1にしか設定できない

[2024-01-25追記]上の理解には正確ではないところがあった。iminsertの歴史や目的について丁寧な補足記事をkaoriyaさんに書いていただいたので、詳しくはこちらを参照のこと。kaoriyaさん、ありがとうございます。

3

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