Commands and menus
Registering and running commands, every place codelet draws a menu, and when clauses
A command is a named function. Registering one makes it callable by id — from a keybinding,
another extension, or a menu item your manifest declares. This page covers registering and
running commands, where codelet draws menus, and the when clauses that scope them.
#Registering and running
activate(context, vscode) {
context.subscriptions.push(
vscode.commands.registerCommand("todo.add", (title: string) => {
// …
}),
);
},vscode.commands.registerCommand(id, callback, thisArg?) returns a Disposable — push it onto
context.subscriptions so stopping the extension unregisters it. Run a command, yours or
another extension's, with vscode.commands.executeCommand(id, ...args):
await vscode.commands.executeCommand("todo.add", "Buy milk");vscode.commands.getCommands(filterInternal?) lists every registered id.
registerTextEditorCommand is the same registration for a command about the file showing: it
does nothing without an active editor, and its edits land through TextEditor.edit as one
transaction.
#What puts a command in the palette
Two things, together: a contributes.commands declaration, and a registered handler with the
same id.
manifest: {
contributes: {
commands: [{ command: "todo.add", title: "Add To Do", category: "To Do", icon: "check" }],
},
},
activate(context, vscode) {
context.subscriptions.push(vscode.commands.registerCommand("todo.add", () => { /* … */ }));
},category prefixes the title in the palette (To Do: Add To Do); icon is used wherever the
command is drawn as a button rather than a row. A command declared but never registered — or
registered but never declared — doesn't show up at all: the palette needs both halves.
A command that takes arguments has nothing to receive them from the palette, so it should stay
undeclared and be named with a leading _ — getCommands(true) filters those out, following
the same convention VS Code uses for its own internal commands:
vscode.commands.registerCommand("_todo.open", (id: string) => {
/* … */
});contributes.menus.commandPalette is the way to keep a declared command out of the palette —
for one that's only ever meant to run from a button, a key, or another extension:
menus: { commandPalette: [{ command: "todo.done", when: "false" }] },A command no item names there is offered, as in VS Code. A command that's named is offered
wherever any of its items' when holds — so two items are two ways of being reachable. Read when
the palette opens, against the same keys every other menu is.
#Menus
Beyond the palette, codelet draws a command in eleven places, keyed by contributes.menus. Each
runs a command with a different argument, because each is answering a different question about
where the reader clicked.
menus: {
"editor/title": [
{ command: "markdown.showPreview", when: "editorLangId == markdown", group: "navigation" },
],
"explorer/context": [
{ command: "markdown.showPreview", when: "resourceLangId == markdown" },
],
},Every contribution point
| Place | Draws | when keys | Command receives |
|---|---|---|---|
editor/title | Button(s) at the right of the tab bar, over the file showing | editorLangId, resourceLangId, resourceFilename, resourceExtname, resourceScheme, or activeWebviewPanelId for an extension's own tab | Nothing |
view/title | Button(s) over a contributed view's own rows | view | Nothing |
explorer/context | Row menu of the file tree, under Rename/Delete | resourceLangId, resourceFilename, resourceExtname, resourceScheme, explorerResourceIsFolder | The row's Uri (the root's, for a click below the last row) |
view/item/context | Row menu of a contributed tree | view, viewItem (the row's contextValue), plus the four resource* keys if the row set resourceUri | The element the TreeDataProvider returned |
editor/title/context | Tab's own context menu, under its three Closes | resourceLangId, resourceFilename, resourceExtname, resourceScheme | The tab's Uri |
editor/context | Right-click in the document itself | editorLangId, resourceLangId, resourceFilename, resourceExtname, resourceScheme | The showing file's Uri |
scm/title | Over a source control's own heading | scmProvider | The SourceControl |
scm/inputBox | Beside a source control's commit box | scmProvider | The SourceControl |
scm/resourceGroup/context | On a resource group's heading | scmProvider, scmResourceGroup, scmResourceGroupState | The SourceControlResourceGroup |
scm/resourceState/context | On a row in a resource group | scmProvider, scmResourceGroup, scmResourceGroupState, scmResourceState | The SourceControlResourceState |
globalActivity | Row of the gear at the foot of the activity bar | The window's own keys alone — no resource | Nothing |
commandPalette | Nothing — only says whether a command is offered there | Same as the window state at the moment the palette opens | — |
editorLangId is only set where the file in question is also the one showing — editor/title
and editor/context get it, the three context menus over a row or a tab that need not be showing
don't. The four scm/* places are drawn by an extension's own pane (codelet/extensions/scm)
rather than by the workbench's chrome directly; read codelet.scm.actions
for how a source control provider reaches them.
globalActivity is the odd one out: it is about the workbench rather than about anything in it,
so there is no resource to write a when against and nothing handed to the command. It is where
VSCode keeps Settings and where codelet's settings extension puts its own row, above the theme
control the workbench draws there itself — the place for a gesture that is the reader's rather
than the project's.
Write the handler to take the argument and fall back to the file showing, so one command answers a button, a row, and a tab that isn't even open:
vscode.commands.registerCommand("markdown.showPreview", async (resource?: Uri) => {
const document = resource
? await vscode.workspace.openTextDocument(resource)
: vscode.window.activeTextEditor?.document;
// …
});#group and order
In the two title bars — editor/title and view/title — "navigation" is the row of buttons
itself; any other group name is the … overflow menu at the end of them. A context menu has no
such split: every group renders, and the group name is only what sorts them.
group@order ("navigation@1") sorts within one group either way.
#Submenus
A menu item may name a group of items instead of a command — contributes.submenus declares the
group, and { submenu: id } puts it somewhere. In a context menu that's a row which opens rows
of its own; in a title bar it's a button that opens a menu, which is the only way to get one
out of a button you contributed.
contributes: {
submenus: [{ id: "todo.open", label: "New Todo", icon: "add" }],
menus: {
"view/title": [{ submenu: "todo.open", when: "view == files", group: "navigation" }],
// The rows, under the submenu's own id — it is a place you named for yourself.
"todo.open": [{ command: "todo.addHere" }, { command: "todo.addToList" }],
},
},The icon is what a title bar draws it as, the label being its tooltip there and its row
everywhere else. Inside a submenu, when and group@order mean exactly what they mean in any
other place, and the items are matched against the same keys the submenu's own placement was —
so a submenu in explorer/context can read resourceLangId in its rows.
Note
A submenu whose every item was filtered out by a when is left out entirely rather than drawn as
an empty menu. Submenus may hold submenus; one that ends up holding itself stops where it comes
round again. editor/tab takes no submenu — a tab is one row of text with nowhere for a menu to
open — and neither does commandPalette, which is a list of commands.
#Rows a manifest can't know
contributes.menus is read once, when your extension is installed — which is what lets the
server render your button before anything has activated. A list that changes while the page is
open can't live there. codelet.window.registerMenuItems is the other half: the workbench asks
you for the rest of a submenu's rows each time it's built, and appends them after the manifest's.
context.subscriptions.push(
codelet.window.registerMenuItems("todo.open", () =>
lists().map((list) => ({
label: `New Todo in ${list.name}`,
icon: "add",
// Read as a manifest's `group` is: the menu rules where it changes, so these sit under a
// line, below whatever the manifest already put there.
group: "lists",
run: () => addTo(list),
})),
),
);A row here carries run() rather than a command id — there's nothing to register a command for
when the row itself only exists at runtime. Return [] for "nothing to add", and keep the
callback cheap: it's asked during a render, not when the menu opens. Naming a submenu no manifest
declared registers nothing, and a second registration for one id replaces the first.
There's no VS Code equivalent, and there couldn't be: there the workbench holds the lists a menu like this is built from (its own terminal dropdown is chrome, not an extension's), where here you hold them.
#when clauses
An expression over context keys: a bare key, !key, key == "value", key != "value", joined
with && and ||. Anything the grammar doesn't reach — parentheses, in — reads as false, as
does a key that was never set.
Beyond the menu-specific keys in the table above, two describe the window itself:
| Key | Value |
|---|---|
workbenchState | "empty" until the tree showing holds a file anywhere, "folder" once it does |
workspaceFolderCount | "0" or "1", matching workbenchState |
Everything else comes from setContext, which any extension can call to name a key of its own
and drive its own views or menu items with it:
await vscode.commands.executeCommand("setContext", "todo.hasItems", items.length > 0);views: { todo: [{ id: "todo.list", name: "To Do", when: "todo.hasItems" }] },A container is its views: every view whose when fails takes its icon or tab strip entry with
it, so setContext is also how a whole activity bar icon appears and disappears.
#Worked example
A button on the tab bar, scoped to one language, that opens a panel — and a button back on that
panel's own tab bar to return to the file. This is exactly what codelet/extensions/markdown
does; see Markdown for the shipped version.
import { defineExtension } from "codelet/extensions";
import type { Uri } from "codelet/extensions";
const COMMAND = "notes.preview";
const BACK = "notes.showSource";
const VIEW_TYPE = "notes.preview";
export const notesPreview = defineExtension({
manifest: {
name: "notes-preview",
contributes: {
commands: [
{ command: COMMAND, title: "Preview Notes", icon: "preview" },
{ command: BACK, title: "Show Source", icon: "file" },
],
menus: {
"editor/title": [
{ command: COMMAND, when: "editorLangId == notes", group: "navigation" },
{ command: BACK, when: `activeWebviewPanelId == ${VIEW_TYPE}`, group: "navigation" },
],
},
},
},
activate(context, vscode) {
let panel: ReturnType<typeof vscode.window.createWebviewPanel> | undefined;
let shownUri: Uri | undefined;
context.subscriptions.push(
vscode.commands.registerCommand(COMMAND, async () => {
const document = vscode.window.activeTextEditor?.document;
if (!document) return;
shownUri = document.uri;
panel ??= vscode.window.createWebviewPanel(
VIEW_TYPE,
"Notes Preview",
vscode.ViewColumn.Beside,
);
panel.webview.html = `<pre>${document.getText()}</pre>`;
panel.reveal();
}),
vscode.commands.registerCommand(BACK, async () => {
if (shownUri) await vscode.window.showTextDocument(shownUri);
}),
);
},
});Two menu items, one command each, and no shared state beyond which file was last previewed —
activeWebviewPanelId is what lets the second button find its way home without either command
being handed an argument.