Vim使いの誰かの参考になればと、初めて筆(キーボード)を執りました。
直面した問題
私はvimで以下のマッピングを定義して、しばらくの間は特に不自由なく使っていました。
inoremap <C-i> <Esc>
しかしある時、インサートモードでTabキーを押してもTabが入らないことに気が付きました。
不思議に思い調べてみると、どうやら
- <Tab>と<C-i>のキーコードは同一である
- よって<C-i>のマッピングを定義すると、<Tab>キーを押した時にもその機能が呼び出される
- 結果的に元々の<Tab>の機能を上書きしてしまう
ということらしいのです。
私が調べた所では、LinuxとMacOSでは既にこの問題の解決方法が編み出されていました。
しかし、私の使用環境はWindows。自力で探すしかありません。
解決方法
結論から言えば、解決方法は簡単でした。
当然ですが、<Tab>と<C-i>では、押すキーが異なります。
であれば、<Tab>や<C-i>が押された時、そのキーコードではなく、キーボードのどのキーが押されているかで判別すれば良かったのです。
私が目をつけたのは、Linux版の先駆者様が使っていた、Pythonのctypesというライブラリでした。
ctypes --- Pythonのための外部関数ライブラリ
簡単に言えば、PythonからCのライブラリを操作できるライブラリです。
これを使って呼び出せる関数の中に、GetKeyboardState()
という、現在のキーボードの状態を配列に格納してくれる関数がありました。
それを使って書いたコードがこちらです。
# coding: UTF-8
import ctypes
import vim
user32 = ctypes.WinDLL('user32')
# Cのbyte型配列を作る
key_tbl = (ctypes.c_byte*256)()
### 戻り値・引数の型を指定する
user32.GetKeyboardState.restype = None
# ※最後のカンマは必須
user32.GetKeyboardState.argtypes = (ctypes.POINTER(ctypes.c_byte),)
# キーボードの状態を配列に格納する
user32.GetKeyboardState(key_tbl)
# <Tab>が押されていた場合
if key_tbl[0x09]&0x80:
vim.command(':let g:tabctrli_tab_pushed = v:true')
# <C-i>が押されていた場合
elif key_tbl[0x11]&0x80 and key_tbl[0x49]&0x80:
vim.command(':let g:tabctrli_tab_pushed = v:false')
※以下のサイトを参考にさせていただきました
実行結果をvimのグローバル変数に入れて返しています。
これはvimとPythonの処理の記述をできるだけ切り離したかったからです。
後はこのスクリプトをVimScriptから実行させて、変数の値で分岐処理をすれば解決…………のはずでした。
新たなる問題
以下のコードを御覧下さい。これが、私が最初に書いたVimScriptです。
let g:tabctrli_tab_pushed = v:null
fu! s:TabctrliMain(mode)
"system()などで実行すると、vimモジュールが認識されない
pyfile C:\gvim\vim80-kaoriya-win64\cmd\tab_ctrli.py
if g:tabctrli_tab_pushed
if a:mode=="i"
return "<Tab>"
elseif a:mode=="n"
return "gt"
endif
else
if a:mode=="i"
return "<Esc>"
elseif a:mode=="n"
return "gT"
endif
endif
endf
inoremap <expr> <C-i> <SID>TabctrliMain("i")
inoremap <expr> <Tab> <SID>TabctrliMain("i")
nnoremap <expr> <C-i> <SID>TabctrliMain("n")
nnoremap <expr> <Tab> <SID>TabctrliMain("n")
既にお気づきの方もいらっしゃるかもしれませんが、下部で定義されているインサートモードのマッピングは、どちらも予期した動作をしません(ノーマルモードのマッピングは動きます)。
例えば<C-i>を押した時、
inoremap <expr> <C-i> <SID>TabctrliMain("i")
というマッピングは関数の実行後、
inoremap <C-i> "<Esc>"
と解釈され、実行されます。
そしてその結果、カーソル位置に"<Esc>"という文字列が挿入されます。
これに関してはこちらの(map - Vim日本語ドキュメント)1.2項が詳しいのですが、要するにキーではなく、ただの文字列だと扱われてしまうのです。
(2019/6/11追記)
先述のように、<Tab>と<C-i>のキーコードは同一のため、上記コードのように<Tab>と<C-i>それぞれのマッピングを定義しても、vimからは同一のマッピングだと認識されます。なので、どちらかのキーのマッピングは不要です。
"修正例
inoremap <expr> <Tab> <SID>TabctrliMain("i")
nnoremap <expr> <Tab> <SID>TabctrliMain("n")
新たなる問題の解決方法
この問題の解決方法は、キーをUnicodeで指定することでした(こちらに関しても上に載せたリンクに記載されています)。
それを反映させたのが、以下のコードです。
let g:tabctrli_tab_pushed = v:null
fu! s:TabctrliMain(mode)
"system()などで実行すると、vimモジュールが認識されない
pyfile C:\gvim\vim80-kaoriya-win64\cmd\tab_ctrli.py
if g:tabctrli_tab_pushed
if a:mode=="i"
" <Tab>のUnicode
return "\u0009"
elseif a:mode=="n"
return "gt"
endif
else
if a:mode=="i"
" <ESC>のUnicode
return "\u001B"
elseif a:mode=="n"
return "gT"
endif
endif
endf
inoremap <expr> <C-i> <SID>TabctrliMain("i")
inoremap <expr> <Tab> <SID>TabctrliMain("i")
nnoremap <expr> <C-i> <SID>TabctrliMain("n")
nnoremap <expr> <Tab> <SID>TabctrliMain("n")
これで本当の解決と相成りました。
インサートモードで<C-i>を押すと脱出でき、<Tab>を押すとちゃんとTabが入るようになりました。
動作の遅延もほとんどありません。
動作環境
- Windows10
- kaoriya版GVim(ver. 8.0.596)
- Python3.7.0