Creating a Custom VS Code Extension for Debugging

Why Create Your Own Debugging Extension?
As developers, we spend a significant portion of our time debugging code. Visual Studio Code offers a robust debugging system out of the box, but there are always specific pain points in our workflow that could use improvement. Maybe you're tired of setting the same breakpoints repeatedly, or perhaps you wish you could visualize certain data structures more effectively during debugging sessions.
I recently faced this challenge while working on a complex React application. The standard debugging tools were helpful, but I found myself repeating the same debugging patterns over and over. That's when I decided to build a custom extension to streamline my workflow. In this article, I'll walk you through the process of creating your own debugging extension for VS Code, from initial setup to publishing.
Setting Up Your Development Environment
Before diving into code, let's make sure we have everything we need to develop a VS Code extension:
- Node.js and npm: VS Code extensions are built with TypeScript/JavaScript, so you'll need Node.js installed (preferably version 14 or later).
- Yeoman and VS Code Extension Generator: These tools simplify the extension creation process.
- Visual Studio Code: You'll need this not just for development but also for testing your extension.
Let's start by installing the required tools if you don't have them already:
# Install Yeoman and the VS Code Extension Generator globally
npm install -g yo generator-code
Scaffolding Your Extension
With the tools installed, we can generate the scaffolding for our extension. Open your terminal and run:
# Create a new extension
yo code
You'll be prompted to provide some information about your extension. For our debugging extension, I recommend the following options:
- Select "New Extension (TypeScript)"
- Name your extension something descriptive like "custom-debug-helper"
- Provide a brief description, like "Enhance VS Code's debugging experience with custom visualizations and tools"
- Choose "Yes" when asked if you want to initialize a git repository
This will generate a basic extension structure with all the necessary files. Navigate to your project folder:
cd custom-debug-helper
code .
Understanding the Extension Structure
Before we add our debugging functionality, let's understand what files were created and their purpose:
package.json
: Contains metadata about your extension and defines contributions (commands, views, etc.)src/extension.ts
: The main entry point for your extension's code.vscode/launch.json
: Configuration for debugging your extensiontsconfig.json
: TypeScript configuration
The heart of our extension will be in src/extension.ts
, which already contains a sample command implementation. We'll modify this file to add our debugging features.
Adding Debugging Functionality
VS Code's extension API provides several ways to interact with the debugging system. For our extension, we'll focus on:
- Creating custom debug configurations
- Adding specialized breakpoint handling
- Providing visualizations for debugging data
Let's start by updating our package.json
to declare the debugging contributions:
"contributes": {
"commands": [
{
"command": "custom-debug-helper.startCustomDebugging",
"title": "Start Custom Debugging Session"
},
{
"command": "custom-debug-helper.visualizeObject",
"title": "Visualize Object Structure"
}
],
"breakpoints": [
{
"language": "javascript"
},
{
"language": "typescript"
}
],
"debuggers": [
{
"type": "custom-debugger",
"label": "Custom JavaScript Debugger",
"program": "./out/debuggerRuntime.js",
"runtime": "node",
"configurationAttributes": {
"launch": {
"required": ["program"],
"properties": {
"program": {
"type": "string",
"description": "The program to debug"
},
"enableCustomVisualizations": {
"type": "boolean",
"description": "Enable custom data visualizations",
"default": true
}
}
}
},
"initialConfigurations": [
{
"type": "custom-debugger",
"request": "launch",
"name": "Launch with Custom Debugger",
"program": "${file}",
"enableCustomVisualizations": true
}
]
}
]
}
Implementing the Core Debugging Logic
Now, let's implement the core debugging functionality in our extension. Create a new file called src/debuggerRuntime.ts
that will handle our custom debugger:
import * as vscode from 'vscode';
import { EventEmitter } from 'events';
/**
* A custom debugger runtime that enhances VS Code's debugging experience
*/
export class CustomDebuggerRuntime extends EventEmitter {
private _variableHandlers: Map string> = new Map();
private _breakpointManager: BreakpointManager;
constructor() {
super();
this._breakpointManager = new BreakpointManager();
// Register default variable visualizers
this.registerVariableHandler('Array', this._visualizeArray);
this.registerVariableHandler('Object', this._visualizeObject);
}
/**
* Start debugging the given program
*/
public start(program: string, enableVisualizations: boolean): void {
// Implementation details for starting the debugger
// This would typically involve launching the program under a debugging protocol
console.log(`Starting debug session for ${program} with visualizations ${enableVisualizations ? 'enabled' : 'disabled'}`);
// For a real implementation, you'd connect to the Debug Adapter Protocol here
}
/**
* Register a custom handler for visualizing variables of a specific type
*/
public registerVariableHandler(typeName: string, handler: (value: any) => string): void {
this._variableHandlers.set(typeName, handler);
}
/**
* Visualize an array with custom formatting
*/
private _visualizeArray(array: any[]): string {
// Example implementation for custom array visualization
if (!Array.isArray(array)) {
return 'Not an array';
}
return `Array(${array.length}) ${array.length > 0 ? '[...]' : '[]'}`;
}
/**
* Visualize an object with custom formatting
*/
private _visualizeObject(obj: any): string {
// Example implementation for custom object visualization
if (typeof obj !== 'object' || obj === null) {
return 'Not an object';
}
const keys = Object.keys(obj);
return `Object {${keys.join(', ')}}`;
}
}
/**
* Manages custom breakpoints
*/
class BreakpointManager {
private _breakpoints: Map = new Map();
// Implementation of breakpoint management logic
}
Next, we need to update our main extension file (src/extension.ts
) to integrate our custom debugger:
import * as vscode from 'vscode';
import { CustomDebuggerRuntime } from './debuggerRuntime';
export function activate(context: vscode.ExtensionContext) {
console.log('Custom Debug Helper extension is active');
// Register a command to start custom debugging
let startDebuggingCmd = vscode.commands.registerCommand('custom-debug-helper.startCustomDebugging', async () => {
// Get the active text editor
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
vscode.window.showErrorMessage('No active editor found');
return;
}
// Get the current file path
const filePath = activeEditor.document.uri.fsPath;
// Start a debugging session
const debugConfiguration = {
type: 'custom-debugger',
request: 'launch',
name: 'Debug Current File',
program: filePath,
enableCustomVisualizations: true
};
// Start debugging
vscode.debug.startDebugging(undefined, debugConfiguration);
});
// Register a command to visualize objects during debugging
let visualizeObjectCmd = vscode.commands.registerCommand('custom-debug-helper.visualizeObject', async () => {
// Get the current debug session
const session = vscode.debug.activeDebugSession;
if (!session) {
vscode.window.showErrorMessage('No active debug session');
return;
}
// Implement visualization logic for the selected variable
// This could open a custom webview with an interactive visualization
vscode.window.showInformationMessage('Visualizing object...');
// In a real extension, you would get the selected variable from the debug session
// and create a visualization for it
});
context.subscriptions.push(startDebuggingCmd, visualizeObjectCmd);
}
export function deactivate() {
// Clean up resources
}
Creating a Custom Debug Interface
To provide a better user experience, let's create a custom UI for our debugger using VS Code's webview API. This will allow us to show visualizations that aren't possible in the standard debug views.
First, let's create a new file src/visualizerView.ts
:
import * as vscode from 'vscode';
export class VisualizerPanel {
public static currentPanel: VisualizerPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
// Set the webview's initial html content
this._update();
// Listen for when the panel is disposed
// This happens when the user closes the panel or when the panel is closed programmatically
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
// Update the content based on view changes
this._panel.onDidChangeViewState(
e => {
if (this._panel.visible) {
this._update();
}
},
null,
this._disposables
);
}
public static createOrShow(extensionUri: vscode.Uri) {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
// If we already have a panel, show it
if (VisualizerPanel.currentPanel) {
VisualizerPanel.currentPanel._panel.reveal(column);
return;
}
// Otherwise, create a new panel
const panel = vscode.window.createWebviewPanel(
'debugVisualizer',
'Debug Visualizer',
column || vscode.ViewColumn.One,
{
// Enable JavaScript in the webview
enableScripts: true,
// Restrict the webview to only load resources from the extension
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')]
}
);
VisualizerPanel.currentPanel = new VisualizerPanel(panel, extensionUri);
}
public dispose() {
VisualizerPanel.currentPanel = undefined;
// Clean up our resources
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
if (disposable) {
disposable.dispose();
}
}
}
/**
* Renders visualization content in the webview
*/
public visualizeData(data: any) {
// Send the data to the webview
this._panel.webview.postMessage({
type: 'update',
data: data
});
}
private _update() {
const webview = this._panel.webview;
// Set the HTML content
webview.html = this._getHtmlForWebview(webview);
}
private _getHtmlForWebview(webview: vscode.Webview) {
// Return HTML content for the webview
return `
Debug Visualizer
No data to visualize yet. Select a variable in the debug view and click "Visualize Object".
`;
}
}
Integrating with VS Code's Debug Adapter Protocol
To make our extension truly useful, we need to integrate with VS Code's Debug Adapter Protocol (DAP). This is a more advanced topic, but here's a simplified implementation to get you started:
// src/debugAdapter.ts
import {
Logger, logger,
LoggingDebugSession,
InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent,
Thread, StackFrame, Scope, Source, Handles
} from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
import { CustomDebuggerRuntime } from './debuggerRuntime';
import * as path from 'path';
export class CustomDebugAdapter extends LoggingDebugSession {
private _runtime: CustomDebuggerRuntime;
private _variableHandles = new Handles();
public constructor() {
super("custom-debugger-log.txt");
this._runtime = new CustomDebuggerRuntime();
// Setup event handlers
this._runtime.on('stopOnEntry', () => {
this.sendEvent(new StoppedEvent('entry', 1));
});
this._runtime.on('stopOnStep', () => {
this.sendEvent(new StoppedEvent('step', 1));
});
this._runtime.on('stopOnBreakpoint', () => {
this.sendEvent(new StoppedEvent('breakpoint', 1));
});
this._runtime.on('output', (output: string) => {
this.sendEvent(new OutputEvent(`${output}\n`));
});
this._runtime.on('end', () => {
this.sendEvent(new TerminatedEvent());
});
}
// Implement the Debug Adapter Protocol methods here
// This is a simplified version - a real implementation would be more complex
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments) {
response.body = response.body || {};
// Capabilities of this debug adapter
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsEvaluateForHovers = true;
response.body.supportsStepBack = false;
response.body.supportsSetVariable = true;
response.body.supportsFunctionBreakpoints = true;
this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}
protected launchRequest(response: DebugProtocol.LaunchResponse, args: any) {
// Start the program
this._runtime.start(args.program, args.enableCustomVisualizations);
this.sendResponse(response);
}
// Add more protocol implementations...
}
Testing Your Extension
Before packaging your extension for distribution, it's essential to test it thoroughly. VS Code makes this easy with its built-in extension debugging capabilities:
- Press F5 in your extension project to launch a new VS Code window with your extension loaded
- In the new window, open a JavaScript or TypeScript file
- Try out your custom debugging commands from the Command Palette (Ctrl+Shift+P)
- Set breakpoints and test your visualizations
During testing, keep an eye on the Debug Console in your extension development window for any error messages or logs that might help you identify issues.
Packaging and Publishing Your Extension
Once you're satisfied with your extension, you can package it for distribution or publish it to the VS Code Marketplace:
# Install vsce if you haven't already
npm install -g vsce
# Package your extension into a .vsix file
vsce package
# Publish to the marketplace (requires an Azure DevOps account)
vsce publish
Publishing to the VS Code Marketplace makes your extension available to millions of developers worldwide. Be sure to include:
- A detailed README with usage instructions
- Screenshots or GIFs demonstrating your extension in action
- Links to the source code repository
- A clear description of what your extension does and who it's for
Real-World Example: Custom React Component Debugger
To make this more concrete, let's look at a real-world example. Say we want to create a specialized debugger for React components that shows the component tree, props, and state in a more intuitive way than the standard debugger.
Here's a snapshot of what the specialized visualization might look like:
// Sample React component debugging visualization
function ReactComponentVisualizer(component) {
return {
name: component.type.name || 'AnonymousComponent',
props: formatProps(component.props),
state: component.state ? formatState(component.state) : null,
children: Array.isArray(component.children)
? component.children.map(child => ReactComponentVisualizer(child))
: []
};
}
// This would be rendered in the webview as an interactive tree
Conclusion
Building a custom VS Code extension for debugging is a powerful way to streamline your development workflow. While it requires an investment of time upfront, the productivity gains can be substantial, especially if you're working on complex projects with specific debugging needs.
The extension we've built in this tutorial provides a foundation that you can extend with features specific to your workflow. Whether you're debugging React applications, Node.js servers, or any other JavaScript/TypeScript code, having tools tailored to your needs can make a world of difference.
What debugging pain points would you like to solve with a custom extension? Have you already built extensions to improve your development experience? Share your thoughts and experiences in the comments below!
Resources
- VS Code Extension API Documentation
- VS Code Debugger Extension Guide
- Debug Adapter Protocol Specification
- GitHub repo for this tutorial:
https://github.com/fixcode/custom-debug-extension-tutorial