VS Code拡張機能開発 第1回 拡張機能プロジェクトのコード構造と処理の流れ
コメントハイライト拡張機能の実現を目指して、まずは推奨されるコードの組み立て方とイベント処理の流れを確認します。
Visual Studio Codeと拡張機能
みなさんは、Visual Studio Codeを使っていますか?
Visual Studio Code(以下、VS Codeと略)は、Microsoftが提供するオープンソースのコードエディタです。
macOS/Windows/Linuxとクロスプラットフォームで利用でき、Microsoftが提供するコア機能も充実しているVS Codeですが、最大の魅力は拡張性の高さにあるといっても過言ではないでしょう。
VS Codeでは、ユーザーである世界中のエンジニアが開発した拡張機能を利用することで、さまざまな言語やフレームワークに対応したり、コードの編集に便利な機能やプレビュー機能を追加したりすることができます。
既存の拡張機能の使い方については、次の記事で詳しく解説しています。
VS Codeの拡張機能の開発は、実は決して難しいものではありません。TypeScriptをベースに開発されているVS Codeは、TypeScriptやHTML/CSSなどの慣れ親しんだWebの言語で、機能を拡張することができるのです。
このシリーズで、簡単なVS Code拡張機能開発を体験してみましょう。
このシリーズで作る拡張機能
コードを書いている際、一時的にTODO
やFIXME
といったコメントを残しておくことはよくあるでしょう。しかし、せっかくコメントを残しておいても、見落として結局対応し忘れたら意味がないですよね。
そこで、未対応コメントを目立つようにハイライトし、いくつ残っているかをカウント表示する拡張機能を作ることにします。
具体的には、次のような機能を搭載します。
1. todo:
で始まる行をハイライトする
2. ハイライトしたコメントの数をエディタ下部のステータスバーに表示する
3. カウント表示をクリックすると、今開いているファイル内の1件目のハイライトコメントに移動する
そして、最終的には、次の項目をVS Codeの設定画面やこの拡張機能のsettings.json
から簡単に変更できるようにします。
- ハイライト対象とする文字列
- ハイライトの文字色
- ハイライトの背景色
拡張機能の雛形を生成する
それでは、開発ツールを使って機能拡張の雛形を作成するところから始めましょう。
VS Code拡張機能の開発には、Yeomanという対話式の雛形生成ツールを利用するのが一般的です。
まずは、Yeomanが提供するコマンドライン雛形生成モジュールyo
と、yo
モジュール上で動作するVS Code拡張機能用の雛形ジェネレータgenerator-code
をnpmでグローバルインストールします。
generator-codeのインストール
$ npm install -g yo generator-code
すると、yo code
コマンドによってVS Code拡張機能の雛形コードを生成することができるようになります。
機能拡張の雛形コードの生成
$ yo code
質問に答えていく形で、今作りたい拡張機能に適した雛形コードが生成されます。 今回は、次のように回答します。
対話式の雛形コード生成
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? todo-highlight
? What's the identifier of your extension? todo-highlight
? What's the description of your extension?
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm
これでtodo-highlight
という名前のVS Code拡張機能を、TypeScriptで開発していくための雛形コードが生成されました。
雛形プロジェクトの構造
生成された雛形プロジェクト(todo-highlight
ディレクトリ)は、ルートディレクトリにpackage.json
が置かれた、みなさんも見慣れているであろうフロントエンドプロジェクトと同じような構成になっています。
主要なファイル構成
.
├── CHANGELOG.md
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── extension.ts
│ └── test
├── tsconfig.json
└── vsc-extension-quickstart.md
主に触ることになる重要なファイルは、extention.ts
とpackage.json
です。
VS Code独自のpackage.json
package.json
を開いてみると、いくつか見慣れないフィールドがあるでしょう。
VS Code拡張機能におけるpackage.json
は、拡張機能の実行タイミングや、動作対象とするVS Codeのバージョン、提供する機能の内容などをVS Codeに伝える役割を持つものとして拡張されています。
package.json(抜粋)
{
"engines": {
"vscode": "^1.78.0" /* どのバージョン以上のVScodeで動作するか? */
},
"activationEvents": [
/* どのタイミングで拡張機能を実行するか? */
],
"contributes": {
/* どんな機能や設定項目をVScodeに提供するか? */
}
}
activationEventsの設定
細かい設定は実装しながら都度解説していきますが、最初に設定しておきたいのはactivationEvents
です。
次のように設定することで、今回作る拡張機能は、Markdown、Mdx、JavaScript、TypeScriptファイルを開いた際に初期化するようにします。
package.json(抜粋)
"activationEvents": [
"onLanguage:markdown",
"onLanguage:mdx",
"onLanguage:javascript",
"onLanguage:typescript"
]
onLanguage:hogehoge
とは、「hogehogeという言語のファイルを開いたら」という意味です。
このhogehogeの部分は、言語名を自由な形式で書いてよいわけではなく、VS Codeの定める言語ごとのidを指定する必要があります。
extension.tsの基本構造
拡張機能のコードは、srcディレクトリに置かれているextension.ts
ファイルに書いていきます。
extension.ts
には、activate
とdeactivate
という関数が設置されており、この2つの関数が基本的な構造となっています。
extension.ts
import * as vscode from "vscode";
// activationEventsとして登録したタイミングで実行
export function activate(context: vscode.ExtensionContext) {
/** 基本的にここにすべて書く */
}
// - VS Codeがシャットダウンしているとき
// - 拡張機能が無効またはアンインストールされているとき
export function deactivate() {
/** クリーンアップなど */
}
activate
関数は、先ほどpackage.json
に登録したactivationEvents
が発火したときに実行される関数です。
deactivate
関数は、VS Codeがシャットダウンされたときや、拡張機能が無効化またはアンインストールされたときに実行されます。
とはいえ、VS Codeには、拡張機能を自動的にクリーンアップする仕組みが備わっているので、deactivate
関数にコードを書くことはあまりありません(後述コラム参照)。
イベントに応じて処理を行う
activate
関数の中では、ユーザーの操作やファイルの変更などのイベントに応じて処理を行うことができます。
VS Code APIのうち、onDid
やonWill
で始まるものが、拡張機能開発で使える組み込みイベントです。
今回作ろうとしているハイライト機能では、どのタイミングでハイライトの更新処理をかけるのかが大変重要になります。そこで、今回は、主に処理の「タイミング」について理解を深めておきます。
具体的には、次のイベントにハイライトを更新する処理を登録します。
- 編集しているファイルが変更されたときに発火する
vscode.window.onDidChangeActiveTextEditor
イベント - ファイル内のテキストが変更されたときに発火する
vscode.workspace.onDidChangeTextDocument
イベント
つまり、新たなファイルを編集し始めたとき、ファイルのテキストが変更されたときに、それぞれハイライトの更新処理をするようにします。
ただし、vscode.window.onDidChangeActiveTextEditor
は、編集中のファイルを閉じたときにも発火するため、ファイルを閉じたときにはハイライトの更新処理を行わないようにしましょう。
ファイルを閉じたときには、vscode.window.onDidChangeActiveTextEditor
のコールバック関数に渡される引数がundefined
になるため、それを利用して処理を分岐させます。
コードでは次のようになります。まだ具体的にハイライトを更新する処理は書いていませんが、どのタイミングで処理が行われるかを把握してください。
ハイライト更新のタイミング
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
// 別なファイルを編集し始めたら or 編集中のファイルを閉じたら
vscode.window.onDidChangeActiveTextEditor((activeEditor) => {
// 編集中のファイルを閉じた場合は何もしない
if (!activeEditor) {
return;
}
// todo: ハイライトを更新
}),
// ファイルのテキストが変更されたら
vscode.workspace.onDidChangeTextDocument((event) => {
// todo: ハイライトを更新
})
);
}
コラム:処理の登録を自動的に解除する仕組み
VS Codeには処理の登録を自動的に解除する仕組みがあります。前掲のコードで、context.subscriptions.push()
内でイベントハンドラの登録を行っているところがポイントです。
VS Codeが提供する次のようなメソッドは、処理の登録を解除し、リソースを解放するためのdispose
メソッドを持つ、Dispose
オブジェクトを返します。
- イベントハンドラを登録するメソッド
- 独自のコマンドを登録するメソッド
- 対話的なUIの機能を登録するメソッド
そして、VS Codeは、拡張機能が無効化もしくはアンインストールされたときに、context.subscription
プロパティ(Dispose
型オブジェクトの配列)に含まれるすべてのオブジェクトのdispose
メソッドを呼び出し、処理の登録を解除します。
登録系メソッドの返り値であるDispose
オブジェクトを、context.subscriptions.push
メソッドを使ってcontext.subscription
プロパティに登録しておくことで、自動的に適切なクリーンアップが行われるようになるのです。
現在編集中のファイルの情報を取得する
VS Codeでは、エディタを分割し、複数のファイルを同時に開くことができますが、簡略化のために、当シリーズで開発するtodo-highlight拡張機能は、開いているファイルの中でも現在メインで編集しているファイルのみを監視し、そのファイルだけハイライト表示を行うようにします。
「現在メインで編集しているファイル」とは、現在カーソルが置かれているファイル、カーソルがエディタ外にある場合は最後に変更を加えたファイルです。
このようなファイルの情報と操作メソッドを格納したオブジェクト(vscode.TextEditor
インスタンス)は、vscode.window.activeTextEditor
プロパティで取得することができます。
このプロパティは、1つもファイルを開いていない場合にはundefined
を返すため、ファイルを開いて編集しているときだけ、そのファイルに対してハイライト更新を実行するようにします。
現在開いているファイルでのみ更新処理を実行
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
// 現在エディタで編集中のファイルを取得
const activeEditor = vscode.window.activeTextEditor;
// 拡張機能が有効化された時点でファイルを開いていれば
if (activeEditor) {
// todo: ハイライトを更新
}
context.subscriptions.push(
// << 省略 >>
);
}
ところで、vscode.workspace.onDidChangeTextDocument
イベントは、ファイルのテキストが変更された場合だけでなく、次の場合にも発火します。
- UndoやRedoが実行されたとき
- ファイルの保存が実行されたとき
- フォーマッターによる変更が加えられたとき
しかしながら、フォーマッターを全ファイルに対してコマンドで一括で実行した場合、対象の全ファイルに対していちいちハイライト更新を実行するのは無駄です。
現在編集中のファイルのみを対象にする
そこで、イベントが発火したファイルが現在エディタで編集中のファイルであるかどうかを判定し、現在編集中のファイルだった場合のみハイライト更新を実行するようにしたほうが良いでしょう。
現在編集のファイルでのみハイライト更新を実行
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
// << 省略 >>
context.subscriptions.push(
// << 省略 >>
vscode.workspace.onDidChangeTextDocument((event) => {
// 現在エディタで編集中のファイルを取得
const activeEditor = vscode.window.activeTextEditor;
// エディタでファイルを開いていなかった場合は何もしない
if (!activeEditor) {
return;
}
// 変更イベントが発生したファイルが現在編集中のファイルであれば
if (event.document.uri === activeEditor.document.uri) {
// todo: ハイライトを更新
}
})
);
}
これでようやく、どこでハイライト更新を行うべきか、全体の流れが見えてきました。
特定の言語のファイルのみを対象にする
あともう一息、一点だけ注意すべきことがあります。
package.json
のactivationEvents
は拡張機能を初期化するタイミングの設定なので、一度初期化されてしまえば、それ以降はすべての種類のファイルに対してハイライト更新処理が実行されてしまうのです。
特定の言語のファイルでのみ機能を実行するには、vscode.TextEditor
インスタンスが持つdocument.languageId
プロパティを使った分岐処理が必要です。次のような判定メソッドを用意しましょう。
言語ごとのIDによって実行対象ファイルを絞り込むための判定メソッド
// 機能を実行する対象ファイル
const TARGET_LANGS = ["markdown", "javascript", "typescript", "mdx"];
// 対象ファイルかどうかを判定するメソッド
const isTargetFile = (editor: vscode.TextEditor) => TARGET_LANGS.includes(editor.document.languageId);
このisTargetFile
メソッドによる分岐処理を追加した完全なコードは、デモリポジトリに掲載しています。
VS Code拡張機能開発シリーズリポジトリ
デモリポジトリのサンプルでは、ハイライト更新を実行するタイミングでVS Codeの出力ウィンドウにメッセージを出力するようにしているので、ぜひ実際に動かして実行タイミングを確認してみてください。
次回へ向けて
今回は、エディタを開いたタイミングや内容が更新されたタイミングで任意のコードを実行できるようにextension.ts
を編集しました。
次回は、実際にコードをハイライトする処理を実装していきます。具体的にはvscode.window.activeTextEditor
などで取得したvscode.TextEditor
インスタンスが持つsetDecorations
メソッドで、特定の範囲のハイライトを実現します。
しかし、その前に、「どんな装飾を適用するか」を定義し、「どの範囲に適用するか」を計算する必要があります。ファイルのテキストの中から特定の範囲を抽出する処理も、VS Code APIを使って行うことになります。
続きは次回、詳しくみていきましょう。