43
21

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 1 year has passed since last update.

Visual Studio CodeAdvent Calendar 2019

Day 24

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

Last updated at Posted at 2019-12-23

Awesome Emacs Keymap

こちらのVSCode拡張を作っています:

Awesome Emacs Keymap logo

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つです。

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

VS Code APIにおける、各種オブジェクトの関係を知る

VS Code APIが提供する各種オブジェクトについてはReferences - VS Code APIに一通り書かれているのですが、これらが体系的に説明されたドキュメントが見当たりません(私がこの拡張を作った2019年1月当時。今はあるかも。)。

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

  • TextDocument
    • 文書1つを表すオブジェクトです。例えば、ファイルを1個開くと、対応するTextDocumentが1個できます。1個新規作成しても、対応するTextDocumentが1個できます。
  • TextEditor
    • テキストエディタ1個を表すオブジェクトです。あるTextDocument1個に複数のTextEditorがアタッチされる、ということがあり得ます(同じファイルを複数のタブで開くなど)
  • 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ではModeHandlerAwesomeEmacsKeymapでは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/VimEditorIdentitymodeHandlerMapを参考にしましょう。
EditorIdentityTextEditorをMapのキーとして使える値にラップし、modeHandlerMapEditorIdentityをキーとしてMapを保持してTextEditorからController(VSCodeVimではModeHandler)を返します。
https://github.com/VSCodeVim/Vim/blob/fa9deaf3f58a7125e773111005c88d2ec10f768e/src/mode/modeHandlerMap.ts#L11)
私の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では、以下のオブジェクトが提供されています。

ところで、VSCodeはデフォルトでマルチカーソルをサポートしています。
したがって、TextEditorArrayとして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.selectionsArray全体を操作している。

......
            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.jsonwhen節で参照可能なcontextを登録できるコマンドです。
これはVSCodeVimではしれっと使ってあるのですが、ドキュメントには載っていません(「正式なAPIに格上げすべき」「ドキュメントに書くべき」といったIssueは上がっています)。

Awesome Emacs KeymapsetContextを使って、選択モードの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#8133alexdima/vscode-vim@2c1ccd7)ようなので、Vim拡張を情報の上流として参照するのは正解な気がします。ドキュメント化されていないAPIが先にVim拡張で使われたりします。

既に他の拡張で実装されている機能はそのまま使う

ここまでで挙げましたが、Awesome Emacs Keymap

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

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

その場合、ライセンス表記はちゃんとしておきます

テストを書く

実装するほぼ全てのコマンドにテストを書いておきます
公式ドキュメントにもVSCode拡張のテストの書き方の解説があります

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

43
21
1

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
43
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?