mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac6c6b2047 |
No files matched your search
@@ -0,0 +1,158 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { executeAction, registerAction } from "@/platform/actions";
|
||||
import * as hints from "@/editor/lsp/hint";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { menuGroupItems } from "@/workbench/contributions";
|
||||
import { navigationExtension } from "@/editor/lsp/navigation";
|
||||
import { activate } from "../index";
|
||||
import { adoptSession, publishContext, useDebug } from "../state";
|
||||
import { quickEvaluateMouse, quickEvaluateMouseDown } from "../editor/quickEvaluate";
|
||||
import { useOverlays } from "../ui/popoverStore";
|
||||
|
||||
const ipc = vi.hoisted(() => ({
|
||||
debugEvaluateIn: vi.fn(async () => ({
|
||||
status: "ok",
|
||||
data: {
|
||||
name: "result",
|
||||
value: "42",
|
||||
typeName: "int",
|
||||
variablesReference: 0,
|
||||
evaluateName: null,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, commands: { ...real.commands, ...ipc } };
|
||||
});
|
||||
const editor = vi.hoisted(() => ({ current: null as unknown }));
|
||||
vi.mock("@/editor/api", async (original) => ({
|
||||
...(await original<typeof import("@/editor/api")>()),
|
||||
activeEditor: () => editor.current,
|
||||
}));
|
||||
|
||||
let view: EditorView;
|
||||
let dispose: () => void;
|
||||
let uninstall: () => void;
|
||||
const doc = "total + order.items";
|
||||
function session(adapter = "debugpy", status: "paused" | "running" | "terminated" = "paused") {
|
||||
adoptSession(7, { adapter });
|
||||
useDebug.setState((s) => ({ sessions: { 7: { ...s.sessions[7]!, status, selectedFrame: 10 } }, active: 7 }));
|
||||
publishContext();
|
||||
}
|
||||
function click() {
|
||||
const e = new MouseEvent("mousedown", { button: 0, altKey: true, bubbles: true, cancelable: true });
|
||||
view.contentDOM.dispatchEvent(e);
|
||||
return e;
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useDebug.setState({ sessions: {}, active: null });
|
||||
setContext("textEditorActive", true);
|
||||
usePlatform.setState({ os: "macos" });
|
||||
useKeymap.setState({ preset: "intellij-macos", overrides: {} });
|
||||
dispose = activate();
|
||||
uninstall = installKeyboardDispatcher(window);
|
||||
view = new EditorView({
|
||||
state: EditorState.create({ doc, extensions: [navigationExtension(), quickEvaluateMouse()] }),
|
||||
parent: document.body,
|
||||
});
|
||||
editor.current = { view, ctx: { path: "/p/app.py" } };
|
||||
vi.spyOn(view, "posAtCoords").mockReturnValue(16);
|
||||
});
|
||||
afterEach(() => {
|
||||
uninstall();
|
||||
dispose();
|
||||
view.destroy();
|
||||
editor.current = null;
|
||||
document.body.replaceChildren();
|
||||
useDebug.setState({ sessions: {}, active: null });
|
||||
publishContext();
|
||||
useKeymap.setState({ overrides: {} });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("paused editor evaluation", () => {
|
||||
it.each(["java-debug", "js-debug", "debugpy"])("evaluates the clicked member through %s", async (adapter) => {
|
||||
session(adapter);
|
||||
view.dispatch({ selection: { anchor: 0, head: 5 } });
|
||||
expect(click().defaultPrevented).toBe(true);
|
||||
await vi.waitFor(() => expect(document.querySelector(".cm-lsp-hint")?.textContent).toContain("order.items = 42"));
|
||||
expect(ipc.debugEvaluateIn).toHaveBeenCalledExactlyOnceWith(7, 10, "order.items", "hover");
|
||||
expect(view.state.selection.main.head).toBe(16);
|
||||
});
|
||||
|
||||
it.each(["intellij-macos", "intellij-windows"])("preserves a clicked selection with %s", async (preset) => {
|
||||
const showHint = vi.spyOn(hints, "showHint");
|
||||
useKeymap.setState({ preset });
|
||||
session();
|
||||
view.dispatch({ selection: { anchor: 0, head: doc.length } });
|
||||
click();
|
||||
await vi.waitFor(() => expect(ipc.debugEvaluateIn).toHaveBeenCalledWith(7, 10, doc, "hover"));
|
||||
expect(view.state.selection.main.from).toBe(0);
|
||||
expect(view.state.selection.main.to).toBe(doc.length);
|
||||
await vi.waitFor(() =>
|
||||
expect(showHint).toHaveBeenCalledWith(view, expect.any(Function), { pos: 16, timeout: 60_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a selected expression spanning several lines", async () => {
|
||||
session();
|
||||
const text = "(total +\n order.items)";
|
||||
view.dispatch({ changes: { from: 0, to: doc.length, insert: text }, selection: { anchor: 0, head: text.length } });
|
||||
click();
|
||||
await vi.waitFor(() => expect(ipc.debugEvaluateIn).toHaveBeenCalledWith(7, 10, text, "hover"));
|
||||
});
|
||||
|
||||
it("lets a higher priority custom mouse action win the shared gesture", () => {
|
||||
session();
|
||||
const run = vi.fn();
|
||||
const remove = registerAction({ id: "test.mouseOverride", title: "Custom", run });
|
||||
useKeymap.setState({ overrides: { "test.mouseOverride": ["Alt+Button1"] } });
|
||||
try {
|
||||
click();
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
expect(ipc.debugEvaluateIn).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
remove();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["none", "running", "terminated"] as const)("leaves Option+click to the editor when %s", async (status) => {
|
||||
if (status !== "none") session("debugpy", status);
|
||||
view.dispatch({ selection: { anchor: 0, head: 5 } });
|
||||
const dispatch = vi.spyOn(view, "dispatch");
|
||||
const e = new MouseEvent("mousedown", { button: 0, altKey: true, cancelable: true });
|
||||
expect(quickEvaluateMouseDown(e, view)).toBe(false);
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
expect(ipc.debugEvaluateIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("honours removal of the mouse binding", () => {
|
||||
session();
|
||||
useKeymap.setState({ overrides: { "debug.quickEvaluate": ["Meta+Alt+F8"] } });
|
||||
click();
|
||||
expect(ipc.debugEvaluateIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["none", "running", "paused", "terminated"] as const)(
|
||||
"shows the contributed menu only while paused: %s",
|
||||
async (status) => {
|
||||
if (status !== "none") session("debugpy", status);
|
||||
const items = menuGroupItems("editor", { paths: ["/p/app.py"], text: doc });
|
||||
const evaluate = items.find((item) => item.kind === "action" && item.action === "debug.evaluate");
|
||||
expect(!!evaluate).toBe(status === "paused");
|
||||
if (evaluate) {
|
||||
view.dispatch({ selection: { anchor: 16 } });
|
||||
await executeAction("debug.evaluate");
|
||||
expect(useOverlays.getState().evaluate?.expression).toBe("order.items");
|
||||
}
|
||||
expect(menuGroupItems("editorTab", { paths: ["/p/app.py"] })).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { call, commands, isCancelled, isTauri, nextRequestId, type RunConfig, ty
|
||||
import { downloadJdkActions } from "@/platform/jdk";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { activeEditor } from "@/editor/api";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { maybeEditorService, notifications, showStatusInfo, toolWindowService } from "@/workbench/contributions";
|
||||
import { classFileBreakpointReason } from "@/platform/classFiles";
|
||||
import { canPlaceBreakpoint } from "./breakpoints/languages";
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
import { focusedDebugView } from "./ui/focusedView";
|
||||
import { browseFocusedConsoleHistory } from "./ui/expressionHistory";
|
||||
import { expressionAt, valueHint } from "./editor/valueTooltip";
|
||||
import { mouseEvaluateTarget } from "./editor/quickEvaluate";
|
||||
import { showHint } from "@/editor/lsp/hint";
|
||||
import { pausedLineTargets, startInlineSmartStep } from "./editor/smartStep";
|
||||
import {
|
||||
@@ -157,28 +159,29 @@ function cursor(): { path: string; line: number; selection: string } | null {
|
||||
}
|
||||
|
||||
/** Evaluate's initial text, as in IDEA: the selection, else the expression at the caret. */
|
||||
function evaluateSeed(): string {
|
||||
const ed = activeEditor();
|
||||
if (!ed) return "";
|
||||
const { state } = ed.view;
|
||||
function evaluateSeed(view: EditorView | undefined = activeEditor()?.view, pos?: number): string {
|
||||
if (!view) return "";
|
||||
const { state } = view;
|
||||
const sel = state.selection.main;
|
||||
if (!sel.empty) return state.sliceDoc(sel.from, sel.to);
|
||||
return expressionAt(ed.view, sel.head)?.text ?? "";
|
||||
if (!sel.empty && (pos == null || (sel.from <= pos && pos <= sel.to))) return state.sliceDoc(sel.from, sel.to);
|
||||
return expressionAt(view, pos ?? sel.head)?.text ?? "";
|
||||
}
|
||||
|
||||
async function quickEvaluate() {
|
||||
const view = activeEditor()?.view;
|
||||
async function quickEvaluate(arg?: unknown) {
|
||||
const target = mouseEvaluateTarget(arg);
|
||||
const view = target?.view ?? activeEditor()?.view;
|
||||
if (!view) return;
|
||||
const sel = view.state.selection.main;
|
||||
const expression = evaluateSeed().trim();
|
||||
const pos = target?.pos ?? sel.from;
|
||||
const expression = evaluateSeed(view, target?.pos).trim();
|
||||
if (!expression) return showHint(view, "Nothing to evaluate at the caret");
|
||||
const session = activeSession();
|
||||
try {
|
||||
const value = await evaluate(expression, "hover", session);
|
||||
if (!session) return;
|
||||
showHint(view, () => valueHint(expression, value, session.id), { pos: sel.from, timeout: 60_000 });
|
||||
showHint(view, () => valueHint(expression, value, session.id), { pos, timeout: 60_000 });
|
||||
} catch (e) {
|
||||
showHint(view, e instanceof Error ? e.message : String(e), { pos: sel.from, tone: "error" });
|
||||
showHint(view, e instanceof Error ? e.message : String(e), { pos, tone: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,8 +680,8 @@ export function registerDebugActions(): () => void {
|
||||
id: "debug.quickEvaluate",
|
||||
title: "Quick Evaluate Expression",
|
||||
category: "Debug",
|
||||
when: "debugSessionActive && textEditorActive",
|
||||
run: () => quickEvaluate(),
|
||||
when: "debugSessionPaused && textEditorActive",
|
||||
run: ({ arg }) => quickEvaluate(arg),
|
||||
},
|
||||
{
|
||||
id: "debug.showToolWindow",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { EditorSelection, Prec, type Extension } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { dispatchMouseShortcut } from "@/platform/keymap";
|
||||
|
||||
export function mouseEvaluateTarget(arg: unknown): { view: EditorView; pos: number } | null {
|
||||
if (!arg || typeof arg !== "object" || !("view" in arg) || !("pos" in arg)) return null;
|
||||
return arg.view instanceof EditorView && typeof arg.pos === "number" ? { view: arg.view, pos: arg.pos } : null;
|
||||
}
|
||||
|
||||
/** Preserve a clicked selection and anchor the value hint at the mouse position. */
|
||||
export function quickEvaluateMouse(): Extension {
|
||||
return Prec.highest(EditorView.domEventHandlers({ mousedown: quickEvaluateMouseDown }));
|
||||
}
|
||||
|
||||
export function quickEvaluateMouseDown(e: MouseEvent, view: EditorView): boolean {
|
||||
if (e.button !== 0) return false;
|
||||
const over = e.target instanceof Element ? e.target : null;
|
||||
if (over?.closest(".cm-tooltip, .cm-lsp-inlay") || (over && !over.closest(".cm-content"))) return false;
|
||||
const pos = view.posAtCoords({ x: e.clientX, y: e.clientY }, false);
|
||||
if (pos == null) return false;
|
||||
const handled = dispatchMouseShortcut(
|
||||
e,
|
||||
(id) => {
|
||||
if (id !== "debug.quickEvaluate") return false;
|
||||
const sel = view.state.selection.main;
|
||||
if (sel.empty || pos < sel.from || pos > sel.to) view.dispatch({ selection: EditorSelection.cursor(pos) });
|
||||
view.focus();
|
||||
return true;
|
||||
},
|
||||
{ view, pos },
|
||||
);
|
||||
if (handled) e.preventDefault();
|
||||
return handled;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { lazyComponent } from "@/ui";
|
||||
import { onIdeEvent } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { selectionValueTooltip, valueTooltip } from "./editor/valueTooltip";
|
||||
import { quickEvaluateMouse } from "./editor/quickEvaluate";
|
||||
import { registerEditorContribution } from "@/editor/api";
|
||||
import {
|
||||
registerMenuGroup,
|
||||
@@ -24,7 +25,16 @@ 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";
|
||||
import {
|
||||
activeSession,
|
||||
handleDebugEvent,
|
||||
isLive,
|
||||
loadWatches,
|
||||
publishContext,
|
||||
resetSessions,
|
||||
stopSession,
|
||||
useDebug,
|
||||
} from "./state";
|
||||
import { DebuggerOverlays } from "./ui/Overlays";
|
||||
import { REMOTE_TYPES } from "./remote";
|
||||
|
||||
@@ -53,6 +63,11 @@ export function activate(): () => void {
|
||||
headerContent: lazyComponent(() => import("./ui/DebugToolWindow").then((m) => m.DebugTabs)),
|
||||
}),
|
||||
registerOverlay({ id: "debugger", component: DebuggerOverlays }),
|
||||
registerMenuGroup({
|
||||
id: "debugger.evaluate",
|
||||
menus: ["editor"],
|
||||
items: () => (activeSession()?.status === "paused" ? [{ kind: "action", action: "debug.evaluate" }] : []),
|
||||
}),
|
||||
// Stop (⌘F2) offers the running debug sessions beside run processes.
|
||||
registerMenuGroup({
|
||||
id: "debugger.stop",
|
||||
@@ -71,6 +86,7 @@ export function activate(): () => void {
|
||||
runToCursorInlay(ctx),
|
||||
valueTooltip(),
|
||||
selectionValueTooltip(),
|
||||
quickEvaluateMouse(),
|
||||
smartStepTargets(ctx),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -407,15 +407,20 @@ function runIfEnabled(id: string): boolean {
|
||||
}
|
||||
|
||||
/** Run a configured mouse binding; editors prepare the clicked caret and focus first. */
|
||||
export function dispatchMouseShortcut(e: MouseEvent, prepare?: () => boolean): boolean {
|
||||
export function dispatchMouseShortcut(e: MouseEvent, prepare?: (id: string) => boolean, arg?: unknown): boolean {
|
||||
if (suspended > 0) return false;
|
||||
const stroke = mouseStrokeOf(e);
|
||||
if (!stroke) return false;
|
||||
const ids = effectiveBindings()
|
||||
.filter(([b]) => b === stroke)
|
||||
.map(([, id]) => id);
|
||||
if (!ids.length || (prepare && !prepare())) return false;
|
||||
for (const id of ids.reverse()) if (runIfEnabled(id)) return true;
|
||||
for (const id of ids.reverse()) {
|
||||
const action = getAction(id);
|
||||
if (!action || !isActionKeyEnabled(action)) continue;
|
||||
if (prepare && !prepare(id)) return false;
|
||||
void executeAction(id, arg);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
formatBinding,
|
||||
dispatchMouseShortcut,
|
||||
installKeyboardDispatcher,
|
||||
isMouseBinding,
|
||||
isValidBinding,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
useKeymap,
|
||||
} from "./keymap";
|
||||
import { registerAction } from "./actions";
|
||||
import { setContext } from "./context";
|
||||
|
||||
const noMods = { metaKey: false, ctrlKey: false, altKey: false, shiftKey: false };
|
||||
|
||||
@@ -84,6 +86,23 @@ describe("mouse shortcuts", () => {
|
||||
return events;
|
||||
};
|
||||
|
||||
it("does not prepare the editor for a disabled mouse action", () => {
|
||||
const run = vi.fn();
|
||||
const prepare = vi.fn(() => true);
|
||||
const remove = registerAction({ id: "test.pausedMouse", title: "Evaluate", when: "debugSessionPaused", run });
|
||||
useKeymap.setState({ overrides: { "test.pausedMouse": ["Alt+Button1"] } });
|
||||
setContext("debugSessionPaused", false);
|
||||
const e = new MouseEvent("mousedown", { button: 0, altKey: true, cancelable: true });
|
||||
try {
|
||||
expect(dispatchMouseShortcut(e, prepare)).toBe(false);
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
} finally {
|
||||
remove();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["the editor", () => editor],
|
||||
["another component", () => document.getElementById("tree")!],
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("IDEA keymap parity", () => {
|
||||
["navigate.nextHighlightedUsage", ["Ctrl+Alt+Down"], ["Ctrl+Alt+Down"]],
|
||||
["analyze.inspectFile", ["Alt+Shift+I"], ["Alt+Shift+I"]],
|
||||
["project.copyElement", ["F5"], ["F5"]],
|
||||
["debug.quickEvaluate", ["Meta+Alt+F8"], ["Ctrl+Alt+F8"]],
|
||||
["debug.quickEvaluate", ["Meta+Alt+F8", "Alt+Button1"], ["Ctrl+Alt+F8", "Alt+Button1"]],
|
||||
["run.chooseDebugConfig", ["Ctrl+Alt+D"], ["Alt+Shift+F9"]],
|
||||
["editor.nextSplitter", ["Alt+Tab"], ["Alt+Tab"]],
|
||||
["toolwindow.resizeLeft", ["Ctrl+Alt+Left"], ["Ctrl+Alt+Shift+Left"]],
|
||||
|
||||
@@ -284,7 +284,7 @@ export const intellijMacos: Record<string, string[]> = {
|
||||
"debug.setValue": ["F2"],
|
||||
"debug.jumpToSource": ["F4", "Meta+Down"],
|
||||
"debug.evaluate": ["Alt+F8"],
|
||||
"debug.quickEvaluate": ["Meta+Alt+F8"],
|
||||
"debug.quickEvaluate": ["Meta+Alt+F8", "Alt+Button1"],
|
||||
"debug.browseConsoleHistory": ["Meta+Alt+E"],
|
||||
|
||||
// VCS
|
||||
|
||||
@@ -278,7 +278,7 @@ export const intellijWindows: Record<string, string[]> = {
|
||||
"debug.setValue": ["F2"],
|
||||
"debug.jumpToSource": ["F4"],
|
||||
"debug.evaluate": ["Alt+F8"],
|
||||
"debug.quickEvaluate": ["Ctrl+Alt+F8"],
|
||||
"debug.quickEvaluate": ["Ctrl+Alt+F8", "Alt+Button1"],
|
||||
"debug.browseConsoleHistory": ["Ctrl+Alt+E"],
|
||||
|
||||
// VCS
|
||||
|
||||
Reference in new issue
Block a user