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
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プロパティがデフォルトのキーバインドで、mac、linux、winで上書きできます。
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拡張機能のデバッグ手順を解説します。
デバッグの開始#
- 拡張機能プロジェクトをVSCodeで開きます
- F5キーを押すか、「Run and Debug」から「Run Extension」を選択します
- 新しいVSCodeウィンドウ(Extension Development Host)が開きます
- このウィンドウで拡張機能をテストします
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アカウントの作成#
- Azure DevOpsにアクセスします
- Microsoftアカウントでサインインします
- 組織を作成します
Personal Access Token(PAT)の取得#
- Azure DevOpsの右上にあるユーザー設定アイコンをクリックします
- 「Personal access tokens」を選択します
- 「New Token」をクリックします
- 以下の設定で作成します:
- Name:任意の名前
- Organization:All accessible organizations
- Scopes:Custom defined → Marketplace → Manage にチェック
- 「Create」をクリックし、表示されたトークンを安全な場所に保存します
パブリッシャーの作成#
- Visual Studio Marketplace管理ページにアクセスします
- 「Create publisher」をクリックします
- パブリッシャーIDと表示名を設定します
- パブリッシャー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"
}
]
}
}
}
|
よくあるトラブルと解決策#
開発中に遭遇しやすい問題と解決策をまとめます。
コマンドが見つからない#
症状: コマンドパレットにコマンドが表示されない
解決策:
- package.jsonの
contributes.commandsでコマンドが正しく宣言されているか確認します
- コマンドIDがextension.tsの
registerCommandと一致しているか確認します
- Extension Development Hostをリロードします
拡張機能がアクティブ化されない#
症状: activate関数が呼び出されない
解決策:
activationEventsが適切に設定されているか確認します
- VSCode 1.74以降では、commandsの宣言で自動アクティベーションされます
- デバッグコンソールでエラーメッセージを確認します
公開時のエラー#
症状: vsce publishがエラーで失敗する
解決策:
- PATの有効期限が切れていないか確認します
- PATのスコープに「Marketplace (Manage)」が含まれているか確認します
engines.vscodeのバージョンが適切か確認します
- README.mdにSVG画像が含まれていないか確認します
TypeScriptのコンパイルエラー#
症状: ビルド時にTypeScriptエラーが発生する
解決策:
@types/vscodeのバージョンがengines.vscodeと一致しているか確認します
npm installで依存関係を再インストールします
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| CsequenceDiagram
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参考リンク#