拡張機能開発
概要
Trae IDEの拡張機能開発により、IDEの機能を拡張し、カスタマイズできます。このガイドでは、拡張機能の作成、テスト、公開について説明します。
拡張機能の種類
テーマ拡張機能
json
{
"name": "my-custom-theme",
"type": "theme",
"version": "1.0.0",
"description": "カスタムテーマ拡張機能",
"contributes": {
"themes": [
{
"label": "My Dark Theme",
"uiTheme": "vs-dark",
"path": "./themes/dark-theme.json"
}
]
}
}言語サポート拡張機能
json
{
"name": "my-language-support",
"type": "language",
"version": "1.0.0",
"description": "新しい言語のサポート",
"contributes": {
"languages": [
{
"id": "mylang",
"aliases": ["MyLang", "mylang"],
"extensions": [".ml"],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "mylang",
"scopeName": "source.mylang",
"path": "./syntaxes/mylang.tmGrammar.json"
}
]
}
}機能拡張機能
json
{
"name": "my-feature-extension",
"type": "feature",
"version": "1.0.0",
"description": "新機能を追加する拡張機能",
"contributes": {
"commands": [
{
"command": "myextension.helloWorld",
"title": "Hello World",
"category": "My Extension"
}
],
"menus": {
"commandPalette": [
{
"command": "myextension.helloWorld",
"when": "editorTextFocus"
}
]
}
}
}開発環境のセットアップ
プロジェクト初期化
bash
# 拡張機能プロジェクトを作成
mkdir my-extension
cd my-extension
# package.jsonを初期化
npm init -y
# 必要な依存関係をインストール
npm install --save-dev @types/trae-ide
npm install --save-dev typescript
npm install --save-dev webpack webpack-cliプロジェクト構造
my-extension/
├── package.json # 拡張機能のマニフェスト
├── tsconfig.json # TypeScript設定
├── webpack.config.js # ビルド設定
├── src/
│ ├── extension.ts # メインエントリーポイント
│ ├── commands/ # コマンド実装
│ ├── providers/ # プロバイダー実装
│ └── utils/ # ユーティリティ
├── resources/ # リソースファイル
├── themes/ # テーマファイル
└── syntaxes/ # 構文ハイライト定義TypeScript設定
json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "out",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "out"]
}拡張機能API
基本的な拡張機能
typescript
// src/extension.ts
import * as trae from 'trae-ide';
export function activate(context: trae.ExtensionContext) {
console.log('拡張機能が有効化されました');
// コマンドを登録
const disposable = trae.commands.registerCommand('myextension.helloWorld', () => {
trae.window.showInformationMessage('Hello World from My Extension!');
});
context.subscriptions.push(disposable);
}
export function deactivate() {
console.log('拡張機能が無効化されました');
}コマンドの実装
typescript
// src/commands/myCommands.ts
import * as trae from 'trae-ide';
export class MyCommands {
static registerCommands(context: trae.ExtensionContext) {
// ファイル作成コマンド
const createFileCommand = trae.commands.registerCommand(
'myextension.createFile',
this.createFile
);
// コード整形コマンド
const formatCodeCommand = trae.commands.registerCommand(
'myextension.formatCode',
this.formatCode
);
context.subscriptions.push(createFileCommand, formatCodeCommand);
}
private static async createFile() {
const fileName = await trae.window.showInputBox({
prompt: 'ファイル名を入力してください',
placeHolder: 'example.txt'
});
if (fileName) {
const workspaceFolder = trae.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
const filePath = trae.Uri.joinPath(workspaceFolder.uri, fileName);
await trae.workspace.fs.writeFile(filePath, Buffer.from(''));
trae.window.showInformationMessage(`ファイル ${fileName} を作成しました`);
}
}
}
private static async formatCode() {
const editor = trae.window.activeTextEditor;
if (editor) {
const document = editor.document;
const text = document.getText();
// カスタム整形ロジック
const formattedText = this.customFormat(text);
const edit = new trae.WorkspaceEdit();
edit.replace(
document.uri,
new trae.Range(0, 0, document.lineCount, 0),
formattedText
);
await trae.workspace.applyEdit(edit);
}
}
private static customFormat(text: string): string {
// カスタム整形ロジックを実装
return text.split('\n').map(line => line.trim()).join('\n');
}
}プロバイダーの実装
typescript
// src/providers/completionProvider.ts
import * as trae from 'trae-ide';
export class MyCompletionProvider implements trae.CompletionItemProvider {
provideCompletionItems(
document: trae.TextDocument,
position: trae.Position,
token: trae.CancellationToken,
context: trae.CompletionContext
): trae.ProviderResult<trae.CompletionItem[] | trae.CompletionList> {
const completionItems: trae.CompletionItem[] = [];
// カスタム補完アイテムを作成
const item1 = new trae.CompletionItem('myFunction', trae.CompletionItemKind.Function);
item1.detail = 'カスタム関数';
item1.documentation = new trae.MarkdownString('カスタム関数の説明');
item1.insertText = new trae.SnippetString('myFunction(${1:param})');
const item2 = new trae.CompletionItem('myVariable', trae.CompletionItemKind.Variable);
item2.detail = 'カスタム変数';
item2.insertText = 'myVariable';
completionItems.push(item1, item2);
return completionItems;
}
}
// プロバイダーを登録
export function registerCompletionProvider(context: trae.ExtensionContext) {
const provider = new MyCompletionProvider();
const disposable = trae.languages.registerCompletionItemProvider(
{ scheme: 'file', language: 'javascript' },
provider,
'.' // トリガー文字
);
context.subscriptions.push(disposable);
}ホバープロバイダー
typescript
// src/providers/hoverProvider.ts
import * as trae from 'trae-ide';
export class MyHoverProvider implements trae.HoverProvider {
provideHover(
document: trae.TextDocument,
position: trae.Position,
token: trae.CancellationToken
): trae.ProviderResult<trae.Hover> {
const range = document.getWordRangeAtPosition(position);
const word = document.getText(range);
// カスタムホバー情報を提供
if (word === 'myFunction') {
const contents = new trae.MarkdownString();
contents.appendCodeblock('function myFunction(param: string): void', 'typescript');
contents.appendMarkdown('カスタム関数の詳細説明');
return new trae.Hover(contents, range);
}
return null;
}
}設定とカスタマイゼーション
拡張機能設定
json
{
"contributes": {
"configuration": {
"title": "My Extension",
"properties": {
"myextension.enableFeature": {
"type": "boolean",
"default": true,
"description": "機能を有効にする"
},
"myextension.customPath": {
"type": "string",
"default": "",
"description": "カスタムパス"
},
"myextension.logLevel": {
"type": "string",
"enum": ["debug", "info", "warn", "error"],
"default": "info",
"description": "ログレベル"
}
}
}
}
}設定の読み取り
typescript
// src/utils/config.ts
import * as trae from 'trae-ide';
export class ConfigManager {
private static readonly EXTENSION_NAME = 'myextension';
static get<T>(key: string, defaultValue?: T): T {
const config = trae.workspace.getConfiguration(this.EXTENSION_NAME);
return config.get<T>(key, defaultValue as T);
}
static async set(key: string, value: any, target?: trae.ConfigurationTarget) {
const config = trae.workspace.getConfiguration(this.EXTENSION_NAME);
await config.update(key, value, target);
}
static isFeatureEnabled(): boolean {
return this.get<boolean>('enableFeature', true);
}
static getCustomPath(): string {
return this.get<string>('customPath', '');
}
static getLogLevel(): string {
return this.get<string>('logLevel', 'info');
}
}テストとデバッグ
ユニットテスト
typescript
// src/test/extension.test.ts
import * as assert from 'assert';
import * as trae from 'trae-ide';
import { MyCommands } from '../commands/myCommands';
suite('Extension Test Suite', () => {
trae.window.showInformationMessage('テストを開始します');
test('コマンド登録テスト', () => {
// コマンドが正しく登録されているかテスト
const commands = trae.commands.getCommands();
assert.ok(commands.includes('myextension.helloWorld'));
});
test('設定読み取りテスト', () => {
// 設定が正しく読み取れるかテスト
const config = trae.workspace.getConfiguration('myextension');
assert.strictEqual(typeof config.get('enableFeature'), 'boolean');
});
});デバッグ設定
json
{
"version": "0.2.0",
"configurations": [
{
"name": "拡張機能をデバッグ",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: compile"
}
]
}ビルドとパッケージング
Webpack設定
javascript
// webpack.config.js
const path = require('path');
module.exports = {
target: 'node',
entry: './src/extension.ts',
output: {
path: path.resolve(__dirname, 'out'),
filename: 'extension.js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
},
devtool: 'source-map',
externals: {
'trae-ide': 'commonjs trae-ide'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
}
};ビルドスクリプト
json
{
"scripts": {
"compile": "webpack --mode development",
"compile-production": "webpack --mode production",
"watch": "webpack --mode development --watch",
"test": "node ./out/test/runTest.js",
"package": "vsce package",
"publish": "vsce publish"
}
}公開とディストリビューション
拡張機能のパッケージング
bash
# 拡張機能をパッケージング
npm run package
# 生成されたVSIXファイルをインストール
code --install-extension my-extension-1.0.0.vsixマーケットプレイスへの公開
bash
# 公開用トークンを設定
vsce login <publisher-name>
# 拡張機能を公開
npm run publish継続的インテグレーション
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Compile
run: npm run compile
- name: Run tests
run: npm test
- name: Package extension
run: npm run package
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: extension-package
path: '*.vsix'ベストプラクティス
パフォーマンス最適化
typescript
// 遅延読み込み
export async function activate(context: trae.ExtensionContext) {
// 重い処理は必要時まで遅延
context.subscriptions.push(
trae.commands.registerCommand('myextension.heavyOperation', async () => {
const { performHeavyOperation } = await import('./heavyModule');
await performHeavyOperation();
})
);
}
// キャッシュの活用
class CacheManager {
private static cache = new Map<string, any>();
static get<T>(key: string): T | undefined {
return this.cache.get(key);
}
static set<T>(key: string, value: T): void {
this.cache.set(key, value);
}
}エラーハンドリング
typescript
// src/utils/errorHandler.ts
import * as trae from 'trae-ide';
export class ErrorHandler {
static handleError(error: Error, context?: string) {
console.error(`[My Extension] ${context || 'Error'}:`, error);
trae.window.showErrorMessage(
`エラーが発生しました: ${error.message}`
);
}
static async safeExecute<T>(
operation: () => Promise<T>,
context: string
): Promise<T | undefined> {
try {
return await operation();
} catch (error) {
this.handleError(error as Error, context);
return undefined;
}
}
}国際化対応
typescript
// src/utils/i18n.ts
import * as trae from 'trae-ide';
export class I18n {
private static messages: { [key: string]: string } = {
'hello.world': 'Hello World',
'file.created': 'ファイルが作成されました',
'error.occurred': 'エラーが発生しました'
};
static getMessage(key: string, ...args: string[]): string {
let message = this.messages[key] || key;
args.forEach((arg, index) => {
message = message.replace(`{${index}}`, arg);
});
return message;
}
}トラブルシューティング
よくある問題
Q: 拡張機能が読み込まれない A: package.jsonのactivationEventsを確認し、適切なイベントが設定されているか確認してください。
Q: コマンドが表示されない A: contributes.commandsでコマンドが正しく定義され、registerCommandで登録されているか確認してください。
Q: TypeScriptコンパイルエラー A: @types/trae-ideが正しくインストールされ、tsconfig.jsonの設定が適切か確認してください。
デバッグのヒント
typescript
// デバッグ用ログ出力
const outputChannel = trae.window.createOutputChannel('My Extension');
function debugLog(message: string) {
outputChannel.appendLine(`[${new Date().toISOString()}] ${message}`);
}
// 拡張機能の状態確認
function checkExtensionState() {
debugLog('拡張機能の状態をチェック中...');
debugLog(`アクティブエディタ: ${trae.window.activeTextEditor?.document.fileName || 'なし'}`);
debugLog(`ワークスペース: ${trae.workspace.workspaceFolders?.length || 0} フォルダ`);
}高度な機能
カスタムビューの作成
typescript
// src/views/customView.ts
import * as trae from 'trae-ide';
export class CustomViewProvider implements trae.TreeDataProvider<CustomItem> {
private _onDidChangeTreeData: trae.EventEmitter<CustomItem | undefined | null | void> = new trae.EventEmitter<CustomItem | undefined | null | void>();
readonly onDidChangeTreeData: trae.Event<CustomItem | undefined | null | void> = this._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: CustomItem): trae.TreeItem {
return element;
}
getChildren(element?: CustomItem): Thenable<CustomItem[]> {
if (!element) {
return Promise.resolve([
new CustomItem('アイテム1', trae.TreeItemCollapsibleState.None),
new CustomItem('アイテム2', trae.TreeItemCollapsibleState.None)
]);
}
return Promise.resolve([]);
}
}
class CustomItem extends trae.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: trae.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.tooltip = `${this.label} - カスタムアイテム`;
}
}このガイドを参考に、Trae IDE用の強力で使いやすい拡張機能を開発してください。