2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-06-05

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()という、現在のキーボードの状態を配列に格納してくれる関数がありました。

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

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')

※以下のサイトを参考にさせていただきました

実行結果を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
2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?