feat(debugger): Show the value of the selected code while paused (#173)

Settings › Debugger › Data Views gains Show value tooltip on code selection
(on by default). While the debugger is paused, selecting an expression on one
line shows its value once the selection settles, with objects expandable like
the hover tooltip.
This commit is contained in:
l0ng-ai
2026-10-29 09:12:40 +08:00
committed by GitHub
parent 1c46a1cd04
commit 7cfbf6c47b
8 changed files with 92 additions and 3 deletions
+3
View File
@@ -221,6 +221,8 @@ pub struct DebuggerSettings {
/// Data Views: list variables by name instead of the adapter's order.
pub sort_values: bool,
pub value_tooltip: bool,
/// While suspended, selecting an expression shows its value.
pub value_tooltip_on_selection: bool,
pub value_tooltip_delay: u32,
/// Stepping: do not step into library code (debugpy `justMyCode`, js-debug `skipFiles`).
pub skip_library_code: bool,
@@ -238,6 +240,7 @@ impl Default for DebuggerSettings {
inline_values: true,
sort_values: false,
value_tooltip: true,
value_tooltip_on_selection: true,
value_tooltip_delay: 700,
skip_library_code: true,
python: PythonDebuggerSettings::default(),
+2
View File
@@ -2244,6 +2244,8 @@ export type DebuggerSettings = {
/** Data Views: list variables by name instead of the adapter's order. */
sortValues?: boolean,
valueTooltip?: boolean,
/** While suspended, selecting an expression shows its value. */
valueTooltipOnSelection?: boolean,
valueTooltipDelay?: number,
/** Stepping: do not step into library code (debugpy `justMyCode`, js-debug `skipFiles`). */
skipLibraryCode?: boolean,
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest";
import { EditorSelection, EditorState } from "@codemirror/state";
import { selectionExpression } from "../editor/valueTooltip";
import { classifyValue, identifiersIn, inlineValue, sortVariables } from "../values";
import { inlineValuesFor } from "../editor/execution";
import { frameLabel, groupFrames, isLibraryFrame } from "../frames";
@@ -109,3 +111,22 @@ describe("sortVariables", () => {
expect(names(sortVariables(items))).toEqual(["[0]", "[1]", "[10]", "[2]"]);
});
});
describe("selectionExpression", () => {
const at = (doc: string, from: number, to: number) =>
selectionExpression(EditorState.create({ doc, selection: EditorSelection.single(from, to) }));
it("takes a one-line selection", () => {
expect(at("total += item.price", 9, 19)).toEqual({ from: 9, text: "item.price" });
});
it("ignores empty, blank, multi-line and multiple selections", () => {
expect(at("a + b", 2, 2)).toBeNull();
expect(at("a b", 1, 4)).toBeNull();
expect(at("a\nb", 0, 3)).toBeNull();
const multi = EditorState.create({
doc: "a + b",
selection: EditorSelection.create([EditorSelection.range(0, 1), EditorSelection.range(4, 5)]),
extensions: EditorState.allowMultipleSelections.of(true),
});
expect(selectionExpression(multi)).toBeNull();
});
});
+59 -2
View File
@@ -3,9 +3,10 @@
* while suspended, hovering an expression (or the selection under the mouse)
* evaluates it in the selected frame.
*/
import type { Extension } from "@codemirror/state";
import type { EditorState, Extension } from "@codemirror/state";
import type { DebugSessionId, Variable } from "@/bindings";
import { hoverTooltip, type EditorView } from "@codemirror/view";
import { hoverTooltip, ViewPlugin, type EditorView, type ViewUpdate } from "@codemirror/view";
import { showHint } from "@/editor/lsp/hint";
import { workflowSettings } from "@/platform/workflowSettings";
import { navigationModifierHeld } from "@/platform/navigationModifier";
import { activeSession, evaluate } from "../state";
@@ -109,3 +110,59 @@ export function valueTooltip(): Extension {
{ hoverTime: BASE_HOVER },
);
}
const SELECTION_DELAY = 400;
const MAX_SELECTION = 200;
/** The selected text when it can be evaluated: one non-blank line, not too long. */
export function selectionExpression(state: EditorState): { from: number; text: string } | null {
const sel = state.selection;
if (sel.ranges.length !== 1 || sel.main.empty) return null;
const text = state.sliceDoc(sel.main.from, sel.main.to).trim();
if (!text || text.length > MAX_SELECTION || text.includes("\n")) return null;
return { from: sel.main.from, text };
}
/**
* Settings › Debugger › Data Views › "Show value tooltip on code selection": while
* suspended, selecting an expression shows its value once the selection settles.
*/
export function selectionValueTooltip(): Extension {
return ViewPlugin.fromClass(
class {
private timer: ReturnType<typeof setTimeout> | undefined;
private seq = 0;
constructor(readonly view: EditorView) {}
update(u: ViewUpdate) {
if (!u.selectionSet) return;
clearTimeout(this.timer);
this.seq++;
if (!u.transactions.some((tr) => tr.isUserEvent("select"))) return;
const seq = this.seq;
this.timer = setTimeout(() => void this.show(seq), SELECTION_DELAY);
}
private async show(seq: number) {
if (!workflowSettings().debugger.valueTooltipOnSelection) return;
const session = activeSession();
if (!session || session.status !== "paused") return;
const expr = selectionExpression(this.view.state);
if (!expr) return;
let value;
try {
value = await evaluate(expr.text, "hover", session);
} catch {
return;
}
// The selection moved on while evaluating.
if (seq !== this.seq) return;
showHint(this.view, () => valueHint(expr.text, value, session.id), { pos: expr.from, timeout: 60_000 });
}
destroy() {
clearTimeout(this.timer);
}
},
);
}
+2 -1
View File
@@ -6,7 +6,7 @@
import { lazyComponent } from "@/ui";
import { onIdeEvent } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { valueTooltip } from "./editor/valueTooltip";
import { selectionValueTooltip, valueTooltip } from "./editor/valueTooltip";
import { registerEditorContribution } from "@/editor/api";
import {
registerMenuGroup,
@@ -65,6 +65,7 @@ export function activate(): () => void {
executionPoint(ctx),
runToCursorInlay(ctx),
valueTooltip(),
selectionValueTooltip(),
smartStepTargets(ctx),
],
}),
@@ -180,6 +180,7 @@ export const EXECUTION_PAGES: PageSpec[] = [
label: "Show value tooltip on mouse hover",
hint: "Hovering a variable while suspended evaluates it in the selected frame.",
},
{ kind: "toggle", key: "debugger.valueTooltipOnSelection", label: "Show value tooltip on code selection" },
{ kind: "custom", label: "Value tooltip delay (ms)", component: TooltipDelayRow },
],
},
+1
View File
@@ -1903,6 +1903,7 @@
"Show the selected coverage suite in editors and the project view": "在编辑器和项目视图中显示所选覆盖率套件",
"Show tool window bars": "显示工具窗口栏",
"Show tool window names": "显示工具窗口名称",
"Show value tooltip on code selection": "选中代码时显示值提示",
"Show value tooltip on mouse hover": "鼠标悬停时显示值工具提示",
"Show values inline": "内联显示值",
"Show virtual space at file bottom": "在文件底部显示虚拟空间",
+3
View File
@@ -85,6 +85,7 @@ export interface DebuggerSettingsValid {
inlineValues: boolean;
sortValues: boolean;
valueTooltip: boolean;
valueTooltipOnSelection: boolean;
valueTooltipDelay: number;
skipLibraryCode: boolean;
python: { attachSubprocess: boolean; gevent: boolean };
@@ -245,6 +246,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
inlineValues: true,
sortValues: false,
valueTooltip: true,
valueTooltipOnSelection: true,
valueTooltipDelay: 700,
skipLibraryCode: true,
python: { attachSubprocess: true, gevent: false },
@@ -380,6 +382,7 @@ export function validateDebugger(raw: unknown): DebuggerSettingsValid {
inlineValues: bool(v.inlineValues, d.inlineValues),
sortValues: bool(v.sortValues, d.sortValues),
valueTooltip: bool(v.valueTooltip, d.valueTooltip),
valueTooltipOnSelection: bool(v.valueTooltipOnSelection, d.valueTooltipOnSelection),
valueTooltipDelay: num(v.valueTooltipDelay, d.valueTooltipDelay, WORKFLOW_LIMITS.tooltipDelay, true),
skipLibraryCode: bool(v.skipLibraryCode, d.skipLibraryCode),
python: {