Extension Development Guide
This comprehensive guide covers everything you need to know about developing extensions for Trae, from basic concepts to advanced techniques.
Getting Started
Prerequisites
- Node.js 16.x or higher
- npm or yarn package manager
- TypeScript knowledge (recommended)
- Basic understanding of Trae's architecture
Development Environment Setup
bash
# Install Trae Extension CLI
npm install -g @trae/extension-cli
# Create a new extension project
trae-ext create my-extension
cd my-extension
# Install dependencies
npm install
# Start development mode
npm run devProject Structure
my-extension/
├── src/
│ ├── extension.ts # Main extension entry point
│ ├── commands/ # Command implementations
│ ├── providers/ # Language service providers
│ ├── views/ # Custom views and panels
│ └── utils/ # Utility functions
├── resources/
│ ├── icons/ # Extension icons
│ └── themes/ # Custom themes
├── package.json # Extension manifest
├── tsconfig.json # TypeScript configuration
└── webpack.config.js # Build configurationExtension Manifest
package.json Configuration
json
{
"name": "my-extension",
"displayName": "My Awesome Extension",
"description": "A sample extension for Trae",
"version": "1.0.0",
"publisher": "your-publisher-name",
"engines": {
"trae": "^3.0.0"
},
"categories": [
"Programming Languages",
"Themes",
"Debuggers"
],
"keywords": [
"typescript",
"javascript",
"productivity"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "myExtension.helloWorld",
"title": "Hello World",
"category": "My Extension"
}
],
"keybindings": [
{
"command": "myExtension.helloWorld",
"key": "ctrl+shift+h",
"mac": "cmd+shift+h",
"when": "editorTextFocus"
}
],
"menus": {
"commandPalette": [
{
"command": "myExtension.helloWorld",
"when": "true"
}
]
},
"configuration": {
"title": "My Extension",
"properties": {
"myExtension.enable": {
"type": "boolean",
"default": true,
"description": "Enable My Extension"
}
}
}
},
"activationEvents": [
"onCommand:myExtension.helloWorld",
"onLanguage:typescript"
],
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"package": "trae-ext package",
"publish": "trae-ext publish"
},
"devDependencies": {
"@trae/extension-api": "^3.0.0",
"@types/node": "^16.x",
"typescript": "^4.x"
}
}Basic Extension Structure
Main Extension File
typescript
// src/extension.ts
import * as trae from '@trae/extension-api';
// Extension activation function
export function activate(context: trae.ExtensionContext) {
console.log('My Extension is now active!');
// Register commands
const disposable = trae.commands.registerCommand(
'myExtension.helloWorld',
() => {
trae.window.showInformationMessage('Hello World from My Extension!');
}
);
context.subscriptions.push(disposable);
// Register other features
registerLanguageFeatures(context);
registerCustomViews(context);
registerEventHandlers(context);
}
// Extension deactivation function
export function deactivate() {
console.log('My Extension is now deactivated!');
}
function registerLanguageFeatures(context: trae.ExtensionContext) {
// Register language features like completion, hover, etc.
const provider = new MyCompletionProvider();
const disposable = trae.languages.registerCompletionItemProvider(
{ scheme: 'file', language: 'typescript' },
provider,
'.', '"', "'"
);
context.subscriptions.push(disposable);
}
function registerCustomViews(context: trae.ExtensionContext) {
// Register custom views and panels
const provider = new MyTreeDataProvider();
trae.window.createTreeView('myExtension.explorer', {
treeDataProvider: provider,
showCollapseAll: true
});
}
function registerEventHandlers(context: trae.ExtensionContext) {
// Register event handlers
const disposable = trae.workspace.onDidChangeTextDocument((event) => {
console.log('Document changed:', event.document.fileName);
});
context.subscriptions.push(disposable);
}Core Extension APIs
Commands API
typescript
// Register a command
const disposable = trae.commands.registerCommand(
'myExtension.myCommand',
(arg1: string, arg2: number) => {
// Command implementation
trae.window.showInformationMessage(`Args: ${arg1}, ${arg2}`);
}
);
// Execute a command
trae.commands.executeCommand('workbench.action.files.save');
// Get all available commands
const commands = await trae.commands.getCommands();Window API
typescript
// Show messages
trae.window.showInformationMessage('Info message');
trae.window.showWarningMessage('Warning message');
trae.window.showErrorMessage('Error message');
// Show input box
const input = await trae.window.showInputBox({
prompt: 'Enter your name',
placeHolder: 'Name',
validateInput: (value) => {
return value.length < 3 ? 'Name must be at least 3 characters' : null;
}
});
// Show quick pick
const selection = await trae.window.showQuickPick(
['Option 1', 'Option 2', 'Option 3'],
{
placeHolder: 'Select an option',
canPickMany: false
}
);
// Show progress
trae.window.withProgress({
location: trae.ProgressLocation.Notification,
title: 'Processing...',
cancellable: true
}, async (progress, token) => {
for (let i = 0; i < 100; i++) {
if (token.isCancellationRequested) {
break;
}
progress.report({ increment: 1, message: `Step ${i + 1}` });
await new Promise(resolve => setTimeout(resolve, 100));
}
});Workspace API
typescript
// Get workspace folders
const workspaceFolders = trae.workspace.workspaceFolders;
// Read file
const fileUri = trae.Uri.file('/path/to/file.txt');
const content = await trae.workspace.fs.readFile(fileUri);
// Write file
const data = Buffer.from('Hello, World!', 'utf8');
await trae.workspace.fs.writeFile(fileUri, data);
// Watch files
const watcher = trae.workspace.createFileSystemWatcher('**/*.ts');
watcher.onDidCreate((uri) => {
console.log('File created:', uri.fsPath);
});
watcher.onDidChange((uri) => {
console.log('File changed:', uri.fsPath);
});
watcher.onDidDelete((uri) => {
console.log('File deleted:', uri.fsPath);
});
// Get configuration
const config = trae.workspace.getConfiguration('myExtension');
const enableFeature = config.get<boolean>('enable', true);
// Update configuration
await config.update('enable', false, trae.ConfigurationTarget.Global);Languages API
typescript
// Completion provider
class MyCompletionProvider implements trae.CompletionItemProvider {
provideCompletionItems(
document: trae.TextDocument,
position: trae.Position,
token: trae.CancellationToken,
context: trae.CompletionContext
): trae.ProviderResult<trae.CompletionItem[]> {
const completionItems: trae.CompletionItem[] = [];
// Add completion items
const item = new trae.CompletionItem('myFunction', trae.CompletionItemKind.Function);
item.detail = 'My custom function';
item.documentation = 'This is a custom function provided by my extension';
item.insertText = new trae.SnippetString('myFunction(${1:param})$0');
completionItems.push(item);
return completionItems;
}
}
// Hover provider
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 === 'myKeyword') {
return new trae.Hover(
new trae.MarkdownString('**My Keyword**: This is a special keyword'),
range
);
}
}
}
// Diagnostic provider
class MyDiagnosticProvider {
private diagnosticCollection: trae.DiagnosticCollection;
constructor() {
this.diagnosticCollection = trae.languages.createDiagnosticCollection('myExtension');
}
updateDiagnostics(document: trae.TextDocument) {
const diagnostics: trae.Diagnostic[] = [];
// Analyze document and create diagnostics
const text = document.getText();
const regex = /TODO:/g;
let match;
while ((match = regex.exec(text)) !== null) {
const startPos = document.positionAt(match.index);
const endPos = document.positionAt(match.index + match[0].length);
const range = new trae.Range(startPos, endPos);
const diagnostic = new trae.Diagnostic(
range,
'TODO comment found',
trae.DiagnosticSeverity.Information
);
diagnostic.source = 'My Extension';
diagnostics.push(diagnostic);
}
this.diagnosticCollection.set(document.uri, diagnostics);
}
}Advanced Features
Custom Views and Panels
typescript
// Tree view provider
class MyTreeDataProvider implements trae.TreeDataProvider<MyTreeItem> {
private _onDidChangeTreeData: trae.EventEmitter<MyTreeItem | undefined | null | void> = new trae.EventEmitter<MyTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: trae.Event<MyTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
getTreeItem(element: MyTreeItem): trae.TreeItem {
return element;
}
getChildren(element?: MyTreeItem): Thenable<MyTreeItem[]> {
if (!element) {
// Return root items
return Promise.resolve([
new MyTreeItem('Item 1', trae.TreeItemCollapsibleState.Collapsed),
new MyTreeItem('Item 2', trae.TreeItemCollapsibleState.None)
]);
} else {
// Return children of element
return Promise.resolve([
new MyTreeItem('Child 1', trae.TreeItemCollapsibleState.None),
new MyTreeItem('Child 2', trae.TreeItemCollapsibleState.None)
]);
}
}
refresh(): void {
this._onDidChangeTreeData.fire();
}
}
class MyTreeItem extends trae.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: trae.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.tooltip = `Tooltip for ${this.label}`;
this.description = 'Description';
}
iconPath = {
light: path.join(__filename, '..', '..', 'resources', 'light', 'item.svg'),
dark: path.join(__filename, '..', '..', 'resources', 'dark', 'item.svg')
};
contextValue = 'myTreeItem';
}
// Webview panel
function createWebviewPanel(context: trae.ExtensionContext) {
const panel = trae.window.createWebviewPanel(
'myExtension.webview',
'My Extension Panel',
trae.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
trae.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
}
function getWebviewContent(): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Extension Panel</title>
<style>
body {
font-family: var(--trae-font-family);
color: var(--trae-foreground);
background-color: var(--trae-editor-background);
padding: 20px;
}
button {
background-color: var(--trae-button-background);
color: var(--trae-button-foreground);
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>My Extension Panel</h1>
<p>This is a custom webview panel.</p>
<button onclick="sendMessage()">Send Message</button>
<script>
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({
command: 'alert',
text: 'Hello from webview!'
});
}
</script>
</body>
</html>
`;
}Configuration and Settings
typescript
// Define configuration schema in package.json
{
"contributes": {
"configuration": {
"title": "My Extension",
"properties": {
"myExtension.feature.enabled": {
"type": "boolean",
"default": true,
"description": "Enable the main feature"
},
"myExtension.feature.timeout": {
"type": "number",
"default": 5000,
"minimum": 1000,
"maximum": 30000,
"description": "Timeout in milliseconds"
},
"myExtension.feature.mode": {
"type": "string",
"enum": ["auto", "manual", "disabled"],
"default": "auto",
"description": "Operation mode"
}
}
}
}
}
// Access configuration in code
class ConfigurationManager {
private config: trae.WorkspaceConfiguration;
constructor() {
this.config = trae.workspace.getConfiguration('myExtension');
// Listen for configuration changes
trae.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('myExtension')) {
this.updateConfiguration();
}
});
}
get isEnabled(): boolean {
return this.config.get<boolean>('feature.enabled', true);
}
get timeout(): number {
return this.config.get<number>('feature.timeout', 5000);
}
get mode(): string {
return this.config.get<string>('feature.mode', 'auto');
}
async setEnabled(enabled: boolean): Promise<void> {
await this.config.update('feature.enabled', enabled, trae.ConfigurationTarget.Global);
}
private updateConfiguration(): void {
this.config = trae.workspace.getConfiguration('myExtension');
// Notify other components about configuration changes
}
}Testing Extensions
typescript
// test/extension.test.ts
import * as assert from 'assert';
import * as trae from '@trae/extension-api';
import * as myExtension from '../src/extension';
suite('Extension Test Suite', () => {
trae.window.showInformationMessage('Start all tests.');
test('Extension should be present', () => {
assert.ok(trae.extensions.getExtension('publisher.my-extension'));
});
test('Should register commands', async () => {
const commands = await trae.commands.getCommands();
assert.ok(commands.includes('myExtension.helloWorld'));
});
test('Command should execute', async () => {
await trae.commands.executeCommand('myExtension.helloWorld');
// Add assertions based on command behavior
});
test('Configuration should work', () => {
const config = trae.workspace.getConfiguration('myExtension');
assert.strictEqual(typeof config.get('feature.enabled'), 'boolean');
});
});Launch Configuration for Testing
json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${workspaceFolder}/npm: compile"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${workspaceFolder}/npm: compile"
}
]
}Publishing Extensions
Packaging
bash
# Install packaging tool
npm install -g @trae/extension-cli
# Package extension
trae-ext package
# This creates a .trex file (Trae Extension)Publishing to Marketplace
bash
# Login to marketplace
trae-ext login
# Publish extension
trae-ext publish
# Publish specific version
trae-ext publish --version 1.0.1
# Publish pre-release
trae-ext publish --pre-releaseMarketplace Configuration
json
// package.json marketplace fields
{
"publisher": "your-publisher-name",
"repository": {
"type": "git",
"url": "https://github.com/username/my-extension.git"
},
"bugs": {
"url": "https://github.com/username/my-extension/issues"
},
"homepage": "https://github.com/username/my-extension#readme",
"license": "MIT",
"icon": "images/icon.png",
"galleryBanner": {
"color": "#C80000",
"theme": "dark"
},
"badges": [
{
"url": "https://img.shields.io/badge/build-passing-brightgreen",
"href": "https://github.com/username/my-extension/actions",
"description": "Build Status"
}
]
}Best Practices
Performance
- Lazy Loading: Only activate when needed
- Efficient Event Handling: Debounce frequent events
- Memory Management: Dispose of resources properly
- Async Operations: Use async/await for I/O operations
typescript
// Lazy loading example
let heavyFeature: HeavyFeature | undefined;
function getHeavyFeature(): HeavyFeature {
if (!heavyFeature) {
heavyFeature = new HeavyFeature();
}
return heavyFeature;
}
// Debouncing example
let timeout: NodeJS.Timeout | undefined;
trae.workspace.onDidChangeTextDocument((event) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
// Process document change
processDocumentChange(event.document);
}, 500);
});
// Proper disposal
export function deactivate() {
if (heavyFeature) {
heavyFeature.dispose();
}
}Error Handling
typescript
// Graceful error handling
async function safeOperation() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
console.error('Operation failed:', error);
trae.window.showErrorMessage(`Operation failed: ${error.message}`);
return null;
}
}
// User-friendly error messages
function handleError(error: Error, context: string) {
const message = `${context}: ${error.message}`;
console.error(message, error);
// Show appropriate message based on error type
if (error.name === 'NetworkError') {
trae.window.showErrorMessage('Network connection failed. Please check your internet connection.');
} else {
trae.window.showErrorMessage(message);
}
}Security
typescript
// Validate user input
function validateInput(input: string): boolean {
// Sanitize and validate input
const sanitized = input.trim();
if (sanitized.length === 0) {
return false;
}
// Check for malicious patterns
const dangerousPatterns = [/<script/i, /javascript:/i, /on\w+=/i];
return !dangerousPatterns.some(pattern => pattern.test(sanitized));
}
// Secure webview content
function getSecureWebviewContent(): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${getNonce()}'">
</head>
<body>
<!-- Content -->
</body>
</html>
`;
}
function getNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}Debugging Extensions
Debug Configuration
json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: compile",
"sourceMaps": true,
"smartStep": true,
"skipFiles": [
"<node_internals>/**"
]
}
]
}Logging
typescript
// Create output channel for logging
const outputChannel = trae.window.createOutputChannel('My Extension');
class Logger {
static info(message: string): void {
const timestamp = new Date().toISOString();
outputChannel.appendLine(`[INFO ${timestamp}] ${message}`);
}
static error(message: string, error?: Error): void {
const timestamp = new Date().toISOString();
outputChannel.appendLine(`[ERROR ${timestamp}] ${message}`);
if (error) {
outputChannel.appendLine(`Stack trace: ${error.stack}`);
}
}
static show(): void {
outputChannel.show();
}
}Migration Guide
From Extension API v2 to v3
typescript
// v2 (deprecated)
trae.workspace.rootPath; // Deprecated
trae.window.showInputBox(options, token); // Changed signature
// v3 (current)
trae.workspace.workspaceFolders?.[0]?.uri.fsPath; // Use workspaceFolders
trae.window.showInputBox(options); // Simplified signature
// Command registration changes
// v2
trae.commands.registerCommand('command', callback);
// v3 (enhanced)
trae.commands.registerCommand('command', callback, thisArg);