Debugger Extension
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, theNode.js debugger extension, which is an excellent showcase for the many debugger features supported by VS Code:
This screenshot shows the following debugging features:
- Debug configuration management.
- Debug actions for starting/stopping and stepping.
- Source-, function-, conditional-, inline breakpoints, and log points.
- Stack traces, including multi-thread and multi-process support.
- Navigating through complex data structures in views and hovers.
- Variable values shown in hovers or inlined in the source.
- Managing watch expressions.
- 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.
We call this intermediary theDebug Adapter (orDA for short) and the abstract protocol that is used between the DA and VS Code is theDebug Adapter Protocol (DAP for short).Since the Debug Adapter Protocol is independent from VS Code, it has its ownweb site where you can find anintroduction and overview, the detailedspecification, and some lists withknown implementations and supporting tools.The history of and motivation behind DAP is explained in thisblog post.
Since debug adapters are independent from VS Code and can be used inother 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.
A more realistic debugger extension contributes many or all of the following declarative items to VS Code:
- List of languages supported by the debugger. VS Code enables the UI to set breakpoints for those languages.
- JSON schema for the debug configuration attributes introduced by the debugger. VS Code uses this schema to verify the configuration in the launch.json editor and provides IntelliSense. Please note that the JSON schema constructs
$ref
anddefinition
are not supported. - Default debug configurations for the initial launch.json created by VS Code.
- Debug configuration snippets that a user can add to a launch.json file.
- Declaration of variables that can be used in debug configurations.
You can find more information incontributes.breakpoints
andcontributes.debuggers
references.
In addition to the purely declarative contributions from above, the Debug Extension API enables this code-based functionality:
- Dynamically generated default debug configurations for the initial launch.json created by VS Code.
- Determine the debug adapter to use dynamically.
- Verify or modify debug configurations before they are passed to the debug adapter.
- Communicate with the debug adapter.
- Send messages to the debug console.
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 calledMock 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 apre-built versionfrom the VS Code Marketplace and play with it:
- Switch to the Extensions viewlet and type "mock" to search for the Mock Debug extension,
- "Install" and "Reload" the extension.
To try Mock Debug:
- Create a new empty folder
mock test
and open it in VS Code. - Create a file
readme.md
and enter several lines of arbitrary text. - Switch to the Run and Debug view (⇧⌘D (Windows, LinuxCtrl+Shift+D)) and select thecreate a launch.json file link.
- VS Code will let you select an "debugger" in order to create a default launch configuration. Pick "Mock Debug".
- Press the greenStart button and thenEnter to confirm the suggested file
readme.md
.
A debug session starts and you can "step" through the readme file, set and hit breakpoints, and run into exceptions (if the wordexception
appears in a line).
Before using Mock Debug as a starting point for your own development, we recommend to uninstall the pre-built version first:
- Switch to the Extensions viewlet and click on the gear icon of the Mock Debug extension.
- Run the "Uninstall" action and then "Reload" the window.
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.gitcd vscode-mock-debugyarn
Open the project foldervscode-mock-debug
in VS Code.
What's in the package?
package.json
is the manifest for the mock-debug extension:- It lists the contributions of the mock-debug extension.
- The
compile
andwatch
scripts are used to transpile the TypeScript source into theout
folder and watch for subsequent source modifications. - The dependencies
vscode-debugprotocol
,vscode-debugadapter
, andvscode-debugadapter-testsupport
are NPM modules that simplify the development of node-based debug adapters.
src/mockRuntime.ts
is amock runtime with a simple debug API.- The code thatadapts the runtime to the Debug Adapter Protocol lives in
src/mockDebug.ts
. Here you find the handlers for the various requests of the DAP. - Since the implementation of debugger extension lives in the debug adapter, there is no need to have extension code at all (i.e. code that runs in the extension host process). However, Mock Debug has a small
src/extension.ts
because it illustrates what can be done in the extension code of a debugger extension.
Now build and launch the Mock Debug extension by selecting theExtension launch configuration and hittingF5
.Initially, this will do a full transpile of the TypeScript sources into theout
folder.After the full build, awatcher 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 yourmock test
project with thereadme.md
file, start a debug session with 'F5', and then step through it:
Since you are running the extension in debug mode, you could now set and hit breakpoints insrc/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 inserver mode and configure VS Code to connect to it. In your VS Code vscode-mock-debug project select the launch configurationServer 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 entersmulti session mode which is indicated by seeing the names of the two debug sessionsExtension andServer showing up in the CALL STACK view:
Now we are able to debug both the extension and the DA simultaneously.A faster way to arrive here is by using theExtension + Server launch configuration which launches both sessions automatically.
An alternative, even simpler approach for debugging the extension and the DA can be foundbelow.
Set a breakpoint at the beginning of methodlaunchRequest(...)
in filesrc/mockDebug.ts
and as a last step configure the mock debugger to connect to the DA server by adding adebugServer
attribute for port4711
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 inlaunchRequest
.
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 insrc/mockDebug.ts
andsrc/mockRuntime.ts
by some code that talks to a "real" debugger or runtime. This involves understanding and implementing the Debug Adapter Protocol. More detailsabout this can be foundhere.
Anatomy of the package.json of a Debugger Extension
Besides providing a debugger-specific implementation of the debug adapter a debugger extension needs apackage.json
that contributes to the various debug-related contribution points.
So let's have a closer look at thepackage.json
of Mock Debug.
Like every VS Code extension, thepackage.json
declares the fundamental propertiesname,publisher, andversion of the extension. Use thecategories 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 thecontributes section which contains the contributions specific to debug extensions.
First, we use thebreakpoints 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 thedebuggers section. Here, one debugger is introduced under a debugtypemock
. The user can reference this type in launch configurations. The optional attributelabel 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 theprogram 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 namedout
orbin
, 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:
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 theruntime attribute. As of today, VS Code supports
node
andmono
runtimes. Our Mock debug adapter from above uses this approach.If your DA implementation needs different executables on different platforms, theprogram 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", }}]
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 thelaunch.json
attributes that are available for this debugger. This schema is used for validating thelaunch.json
and supporting IntelliSense and hover help when editing the launch configuration.
TheinitialConfigurations define the initial content of the defaultlaunch.json
for this debugger. This information is used when a project does not have alaunch.json
and a user starts a debug session or selects thecreate 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 correspondinglaunch.json
:
Instead of defining the initial content of thelaunch.json
statically in thepackage.json
, it is possible to compute the initial configurations dynamically by implementing aDebugConfigurationProvider
(for details see the sectionUsing a DebugConfigurationProvider below).
configurationSnippets define launch configuration snippets that get surfaced in IntelliSense when editing thelaunch.json
. As a convention, prefix thelabel
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.
Thevariables 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 variableAskForProgramName
to the commandextension.mock-debug.getProgramName
. Theimplementation of this command insrc/extension.ts
uses theshowInputBox
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 thepackage.json
is not sufficient, aDebugConfigurationProvider
can be used to dynamically control the following aspects of a debug extension:
- The initial debug configurations for a newly created launch.json can be generated dynamically, e.g. based on some contextual information available in the workspace.
- A launch configuration can beresolved (or modified) before it is used to start a new debug session. This allows for filling in default values based on information available in the workspace. Tworesolve methods exist:
resolveDebugConfiguration
is called before variables are substituted in the launch configuration,resolveDebugConfigurationWithSubstitutedVariables
is called after all variables have been substituted. The former must be used if the validation logic inserts additional variables into the debug configuration. The latter must be used if the validation logic needs access to the final values of all debug configuration attributes.
TheMockConfigurationProvider
insrc/extension.ts
implementsresolveDebugConfiguration
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 viavscode.debug.registerDebugConfigurationProvider
, typically in the extension'sactivate
function.To ensure that theDebugConfigurationProvider
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 theonDebug
event in thepackage.json
:
"activationEvents": [ "onDebug", // ...],
This catch-allonDebug
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), theonDebug
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:
onDebugInitialConfigurations
is fired just before theprovideDebugConfigurations
method of theDebugConfigurationProvider
is called.onDebugResolve:type
is fired just before theresolveDebugConfiguration
orresolveDebugConfigurationWithSubstitutedVariables
methods of theDebugConfigurationProvider
for the specified type is called.
Rule of thumb: If activation of a debug extensions is cheap, useonDebug
. If it is expensive, useonDebugInitialConfigurations
and/oronDebugResolve
depending on whether theDebugConfigurationProvider
implements the corresponding methodsprovideDebugConfigurations
and/orresolveDebugConfiguration
.
Publishing your debugger extension
Once you have created your debugger extension you can publish it to the Marketplace:
- Update the attributes in the
package.json
to reflect the naming and purpose of your debugger extension. - Upload to the Marketplace as described inPublishing Extension.
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. ADebugAdapterDescriptorFactory
has a methodcreateDebugAdapterDescriptor
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:
DebugAdapterExecutable
: this object describes a debug adapter as an external executable with a path and optional arguments and runtime. The executable must implement the Debug Adapter Protocol and communicate via stdin/stdout. This is VS Code's default mode of operation and VS Code uses this descriptor automatically with the corresponding values from the package.json if noDebugAdapterDescriptorFactory
is explicitly registered.DebugAdapterServer
: this object describes a debug adapter running as a server that communicates via a specific local or remote port. A debug adapter implementation based on thevscode-debugadapter
npm module supports this server mode automatically.DebugAdapterInlineImplementation
: this object describes a debug adapter as a JavaScript or Typescript object that implements thevscode.DebugAdapter
interface. A debug adapter implementation based on version 1.38-pre.4 or later of thevscode-debugadapter
npm module implements the interface automatically.
Mock Debug shows examples for thethree types of DebugAdapterDescriptorFactories and how they areregistered for the 'mock' debug type. The run mode to use can be selected bysetting the global variablerunMode
to one of the possible valuesexternal
,server
, orinline
.
For development, theinline
andserver
modes are particularly useful because they allow for debugging extension and debug adapter within a single process.