Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.51.0 (prerelease)

Date: 2025-09-04
Expand Down
4 changes: 4 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Finally, you can also see a full list by using a meta command: `Go: Show All Com

<!-- Everything below this line is generated. DO NOT EDIT. -->

### `Go: Open in new Document`

Open selected variable in a new document.

### `Go: Current GOPATH`

See the currently set GOPATH.
Expand Down
16 changes: 16 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -3487,6 +3492,10 @@
{
"command": "go.explorer.open",
"when": "false"
},
{
"command": "go.debug.openVariableAsDoc",
"when": "false"
}
],
"debug/callstack/context": [
Expand All @@ -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",
Expand Down
140 changes: 140 additions & 0 deletions extension/src/goDebugCommands.ts
Original file line number Diff line number Diff line change
@@ -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<string, vscode.DebugSession>();

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<string, vscode.DebugSession>

constructor(sessionsSet: Map<string, vscode.DebugSession>) {
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<string> {
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<string, string> = {
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));
}
}
2 changes: 2 additions & 0 deletions extension/src/goDebugConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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') {}
Expand Down