VS Code 插件:命令、菜单与快捷键——从 contributes 到代码注册

在 VS Code 插件里,最常见的扩展能力就是「加一个命令,挂到菜单或快捷键上」。
这一篇不走「按 API 文档从上到下扫一遍」的路线,而是想把整条链路讲清楚:一个命令从 package.json 里的 contributes 声明,到真正出现在命令面板、右键菜单和快捷键里,中间到底发生了什么。

可以先用一句话定个调:

  • 命令(command)是核心动作,菜单和快捷键只是触发命令的入口。

在 VS Code 里:

  • 命令有一个全局唯一的 id(如 "extension.sayHello"),代表一个可以被触发的动作;
  • 命令可以被多种入口触发:
    • 命令面板(Ctrl+Shift+P);
    • 菜单/右键菜单;
    • 快捷键;
    • 代码内部直接调用 vscode.commands.executeCommand

所以在设计插件时,思路一般是:

  • 先想清楚你要暴露哪些「动作」,为它们定义命令;
  • 再根据需要,把这些命令挂到合适的位置上(菜单、快捷键等)。

第一步,是在 package.json 里告诉 VS Code:「我有这些命令」。

典型结构是这样的:

1"contributes": {
2  "commands": [
3    {
4      "command": "extension.sayHello",
5      "title": "Say Hello",
6      "category": "Demo"
7    }
8  ]
9}

要点:

  • command:命令 id,后续会在代码和菜单/快捷键里引用它;
  • title:显示在命令面板等位置的文案;
  • category:可选,用于在命令面板中分组显示(如 Demo: Say Hello)。

这一步只是「登记了命令的存在」,并没有告诉 VS Code 触发时要干什么。

第二步,在扩展的 activate 函数中,用 vscode.commands.registerCommand 注册命令的具体实现。

示意代码:

 1import * as vscode from "vscode";
 2
 3export function activate(context: vscode.ExtensionContext) {
 4  const disposable = vscode.commands.registerCommand(
 5    "extension.sayHello",
 6    () => {
 7      vscode.window.showInformationMessage("Hello from extension!");
 8    }
 9  );
10
11  context.subscriptions.push(disposable);
12}

要点:

  • registerCommand(id, handler)
    • id 必须和 package.json 里声明的命令 id 一致;
    • handler 是执行逻辑,可以接收参数(例如来自 executeCommand 或菜单的上下文)。
  • 返回值是一个 disposable,需要加到 context.subscriptions 里,确保扩展卸载时正确清理。

结合上一节,可以把命令的“注册过程”理解成:

  • package.json 里声明:我有一个叫 extension.sayHello 的命令
  • activate 里注册:当这个命令被触发时,请执行这段 handler

有了命令之后,下一步就是决定它在哪些菜单里出现。
这依然是通过 package.jsoncontributes.menus 来完成。

示意结构(只展示一部分):

 1"contributes": {
 2  "menus": {
 3    "editor/context": [
 4      {
 5        "command": "extension.sayHello",
 6        "group": "navigation",
 7        "when": "editorTextFocus"
 8      }
 9    ]
10  }
11}

几个关键点:

  • editor/context
    • 表示编辑器里的右键菜单(上下文菜单);
    • 还有其他位置,如 explorer/context(资源管理器右键)、editor/title(编辑器标题栏)等。
  • command
    • 填之前声明的命令 id;
  • group
    • 决定菜单项在该菜单中的相对分组位置,类似「分组/排序键」;
  • when
    • 一段条件表达式,决定菜单项何时显示;
    • editorTextFocus 表示只有编辑器有焦点时才显示。

这样,当用户在编辑器里右键时,就会在对应分组里看到「Say Hello」菜单项,点击后触发 extension.sayHello 命令。

快捷键同样通过 contributes.keybindings 来声明。

示意片段:

1"contributes": {
2  "keybindings": [
3    {
4      "command": "extension.sayHello",
5      "key": "ctrl+alt+h",
6      "when": "editorTextFocus"
7    }
8  ]
9}

字段含义:

  • command:依然是命令 id;
  • key:按键组合,支持平台差异(如 cmd / ctrl);
  • when:条件表达式,和菜单里的 when 用的是同一套上下文系统。

理解上可以记成:

  • 命令是动作;
  • 快捷键就是「把某个按键组合映射到这个命令」的规则;
  • when 决定这条映射规则什么时候生效。

用一个具体例子把上面几块串起来:

  1. package.json 里:
    • contributes.commands:声明 extension.sayHello 命令;
    • contributes.menus.editor/context:把该命令挂到编辑器右键菜单;
    • contributes.keybindings:为该命令绑定一个快捷键。
  2. 在扩展入口代码中:
    • activate(context) 里,用 vscode.commands.registerCommand("extension.sayHello", handler) 注册实现;
    • 把返回的 disposable 放进 context.subscriptions
  3. 运行时:
    • 当用户打开某种文档/视图,满足 activationEvents 时,VS Code 激活扩展并调用 activate
    • 菜单和快捷键系统根据 contributes 中的声明,将命令挂到对应位置;
    • 用户通过命令面板、菜单、快捷键等任一入口触发命令,最终执行 handler。

理解了这条链路之后,再看更复杂的情况(比如带参数命令、条件菜单、不同平台快捷键)就会简单很多。

在实际做命令/菜单/快捷键扩展时,常见的一些问题包括:

  • 命令 id 不一致
    • package.json 里写的是一个 id,代码里注册时不小心写成了另一个,导致命令在 UI 里可见但点了没反应。
  • 滥用全局激活事件
    • 为了图省事把所有命令都挂在 *onStartupFinished 下,扩展一上来就被激活,拖慢启动;
    • 更推荐用 onCommand:xxx 或语言/文件类型等更精确的激活条件。
  • when 条件写得太宽或太窄
    • 太宽:菜单/快捷键在不合适的上下文也出现或生效;
    • 太窄:用户预期看到的菜单/快捷键没有出现。
    • 建议多用 VS Code 的「开发者工具」面板和内置文档来确认可用的上下文 key。

答题或写文时,可以把实践建议归纳成一句:

  • 先把命令划清边界,再用菜单和快捷键有节制地暴露入口,同时用合适的激活事件和 when 条件控制好「何时可见 / 可用」。