VS Code拡張機能開発 第1回 拡張機能プロジェクトのコード構造と処理の流れ

コメントハイライト拡張機能の実現を目指して、まずは推奨されるコードの組み立て方とイベント処理の流れを確認します。

発行

著者 菅野 亜実 フロントエンド・エンジニア
VS Code拡張機能開発 シリーズの記事一覧

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拡張機能開発を体験してみましょう。

このシリーズで作る拡張機能

コードを書いている際、一時的にTODOFIXMEといったコメントを残しておくことはよくあるでしょう。しかし、せっかくコメントを残しておいても、見落として結局対応し忘れたら意味がないですよね。

そこで、未対応コメントを目立つようにハイライトし、いくつ残っているかをカウント表示する拡張機能を作ることにします。

具体的には、次のような機能を搭載します。

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.tspackage.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には、activatedeactivateという関数が設置されており、この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のうち、onDidonWillで始まるものが、拡張機能開発で使える組み込みイベントです。

今回作ろうとしているハイライト機能では、どのタイミングでハイライトの更新処理をかけるのかが大変重要になります。そこで、今回は、主に処理の「タイミング」について理解を深めておきます。

具体的には、次のイベントにハイライトを更新する処理を登録します。

  • 編集しているファイルが変更されたときに発火する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.jsonactivationEventsは拡張機能を初期化するタイミングの設定なので、一度初期化されてしまえば、それ以降はすべての種類のファイルに対してハイライト更新処理が実行されてしまうのです。

特定の言語のファイルでのみ機能を実行するには、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を使って行うことになります。

続きは次回、詳しくみていきましょう。