Codelet logoCodelet

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.

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" },
  ],
},
PlaceDrawswhen keysCommand receives
editor/titleButton(s) at the right of the tab bar, over the file showingeditorLangId, resourceLangId, resourceFilename, resourceExtname, resourceScheme, or activeWebviewPanelId for an extension's own tabNothing
view/titleButton(s) over a contributed view's own rowsviewNothing
explorer/contextRow menu of the file tree, under Rename/DeleteresourceLangId, resourceFilename, resourceExtname, resourceScheme, explorerResourceIsFolderThe row's Uri (the root's, for a click below the last row)
view/item/contextRow menu of a contributed treeview, viewItem (the row's contextValue), plus the four resource* keys if the row set resourceUriThe element the TreeDataProvider returned
editor/title/contextTab's own context menu, under its three ClosesresourceLangId, resourceFilename, resourceExtname, resourceSchemeThe tab's Uri
editor/contextRight-click in the document itselfeditorLangId, resourceLangId, resourceFilename, resourceExtname, resourceSchemeThe showing file's Uri
scm/titleOver a source control's own headingscmProviderThe SourceControl
scm/inputBoxBeside a source control's commit boxscmProviderThe SourceControl
scm/resourceGroup/contextOn a resource group's headingscmProvider, scmResourceGroup, scmResourceGroupStateThe SourceControlResourceGroup
scm/resourceState/contextOn a row in a resource groupscmProvider, scmResourceGroup, scmResourceGroupState, scmResourceStateThe SourceControlResourceState
globalActivityRow of the gear at the foot of the activity barThe window's own keys alone — no resourceNothing
commandPaletteNothing — only says whether a command is offered thereSame 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.

#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:

KeyValue
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.