VSCode 拡張機能開発で自分だけのツールを作る

Visual Studio Code(VSCode)は、拡張機能によってその機能を無限に拡張できるエディタです。Marketplaceには88,000以上の拡張機能が公開されていますが、自分の業務に完璧にフィットするツールは自分で作るしかない場面もあります。

本記事では、VSCode 拡張機能開発の基礎から、実際にMarketplaceへ公開するまでの全工程を、初心者にも分かりやすく解説します。TypeScriptを使った開発環境の構築、Extension APIの基本、コマンド・メニュー・キーバインドの登録方法、デバッグのコツ、そして公開の手順までを網羅します。

この記事で得られること

  • Yeomanを使った拡張機能プロジェクトの生成方法
  • Extension APIの基本概念と活用方法
  • package.jsonにおけるContribution Pointsの設定
  • 効率的なデバッグとテストの手法
  • Marketplaceへの公開手順と注意点

前提条件と実行環境

VSCode 拡張機能開発を始めるための環境を準備します。

項目 要件
Node.js 20.x LTS以上を推奨
VSCode バージョン1.80以上
Git バージョン管理に必要
OS Windows 10/11、macOS 10.15以上、Linux

開発ツールのインストール

拡張機能開発にはYeomanとVS Code Extension Generatorが必要です。以下のコマンドでグローバルインストールします。

1
2
# Yeomanとジェネレーターをグローバルインストール
npm install --global yo generator-code

一度だけ使用する場合は、npxコマンドで直接実行できます。

1
2
# npxを使用して直接実行
npx --package yo --package generator-code -- yo code

拡張機能プロジェクトの生成

Yeomanを使って、TypeScript製の拡張機能プロジェクトを生成します。

プロジェクトの作成手順

ターミナルでyo codeコマンドを実行し、対話形式でプロジェクトを設定します。

1
yo code

以下のように質問に回答していきます。

1
2
3
4
5
6
7
8
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? hello-world
? What's the identifier of your extension? hello-world
? What's the description of your extension? My first VSCode extension
? Initialize a git repository? Yes
? Which bundler to use? unbundled
? Which package manager to use? npm
? Do you want to open the new folder with Visual Studio Code? Open with `code`

生成されるプロジェクト構造

プロジェクト生成後、以下のようなディレクトリ構造が作成されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
hello-world/
├── .vscode/
│   ├── launch.json      # デバッグ設定
│   └── tasks.json       # ビルドタスク設定
├── src/
│   └── extension.ts     # 拡張機能のエントリーポイント
├── .gitignore
├── .vscodeignore        # パッケージング時の除外設定
├── CHANGELOG.md
├── package.json         # 拡張機能マニフェスト
├── README.md
└── tsconfig.json        # TypeScript設定

Extension APIの基本概念

VSCode拡張機能開発の核となる3つの概念を理解しましょう。

Activation Events(アクティベーションイベント)

拡張機能がいつ読み込まれるかを定義します。VSCode 1.74以降では、contributes.commandsで宣言したコマンドは自動的にアクティベーションイベントとして登録されるため、多くの場合明示的な設定は不要です。

主なアクティベーションイベントは以下の通りです。

イベント 説明
onCommand:commandId 特定のコマンド実行時
onLanguage:languageId 特定の言語のファイルを開いた時
onView:viewId 特定のビューが表示された時
onStartupFinished VSCode起動完了時
* VSCode起動時(非推奨)

Contribution Points

package.jsonのcontributesフィールドで定義する静的な宣言です。コマンド、メニュー、キーバインド、設定項目などをVSCodeに登録します。

VS Code API

拡張機能のコードから呼び出すJavaScript APIです。エディタの操作、ファイルシステムへのアクセス、通知の表示など、VSCodeのほぼすべての機能を制御できます。

エントリーポイント(extension.ts)の解説

生成されたsrc/extension.tsを詳しく見ていきましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import * as vscode from 'vscode';

// 拡張機能がアクティブ化された時に呼び出される
export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "hello-world" is now active!');

  // コマンドを登録
  const disposable = vscode.commands.registerCommand('hello-world.helloWorld', () => {
    // コマンド実行時の処理
    vscode.window.showInformationMessage('Hello World from hello-world!');
  });

  // 登録したコマンドをサブスクリプションに追加
  context.subscriptions.push(disposable);
}

// 拡張機能が非アクティブ化された時に呼び出される
export function deactivate() {}

activate関数の役割

activate関数は拡張機能がアクティブ化されたときに一度だけ呼び出されます。この中でコマンドの登録、イベントリスナーの設定、リソースの初期化を行います。

ExtensionContextの活用

contextパラメータは拡張機能のライフサイクル管理に使用します。subscriptions配列に追加したDisposableオブジェクトは、拡張機能の非アクティブ化時に自動的にクリーンアップされます。

deactivate関数の役割

deactivate関数は拡張機能が無効化されるときに呼び出されます。明示的なクリーンアップが必要な場合に処理を記述しますが、多くの場合は空のまま問題ありません。

package.json - 拡張機能マニフェストの設定

package.jsonは拡張機能のメタ情報と機能を宣言するマニフェストファイルです。

基本的なフィールド

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "hello-world",
  "displayName": "Hello World",
  "description": "My first VSCode extension",
  "version": "0.0.1",
  "publisher": "your-publisher-id",
  "engines": {
    "vscode": "^1.80.0"
  },
  "categories": ["Other"],
  "main": "./out/extension.js",
  "activationEvents": [],
  "contributes": {
    "commands": [
      {
        "command": "hello-world.helloWorld",
        "title": "Hello World"
      }
    ]
  }
}

主要フィールドの解説

フィールド 必須 説明
name Yes 拡張機能の識別子(小文字、スペース不可)
displayName No Marketplaceに表示される名前
description No 拡張機能の説明
version Yes SemVer形式のバージョン
publisher Yes パブリッシャーID
engines.vscode Yes 対応するVSCodeバージョン
main No エントリーポイントのパス
contributes No Contribution Pointsの定義
activationEvents No アクティベーションイベントの定義

コマンドの登録と実装

拡張機能の基本機能であるコマンドの登録方法を詳しく解説します。

package.jsonでのコマンド宣言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "contributes": {
    "commands": [
      {
        "command": "hello-world.helloWorld",
        "title": "Hello World",
        "category": "Hello",
        "icon": {
          "light": "resources/light/hello.svg",
          "dark": "resources/dark/hello.svg"
        }
      },
      {
        "command": "hello-world.sayGoodbye",
        "title": "Say Goodbye",
        "category": "Hello"
      }
    ]
  }
}

categoryを指定すると、コマンドパレットで「Hello: Hello World」のように表示され、関連コマンドがグループ化されます。

extension.tsでのコマンド実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  // Hello Worldコマンド
  const helloCommand = vscode.commands.registerCommand(
    'hello-world.helloWorld',
    () => {
      vscode.window.showInformationMessage('Hello World!');
    }
  );

  // Say Goodbyeコマンド
  const goodbyeCommand = vscode.commands.registerCommand(
    'hello-world.sayGoodbye',
    () => {
      vscode.window.showInformationMessage('Goodbye!');
    }
  );

  // 複数のDisposableをまとめて登録
  context.subscriptions.push(helloCommand, goodbyeCommand);
}

引数付きコマンドの実装

コマンドは引数を受け取ることもできます。

1
2
3
4
5
6
7
8
9
const greetCommand = vscode.commands.registerCommand(
  'hello-world.greet',
  (name: string) => {
    vscode.window.showInformationMessage(`Hello, ${name}!`);
  }
);

// プログラムから引数付きでコマンドを実行
vscode.commands.executeCommand('hello-world.greet', 'Alice');

メニューへのコマンド追加

コマンドをエディタのコンテキストメニューやタイトルバーに追加する方法を解説します。

メニューの種類

VSCodeには多様なメニュー配置場所があります。

メニューID 配置場所
editor/context エディタの右クリックメニュー
editor/title エディタタイトルバー
explorer/context エクスプローラーの右クリックメニュー
commandPalette コマンドパレット
view/title ビューのタイトルバー

メニュー設定の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "contributes": {
    "commands": [
      {
        "command": "hello-world.formatSelection",
        "title": "Format Selection",
        "category": "Hello"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "hello-world.formatSelection",
          "when": "editorHasSelection",
          "group": "1_modification"
        }
      ],
      "editor/title": [
        {
          "command": "hello-world.formatSelection",
          "when": "resourceLangId == javascript",
          "group": "navigation"
        }
      ],
      "commandPalette": [
        {
          "command": "hello-world.formatSelection",
          "when": "editorHasSelection"
        }
      ]
    }
  }
}

when句による表示条件

when句を使って、メニュー項目の表示条件を細かく制御できます。

条件 説明
editorHasSelection テキストが選択されている
editorTextFocus エディタにフォーカスがある
resourceLangId == javascript JavaScriptファイルを編集中
resourceExtname == .md Markdownファイルを編集中
workspaceFolderCount > 0 ワークスペースが開かれている

グループによる並び順

groupプロパティでメニュー内の配置位置を制御します。

1
2
3
4
5
6
7
navigation(先頭に配置)
1_modification
9_cutcopypaste
z_commands(末尾に配置)

グループ内での順序は@記号で指定します。

1
2
3
{
  "group": "navigation@1"
}

キーバインドの登録

コマンドにショートカットキーを割り当てる方法を解説します。

基本的なキーバインド設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "contributes": {
    "keybindings": [
      {
        "command": "hello-world.helloWorld",
        "key": "ctrl+shift+h",
        "mac": "cmd+shift+h",
        "when": "editorTextFocus"
      },
      {
        "command": "hello-world.formatSelection",
        "key": "ctrl+alt+f",
        "mac": "cmd+alt+f",
        "when": "editorHasSelection"
      }
    ]
  }
}

キー修飾子

修飾子 Windows/Linux macOS
Ctrl ctrl ctrl
Command N/A cmd
Alt/Option alt alt
Shift shift shift

プラットフォーム別の設定

keyプロパティがデフォルトのキーバインドで、maclinuxwinで上書きできます。

1
2
3
4
5
6
{
  "command": "hello-world.helloWorld",
  "key": "ctrl+shift+h",
  "mac": "cmd+shift+h",
  "linux": "ctrl+alt+h"
}

設定項目の追加

拡張機能にユーザーがカスタマイズ可能な設定項目を追加する方法です。

設定の宣言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
  "contributes": {
    "configuration": {
      "title": "Hello World",
      "properties": {
        "hello-world.greeting": {
          "type": "string",
          "default": "Hello",
          "description": "Greeting message to display"
        },
        "hello-world.showNotifications": {
          "type": "boolean",
          "default": true,
          "description": "Show notification messages"
        },
        "hello-world.messageCount": {
          "type": "number",
          "default": 1,
          "minimum": 1,
          "maximum": 10,
          "description": "Number of messages to show"
        },
        "hello-world.language": {
          "type": "string",
          "default": "en",
          "enum": ["en", "ja", "zh"],
          "enumDescriptions": [
            "English",
            "Japanese",
            "Chinese"
          ],
          "description": "Language for messages"
        }
      }
    }
  }
}

設定値の取得

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const config = vscode.workspace.getConfiguration('hello-world');
  
  const greeting = config.get<string>('greeting', 'Hello');
  const showNotifications = config.get<boolean>('showNotifications', true);
  const messageCount = config.get<number>('messageCount', 1);
  
  const command = vscode.commands.registerCommand('hello-world.greet', () => {
    if (showNotifications) {
      for (let i = 0; i < messageCount; i++) {
        vscode.window.showInformationMessage(`${greeting} World!`);
      }
    }
  });

  context.subscriptions.push(command);
}

設定変更の監視

1
2
3
4
5
6
7
8
vscode.workspace.onDidChangeConfiguration((event) => {
  if (event.affectsConfiguration('hello-world')) {
    // 設定が変更された時の処理
    const config = vscode.workspace.getConfiguration('hello-world');
    const newGreeting = config.get<string>('greeting');
    console.log(`Greeting changed to: ${newGreeting}`);
  }
});

デバッグ方法

VSCode拡張機能のデバッグ手順を解説します。

デバッグの開始

  1. 拡張機能プロジェクトをVSCodeで開きます
  2. F5キーを押すか、「Run and Debug」から「Run Extension」を選択します
  3. 新しいVSCodeウィンドウ(Extension Development Host)が開きます
  4. このウィンドウで拡張機能をテストします

launch.jsonの設定

生成されたlaunch.jsonは以下のような内容です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}"
      ],
      "outFiles": [
        "${workspaceFolder}/out/**/*.js"
      ],
      "preLaunchTask": "${defaultBuildTask}"
    },
    {
      "name": "Extension Tests",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
      ],
      "outFiles": [
        "${workspaceFolder}/out/test/**/*.js"
      ],
      "preLaunchTask": "${defaultBuildTask}"
    }
  ]
}

ブレークポイントの設定

TypeScriptファイルの行番号の左側をクリックしてブレークポイントを設定します。デバッグ実行中にその行が実行されると、処理が一時停止し、変数の値を確認できます。

デバッグコンソールの活用

console.log()の出力は、デバッグコンソールに表示されます。開発中は積極的にログを出力してデバッグに活用しましょう。

1
2
3
console.log('Extension activated');
console.log('Config:', JSON.stringify(config, null, 2));
console.error('Error occurred:', error);

変更の反映

コードを変更した後、Extension Development HostウィンドウでCtrl+R(Cmd+R)を押すか、コマンドパレットから「Developer: Reload Window」を実行すると、変更が反映されます。

パッケージングと公開準備

拡張機能をMarketplaceに公開するための準備を行います。

vsceのインストール

1
npm install -g @vscode/vsce

.vscodeignoreの設定

パッケージに含めないファイルを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts

README.mdの作成

Marketplaceページに表示されるREADMEを作成します。以下の内容を含めましょう。

  • 拡張機能の概要と特徴
  • インストール方法
  • 使用方法とスクリーンショット
  • 設定項目の説明
  • 既知の問題と制限事項
  • 変更履歴

アイコンの準備

Marketplaceに表示されるアイコンを用意します。

  • サイズ:128x128ピクセル以上(Retina対応は256x256)
  • 形式:PNG(SVGは使用不可)
  • 背景:透過または単色

package.jsonに以下を追加します。

1
2
3
4
5
6
7
{
  "icon": "images/icon.png",
  "galleryBanner": {
    "color": "#1e1e1e",
    "theme": "dark"
  }
}

パッケージの作成

1
2
3
4
# .vsixファイルを生成
vsce package

# 生成例: hello-world-0.0.1.vsix

ローカルでのテスト

生成したvsixファイルをローカルでインストールしてテストします。

1
code --install-extension hello-world-0.0.1.vsix

Marketplaceへの公開

拡張機能をVS Code Marketplaceに公開する手順です。

Azure DevOpsアカウントの作成

  1. Azure DevOpsにアクセスします
  2. Microsoftアカウントでサインインします
  3. 組織を作成します

Personal Access Token(PAT)の取得

  1. Azure DevOpsの右上にあるユーザー設定アイコンをクリックします
  2. 「Personal access tokens」を選択します
  3. 「New Token」をクリックします
  4. 以下の設定で作成します:
    • Name:任意の名前
    • Organization:All accessible organizations
    • Scopes:Custom defined → Marketplace → Manage にチェック
  5. 「Create」をクリックし、表示されたトークンを安全な場所に保存します

パブリッシャーの作成

  1. Visual Studio Marketplace管理ページにアクセスします
  2. 「Create publisher」をクリックします
  3. パブリッシャーIDと表示名を設定します
  4. パブリッシャーIDをpackage.jsonのpublisherフィールドに設定します

vsceへのログイン

1
2
vsce login <publisher-id>
# Personal Access Tokenの入力を求められます

公開の実行

1
2
3
4
5
6
# Marketplaceに公開
vsce publish

# バージョンを自動インクリメントして公開
vsce publish minor  # 0.0.1 → 0.1.0
vsce publish patch  # 0.0.1 → 0.0.2

公開後の確認

公開から数分後、以下で拡張機能を確認できます。

  • Marketplace: https://marketplace.visualstudio.com/items?itemName=<publisher>.<extension-name>
  • VSCode内: 拡張機能パネルで検索

実践的な拡張機能の例

学んだ内容を活かして、実用的な拡張機能を作成してみましょう。

現在時刻挿入コマンド

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const insertDateCommand = vscode.commands.registerCommand(
    'hello-world.insertDate',
    () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showErrorMessage('No active editor found');
        return;
      }

      const config = vscode.workspace.getConfiguration('hello-world');
      const format = config.get<string>('dateFormat', 'YYYY-MM-DD HH:mm:ss');
      
      const now = new Date();
      const dateString = formatDate(now, format);

      editor.edit((editBuilder) => {
        editBuilder.insert(editor.selection.active, dateString);
      });
    }
  );

  context.subscriptions.push(insertDateCommand);
}

function formatDate(date: Date, format: string): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');

  return format
    .replace('YYYY', String(year))
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hours)
    .replace('mm', minutes)
    .replace('ss', seconds);
}

package.jsonへの追加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
  "contributes": {
    "commands": [
      {
        "command": "hello-world.insertDate",
        "title": "Insert Current Date/Time",
        "category": "Hello"
      }
    ],
    "keybindings": [
      {
        "command": "hello-world.insertDate",
        "key": "ctrl+shift+d",
        "mac": "cmd+shift+d",
        "when": "editorTextFocus"
      }
    ],
    "configuration": {
      "title": "Hello World",
      "properties": {
        "hello-world.dateFormat": {
          "type": "string",
          "default": "YYYY-MM-DD HH:mm:ss",
          "description": "Date format for insert command"
        }
      }
    },
    "menus": {
      "editor/context": [
        {
          "command": "hello-world.insertDate",
          "group": "1_modification"
        }
      ]
    }
  }
}

よくあるトラブルと解決策

開発中に遭遇しやすい問題と解決策をまとめます。

コマンドが見つからない

症状: コマンドパレットにコマンドが表示されない

解決策:

  1. package.jsonのcontributes.commandsでコマンドが正しく宣言されているか確認します
  2. コマンドIDがextension.tsのregisterCommandと一致しているか確認します
  3. Extension Development Hostをリロードします

拡張機能がアクティブ化されない

症状: activate関数が呼び出されない

解決策:

  1. activationEventsが適切に設定されているか確認します
  2. VSCode 1.74以降では、commandsの宣言で自動アクティベーションされます
  3. デバッグコンソールでエラーメッセージを確認します

公開時のエラー

症状: vsce publishがエラーで失敗する

解決策:

  1. PATの有効期限が切れていないか確認します
  2. PATのスコープに「Marketplace (Manage)」が含まれているか確認します
  3. engines.vscodeのバージョンが適切か確認します
  4. README.mdにSVG画像が含まれていないか確認します

TypeScriptのコンパイルエラー

症状: ビルド時にTypeScriptエラーが発生する

解決策:

  1. @types/vscodeのバージョンがengines.vscodeと一致しているか確認します
  2. npm installで依存関係を再インストールします
  3. npm run compileでエラーメッセージを確認します

拡張機能開発のアーキテクチャ図

VSCode拡張機能の基本的なアーキテクチャを図示します。

flowchart TB
    subgraph VSCode["VS Code"]
        direction TB
        A[Extension Host Process]
        B[Main Process]
        C[Renderer Process]
    end
    
    subgraph Extension["Your Extension"]
        direction TB
        D[package.json]
        E[extension.ts]
        F[Commands]
        G[Event Handlers]
    end
    
    subgraph API["VS Code API"]
        direction TB
        H[vscode.commands]
        I[vscode.window]
        J[vscode.workspace]
        K[vscode.languages]
    end
    
    D -->|Contribution Points| B
    E -->|activate/deactivate| A
    F --> H
    G --> I
    G --> J
    G --> K
    A <-->|IPC| B
    B <-->|IPC| C
sequenceDiagram
    participant U as User
    participant V as VS Code
    participant E as Extension
    participant A as VS Code API

    U->>V: Open file / Execute command
    V->>E: Activation Event triggered
    E->>E: activate() called
    E->>A: Register commands/handlers
    A-->>E: Disposable returned
    
    U->>V: Execute extension command
    V->>E: Command handler invoked
    E->>A: Call VS Code API
    A-->>E: Result returned
    E-->>V: Command completed
    V-->>U: Show result

参考リンク