Actions and commands are the primary way to implement executable functionality in VS Code. The Actions framework provides a unified system for defining commands, registering them in menus, and binding them to keybindings.
Overview
VS Code distinguishes between:
Commands : Executable functions registered in the CommandsRegistry
Actions : Higher-level abstractions that combine commands with UI metadata (menus, keybindings, icons, etc.)
Menu Items : Commands displayed in specific menus with context-aware visibility
Command Actions
ICommandAction Interface
The ICommandAction interface defines the structure of a command with its metadata:
export interface ICommandAction {
id : string ;
title : string | ICommandActionTitle ;
shortTitle ?: string | ICommandActionTitle ;
/**
* Metadata about this command, used for API commands and keybindings
*/
metadata ?: ICommandMetadata ;
category ?: keyof typeof Categories | ILocalizedString | string ;
tooltip ?: string | ILocalizedString ;
icon ?: Icon ;
source ?: ICommandActionSource ;
/**
* Precondition controls enablement (shown grey in menus if false)
*/
precondition ?: ContextKeyExpression ;
/**
* The action is a toggle action with toggle state
*/
toggled ?: ContextKeyExpression | ICommandActionToggleInfo ;
}
Localized Strings
Commands should use ILocalizedString for internationalization:
export interface ILocalizedString {
/**
* The localized value of the string
*/
value : string ;
/**
* The original (non-localized) value of the string
*/
original : string ;
}
Example:
const command : ICommandAction = {
id: 'workbench.action.toggleSidebarVisibility' ,
title: {
value: localize ( 'toggleSidebar' , "Toggle Sidebar Visibility" ),
original: 'Toggle Sidebar Visibility'
},
category: Categories . View
};
Action2: The Modern Action API
Action2 is the recommended way to register actions. It combines command registration, menu items, and keybindings in a single declaration.
Basic Action2 Structure
import { Action2 , registerAction2 } from 'vs/platform/actions/common/actions' ;
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
class MyAction extends Action2 {
constructor () {
super ({
id: 'myExtension.myCommand' ,
title: {
value: 'My Command' ,
original: 'My Command'
},
category: 'My Extension' ,
f1: true , // Show in Command Palette
precondition: ContextKeyExpr . equals ( 'editorTextFocus' , true )
});
}
async run ( accessor : ServicesAccessor , ... args : any []) : Promise < void > {
// Access services via the accessor
const editorService = accessor . get ( IEditorService );
const configService = accessor . get ( IConfigurationService );
// Execute command logic
const activeEditor = editorService . activeEditor ;
// ...
}
}
// Register the action
registerAction2 ( MyAction );
Action2 with Menus and Keybindings
import { Action2 , registerAction2 , MenuId } from 'vs/platform/actions/common/actions' ;
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
import { KeyCode , KeyMod } from 'vs/base/common/keyCodes' ;
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey' ;
import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
import { INotificationService } from 'vs/platform/notification/common/notification' ;
class FormatDocumentAction extends Action2 {
constructor () {
super ({
id: 'editor.action.formatDocument' ,
title: {
value: 'Format Document' ,
original: 'Format Document'
},
category: 'Editor' ,
// Command Palette
f1: true ,
// Menus
menu: [
{
id: MenuId . EditorContext ,
when: ContextKeyExpr . and (
ContextKeyExpr . equals ( 'editorTextFocus' , true ),
ContextKeyExpr . equals ( 'editorReadonly' , false )
),
group: '1_modification' ,
order: 1
},
{
id: MenuId . EditorTitle ,
when: ContextKeyExpr . equals ( 'editorTextFocus' , true ),
group: 'navigation' ,
order: 10
}
],
// Keybindings
keybinding: {
primary: KeyMod . Shift | KeyMod . Alt | KeyCode . KeyF ,
when: ContextKeyExpr . equals ( 'editorTextFocus' , true ),
weight: KeybindingWeight . EditorContrib
},
// Icon
icon: Codicon . formatDocument ,
// Precondition
precondition: ContextKeyExpr . and (
ContextKeyExpr . equals ( 'editorTextFocus' , true ),
ContextKeyExpr . equals ( 'editorReadonly' , false )
)
});
}
async run ( accessor : ServicesAccessor ) : Promise < void > {
const configService = accessor . get ( IConfigurationService );
const notificationService = accessor . get ( INotificationService );
try {
// Format the document
await formatDocument ();
notificationService . info ( 'Document formatted successfully' );
} catch ( error ) {
notificationService . error ( 'Failed to format document' );
}
}
}
registerAction2 ( FormatDocumentAction );
Action2Options
export type IAction2Options = ICommandPaletteOptions | IBaseAction2Options ;
export interface ICommandPaletteOptions extends IAction2CommonOptions {
/**
* The title with localized strings
*/
title : ICommandActionTitle ;
/**
* The category for Command Palette
*/
category ?: keyof typeof Categories | ILocalizedString ;
/**
* Shorthand to add this command to the command palette
*/
f1 : true ;
}
interface IAction2CommonOptions extends ICommandAction {
/**
* One or many menu items
*/
menu ?: OneOrN <{ id : MenuId ; precondition ?: null } & Omit < IMenuItem , 'command' >>;
/**
* One or many keybindings
*/
keybinding ?: OneOrN < Omit < IKeybindingRule , 'id' >>;
}
VS Code defines over 100 menu locations. Here are the most commonly used:
Editor Menus
View Menus
Global Menus
SCM Menus
MenuId . EditorContext // Right-click in editor
MenuId . EditorTitle // Editor tab toolbar
MenuId . EditorTitleContext // Right-click on editor tab
MenuId . EditorLineNumberContext // Right-click on line numbers
You can register menu items independently from commands:
import { MenuRegistry , MenuId } from 'vs/platform/actions/common/actions' ;
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey' ;
MenuRegistry . appendMenuItem ( MenuId . EditorContext , {
command: {
id: 'myExtension.myCommand' ,
title: 'My Command' ,
icon: Codicon . sparkle
},
when: ContextKeyExpr . and (
ContextKeyExpr . equals ( 'resourceExtname' , '.md' ),
ContextKeyExpr . equals ( 'editorTextFocus' , true )
),
group: 'navigation' ,
order: 1
});
export interface IMenuItem {
command : ICommandAction ;
alt ?: ICommandAction ; // Alternative command (Shift+Click)
/**
* Menu item is hidden if this expression returns false
*/
when ?: ContextKeyExpression ;
group ?: 'navigation' | string ;
order ?: number ;
isHiddenByDefault ?: boolean ;
}
Menu Groups and Ordering
Menu items are organized into groups with ordering:
// Common groups:
// - 'navigation' - Primary actions (shown as icons)
// - '1_modification' - Edit operations
// - '2_workspace' - Workspace operations
// - '3_compare' - Compare/diff operations
// - '4_search' - Search operations
// - '5_cutcopypaste' - Cut/copy/paste
// - '9_cutcopypaste' - Last group
MenuRegistry . appendMenuItem ( MenuId . EditorContext , {
command: { id: 'editor.action.rename' , title: 'Rename Symbol' },
group: '1_modification' ,
order: 1.1
});
Create hierarchical menus using submenus:
export interface ISubmenuItem {
title : string | ICommandActionTitle ;
submenu : MenuId ;
icon ?: Icon ;
when ?: ContextKeyExpression ;
group ?: 'navigation' | string ;
order ?: number ;
}
Example:
// Define a custom submenu
const MySubMenuId = new MenuId ( 'myExtension.submenu' );
// Register items in the submenu
MenuRegistry . appendMenuItem ( MySubMenuId , {
command: { id: 'myExtension.action1' , title: 'Action 1' }
});
MenuRegistry . appendMenuItem ( MySubMenuId , {
command: { id: 'myExtension.action2' , title: 'Action 2' }
});
// Add the submenu to a parent menu
MenuRegistry . appendMenuItem ( MenuId . EditorContext , {
submenu: MySubMenuId ,
title: 'My Extension Actions' ,
group: '1_modification' ,
order: 10
});
Context Keys and When Clauses
Actions use context keys to control visibility and enablement:
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey' ;
// Single condition
when : ContextKeyExpr . equals ( 'editorTextFocus' , true )
// AND condition
when : ContextKeyExpr . and (
ContextKeyExpr . equals ( 'editorTextFocus' , true ),
ContextKeyExpr . equals ( 'editorLangId' , 'typescript' )
)
// OR condition
when : ContextKeyExpr . or (
ContextKeyExpr . equals ( 'resourceExtname' , '.ts' ),
ContextKeyExpr . equals ( 'resourceExtname' , '.js' )
)
// NOT condition
when : ContextKeyExpr . not ( 'editorReadonly' )
// Comparison
when : ContextKeyExpr . greater ( 'multiDiffEditorEnableViewChanges' , 0 )
// Regex matching
when : ContextKeyExpr . regex ( 'resourceFilename' , /test \. ts $ / )
Toggle Actions
Actions can represent toggle states:
export interface ICommandActionToggleInfo {
/**
* The condition that marks the action as toggled
*/
condition : ContextKeyExpression ;
icon ?: Icon ;
tooltip ?: string ;
title ?: string ;
}
Example:
class ToggleLineNumbersAction extends Action2 {
constructor () {
super ({
id: 'editor.action.toggleLineNumbers' ,
title: { value: 'Toggle Line Numbers' , original: 'Toggle Line Numbers' },
toggled: {
condition: ContextKeyExpr . equals ( 'config.editor.lineNumbers' , 'on' ),
title: 'Line Numbers' , // Title when checked
icon: Codicon . check
}
});
}
run ( accessor : ServicesAccessor ) : void {
const configService = accessor . get ( IConfigurationService );
const current = configService . getValue ( 'editor.lineNumbers' );
configService . updateValue ( 'editor.lineNumbers' , current === 'on' ? 'off' : 'on' );
}
}
Real-World Example
Here’s a complete example from VS Code’s codebase:
registerAction2 ( class extends Action2 {
constructor () {
super ({
id: 'workbench.action.closeActiveEditor' ,
title: {
value: localize ( 'closeActiveEditor' , "Close Editor" ),
original: 'Close Editor' ,
mnemonicTitle: localize ({ key: 'miCloseEditor' , comment: [ '&& denotes a mnemonic' ] }, "&&Close Editor" )
},
f1: true ,
category: Categories . View ,
precondition: undefined ,
keybinding: {
weight: KeybindingWeight . WorkbenchContrib ,
when: undefined ,
primary: KeyMod . CtrlCmd | KeyCode . KeyW ,
win: { primary: KeyMod . CtrlCmd | KeyCode . F4 , secondary: [ KeyMod . CtrlCmd | KeyCode . KeyW ] }
},
menu: [
{
id: MenuId . EditorTitleContext ,
group: '1_close' ,
order: 10 ,
when: ContextKeyExpr . not ( 'config.workbench.editor.showTabs.enabled' )
}
],
icon: Codicon . close
});
}
async run ( accessor : ServicesAccessor ) : Promise < void > {
const editorService = accessor . get ( IEditorService );
const activeEditor = editorService . activeEditorPane ;
if ( activeEditor ) {
await editorService . closeEditor ( activeEditor );
}
}
});
Best Practices
Use Action2 : Always prefer Action2 and registerAction2() over the older action registration methods. It’s more concise and maintainable.
Localize strings : Always use ILocalizedString with both value and original properties for all user-facing text.
Context awareness : Use when clauses to show actions only in relevant contexts. This keeps menus clean and improves UX.
Group and order : Use consistent grouping and ordering conventions to keep related actions together.
See Also