Building VS Code Extensions

VS Code extensions range from a JSON color theme to a full embedded app. Learn how to scaffold one, build a code decorator, and add a webview UI — and understand how the pieces connect.

March 30, 20268 min read

VS Code extensions span an enormous range: a color theme is an extension. A problem matcher regex is an extension. The AI Toolkit — a full application embedded in your editor — is an extension. The thing that turns your To-Do comments a different color is an extension.

That range is the point. You don't need to build the AI Toolkit. You can build the thing that makes your specific workflow faster, and it ships as the same format.

Scaffolding an Extension

Two tools:

Bash
npm install -g yo generator-code yo code

The generator walks you through:

  • New Extension (TypeScript or JavaScript)
  • New Color Theme
  • New Language Support
  • New Snippets
  • New Keymap
  • Web Extension (runs in browser VS Code)

Pick "New Extension" for most things. It generates the boilerplate and you're writing real code immediately.

Alternatively, clone the boilerplate from the VS Code extension samples repo and start from there.

The package.json is the Manifest

The most important thing in any VS Code extension is package.json. It declares everything your extension contributes to VS Code:

JSON
{ "name": "my-extension", "displayName": "My Extension", "publisher": "your-publisher-name", "activationEvents": ["onLanguage:javascript", "onLanguage:typescript"], "contributes": { "commands": [ { "command": "myExtension.doSomething", "title": "My Extension: Do Something" } ], "configuration": { "properties": { "myExtension.enabled": { "type": "boolean", "default": true } } } } }

activationEvents: when does your extension load? Lazy loading matters — every extension has a startup cost. Common values:

  • "onCommand:myExtension.doSomething" — only when that command runs (cheapest)
  • "onLanguage:typescript" — when a TypeScript file opens
  • "onStartupFinished" — after VS Code finishes starting
  • "*" — always (avoid this)

contributes: what does your extension add to VS Code?

  • commands — entries in the Command Palette
  • keybindings — keyboard shortcuts
  • configuration — settings the user can configure
  • themes — color themes
  • languages — language support
  • problemMatchers — output parsers for Tasks
  • menus — entries in right-click menus, title bar, etc.

publisher: your publisher ID from the VS Code Marketplace. Required for publishing.

Example 1: A Color Theme

The simplest extension. Generate one with yo code → "New Color Theme."

The output: a package.json and a themes/your-theme-color-theme.json.

JSON
{ "name": "Very Cool Colors", "type": "dark", "colors": { "editor.background": "#1a1a2e", "editor.foreground": "#e0e0e0", "activityBar.background": "#16213e", "sideBar.background": "#0f3460" }, "tokenColors": [ { "scope": "comment", "settings": { "foreground": "#6a9955", "fontStyle": "italic" } }, { "scope": "string", "settings": { "foreground": "#ce9178" } }, { "scope": "variable", "settings": { "foreground": "#9cdcfe" } } ] }

colors: the VS Code UI (panels, activity bar, status bar). tokenColors: the syntax highlighting. Each scope maps to a language token type — comments, strings, keywords, variables, etc.

Testing it: hit F5. A new VS Code window launches with your extension loaded. Use Command Palette → "Color Theme" to apply it. When you change the JSON, reload the extension window with Cmd+R.

Example 2: Code Decorations (Comment Highlighter)

This is where things get interesting. An extension that analyzes your code and adds visual decoration — like highlighting every FIXME comment with a red background.

JavaScript
// extension.js const vscode = require('vscode'); // Create the decoration type once — it's reused const fixmeDecoration = vscode.window.createTextEditorDecorationType({ backgroundColor: 'rgba(255, 0, 0, 0.2)', border: '1px solid rgba(255, 0, 0, 0.5)', }); function decorateWords(editor) { if (!editor) return; const text = editor.document.getText(); const ranges = []; const pattern = /\bFIXME\b/g; let match; while ((match = pattern.exec(text)) !== null) { const startPos = editor.document.positionAt(match.index); const endPos = editor.document.positionAt(match.index + match[0].length); ranges.push(new vscode.Range(startPos, endPos)); } editor.setDecorations(fixmeDecoration, ranges); } function activate(context) { // Run on the current file immediately decorateWords(vscode.window.activeTextEditor); // Re-run when the file changes context.subscriptions.push( vscode.workspace.onDidChangeTextDocument((e) => { const editor = vscode.window.activeTextEditor; if (editor && e.document === editor.document) { decorateWords(editor); } }) ); // Re-run when switching files context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(decorateWords) ); // Register a command to run on demand context.subscriptions.push( vscode.commands.registerCommand('myExtension.decorateWords', () => { decorateWords(vscode.window.activeTextEditor); vscode.window.showInformationMessage('Decorations applied!'); }) ); } function deactivate() {} module.exports = { activate, deactivate };

The two required exports: every VS Code extension must export activate (runs when the extension starts) and deactivate (cleanup when it stops). That's the minimum contract.

context.subscriptions: add disposables here. When the extension deactivates, VS Code automatically disposes everything in the subscriptions array. This prevents memory leaks.

Scoping the decoration to active files only is important. Don't scan files the user isn't looking at — that's unnecessary CPU usage, and users will notice.

Example 3: Webview UI

Extensions can render arbitrary HTML in a panel. This is how the AI Toolkit works — and how you'd build a regex playground, a database viewer, a dashboard, or any other tool with a real UI.

The key constraint: the HTML must be self-contained (no external scripts or stylesheets). Your build tool (Vite, esbuild) needs to inline everything. The end result is a single HTML file with embedded CSS and JavaScript.

The communication model: your extension's main process (Node.js) and the webview (a browser-like environment) communicate via message passing — like a server and a web page, but with postMessage instead of HTTP.

JavaScript
// extension.js function createWebviewPanel(context) { const panel = vscode.window.createWebviewPanel( 'regexPlayground', // internal ID 'Regex Playground', // tab title vscode.ViewColumn.One, // where to open { enableScripts: true, // allow JS in the webview localResourceRoots: [ // which local files the webview can load vscode.Uri.joinPath(context.extensionUri, 'media') ] } ); // Load the HTML panel.webview.html = getWebviewContent(context, panel.webview); // Receive messages from the webview panel.webview.onDidReceiveMessage( (message) => { switch (message.command) { case 'evaluateRegex': const results = evaluateRegex(message.pattern, message.flags, message.text); // Send results back to the webview panel.webview.postMessage({ command: 'results', data: results }); break; } }, undefined, context.subscriptions ); }
JavaScript
// media/webview.js (runs in the webview) const vscode = acquireVsCodeApi(); // the VS Code webview API document.getElementById('runBtn').addEventListener('click', () => { // Send a message to the extension's main process vscode.postMessage({ command: 'evaluateRegex', pattern: document.getElementById('pattern').value, flags: document.getElementById('flags').value, text: document.getElementById('input').value, }); }); // Receive results from the main process window.addEventListener('message', (event) => { const message = event.data; if (message.command === 'results') { renderResults(message.data); } });

VS Code CSS variables: VS Code automatically sets CSS variables based on the current theme — --vscode-editor-background, --vscode-editor-foreground, --vscode-button-background, etc. Use these in your webview CSS and your UI will adapt to whatever theme the user has.

CSS
body { background-color: var(--vscode-editor-background); color: var(--vscode-editor-foreground); } button { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); }

Debugging Your Extension

The generated launch.json includes an extensionHost configuration that launches a second VS Code window with your extension loaded:

JSON
{ "type": "extensionHost", "request": "launch", "name": "Extension Development Host", "args": ["--extensionDevelopmentPath=${workspaceFolder}"] }

Hit F5 → a new VS Code window opens with your extension active. Make changes, then Cmd+R in the extension host window to reload.

Set breakpoints in your extension code in the main window — they'll pause when that code runs in the extension host.

Publishing

Bash
npm install -g @vscode/vsce vsce package # creates a .vsix file vsce publish # publishes to the VS Code Marketplace

You need a publisher account at the VS Code Marketplace and a Personal Access Token from Azure DevOps. The process is similar to publishing on npm.

The .vsix file can also be installed directly without publishing: Extensions panel → "..." → "Install from VSIX."

What's Actually Possible

The VS Code API surface is enormous:

  • Read and write files
  • Run shell commands
  • Access the current selection, cursor position, and document
  • Create custom tree views in the sidebar
  • Add entries to any menu (right-click, title bar, status bar)
  • Hook into Git operations
  • Extend Language Servers (autocomplete, hover info, diagnostics)
  • Create custom editors for binary files
  • Embed full web applications as panels

The result: anything that would be a standalone Electron app can instead be a VS Code extension. You get the distribution mechanism (Marketplace), the shell (the editor users are already in), and all the editor APIs for free. You just build the functionality.

Someone put Doom in a VS Code extension. Your productivity tool is probably easier to build than that.

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.