Vimで<Tab>と<C-i>のマッピングを使い分けられるようになるまで（Windows）

Vim使いの誰かの参考になればと、初めて筆（キーボード）を執りました。

# 直面した問題
私はvimで以下のマッピングを定義して、しばらくの間は特に不自由なく使っていました。

```VimScript
inoremap <C-i> <Esc>
```

しかしある時、インサートモードでTabキーを押してもTabが入らないことに気が付きました。

不思議に思い調べてみると、どうやら

* \<Tab>と\<C-i>のキーコードは同一である
* よって\<C-i>のマッピングを定義すると、\<Tab>キーを押した時にもその機能が呼び出される
* 結果的に元々の\<Tab>の機能を上書きしてしまう

ということらしいのです。

私が調べた所では、LinuxとMacOSでは既にこの問題の解決方法が編み出されていました。

* [TabとCtrl-iどちらを入力されたか区別する(Linux限定)]( https://qiita.com/norio13/items/9c05412796a7dea5cd91)
* [vimでtabと<C-i>を使い分ける(mac)](https://qiita.com/zebult/items/e330e6c56b95301a330f)

しかし、私の使用環境はWindows。自力で探すしかありません。

# 解決方法
結論から言えば、解決方法は簡単でした。

当然ですが、\<Tab>と\<C-i>では、押すキーが異なります。

であれば、\<Tab>や\<C-i>が押された時、そのキーコードではなく、キーボードのどのキーが押されているかで判別すれば良かったのです。

私が目をつけたのは、Linux版の先駆者様が使っていた、Pythonのctypesというライブラリでした。

[ctypes --- Pythonのための外部関数ライブラリ](https://docs.python.org/ja/3/library/ctypes.html)

簡単に言えば、PythonからCのライブラリを操作できるライブラリです。

これを使って呼び出せる関数の中に、`GetKeyboardState()`という、現在のキーボードの状態を配列に格納してくれる関数がありました。

それを使って書いたコードがこちらです。

```Python:tab_ctrli.py
# 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')
```
※以下のサイトを参考にさせていただきました

* [Python: ctypesパターン集](https://qiita.com/everylittle/items/6e18ba23f38502c18f3e)
* [ctypes ~pythonでCをイジる(Macで)~](http://curlnoodle.hatenablog.com/entry/2013/12/30/221858)
* [キーボードの状態を取得する（WindowsAPI）](http://rina.jpn.ph/~rance/directx8/03/03.html)

実行結果をvimのグローバル変数に入れて返しています。
これはvimとPythonの処理の記述をできるだけ切り離したかったからです。

後はこのスクリプトをVimScriptから実行させて、変数の値で分岐処理をすれば解決…………のはずでした。

# 新たなる問題
以下のコードを御覧下さい。これが、私が最初に書いた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日本語ドキュメント](https://vim-jp.org/vimdoc-ja/map.html)）1.2項が詳しいのですが、要するにキーではなく、ただの文字列だと扱われてしまうのです。

(2019/6/11追記)
先述のように、\<Tab>と\<C-i>のキーコードは同一のため、上記コードのように\<Tab>と\<C-i>それぞれのマッピングを定義しても、vimからは同一のマッピングだと認識されます。なので、どちらかのキーのマッピングは不要です。

```VimScript
"修正例
inoremap <expr> <Tab> <SID>TabctrliMain("i")
nnoremap <expr> <Tab> <SID>TabctrliMain("n")
```

# 新たなる問題の解決方法
この問題の解決方法は、**キーをUnicodeで指定する**ことでした（こちらに関しても上に載せたリンクに記載されています）。

それを反映させたのが、以下のコードです。

```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"
      " <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