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.
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:
npm install -g yo generator-code
yo codeThe 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:
{
"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 Palettekeybindings— keyboard shortcutsconfiguration— settings the user can configurethemes— color themeslanguages— language supportproblemMatchers— output parsers for Tasksmenus— 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.
{
"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.
// 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.
// 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
);
}// 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.
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:
{
"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
npm install -g @vscode/vsce
vsce package # creates a .vsix file
vsce publish # publishes to the VS Code MarketplaceYou 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.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.