дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(run): target the selected configuration's run or debug session from the toolbar (#326)
With several sessions running, the toolbar's Rerun and Stop could act on a different configuration than the selected one: when the selected configuration had no matching run session, the widget fell back to the most recently started session of another configuration; and debug sessions did not record the configuration they were started from, so a configuration being debugged never matched and a running npm session took over the buttons. The widget, Run (Shift+F10) and Stop (Cmd+F2) now target the selected configuration's session, whether it was started with Run or Debug (the most recent when both exist); Rerun of a debug session reruns it in debug. When the selected configuration is not running, the widget offers Run / Debug for it, and Stop remains available for the other running sessions (with a chooser when several are running). Debug sessions keep their originating run configuration. In a focused Run or Debug tool window, Stop still stops the selected tab's process. Fixes #318
Цей коміт міститься в:
1 джерело
e85ffa2ae8
коміт
a7b5758f5a
13 files changed
+385
-87
No files matched your search
@@ -0,0 +1,104 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import {
|
||||
provideNotificationService,
|
||||
provideToolWindowService,
|
||||
useDebugRunSessions,
|
||||
type ToolWindowService,
|
||||
} from "@/workbench/contributions";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { registerDebugActions } from "../actions";
|
||||
import { adoptSession, publishContext, rerunSession, useDebug } from "../state";
|
||||
|
||||
const ipc = vi.hoisted(() => ({
|
||||
javaDebugServer: vi.fn(async () => ({ status: "ok", data: 5005 })),
|
||||
debugStart: vi.fn(async () => ({ status: "ok", data: 42 })),
|
||||
debugStop: vi.fn(async () => ({ status: "ok", data: null })),
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } };
|
||||
});
|
||||
const config: RunConfig = {
|
||||
id: "node:server",
|
||||
name: "server",
|
||||
kind: "node",
|
||||
command: "node",
|
||||
args: ["server.js"],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: "js-debug",
|
||||
};
|
||||
beforeEach(() => {
|
||||
useDebug.setState({ sessions: {}, active: null, console: {} });
|
||||
useDebugRunSessions.setState({ sessions: [] });
|
||||
useProject.setState({ project: { root: "/p" } as never });
|
||||
provideNotificationService({ notify: vi.fn() });
|
||||
provideToolWindowService({ show: vi.fn() } as unknown as ToolWindowService);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("keeps the RunConfig through debug.startConfig and reruns through its debug preparation", async () => {
|
||||
const dispose = registerDebugActions();
|
||||
const restart = vi.fn();
|
||||
const unregister = registerActions([{ id: "run.debugConfig", title: "Debug", run: ({ arg }) => restart(arg) }]);
|
||||
try {
|
||||
await executeAction("debug.startConfig", config);
|
||||
const session = useDebug.getState().sessions[42]!;
|
||||
expect(session.runConfig).toEqual(config);
|
||||
expect(session.startedAt).toBeGreaterThan(0);
|
||||
expect(useDebugRunSessions.getState().sessions[0]?.configId).toBe(config.id);
|
||||
await rerunSession(42);
|
||||
expect(ipc.debugStop).toHaveBeenCalledWith(42);
|
||||
expect(restart).toHaveBeenCalledWith(config);
|
||||
} finally {
|
||||
dispose();
|
||||
unregister();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves the source configuration for a separately built debug binary", async () => {
|
||||
const dispose = registerDebugActions();
|
||||
try {
|
||||
await executeAction("debug.startConfig", { config, origin: { ...config, id: "source:server" } });
|
||||
expect(useDebug.getState().sessions[42]?.runConfig?.id).toBe("source:server");
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the originating JVM configuration when attaching to its build process", async () => {
|
||||
const dispose = registerDebugActions();
|
||||
try {
|
||||
await executeAction("debug.attach", {
|
||||
name: "server",
|
||||
module: "",
|
||||
hostName: "127.0.0.1",
|
||||
port: 5005,
|
||||
process: 7,
|
||||
runConfig: config,
|
||||
});
|
||||
expect(useDebug.getState().sessions[42]).toMatchObject({ runConfig: config, process: 7, launch: null });
|
||||
expect(useDebugRunSessions.getState().sessions[0]?.rerun).toBeTypeOf("function");
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes a paused child through its originating root rather than the active unrelated session", () => {
|
||||
adoptSession(1, { runConfig: config });
|
||||
adoptSession(2, { parent: 1, status: "paused", stoppedThread: 1 });
|
||||
adoptSession(3, { runConfig: { ...config, id: "other" } });
|
||||
useDebug.setState({ active: 3 });
|
||||
publishContext();
|
||||
const roots = useDebugRunSessions.getState().sessions;
|
||||
expect(roots.map((s) => [s.configId, s.paused])).toEqual([
|
||||
[config.id, true],
|
||||
["other", false],
|
||||
]);
|
||||
roots[0]!.resume();
|
||||
expect(useDebug.getState().active).toBe(2);
|
||||
});
|
||||
@@ -211,17 +211,19 @@ async function startRemote(config: RunConfig, root: string) {
|
||||
}
|
||||
const launch = await javaAttachConfig({ name: config.name, module: t.module, hostName, port }, root);
|
||||
// A listen relay accepts one debugger: rerunning listens again through the configuration.
|
||||
await startSession(launch, { rerunnable: t.mode === "attach" });
|
||||
await startSession(launch, { runConfig: config, rerunnable: t.mode === "attach" });
|
||||
return;
|
||||
}
|
||||
const launch = remoteAttachConfig(config, root);
|
||||
if (launch) await startSession(launch);
|
||||
if (launch) await startSession(launch, { runConfig: config });
|
||||
}
|
||||
|
||||
async function startConfig(arg: unknown) {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root) return;
|
||||
let config: RunConfig | null = isRunConfig(arg) ? arg : null;
|
||||
const origin = arg && typeof arg === "object" && "origin" in arg && isRunConfig(arg.origin) ? arg.origin : null;
|
||||
const input = arg && typeof arg === "object" && "config" in arg ? arg.config : arg;
|
||||
let config: RunConfig | null = isRunConfig(input) ? input : null;
|
||||
if (!config) {
|
||||
if (!isTauri()) {
|
||||
notifications().notify({ group: "run", level: "warning", title: "Debugging needs the desktop app" });
|
||||
@@ -256,7 +258,7 @@ async function startConfig(arg: unknown) {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
.catch(() => undefined);
|
||||
await startSession(await javaLaunchConfig(config, root));
|
||||
await startSession(await javaLaunchConfig(config, root), { runConfig: origin ?? config });
|
||||
return;
|
||||
}
|
||||
const launch = launchConfigFor(config, root);
|
||||
@@ -272,7 +274,7 @@ async function startConfig(arg: unknown) {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
.catch(() => undefined);
|
||||
await startSession(launch);
|
||||
await startSession(launch, { runConfig: origin ?? config });
|
||||
}
|
||||
|
||||
export function registerDebugActions(): () => void {
|
||||
@@ -312,7 +314,11 @@ export function registerDebugActions(): () => void {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root || !isJavaAttachTarget(arg)) return;
|
||||
const launch = await javaAttachConfig(arg, root);
|
||||
await startSession(launch, { process: (arg.process ?? null) as RunId | null, rerunnable: false });
|
||||
await startSession(launch, {
|
||||
process: (arg.process ?? null) as RunId | null,
|
||||
runConfig: arg.runConfig,
|
||||
rerunnable: false,
|
||||
});
|
||||
})(),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -102,7 +102,9 @@ export interface JavaAttachTarget {
|
||||
port: number;
|
||||
}
|
||||
|
||||
export function isJavaAttachTarget(arg: unknown): arg is JavaAttachTarget & { process?: number | null } {
|
||||
export function isJavaAttachTarget(
|
||||
arg: unknown,
|
||||
): arg is JavaAttachTarget & { process?: number | null; runConfig?: RunConfig } {
|
||||
return !!arg && typeof arg === "object" && "port" in arg && typeof (arg as { port: unknown }).port === "number";
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,12 @@ import {
|
||||
type DebugSessionId,
|
||||
type LaunchConfig,
|
||||
type RunId,
|
||||
type RunConfig,
|
||||
type StackFrame,
|
||||
type ThreadInfo,
|
||||
type Variable,
|
||||
} from "@/platform/ipc";
|
||||
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
|
||||
import { maybeEditorService, notifications, toolWindowService, useDebugRunSessions } from "@/workbench/contributions";
|
||||
import { breakpointsOf, consumeTemporary, refreshStates, resetStatuses } from "./breakpoints/store";
|
||||
import { interpolateLog, logParts, suspends } from "./breakpoints/model";
|
||||
import { syncSpecial, useSpecialBreakpoints } from "./breakpoints/special";
|
||||
@@ -32,6 +33,9 @@ export type SessionStatus = "starting" | "running" | "paused" | "terminated";
|
||||
export interface DebugSession {
|
||||
id: DebugSessionId;
|
||||
name: string;
|
||||
/** Originating configuration, retained for toolbar targeting and prepared debug reruns. */
|
||||
runConfig?: RunConfig;
|
||||
startedAt?: number;
|
||||
adapter: string;
|
||||
parent: DebugSessionId | null;
|
||||
/** Launch configuration (root sessions), used by Rerun. */
|
||||
@@ -136,6 +140,7 @@ function blank(id: DebugSessionId, p: Partial<DebugSession> = {}): DebugSession
|
||||
return {
|
||||
id,
|
||||
name: "Debug",
|
||||
startedAt: Date.now(),
|
||||
adapter: "",
|
||||
parent: null,
|
||||
launch: null,
|
||||
@@ -211,10 +216,32 @@ function settleStart(id: DebugSessionId | null) {
|
||||
|
||||
export function publishContext() {
|
||||
const s = activeSession();
|
||||
useDebugRunSessions.setState({
|
||||
sessions: Object.values(useDebug.getState().sessions)
|
||||
.filter((session) => session.parent == null && isLive(session))
|
||||
.map((session) => {
|
||||
const paused = Object.values(useDebug.getState().sessions).find(
|
||||
(child) => child.status === "paused" && rootOf(child).id === session.id,
|
||||
);
|
||||
return {
|
||||
id: session.id,
|
||||
configId: session.runConfig?.id ?? null,
|
||||
name: session.runConfig?.name ?? session.name,
|
||||
startedAt: session.startedAt ?? 0,
|
||||
paused: !!paused,
|
||||
rerun: session.runConfig || session.launch ? () => void rerunSession(session.id) : null,
|
||||
stop: () => void stopSession(session.id),
|
||||
resume: () => {
|
||||
selectSession(paused?.id ?? session.id);
|
||||
void executeAction("debug.resume");
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
const anyLive = Object.values(useDebug.getState().sessions).some(isLive);
|
||||
setContext("debugSessionActive", anyLive);
|
||||
setContext("debugSessionPaused", s?.status === "paused");
|
||||
setContext("debugCanRerun", !!s && !!rootOf(s).launch);
|
||||
setContext("debugCanRerun", !!s && !!(rootOf(s).runConfig || rootOf(s).launch));
|
||||
setContext(
|
||||
"debugCanHotSwap",
|
||||
Object.values(useDebug.getState().sessions).some((x) => isLive(x) && !!x.caps?.hotCodeReplace),
|
||||
@@ -253,6 +280,7 @@ export function clearConsole(session: DebugSessionId) {
|
||||
export async function startSession(
|
||||
launch: LaunchConfig,
|
||||
options: {
|
||||
runConfig?: RunConfig;
|
||||
/** A process started for this session (e.g. Maven with a JDWP agent): Stop ends it too. */
|
||||
process?: RunId | null;
|
||||
/** Rerun repeats the launch (false for attaching to a process that is gone afterwards). */
|
||||
@@ -275,6 +303,7 @@ export async function startSession(
|
||||
} catch {
|
||||
// Workbench not ready (tests).
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
pendingStarts++;
|
||||
try {
|
||||
const id = await call(commands.debugStart(launch));
|
||||
@@ -287,6 +316,8 @@ export async function startSession(
|
||||
[id]: {
|
||||
...cur,
|
||||
name: launch.name,
|
||||
runConfig: options.runConfig,
|
||||
startedAt,
|
||||
adapter: launch.adapter,
|
||||
launch: options.rerunnable === false ? null : launch,
|
||||
process: options.process ?? null,
|
||||
@@ -353,6 +384,11 @@ export async function rerunSession(id: DebugSessionId) {
|
||||
const s = useDebug.getState().sessions[id];
|
||||
if (!s) return;
|
||||
const root = rootOf(s);
|
||||
if (root.runConfig) {
|
||||
if (isLive(root)) await stopSession(root.id);
|
||||
await executeAction("run.debugConfig", root.runConfig);
|
||||
return;
|
||||
}
|
||||
const launch = root.launch;
|
||||
if (!launch) return;
|
||||
if (isLive(root)) await stopSession(root.id);
|
||||
|
||||
Reference in new issue
Заблокувати користувача