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: {