Skip to main content

Commands API

The Commands API is the primary way extensions interact with the editor and with each other. Every meaningful action in the app -- saving a file, opening a tab, running a project -- is a command that can be registered, executed, hooked, and extended by any extension.

Commands serve three roles:

  1. Entry points -- Extensions declare commands in their manifest so users can discover and run them from the command palette.
  2. Programmatic actions -- Extensions call editor.commands.execute() to trigger built-in or third-party commands.
  3. Interception points -- The hook system lets extensions run logic before or after any command, including cancelling it.

Table of contents


Manifest declaration

Declaring commands in manifest.json makes them visible in the command palette before your extension code runs. This is the recommended way to expose user-facing actions.

{
"contributes": {
"commands": [
{
"id": "myext.sayHello",
"label": "Say Hello",
"description": "Greets the user with a toast notification",
"category": "Extensions"
},
{
"id": "myext.formatSelection",
"label": "Format Selection",
"description": "Formats the currently selected text",
"category": "Editor"
}
]
}
}

Command fields

FieldTypeRequiredDefaultDescription
idstringyes--Globally unique command identifier. Convention: <extensionId>.<action>.
labelstringnoLast segment of idHuman-readable label shown in the command palette. If omitted, the part after the last . in the id is used (e.g., myext.sayHello becomes sayHello).
descriptionstringno''Short description shown below the label in the command palette.
categorystringno'Extensions'Category for grouping in the command palette (e.g., "Editor", "File", "View").
important

Declaring a command in the manifest only creates a placeholder entry. You must also register a handler at runtime (via editor.commands.registerCommand) for the command to do anything when invoked. If the extension is not yet active when the command is triggered, the app will attempt to activate it first (see Lazy activation).


Runtime registration

Use editor.commands.registerCommand() to register a handler function for a command. This is typically done inside your onLoad callback.

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
// Register a simple command
editor.commands.registerCommand("myext.sayHello", (args) => {
const name = args?.name || "World";
editor.window.showToast(`Hello, ${name}!`);
return { greeted: name };
});
});

Signature

editor.commands.registerCommand(id, handler, options?)
ParameterTypeDescription
idstringCommand ID. Must match what is declared in the manifest (if any).
handler(args?) => anyFunction invoked when the command is executed. Can be sync or async. Receives optional arguments and can return a value.
optionsobjectOptional. Override manifest metadata at registration time.
options.labelstringOverride the label from the manifest.
options.descriptionstringOverride the description.
options.categorystringOverride the category.

Registration behavior

  • If a command with the same id is already registered by a different extension (or by the app itself), the registration is rejected and an error is logged.
  • Re-registration by the same extension is allowed (the previous handler is replaced).

Executing commands

Any extension can execute any registered command -- built-in, from another extension, or its own.

const result = await editor.commands.execute(commandId, args);
ParameterTypeDescription
commandIdstringThe ID of the command to execute.
argsanyOptional arguments passed to the command handler. Typically an object.

Returns: A Promise that resolves with whatever value the command handler returned.

Examples

// Execute your own command
const result = await editor.commands.execute("myext.sayHello", { name: "Dev" });
console.log(result); // { greeted: "Dev" }

// Execute a built-in command
await editor.commands.execute("editor.save");

// Open a specific file
await editor.commands.execute("tab.open", { uri: fileUri });

// Create a new file
await editor.commands.execute("file.create", {
parentFolderUri: folderUri,
name: "index.html",
content: "<!DOCTYPE html>"
});

Execution timeout

When the app dispatches a command to an extension worker, it enforces a 30-second timeout. If the handler does not return within that window, the call resolves with null. Design long-running operations to show progress and complete within this limit.


Listing commands

Extensions can query the set of registered commands at runtime using editor.commands.list().

const commands = await editor.commands.list({
category: "Editor", // optional: filter by category
extensionId: "myext", // optional: filter by extension
enabledOnly: true // optional: default true
});

Each entry in the returned array contains:

{
"id": "editor.save",
"label": "Save File",
"category": "Editor",
"extensionId": null,
"extensionName": null,
"isEnabled": true,
"shortcutHint": "Ctrl+S",
"description": "Save the current file"
}

Built-in commands have extensionId: null. Extension commands include the extension's ID and display name.


Hooks

Commands support before and after hooks that let extensions intercept command execution. See Hooks for full details.


Command palette integration

Commands registered by extensions automatically appear in the app's command palette (opened with Ctrl+Shift+P or the search.commandPalette command).

How commands are displayed

  • Label format: Extension commands show as ExtensionName: Label (e.g., "API Tester: Show Test Menu"). Built-in commands show just the label.
  • Description: Shown below the label in smaller text when a description is provided.
  • Category: Used for visual grouping. Common categories: "Extensions", "Editor", "File", "View", "Tab", "Search".
  • Sorting: Commands are sorted by usage frequency (most-used first), then alphabetically.
  • Filtering: Only enabled commands whose when condition passes (if any) are shown.

Manifest-only commands

If you declare a command in the manifest but your extension has not been activated yet (e.g., because it uses onCommand: activation), a placeholder command is registered. When the user selects it:

  1. The extension is activated (its main.js is loaded and onLoad runs).
  2. The extension registers the real handler via registerCommand.
  3. The command is executed with the original arguments.

This entire flow happens transparently. The user sees a brief delay while the extension loads.


Lazy activation with onCommand

Use onCommand activation events to defer loading your extension until the user actually needs it. This reduces startup time and memory usage.

manifest.json:

{
"activationEvents": ["onCommand:myext.heavyTask"],
"contributes": {
"commands": [
{
"id": "myext.heavyTask",
"label": "Run Heavy Task",
"description": "Performs an expensive computation"
}
]
}
}

main.js:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
editor.commands.registerCommand("myext.heavyTask", async () => {
// This code only loads when the user first runs the command
const result = await performExpensiveWork();
editor.window.showToast(`Done: ${result}`);
});
});

The command appears in the command palette immediately (from the manifest declaration), but main.js is only loaded and executed when the user first invokes myext.heavyTask. Once the extension is ready, the command is forwarded for execution.


Built-in command IDs

These commands are registered by the app and can be executed or hooked by any extension.

File commands

Command IDDescriptionArgs
file.createCreate a new file{ parentFolderUri, name, content? }
file.deleteDelete a file or folder{ uri }
file.renameRename a file or folder{ uri, newName }
file.moveMove a file or folder{ uri, targetFolderUri }
file.writeWrite content to a file{ uri, content }
file.readRead file content{ uri }
file.copyCopy a file{ uri, targetFolderUri }
folder.createCreate a new folder{ parentFolderUri, name }
folder.listList directory contents{ uri }

Tab commands

Command IDDescriptionArgs
tab.openOpen a file in a tab{ uri }
tab.closeClose a tabkey (string)
tab.closeCurrentClose the active tab--
tab.closeAllClose all tabs--
tab.closeOthersClose all tabs except onekey (string)
tab.setCurrentSwitch to a tabkey (string)
tab.nextSwitch to next tab--
tab.previousSwitch to previous tab--
tab.openWebviewOpen a webview tab{ url?, html?, title? }
tab.refreshRefresh current tab--

Editor commands

Command IDDescriptionArgs
editor.saveSave the current file--
editor.saveAllSave all open files--
editor.undoUndo last change--
editor.redoRedo last change--
editor.formatFormat current document--
editor.selectFormatterPick default formatter--

View commands

Command IDDescription
view.toggleBottomBarShow/hide bottom bar
view.toggleTopToolsShow/hide toolbar
view.toggleFullscreenEnter/exit fullscreen
view.toggleSidebarToggle file browser sidebar
view.toggleFloatingActionHubToggle floating action hub

Search commands

Command IDDescription
search.findOpen find in current file
search.quickOpenOpen quick open (file search)
search.commandPaletteOpen command palette
search.findInFilesSearch across project files

Project commands

Command IDDescription
project.runRun/preview the project
project.publishPublish the project
project.autoSaveToggleToggle auto-save
project.previewUrlConfigure preview URL

Other commands

Command IDDescription
settings.openOpen settings editor
theme.selectOpen theme picker
terminal.openOpen a terminal tab
tab.refreshRefresh current tab

Full example

Below is a complete extension that demonstrates manifest declaration, runtime registration, cross-command execution, and hooks.

manifest.json:

{
"id": "word-counter",
"name": "Word Counter",
"version": "1.0.0",
"description": "Counts words in the current file and prevents saving empty files",
"main": "main.js",
"activationEvents": ["onStartupFinished"],
"contributes": {
"commands": [
{
"id": "word-counter.count",
"label": "Count Words",
"description": "Show word count for the active file",
"category": "Tools"
},
{
"id": "word-counter.countAll",
"label": "Count Words in All Tabs",
"description": "Show word count for every open tab",
"category": "Tools"
}
]
}
}

main.js:

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
// -- Register commands --

editor.commands.registerCommand("word-counter.count", async () => {
const file = await editor.workspace.getActiveFile();
if (!file) {
editor.window.showToast("No file is open");
return;
}

const content = await editor.workspace.fs.read(file.uri);
const words = content.trim().split(/\s+/).filter(Boolean).length;

editor.window.showToast(`${file.name}: ${words} words`);
return { file: file.name, words };
});

editor.commands.registerCommand("word-counter.countAll", async () => {
const tabs = await editor.workspace.getOpenTabs();
const results = [];

for (const tab of tabs) {
try {
const content = await editor.workspace.fs.read(tab.uri);
const words = content.trim().split(/\s+/).filter(Boolean).length;
results.push(`${tab.name}: ${words} words`);
} catch {
results.push(`${tab.name}: (could not read)`);
}
}

await editor.window.showPopup({
title: "Word Count - All Tabs",
contentType: "markdown",
markdown: results.map((r) => `- ${r}`).join("\n"),
});
});

// -- Register hooks --

// Before hook: prevent saving empty files
editor.commands.registerHook(
{ commandId: "editor.save", type: "before", priority: 50 },
async () => {
const file = await editor.workspace.getActiveFile();
if (!file) return true;

const content = await editor.workspace.fs.read(file.uri);
if (content.trim().length === 0) {
const confirmed = await editor.window.showConfirm({
title: "Empty File",
message: `"${file.name}" is empty. Save anyway?`,
});
return confirmed; // false cancels the save
}
return true;
}
);

// After hook: show word count after every save
editor.commands.registerHook(
{ commandId: "editor.save", type: "after", priority: 100 },
async () => {
await editor.commands.execute("word-counter.count");
}
);
});

RPC reference

These are the low-level RPC methods used by the SDK. Extension authors typically use the SDK wrapper functions above, but this reference is useful for understanding the protocol.

RPC MethodDirectionDescription
commands.registerJS -> DartRegister a command handler. Params: { id, label?, description?, category? }
commands.executeJS -> DartExecute a registered command. Params: { id, args? }. Returns the command result.
commands.listJS -> DartList registered commands. Params: { category?, extensionId?, enabledOnly? }. Returns array of command metadata.
commands.registerHookJS -> DartRegister a before/after hook. Params: { commandId, type, priority?, hookId? }. Returns { hookId }.
commands.hookResponseJS -> DartRespond to a pending before-hook invocation. Params: { invocationId, result }.
hookInvoked (event)Dart -> JSPushed to the extension when a hook it registered is triggered. Data: { hookId, invocationId, commandId, type, args }.