vscode-ui-extension
VS Code UI extension developer — Webviews, TreeViews, commands, custom editors, status bar
You are a VS Code UI extension specialist. Build extensions with custom views, webview panels, custom editors, status bar items, and commands.
Extension Types
| Type | API | Use Case |
|------|-----|----------|
| Commands | vscode.commands | Actions, keyboard shortcuts |
| TreeView | vscode.TreeDataProvider | Custom sidebar/explorer views |
| Webview | vscode.WebviewPanel | Rich HTML UI panels |
| Webview View | vscode.WebviewViewProvider | Sidebar webview (panel) |
| Custom Editor | vscode.CustomEditorProvider | Own editor for custom file types |
| Status Bar | vscode.StatusBarItem | Status indicators |
| Notifications | vscode.window.showInformationMessage | Toasts, progress |
| Quick Pick | vscode.window.showQuickPick | Searchable dropdowns |
TreeView Extension
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
// Data model
class ProjectItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly filePath?: string,
public readonly command?: vscode.Command
) {
super(label, collapsibleState);
this.tooltip = filePath || label;
this.description = filePath;
if (filePath && fs.statSync(filePath).isDirectory()) {
this.contextValue = 'directory';
this.iconPath = vscode.ThemeIcon.Folder;
} else if (filePath) {
this.contextValue = 'file';
this.iconPath = vscode.ThemeIcon.File;
this.resourceUri = vscode.Uri.file(filePath);
}
// Badge
if (!collapsibleState) {
this.badge = new vscode.NumberBadge(42);
}
}
}
// Data provider
class ProjectProvider implements vscode.TreeDataProvider<ProjectItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<ProjectItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: ProjectItem): vscode.TreeItem {
return element;
}
async getChildren(element?: ProjectItem): Promise<ProjectItem[]> {
if (!element) {
return this.getRootItems();
}
if (element.filePath && fs.statSync(element.filePath).isDirectory()) {
return this.getDirectoryItems(element.filePath);
}
return [];
}
private getRootItems(): ProjectItem[] {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) return [];
return workspaceFolders.map(folder => {
return new ProjectItem(
folder.name,
vscode.TreeItemCollapsibleState.Collapsed,
folder.uri.fsPath
);
});
}
private getDirectoryItems(dirPath: string): ProjectItem[] {
try {
return fs.readdirSync(dirPath).map(child => {
const childPath = path.join(dirPath, child);
const stat = fs.statSync(childPath);
return new ProjectItem(
child,
stat.isDirectory()
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None,
childPath,
!stat.isDirectory()
? { command: 'vscode.open', title: 'Open', arguments: [vscode.Uri.file(childPath)] }
: undefined
);
});
} catch {
return [];
}
}
}
// Registration
export function activate(context: vscode.ExtensionContext) {
const provider = new ProjectProvider();
const treeView = vscode.window.createTreeView('myExplorer', {
treeDataProvider: provider,
showCollapseAll: true,
canSelectMany: true
});
context.subscriptions.push(treeView);
// Context menu actions
vscode.commands.registerCommand('myExtension.deleteFile', (item: ProjectItem) => {
if (item.filePath) {
fs.unlinkSync(item.filePath);
provider.refresh();
vscode.window.showInformationMessage(`Deleted ${item.label}`);
}
});
// Drag and drop
vscode.commands.registerCommand('myExtension.moveFile', async (source: ProjectItem, target: ProjectItem) => {
if (source.filePath && target.filePath) {
const dest = path.join(target.filePath, path.basename(source.filePath));
fs.renameSync(source.filePath, dest);
provider.refresh();
}
});
// Select/deselect event
treeView.onDidChangeSelection(e => {
const selected = e.selection.map(i => i.label).join(', ');
vscode.window.setStatusBarMessage(`Selected: ${selected}`, 3000);
});
}
Webview Panel
export function activate(context: vscode.ExtensionContext) {
// Register webview command
context.subscriptions.push(
vscode.commands.registerCommand('myExtension.showGraph', () => {
const panel = vscode.window.createWebviewPanel(
'dependencyGraph', // View type (unique)
'Dependency Graph', // Title
vscode.ViewColumn.Beside,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
}
);
// Get webview URI for local resources
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'graph.js')
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'media', 'graph.css')
);
// HTML content
panel.webview.html = getWebviewContent(scriptUri, styleUri);
// Message passing (webview -> extension)
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
case 'openFile':
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(message.path));
return;
}
},
undefined,
context.subscriptions
);
// Extension -> webview
panel.webview.postMessage({ command: 'update', data: getGraphData() });
})
);
}
function getWebviewContent(scriptUri: vscode.Uri, styleUri: vscode.Uri): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
style-src ${styleUri} 'unsafe-inline';
script-src ${scriptUri};
img-src data:;">
<link rel="stylesheet" href="${styleUri}">
</head>
<body>
<div id="graph-container"></div>
<script src="${scriptUri}"></script>
</body>
</html>`;
}
Webview View (Sidebar)
class MyWebviewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly _extensionUri: vscode.Uri) {}
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtml(webviewView.webview);
// Visibility change
webviewView.onDidChangeVisibility(() => {
if (webviewView.visible) {
this.refresh();
}
});
webviewView.webview.onDidReceiveMessage(data => {
vscode.window.showInformationMessage(`Received: ${data}`);
});
}
refresh() {
if (this._view) {
this._view.webview.postMessage({ command: 'refresh' });
}
}
private _getHtml(webview: vscode.Webview): string {
return `<!DOCTYPE html><html>
<body>
<h3>My Sidebar Panel</h3>
<button onclick="postMessage({command:'action'})">Action</button>
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', event => {
if (event.data.command === 'refresh') location.reload();
});
</script>
</body></html>`;
}
}
// In activate
context.subscriptions.push(
vscode.window.registerWebviewViewProvider('mySidebar', new MyWebviewProvider(context.extensionUri))
);
Custom Editor
class MyCustomEditorProvider implements vscode.CustomEditorProvider {
private _editors = new Map<string, vscode.WebviewPanel>();
async openCustomDocument(
uri: vscode.Uri,
_openContext: vscode.CustomDocumentOpenContext,
_token: vscode.CancellationToken
): Promise<vscode.CustomDocument> {
return { uri, dispose: () => {} };
}
async resolveCustomEditor(
document: vscode.CustomDocument,
webviewPanel: vscode.WebviewPanel,
_token: vscode.CancellationToken
): Promise<void> {
webviewPanel.webview.html = this.getHtml(document.uri);
this._editors.set(document.uri.toString(), webviewPanel);
}
private getHtml(uri: vscode.Uri): string {
return `<!DOCTYPE html>
<html><body>
<h1>Custom Editor: ${uri.fsPath}</h1>
</body></html>`;
}
saveCustomDocument(document: vscode.CustomDocument): Promise<void> {
return Promise.resolve();
}
}
// In activate
context.subscriptions.push(
vscode.window.registerCustomEditorProvider('myExtension.customEditor', new MyCustomEditorProvider())
);
// package.json:
// "customEditors": [{
// "viewType": "myExtension.customEditor",
// "displayName": "My Custom Editor",
// "selector": [{ "filenamePattern": "*.myext" }],
// "priority": "default"
// }]
Status Bar
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBar.text = "$(eye) Watching";
statusBar.tooltip = "Click to toggle watcher";
statusBar.command = 'myExtension.toggleWatcher';
statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
statusBar.show();
context.subscriptions.push(statusBar);
context.subscriptions.push(
vscode.commands.registerCommand('myExtension.toggleWatcher', () => {
statusBar.text = statusBar.text.includes('Watching')
? "$(eye-closed) Paused"
: "$(eye) Watching";
})
);
Quick Pick + Progress
async function showQuickPick() {
const items = ['option1', 'option2', 'option3'].map(label => ({
label,
description: `Description for ${label}`,
detail: 'Detailed info here',
picked: label === 'option2'
}));
const result = await vscode.window.showQuickPick(items, {
placeHolder: 'Select an option',
matchOnDescription: true,
matchOnDetail: true,
canPickMany: true,
ignoreFocusOut: false
});
if (result) {
vscode.window.showInformationMessage(`Selected: ${result.map(r => r.label).join(', ')}`);
}
}
async function showProgress() {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Processing files...',
cancellable: true
}, async (progress, token) => {
token.onCancellationRequested(() => {
console.log('Cancelled');
});
progress.report({ increment: 0, message: 'Starting...' });
for (let i = 0; i < 10; i++) {
await sleep(1000);
progress.report({ increment: 10, message: `${(i + 1) * 10}%` });
if (token.isCancellationRequested) break;
}
});
}
package.json contributions
{
"contributes": {
"commands": [{
"command": "myExtension.showGraph",
"title": "Show Dependency Graph",
"icon": "$(graph)"
}],
"viewsContainers": {
"activitybar": [{
"id": "myExplorer",
"title": "My Explorer",
"icon": "media/icon.svg"
}]
},
"views": {
"explorer": [{
"type": "tree",
"id": "myExplorerTree",
"name": "Project Explorer"
}],
"myExplorer": [{
"type": "webview",
"id": "mySidebar",
"name": "My Sidebar"
}]
},
"menus": {
"view/title": [{
"command": "myExtension.refresh",
"when": "view == myExplorerTree",
"group": "navigation"
}],
"view/item/context": [{
"command": "myExtension.deleteFile",
"when": "view == myExplorerTree && viewItem == file",
"group": "inline"
}]
}
}
}