How extensions surface functionality in the editor — declaring contribution points in the manifest and wiring them to runtime code
commands contributionwhen clause contextssetContextconfiguration contributioncontributes MapAlmost everything an extension adds to VS Code is declared statically in the contributes object of package.json — then backed at runtime by code in extension.ts.
Static JSON in contributes. VS Code reads it without running your code, so palette entries, menus and settings appear immediately on install.
Runtime code in activate() supplies the behaviour — e.g. commands.registerCommand binds a declared command ID to a handler.
A command declared but not registered shows in the palette but errors on invoke. A command registered but not declared works in code but is invisible in the UI.
commands Contribution{
"contributes": {
"commands": [
{
"command": "myExt.helloWorld",
"title": "Hello World",
"category": "My Extension",
"icon": "$(rocket)",
"enablement": "editorHasSelection"
}
]
}
}
import * as vscode from 'vscode';
export function activate(
context: vscode.ExtensionContext
) {
const disposable =
vscode.commands.registerCommand(
'myExt.helloWorld',
() => {
vscode.window.showInformationMessage(
'Hello World from My Extension!'
);
}
);
context.subscriptions.push(disposable);
}
In the palette the entry shows as My Extension: Hello World — category is the prefix, title the label. The ID itself is never shown to users.
icon uses a $(codicon) reference (or light/dark paths) for toolbar buttons. enablement greys the command out when its when-style clause is false.
Extensions are lazily loaded. activationEvents in package.json tells VS Code when to run your activate() — keeping startup fast.
| Event | Fires when… | Use for |
|---|---|---|
onCommand:myExt.foo | The command is invoked | Most commands |
onLanguage:python | A file of that language opens | Language tooling |
onStartupFinished | Shortly after the window loads | Background setup |
workspaceContains:**/*.foo | A matching file exists in the folder | Project-specific tools |
onView:myView | A contributed view becomes visible | Tree / webview views |
onUri | A vscode:// URI targets the extension | Deep links / auth callbacks |
onDebug | A debug session starts | Debug adapters |
* | On startup, always | Avoid — slows the editor |
Since VS Code 1.74, an onCommand event is generated automatically for every command in contributes.commands. Modern generators (yo code) ship an empty activationEvents array.
onLanguage from contributed languagesonView / onWebviewPanel from contributed viewsActivate as late as possible. Never use * unless you genuinely must run on every window.
The palette (Ctrl/Cmd+Shift+P) is the primary surface for commands. Typing > switches it into command mode (the default when opened with the shortcut).
contributes.commands appears by default> prefixThe Quick Open widget is shared: no prefix = files, > = commands, @ = symbols, : = go to line. Your commands live behind >.
To hide a command from the palette unless a condition holds, add a commandPalette entry under menus with a when clause:
"menus": {
"commandPalette": [
{
"command": "myExt.formatPython",
"when": "editorLangId == python"
}
]
}
A commandPalette when only controls visibility; use enablement on the command to grey it out instead of hiding it.
{
"contributes": {
"keybindings": [
{
"command": "myExt.helloWorld",
"key": "ctrl+alt+h",
"mac": "cmd+alt+h",
"when": "editorTextFocus"
},
{
"command": "myExt.formatPython",
"key": "ctrl+k ctrl+f",
"when": "editorLangId == python"
}
]
}
}
command — the ID to runkey — Windows/Linux bindingmac / linux / win — platform overrideswhen — context gate for the bindingargs — optional argument passed to the commandTwo-stroke bindings use a space: ctrl+k ctrl+c. The first chord shows a pending indicator in the status bar.
Avoid hijacking built-in keys. Prefer the ctrl+k prefix space, always set a when clause, and let users rebind freely.
"menus": {
"editor/context": [
{
"command": "myExt.formatPython",
"when": "editorLangId == python",
"group": "1_modification@1"
}
],
"editor/title": [
{
"command": "myExt.preview",
"when": "resourceExtname == .md",
"group": "navigation@1"
}
],
"explorer/context": [
{ "command": "myExt.analyse", "group": "2_workspace" }
],
"view/title": [
{ "command": "myExt.refresh", "when": "view == myView",
"group": "navigation" }
]
}
commandPalette — palette visibilityeditor/context — right-click in editoreditor/title — editor tab toolbarexplorer/context — file explorerview/title — a view's headerview/item/context — tree item right-clicknavigation first, then 1_modification, 2_workspace…@1, @2 set order within a groupnavigation renders as inline icons in title barswhen Clause ContextsThe when clause is a small boolean expression language evaluated against the editor's current state. It gates keybindings, menus and view visibility.
| Context key | Meaning |
|---|---|
editorLangId == 'python' | Active editor's language |
editorHasSelection | Text is selected |
editorTextFocus | Focus is in a text editor |
resourceExtname == .md | File extension of the resource |
resourceScheme == file | URI scheme of the resource |
view == myView | The focused view's ID |
viewItem == folder | A tree item's contextValue |
isLinux / isMac / isWindows | Host platform |
&& and, || or, ! not, == / !=, =~ regex, in. Example: editorLangId == python && editorHasSelection.
Set your own boolean context from code, then reference it in any when clause:
// In extension.ts
vscode.commands.executeCommand(
'setContext',
'myExt.isAuthed',
true
);
// In package.json
{
"command": "myExt.sync",
"when": "myExt.isAuthed"
}
Keep custom keys namespaced (myExt.*) and update them whenever the underlying state changes.
"configuration": {
"title": "My Extension",
"properties": {
"myExt.maxItems": {
"type": "number",
"default": 20,
"minimum": 1,
"description": "Maximum items to show.",
"scope": "resource"
},
"myExt.logLevel": {
"type": "string",
"default": "info",
"enum": ["off", "info", "debug"],
"enumDescriptions": [
"No logging",
"Standard logging",
"Verbose logging"
],
"description": "Logging verbosity.",
"scope": "window"
},
"myExt.enableTelemetry": {
"type": "boolean",
"default": true,
"description": "Send anonymous usage data.",
"scope": "application"
}
}
}
type — string, number, boolean, array, objectdefault — value when unsetdescription / markdownDescriptionenum + enumDescriptions — fixed choicesapplication — user-global onlywindow — user or workspaceresource — per folder / file (also language-overridable)Always prefix with your extension name (myExt.*) so settings group neatly in the Settings UI.
import * as vscode from 'vscode';
// Read a value (typed, with fallback)
const config =
vscode.workspace.getConfiguration('myExt');
const max = config.get<number>('maxItems', 20);
const level =
config.get<string>('logLevel', 'info');
// Update a value
await config.update(
'logLevel',
'debug',
vscode.ConfigurationTarget.Workspace
);
// React to changes
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExt.logLevel')) {
reconfigureLogger();
}
});
Pass the section prefix to scope reads. .get<T>(key, default) returns the effective value after merging user, workspace and folder settings.
ConfigurationTarget.Global — user settings.Workspace — .vscode/settings.json.WorkspaceFolder — specific folderFires on any settings change. Guard with e.affectsConfiguration('myExt.x') so you only react to your keys.
For resource-scoped settings, pass a URI: getConfiguration('myExt', uri) to read the folder-specific value.
Add a custom panel to the Activity Bar by contributing a view container plus the views inside it. A teaser here — tree data providers come in Part 4.
"viewsContainers": {
"activitybar": [
{
"id": "myExt-explorer",
"title": "My Extension",
"icon": "resources/icon.svg"
}
]
},
"views": {
"myExt-explorer": [
{
"id": "myExt.itemsView",
"name": "Items",
"icon": "$(list-tree)",
"contextualTitle": "My Extension Items"
}
]
}
viewsContainers creates the Activity Bar entry; views populates it. The container id is the key that links them.
The contributes map has many more keys. A quick tour of the most common — each is purely declarative JSON.
| Contribution point | What it adds |
|---|---|
languages | Registers a language ID, file extensions, and a configuration file (brackets, comments) |
grammars | TextMate grammar for syntax highlighting, mapped to a language scope |
snippets | Code snippets for a language, loaded from a JSON snippets file |
themes | Colour themes — a label, uiTheme base, and a theme JSON path |
iconThemes | File icon themes that style the explorer and tabs |
jsonValidation | Associates a JSON schema with a file pattern for validation & IntelliSense |
customEditors | Webview-based editors for custom file types (e.g. an image or CAD viewer) |
walkthroughs | Step-by-step Getting Started pages shown on the Welcome screen |
The full, authoritative list lives in the Contribution Points reference. Many of these (customEditors, walkthroughs, views) also need runtime registration — covered in later parts.
contributes declares UI; activate() registers behaviourregisterCommandonCommand is auto-derivedwhen clausesgroup + @n control placement & orderconfiguration.properties, read via getConfigurationkeybindings entry (try a ctrl+k chord)editor/context menu entry gated by a when clause (e.g. editorLangId == markdown)yo code (scaffold) · vsce (package & publish) · the Extension Development Host (F5) · the built-in Developer: Inspect Context Keys command for debugging when clauses.