Visual Studio Code has become the editor of choice for millions of developers, largely due to its extensibility. In 2025, the extension ecosystem is more powerful than ever, enabling developers to create sophisticated tools that transform their development experience.
Getting Started with Extension Development
Prerequisites & Setup
bash
# Install Node.js (18+ recommended)
node --version
# Install Yeoman and VS Code Extension Generator
npm install -g yo generator-code
# Generate extension scaffolding
yo code
# Choose options:
? Type of extension: New Extension (TypeScript)
? Extension name: my-custom-extension
? Identifier: my-custom-extension
? Description: My awesome VS Code extension
? Initialize a git repository: Yes
? Bundle the code: Yes (using webpack)
? Package manager: npm
# Navigate to extension folder
cd my-custom-extension
# Open in VS Code
code .Project Structure
my-custom-extension/
├── src/
│ ├── extension.ts # Main entry point
│ ├── test/ # Extension tests
│ └── types/ # TypeScript types
├── resources/ # Icons, images
├── vscode/ # VS Code API
├── .vscode/ # VS Code configuration
│ ├── launch.json # Debug configuration
│ ├── settings.json # Editor settings
│ └── tasks.json # Build tasks
├── package.json # Extension manifest
├── tsconfig.json # TypeScript config
└── webpack.config.js # Bundler configUnderstanding package.json
Essential Manifest Fields
json
{
"name": "my-custom-extension",
"displayName": "My Custom Extension",
"description": "My awesome VS Code extension",
"version": "1.0.0",
"publisher": "your-publisher-name",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other",
"Programming Languages"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "myextension.helloWorld",
"title": "Hello World"
}
],
"keybindings": [
{
"command": "myextension.helloWorld",
"key": "ctrl+shift+h",
"mac": "cmd+shift+h"
}
],
"menus": {
"editor/context": [
{
"command": "myextension.helloWorld",
"group": "navigation"
}
]
}
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"@types/node": "20.x",
"typescript": "^5.3.3"
}
}Core VS Code API Concepts
Extension Activation
typescript
// src/extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
console.log('Extension "my-custom-extension" is now active!');
// Register commands
let disposable = vscode.commands.registerCommand(
'myextension.helloWorld',
() => {
vscode.window.showInformationMessage('Hello World from My Extension!');
}
);
context.subscriptions.push(disposable);
}
export function deactivate() {
console.log('Extension "my-custom-extension" is now deactivated!');
}Extension Context
typescript
export function activate(context: vscode.ExtensionContext) {
// Global state (persisted across sessions)
context.globalState.update('usageCount', 42);
// Workspace state (workspace-specific)
context.workspaceState.update('workspaceConfig', { theme: 'dark' });
// Secrets (for sensitive data)
context.secrets.store('apiToken', 'your-token-here');
// Extension storage path
const storagePath = context.storageUri;
console.log('Storage path:', storagePath?.fsPath);
// Global storage path (shared across workspaces)
const globalStoragePath = context.globalStorageUri;
console.log('Global storage:', globalStoragePath.fsPath);
// Extension path
const extensionPath = context.extensionPath;
console.log('Extension path:', extensionPath);
// Subscriptions (dispose on deactivation)
const statusBarItem = vscode.window.createStatusBarItem();
context.subscriptions.push(statusBarItem);
}Building Practical Extensions
Extension 1: Custom Code Snippet Generator
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const generateSnippet = vscode.commands.registerCommand(
'snippetGen.generate',
async () => {
// Get user input
const snippetName = await vscode.window.showInputBox({
prompt: 'Enter snippet name',
placeHolder: 'mySnippet'
});
if (!snippetName) {
return;
}
// Get selected text
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
// Generate snippet
const snippet = generateSnippetCode(snippetName, selectedText);
// Insert at cursor
editor.edit(editBuilder => {
editBuilder.insert(selection.start, snippet);
});
vscode.window.showInformationMessage(
`Snippet ${snippetName} generated!`
);
}
);
context.subscriptions.push(generateSnippet);
}
function generateSnippetCode(name: string, code: string): string {
// Convert code to snippet format
const tabstops = createTabstops(code);
return `
// ${name}
${tabstops}
`;
}
function createTabstops(code: string): string {
// Auto-generate tabstops for function parameters, etc.
let tabIndex = 1;
return code.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g, (match) => {
if (match === 'function' || match === 'const' || match === 'let') {
return match;
}
return `${match}${tabIndex++}`;
});
}Extension 2: File Watcher & Auto-Formatter
typescript
import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
// Create file system watcher
const watcher = vscode.workspace.createFileSystemWatcher(
'**/*.{js,ts,jsx,tsx}',
false, // ignoreCreateEvents
false, // ignoreChangeEvents
true // ignoreDeleteEvents
);
watcher.onDidDelete(async (uri) => {
// Check if it was a test file
if (uri.fsPath.includes('.test.') || uri.fsPath.includes('.spec.')) {
const answer = await vscode.window.showWarningMessage(
`Test file deleted: ${path.basename(uri.fsPath)}`,
'Restore',
'Dismiss'
);
if (answer === 'Restore') {
// Restore from git
const terminal = vscode.window.createTerminal('Restore Test');
terminal.sendText(`git checkout HEAD -- ${uri.fsPath}`);
}
}
});
// Auto-format on save
const formatOnSave = vscode.workspace.onDidSaveTextDocument(
async (document) => {
const config = vscode.workspace.getConfiguration('autoFormatter');
if (config.get('enabled', false)) {
const edits = await vscode.commands.executeCommand<vscode.TextEdit[]>(
'vscode.executeFormatDocumentProvider',
document.uri,
{
tabSize: 2,
insertSpaces: true
}
);
if (edits) {
const edit = new vscode.WorkspaceEdit();
edit.set(document.uri, edits);
await vscode.workspace.applyEdit(edit);
}
}
}
);
context.subscriptions.push(watcher, formatOnSave);
}Extension 3: Custom Sidebar View
typescript
import * as vscode from 'vscode';
class TaskItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly command?: vscode.Command
) {
super(label, collapsibleState);
this.tooltip = `${this.label}`;
this.description = this.label;
}
}
class TaskProvider implements vscode.TreeDataProvider<TaskItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<
TaskItem | undefined | void
>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private tasks: TaskItem[] = [];
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: TaskItem): vscode.TreeItem {
return element;
}
getChildren(element?: TaskItem): Thenable<TaskItem[]> {
if (!element) {
// Root items
return Promise.resolve(this.tasks);
}
return Promise.resolve([]);
}
addTask(label: string): void {
const task = new TaskItem(
label,
vscode.TreeItemCollapsibleState.None,
{
command: 'taskList.openTask',
title: 'Open Task',
arguments: [label]
}
);
this.tasks.push(task);
this.refresh();
}
}
export function activate(context: vscode.ExtensionContext) {
const taskProvider = new TaskProvider();
vscode.window.registerTreeDataProvider('taskList', taskProvider);
vscode.commands.registerCommand('taskList.refresh', () => {
taskProvider.refresh();
});
vscode.commands.registerCommand('taskList.addTask', async () => {
const taskName = await vscode.window.showInputBox({
prompt: 'Enter task name',
placeHolder: 'New task'
});
if (taskName) {
taskProvider.addTask(taskName);
}
});
vscode.commands.registerCommand('taskList.openTask', (taskName: string) => {
vscode.window.showInformationMessage(`Opening task: ${taskName}`);
});
}Advanced Extension Features
Language Server Protocol (LSP) Integration
typescript
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions
} from 'vscode-languageclient/node';
export function activate(context: vscode.ExtensionContext) {
const serverOptions: ServerOptions = {
command: 'node',
args: [context.asAbsolutePath('server/dist/server.js')],
options: {
cwd: context.extensionPath
}
};
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: 'file', language: 'mylang' }
],
synchronize: {
configurationSection: 'myLanguageServer'
}
};
const client = new LanguageClient(
'myLanguageServer',
'My Language Server',
serverOptions,
clientOptions
);
context.subscriptions.push(client.start());
}Webview Panels
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const showPreview = vscode.commands.registerCommand(
'preview.show',
() => {
const panel = vscode.window.createWebviewPanel(
'preview',
'Preview',
vscode.ViewColumn.Two,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = getWebviewContent();
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
return;
case 'update':
panel.title = message.text;
return;
}
},
undefined,
context.subscriptions
);
}
);
context.subscriptions.push(showPreview);
}
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>Preview</title>
</head>
<body>
<h1>Webview Preview</h1>
<button onclick="sendAlert()">Click Me</button>
<script>
const vscode = acquireVsCodeApi();
function sendAlert() {
vscode.postMessage({
command: 'alert',
text: 'Hello from webview!'
});
}
</script>
</body>
</html>`;
}Configuration Settings
typescript
// package.json - contributes section
{
"contributes": {
"configuration": {
"title": "My Extension",
"properties": {
"myExtension.enable": {
"type": "boolean",
"default": true,
"description": "Enable the extension"
},
"myExtension.maxItems": {
"type": "number",
"default": 100,
"minimum": 1,
"maximum": 1000,
"description": "Maximum number of items"
},
"myExtension.colors": {
"type": "array",
"items": {
"type": "string"
},
"default": ["red", "green", "blue"],
"description": "Color palette"
},
"myExtension.advanced": {
"type": "object",
"properties": {
"timeout": {
"type": "number",
"default": 5000
},
"retry": {
"type": "boolean",
"default": true
}
}
}
}
}
}
}
// Access configuration in extension
const config = vscode.workspace.getConfiguration('myExtension');
const isEnabled = config.get('enable', true);
const maxItems = config.get('maxItems', 100);
// Watch for configuration changes
const configChangeListener = vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExtension')) {
console.log('Configuration changed!');
}
});Testing Extensions
Unit Testing
typescript
// src/test/extension.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
test('Command registration', async () => {
const commands = await vscode.commands.getCommands(true);
assert.ok(commands.includes('myextension.helloWorld'));
});
});Integration Testing
typescript
import * as vscode from 'vscode';
import * as path from 'path';
suite('Extension Integration Tests', () => {
let extensionContext: vscode.ExtensionContext;
suiteSetup(async () => {
// Activate extension
const ext = vscode.extensions.getExtension('publisher.my-extension');
if (ext) {
await ext.activate();
extensionContext = ext.exports;
}
});
test('Create new file', async () => {
const uri = vscode.Uri.parse(
'untitled:' + path.join(vscode.workspace.rootPath || '', 'test.txt')
);
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc);
await editor.edit(edit => {
edit.insert(new vscode.Position(0, 0), 'Hello, World!');
});
assert.strictEqual(doc.getText(), 'Hello, World!');
// Clean up
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
});Publishing Extensions
Prepare for Publishing
bash
# Install vsce (Visual Studio Code Extensions)
npm install -g @vscode/vsce
# Create publisher account
# Visit: https://marketplace.visualstudio.com/manage
# Login to vsce
vsce login your-publisher-name
# Package extension
vsce package
# Publish extension
vsce publish
# Publish specific version
vsce publish minor
vsce publish patch
vsce publish majorpackage.json for Publishing
json
{
"name": "my-custom-extension",
"displayName": "My Custom Extension",
"description": "My awesome VS Code extension",
"version": "1.0.0",
"publisher": "your-publisher-name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/my-custom-extension"
},
"bugs": {
"url": "https://github.com/yourusername/my-custom-extension/issues"
},
"homepage": "https://github.com/yourusername/my-custom-extension#readme",
"keywords": [
"vscode",
"extension",
"productivity"
],
"icon": "resources/icon.png",
"galleryBanner": {
"color": "#007acc",
"theme": "dark"
},
"badges": [
{
"url": "https://img.shields.io/badge/license-MIT-blue.svg",
"href": "https://github.com/yourusername/my-custom-extension/blob/main/LICENSE",
"description": "License: MIT"
}
]
}Best Practices for 2025
Performance Optimization
typescript
// Use disposable pattern correctly
class MyExtension {
private disposables: vscode.Disposable[] = [];
constructor() {
this.disposables.push(
vscode.commands.registerCommand('myext.cmd', this.handleCommand)
);
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
}
// Debounce expensive operations
import { debounce } from 'lodash';
const debouncedUpdate = debounce(() => {
// Expensive operation
}, 300);
// Use workers for heavy computation
const worker = new Worker(
context.asAbsolutePath('worker.js'),
{ type: 'module' }
);Error Handling
typescript
// Comprehensive error handling
async function safeExecute<T>(
fn: () => Promise<T>,
defaultValue: T
): Promise<T> {
try {
return await fn();
} catch (error) {
vscode.window.showErrorMessage(`Error: ${error}`);
console.error('Extension error:', error);
return defaultValue;
}
}
// Usage
const result = await safeExecute(
() => fetchUserData(),
null
);User Experience
typescript
// Show progress for long operations
async function longRunningOperation() {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Processing...',
cancellable: true
},
async (progress, token) => {
for (let i = 0; i < 100; i++) {
if (token.isCancellationRequested) {
vscode.window.showInformationMessage('Operation cancelled');
return;
}
progress.report({ increment: 1, message: `${i}%` });
await new Promise(resolve => setTimeout(resolve, 100));
}
}
);
}
// Quick pick for user selection
async function selectOption() {
const options = [
{ label: 'Option 1', description: 'First option' },
{ label: 'Option 2', description: 'Second option' },
{ label: 'Option 3', description: 'Third option' }
];
const selected = await vscode.window.showQuickPick(options, {
placeHolder: 'Select an option'
});
if (selected) {
vscode.window.showInformationMessage(`Selected: ${selected.label}`);
}
}Extension Ideas for 2025
AI-Powered Extensions
typescript
// Integrate with AI APIs
async function getAISuggestion(code: string) {
const response = await fetch('https://api.openai.com/v1/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4',
prompt: `Suggest improvements for this code:\n\n${code}`,
max_tokens: 500
})
});
const data = await response.json();
return data.choices[0].text;
}Remote Development Support
typescript
// Support for remote workspaces
const isRemote = vscode.env.remoteName;
if (isRemote) {
console.log(`Running in remote environment: ${isRemote}`);
}
// Handle remote file systems
vscode.workspace.findFiles('**/*.ts').then(files => {
files.forEach(file => {
console.log('Found file:', file.toString());
});
});Building VS Code extensions is a powerful way to customize your development environment. Start small, iterate quickly, and focus on solving real problems. The extensibility of VS Code makes it possible to create tools that significantly improve developer productivity and happiness!