Monaco 命令与操作:Editor Actions、快捷键与 Theia 命令系统

这一篇聚焦在 Monaco 自身的命令与操作能力上,以及这些能力如何与 Theia 的命令/快捷键系统对接。

Monaco 允许在编辑器实例上注册命令式的“操作”,常见用于:

  • 增加右键菜单项;
  • 为编辑器特定状态添加行为(例如格式化当前选区、注释/取消注释等)。

注册一个简单的 action 示例:

 1const editor = monaco.editor.create(domNode, { /* ... */ });
 2
 3editor.addAction({
 4  id: 'my-indent-action',
 5  label: 'Indent Two Spaces',
 6  keybindings: [
 7    monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_CLOSE_SQUARE_BRACKET, // Ctrl/Cmd + ]
 8  ],
 9  contextMenuGroupId: 'navigation',
10  contextMenuOrder: 1.5,
11  run(ed) {
12    const model = ed.getModel();
13    if (!model) {
14      return;
15    }
16    // 这里只是示意:实际应按行缩进
17    const selection = ed.getSelection();
18    if (!selection) {
19      return;
20    }
21    const text = model.getValueInRange(selection);
22    model.pushEditOperations(
23      [selection],
24      [{ range: selection, text: '  ' + text }],
25      () => null,
26    );
27  },
28});

关键字段:

  • id:操作的唯一标识;
  • label:显示在菜单/命令面板中的名称;
  • keybindings:与此操作关联的快捷键(使用 KeyModKeyCode 组合);
  • contextMenuGroupId / contextMenuOrder:控制在编辑器右键菜单中的分组与顺序;
  • run:执行逻辑,参数是当前编辑器实例。

Monaco 内部有自己的快捷键系统,keybindings 使用形如 CtrlCmd | Shift | F10 这一类组合。
常见写法:

1const keybinding = monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S; // Ctrl/Cmd + S

在仅使用 Monaco 的场景中,可以完全依赖它的 keybinding 机制来定义编辑器内的快捷键。
在 Theia 中,则需要和它自己的命令/快捷键系统协同工作。

Theia 有一套独立的命令系统(CommandRegistry / CommandContribution),
常见模式是:

  • CommandContribution 中注册命令 ID 与执行逻辑;
  • KeybindingContribution 中为命令绑定快捷键;
  • 编辑器相关的命令通常由某个 EditorService 或 EditorManager 内部转发到具体的 Monaco 编辑器。

典型示意:

 1export const MyCommands = {
 2  indentTwoSpaces: {
 3    id: 'my.indentTwoSpaces',
 4    label: 'Indent Two Spaces',
 5  },
 6} as const;
 7
 8@injectable()
 9export class MyCommandContribution implements CommandContribution {
10  constructor(
11    @inject(EditorManager) protected readonly editorManager: EditorManager,
12  ) {}
13
14  registerCommands(registry: CommandRegistry): void {
15    registry.registerCommand(MyCommands.indentTwoSpaces, {
16      execute: () => {
17        const widget = this.editorManager.currentEditor;
18        if (!widget) {
19          return;
20        }
21        const editor = widget.editor; // TextEditor 抽象
22        // 通过 TextEditor 抽象或向下转型到 MonacoEditor 调用 Monaco API
23      },
24    });
25  }
26}

在这种模式下:

  • 编辑器行为的“来源”是 Theia 命令系统;
  • 编辑器内部的具体执行则是通过 MonacoEditor 调用 Monaco 的编辑操作或 action。

可以归纳为两种常见协同方式:

  1. 以 Theia 命令为主,Monaco Action 为辅

    • 大部分编辑器相关操作都注册成 Theia 命令;
    • 在实现上,通过 MonacoEditor 的 API 或 editor.getAction(id) 来执行 Monaco 内建操作;
    • 好处是所有行为都能通过 Theia 的命令/快捷键体系统一管理。
  2. 在 Monaco 内注册特定的编辑器内部行为

    • 将某些仅在编辑器上下文有意义的操作注册为 Monaco action(例如某个特定语言的临时帮助);
    • 通过编辑器右键菜单或特定快捷键触发,不一定映射到全局命令系统。

在像 Theia 这样的 IDE 场景中,更推荐第一个路径:
命令的“源头”统一在 Theia 层,Monaco 作为执行载体,这样对扩展、快捷键自定义、命令面板都更友好。

Monaco 为编辑器本身提供了一套完整的动作与快捷键机制(addActionrunkeybindings),
Theia 则在其之上建立了统一的命令系统和快捷键层,将编辑器操作纳入整个 IDE 的行为编排中。

理解这两层的分工,有助于在扩展中合理地选择:
哪些操作应该是全局命令(从 Theia 命令系统切入),哪些只是编辑器内部的小功能(可以直接用 Monaco action 实现)。**