VSCodeのキーバインド拡張を作ったので、その勘所を紹介

# [Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)
こちらのVSCode拡張を作っています:

[![Awesome Emacs Keymap logo](https://github.com/tuttieee/vscode-emacs-mcx/blob/master/images/icon.png?raw=true)](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)

Emacsキーバインド拡張です。
GitHub: https://github.com/tuttieee/vscode-emacs-mcx

同Advent Calendarの[VSCodeのEmacsキーバインド拡張まとめ](https://qiita.com/kurun_pan/items/507517c3f569b080abe2)でも紹介いただきました。ありがとうございます…！

既存のEmacsキーバインド拡張はキーバインドの再現性が低かったり、細かいところで使い勝手が悪かったりで正直使い物にならないものばかりだったので、自分で作り直しました。

おそらくVSCodeのEmacsキーバインド拡張は[Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=hiro-sun.vscode-emacs)が最古参で、これをForkした[Emacs Friendly Keymap](https://marketplace.visualstudio.com/items?itemName=lfs.vscode-emacs-friendly)が登場してからはこちらがデファクトだったように思います。
他にもいくつかありますが、[Emacs Friendly Keymap](https://marketplace.visualstudio.com/items?itemName=lfs.vscode-emacs-friendly)も含め、それらはほとんどが[Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=hiro-sun.vscode-emacs)をベースにForkしたものです。
（この辺りの事情については[VSCode の Emacs keymaps 拡張についてのメモ](https://ganaware.hatenadiary.jp/entry/2019/01/02/235708)が非常によくまとまっています。私の[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)も載せていただきました。ありがとうございます。）

ところが、これらの拡張はFork元のコードベースの設計に問題があり、ここからのForkでは上記問題を解決できないと判断して、スクラッチでEmacsキーバインド拡張を作ることにしました（本文で後述しますが、マルチカーソルサポートの不足が最初で最大の動機です）。

（ちなみに、VimもAtomもSublimeもMS公式のキーバインドがあるのに、Emacsは公式提供がなく冷遇されています…）

本稿では、ゼロからVSCode用キーバインド拡張を作ってみて得られたTipsをつらつらと羅列していきます。
体系的な入門記事ではない点、ご承知おきください。

# Step0: まず公式ドキュメントを読み、チュートリアルをやる
まず最低限、 https://code.visualstudio.com/api で自分が作りたいもの（私の場合はキーバインド拡張）に関連する部分にざっと目を通します。
必要なことは（ほぼ）全てここに書いてあります（例外あり。後述。）。
全部を読む必要はなくて、例えば私はカラーテーマを作りたいわけではないのでそのページは飛ばしたりしました。

# MS公式のキーバインド拡張のコードを参考にする

作り始めたら、MS公式が開発しているキーバインド拡張のコードを参考にします。
公式なだけあって、これらの拡張の開発者はVSCode Extension APIに精通しており、各種機能を実装するためのスマートな正解が詰まっています。
また、APIリファレンスに載っていないAPIが使われていたりするので、ドキュメントを読むだけでは得られない発見があります。

私が開発中に参考にしたRepositoryは主に以下の2つです。

* [VSCodeVim/Vim](https://github.com/VSCodeVim/Vim)
    * MS公式のVimキーバインドです
* [microsoft/vscode-extension-samples](https://github.com/Microsoft/vscode-extension-samples)
  * 特に[vim-sample](https://github.com/microsoft/vscode-extension-samples/tree/master/vim-sample)

本稿の以降の内容は、ほとんどこれらのリポジトリを読んで得られたTipsの羅列です。

# VS Code APIにおける、各種オブジェクトの関係を知る
VS Code APIが提供する各種オブジェクトについては[References - VS Code API](https://code.visualstudio.com/api/references/vscode-api)に一通り書かれているのですが、これらが体系的に説明されたドキュメントが見当たりません（私がこの拡張を作った2019年1月当時。今はあるかも。）。

とりあえず、本稿の説明のために、以下を抜粋して説明します。

* [`TextDocument`](https://code.visualstudio.com/api/references/vscode-api#TextDocument)
    * 文書１つを表すオブジェクトです。例えば、ファイルを１個開くと、対応する`TextDocument`が１個できます。1個新規作成しても、対応する`TextDocument`が１個できます。
* [`TextEditor`](https://code.visualstudio.com/api/references/vscode-api#TextEditor)
    * テキストエディタ１個を表すオブジェクトです。ある`TextDocument`1個に複数の`TextEditor`がアタッチされる、ということがあり得ます（同じファイルを複数のタブで開くなど）
* [`window`](https://code.visualstudio.com/api/references/vscode-api#window)
    * 現在のVSCodeのウィンドウを表すオブジェクト（正確には名前空間）です
    * そのウィンドウ内でアクティブな`TextEditor`を表す`window.activeTextEditor`などのプロパティを持ちます
* [`Selection`](https://code.visualstudio.com/api/references/vscode-api#Selection)
    * カーソルを表現します。`anchor`, `active`プロパティを持ちます。通常時はこの２つは一致しますが、状態選択時には一致せず、キー操作可能な方のカーソルが`active`、固定されたカーソルが`anchor`となります。これら２つに挟まれた領域が選択領域となります。

その他にも様々なAPIが公開されていますが、特にキーバインド拡張を作る際には↑あたりが重要になると思うので、上記リンクを基点にしていろいろ眺めてみると良いでしょう。

# 状態を持つキーバインド拡張は、Editorと1対1対応するControllerオブジェクトを作る
状態を持つキーバインド拡張とは、例えば

* Vimのモード切り替え
* Emacsの選択モード

などです。
あるキーバインドが特定のコマンドを発行して終わり（＝ステートレス）、ではなく、継続的に状態を保持するような機能のことです。

このような状態を持ったキーバインド拡張を作るときは、その状態を保持する`Controller`クラスを作るのが自然かと思います（Controllerという名前が自然かはわかりませんが、[vim-sample](https://github.com/microsoft/vscode-extension-samples/tree/master/vim-sample)では[そのように名付けられている](https://github.com/microsoft/vscode-extension-samples/blob/master/vim-sample/src/controller.ts)ので、ここではControllerと呼びます。同種のクラスは、[VSCodeVIM](https://github.com/VSCodeVim/Vim)では[ModeHandler](https://github.com/VSCodeVim/Vim/blob/master/src/mode/modeHandler.ts)、[AwesomeEmacsKeymap](https://github.com/tuttieee/vscode-emacs-mcx)では[EmacsEmulator](https://github.com/tuttieee/vscode-emacs-mcx/blob/master/src/emulator.ts)という名前になっています）。

```typescript
class Controller {
    private _currentMode: Mode;  // 現在のモード

    ...略...
}
```

ここで、`Controller`のインスタンスはシングルトンにせず、VSCodeの"Editor"と１対１対応するようにします。
例えばシングルトンにしてしまうと、あるタブで選択モードに入ったまま別のタブに移動すると、そちらでも選択モードが続いたりといったことになります。
また、ここでVSCodeの"Editor"とは、VSCodeのタブ１つに対応するエンティティ、くらいの意味で使っています。

素朴に考えると、以下のようなObjectを作って"Editor"と`Controller`の対応を保持しておき、

```typescript
const editorControllerMap: {[key: Editor]: Controller} = {};
```

必要に応じて

```typescript
const activeController = activeEditor && editorControllerMap[activeEditor];
```

のようにして、現在使用中のタブに対応する`Controller`インスタンスを得るような実装にすれば良いと思うでしょう。

ところが実は、VSCodeはここでいう"Editor"の概念に綺麗に対応したオブジェクトを持っておらず、実装に困ります。
~~`TextEditor`オブジェクトが近いのですが、これはVSCode内部の都合で適宜GCされることがあり、永続的なキーとして使えないため、ここでの用途には利用できません。~~
(2022/11/15 Update): 
まさに`TextEditor`オブジェクトをこの用途に利用できるようです: Ref -> https://github.com/microsoft/vscode/issues/40652#issuecomment-354981592

~~そこで、[VSCodeVim/Vim](https://github.com/VSCodeVim/Vim)の[EditorIdentity](https://github.com/VSCodeVim/Vim/blob/5a87b6a84fe25ef7ea4b7ccc40d5147c5cc9d903/src/editorIdentity.ts)と[modeHandlerMap](https://github.com/VSCodeVim/Vim/blob/2e3317b4a55963655d51072aacde796e7f58a2e4/src/mode/modeHandlerMap.ts)を参考にしましょう。
`EditorIdentity`は`TextEditor`をMapのキーとして使える値にラップし、`modeHandlerMap`は`EditorIdentity`をキーとしてMapを保持して`TextEditor`から`Controller`（VSCodeVimでは`ModeHandler`）を返します。~~（https://github.com/VSCodeVim/Vim/blob/fa9deaf3f58a7125e773111005c88d2ec10f768e/src/mode/modeHandlerMap.ts#L11）
私の[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)はこのコードを[コピペして使っています](https://github.com/tuttieee/vscode-emacs-mcx/blob/master/src/editorIdentity.ts)

（`EditorIdentity`の中身を読むと単純なファイル名の比較なのですが、MS公式のVSCodeVimがこうやっているので、これで良いのでしょう。）

(2022/11/15 Update): 
現在では `document.uri` をキーに使っているようです

**2020-05-20追記**
現在のVSCode Extension APIには[`commands.registerTextEditorCommand`](https://code.visualstudio.com/api/references/vscode-api#commands.registerTextEditorCommand)があり、`commands.registerCommand`の代わりにこれを使うとコールバックで`textEditor`を得られるようだ。


# Selectionは任意個
上述の通り、VS Code APIでは、以下のオブジェクトが提供されています。

* エディタを表現する[TextEditor](https://code.visualstudio.com/api/references/vscode-api#TextEditor)
* カーソルの位置を表現する[Selection](https://code.visualstudio.com/api/references/vscode-api#Selection)

ところで、VSCodeはデフォルトでマルチカーソルをサポートしています。
したがって、`TextEditor`は**Arrayとして**`TextEditor.selections: Selection[]`を持ちます。`TextEditor.selection: Selection`もありますが、これは`TextEditor.selections[0]`へのshorthandです。
See https://code.visualstudio.com/api/references/vscode-api#TextEditor

キーバインド拡張を作るときは、全てのコマンドでArray `TextEditor.selections: Selection[]` を操作するようにしないと、マルチカーソルに対応しない拡張になってしまいます。
例: https://github.com/tuttieee/vscode-emacs-mcx/blob/16ab3ad6363294f7e112d21717241fb882b79772/src/commands/move.ts#L23
↓カーソル移動コマンド。`textEditor.selections`Array全体を操作している。

```typescript
...略...
            const newSelections = textEditor.selections.map((selection) => {
                const offset = doc.offsetAt(selection.active);
                const newActivePos = doc.positionAt(offset + prefixArgument);
                const newAnchorPos = isInMarkMode ? selection.anchor : newActivePos;
                return new vscode.Selection(newAnchorPos, newActivePos);
            });
...略...
```

ちなみに、私が[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)を（既存拡張のForkではなく）フルスクラッチで作り直した最大の理由の一つがこれです。
既存のEmacsキーバインド拡張は全て`TextEditor.selection: Selection`を操作するように書かれていました。

# `setContext ` などの非公開コマンドを知る
公式ドキュメントの[Commands](https://code.visualstudio.com/api/references/commands)ページには、利用可能なコマンドが列挙されています。

また、デフォルトの`keybindings.json`を見れば、シンプルなコマンドは載っています（https://code.visualstudio.com/api/references/commands#simple-commands）
特にキーバインド拡張を作るときにはこちらを見ることが多くなるでしょう。

しかし、このどちらにも載っていないコマンドが存在します。

例えば、`setContext`コマンド。
`keybinding.json`の`when`節で参照可能なcontextを登録できるコマンドです。
これはVSCodeVimでは[しれっと使ってある](https://github.com/VSCodeVim/Vim/blob/381f026af494fb33a717cbf933a5381446973656/src/util/vscode-context.ts#L15)のですが、ドキュメントには載っていません（[「正式なAPIに格上げすべき」](https://github.com/microsoft/vscode/issues/10471)、[「ドキュメントに書くべき」](https://github.com/Microsoft/vscode/issues/46445)といったIssueは上がっています）。

[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)は`setContext`を使って、選択モードのON/OFFを表す`emacs-mcx.inMarkMode`というフラグを"when"内で[参照可能にしてあります](https://github.com/tuttieee/vscode-emacs-mcx/issues/56)。

この辺りは万人が知っておくべきコマンドではないですが、「VSCodeVimでは実現できているあの機能、どうすればいいのかドキュメントからは分からない…」といったことはそれなりにあるので、やはり公式が作っている拡張のコードを読むのが情報源としては一番だと思います。


また、最近[ドキュメント化された](https://code.visualstudio.com/docs/getstarted/keybindings#_command-arguments)ようですが、私が開発していたときには`type`コマンドもドキュメント化されておらず[サンプルリポジトリのvim-sample](https://github.com/microsoft/vscode-extension-samples/tree/master/vim-sample)を参考にしていました。
さらに今もドキュメントに載っていない`replacePreviousChar`,`compositionStart`, `compositionEnd`も[しれっと使われています](https://github.com/microsoft/vscode-extension-samples/blob/926c5c045e3274a0572f4a07ca0adf6d2fdbd949/vim-sample/src/extension.ts)。これらはIME対応に必要なコマンドになります([#341](https://github.com/VSCodeVim/Vim/issues/341), [#1287](https://github.com/VSCodeVim/Vim/issues/1287)） 。

どうやら、VSCodeのコア開発者[@alexdima](https://github.com/alexdima)氏がVim拡張（[alexdima/vscode-vim ](https://github.com/alexdima/vscode-vim)。現在は[microsoft/vscode-extension-samples](https://github.com/Microsoft/vscode-extension-samples/tree/master/vim-sample)に移動。）で必要になったコマンドをVSCode本体に取り込む流れもあったりした（[microsoft/vscode#8133](https://github.com/microsoft/vscode/issues/8133)、[alexdima/vscode-vim@2c1ccd7](https://github.com/alexdima/vscode-vim/commit/2c1ccd7e6ed9f2a3ed9348253cc6fc529ba6186f)）ようなので、Vim拡張を情報の上流として参照するのは正解な気がします。ドキュメント化されていないAPIが先にVim拡張で使われたりします。

# 既に他の拡張で実装されている機能はそのまま使う
ここまでで挙げましたが、[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)は

* "Editor"から"Controller"へのマッピング
* `when`で参照可能なcontextの登録

といった処理はVSCodeVimのコードをほとんどそのまま拝借しています。
既存拡張から取り入れたい機能がある場合、そのコードを読んでパクるのが一番です。

その場合、ライセンス表記は[ちゃんとしておきます](https://github.com/tuttieee/vscode-emacs-mcx/blob/16ab3ad6363294f7e112d21717241fb882b79772/LICENSE#L27)

# テストを書く

実装するほぼ全てのコマンドに[テストを書いておきます](https://github.com/tuttieee/vscode-emacs-mcx/tree/master/src/test)。
公式ドキュメントにも[VSCode拡張のテストの書き方の解説があります](https://code.visualstudio.com/api/working-with-extensions/testing-extension)。

例えば[Awesome Emacs Keymap](https://github.com/tuttieee/vscode-emacs-mcx)だと、様々な独立した機能（カーソル移動、選択モード、KillRing、etc...）が全部入りなパッケージになってしまいます。
リリースのたびにそれらの全機能を手動でテストするのは非現実的です。
特に`ctrl-u`のような複雑な挙動のキーバインドや、`ctrl-k`のような行末の改行の扱いがセンシティブなキーバインドなど、考慮する項目が多いほど、テストを書く労力がペイすると思います。
一度テストを書いておけば、少なくともそれらの挙動についてはデグレを恐れずに機能追加ができます。
機能追加要望を貰ったときに、迅速に開発に取りかかれます。