diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad7f72066..f9fdc9e324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ## Unreleased +Added menu in a debugger that will show variable in a new document with respect to special chars like `\r\n\t` + ## v0.49.1 (prerelease) Date: 2025-08-21 diff --git a/docs/commands.md b/docs/commands.md index c13f5ab2d4..271faa2152 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -23,6 +23,10 @@ Finally, you can also see a full list by using a meta command: `Go: Show All Com +### `Go: Open in new Document` + +Open selected variable in a new document. + ### `Go: Current GOPATH` See the currently set GOPATH. diff --git a/extension/package.json b/extension/package.json index 2d48b1fff8..e990664bba 100644 --- a/extension/package.json +++ b/extension/package.json @@ -212,6 +212,11 @@ } }, "commands": [ + { + "command": "go.debug.openVariableAsDoc", + "title": "Go: Open in new Document", + "description": "Open selected variable in a new document." + }, { "command": "go.gopath", "title": "Go: Current GOPATH", @@ -3487,6 +3492,10 @@ { "command": "go.explorer.open", "when": "false" + }, + { + "command": "go.debug.openVariableAsDoc", + "when": "false" } ], "debug/callstack/context": [ @@ -3495,6 +3504,13 @@ "when": "debugType == 'go' && (callStackItemType == 'stackFrame' || (callStackItemType == 'thread' && callStackItemStopped))" } ], + "debug/variables/context": [ + { + "command": "go.debug.openVariableAsDoc", + "when": "debugType=='go'", + "group": "navigation" + } + ], "editor/context": [ { "when": "editorTextFocus && config.go.editorContextMenuCommands.toggleTestFile && resourceLangId == go", diff --git a/extension/src/goDebugCommands.ts b/extension/src/goDebugCommands.ts new file mode 100644 index 0000000000..d3c194c54a --- /dev/null +++ b/extension/src/goDebugCommands.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +/** + * Registers commands to improve the debugging experience for Go. + * + * Currently, it adds a command to open a variable in a new text document. + */ +export function registerGoDebugCommands(ctx: vscode.ExtensionContext) { + // Track sessions since vscode doesn't provide a list of them. + const sessions = new Map(); + + ctx.subscriptions.push( + vscode.debug.onDidStartDebugSession((s) => sessions.set(s.id, s)), + vscode.debug.onDidTerminateDebugSession((s) => sessions.delete(s.id)), + vscode.workspace.registerTextDocumentContentProvider('go-debug-variable', new VariableContentProvider(sessions)), + vscode.commands.registerCommand('go.debug.openVariableAsDoc', async (ref: VariableRef) => { + const uri = VariableContentProvider.uriForRef(ref); + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); + }) + ); +} + +class VariableContentProvider implements vscode.TextDocumentContentProvider { + sessions: Map + + constructor(sessionsSet: Map) { + this.sessions = sessionsSet; + } + + static uriForRef(ref: VariableRef) { + return vscode.Uri.from({ + scheme: 'go-debug-variable', + authority: `${ref.container.variablesReference}@${ref.sessionId}`, + path: `/${ref.variable.name}` + }); + } + + async provideTextDocumentContent(uri: vscode.Uri): Promise { + const name = uri.path.replace(/^\//, ''); + const [container, sessionId] = uri.authority.split('@', 2); + if (!container || !sessionId) { + throw new Error('Invalid URI'); + } + + const session = this.sessions.get(sessionId); + if (!session) return 'Debug session has been terminated'; + + const { variables } = await session.customRequest('variables', { + variablesReference: parseInt(container, 10) + }) as { variables: Variable[] }; + + const v = variables.find(v => v.name === name); + if (!v) return `Cannot resolve variable ${name}`; + + if (!v.memoryReference) { + const { result } = await session.customRequest('evaluate', { + expression: v.evaluateName, + context: 'clipboard' + }) as { result: string }; + + v.value = result ?? v.value; + + return parseVariable(v); + } + + const chunk = 1 << 14; + let offset = 0; + const full: Uint8Array[] = []; + + while (true) { + const resp = await session.customRequest('readMemory', { + memoryReference: v.memoryReference, + offset, + count: chunk + }) as { address: string; data: string; unreadableBytes: number }; + + if (!resp.data) break; + full.push(Buffer.from(resp.data, 'base64')); + + if (resp.unreadableBytes === 0) break; + offset += chunk; + } + + return Buffer.concat(full).toString('utf-8'); + } +} + +/** + * A reference to a variable, used to pass data between commands. + */ +interface VariableRef { + sessionId: string; + container: Container; + variable: Variable; +} + +/** + * A container for variables, used to pass data between commands. + */ +interface Container { + name: string; + variablesReference: number; + expensive: boolean; +} + +/** + * A variable, used to pass data between commands. + */ +interface Variable { + name: string; + value: string; + evaluateName: string; + variablesReference: number; + memoryReference?: string; +} + +const escapeCodes: Record = { + r: '\r', + n: '\n', + t: '\t' +}; + +/** + * Parses a variable value, unescaping special characters. + */ +function parseVariable(variable: Variable) { + const raw = variable.value.trim(); + + try { + return JSON.parse(raw); + } catch (_) { + return raw.replace(/\\[nrt\\"'`]/, (_, s) => (s in escapeCodes ? escapeCodes[s] : s)); + } +} diff --git a/extension/src/goDebugConfiguration.ts b/extension/src/goDebugConfiguration.ts index a1de509828..f221984533 100644 --- a/extension/src/goDebugConfiguration.ts +++ b/extension/src/goDebugConfiguration.ts @@ -34,6 +34,7 @@ import { resolveHomeDir } from './utils/pathUtils'; import { createRegisterCommand } from './commands'; import { GoExtensionContext } from './context'; import { spawn } from 'child_process'; +import { registerGoDebugCommands } from './goDebugCommands'; let dlvDAPVersionChecked = false; @@ -45,6 +46,7 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr const registerCommand = createRegisterCommand(ctx, goCtx); registerCommand('go.debug.pickProcess', () => pickProcess); registerCommand('go.debug.pickGoProcess', () => pickGoProcess); + registerGoDebugCommands(ctx); } constructor(private defaultDebugAdapterType: string = 'go') {}