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:
- Entry points -- Extensions declare commands in their manifest so users can discover and run them from the command palette.
- Programmatic actions -- Extensions call
editor.commands.execute()to trigger built-in or third-party commands. - Interception points -- The hook system lets extensions run logic before or after any command, including cancelling it.
Table of contents
- Manifest declaration
- Runtime registration
- Executing commands
- Listing commands
- Hooks
- Command palette integration
- Lazy activation with onCommand
- Built-in command IDs
- Full example
- RPC reference
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | -- | Globally unique command identifier. Convention: <extensionId>.<action>. |
label | string | no | Last segment of id | Human-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). |
description | string | no | '' | Short description shown below the label in the command palette. |
category | string | no | 'Extensions' | Category for grouping in the command palette (e.g., "Editor", "File", "View"). |
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?)
| Parameter | Type | Description |
|---|---|---|
id | string | Command ID. Must match what is declared in the manifest (if any). |
handler | (args?) => any | Function invoked when the command is executed. Can be sync or async. Receives optional arguments and can return a value. |
options | object | Optional. Override manifest metadata at registration time. |
options.label | string | Override the label from the manifest. |
options.description | string | Override the description. |
options.category | string | Override the category. |
Registration behavior
- If a command with the same
idis 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);
| Parameter | Type | Description |
|---|---|---|
commandId | string | The ID of the command to execute. |
args | any | Optional 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
descriptionis 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
whencondition 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:
- The extension is activated (its
main.jsis loaded andonLoadruns). - The extension registers the real handler via
registerCommand. - 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 ID | Description | Args |
|---|---|---|
file.create | Create a new file | { parentFolderUri, name, content? } |
file.delete | Delete a file or folder | { uri } |
file.rename | Rename a file or folder | { uri, newName } |
file.move | Move a file or folder | { uri, targetFolderUri } |
file.write | Write content to a file | { uri, content } |
file.read | Read file content | { uri } |
file.copy | Copy a file | { uri, targetFolderUri } |
folder.create | Create a new folder | { parentFolderUri, name } |
folder.list | List directory contents | { uri } |
Tab commands
| Command ID | Description | Args |
|---|---|---|
tab.open | Open a file in a tab | { uri } |
tab.close | Close a tab | key (string) |
tab.closeCurrent | Close the active tab | -- |
tab.closeAll | Close all tabs | -- |
tab.closeOthers | Close all tabs except one | key (string) |
tab.setCurrent | Switch to a tab | key (string) |
tab.next | Switch to next tab | -- |
tab.previous | Switch to previous tab | -- |
tab.openWebview | Open a webview tab | { url?, html?, title? } |
tab.refresh | Refresh current tab | -- |
Editor commands
| Command ID | Description | Args |
|---|---|---|
editor.save | Save the current file | -- |
editor.saveAll | Save all open files | -- |
editor.undo | Undo last change | -- |
editor.redo | Redo last change | -- |
editor.format | Format current document | -- |
editor.selectFormatter | Pick default formatter | -- |
View commands
| Command ID | Description |
|---|---|
view.toggleBottomBar | Show/hide bottom bar |
view.toggleTopTools | Show/hide toolbar |
view.toggleFullscreen | Enter/exit fullscreen |
view.toggleSidebar | Toggle file browser sidebar |
view.toggleFloatingActionHub | Toggle floating action hub |
Search commands
| Command ID | Description |
|---|---|
search.find | Open find in current file |
search.quickOpen | Open quick open (file search) |
search.commandPalette | Open command palette |
search.findInFiles | Search across project files |
Project commands
| Command ID | Description |
|---|---|
project.run | Run/preview the project |
project.publish | Publish the project |
project.autoSaveToggle | Toggle auto-save |
project.previewUrl | Configure preview URL |
Other commands
| Command ID | Description |
|---|---|
settings.open | Open settings editor |
theme.select | Open theme picker |
terminal.open | Open a terminal tab |
tab.refresh | Refresh 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 Method | Direction | Description |
|---|---|---|
commands.register | JS -> Dart | Register a command handler. Params: { id, label?, description?, category? } |
commands.execute | JS -> Dart | Execute a registered command. Params: { id, args? }. Returns the command result. |
commands.list | JS -> Dart | List registered commands. Params: { category?, extensionId?, enabledOnly? }. Returns array of command metadata. |
commands.registerHook | JS -> Dart | Register a before/after hook. Params: { commandId, type, priority?, hookId? }. Returns { hookId }. |
commands.hookResponse | JS -> Dart | Respond to a pending before-hook invocation. Params: { invocationId, result }. |
hookInvoked (event) | Dart -> JS | Pushed to the extension when a hook it registered is triggered. Data: { hookId, invocationId, commandId, type, args }. |