VSCode Extensions run in a separate Extension Host process that is isolated from the main VSCode instance. This architecture requires specific debug configurations that fundamentally differ from standard Node.js applications. The Extension Host communicates with the VSCode UI via IPC (Inter-Process Communication), which brings additional debugging complexity.
Debugging occurs through two primary mechanisms: - Extension Development Host: A separate VSCode instance for extension execution - Debug Adapter Protocol: Standardized communication between debugger and Extension Host
The .vscode/launch.json file defines debug
configurations. The yo code template creates a basic
configuration:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${workspaceFolder}/.vscode/tasks.json#npm: compile"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: compile"
}
]
}For more complex debugging scenarios, additional configurations are required:
{
"configurations": [
{
"name": "Run Extension (Development)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-extensions",
"${workspaceFolder}/test-workspace"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"env": {
"NODE_ENV": "development",
"VSCODE_DEBUG_MODE": "true"
},
"sourceMaps": true,
"smartStep": true,
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Attach to Extension Host",
"type": "node",
"request": "attach",
"port": 5870,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}
]
}For effective debugging, tsconfig.json must enable
Source Maps:
{
"compilerOptions": {
"sourceMap": true,
"inlineSourceMap": false,
"sourceRoot": "../src"
},
"include": ["src/**/*"]
}The corresponding tasks.json for automatic
compilation:
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"group": "build",
"presentation": {
"panel": "shared",
"reveal": "silent",
"clear": true
},
"problemMatcher": "$tsc"
},
{
"type": "npm",
"script": "watch",
"group": "build",
"presentation": {
"panel": "shared",
"reveal": "silent"
},
"isBackground": true,
"problemMatcher": "$tsc-watch"
}
]
}VSCode supports various breakpoint types for extension debugging:
// src/extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext): void {
// Conditional Breakpoint: Right-click -> Add Conditional Breakpoint
const disposable = vscode.commands.registerCommand('extension.debug', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
// Breakpoint here for undefined editor
vscode.window.showErrorMessage('No active editor');
return;
}
const document = editor.document;
const selection = editor.selection;
// Logpoint: Right-click -> Add Logpoint
// Log: "Selection: {selection.start.line}:{selection.start.character}"
try {
await processSelection(document, selection);
} catch (error) {
// Exception Breakpoint: Debug Console -> Break on exceptions
console.error('Processing failed:', error);
throw error;
}
});
context.subscriptions.push(disposable);
}
async function processSelection(
document: vscode.TextDocument,
selection: vscode.Selection
): Promise<void> {
// Function Breakpoint: Debug -> New Function Breakpoint
const text = document.getText(selection);
if (text.length === 0) {
return; // Breakpoint for empty selection
}
// Hitcount Breakpoint: Right-click -> Edit Breakpoint -> Hit Count
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
// Processing...
}
}The Debug Console provides extended logging functionality:
// src/debugUtils.ts
export class DebugLogger {
private static instance: DebugLogger;
private outputChannel: vscode.OutputChannel;
private constructor() {
this.outputChannel = vscode.window.createOutputChannel('Extension Debug');
}
public static getInstance(): DebugLogger {
if (!DebugLogger.instance) {
DebugLogger.instance = new DebugLogger();
}
return DebugLogger.instance;
}
public log(message: string, data?: any): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
// Debug Console Output
console.log(logEntry, data);
// Output Channel
this.outputChannel.appendLine(logEntry);
if (data) {
this.outputChannel.appendLine(JSON.stringify(data, null, 2));
}
}
public logError(error: Error, context?: string): void {
const errorMsg = `ERROR${context ? ` in ${context}` : ''}: ${error.message}`;
this.log(errorMsg);
this.outputChannel.appendLine(`Stack: ${error.stack}`);
}
public logPerformance<T>(
operation: string,
fn: () => T | Promise<T>
): T | Promise<T> {
const start = performance.now();
this.log(`Starting: ${operation}`);
const result = fn();
if (result instanceof Promise) {
return result.then(value => {
const duration = performance.now() - start;
this.log(`Completed: ${operation} (${duration.toFixed(2)}ms)`);
return value;
});
} else {
const duration = performance.now() - start;
this.log(`Completed: ${operation} (${duration.toFixed(2)}ms)`);
return result;
}
}
}
// Usage
const logger = DebugLogger.getInstance();
logger.log('Extension activated');For in-depth debugging, the Extension Host can be attached directly:
// src/extension.ts
export function activate(context: vscode.ExtensionContext): void {
// Debug-specific configuration
if (process.env.VSCODE_DEBUG_MODE === 'true') {
enableDebugMode(context);
}
// Normal extension logic
registerCommands(context);
}
function enableDebugMode(context: vscode.ExtensionContext): void {
// Debug-specific disposables
const debugDisposables: vscode.Disposable[] = [];
// Command for debug information
debugDisposables.push(
vscode.commands.registerCommand('extension.showDebugInfo', () => {
const debugInfo = {
extensionId: context.extension.id,
extensionPath: context.extensionPath,
globalStoragePath: context.globalStorageUri.fsPath,
workspaceState: context.workspaceState.keys(),
subscriptions: context.subscriptions.length
};
vscode.window.showInformationMessage(
`Debug Info: ${JSON.stringify(debugInfo, null, 2)}`
);
})
);
// Memory Usage Monitoring
const memoryMonitor = setInterval(() => {
const usage = process.memoryUsage();
console.log('Memory Usage:', {
rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`
});
}, 30000);
debugDisposables.push(new vscode.Disposable(() => {
clearInterval(memoryMonitor);
}));
context.subscriptions.push(...debugDisposables);
}Webviews require special debugging techniques:
// src/webviewProvider.ts
export class DebugWebviewProvider implements vscode.WebviewViewProvider {
public resolveWebviewView(webviewView: vscode.WebviewView): void {
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: []
};
// Debug mode for webview
if (process.env.NODE_ENV === 'development') {
webviewView.webview.html = this.getDebugHtml();
} else {
webviewView.webview.html = this.getProductionHtml();
}
// Message handling with debug logging
webviewView.webview.onDidReceiveMessage(message => {
console.log('Webview Message:', message);
switch (message.command) {
case 'debug':
this.handleDebugMessage(message.data);
break;
default:
console.warn('Unknown webview message:', message);
}
});
}
private getDebugHtml(): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>Debug Webview</title>
</head>
<body>
<h1>Debug Mode</h1>
<button onclick="sendDebugMessage()">Send Debug Info</button>
<div id="debug-output"></div>
<script>
const vscode = acquireVsCodeApi();
function sendDebugMessage() {
const debugData = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
location: window.location.href
};
vscode.postMessage({
command: 'debug',
data: debugData
});
}
// Debug Console for webview
window.addEventListener('error', (event) => {
vscode.postMessage({
command: 'debug',
data: {
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno
}
});
});
</script>
</body>
</html>
`;
}
}For extensions running on remote environments:
{
"name": "Attach to Remote Extension Host",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 5870,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}Performance analysis and profiling:
// src/performance.ts
export class PerformanceProfiler {
private measurements: Map<string, number> = new Map();
public startMeasurement(name: string): void {
this.measurements.set(name, performance.now());
}
public endMeasurement(name: string): number {
const start = this.measurements.get(name);
if (!start) {
throw new Error(`No measurement started for: ${name}`);
}
const duration = performance.now() - start;
this.measurements.delete(name);
console.log(`Performance: ${name} took ${duration.toFixed(2)}ms`);
return duration;
}
public measureAsync<T>(name: string, promise: Promise<T>): Promise<T> {
this.startMeasurement(name);
return promise.finally(() => {
this.endMeasurement(name);
});
}
}
// Usage with decorator
export function measure(target: any, propertyName: string, descriptor: PropertyDescriptor): void {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const profiler = new PerformanceProfiler();
const measurementName = `${target.constructor.name}.${propertyName}`;
profiler.startMeasurement(measurementName);
try {
const result = method.apply(this, args);
if (result instanceof Promise) {
return result.finally(() => {
profiler.endMeasurement(measurementName);
});
} else {
profiler.endMeasurement(measurementName);
return result;
}
} catch (error) {
profiler.endMeasurement(measurementName);
throw error;
}
};
}| Aspect | Java/Eclipse | VSCode/TypeScript |
|---|---|---|
| Debug Architecture | JVM Debug Interface | Extension Host + DAP |
| Breakpoints | Bytecode-based | Source Map-based |
| Hot Reloading | HotSwap (limited) | Restart required |
| Memory Debugging | JVisualVM, MAT | Node.js –inspect |
| Remote Debugging | JDWP | Node.js Debug Protocol |
| Step Debugging | Bytecode level | JavaScript level |
Source Maps: Always keep enabled for accurate debugging in TypeScript source code.
Conditional Breakpoints: Use for specific debugging scenarios without code changes.
Output Channels: Create separate channels for different extension components.
Error Handling: Implement comprehensive try-catch blocks with detailed logging.
Performance Monitoring: Instrument critical operations with time measurements.
Debug Mode: Control development-specific features via environment variables:
const isDevelopment = process.env.NODE_ENV === 'development' ||
process.env.VSCODE_DEBUG_MODE === 'true';
if (isDevelopment) {
// Activate additional debug features
}Memory Leaks: Monitor extension lifecycle and manage disposables correctly:
export function activate(context: vscode.ExtensionContext): void {
const disposables: vscode.Disposable[] = [];
// Collect all disposables
disposables.push(
vscode.commands.registerCommand('...', handler),
vscode.workspace.onDidChangeTextDocument(listener)
);
// Automatic registration
context.subscriptions.push(...disposables);
}