不完全にしておよそ役に立たない VSCodeVim の設定の話

こんにちはNです。

ネットオン開発 Sec.では(特に決まっている訳ではないですが)基本的に開発には VSCode を使っています。
私もエディタとして VSCode を使用しているのですが、以前はずっと Vim を使用していたこともあって、Vim のキーバインドで VSCode を操作できる VSCodeVim という拡張機能をインストールしています。

便利に使っているのですが、本物の Vim とまったく同じように使用できるわけではありません。
そこで今回は VSCodeVim を使用するにあたって知っているとお得な設定方法を紹介したいと思います。

VSCodeVim から VSCode の機能を呼び出す

まず一つ目ですが、Vim のキーバインドに VSCode の機能を割り当てることができることが大きなメリットではないかと思います。
例えば、以下の例では インサートモード の場合に Ctrl + k で「カーソル位置から行末までの文字列を切り取る」というコマンドを割り当てています。

"vim.insertModeKeyBindingsNonRecursive": [
  {
    "before": ["<C-k>"],
    "commands": ["cursorEndSelect", "editor.action.clipboardCutAction"]
  }
]

vim.insertModeKeyBindingsNonRecursive は、インサートモードでのキーバインドを置き換えるための設定項目です。
NonRecursive となっているので、再帰的な置き換えは行いません。
ここで、before に書かれているのが置き換え対象のキーバインドで、commands に書かれているのが置き換え後の実行コマンドです。
commands と複数形になっていることから分かるように、複数のコマンドを既述することができます。
cursorEndSelect も editor.action.clipboardCutAction も VSCode の既存コマンドですが、それに限らず外部拡張機能の API 呼び出しを割り当てることも可能です。

VSCodeVim から 他拡張機能のコマンドを呼び出す

外部拡張機能の呼び出し例としては以下のようになります。
この設定では space と L を続けて押下することで、ビジュアルモードで選択した対象の変数を console.log で出力することができます。
turboConsoleLog は、選択した対象を console.log で出力する拡張機能です。
このように、任意の拡張機能の機能呼び出しを、既定のキーバインドやコマンドパレットを経由せずに実行することができるようになります。

"vim.visualModeKeyBindingsNonRecursive": [
  {
    "before": ["<space>", "l"],
    "commands": ["turboConsoleLog.displayLogMessage"],
    "when": ["editorTextFocus"]
  }
]

つまり、commands に任意のコマンドを記述することで、Vim キーバインドからマクロのようにコマンドの実行をまとめて行うことができます。
(まあ、かといってそれがそんなに便利か?といわれるとそんなにでもない気がしますが、たまに便利です。)

任意のキーストロークを割り当てる

commands で API 呼び出しを行うだけでなく、既存のキーストロークを呼び出すこともできます。
以下の例は、ビジュアルモードで Ctrl + l を押下すると、g b が呼び出されるという設定です。
g b は VSCodeVim からマルチカーソルの「カーソル追加」を呼び出すコマンドです。
単にデフォルトの g b を使用してもいいのですが、キーストローク的に連打しにくいため私は Ctrl + L を割り当てています。

"vim.visualModeKeyBindingsNonRecursive": [
    {
      "before": ["<C-l>"],
      "after": ["g", "b"]
    }
]

ただ、正直言って VSCodeVim 経由でマルチカーソルを使用するのはあまり使い勝手がいい感じはしません。
そもそも Vim の矩形選択(Ctrl + V)と近い動作をするのですが、かといってそれとまったく同じというわけではなく、経験からくる直感に反する動作をするときがあります。
もう素直に Ctrl + V を使うか、検索/置換を使った方がいいのではと思うこともあります。

上記の2つの記述方法を組み合わせて、既存のコマンドと任意のキーストロークを組み合わせて実行することも可能です。

任意のコマンドと任意のキーストロークを割り当てる

以下の例では、ノーマルモードで space と t を続けて押下すると、カーソル位置の変数の型をコピーして、一単語カーソルを進めた後でインサートモードに入り、「:」を入力した後でノーマルモードに戻ってクリップボードの中身をペーストするという設定です。
extension.copyHoverType は Copy Hover Typeという拡張機能のコマンドで、VSCode のホバー機能で表示される変数の型をコピーするものです。
変数の型情報を明示的に書きたいときに便利かもしれません。

"vim.normalModeKeyBindingsNonRecursive": [
  {
    "before": ["<space>", "t"],
    "commands": [
      {
        "command": "extension.copyHoverType"
      },
      {
        "command": "vim.remap",
        "args": {
          "after": ["w", "i", ":", "", "Esc", "p"]
        }
      }
    ]
  },
]

上記のような記法でコマンドとキーストロークを組み合わせる場合に注意すべき点として、これらのコマンドが(多分)非同期で実行されるということが挙げられます。
つまり、コマンドの実行順序が保証されないということです。
以下のように、copyHoverType で変数の型情報をコピーしたうえで、それを変数の後ろに貼り付けた後、 copyRelativeFilePath コマンドでファイルの相対パスをコピーする設定にしたとします。(何のために?)
これを実行すると、変数の後ろに型ではなくファイルの相対パスが貼り付けられてしまいます。
つまり、2つ目に指定した vim.remap の実行前に copyRelativeFilePath が実行されてしまうということです。
この点についてあまり細かく動作を調査していなので断定的なことは言えませんが、実行順序の保証が必要ない場合においては有用な記述方法かもしれません。(そもそも実用性がある例があまり思いつかなかったです)

  "vim.normalModeKeyBindingsNonRecursive": [
    {
      "before": ["<space>", "t"],
      "commands": [
        {
          "command": "extension.copyHoverType"
        },
        {
          "command": "vim.remap",
          "args": {
            "after": ["w", "i", ":", "", "Esc", "p"]
          }
        },
        {
          "command": "copyRelativeFilePath"
        }
      ]
    },
  ]

なお、上記の設定は全て settings.json に記述する形式ですが、VSCodeVim には settings.json 以外にも設定を記述する方法があります。
それが keybindings.json です。

keybindings.json に設定を書く場合

Alt キーと組み合わせたキーバインドを設定する場合、keybindings.json に書く必要があります。
以下のように、when 式に vim.mode との比較を書くことで、ノーマルモードの時だけ有効になるキーバインドを設定することができます。

{
    "key": "alt+s",
    "command": "workbench.action.splitEditor",
    "when": "inputFocus && vim.mode == 'Normal'",
}

また、keybindings.json に記述する場合、Vim のモードをより詳細に指定することができます。
以下の例では、ビジュアルモード、行選択モード、矩形選択モードの場合のリマッピングを行っています。
矩形選択モードの場合だけ特定のキーバインドを設定したいときなどは keybindings.json に書く必要があります。

{
  "key": "ctrl+r",
  "command": "vim.remap",
  "when": "inputFocus && vim.mode == 'Visual' || editorTextFocus && vim.mode == 'VisualLine' || editorTextFocus && vim.mode == 'VisualBlock'",
  "args": {
    "after": ["<Esc>", "<C-r>"]
  }
}

これとは別形式で、単に特定のキーストロークに置き換えたい場合は、以下のようにすることで実現できます。
以下の例では、インサートモードで Ctrl + d を押下すると、Delete が呼び出されるという設定です。
settings.json での記法における before / after に相当します。

{
  "key": "ctrl+d",
  "command": "vim.remap",
  "when": "inputFocus && vim.mode == 'Insert'",
  "args": {
    "after": ["<Delete>"]
  }
}

私の場合、最初に触ったエディタが Emacs であったこともあり、インサートモード では Emacs のキーバインドでカーソル移動を行うようにしています。
つまり、Ctrl + p/n/b/f で上下左右、Ctrl + a/e で行頭/行末に移動、Ctrl + d でデリート、Ctrl + h でバックスペースといった感じです。
ノーマルモード では HJKL でカーソル移動するわけですから混乱するのではと思われるかもしれませんが、慣れです。
このようにすると、日本語の文章を入力する際にノーマルモードに戻る回数を減らせるという利点があります。

IME の設定について

Vim で日本語入力する場合、IME の状態と Vim のモードを同時に意識する必要があり、単に IME オフの状態で入力するのと比べて一段階面倒が増えます。
これを解消する方法として、 ノーマルモード に戻ったら自動的に IME がオフになるようにする設定もあるのですが、VSCodeVim ではそのような設定は(実は)上手く動きません。
公式では im-select を用いた方法が紹介されているのですが、これはキーボードレイアウトの切り替えであって、IME の切り替えではないので、日本語入力を行う際にはあまり役に立ちません。
以下の Qiita の記事にあるような方法を使うのが一般的にだと思います。
Win 版の VS Code+VSCodeVim でノーマルモードに戻った時に IME を半角英数入力にする

まとめ

Vim や NVim から VSCode に移行してくる人はそれなりにいると思いますが、既存の .vimrc とか init.lua の動作を完全に対応させることはできなさそうなので、もう新しい環境になじんだ方がいいんじゃないかと思います。
少しでもみなさんのお役に立てれば幸いです。