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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-30 10:51:37 +08:00
1 джерело e85ffa2ae8
коміт a7b5758f5a
13 files changed
+385 -87

No files matched your search

+104
Переглянути файл
@@ -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);
});
+12 -6
Переглянути файл
@@ -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,
});
})(),
),
},
+3 -1
Переглянути файл
@@ -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";
}
+38 -2
Переглянути файл
@@ -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);
+123
Переглянути файл
@@ -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);
});
+31 -63
Переглянути файл
@@ -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"
+1 -1
Переглянути файл
@@ -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);
+20 -13
Переглянути файл
@@ -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 => ({
+1
Переглянути файл
@@ -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
Переглянути файл
@@ -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);
+32
Переглянути файл
@@ -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)),
];
}
+2 -1
Переглянути файл
@@ -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);
+14
Переглянути файл
@@ -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: [] }));