From 7cfbf6c47b3e83baa01b60d50160879cc0e43c83 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:12:40 +0800 Subject: [PATCH] feat(debugger): Show the value of the selected code while paused (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/ide-core/src/workflow_settings.rs | 3 + src/bindings.ts | 2 + .../debugger/__tests__/values.test.ts | 21 +++++++ src/features/debugger/editor/valueTooltip.ts | 61 ++++++++++++++++++- src/features/debugger/index.ts | 3 +- src/features/settings/pages/execution.tsx | 1 + src/platform/locales/text/zh.json | 1 + src/platform/workflowSettings.ts | 3 + 8 files changed, 92 insertions(+), 3 deletions(-) diff --git a/crates/ide-core/src/workflow_settings.rs b/crates/ide-core/src/workflow_settings.rs index 56b6a80a..64fb58fb 100644 --- a/crates/ide-core/src/workflow_settings.rs +++ b/crates/ide-core/src/workflow_settings.rs @@ -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(), diff --git a/src/bindings.ts b/src/bindings.ts index 73996c71..6c9ddf95 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -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, diff --git a/src/features/debugger/__tests__/values.test.ts b/src/features/debugger/__tests__/values.test.ts index a22584c5..38a5bcfc 100644 --- a/src/features/debugger/__tests__/values.test.ts +++ b/src/features/debugger/__tests__/values.test.ts @@ -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(); + }); +}); diff --git a/src/features/debugger/editor/valueTooltip.ts b/src/features/debugger/editor/valueTooltip.ts index 6c528443..32d12d51 100644 --- a/src/features/debugger/editor/valueTooltip.ts +++ b/src/features/debugger/editor/valueTooltip.ts @@ -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 | 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); + } + }, + ); +} diff --git a/src/features/debugger/index.ts b/src/features/debugger/index.ts index edc93a7b..18ade51d 100644 --- a/src/features/debugger/index.ts +++ b/src/features/debugger/index.ts @@ -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), ], }), diff --git a/src/features/settings/pages/execution.tsx b/src/features/settings/pages/execution.tsx index fdf638a2..60c2a06c 100644 --- a/src/features/settings/pages/execution.tsx +++ b/src/features/settings/pages/execution.tsx @@ -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 }, ], }, diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 3b66e13b..4227b63b 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -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": "在文件底部显示虚拟空间", diff --git a/src/platform/workflowSettings.ts b/src/platform/workflowSettings.ts index 83fe6ee0..42871480 100644 --- a/src/platform/workflowSettings.ts +++ b/src/platform/workflowSettings.ts @@ -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: {