Skip to content
All essays
CraftMarch 20, 202518 min

VS Code Extensions Development: Building Custom Tools

Learn to build powerful VS Code extensions from scratch. Create custom tools that enhance your development workflow.

Ü
Ümit Uz
Mobile & Full Stack Developer

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 config

Understanding 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 major

package.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!

Enjoyed this essay? Share it.

Related essays

Next essay
Edge Computing with Cloudflare Workers