Debugger Extension (original) (raw)

In this article

Visual Studio Code's debugging architecture allows extension authors to easily integrate existing debuggers into VS Code, while having a common user interface with all of them.

VS Code ships with one built-in debugger extension, the Node.js debugger extension, which is an excellent showcase for the many debugger features supported by VS Code:

VS Code Debug Features

This screenshot shows the following debugging features:

  1. Debug configuration management.
  2. Debug actions for starting/stopping and stepping.
  3. Source-, function-, conditional-, inline breakpoints, and log points.
  4. Stack traces, including multi-thread and multi-process support.
  5. Navigating through complex data structures in views and hovers.
  6. Variable values shown in hovers or inlined in the source.
  7. Managing watch expressions.
  8. Debug console for interactive evaluation with autocomplete.

This documentation will help you create a debugger extension which can make any debugger work with VS Code.

Debugging Architecture of VS Code

VS Code implements a generic (language-agnostic) debugger UI based on an abstract protocol that we've introduced to communicate with debugger backends. Because debuggers typically do not implement this protocol, some intermediary is needed to "adapt" the debugger to the protocol. This intermediary is typically a standalone process that communicates with the debugger.

VS Code Debug Architecture

We call this intermediary the Debug Adapter (or DA for short) and the abstract protocol that is used between the DA and VS Code is the Debug Adapter Protocol (DAP for short). Since the Debug Adapter Protocol is independent from VS Code, it has its own web site where you can find an introduction and overview, the detailed specification, and some lists with known implementations and supporting tools. The history of and motivation behind DAP is explained in this blog post.

Since debug adapters are independent from VS Code and can be used in other developments tools, they do not match VS Code's extensibility architecture which is based on extensions and contribution points.

For this reason VS Code provides a contribution point, debuggers, where a debug adapter can be contributed under a specific debug type (e.g. node for the Node.js debugger). VS Code launches the registered DA whenever the user starts a debug session of that type.

So in its most minimal form, a debugger extension is just a declarative contribution of a debug adapter implementation and the extension is basically a packaging container for the debug adapter without any additional code.

VS Code Debug Architecture 2

A more realistic debugger extension contributes many or all of the following declarative items to VS Code:

You can find more information in contributes.breakpoints and contributes.debuggers references.

In addition to the purely declarative contributions from above, the Debug Extension API enables this code-based functionality:

In the rest of this document we show how to develop a debugger extension.

The Mock Debug Extension

Since creating a debug adapter from scratch is a bit heavy for this tutorial, we will start with a simple DA which we have created as an educational "debug adapter starter kit". It is called Mock Debug because it does not talk to a real debugger, but mocks one. Mock Debug simulates a debugger and supports step, continue, breakpoints, exceptions, and variable access, but it is not connected to any real debugger.

Before delving into the development setup for mock-debug, let's first install a pre-built versionfrom the VS Code Marketplace and play with it:

To try Mock Debug:

A debug session starts and you can "step" through the readme file, set and hit breakpoints, and run into exceptions (if the word exception appears in a line).

Mock Debugger running

Before using Mock Debug as a starting point for your own development, we recommend to uninstall the pre-built version first:

Development Setup for Mock Debug

Now let's get the source for Mock Debug and start development on it within VS Code:

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

Open the project folder vscode-mock-debug in VS Code.

What's in the package?

Now build and launch the Mock Debug extension by selecting the Extension launch configuration and hitting F5. Initially, this will do a full transpile of the TypeScript sources into the out folder. After the full build, a watcher task is started that transpiles any changes you make.

After transpiling the source, a new VS Code window labelled "[Extension Development Host]" appears with the Mock Debug extension now running in debug mode. From that window open your mock test project with the readme.md file, start a debug session with 'F5', and then step through it:

Debugging Extension and Server

Since you are running the extension in debug mode, you could now set and hit breakpoints in src/extension.ts but as I've mentioned above, there is not much interesting code executing in the extension. The interesting code runs in the debug adapter which is a separate process.

In order to debug the debug adapter itself, we have to run it in debug mode. This is most easily achieved by running the debug adapter in server mode and configure VS Code to connect to it. In your VS Code vscode-mock-debug project select the launch configuration Server from the dropdown menu and press the green start button.

Since we already had an active debug session for the extension the VS Code debugger UI now enters multi session mode which is indicated by seeing the names of the two debug sessions Extension and Server showing up in the CALL STACK view:

Debugging Extension and Server

Now we are able to debug both the extension and the DA simultaneously. A faster way to arrive here is by using the Extension + Server launch configuration which launches both sessions automatically.

An alternative, even simpler approach for debugging the extension and the DA can be found below.

Set a breakpoint at the beginning of method launchRequest(...) in file src/mockDebug.ts and as a last step configure the mock debugger to connect to the DA server by adding a debugServer attribute for port 4711 to your mock test launch config:

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

If you now launch this debug configuration, VS Code does not start the mock debug adapter as a separate process, but directly connects to local port 4711 of the already running server, and you should hit the breakpoint in launchRequest.

With this setup, you can now easily edit, transpile, and debug Mock Debug.

But now the real work begins: you will have to replace the mock implementation of the debug adapter in src/mockDebug.ts and src/mockRuntime.ts by some code that talks to a "real" debugger or runtime. This involves understanding and implementing the Debug Adapter Protocol. More details about this can be found here.

Anatomy of the package.json of a Debugger Extension

Besides providing a debugger-specific implementation of the debug adapter a debugger extension needs a package.json that contributes to the various debug-related contribution points.

So let's have a closer look at the package.json of Mock Debug.

Like every VS Code extension, the package.json declares the fundamental properties name, publisher, and version of the extension. Use the categories field to make the extension easier to find in the VS Code Extension Marketplace.

{
  "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"]
}

Now take a look at the contributes section which contains the contributions specific to debug extensions.

First, we use the breakpoints contribution point to list the languages for which setting breakpoints will be enabled. Without this, it would not be possible to set breakpoints in Markdown files.

Next is the debuggers section. Here, one debugger is introduced under a debug type mock. The user can reference this type in launch configurations. The optional attribute label can be used to give the debug type a nice name when showing it in the UI.

Since the debug extension uses a debug adapter, a relative path to its code is given as the program attribute. In order to make the extension self-contained the application must live inside the extension folder. By convention, we keep this application inside a folder named out or bin, but you are free to use a different name.

Since VS Code runs on different platforms, we have to make sure that the DA program supports the different platforms as well. For this we have the following options:

  1. If the program is implemented in a platform independent way, e.g. as program that runs on a runtime that is available on all supported platforms, you can specify this runtime via the runtime attribute. As of today, VS Code supports node and mono runtimes. Our Mock debug adapter from above uses this approach.
  2. If your DA implementation needs different executables on different platforms, the program attribute can be qualified for specific platforms like this:
"debuggers": [{  
    "type": "gdb",  
    "windows": {  
        "program": "./bin/gdbDebug.exe",  
    },  
    "osx": {  
        "program": "./bin/gdbDebug.sh",  
    },  
    "linux": {  
        "program": "./bin/gdbDebug.sh",  
    }  
}]  
  1. A combination of both approaches is possible too. The following example is from the Mono DA which is implemented as a mono application that needs a runtime on macOS and Linux but not on Windows:
"debuggers": [{  
    "type": "mono",  
    "program": "./bin/monoDebug.exe",  
    "osx": {  
        "runtime": "mono"  
    },  
    "linux": {  
        "runtime": "mono"  
    }  
}]  

configurationAttributes declares the schema for the launch.json attributes that are available for this debugger. This schema is used for validating the launch.json and supporting IntelliSense and hover help when editing the launch configuration.

The initialConfigurations define the initial content of the default launch.json for this debugger. This information is used when a project does not have a launch.json and a user starts a debug session or selects the create a launch.json file link in the Run and Debug view. In this case VS Code lets the user pick a debug environment and then creates the corresponding launch.json:

Debugger Quickpick

Instead of defining the initial content of the launch.json statically in the package.json, it is possible to compute the initial configurations dynamically by implementing a DebugConfigurationProvider (for details see the section Using a DebugConfigurationProvider below).

configurationSnippets define launch configuration snippets that get surfaced in IntelliSense when editing the launch.json. As a convention, prefix the label attribute of a snippet by the debug environment name so that it can be clearly identified when presented in a list of many snippet proposals.

The variables contribution binds "variables" to "commands". These variables can be used in the launch configuration using the ${command:xyz} syntax and the variables are substituted by the value returned from the bound command when a debug session is started.

The implementation of a command lives in the extension and it can range from a simple expression with no UI, to sophisticated functionality based on the UI features available in the extension API. Mock Debug binds a variable AskForProgramName to the command extension.mock-debug.getProgramName. The implementation of this command in src/extension.ts uses the showInputBox to let the user enter a program name:

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'
  });
});

The variable can now be used in any string typed value of a launch configuration as ${command:AskForProgramName}.

Using a DebugConfigurationProvider

If the static nature of debug contributions in the package.json is not sufficient, a DebugConfigurationProvider can be used to dynamically control the following aspects of a debug extension:

The MockConfigurationProvider in src/extension.ts implements resolveDebugConfiguration to detect the case where a debug session is started when no launch.json exists, but a Markdown file is open in the active editor. This is a typical scenario where the user has a file open in the editor and just wants to debug it without creating a launch.json.

A debug configuration provider is registered for a specific debug type via vscode.debug.registerDebugConfigurationProvider, typically in the extension's activate function. To ensure that the DebugConfigurationProvider is registered early enough, the extension must be activated as soon as the debug functionality is used. This can be easily achieved by configuring extension activation for the onDebug event in the package.json:

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

This catch-all onDebug is triggered as soon as any debug functionality is used. This works fine as long as the extension has cheap startup costs (i.e. does not spend a lot of time in its startup sequence). If a debug extension has an expensive startup (for instance because of starting a language server), the onDebug activation event could negatively affect other debug extensions, because it is triggered rather early and does not take a specific debug type into account.

A better approach for expensive debug extensions is to use more fine-grained activation events:

Rule of thumb: If activation of a debug extensions is cheap, use onDebug. If it is expensive, use onDebugInitialConfigurations and/or onDebugResolve depending on whether the DebugConfigurationProvider implements the corresponding methods provideDebugConfigurations and/or resolveDebugConfiguration.

Publishing your debugger extension

Once you have created your debugger extension you can publish it to the Marketplace:

Alternative approach to develop a debugger extension

As we have seen, developing a debugger extension typically involves debugging both the extension and the debug adapter in two parallel sessions. As explained above VS Code supports this nicely but development could be easier if both the extension and the debug adapter would be one program that could be debugged in one debug session.

This approach is in fact easily doable as long as your debug adapter is implemented in TypeScript/JavaScript. The basic idea is to run the debug adapter directly inside the extension and to make VS Code to connect to it instead of launching a new external debug adapter per session.

For this VS Code provides extension API to control how a debug adapter is created and run. A DebugAdapterDescriptorFactory has a method createDebugAdapterDescriptor that is called by VS Code when a debug session starts and a debug adapter is needed. This method must return a descriptor object (DebugAdapterDescriptor) that describes how the debug adapter is run.

Today VS Code supports three different ways for running a debug adapter and consequently offers three different descriptor types:

Mock Debug shows examples for the three types of DebugAdapterDescriptorFactories and how they are registered for the 'mock' debug type. The run mode to use can be selected by setting the global variable runMode to one of the possible values external, server, or inline.

For development, the inline and server modes are particularly useful because they allow for debugging extension and debug adapter within a single process.

05/08/2025