mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(debugger): Show a Run to Cursor button when hovering a line while paused (#168)
While the debugger is suspended, hovering the start of a line shows a small button left of its text: Run to Cursor runs to that line, and on the execution line it resumes the program. Settings › Debugger › Show inlay Run to Cursor popup turns it off.
This commit is contained in:
@@ -215,6 +215,8 @@ pub struct DebuggerSettings {
|
||||
pub focus_app_on_breakpoint: bool,
|
||||
pub hide_on_termination: bool,
|
||||
pub breakpoints_any_file: bool,
|
||||
/// While suspended, hovering the start of a line shows a Run to Cursor button.
|
||||
pub run_to_cursor_inlay: bool,
|
||||
pub inline_values: bool,
|
||||
pub value_tooltip: bool,
|
||||
pub value_tooltip_delay: u32,
|
||||
@@ -230,6 +232,7 @@ impl Default for DebuggerSettings {
|
||||
focus_app_on_breakpoint: false,
|
||||
hide_on_termination: false,
|
||||
breakpoints_any_file: false,
|
||||
run_to_cursor_inlay: true,
|
||||
inline_values: true,
|
||||
value_tooltip: true,
|
||||
value_tooltip_delay: 700,
|
||||
|
||||
@@ -2150,6 +2150,8 @@ export type DebuggerSettings = {
|
||||
focusAppOnBreakpoint?: boolean,
|
||||
hideOnTermination?: boolean,
|
||||
breakpointsAnyFile?: boolean,
|
||||
/** While suspended, hovering the start of a line shows a Run to Cursor button. */
|
||||
runToCursorInlay?: boolean,
|
||||
inlineValues?: boolean,
|
||||
valueTooltip?: boolean,
|
||||
valueTooltipDelay?: number,
|
||||
|
||||
@@ -74,6 +74,12 @@ function pausedTarget(): { s: DebugSession; thread: number } | null {
|
||||
return thread == null ? null : { s, thread };
|
||||
}
|
||||
|
||||
/** Run to Cursor at `line` (0-based) of `path` in the paused session. */
|
||||
export function runToLine(path: string, line: number) {
|
||||
const t = pausedTarget();
|
||||
if (t) void report("Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, path, line, false)));
|
||||
}
|
||||
|
||||
function notSupported(what: string, s: DebugSession) {
|
||||
notifications().notify({
|
||||
group: "run",
|
||||
|
||||
@@ -185,3 +185,25 @@
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 0 0 0 1px var(--accent);
|
||||
}
|
||||
|
||||
/* Inlay Run to Cursor button, left of a hovered line's text. */
|
||||
.runToCursor {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-popup);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--bg-popup);
|
||||
box-shadow: var(--shadow-tooltip);
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.runToCursor:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hoveredLine } from "./runToCursor";
|
||||
|
||||
describe("inlay Run to Cursor", () => {
|
||||
const line = { number: 3, text: " foo()" };
|
||||
it("targets a non-blank line when hovering its first characters", () => {
|
||||
expect(hoveredLine(100, 8, 100, line)).toBe(2);
|
||||
expect(hoveredLine(100, 8, 132, line)).toBe(2);
|
||||
});
|
||||
it("ignores the pointer further right, left of the text, and blank lines", () => {
|
||||
expect(hoveredLine(100, 8, 133, line)).toBeNull();
|
||||
expect(hoveredLine(100, 8, 99, line)).toBeNull();
|
||||
expect(hoveredLine(100, 8, 104, { number: 4, text: " " })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Inlay Run to Cursor: while the debugger is paused, hovering the start of a
|
||||
* line shows a small button left of its text that runs to that line (Resume on
|
||||
* the execution line itself). Settings › Debugger › "Show inlay Run to Cursor popup".
|
||||
*/
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import type { EditorDocContext } from "@/editor/api";
|
||||
import { iconNode } from "@/editor/lsp/iconMarkup";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { runToLine } from "../actions";
|
||||
import { activeSession, useExecutionPoint } from "../state";
|
||||
import styles from "./editor.module.css";
|
||||
|
||||
/** Hover area from the left edge of the text, in characters. */
|
||||
const HOVER_CHARS = 4;
|
||||
const BUTTON = 20;
|
||||
|
||||
/** The 0-based line whose start the pointer is over, or null. */
|
||||
export function hoveredLine(
|
||||
textLeft: number,
|
||||
charWidth: number,
|
||||
x: number,
|
||||
line: { number: number; text: string },
|
||||
): number | null {
|
||||
if (x < textLeft || x - textLeft > charWidth * HOVER_CHARS) return null;
|
||||
return line.text.trim() ? line.number - 1 : null;
|
||||
}
|
||||
|
||||
export function runToCursorInlay(ctx: EditorDocContext): Extension {
|
||||
if (ctx.virtual) return [];
|
||||
const path = ctx.path;
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
private button: HTMLButtonElement | null = null;
|
||||
private line = -1;
|
||||
private unsub: () => void;
|
||||
constructor(readonly view: EditorView) {
|
||||
view.scrollDOM.addEventListener("mousemove", this.onMove);
|
||||
view.scrollDOM.addEventListener("mouseleave", this.onLeave);
|
||||
view.scrollDOM.addEventListener("scroll", this.hide);
|
||||
// Resumed / stopped / moved: the button's action may no longer apply.
|
||||
this.unsub = useExecutionPoint.subscribe(this.hide);
|
||||
}
|
||||
|
||||
private enabled() {
|
||||
return workflowSettings().debugger.runToCursorInlay && activeSession()?.status === "paused";
|
||||
}
|
||||
|
||||
private onMove = (e: MouseEvent) => {
|
||||
if (e.target instanceof Node && this.button?.contains(e.target)) return;
|
||||
if (!this.enabled() || e.buttons) return this.hide();
|
||||
const { view } = this;
|
||||
const pos = view.posAtCoords({ x: e.clientX, y: e.clientY }, false);
|
||||
const block = view.lineBlockAtHeight(e.clientY - view.documentTop);
|
||||
if (pos == null || pos < block.from || pos > block.to) return this.hide();
|
||||
const line = view.state.doc.lineAt(block.from);
|
||||
const textLeft = view.coordsAtPos(line.from, 1)?.left;
|
||||
if (textLeft == null) return this.hide();
|
||||
const target = hoveredLine(textLeft, view.defaultCharacterWidth, e.clientX, line);
|
||||
if (target == null) return this.hide();
|
||||
if (target !== this.line) this.show(target, block.top + view.documentTop, line.from);
|
||||
};
|
||||
|
||||
private onLeave = (e: MouseEvent) => {
|
||||
if (!(e.relatedTarget instanceof Node && this.button?.contains(e.relatedTarget))) this.hide();
|
||||
};
|
||||
|
||||
private show(line: number, top: number, lineFrom: number) {
|
||||
this.hide();
|
||||
const { view } = this;
|
||||
const exec = useExecutionPoint.getState();
|
||||
const atExecution = exec.path === path && exec.line === line;
|
||||
const label = atExecution ? tx("Resume Program") : tx("Run to Cursor");
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = styles.runToCursor!;
|
||||
b.title = label;
|
||||
b.setAttribute("aria-label", label);
|
||||
b.append(iconNode(atExecution ? "resume" : "run-to-cursor", 14));
|
||||
b.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
b.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
if (atExecution) executeAction("debug.resume");
|
||||
else runToLine(path, line);
|
||||
});
|
||||
b.addEventListener("mouseleave", (e) => {
|
||||
if (!(e.relatedTarget instanceof Node && view.scrollDOM.contains(e.relatedTarget))) this.hide();
|
||||
});
|
||||
// Left of the first non-space character; over the gutter edge when the indent is too narrow.
|
||||
const text = view.state.doc.lineAt(lineFrom).text;
|
||||
const indentEnd = lineFrom + (text.length - text.trimStart().length);
|
||||
const textX = view.coordsAtPos(indentEnd)?.left ?? view.contentDOM.getBoundingClientRect().left;
|
||||
const host = view.dom.getBoundingClientRect();
|
||||
const lineHeight = view.lineBlockAt(lineFrom).height;
|
||||
b.style.left = `${textX - host.left - BUTTON - 2}px`;
|
||||
b.style.top = `${top - host.top + (lineHeight - BUTTON) / 2}px`;
|
||||
view.dom.appendChild(b);
|
||||
this.button = b;
|
||||
this.line = line;
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
this.button?.remove();
|
||||
this.button = null;
|
||||
this.line = -1;
|
||||
};
|
||||
|
||||
destroy() {
|
||||
this.hide();
|
||||
this.unsub();
|
||||
this.view.scrollDOM.removeEventListener("mousemove", this.onMove);
|
||||
this.view.scrollDOM.removeEventListener("mouseleave", this.onLeave);
|
||||
this.view.scrollDOM.removeEventListener("scroll", this.hide);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { loadExceptionBreakpoints } from "./breakpoints/exceptions";
|
||||
import { loadSpecialBreakpoints } from "./breakpoints/special";
|
||||
import { breakpointGutter } from "./editor/breakpointGutter";
|
||||
import { executionPoint } from "./editor/execution";
|
||||
import { runToCursorInlay } from "./editor/runToCursor";
|
||||
import { installSmartStepPicking, smartStepTargets } from "./editor/smartStep";
|
||||
import { onHotCodeReplace } from "./hotswap";
|
||||
import { handleDebugEvent, isLive, loadWatches, publishContext, resetSessions, stopSession, useDebug } from "./state";
|
||||
@@ -59,7 +60,13 @@ export function activate(): () => void {
|
||||
registerEditorContribution({
|
||||
id: "debugger",
|
||||
order: 60,
|
||||
extensions: (ctx) => [breakpointGutter(ctx), executionPoint(ctx), valueTooltip(), smartStepTargets(ctx)],
|
||||
extensions: (ctx) => [
|
||||
breakpointGutter(ctx),
|
||||
executionPoint(ctx),
|
||||
runToCursorInlay(ctx),
|
||||
valueTooltip(),
|
||||
smartStepTargets(ctx),
|
||||
],
|
||||
}),
|
||||
installSmartStepPicking(),
|
||||
onIdeEvent("debug", (e) => {
|
||||
|
||||
@@ -149,6 +149,11 @@ export const EXECUTION_PAGES: PageSpec[] = [
|
||||
label: "Allow placing breakpoints in any file",
|
||||
hint: "Otherwise line breakpoints can only be set in files of languages that have a debugger.",
|
||||
},
|
||||
{
|
||||
kind: "toggle",
|
||||
key: "debugger.runToCursorInlay",
|
||||
label: "Show inlay Run to Cursor popup",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1880,6 +1880,7 @@
|
||||
"Show hard wrap and visual guides (configured in Code Style options)": "显示硬换行和视觉参考线(在代码样式选项中配置)",
|
||||
"Show in Finder": "在访达中显示",
|
||||
"Show in tool window": "在工具窗口中显示",
|
||||
"Show inlay Run to Cursor popup": "显示内嵌的“运行到光标处”按钮",
|
||||
"Show indent guides": "显示缩进参考线",
|
||||
"Show inlay hints": "显示嵌入提示",
|
||||
"Show inlay hints for:": "为以下语言显示嵌入提示:",
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface DebuggerSettingsValid {
|
||||
focusAppOnBreakpoint: boolean;
|
||||
hideOnTermination: boolean;
|
||||
breakpointsAnyFile: boolean;
|
||||
runToCursorInlay: boolean;
|
||||
inlineValues: boolean;
|
||||
valueTooltip: boolean;
|
||||
valueTooltipDelay: number;
|
||||
@@ -239,6 +240,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
|
||||
focusAppOnBreakpoint: false,
|
||||
hideOnTermination: false,
|
||||
breakpointsAnyFile: false,
|
||||
runToCursorInlay: true,
|
||||
inlineValues: true,
|
||||
valueTooltip: true,
|
||||
valueTooltipDelay: 700,
|
||||
@@ -372,6 +374,7 @@ export function validateDebugger(raw: unknown): DebuggerSettingsValid {
|
||||
focusAppOnBreakpoint: bool(v.focusAppOnBreakpoint, d.focusAppOnBreakpoint),
|
||||
hideOnTermination: bool(v.hideOnTermination, d.hideOnTermination),
|
||||
breakpointsAnyFile: bool(v.breakpointsAnyFile, d.breakpointsAnyFile),
|
||||
runToCursorInlay: bool(v.runToCursorInlay, d.runToCursorInlay),
|
||||
inlineValues: bool(v.inlineValues, d.inlineValues),
|
||||
valueTooltip: bool(v.valueTooltip, d.valueTooltip),
|
||||
valueTooltipDelay: num(v.valueTooltipDelay, d.valueTooltipDelay, WORKFLOW_LIMITS.tooltipDelay, true),
|
||||
|
||||
Reference in New Issue
Block a user