44 Debugging Your Own Extensions

44.1 Debug Architecture in VSCode Extensions

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

44.2 Launch Configuration

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

44.3 Advanced Debug Configurations

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

44.4 TypeScript Source Maps

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

44.5 Breakpoint Strategies

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...
    }
}

44.6 Debug Console and Logging

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

44.7 Extension Host Debugging

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

44.8 Webview Debugging

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>
        `;
    }
}

44.9 Remote Debugging

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

44.10 Performance Debugging

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

44.11 Comparison to Java/Eclipse Debugging

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

44.12 Best Practices

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