Awesome Emacs Keymap
こちらのVSCode拡張を作っています:
Emacsキーバインド拡張です。
GitHub: https://github.com/tuttieee/vscode-emacs-mcx
同Advent CalendarのVSCodeのEmacsキーバインド拡張まとめでも紹介いただきました。ありがとうございます…!
既存のEmacsキーバインド拡張はキーバインドの再現性が低かったり、細かいところで使い勝手が悪かったりで正直使い物にならないものばかりだったので、自分で作り直しました。
おそらくVSCodeのEmacsキーバインド拡張はEmacs Keymapが最古参で、これをForkしたEmacs Friendly Keymapが登場してからはこちらがデファクトだったように思います。
他にもいくつかありますが、Emacs Friendly Keymapも含め、それらはほとんどがEmacs KeymapをベースにForkしたものです。
(この辺りの事情についてはVSCode の Emacs keymaps 拡張についてのメモが非常によくまとまっています。私のAwesome Emacs Keymapも載せていただきました。ありがとうございます。)
ところが、これらの拡張は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
- MS公式のVimキーバインドです
- microsoft/vscode-extension-samples
本稿の以降の内容は、ほとんどこれらのリポジトリを読んで得られたTipsの羅列です。
VS Code APIにおける、各種オブジェクトの関係を知る
VS Code APIが提供する各種オブジェクトについてはReferences - VS Code APIに一通り書かれているのですが、これらが体系的に説明されたドキュメントが見当たりません(私がこの拡張を作った2019年1月当時。今はあるかも。)。
とりあえず、本稿の説明のために、以下を抜粋して説明します。
-
TextDocument
- 文書1つを表すオブジェクトです。例えば、ファイルを1個開くと、対応する
TextDocument
が1個できます。1個新規作成しても、対応するTextDocument
が1個できます。
- 文書1つを表すオブジェクトです。例えば、ファイルを1個開くと、対応する
-
TextEditor
- テキストエディタ1個を表すオブジェクトです。ある
TextDocument
1個に複数のTextEditor
がアタッチされる、ということがあり得ます(同じファイルを複数のタブで開くなど)
- テキストエディタ1個を表すオブジェクトです。ある
-
window
- 現在のVSCodeのウィンドウを表すオブジェクト(正確には名前空間)です
- そのウィンドウ内でアクティブな
TextEditor
を表すwindow.activeTextEditor
などのプロパティを持ちます
-
Selection
- カーソルを表現します。
anchor
,active
プロパティを持ちます。通常時はこの2つは一致しますが、状態選択時には一致せず、キー操作可能な方のカーソルがactive
、固定されたカーソルがanchor
となります。これら2つに挟まれた領域が選択領域となります。
- カーソルを表現します。
その他にも様々なAPIが公開されていますが、特にキーバインド拡張を作る際には↑あたりが重要になると思うので、上記リンクを基点にしていろいろ眺めてみると良いでしょう。
状態を持つキーバインド拡張は、Editorと1対1対応するControllerオブジェクトを作る
状態を持つキーバインド拡張とは、例えば
- Vimのモード切り替え
- Emacsの選択モード
などです。
あるキーバインドが特定のコマンドを発行して終わり(=ステートレス)、ではなく、継続的に状態を保持するような機能のことです。
このような状態を持ったキーバインド拡張を作るときは、その状態を保持するController
クラスを作るのが自然かと思います(Controllerという名前が自然かはわかりませんが、vim-sampleではそのように名付けられているので、ここではControllerと呼びます。同種のクラスは、VSCodeVIMではModeHandler、AwesomeEmacsKeymapではEmacsEmulatorという名前になっています)。
class Controller {
private _currentMode: Mode; // 現在のモード
...略...
}
ここで、Controller
のインスタンスはシングルトンにせず、VSCodeの"Editor"と1対1対応するようにします。
例えばシングルトンにしてしまうと、あるタブで選択モードに入ったまま別のタブに移動すると、そちらでも選択モードが続いたりといったことになります。
また、ここでVSCodeの"Editor"とは、VSCodeのタブ1つに対応するエンティティ、くらいの意味で使っています。
素朴に考えると、以下のようなObjectを作って"Editor"とController
の対応を保持しておき、
const editorControllerMap: {[key: Editor]: Controller} = {};
必要に応じて
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のEditorIdentityとmodeHandlerMapを参考にしましょう。(https://github.com/VSCodeVim/Vim/blob/fa9deaf3f58a7125e773111005c88d2ec10f768e/src/mode/modeHandlerMap.ts#L11)
EditorIdentity
はTextEditor
をMapのキーとして使える値にラップし、modeHandlerMap
はEditorIdentity
をキーとしてMapを保持してTextEditor
からController
(VSCodeVimではModeHandler
)を返します。
私のAwesome Emacs Keymapはこのコードをコピペして使っています
(EditorIdentity
の中身を読むと単純なファイル名の比較なのですが、MS公式のVSCodeVimがこうやっているので、これで良いのでしょう。)
(2022/11/15 Update):
現在では document.uri
をキーに使っているようです
2020-05-20追記
現在のVSCode Extension APIにはcommands.registerTextEditorCommand
があり、commands.registerCommand
の代わりにこれを使うとコールバックでtextEditor
を得られるようだ。
Selectionは任意個
上述の通り、VS Code APIでは、以下のオブジェクトが提供されています。
- エディタを表現するTextEditor
- カーソルの位置を表現する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全体を操作している。
...略...
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を(既存拡張のForkではなく)フルスクラッチで作り直した最大の理由の一つがこれです。
既存のEmacsキーバインド拡張は全てTextEditor.selection: Selection
を操作するように書かれていました。
setContext
などの非公開コマンドを知る
公式ドキュメントのCommandsページには、利用可能なコマンドが列挙されています。
また、デフォルトのkeybindings.json
を見れば、シンプルなコマンドは載っています(https://code.visualstudio.com/api/references/commands#simple-commands)
特にキーバインド拡張を作るときにはこちらを見ることが多くなるでしょう。
しかし、このどちらにも載っていないコマンドが存在します。
例えば、setContext
コマンド。
keybinding.json
のwhen
節で参照可能なcontextを登録できるコマンドです。
これはVSCodeVimではしれっと使ってあるのですが、ドキュメントには載っていません(「正式なAPIに格上げすべき」、「ドキュメントに書くべき」といったIssueは上がっています)。
Awesome Emacs KeymapはsetContext
を使って、選択モードのON/OFFを表すemacs-mcx.inMarkMode
というフラグを"when"内で参照可能にしてあります。
この辺りは万人が知っておくべきコマンドではないですが、「VSCodeVimでは実現できているあの機能、どうすればいいのかドキュメントからは分からない…」といったことはそれなりにあるので、やはり公式が作っている拡張のコードを読むのが情報源としては一番だと思います。
また、最近ドキュメント化されたようですが、私が開発していたときにはtype
コマンドもドキュメント化されておらずサンプルリポジトリのvim-sampleを参考にしていました。
さらに今もドキュメントに載っていないreplacePreviousChar
,compositionStart
, compositionEnd
もしれっと使われています。これらはIME対応に必要なコマンドになります(#341, #1287) 。
どうやら、VSCodeのコア開発者@alexdima氏がVim拡張(alexdima/vscode-vim 。現在はmicrosoft/vscode-extension-samplesに移動。)で必要になったコマンドをVSCode本体に取り込む流れもあったりした(microsoft/vscode#8133、alexdima/vscode-vim@2c1ccd7)ようなので、Vim拡張を情報の上流として参照するのは正解な気がします。ドキュメント化されていないAPIが先にVim拡張で使われたりします。
既に他の拡張で実装されている機能はそのまま使う
ここまでで挙げましたが、Awesome Emacs Keymapは
- "Editor"から"Controller"へのマッピング
-
when
で参照可能なcontextの登録
といった処理はVSCodeVimのコードをほとんどそのまま拝借しています。
既存拡張から取り入れたい機能がある場合、そのコードを読んでパクるのが一番です。
その場合、ライセンス表記はちゃんとしておきます
テストを書く
実装するほぼ全てのコマンドにテストを書いておきます。
公式ドキュメントにもVSCode拡張のテストの書き方の解説があります。
例えばAwesome Emacs Keymapだと、様々な独立した機能(カーソル移動、選択モード、KillRing、etc...)が全部入りなパッケージになってしまいます。
リリースのたびにそれらの全機能を手動でテストするのは非現実的です。
特にctrl-u
のような複雑な挙動のキーバインドや、ctrl-k
のような行末の改行の扱いがセンシティブなキーバインドなど、考慮する項目が多いほど、テストを書く労力がペイすると思います。
一度テストを書いておけば、少なくともそれらの挙動についてはデグレを恐れずに機能追加ができます。
機能追加要望を貰ったときに、迅速に開発に取りかかれます。