Skip to content

extension: add debug menu for var show in doc #3818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.49.0 (prerelease)

Date: 2025-07-07
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 @@ -3482,6 +3487,10 @@
{
"command": "go.explorer.open",
"when": "false"
},
{
"command": "go.debug.openVariableAsDoc",
"when": "false"
}
],
"debug/callstack/context": [
Expand All @@ -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",
Expand Down
139 changes: 139 additions & 0 deletions extension/src/goDebugCommands.ts
Original file line number Diff line number Diff line change
@@ -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<string, vscode.DebugSession>();
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<string> {
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<string, string> = {
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));
}
}
}
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