From 0daf8ac15df049d97d06c087d2f54a39efb52b08 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:42:17 +0800 Subject: [PATCH] feat(debugger): Show the value of the selected code while paused 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 56b6a80af..64fb58fbe 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 ab816e207..2a279ba24 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -2239,6 +2239,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 a22584c54..38a5bcfc4 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 6c528443e..32d12d51e 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 edc93a7ba..18ade51d0 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 fdf638a2c..60c2a06c3 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 fc488ad34..3c0d8df92 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 83fe6ee07..428714803 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: {