mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat(debugger): flash the taskbar when a breakpoint is hit in an unfocused window (#1810)
A breakpoint stop now grabs attention like IDEA's: the owning window's taskbar button flashes while the window is in the background (new window_flash_taskbar command wrapping request_user_attention, which never flashes a focused window), and the flash is stopped when the window is focused, the paused thread resumes, or the session ends. A window running with Focus application on breakpoint still focuses instead of flashing, and repeated hits while already flashing do not re-request attention. Closes #1742 Signed-off-by: Inf1mum <[email protected]>
This commit is contained in:
1 parent
1f969694a0
commit
fb694aaceb
6 files changed
+191
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const ipc = vi.hoisted(() => {
|
||||
const ok = <T>(data: T) => Promise.resolve({ status: "ok" as const, data });
|
||||
return {
|
||||
ok,
|
||||
debugCapabilities: vi.fn(async () => ok(null)),
|
||||
debugThreads: vi.fn(async () => ok([{ id: 1, name: "main" }])),
|
||||
debugStackTrace: vi.fn(async () => ok([])),
|
||||
windowFlashTaskbar: vi.fn(async (_flash: boolean) => ok(null)),
|
||||
};
|
||||
});
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
const { ok: _ok, ...commands } = ipc;
|
||||
return { ...real, commands: { ...real.commands, ...commands } };
|
||||
});
|
||||
|
||||
import { useProject } from "@/platform/project";
|
||||
import { adoptSession, handleDebugEvent, useDebug } from "../state";
|
||||
import { stopTaskbarFlash } from "../taskbarFlash";
|
||||
|
||||
const stopped = (reason: string) =>
|
||||
handleDebugEvent(1, { kind: "stopped", threadId: 1, reason, description: null, allThreadsStopped: false });
|
||||
const flashes = () => ipc.windowFlashTaskbar.mock.calls.filter(([flash]) => flash).length;
|
||||
const stops = () => ipc.windowFlashTaskbar.mock.calls.filter(([flash]) => !flash).length;
|
||||
const hasFocus = vi.spyOn(document, "hasFocus");
|
||||
|
||||
beforeAll(() => {
|
||||
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ = {};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Stop any flash left over from the previous test before the mock is cleared.
|
||||
stopTaskbarFlash();
|
||||
vi.clearAllMocks();
|
||||
useDebug.setState({ sessions: {}, active: null, stopSeq: 0 });
|
||||
useProject.setState({ settings: {} as never });
|
||||
adoptSession(1, { adapter: "java-debug" });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
|
||||
hasFocus.mockRestore();
|
||||
useProject.setState({ settings: null });
|
||||
});
|
||||
|
||||
describe("taskbar flash on a breakpoint in a background window", () => {
|
||||
it("flashes when a breakpoint stops an unfocused window", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
expect(flashes()).toBe(1);
|
||||
expect(stops()).toBe(0);
|
||||
});
|
||||
|
||||
it("does not flash when the window is focused", () => {
|
||||
hasFocus.mockReturnValue(true);
|
||||
stopped("breakpoint");
|
||||
expect(ipc.windowFlashTaskbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not flash for a stop that is not a breakpoint", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("step");
|
||||
stopped("exception");
|
||||
expect(ipc.windowFlashTaskbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-request attention for repeated breakpoints while already flashing", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
stopped("breakpoint");
|
||||
stopped("function breakpoint");
|
||||
expect(flashes()).toBe(1);
|
||||
expect(stops()).toBe(0);
|
||||
});
|
||||
|
||||
it("stops the flash when the window is focused", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
hasFocus.mockReturnValue(true);
|
||||
window.dispatchEvent(new Event("focus"));
|
||||
expect(flashes()).toBe(1);
|
||||
expect(stops()).toBe(1);
|
||||
// Further focus events send no more stops: the flash is forgotten. A breakpoint that
|
||||
// hits afterwards finds the window focused and does not flash again (criterion 3).
|
||||
window.dispatchEvent(new Event("focus"));
|
||||
stopped("breakpoint");
|
||||
expect(stops()).toBe(1);
|
||||
expect(flashes()).toBe(1);
|
||||
expect(ipc.windowFlashTaskbar).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("stops the flash when the session resumes", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
handleDebugEvent(1, { kind: "continued", threadId: 1, allThreadsContinued: true });
|
||||
expect(stops()).toBe(1);
|
||||
});
|
||||
|
||||
it("stops the flash when the session terminates", () => {
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
handleDebugEvent(1, { kind: "terminated" });
|
||||
expect(stops()).toBe(1);
|
||||
});
|
||||
|
||||
it("does not flash when Focus application on breakpoint takes the window instead", () => {
|
||||
useProject.setState({ settings: { debugger: { focusAppOnBreakpoint: true } } as never });
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
expect(ipc.windowFlashTaskbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("never touches the shell outside Tauri", () => {
|
||||
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
|
||||
hasFocus.mockReturnValue(false);
|
||||
stopped("breakpoint");
|
||||
expect(ipc.windowFlashTaskbar).not.toHaveBeenCalled();
|
||||
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ = {};
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,7 @@ import { breakpointsOf, consumeTemporary, flushBreakpoints, refreshStates, reset
|
||||
import { interpolateLog, logParts, suspends } from "./breakpoints/model";
|
||||
import { syncSpecial, useSpecialBreakpoints } from "./breakpoints/special";
|
||||
import { refreshExceptionFilters } from "./breakpoints/exceptions";
|
||||
import { flashTaskbarForBreakpoint, stopTaskbarFlash } from "./taskbarFlash";
|
||||
import { missingAdapter } from "./missingAdapter";
|
||||
import { startFailureMessage } from "./startFailure";
|
||||
|
||||
@@ -713,6 +714,7 @@ export function sessionSupports(cap: keyof DebugCapabilities, s: DebugSession |
|
||||
}
|
||||
|
||||
function markTerminated(id: DebugSessionId) {
|
||||
stopTaskbarFlash();
|
||||
patch(id, { status: "terminated", endedAt: Date.now(), frames: [], selectedFrame: null, stoppedThread: null });
|
||||
const st = useDebug.getState();
|
||||
if (st.active === id) {
|
||||
@@ -1013,13 +1015,17 @@ export function handleDebugEvent(id: DebugSessionId, event: DebugEvent) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// Attention for a background window: focusing the app and flashing the taskbar are
|
||||
// alternatives (IDEA), so a window configured to grab focus does not also flash.
|
||||
if (dbg.focusAppOnBreakpoint) focusAppWindow();
|
||||
else if (isBp) flashTaskbarForBreakpoint();
|
||||
break;
|
||||
}
|
||||
case "continued": {
|
||||
const cur = useDebug.getState().sessions[id]!;
|
||||
if (cur.status === "terminated") break;
|
||||
if (!event.allThreadsContinued && event.threadId !== (cur.selectedThread ?? cur.stoppedThread)) break;
|
||||
stopTaskbarFlash();
|
||||
patch(id, {
|
||||
status: "running",
|
||||
frames: [],
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* IDEA-style taskbar flash while a breakpoint holds this window in the background:
|
||||
* the shell flashes the native window (`window_flash_taskbar`, which never flashes a
|
||||
* focused window), this side decides when to ask — on a breakpoint stop while the
|
||||
* window is unfocused — and stops the flash when the window is focused again or the
|
||||
* paused session resumes or ends.
|
||||
*/
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
|
||||
/** Whether this window started a flash that nothing has stopped yet. */
|
||||
let flashing = false;
|
||||
/** The reset listener is attached once per page, on the first flash request. */
|
||||
let listening = false;
|
||||
|
||||
/** Stops the flash this window started (no-op when none is running: no extra IPC). */
|
||||
export function stopTaskbarFlash() {
|
||||
if (!flashing) return;
|
||||
flashing = false;
|
||||
if (!isTauri()) return;
|
||||
void call(commands.windowFlashTaskbar(false)).catch(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the shell to flash the taskbar for a breakpoint hit in a background window.
|
||||
* While already flashing, further hits do not re-request attention; a window that
|
||||
* is focused right now (or became focused while the request was in flight) never
|
||||
* flashes.
|
||||
*/
|
||||
export function flashTaskbarForBreakpoint() {
|
||||
if (!isTauri()) return;
|
||||
if (!listening) {
|
||||
listening = true;
|
||||
window.addEventListener("focus", stopTaskbarFlash);
|
||||
}
|
||||
if (flashing || document.hasFocus()) return;
|
||||
flashing = true;
|
||||
void call(commands.windowFlashTaskbar(true)).catch(() => {
|
||||
flashing = false;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user