diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d70ae110..9b183fed98 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.0 (prerelease) Date: 2025-07-07 diff --git a/docs/commands.md b/docs/commands.md index 6730c7caeb..fe49f56116 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 db142e59f1..3b58acdd6d 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", @@ -3482,6 +3487,10 @@ { "command": "go.explorer.open", "when": "false" + }, + { + "command": "go.debug.openVariableAsDoc", + "when": "false" } ], "debug/callstack/context": [ @@ -3490,6 +3499,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..19b7221523 --- /dev/null +++ b/extension/src/goDebugCommands.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------- + * 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'; +import { TextDecoder } from 'util'; + +// Track sessions since vscode doesn't provide a list of them. +const sessions = new Map(); +vscode.debug.onDidStartDebugSession((s) => sessions.set(s.id, s)); +vscode.debug.onDidTerminateDebugSession((s) => sessions.delete(s.id)); + +/** + * 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) { + class VariableContentProvider implements vscode.TextDocumentContentProvider { + 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 = 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; + let 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; + } + + const allBytes = Buffer.concat(full); + + return new TextDecoder('utf-8').decode(allBytes); + } + } + + ctx.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider('go-debug-variable', new VariableContentProvider()) + ); + + ctx.subscriptions.push( + 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); + }) + ); + + /** + * 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) { + let 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 243a4a29a3..bad7017379 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') {}