Why Build a VS Code Extension? 🌟
Visual Studio Code is more than an editor—it's a playground for developers. With over 50,000 extensions on the Marketplace, the community loves customizing their experience. But have you ever thought, Hey, I could add that feature myself!
? Today, we'll turn you from a consumer into a creator, showing you how to craft your own VS Code extension from scratch—and dive into advanced features like TreeViews, Webviews, testing, CI, and more.
By the end of this guide, you’ll have:
- 🎉 A fully functional "Hello World" extension with actionable commands
- 🌲 A custom Tree View sidebar
- 🌐 A Webview panel for rich UIs
- 🧪 Automated unit & integration tests using Mocha
- 🤖 CI setup in GitHub Actions
- 📦 Knowledge of packaging, publishing, and maintaining your extension
Ready to level up your dev game? Let’s dive in.
Prerequisites: Your Toolkit 🧰
Make sure you have:
-
Node.js (v14+) – Your JavaScript runtime engine. Check with
node -v
. -
npm or Yarn – Package manager.
npm -v
oryarn -v
. - Visual Studio Code – Of course!
- Yeoman & VS Code Extension Generator – For scaffolding.
- vsce – VS Code Extension CLI for packaging and publishing.
# Install global dependencies
npm install -g yo generator-code vsce
# OR with yarn
yarn global add yo generator-code vsce
Pro Tip: Use
nvm
to manage Node versions per project.
Step 1: Scaffold the Extension 🔧
Yeoman scaffolds boilerplate code:
yo code
Prompts you'll see:
-
Type of extension: ✏️
TypeScript
(for safety and intellisense) -
Extension name:
my-first-extension
-
Identifier:
myFirstExtension
- Description: A brief summary
- Initialize git?: Up to you!
Generated structure:
my-first-extension/
├── .vscode/ # debug configurations
├── src/ # TypeScript source
│ └── extension.ts # activation & commands
├── package.json # manifest & contributions
├── tsconfig.json # compile options
├── README.md # your docs
├── .github/ # (if you choose CI)
└── test/ # Mocha tests
Step 2: Deep Dive into package.json
📦
Your extension’s manifest controls activation, commands, keybindings, views, configuration, and more.
{
"name": "my-first-extension",
"displayName": "My First Extension",
"description": "Says hello, shows TreeView, and Webview!",
"version": "0.0.1",
"engines": { "vscode": "^1.60.0" },
"activationEvents": [
"onCommand:extension.sayHello",
"onView:myTreeView"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{ "command": "extension.sayHello", "title": "Hello World" }
],
"keybindings": [
{
"command": "extension.sayHello",
"key": "ctrl+alt+h",
"when": "editorTextFocus"
}
],
"views": {
"explorer": [
{ "id": "myTreeView", "name": "My Custom View" }
]
},
"configuration": {
"type": "object",
"properties": {
"myExtension.showWelcome": {
"type": "boolean",
"default": true,
"description": "Show welcome message on activation"
}
}
}
}
}
-
activationEvents
: Lazy-load your extension only when needed (commands, views, languages). -
contributes.views
: Register a TreeView in the Explorer panel. -
keybindings
: Let power users invoke commands with shortcuts. -
configuration
: Expose settings under VS Code’s Settings UI.
Step 3: Implement Core Features 🖋️
3.1 Say Hello Command
In src/extension.ts
:
import * as vscode from 'vscode';
import { MyTreeDataProvider } from './treeProvider';
import { createWebviewPanel } from './webview';
export function activate(context: vscode.ExtensionContext) {
const config = vscode.workspace.getConfiguration('myExtension');
if (config.get('showWelcome')) {
vscode.window.showInformationMessage('Welcome to My First Extension! 🌟');
}
// Hello Command
const hello = vscode.commands.registerCommand('extension.sayHello', () => {
vscode.window.showInformationMessage('Hello, VS Code Extension!', '🎉 Celebrate', '📖 Docs')
.then(selection => {
if (selection === '📖 Docs') {
vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/api'));
}
});
});
// TreeView
const treeDataProvider = new MyTreeDataProvider();
vscode.window.createTreeView('myTreeView', { treeDataProvider });
// Webview
const webviewCmd = vscode.commands.registerCommand('extension.showWebview', () => {
createWebviewPanel(context);
});
context.subscriptions.push(hello, webviewCmd);
}
export function deactivate() {}
3.2 Custom TreeView
Create src/treeProvider.ts
:
import * as vscode from 'vscode';
export class MyTreeDataProvider implements vscode.TreeDataProvider<MyTreeItem> {
private _onDidChange = new vscode.EventEmitter<MyTreeItem | void>();
readonly onDidChangeTreeData = this._onDidChange.event;
getChildren(): MyTreeItem[] {
return [
new MyTreeItem('Item One'),
new MyTreeItem('Item Two')
];
}
getTreeItem(element: MyTreeItem): vscode.TreeItem {
return element;
}
}
class MyTreeItem extends vscode.TreeItem {
constructor(label: string) {
super(label);
this.tooltip = `Tooltip for ${label}`;
this.command = {
command: 'extension.sayHello',
title: 'Say Hello',
arguments: []
};
}
}
3.3 Rich Webview Panel
Create src/webview.ts
:
import * as vscode from 'vscode';
export function createWebviewPanel(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'myWebview', 'My Webview', vscode.ViewColumn.One, { enableScripts: true }
);
panel.webview.html = getWebviewContent();
}
function getWebviewContent(): string {
return `
Webview
Hello from Webview!
Click me
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({ command: 'alert', text: 'Button clicked!' });
}
`;
}
Enter fullscreen mode
Exit fullscreen mode
Add message listener in activate if needed.
Step 4: Compile, Debug & Lint 🚦
Compile: npm run compile
Lint: Add ESLint (npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin)
Debug: Press F5. In the Extension Development Host:
Command Palette: Ctrl+Shift+P → Hello World
Explorer: Find My Custom View
Command Palette: Show Webview
Step 5: Testing 🧪
Yeoman scaffold includes Mocha. In test\extension.test.ts:
import * as assert from 'assert';
import * as vscode from 'vscode';
describe('Extension Tests', () => {
it('should activate extension', async () => {
const ext = vscode.extensions.getExtension('your-publisher.my-first-extension');
await ext?.activate();
assert.ok(ext?.isActive);
});
});
Enter fullscreen mode
Exit fullscreen mode
Run tests:
npm test
Enter fullscreen mode
Exit fullscreen mode
Consider adding integration tests with @vscode/test-electron for UI flows.
Step 6: Continuous Integration 🤖
Use GitHub Actions. Example .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with: { 'node-version': '14' }
- run: npm install
- run: npm run compile
- run: npm test
- run: npm run lint
Enter fullscreen mode
Exit fullscreen mode
This ensures every PR builds, compiles, and tests cleanly.
Step 7: Package & Publish 📦
Package: vsce package → produces my-first-extension-0.0.1.vsix
Publish:
vsce login
vsce publish
Enter fullscreen mode
Exit fullscreen mode
Versioning: Follow Semantic Versioning to keep users happy.
Pro Tips & Best Practices 🧙♂️
Lazy Activation: Only load heavy modules on demand.
Telemetry: Use vscode-extension-telemetry; always ask permission and respect GDPR.
Localization: Support multiple languages with package.nls.json.
Performance: Avoid blocking the main thread. Use background tasks if needed.
Documentation: Include a clear README, CHANGELOG, and demo GIFs.
Community: Respond to issues, tag PRs, and keep dependencies updated.
Wrapping Up 🎁
You’ve built commands, views, rich web UIs, added testing, CI, and deployed to the Marketplace. But this is just the beginning:
Explore Debug Adapters and Language Servers
Create Custom Themes and Syntax Grammars
Integrate AI/ML for smarter coding assistance
Drop your extension links in the comments, share your learnings, and let’s push the boundaries of what VS Code can do—together. Happy coding! 💪Enjoyed this deep dive? Follow me for more tutorials and code adventures!