дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02: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.
Цей коміт міститься в:
1 джерело
2ec7f5d630
коміт
4dcf0acc07
10 files changed
+186
-1
No files matched your search
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача