Vimで日本語入力をEscでオフにする(macism / im-select / SKK)
日本語でコメントを書き終えてEscを押し、そのままjで下の行へ移ろうとしたら、カーソルが動かずに画面の下で変換が始まる。Vimを日本語で使っていれば一度は踏む事故です。ネットの解説は「im-selectを入れてInsertLeaveに1行書く」で終わるものが多いのですが、実際に手元で組んでみると、その1行が効く場面と効かない場面がはっきり分かれました。
この記事では、外部コマンドのmacismとim-selectを自動コマンドから呼ぶ方法、Vimだけが持つimactivatefuncに任せる方法、OSのIMEを使わないSKK(skkeleton)の3つを並べます。
検証環境:macOS 27.0.1(Apple Silicon)、ターミナルのVim 9.1.1752(/usr/bin/vim)とNVIM v0.12.6。入力ソースはABCとことえり(ローマ字入力)の2つ。macism v4.0.0とim-select(GitHubのmacOS/out/appleにあるバイナリ)、hyperfine 2.0.0、Deno 2.9.8、denops.vimとskkeletonは2026-10-31時点のmainです。tmuxの中でVimを起動してキーを送り、そのたびにmacismを引数なしで実行して入力ソースIDを読み直しました。下に載せている値はすべてその実行結果です。MacVimは手元に無いので、GUIでの挙動は確かめていません。
目次(9項目)
日本語のままEscを押すと何が起きるか
挿入モードでEscを押してもOSの入力ソースは日本語のままです。ノーマルモードに戻ったつもりでddと打つと、キーはVimに届く前にIMEの変換に入ります。確定すると、Vimには全角の文字列が渡ります。
確定した全角文字をVimが受け取ったときの動きを、tmuxからdd、j、:wを送って確かめました。3行のファイルは1行も消えず、カーソルは1行目のまま、&modifiedも0でした。エラーメッセージも出ません。全角のdはVimにとって未定義のキーなので、黙って捨てられます。誤操作でファイルが壊れないのは救いですが、何も起きないので原因に気づきにくく、「Vimが反応しない」と感じる正体はたいていこれです。
ターミナルのVimではiminsertだけでは効かない
Vimのヘルプには、挿入モードを抜けるたびにIMをオフにする例としてinoremap <ESC> <ESC>:set iminsert=0<CR>が載っています。これをターミナルで試しました。入力ソースを日本語にしてからVimを起動し、:set imdisable iminsert=0 imsearch=0を実行して挿入モードに入り、Escで抜けます。
| 操作 | Vim 9.1 | Neovim 0.12.6 |
|---|---|---|
| 起動直後 | Japanese | Japanese |
| imdisableを設定 | 設定できる | E519: Option not supported: imdisable |
| iminsert=0で挿入 | Japanese | Japanese |
| Escのあと | Japanese | Japanese |
| ABCでiminsert=2 | ABCのまま | ABCのまま |
どちらの方向にも入力ソースは動きませんでした。ターミナルの中のVimは、端末から送られてくるバイト列を読んでいるだけです。IMEとやり取りしているのはTerminal.appやiTerm2のほうで、Vimからは手が届きません。has('xim')も0でした。Neovimではimdisableそのものが削除されていて、:help vim_diffの削除済みオプションの一覧にimactivatefunc、imstatusfuncと並んで載っています。
ただ、Vimにはもう一段あります。imactivatefuncとimstatusfuncに関数を登録すると、ターミナルでもVimはIMの状態を問い合わせ、オン・オフが必要な場面でその関数を呼びます。関数の中でmacismを呼べば、iminsertの仕組みがそのまま入力ソースにつながります。
let s:ascii = 'com.apple.keylayout.ABC'
let s:ja = 'com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese'
function! ImActivate(active) abort
call system('macism ' . (a:active ? s:ja : s:ascii))
endfunction
function! ImStatus() abort
return trim(system('macism')) !=# s:ascii
endfunction
set imactivatefunc=ImActivate imstatusfunc=ImStatus
挿入モードで日本語に切り替えてからEscを押すと、入力ソースはABCに戻り、&iminsertは2になりました。Vimが「このバッファではIMがオンだった」と覚えた値です。もう一度iで入ると日本語に戻り、:set iminsert=0にしてから入るとABCのままでした。後で自動コマンドで組む「入る前が日本語だったときだけ戻す」動きが、Vimでは設定3行で手に入ります。
引っかかるのはimstatusfuncの呼ばれる回数です。ヘルプにも「とても頻繁に呼ばれるので速くすること」とあります。数えてみると、iで入って30文字を0.12秒間隔で打ち、Escで抜けるまでに36回呼ばれました。ほぼ1キーに1回です。そのたびにsystem()で外部コマンドを起動するので、1回20ミリ秒台の待ちが打鍵ごとに乗ります。Neovimにはこの仕組み自体が無いので、次の自動コマンドの方法を使うことになります。
macismとim-selectで入力ソースを切り替える
どちらも、引数なしで実行すると今の入力ソースIDを表示し、IDを渡すとそれに切り替えるコマンドです。macismはREADMEにbrew tap laishulu/homebrewからの手順がありますが、今回はGitHubのリリースからv4.0.0のバイナリを取って使いました。
# 今の入力ソースID
macism
# com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese
# 英字へ
macism com.apple.keylayout.ABC
im-select com.apple.keylayout.ABC
ABCとことえりを交互に2往復ずつ切り替え、0.5秒おいてからmacism、im-select、TIS APIを直接呼ぶ小さなSwiftのプログラムの3つで読み直しました。8回とも、3つの読み直しはそろって切り替え先のIDを返しています。読み直しの上では、どちらのコマンドも確実に切り替わっていました。
違いが出たのは、存在しないIDを渡したときです。macismはInput source com.example.nope does not exist!と表示して終了コード0、im-selectは何も表示せずに終了コード1でした。RomajiTypingのように末尾の.Japaneseを落としたIDでもmacismは0で終わるので、設定を書いた直後はIDを手で打って確かめておくと安全です。
macismのREADMEが強調しているのは、日本語や中国語の入力ソースへ切り替えたとき、メニューバーのアイコンは変わるのに入力は前のままになる問題です。ソースのTSMUtils.swiftは、キーボードフォーカスを持たないプロセスがTISSelectInputSourceを呼ぶと、フォーカスのあるアプリで入力メソッドが直後に外れる、と原因を説明しています。macismはCJKVの入力ソースに限って、フォーカスのあるアプリに切り替えを頼む経路を先に試し、だめなら一時的なウィンドウを出して150ミリ秒待ちます。im-selectのmain.mはTISSelectInputSourceを1回呼ぶだけです。
注意したいのは、この問題が状態の読み直しでは見えないことです。壊れた状態でも、TISが返すIDは切り替え先になっています。今回の8回でも読み直しの結果は一致しましたが、それはim-selectが安全だという意味になりません。日本語の入力欄に実際にキーを打ち込んで追従を確かめる手順はこの検証では取っておらず、問題そのものは再現させていません。また、英字への切り替えはmacismでもTISSelectInputSourceだけの経路を通ります。差が出るのは日本語へ戻す側だけです。
InsertLeaveで英字にして、InsertEnterで戻す
Neovimでも使える方法として、挿入モードを抜けるときに今の入力ソースを覚えてABCにし、入るときに覚えた値が日本語なら戻す自動コマンドを書きます。Vim scriptではjob_start()で切り替えを非同期にしています。
let s:im = 'macism'
let s:ascii = 'com.apple.keylayout.ABC'
let s:prev = ''
function! s:Leave() abort
let s:prev = trim(system(s:im))
if s:prev !=# s:ascii
call job_start([s:im, s:ascii])
endif
endfunction
function! s:Enter() abort
if s:prev !=# '' && s:prev !=# s:ascii
call job_start([s:im, s:prev])
endif
endfunction
augroup ime_switch
autocmd!
autocmd InsertLeave * call s:Leave()
autocmd InsertEnter * call s:Enter()
augroup END
Neovimではvim.system()を使います。:wait()を付けると結果を待ち、付けなければ起動だけして戻ります。
local im = 'macism'
local ascii = 'com.apple.keylayout.ABC'
local prev = nil
local group = vim.api.nvim_create_augroup('ime_switch', {})
vim.api.nvim_create_autocmd('InsertLeave', {
group = group,
callback = function()
prev = vim.trim(vim.system({ im }):wait().stdout)
if prev ~= ascii then
vim.system({ im, ascii })
end
end,
})
vim.api.nvim_create_autocmd('InsertEnter', {
group = group,
callback = function()
if prev and prev ~= ascii then
vim.system({ im, prev })
end
end,
})
両方に発火のたびに記録を残す1行を足し、Escの代わりになるキーの記事で扱っているinoremap jk <Esc>も入れて、抜け方を変えながら入力ソースを読みました。VimとNeovimで結果は同じでした。
| 操作 | 直後の入力ソース | 発火した自動コマンド |
|---|---|---|
| 日本語でEsc | ABC | InsertLeave |
| i で入り直す | Japanese | InsertEnter(戻す) |
| jk で抜ける | ABC | InsertLeave |
| o で入る | Japanese | InsertEnter(戻す) |
| Ctrl-c で抜ける | Japanese のまま | なし |
| Ctrl-[ で抜ける | ABC | InsertLeave |
jkは<Esc>に展開されるので、InsertLeaveは普通に発火します。抜けるのはCtrl-cだけです。ヘルプのi_CTRL-Cにあるとおり、Ctrl-cで挿入モードを抜けるとInsertLeaveが起きず、入力ソースは日本語のまま残りました。Ctrl-cで抜ける癖があるならinoremap <C-c> <Esc>を足します。Vimで確かめると、足したあとはCtrl-cでもABCに戻りました。
Escのたびに外部コマンドを呼ぶ遅さ
外部コマンド1回ぶんの時間をhyperfineで測りました(-Nでシェルを挟まず、30回または20回の平均)。
| コマンド | macism | im-select |
|---|---|---|
| 今のIDを読む | 8.6 ms | 35.6 ms |
| ABCへ(すでにABC) | 8.2 ms | 35.5 ms |
| ABCから日本語へ | 25.0 ms | 47.1 ms |
次に、Vimの中から呼んだときの戻りの速さをreltime()とNeovimのvim.uv.hrtime()で20回ずつ測りました。
| 呼び方 | 1回あたり |
|---|---|
Vim system('macism') | 23.6 ms |
Vim job_start()(待たない) | 0.9 ms |
Neovim vim.fn.system() | 13.4 ms |
Neovim vim.system():wait() | 8.4 ms |
Neovim vim.system()(待たない) | 0.6 ms |
Vimのsystem()がhyperfineの値より15ミリ秒ほど遅いのは、'shell'(ここではzsh)を一度起動してからコマンドを実行するからです。前の節の設定で、抜けるときの読み取りだけを同期にし、切り替えをjob_start()や待たないvim.system()にしているのはこのためです。読み取りは結果が要るので待つしかありませんが、切り替えは待つ理由がありません。Esc1回あたりの待ちは、Vimで20ミリ秒台、Neovimで10ミリ秒弱になります。im-selectに替えると読み取りだけで35ミリ秒を超えます。
測っている途中で、EscからABCに変わるまでが1秒近く遅れる場面がありました。-Nu NONEのVimはttimeoutがオフで、Escのあとに矢印キーの続きが来ないかtimeoutlenの1000ミリ秒待ってから挿入モードを抜けるためです。0.8秒後に読むとまだ日本語で、1.3秒後にABCでした。macOS付属の/usr/bin/vimも、/usr/share/vim/vimrcがskip_defaults_vimを立てるので、vimrcが無いと[&ttimeout, &ttimeoutlen, &timeoutlen]は[0, -1, 1000]でした。set ttimeout ttimeoutlen=50を入れると遅れは消えます。Neovimは最初からttimeoutlen=50です。
IMEを使わずVimの中で変換するSKK(skkeleton)
ここまでの方法は、どれもOSの入力ソースを外から動かしています。SKKは発想が逆で、OSの入力ソースはずっとABCに置いたまま、かな入力と漢字変換をVimのプラグインが受け持ちます。skkeletonはDenoで動くdenops.vimの上に作られたSKKで、VimとNeovimの両方で動きます。用意するものは、Deno、denops.vim、skkeleton、辞書のSKK-JISYO.Lの4つです。プラグインの入れ方はVimにプラグインを入れる方法と同じで、今回は標準パッケージのpack/*/startに2つを置きました。
function! s:skkeleton_init() abort
call skkeleton#config({
\ 'globalDictionaries': ['~/.skk/SKK-JISYO.L'],
\ })
endfunction
augroup skkeleton_init
autocmd!
autocmd User skkeleton-initialize-pre call s:skkeleton_init()
augroup END
imap <C-j> <Plug>(skkeleton-toggle)
cmap <C-j> <Plug>(skkeleton-toggle)
Neovimでは同じことをLuaで書きます。
vim.api.nvim_create_autocmd('User', {
pattern = 'skkeleton-initialize-pre',
callback = function()
vim.fn['skkeleton#config']({
globalDictionaries = { '~/.skk/SKK-JISYO.L' },
})
end,
})
vim.keymap.set({ 'i', 'c' }, '<C-j>', '<Plug>(skkeleton-toggle)')
挿入モードでCtrl-jを押してからNihongoと打つと▽にほんご、Spaceで▼日本語になりました。SKKは大文字で変換の始まりを示すのが特徴で、KaKuのように途中にも大文字を入れると送りがなの位置になり、▼掛くが出ました。NeovimではKanjiのあとSpaceを押すたびに▼漢字、▼幹事と候補が進み、xで▼漢字に戻りました。辞書のSKK-JISYO.LはEUC-JPですが、エンコーディングを指定しなくても変換できています。
この方法の利点は、Escの問題がそもそも起きないことです。Escで抜けたあとも入力ソースはABCのままで、skkeleton#mode()は空文字(無効)を返しました。oで入り直してabcと打つと、そのまま英字が入りました。既定では挿入モードを抜けるとskkeletonがオフになり、keepStateを有効にすると前回の状態を持ち越します。外部コマンドを呼ばないので、前の節の待ち時間もありません。
代わりに、変換の操作をSKKの流儀で覚え直す必要があります。普段のIMEは文節を自動で区切りますが、SKKはどこから変換するかを自分で大文字で示します。Denoの実行環境が常に裏で動くことも、手元の環境によっては負担です。また、OSの入力ソースが日本語のままだと、キーは最初の節のとおりskkeletonに届く前にことえりの変換に入るので、Vimを使う間はABCに固定しておく前提になります。ダイグラフで特殊記号を入力する方法もIMEを通さない入力ですが、あちらは記号を1文字ずつ入れる仕組みで、日本語の文章を書く用途とは別物です。
どれを選ぶか
| 方法 | 使える環境 | Escごとの待ち | 向いている人 |
|---|---|---|---|
| imactivatefunc | Vimのみ | 打鍵ごとに20 ms台 | Vimだけを使い、設定を短くしたい |
| 自動コマンド+macism | Vim / Neovim | 約10〜20 ms | 普段のIMEをそのまま使いたい |
| 自動コマンド+im-select | Vim / Neovim | 35 ms以上 | すでにim-selectを入れている |
| skkeleton | Vim / Neovim | なし | 変換の操作を覚え直してよい |
普段どおりことえりやGoogle日本語入力で書きたいなら、自動コマンドとmacismの組み合わせがいちばん素直です。英字へ戻すだけで十分なら、InsertEnterの自動コマンドを書かずにInsertLeaveだけにする手もあります。日本語へ切り替える場面が無くなるので、READMEとソースの説明に従えば、どちらのコマンドでも問題の経路を通りません。
コメントや文書をVimの中でまとめて日本語で書く時間が長い人には、skkeletonを試す価値があります。VSCodeからVimに移行する方法の記事で、移行の最初に効くのは日本語入力の切り替えだと書きましたが、SKKはその問題を切り替えではなく入力の置き場所を変えることで消します。
検証を始めたとき、2つ目の節の見出しは「ターミナルのVimではiminsertが効かない」で終わる予定でした。実際にiminsertを何度変えても入力ソースは動かず、予想どおりだと思っていたところで、念のためimactivatefuncに記録だけ残す関数を入れたらact=1が出てきました。ターミナルのVimはIMEに触れないので呼ばれもしないだろう、と決めつけていたのが外れた形です。Neovimで同じことをしようとしてE519を見たのはその後で、同じ:help iminsertを読んでいても、手元のどちらで打ったかで書けることが変わると実感しました。
まとめ
ターミナルのVimとNeovimは、iminsertやimdisableだけではOSの入力ソースを動かせません。Escで英字に戻すには、外部コマンドをInsertLeaveから呼ぶのが共通の解で、速さと日本語への戻し方の両面でmacismが有利でした。Vimならimactivatefuncとimstatusfuncに任せる道もあります。
自動コマンドで組むなら、Ctrl-cではInsertLeaveが起きないことと、ttimeoutがオフだと切り替えが1秒遅れて見えることを押さえておきます。切り替えそのものをやめたいなら、入力ソースをABCに置いたまま変換するskkeletonがあります。