дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +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);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { useDebugRunSessions, type DebugRunSession } from "@/workbench/contributions";
|
||||
import { applicationConfig } from "./application";
|
||||
import { activate } from "./index";
|
||||
import { RunWidget } from "./RunWidget";
|
||||
import { resetRuns, useRun, type RunSession } from "./store";
|
||||
import { selectedTarget } from "./sessionTargets";
|
||||
|
||||
const config = (id: string): RunConfig => ({
|
||||
id,
|
||||
name: id,
|
||||
kind: "shell",
|
||||
command: "sleep",
|
||||
args: ["60"],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: false,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
});
|
||||
const app = config("SdCloudOaApplication");
|
||||
const npm = config("sdcloud-oa-vue3: npm run dev");
|
||||
const run = (c: RunConfig, startedAt = 1): RunSession => ({
|
||||
id: c.id,
|
||||
config: c,
|
||||
runId: 1,
|
||||
status: "running",
|
||||
exitCode: null,
|
||||
startedAt,
|
||||
endedAt: null,
|
||||
stdinClosed: false,
|
||||
});
|
||||
const debug = (c: RunConfig, startedAt = 2): DebugRunSession => ({
|
||||
id: 1,
|
||||
configId: c.id,
|
||||
name: c.name,
|
||||
startedAt,
|
||||
paused: false,
|
||||
rerun: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
resume: vi.fn(),
|
||||
});
|
||||
let dispose: () => void;
|
||||
beforeEach(() => {
|
||||
resetRuns();
|
||||
useRun.setState({ configs: [app, npm], selectedId: app.id, loaded: true });
|
||||
useDebugRunSessions.setState({ sessions: [] });
|
||||
setContext("projectOpen", true);
|
||||
dispose = activate();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
dispose();
|
||||
useDebugRunSessions.setState({ sessions: [] });
|
||||
});
|
||||
|
||||
it("reruns and stops the selected configuration among several running sessions", () => {
|
||||
useRun.setState({ sessions: [run(app), run(npm, 3)] });
|
||||
render(<RunWidget />);
|
||||
expect(screen.getByRole("button", { name: `Rerun '${app.name}'` })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: `Stop '${app.name}'` }));
|
||||
expect(useRun.getState().sessions.map((s) => !!s.stopped)).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it("offers Run/Debug for an idle selection and Stop for the other process", () => {
|
||||
useRun.setState({ sessions: [run(npm)] });
|
||||
render(<RunWidget />);
|
||||
expect(screen.getByRole("button", { name: `Run '${app.name}'` })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: `Debug '${app.name}'` })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /Rerun/ })).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: `Stop '${npm.name}'` }));
|
||||
expect(useRun.getState().sessions[0]?.stopped).toBe(true);
|
||||
});
|
||||
|
||||
it("offers a stop list when the selection is idle and several other sessions run", () => {
|
||||
useRun.setState({ sessions: [run(npm), run(config("other"))] });
|
||||
render(<RunWidget />);
|
||||
expect(screen.getByRole("button", { name: "Stop (2 running)" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /Rerun/ })).toBeNull();
|
||||
});
|
||||
|
||||
it("targets the selected debug session while npm runs, through buttons and shortcut actions", async () => {
|
||||
const session = debug(app);
|
||||
useRun.setState({ sessions: [run(npm, 3)] });
|
||||
useDebugRunSessions.setState({ sessions: [session] });
|
||||
render(<RunWidget />);
|
||||
fireEvent.click(screen.getByRole("button", { name: `Rerun '${app.name}'` }));
|
||||
await vi.waitFor(() => expect(session.rerun).toHaveBeenCalledOnce());
|
||||
await executeAction("run.run");
|
||||
expect(session.rerun).toHaveBeenCalledTimes(2);
|
||||
fireEvent.click(screen.getByRole("button", { name: `Stop '${app.name}'` }));
|
||||
await executeAction("run.stop");
|
||||
expect(session.stop).toHaveBeenCalledTimes(2);
|
||||
expect(useRun.getState().sessions[0]?.stopped).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores an unrelated debug session for an idle selection", () => {
|
||||
useDebugRunSessions.setState({ sessions: [debug(npm)] });
|
||||
render(<RunWidget />);
|
||||
expect(screen.getByRole("button", { name: `Run '${app.name}'` })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /Rerun/ })).toBeNull();
|
||||
});
|
||||
|
||||
it.each([1, 3])("prefers the newest Run or Debug session of the same config (run start %s)", (startedAt) => {
|
||||
expect(selectedTarget(app.id, [run(app, startedAt)], [debug(app, 2)])?.debug).toBe(startedAt < 2);
|
||||
});
|
||||
|
||||
it("matches a detected Spring configuration after its object and id are regenerated", () => {
|
||||
const detect = () =>
|
||||
applicationConfig(
|
||||
{ className: "com.example.SdCloudOaApplication", spring: true },
|
||||
"src/main/java/com/example/SdCloudOaApplication.java",
|
||||
);
|
||||
const original = detect();
|
||||
const regenerated = detect();
|
||||
expect(regenerated).not.toBe(original);
|
||||
expect(regenerated.id).toBe(original.id);
|
||||
expect(selectedTarget(regenerated.id, [run(original), run(npm)], [])?.name).toBe(original.name);
|
||||
});
|
||||
@@ -2,13 +2,13 @@ import { useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { BarButton, Icon, IconButton, Menu, type MenuItem } from "@/ui";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { useContextKeys } from "@/platform/context";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { runExecutors, useRunExecutors } from "@/workbench/contributions";
|
||||
import { runExecutors, useRunExecutors, useDebugRunSessions } from "@/workbench/contributions";
|
||||
import { configIcon } from "./configs";
|
||||
import { useRemoteLabel } from "./remote";
|
||||
import { CURRENT_FILE, rerunSession, sessionsOf, startRun, stopSession, useRun } from "./store";
|
||||
import { CURRENT_FILE, useRun } from "./store";
|
||||
import { compoundTargets, selectedTarget } from "./sessionTargets";
|
||||
import { isDebugOnly } from "./types";
|
||||
import styles from "./Run.module.css";
|
||||
|
||||
@@ -25,15 +25,15 @@ export function RunWidget() {
|
||||
// Remote JVM Debug and the like: only Debug applies, as in IDEA.
|
||||
const debugOnly = !!selected && isDebugOnly(selected);
|
||||
const running = sessions.filter((s) => s.status === "running" || s.status === "starting");
|
||||
// A compound is running while any of its configurations is; Stop stops them all.
|
||||
const compoundRunning = selected?.kind === "compound" ? sessionsOf(selected, running) : [];
|
||||
const runningSelected = compoundRunning[0] ?? running.find((s) => s.config.id === selectedId) ?? null;
|
||||
const target = runningSelected ?? running[running.length - 1] ?? null;
|
||||
// Debug sessions belong to the debugger feature; like IDEA the widget offers their Rerun / Stop.
|
||||
const debugging = useContextKeys((c) => !!c.debugSessionActive);
|
||||
const canRerunDebug = useContextKeys((c) => !!c.debugCanRerun);
|
||||
const debugPaused = useContextKeys((c) => !!c.debugSessionPaused);
|
||||
const active = !!runningSelected || debugging;
|
||||
const debug = useDebugRunSessions((s) => s.sessions);
|
||||
const compoundRunning = compoundTargets(selected, running, debug);
|
||||
const target = selectedTarget(selectedId, running, debug);
|
||||
const debugging = !!target?.debug;
|
||||
const active = !!target || compoundRunning.length > 0;
|
||||
const count = running.length + debug.length;
|
||||
const stopName = compoundRunning.length
|
||||
? selected?.name
|
||||
: (target?.name ?? running[0]?.config.name ?? debug[0]?.name);
|
||||
const moreRef = useRef<HTMLButtonElement>(null);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const executors = useRunExecutors((s) => s.executors);
|
||||
@@ -80,43 +80,9 @@ export function RunWidget() {
|
||||
<Icon name="chevron-down" size={14} className={styles.widgetChevron} />
|
||||
</BarButton>
|
||||
<span className={styles.widgetButtons}>
|
||||
{target ? (
|
||||
{active ? (
|
||||
<>
|
||||
<IconButton
|
||||
icon="rerun-filled"
|
||||
size="large"
|
||||
iconSize={16}
|
||||
className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledRun)}
|
||||
title={tx`Rerun '${compoundRunning.length && selected ? selected.name : target.config.name}'`}
|
||||
shortcut={shortcutLabel("run.run")}
|
||||
onClick={() => (compoundRunning.length && selected ? void startRun(selected) : rerunSession(target.id))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="stop-filled"
|
||||
size="large"
|
||||
iconSize={16}
|
||||
className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledStop)}
|
||||
title={
|
||||
compoundRunning.length && selected
|
||||
? tx`Stop '${selected.name}'`
|
||||
: running.length > 1
|
||||
? tx`Stop (${running.length} running)`
|
||||
: tx`Stop '${target.config.name}'`
|
||||
}
|
||||
shortcut={shortcutLabel("run.stop")}
|
||||
onClick={() =>
|
||||
compoundRunning.length
|
||||
? compoundRunning.forEach((s) => void stopSession(s.id))
|
||||
: running.length > 1
|
||||
? void executeAction("run.stop")
|
||||
: void stopSession(target.id)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : debugging ? (
|
||||
// IDEA while debugging: Resume (when paused), Rerun Debug and Stop as filled buttons.
|
||||
<>
|
||||
{debugPaused && (
|
||||
{target?.paused && (
|
||||
<IconButton
|
||||
icon="resume"
|
||||
size="large"
|
||||
@@ -124,27 +90,18 @@ export function RunWidget() {
|
||||
className={styles.widgetButton}
|
||||
title="Resume Program"
|
||||
shortcut={shortcutLabel("debug.resume")}
|
||||
onClick={() => void executeAction("debug.resume")}
|
||||
onClick={() => target.resume()}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon="restart-debug-filled"
|
||||
icon={debugging ? "restart-debug-filled" : "rerun-filled"}
|
||||
size="large"
|
||||
iconSize={16}
|
||||
className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledRun)}
|
||||
title="Rerun Debug Session"
|
||||
shortcut={shortcutLabel("debug.rerun")}
|
||||
disabled={!canRerunDebug}
|
||||
onClick={() => void executeAction("debug.rerun")}
|
||||
/>
|
||||
<IconButton
|
||||
icon="stop-filled"
|
||||
size="large"
|
||||
iconSize={16}
|
||||
className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledStop)}
|
||||
title="Stop Debug Session"
|
||||
shortcut={shortcutLabel("debug.stop")}
|
||||
onClick={() => void executeAction("debug.stop")}
|
||||
title={tx`Rerun '${selected?.name ?? target?.name ?? ""}'`}
|
||||
shortcut={shortcutLabel("run.run")}
|
||||
disabled={!!target && !target.rerun}
|
||||
onClick={() => void executeAction("run.run")}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -179,6 +136,17 @@ export function RunWidget() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{count > 0 && (
|
||||
<IconButton
|
||||
icon="stop-filled"
|
||||
size="large"
|
||||
iconSize={16}
|
||||
className={clsx(styles.widgetButton, styles.widgetFilled, styles.widgetFilledStop)}
|
||||
title={active || count === 1 ? tx`Stop '${stopName ?? ""}'` : tx`Stop (${count} running)`}
|
||||
shortcut={shortcutLabel("run.stop")}
|
||||
onClick={() => void executeAction("run.stop")}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
ref={moreRef}
|
||||
icon="more"
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function launchProduced(p: Produced, debug: boolean): Promise<void>
|
||||
return;
|
||||
}
|
||||
if (!(await buildFirst(p.debug.build))) return;
|
||||
await executeAction("debug.startConfig", p.debug.config);
|
||||
await executeAction("debug.startConfig", { config: p.debug.config, origin: config });
|
||||
return;
|
||||
}
|
||||
await executeAction("run.debugConfig", config);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { onWorkflowSettingsChange } from "@/platform/workflowSettings";
|
||||
import { showContextMenu, lazyComponent, type MenuItem } from "@/ui";
|
||||
import {
|
||||
menuGroupItems,
|
||||
useDebugRunSessions,
|
||||
notifications,
|
||||
registerMenuGroup,
|
||||
registerOverlay,
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
CURRENT_FILE,
|
||||
currentFileSelected,
|
||||
prepareLaunch,
|
||||
sessionsOf,
|
||||
setBeforeLaunchRunner,
|
||||
loadConfigs,
|
||||
failedTests,
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
stopSession,
|
||||
useRun,
|
||||
} from "./store";
|
||||
import { compoundTargets, selectedTarget } from "./sessionTargets";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
function hasProject() {
|
||||
@@ -84,6 +85,11 @@ async function runSelected() {
|
||||
useRun.setState({ chooserOpen: true, chooserMode: "run" });
|
||||
return;
|
||||
}
|
||||
const target = selectedTarget(config.id, useRun.getState().sessions, useDebugRunSessions.getState().sessions);
|
||||
if (target) {
|
||||
target.rerun?.();
|
||||
return;
|
||||
}
|
||||
await startRun(config);
|
||||
}
|
||||
|
||||
@@ -172,9 +178,8 @@ function configFromArg(arg: unknown): RunConfig | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* IDEA's Stop: in the Run or Debug tool window, the selected tab's process; otherwise the
|
||||
* only running process, or a chooser of every running one (run sessions and the
|
||||
* `stopProcess` items other features contribute, e.g. debug sessions).
|
||||
* Stop: in the Run or Debug tool window, the selected tab's process; otherwise the selected
|
||||
* configuration's session, or a chooser of every running process.
|
||||
*/
|
||||
function stopAction() {
|
||||
const ctx = getContext();
|
||||
@@ -183,22 +188,24 @@ function stopAction() {
|
||||
return;
|
||||
}
|
||||
const running = runningSessions();
|
||||
const { activeSession } = useRun.getState();
|
||||
if (ctx.toolWindowFocus && ctx.activeToolWindow === "run") {
|
||||
const active = running.find((s) => s.id === activeSession);
|
||||
const active = running.find((s) => s.id === useRun.getState().activeSession);
|
||||
if (active) {
|
||||
void stopSession(active.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A selected compound stops all of its configurations.
|
||||
const debug = useDebugRunSessions.getState().sessions;
|
||||
const selected = selectedConfig();
|
||||
if (selected?.kind === "compound") {
|
||||
const own = sessionsOf(selected, running);
|
||||
if (own.length) {
|
||||
own.forEach((s) => void stopSession(s.id));
|
||||
return;
|
||||
}
|
||||
const compound = compoundTargets(selected, running, debug);
|
||||
if (compound.length) {
|
||||
compound.forEach((s) => s.stop());
|
||||
return;
|
||||
}
|
||||
const target = selectedTarget(useRun.getState().selectedId, running, debug);
|
||||
if (target) {
|
||||
target.stop();
|
||||
return;
|
||||
}
|
||||
const items = [
|
||||
...running.map((s): MenuItem => ({
|
||||
|
||||
@@ -77,6 +77,7 @@ export async function debugWithAgent(config: RunConfig): Promise<boolean> {
|
||||
hostName: "127.0.0.1",
|
||||
port,
|
||||
process: runId,
|
||||
runConfig: config,
|
||||
});
|
||||
};
|
||||
const session = await startRun(config, undefined, undefined, {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { executeAction } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import {
|
||||
useDebugRunSessions,
|
||||
provideNotificationService,
|
||||
provideToolWindowService,
|
||||
registerMenuGroup,
|
||||
@@ -51,6 +52,7 @@ const running = (id: string, c: RunConfig, runId: number | null = null): RunSess
|
||||
|
||||
beforeEach(() => {
|
||||
resetRuns();
|
||||
useDebugRunSessions.setState({ sessions: [] });
|
||||
confirm.mockClear();
|
||||
confirm.mockResolvedValue(true);
|
||||
provideToolWindowService({ show: vi.fn(), isVisible: () => true } as unknown as ToolWindowService);
|
||||
@@ -153,6 +155,8 @@ describe("Stop", () => {
|
||||
setContext("projectOpen", true);
|
||||
useRun.setState({
|
||||
sessions: [running("run1", config("a"), 1), running("run2", config("b"), 2)],
|
||||
configs: [config("a"), config("b")],
|
||||
selectedId: "a",
|
||||
activeSession: "run2",
|
||||
});
|
||||
setContext("toolWindowFocus", true);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { compoundChildren } from "./configs";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import type { DebugRunSession } from "@/workbench/contributions";
|
||||
import { rerunSession, sessionsOf, stopSession, useRun, type RunSession } from "./store";
|
||||
|
||||
/** Selection, never tool-window focus or an unrelated process, determines the primary target. */
|
||||
export function selectedTarget(selectedId: string | null, runs: RunSession[], debug: DebugRunSession[]) {
|
||||
const candidates = [
|
||||
...runs
|
||||
.filter((s) => (s.status === "running" || s.status === "starting") && s.config.id === selectedId)
|
||||
.map((s) => ({
|
||||
name: s.config.name,
|
||||
startedAt: s.startedAt,
|
||||
debug: false,
|
||||
paused: false,
|
||||
rerun: () => rerunSession(s.id),
|
||||
stop: () => void stopSession(s.id),
|
||||
resume: () => {},
|
||||
})),
|
||||
...debug.filter((s) => s.configId != null && s.configId === selectedId).map((s) => ({ ...s, debug: true })),
|
||||
];
|
||||
return candidates.sort((a, b) => b.startedAt - a.startedAt)[0] ?? null;
|
||||
}
|
||||
|
||||
export function compoundTargets(config: RunConfig | undefined, runs: RunSession[], debug: DebugRunSession[]) {
|
||||
if (config?.kind !== "compound") return [];
|
||||
const ids = new Set(compoundChildren(config, useRun.getState().configs).map((c) => c.id));
|
||||
return [
|
||||
...sessionsOf(config, runs).map((s) => ({ stop: () => void stopSession(s.id) })),
|
||||
...debug.filter((s) => s.configId != null && ids.has(s.configId)),
|
||||
];
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { downloadJdkActions } from "@/platform/jdk";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
|
||||
import { maybeEditorService, notifications, toolWindowService, useDebugRunSessions } from "@/workbench/contributions";
|
||||
import { ConsoleModel } from "./console";
|
||||
import { jvmFrameTarget } from "./links";
|
||||
import { confirmRerun, confirmTerminate } from "./dialogs";
|
||||
@@ -261,6 +261,7 @@ export function adoptTemporary(config: RunConfig): RunConfig {
|
||||
const busy = new Set(
|
||||
state.sessions.filter((x) => x.status === "running" || x.status === "starting").map((x) => x.config.id),
|
||||
);
|
||||
for (const session of useDebugRunSessions.getState().sessions) if (session.configId) busy.add(session.configId);
|
||||
while (temporary.length > MAX_TEMPORARY) {
|
||||
const evict = temporary.find((id) => id !== config.id && !busy.has(id)) ?? temporary[0]!;
|
||||
temporary = temporary.filter((id) => id !== evict);
|
||||
|
||||
@@ -1256,3 +1256,17 @@ export function showStatusInfo(text: string, ms = 8000) {
|
||||
if (useStatusInfo.getState().seq === seq) useStatusInfo.setState({ text: null });
|
||||
}, ms);
|
||||
}
|
||||
|
||||
/** Live debugger roots exposed to the run toolbar without coupling feature stores. */
|
||||
export interface DebugRunSession {
|
||||
id: number;
|
||||
configId: string | null;
|
||||
name: string;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
rerun: (() => void) | null;
|
||||
stop: () => void;
|
||||
resume: () => void;
|
||||
}
|
||||
|
||||
export const useDebugRunSessions = create<{ sessions: DebugRunSession[] }>(() => ({ sessions: [] }));
|
||||
Reference in new issue
Заблокувати користувача