参加你附近的 ,了解 VS Code 中的 AI 辅助开发。

调试器扩展

Visual Studio Code 的调试架构允许扩展作者轻松地将现有调试器集成到 VS Code 中,同时所有调试器都具有一个通用的用户界面。

VS Code 自带一个内置的调试器扩展,即 Node.js 调试器扩展,它是 VS Code 支持的众多调试器功能的一个绝佳展示。

VS Code Debug Features

此屏幕截图显示了以下调试功能:

  1. 调试配置管理。
  2. 用于启动/停止和步进的调试操作。
  3. 源、函数、条件、内联断点和日志点。
  4. 堆栈跟踪,包括多线程和多进程支持。
  5. 在视图和悬停中导航复杂数据结构。
  6. 在悬停或源代码中内联显示的变量值。
  7. 管理监视表达式。
  8. 用于交互式评估和自动补全的调试控制台。

本文档将帮助您创建一个调试器扩展,使任何调试器都能与 VS Code 配合使用。

VS Code 的调试架构

VS Code 基于我们引入的抽象协议实现了一个通用(与语言无关)的调试器 UI,用于与调试器后端通信。由于调试器通常不实现此协议,因此需要一些中间件来“适配”调试器以符合协议。此中间件通常是一个与调试器通信的独立进程。

VS Code Debug Architecture

我们将此中间件称为 **调试适配器**(简称 **DA**),DA 和 VS Code 之间使用的抽象协议是 **调试适配器协议**(简称 **DAP**)。由于调试适配器协议独立于 VS Code,它有自己的网站,您可以在其中找到介绍和概述、详细的规范以及一些包含已知实现和支持工具的列表。DAP 的历史和动机在此博客文章中有所解释。

由于调试适配器独立于 VS Code,并且可以在其他开发工具中使用,因此它们不符合基于扩展和贡献点的 VS Code 扩展性架构。

因此,VS Code 提供了一个贡献点 `debuggers`,可以在其中为特定的调试类型(例如 Node.js 调试器的 `node`)贡献一个调试适配器。每当用户启动该类型的调试会话时,VS Code 就会启动注册的 DA。

因此,以最简单的形式,调试器扩展只是调试适配器实现的声明性贡献,而该扩展基本上是调试适配器的打包容器,没有任何额外的代码。

VS Code Debug Architecture 2

一个更真实的调试器扩展将许多或所有以下声明性项贡献给 VS Code:

  • 调试器支持的语言列表。VS Code 启用 UI 以便为这些语言设置断点。
  • 调试器引入的调试配置属性的 JSON 模式。VS Code 使用此模式来验证 launch.json 编辑器中的配置并提供 IntelliSense。请注意,不支持 JSON 模式构造 `$ref` 和 `definition`。
  • VS Code 创建的初始 launch.json 的默认调试配置。
  • 用户可以添加到 launch.json 文件的调试配置片段。
  • 可在调试配置中使用的变量声明。

您可以在 `contributes.breakpoints``contributes.debuggers` 参考中找到更多信息。

除了上述纯声明性贡献之外,调试扩展 API 还支持以下基于代码的功能:

  • VS Code 创建的初始 launch.json 的动态生成的默认调试配置。
  • 动态确定要使用的调试适配器。
  • 在将调试配置传递给调试适配器之前进行验证或修改。
  • 与调试适配器通信。
  • 向调试控制台发送消息。

本文档的其余部分将介绍如何开发调试器扩展。

模拟调试扩展

由于从头开始创建一个调试适配器对于本教程来说有点繁重,我们将从一个简单的 DA 开始,我们将其创建为一个教育性的“调试适配器入门工具包”。它被称为 *模拟调试*,因为它不与真正的调试器通信,而是模拟一个。模拟调试模拟了一个调试器,并支持单步执行、继续、断点、异常和变量访问,但它没有连接到任何真正的调试器。

在深入了解模拟调试的开发设置之前,让我们首先从 VS Code Marketplace 安装一个预构建版本并试用它:

  • 切换到“扩展”视图并输入“mock”搜索 Mock Debug 扩展。
  • “安装”并“重新加载”扩展。

要尝试模拟调试:

  • 创建一个新的空文件夹 `mock test` 并在 VS Code 中打开它。
  • 创建一个文件 `readme.md` 并输入几行任意文本。
  • 切换到“运行和调试”视图(⇧⌘D(Windows、Linux Ctrl+Shift+D)并选择“**创建 launch.json 文件**”链接。
  • VS Code 将允许您选择一个“调试器”以创建默认的启动配置。选择“Mock Debug”。
  • 按下绿色的“**开始**”按钮,然后按 Enter 确认建议的文件 `readme.md`。

调试会话开始,您可以“单步”执行 readme 文件,设置并命中断点,以及遇到异常(如果行中出现单词 `exception`)。

Mock Debugger running

在将 Mock Debug 用作您自己开发的起点之前,我们建议您首先卸载预构建版本:

  • 切换到“扩展”视图并单击 Mock Debug 扩展的齿轮图标。
  • 运行“卸载”操作,然后“重新加载”窗口。

模拟调试的开发设置

现在让我们获取 Mock Debug 的源代码并在 VS Code 中开始开发它:

git clone https://github.com/microsoft/vscode-mock-debug.git
cd vscode-mock-debug
yarn

在 VS Code 中打开项目文件夹 `vscode-mock-debug`。

包中有什么?

  • `package.json` 是 mock-debug 扩展的清单。
    • 它列出了 mock-debug 扩展的贡献。
    • `compile` 和 `watch` 脚本用于将 TypeScript 源代码转换为 `out` 文件夹并监视后续的源代码修改。
    • 依赖项 `vscode-debugprotocol`、`vscode-debugadapter` 和 `vscode-debugadapter-testsupport` 是简化基于 Node 的调试适配器开发的 NPM 模块。
  • `src/mockRuntime.ts` 是一个带有简单调试 API 的 *模拟* 运行时。
  • 将运行时 *适配* 到调试适配器协议的代码位于 `src/mockDebug.ts` 中。在这里您可以找到 DAP 各种请求的处理程序。
  • 由于调试器扩展的实现位于调试适配器中,因此根本不需要扩展代码(即在扩展主机进程中运行的代码)。但是,Mock Debug 有一个小的 `src/extension.ts`,因为它说明了在调试器扩展的扩展代码中可以做些什么。

现在,通过选择 **Extension** 启动配置并按下 `F5` 来构建并启动 Mock Debug 扩展。最初,这将对 TypeScript 源代码进行完整的转译到 `out` 文件夹中。在完整构建之后,将启动一个 *监视任务*,该任务将转译您所做的任何更改。

转译源代码后,将出现一个标记为“[Extension Development Host]”的新 VS Code 窗口,其中 Mock Debug 扩展现在以调试模式运行。从该窗口中打开带有 `readme.md` 文件的 `mock test` 项目,使用“F5”启动调试会话,然后单步执行它。

Debugging Extension and Server

由于您在调试模式下运行扩展,因此现在可以在 `src/extension.ts` 中设置并命中断点,但正如我上面提到的,在扩展中执行的有趣代码不多。有趣的代码在调试适配器中运行,这是一个单独的进程。

为了调试调试适配器本身,我们必须在调试模式下运行它。最简单的方法是让调试适配器以 *服务器模式* 运行,并配置 VS Code 连接到它。在您的 VS Code vscode-mock-debug 项目中,从下拉菜单中选择启动配置 **Server**,然后按下绿色的启动按钮。

由于我们已经有一个活动的扩展调试会话,VS Code 调试器 UI 现在进入 *多会话* 模式,这通过在 CALL STACK 视图中看到两个调试会话 **Extension** 和 **Server** 的名称来表示。

Debugging Extension and Server

现在我们能够同时调试扩展和 DA。更快的方法是使用 **Extension + Server** 启动配置,它会自动启动两个会话。

调试扩展和 DA 的另一种甚至更简单的方法可以在下方找到。

在文件 `src/mockDebug.ts` 中的方法 `launchRequest(...)` 的开头设置一个断点,最后一步是配置模拟调试器以连接到 DA 服务器,方法是为您的模拟测试启动配置添加一个端口 `4711` 的 `debugServer` 属性。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "mock",
      "request": "launch",
      "name": "mock test",
      "program": "${workspaceFolder}/readme.md",
      "stopOnEntry": true,
      "debugServer": 4711
    }
  ]
}

如果您现在启动此调试配置,VS Code 不会将模拟调试适配器作为单独的进程启动,而是直接连接到已运行的服务器的本地端口 4711,并且您应该命中 `launchRequest` 中的断点。

通过此设置,您现在可以轻松编辑、转译和调试 Mock Debug。

但现在真正的工作开始了:您必须用与“真实”调试器或运行时通信的代码替换 `src/mockDebug.ts` 和 `src/mockRuntime.ts` 中的模拟实现。这涉及理解和实现调试适配器协议。有关此内容的更多详细信息可以在此处找到。

调试器扩展的 package.json 结构

除了提供调试适配器的特定于调试器的实现之外,调试器扩展还需要一个 `package.json` 文件来贡献各种与调试相关的贡献点。

因此,让我们仔细看看 Mock Debug 的 `package.json`。

像每个 VS Code 扩展一样,`package.json` 声明了扩展的基本属性 **name**、**publisher** 和 **version**。使用 **categories** 字段可以使扩展在 VS Code 扩展市场中更容易找到。

{
  "name": "mock-debug",
  "displayName": "Mock Debug",
  "version": "0.24.0",
  "publisher": "...",
  "description": "Starter extension for developing debug adapters for VS Code.",
  "author": {
    "name": "...",
    "email": "..."
  },
  "engines": {
    "vscode": "^1.17.0",
    "node": "^7.9.0"
  },
  "icon": "images/mock-debug-icon.png",
  "categories": ["Debuggers"],

  "contributes": {
    "breakpoints": [{ "language": "markdown" }],
    "debuggers": [
      {
        "type": "mock",
        "label": "Mock Debug",

        "program": "./out/mockDebug.js",
        "runtime": "node",

        "configurationAttributes": {
          "launch": {
            "required": ["program"],
            "properties": {
              "program": {
                "type": "string",
                "description": "Absolute path to a text file.",
                "default": "${workspaceFolder}/${command:AskForProgramName}"
              },
              "stopOnEntry": {
                "type": "boolean",
                "description": "Automatically stop after launch.",
                "default": true
              }
            }
          }
        },

        "initialConfigurations": [
          {
            "type": "mock",
            "request": "launch",
            "name": "Ask for file name",
            "program": "${workspaceFolder}/${command:AskForProgramName}",
            "stopOnEntry": true
          }
        ],

        "configurationSnippets": [
          {
            "label": "Mock Debug: Launch",
            "description": "A new configuration for launching a mock debug program",
            "body": {
              "type": "mock",
              "request": "launch",
              "name": "${2:Launch Program}",
              "program": "^\"\\${workspaceFolder}/${1:Program}\""
            }
          }
        ],

        "variables": {
          "AskForProgramName": "extension.mock-debug.getProgramName"
        }
      }
    ]
  },

  "activationEvents": ["onDebug", "onCommand:extension.mock-debug.getProgramName"]
}

现在来看看 **contributes** 部分,它包含特定于调试扩展的贡献。

首先,我们使用 **breakpoints** 贡献点列出将启用设置断点的语言。如果没有此功能,将无法在 Markdown 文件中设置断点。

接下来是 **debuggers** 部分。在这里,一个调试器以调试 **类型** `mock` 引入。用户可以在启动配置中引用此类型。可选属性 **label** 可用于在 UI 中显示时为调试类型提供一个友好的名称。

由于调试扩展使用调试适配器,因此其代码的相对路径作为 **program** 属性给出。为了使扩展自包含,应用程序必须位于扩展文件夹内。按照惯例,我们将此应用程序保存在名为 `out` 或 `bin` 的文件夹中,但您可以随意使用不同的名称。

由于 VS Code 在不同的平台上运行,我们必须确保 DA 程序也支持不同的平台。为此,我们有以下选项:

  1. 如果程序以平台无关的方式实现,例如作为在所有支持平台上都可用的运行时上运行的程序,您可以通过 **runtime** 属性指定此运行时。目前,VS Code 支持 `node` 和 `mono` 运行时。我们上面提到的 Mock 调试适配器就采用了这种方法。

  2. 如果您的 DA 实现需要在不同平台上使用不同的可执行文件,则可以像这样为特定平台限定 **program** 属性:

    "debuggers": [{
        "type": "gdb",
        "windows": {
            "program": "./bin/gdbDebug.exe",
        },
        "osx": {
            "program": "./bin/gdbDebug.sh",
        },
        "linux": {
            "program": "./bin/gdbDebug.sh",
        }
    }]
    
  3. 两种方法也可以结合使用。以下示例来自 Mono DA,它作为 mono 应用程序实现,在 macOS 和 Linux 上需要运行时,但在 Windows 上不需要:

    "debuggers": [{
        "type": "mono",
        "program": "./bin/monoDebug.exe",
        "osx": {
            "runtime": "mono"
        },
        "linux": {
            "runtime": "mono"
        }
    }]
    

**configurationAttributes** 声明了可用于此调试器的 `launch.json` 属性的模式。此模式用于验证 `launch.json` 并在编辑启动配置时支持 IntelliSense 和悬停帮助。

**initialConfigurations** 定义了此调试器默认 `launch.json` 的初始内容。当项目没有 `launch.json` 且用户启动调试会话或在“运行和调试”视图中选择“**创建 launch.json 文件**”链接时,将使用此信息。在这种情况下,VS Code 会让用户选择一个调试环境,然后创建相应的 `launch.json`。

Debugger Quickpick

除了在 `package.json` 中静态定义 `launch.json` 的初始内容之外,还可以通过实现 `DebugConfigurationProvider` 动态计算初始配置(有关详细信息,请参阅下文使用 DebugConfigurationProvider 部分)。

**configurationSnippets** 定义了在编辑 `launch.json` 时 IntelliSense 中显示的启动配置片段。作为惯例,请在片段的 `label` 属性前加上调试环境名称,以便在许多片段建议列表中显示时可以清楚地识别它。

**variables** 贡献将“变量”绑定到“命令”。这些变量可以使用 **${command:xyz}** 语法在启动配置中使用,当调试会话启动时,这些变量将被绑定命令返回的值替换。

命令的实现位于扩展中,它可以从没有 UI 的简单表达式到基于扩展 API 中可用的 UI 功能的复杂功能。Mock Debug 将变量 `AskForProgramName` 绑定到命令 `extension.mock-debug.getProgramName`。此命令在 `src/extension.ts` 中的实现使用 `showInputBox` 让用户输入程序名称。

vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => {
  return vscode.window.showInputBox({
    placeHolder: 'Please enter the name of a markdown file in the workspace folder',
    value: 'readme.md'
  });
});

该变量现在可以在启动配置的任何字符串类型值中用作 **${command:AskForProgramName}**。

使用 DebugConfigurationProvider

如果 `package.json` 中调试贡献的静态性质不足以满足需求,可以使用 `DebugConfigurationProvider` 动态控制调试扩展的以下方面:

  • 可以动态生成新创建的 launch.json 的初始调试配置,例如,基于工作区中可用的某些上下文信息。
  • 在用于启动新的调试会话之前,可以**解析**(或修改)启动配置。这允许根据工作区中可用的信息填充默认值。存在两种**解析**方法:`resolveDebugConfiguration` 在变量替换之前调用,`resolveDebugConfigurationWithSubstitutedVariables` 在所有变量替换之后调用。如果验证逻辑在调试配置中插入了额外的变量,则必须使用前者。如果验证逻辑需要访问所有调试配置属性的最终值,则必须使用后者。

`src/extension.ts` 中的 `MockConfigurationProvider` 实现了 `resolveDebugConfiguration`,以检测在没有 launch.json 的情况下启动调试会话但活动编辑器中打开 Markdown 文件的情况。这是一种典型场景,用户在编辑器中打开一个文件,只想调试它而无需创建 launch.json。

调试配置提供程序通过 `vscode.debug.registerDebugConfigurationProvider` 注册到特定的调试类型,通常在扩展的 `activate` 函数中。为确保 `DebugConfigurationProvider` 尽早注册,一旦使用调试功能,扩展就必须激活。这可以通过在 `package.json` 中为 `onDebug` 事件配置扩展激活来轻松实现。

"activationEvents": [
    "onDebug",
    // ...
],

这个包罗万象的 `onDebug` 在使用任何调试功能时都会触发。只要扩展启动成本低(即在启动序列中不花费大量时间),这就能很好地工作。如果调试扩展启动成本高(例如,因为启动了语言服务器),`onDebug` 激活事件可能会对其他调试扩展产生负面影响,因为它触发得相当早,并且不考虑特定的调试类型。

对于昂贵的调试扩展,更好的方法是使用更细粒度的激活事件:

  • `onDebugInitialConfigurations` 在调用 `DebugConfigurationProvider` 的 `provideDebugConfigurations` 方法之前触发。
  • `onDebugResolve:type` 在调用指定类型的 `DebugConfigurationProvider` 的 `resolveDebugConfiguration` 或 `resolveDebugConfigurationWithSubstitutedVariables` 方法之前触发。

**经验法则:** 如果调试扩展的激活成本低,请使用 `onDebug`。如果成本高,则根据 `DebugConfigurationProvider` 是否实现相应的 `provideDebugConfigurations` 和/或 `resolveDebugConfiguration` 方法,使用 `onDebugInitialConfigurations` 和/或 `onDebugResolve`。

发布您的调试器扩展

创建调试器扩展后,您可以将其发布到市场:

  • 更新 `package.json` 中的属性以反映调试器扩展的命名和用途。
  • 按照发布扩展中的说明上传到市场。

开发调试器扩展的替代方法

如我们所见,开发调试器扩展通常涉及在两个并行会话中调试扩展和调试适配器。如上所述,VS Code 很好地支持了这一点,但如果扩展和调试适配器是一个程序,并且可以在一个调试会话中进行调试,那么开发可能会更容易。

如果您的调试适配器是用 TypeScript/JavaScript 实现的,这种方法实际上很容易实现。基本思想是在扩展内部直接运行调试适配器,并让 VS Code 连接到它,而不是为每个会话启动一个新的外部调试适配器。

为此,VS Code 提供了扩展 API 来控制调试适配器的创建和运行方式。`DebugAdapterDescriptorFactory` 有一个 `createDebugAdapterDescriptor` 方法,该方法在调试会话启动并且需要调试适配器时由 VS Code 调用。此方法必须返回一个描述调试适配器如何运行的描述符对象(`DebugAdapterDescriptor`)。

目前,VS Code 支持三种不同的运行调试适配器的方式,因此提供三种不同的描述符类型:

  • DebugAdapterExecutable:此对象将调试适配器描述为具有路径、可选参数和运行时的外部可执行文件。可执行文件必须实现调试适配器协议并通过 stdin/stdout 进行通信。这是 VS Code 的默认操作模式,如果没有明确注册 `DebugAdapterDescriptorFactory`,VS Code 会自动使用 package.json 中的相应值来使用此描述符。
  • DebugAdapterServer:此对象描述了一个作为服务器运行的调试适配器,通过特定的本地或远程端口进行通信。基于 `vscode-debugadapter` npm 模块的调试适配器实现会自动支持此服务器模式。
  • DebugAdapterInlineImplementation:此对象将调试适配器描述为实现 `vscode.DebugAdapter` 接口的 JavaScript 或 TypeScript 对象。基于 `vscode-debugadapter` npm 模块的 1.38-pre.4 或更高版本的调试适配器实现会自动实现该接口。

Mock Debug 展示了三种 DebugAdapterDescriptorFactory 类型的示例,以及它们如何为“mock”调试类型注册。可以通过将全局变量 `runMode` 设置为 `external`、`server` 或 `inline` 中的一个来选择要使用的运行模式。

对于开发,`inline` 和 `server` 模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。