дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(run): keep service debugging in the Services tool window (#1844)
Closes #1684. Debug and rerun started from the Services tool window used startSession(), which opened the Debug tool window whenever run.showOnStart was on. Services-originated debug sessions and reruns now start with show: false and stay in the Services detail pane (in-place launches and reruns keep that origin; a breakpoint hit still shows the paused session). Debug from the toolbar, gutter, context menu and the Debug window keeps opening the Debug window. Also: palette themes inherit the macOS window button colours, and the title bar's fullscreen buttons skip Tauri calls outside Tauri (fixes the theme-token and title-bar test failures on main). Co-authored-by: xtr-hub <[email protected]>
Цей коміт міститься в:
1 джерело
f407880204
коміт
b790a9ddb6
16 files changed
+314
-92
No files matched your search
@@ -174,6 +174,20 @@ it("keeps the RunConfig through debug.startConfig and reruns through its debug p
|
||||
}
|
||||
});
|
||||
|
||||
it("reruns a debug service in place without showing the Debug tool window", 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);
|
||||
await rerunSession(42, { show: false });
|
||||
expect(restart).toHaveBeenCalledWith({ config, show: false });
|
||||
} finally {
|
||||
dispose();
|
||||
unregister();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves the source configuration for a separately built debug binary", async () => {
|
||||
const dispose = registerDebugActions();
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { addBreakpoint, loadBreakpoints, removeAllBreakpoints, useBreakpoints } from "../breakpoints/store";
|
||||
import { startSession, useDebug } from "../state";
|
||||
import { handleDebugEvent, startPreparedSession, startSession, useDebug } from "../state";
|
||||
import type { LaunchConfig } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import {
|
||||
provideNotificationService,
|
||||
provideToolWindowService,
|
||||
@@ -13,6 +14,8 @@ const ipc = vi.hoisted(() => ({
|
||||
debugStart: vi.fn(async () => ({ status: "ok", data: 42 })),
|
||||
debugCapabilities: vi.fn(async () => ({ status: "ok", data: {} })),
|
||||
debugExceptionFilters: vi.fn(async () => ({ status: "ok", data: [] })),
|
||||
debugThreads: vi.fn(async () => ({ status: "ok", data: [] })),
|
||||
debugStackTrace: vi.fn(async () => ({ status: "ok", data: [] })),
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
@@ -34,11 +37,46 @@ beforeEach(() => {
|
||||
localStorage.clear();
|
||||
useBreakpoints.setState({ root: "/p", byFile: {}, muted: false, status: {} });
|
||||
useDebug.setState({ sessions: {}, active: null, console: {} });
|
||||
useProject.setState({ settings: null });
|
||||
provideNotificationService({ notify: vi.fn() });
|
||||
provideToolWindowService({ show: vi.fn() } as unknown as ToolWindowService);
|
||||
});
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it.each([undefined, true, false])(
|
||||
"respects showOnStart for direct and prepared starts (show: %s)",
|
||||
async (foreground) => {
|
||||
for (const showOnStart of [true, false]) {
|
||||
useProject.setState({ settings: { run: { showOnStart } } as never });
|
||||
for (const prepared of [false, true]) {
|
||||
const show = vi.fn();
|
||||
provideToolWindowService({ show } as unknown as ToolWindowService);
|
||||
if (prepared) {
|
||||
await startPreparedSession({ id: "java", name: "Java" } as never, async () => config("launch"), {
|
||||
show: foreground,
|
||||
});
|
||||
} else await startSession(config("launch"), { show: foreground });
|
||||
if (showOnStart && foreground !== false) expect(show).toHaveBeenCalledWith("debug");
|
||||
else expect(show).not.toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["breakpoint", "function breakpoint", "exception", "pause"])(
|
||||
"reveals an in-place session on suspend (%s)",
|
||||
async (reason) => {
|
||||
const show = vi.fn();
|
||||
provideToolWindowService({ show } as unknown as ToolWindowService);
|
||||
await startSession(config("launch"), { show: false });
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
handleDebugEvent(42, { kind: "stopped", threadId: 1, reason, description: null, allThreadsStopped: true });
|
||||
expect(show).toHaveBeenCalledWith("debug");
|
||||
expect(useDebug.getState().active).toBe(42);
|
||||
expect(useDebug.getState().sessions[42]).toMatchObject({ status: "paused", selectedThread: 1 });
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["launch", "attach"])("awaits every file before %s can send configurationDone", async (request) => {
|
||||
const completed: string[] = [];
|
||||
const release: Array<() => void> = [];
|
||||
|
||||
@@ -236,7 +236,7 @@ async function listenForJvm(name: string, host: string, port: number): Promise<n
|
||||
}
|
||||
|
||||
/** Remote JVM Debug, Go Remote, Python / Node.js remote: attach over host:port. */
|
||||
async function startRemote(config: RunConfig, root: string) {
|
||||
async function startRemote(config: RunConfig, root: string, show?: boolean) {
|
||||
if (config.kind === REMOTE_JVM) {
|
||||
const t = remoteJvmTarget(config);
|
||||
let hostName = t.host;
|
||||
@@ -248,37 +248,42 @@ 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, { runConfig: config, rerunnable: t.mode === "attach" });
|
||||
await startSession(launch, { runConfig: config, rerunnable: t.mode === "attach", show });
|
||||
return;
|
||||
}
|
||||
const launch = remoteAttachConfig(config, root);
|
||||
if (launch) await startSession(launch, { runConfig: config });
|
||||
if (launch) await startSession(launch, { runConfig: config, show });
|
||||
}
|
||||
|
||||
/** Preparation belongs to a session from the moment the action begins. */
|
||||
async function startJavaConfig(config: RunConfig, root: string, origin: RunConfig) {
|
||||
await startPreparedSession(origin, async (signal, report) => {
|
||||
const request = nextRequestId();
|
||||
const cancel = () => {
|
||||
void commands.cancelRequest(request).catch(() => undefined);
|
||||
};
|
||||
signal.addEventListener("abort", cancel, { once: true });
|
||||
try {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
.catch(() => undefined);
|
||||
signal.throwIfAborted();
|
||||
return await javaLaunchConfig(config, root, { request, signal, report });
|
||||
} finally {
|
||||
signal.removeEventListener("abort", cancel);
|
||||
}
|
||||
});
|
||||
async function startJavaConfig(config: RunConfig, root: string, origin: RunConfig, show?: boolean) {
|
||||
await startPreparedSession(
|
||||
origin,
|
||||
async (signal, report) => {
|
||||
const request = nextRequestId();
|
||||
const cancel = () => {
|
||||
void commands.cancelRequest(request).catch(() => undefined);
|
||||
};
|
||||
signal.addEventListener("abort", cancel, { once: true });
|
||||
try {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
.catch(() => undefined);
|
||||
signal.throwIfAborted();
|
||||
return await javaLaunchConfig(config, root, { request, signal, report });
|
||||
} finally {
|
||||
signal.removeEventListener("abort", cancel);
|
||||
}
|
||||
},
|
||||
{ show },
|
||||
);
|
||||
}
|
||||
|
||||
async function startConfig(arg: unknown) {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root) return;
|
||||
const origin = arg && typeof arg === "object" && "origin" in arg && isRunConfig(arg.origin) ? arg.origin : null;
|
||||
const show = arg && typeof arg === "object" && "show" in arg ? (arg as { show?: boolean }).show : undefined;
|
||||
const input = arg && typeof arg === "object" && "config" in arg ? arg.config : arg;
|
||||
let config: RunConfig | null = isRunConfig(input) ? input : null;
|
||||
if (!config) {
|
||||
@@ -303,16 +308,16 @@ async function startConfig(arg: unknown) {
|
||||
config = configs[0]!;
|
||||
// Through Run's debug flow: Maven / Gradle / scripts start the JVM with a JDWP agent there.
|
||||
if (getAction("run.debugConfig")) {
|
||||
await executeAction("run.debugConfig", config);
|
||||
await executeAction("run.debugConfig", show === false ? { config, show: false } : config);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isRemoteConfig(config)) {
|
||||
await startRemote(config, root);
|
||||
await startRemote(config, root, show);
|
||||
return;
|
||||
}
|
||||
if (isJavaDebuggable(config)) {
|
||||
await startJavaConfig(config, root, origin ?? config);
|
||||
await startJavaConfig(config, root, origin ?? config, show);
|
||||
return;
|
||||
}
|
||||
const dotnet = config.kind === "dotnet" && config.debugAdapter === "netcoredbg";
|
||||
@@ -334,7 +339,7 @@ async function startConfig(arg: unknown) {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
.catch(() => undefined);
|
||||
await startSession(launch, { runConfig: origin ?? config });
|
||||
await startSession(launch, { runConfig: origin ?? config, show });
|
||||
await xdebugCheck;
|
||||
}
|
||||
|
||||
@@ -410,6 +415,7 @@ export function registerDebugActions(): () => void {
|
||||
process: (arg.process ?? null) as RunId | null,
|
||||
runConfig: arg.runConfig,
|
||||
rerunnable: false,
|
||||
show: (arg as { show?: boolean }).show,
|
||||
});
|
||||
if (session == null && isPythonAttachTarget(arg) && arg.process != null)
|
||||
await stopAssociatedProcess(arg.process);
|
||||
|
||||
@@ -308,6 +308,8 @@ export function publishContext() {
|
||||
processId: session.process,
|
||||
httpPort: sessionHttpPort(session.id),
|
||||
rerun: session.runConfig || session.launch ? () => void rerunSession(session.id) : null,
|
||||
rerunInPlace:
|
||||
session.runConfig || session.launch ? () => void rerunSession(session.id, { show: false }) : null,
|
||||
stop: (options?: { stopProcess?: boolean }) => stopSession(session.id, options),
|
||||
show: () => {
|
||||
if (!useDebug.getState().sessions[session.id]) return;
|
||||
@@ -497,6 +499,7 @@ const preparations = new Map<DebugSessionId, AbortController>();
|
||||
export async function startPreparedSession(
|
||||
config: RunConfig,
|
||||
prepare: (signal: AbortSignal, report: (text: string, kind?: "system" | "stderr") => void) => Promise<LaunchConfig>,
|
||||
options: { show?: boolean } = {},
|
||||
): Promise<DebugSessionId | null> {
|
||||
const id = --preparationSeq as DebugSessionId;
|
||||
const controller = new AbortController();
|
||||
@@ -511,7 +514,7 @@ export async function startPreparedSession(
|
||||
}));
|
||||
publishContext();
|
||||
try {
|
||||
if (workflowSettings().run.showOnStart) toolWindowService().show("debug");
|
||||
if (options.show !== false && workflowSettings().run.showOnStart) toolWindowService().show("debug");
|
||||
} catch {
|
||||
/* no workbench (tests) */
|
||||
}
|
||||
@@ -522,7 +525,11 @@ export async function startPreparedSession(
|
||||
report(tx`Starting debugger for ${config.name}…` + "\n");
|
||||
const launch = await prepare(controller.signal, report);
|
||||
controller.signal.throwIfAborted();
|
||||
return await startSession(launch, { runConfig: config, preparation: { id, signal: controller.signal } });
|
||||
return await startSession(launch, {
|
||||
runConfig: config,
|
||||
preparation: { id, signal: controller.signal },
|
||||
show: options.show,
|
||||
});
|
||||
} catch (e) {
|
||||
if (controller.signal.aborted || isCancelled(e)) return null;
|
||||
report((e instanceof Error ? e.message : String(e)) + "\n", "stderr");
|
||||
@@ -542,6 +549,8 @@ export async function startSession(
|
||||
process?: RunId | null;
|
||||
/** Rerun repeats the launch (false for attaching to a process that is gone afterwards). */
|
||||
rerunnable?: boolean;
|
||||
/** `false`: keep the current tool window (the Services panel starts debuggers in place). */
|
||||
show?: boolean;
|
||||
} = {},
|
||||
): Promise<DebugSessionId | null> {
|
||||
if (!isTauri()) {
|
||||
@@ -553,7 +562,7 @@ export async function startSession(
|
||||
const previousActive = useDebug.getState().active;
|
||||
if (!afterDetach) dropFinished();
|
||||
try {
|
||||
if (workflowSettings().run.showOnStart) toolWindowService().show("debug");
|
||||
if (options.show !== false && workflowSettings().run.showOnStart) toolWindowService().show("debug");
|
||||
} catch {
|
||||
// Workbench not ready (tests).
|
||||
}
|
||||
@@ -733,19 +742,22 @@ export function removeSession(id: DebugSessionId) {
|
||||
publishContext();
|
||||
}
|
||||
|
||||
export async function rerunSession(id: DebugSessionId) {
|
||||
export async function rerunSession(id: DebugSessionId, options: { show?: boolean } = {}) {
|
||||
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);
|
||||
await executeAction(
|
||||
"run.debugConfig",
|
||||
options.show === false ? { config: root.runConfig, show: false } : root.runConfig,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const launch = root.launch;
|
||||
if (!launch) return;
|
||||
if (isLive(root)) await stopSession(root.id);
|
||||
await startSession(launch);
|
||||
await startSession(launch, { show: options.show });
|
||||
}
|
||||
|
||||
async function loadCapabilities(id: DebugSessionId) {
|
||||
|
||||
@@ -48,22 +48,28 @@ export function taskLabel(task: BeforeLaunchTask, configs: readonly RunConfig[]
|
||||
}
|
||||
|
||||
/** Runs `config` to completion: true when it exited with code 0. */
|
||||
async function runToEnd(config: RunConfig, chain: readonly string[]): Promise<boolean> {
|
||||
async function runToEnd(config: RunConfig, chain: readonly string[], show?: boolean): Promise<boolean> {
|
||||
let done!: (ok: boolean) => void;
|
||||
const exited = new Promise<boolean>((resolve) => (done = resolve));
|
||||
const session = await startRun(config, undefined, undefined, {
|
||||
chain,
|
||||
show,
|
||||
onExit: (code, stopped) => done(code === 0 && !stopped),
|
||||
});
|
||||
if (!session || session.status === "failed") return false;
|
||||
return exited;
|
||||
}
|
||||
|
||||
async function runTask(task: BeforeLaunchTask, config: RunConfig, chain: readonly string[]): Promise<string | null> {
|
||||
async function runTask(
|
||||
task: BeforeLaunchTask,
|
||||
config: RunConfig,
|
||||
chain: readonly string[],
|
||||
show?: boolean,
|
||||
): Promise<string | null> {
|
||||
const target = task.target ?? "";
|
||||
switch (task.kind) {
|
||||
case "dotnetBuild":
|
||||
return (await runToEnd(dotnetBuildConfig(config), chain)) ? null : "Build failed";
|
||||
return (await runToEnd(dotnetBuildConfig(config), chain, show)) ? null : "Build failed";
|
||||
case "build": {
|
||||
if (!getAction(BUILD_ACTION)) return "Building is not available";
|
||||
if (!(await executeAction(BUILD_ACTION, { activate: false }))) return "A build is already running";
|
||||
@@ -73,14 +79,16 @@ async function runTask(task: BeforeLaunchTask, config: RunConfig, chain: readonl
|
||||
if (chain.includes(target)) return "The before launch tasks run in a cycle";
|
||||
const other = useRun.getState().configs.find((c) => c.id === target);
|
||||
if (!other) return `Configuration '${target}' no longer exists`;
|
||||
return (await runToEnd(other, chain)) ? null : `'${other.name}' failed`;
|
||||
return (await runToEnd(other, chain, show)) ? null : `'${other.name}' failed`;
|
||||
}
|
||||
case "tool": {
|
||||
const tool = workflowSettings().externalTools.find((t) => t.id === target);
|
||||
if (!tool) return `External tool '${target}' no longer exists`;
|
||||
const run = await externalToolConfig(tool);
|
||||
if (!run) return `'${tool.name}' was not run`;
|
||||
return (await runToEnd({ ...run, id: `${run.id}:before:${config.id}` }, chain)) ? null : `'${tool.name}' failed`;
|
||||
return (await runToEnd({ ...run, id: `${run.id}:before:${config.id}` }, chain, show))
|
||||
? null
|
||||
: `'${tool.name}' failed`;
|
||||
}
|
||||
case "command": {
|
||||
if (!target.trim()) return "No command";
|
||||
@@ -96,7 +104,7 @@ async function runTask(task: BeforeLaunchTask, config: RunConfig, chain: readonl
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
};
|
||||
return (await runToEnd(run, chain)) ? null : `'${target}' failed`;
|
||||
return (await runToEnd(run, chain, show)) ? null : `'${target}' failed`;
|
||||
}
|
||||
case "toolWindow": {
|
||||
if (!useContributions.getState().toolWindows.has(target)) return `Tool window '${target}' is not available`;
|
||||
@@ -109,11 +117,11 @@ async function runTask(task: BeforeLaunchTask, config: RunConfig, chain: readonl
|
||||
}
|
||||
|
||||
/** The installed runner: every task in order; a failure is reported and aborts. */
|
||||
export async function runBeforeLaunch(config: RunConfig, chain: readonly string[]): Promise<boolean> {
|
||||
export async function runBeforeLaunch(config: RunConfig, chain: readonly string[], show?: boolean): Promise<boolean> {
|
||||
for (const task of config.beforeLaunch ?? []) {
|
||||
let error: string | null;
|
||||
try {
|
||||
error = await runTask(task, config, chain);
|
||||
error = await runTask(task, config, chain, show);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ function reportMissing(p: Produced) {
|
||||
}
|
||||
|
||||
/** Builds with `build` in the Run window; resolves whether it succeeded. */
|
||||
async function buildFirst(build: RunConfig): Promise<boolean> {
|
||||
const session = await startRun(build);
|
||||
async function buildFirst(build: RunConfig, show?: boolean): Promise<boolean> {
|
||||
const session = await startRun(build, undefined, undefined, { show });
|
||||
if (!session) return false;
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
@@ -139,7 +139,7 @@ async function buildFirst(build: RunConfig): Promise<boolean> {
|
||||
}
|
||||
|
||||
/** Runs or debugs a production (adopting it as the selected temporary configuration). */
|
||||
export async function launchProduced(p: Produced, debug: boolean): Promise<void> {
|
||||
export async function launchProduced(p: Produced, debug: boolean, show?: boolean): Promise<void> {
|
||||
if (p.missing || !p.config) {
|
||||
reportMissing(p);
|
||||
return;
|
||||
@@ -160,11 +160,11 @@ export async function launchProduced(p: Produced, debug: boolean): Promise<void>
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!(await buildFirst(p.debug.build))) return;
|
||||
await executeAction("debug.startConfig", { config: p.debug.config, origin: config });
|
||||
if (!(await buildFirst(p.debug.build, show))) return;
|
||||
await executeAction("debug.startConfig", { config: p.debug.config, origin: config, show });
|
||||
return;
|
||||
}
|
||||
await executeAction("run.debugConfig", config);
|
||||
await executeAction("run.debugConfig", show === false ? { config, show: false } : config);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,15 +107,15 @@ async function runSelected() {
|
||||
await startRun(config);
|
||||
}
|
||||
|
||||
async function debugConfig(config: RunConfig, chain: readonly string[] = []) {
|
||||
async function debugConfig(config: RunConfig, chain: readonly string[] = [], show?: boolean) {
|
||||
if (config.kind === "compound") {
|
||||
if (!(await prepareLaunch(config, chain))) return;
|
||||
if (!(await prepareLaunch(config, chain, show))) return;
|
||||
const children = compoundChildren(config, useRun.getState().configs);
|
||||
await Promise.all(children.map((c) => debugConfig(c, [...chain, config.id])));
|
||||
await Promise.all(children.map((c) => debugConfig(c, [...chain, config.id], show)));
|
||||
return;
|
||||
}
|
||||
if (remoteDebugBlocked(config)) return;
|
||||
if (!(await prepareLaunch(config, chain))) return;
|
||||
if (!(await prepareLaunch(config, chain, show))) return;
|
||||
recordRun(config.id);
|
||||
// Python on a Docker / Compose target: debugpy in the container, the debugger attaches.
|
||||
if (containerDebuggable(config) && getAction("debug.attach")) {
|
||||
@@ -124,28 +124,28 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = []) {
|
||||
}
|
||||
// Remote JVM Debug, Go Remote…: the debugger attaches, nothing is launched here.
|
||||
if (isDebugOnly(config) && getAction("debug.startConfig")) {
|
||||
await executeAction("debug.startConfig", config);
|
||||
await executeAction("debug.startConfig", show === false ? { config, show: false } : config);
|
||||
return;
|
||||
}
|
||||
// A single compiled file (rustc / clang): build with debug info, then debug the binary.
|
||||
const compiled = await compiledDebugPlan(config);
|
||||
if (compiled?.debug) {
|
||||
await launchProduced(compiled, true);
|
||||
await launchProduced(compiled, true, show);
|
||||
return;
|
||||
}
|
||||
// "Build and run using: Gradle": the JavaExec JVM gets the agent, the debugger attaches to it.
|
||||
const delegated = getAction("debug.attach") ? await gradleDelegation(config) : null;
|
||||
if (delegated) {
|
||||
await debugWithAgent(delegated);
|
||||
await debugWithAgent(delegated, show);
|
||||
return;
|
||||
}
|
||||
// Maven / Gradle: the build tool starts the JVM with a debug agent, the debugger attaches.
|
||||
if (jdwpLaunch(config) && getAction("debug.attach")) {
|
||||
await debugWithAgent(config);
|
||||
await debugWithAgent(config, show);
|
||||
return;
|
||||
}
|
||||
if (getAction("debug.startConfig")) {
|
||||
await executeAction("debug.startConfig", config);
|
||||
await executeAction("debug.startConfig", show === false ? { config, show: false } : config);
|
||||
return;
|
||||
}
|
||||
notifications().notify({
|
||||
@@ -155,7 +155,7 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = []) {
|
||||
body: tx`Running '${config.name}' without the debugger.`,
|
||||
key: "run-no-debugger",
|
||||
});
|
||||
await startRun(config, undefined, undefined, { prepared: true });
|
||||
await startRun(config, undefined, undefined, { prepared: true, show });
|
||||
}
|
||||
|
||||
async function debugSelected() {
|
||||
@@ -537,7 +537,8 @@ export function activate(): () => void {
|
||||
run: async ({ arg }) => {
|
||||
await ensureConfigs();
|
||||
const config = configFromArg(arg);
|
||||
if (config) await debugConfig(config);
|
||||
const show = arg && typeof arg === "object" && "show" in arg ? (arg as { show?: boolean }).show : undefined;
|
||||
if (config) await debugConfig(config, [], show);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -73,7 +73,7 @@ export function jdwpLaunch(config: RunConfig): RunConfig | null {
|
||||
}
|
||||
|
||||
/** Runs `config` with a JDWP agent and attaches the Java debugger to every JVM it starts. */
|
||||
export async function debugWithAgent(config: RunConfig): Promise<boolean> {
|
||||
export async function debugWithAgent(config: RunConfig, show?: boolean): Promise<boolean> {
|
||||
const launch = jdwpLaunch(config);
|
||||
if (!launch) return false;
|
||||
let sessionId: string | null = null;
|
||||
@@ -88,10 +88,12 @@ export async function debugWithAgent(config: RunConfig): Promise<boolean> {
|
||||
port,
|
||||
process: runId,
|
||||
runConfig: config,
|
||||
show,
|
||||
});
|
||||
};
|
||||
const session = await startRun(config, undefined, undefined, {
|
||||
launch,
|
||||
show,
|
||||
onLine: (line) => {
|
||||
const port = jdwpListeningPort(line);
|
||||
if (port === null) return;
|
||||
|
||||
@@ -21,7 +21,17 @@ import { compoundChildren, compoundCycle, formatEnv, parseEnv, validateConfig }
|
||||
import { contextConfig } from "./context";
|
||||
import { EditConfigsOverlay, moveConfig, treeRows } from "./EditConfigsDialog";
|
||||
import { pasteRows } from "./EnvDialog";
|
||||
import { CURRENT_FILE, resetRuns, selectConfig, sessionsOf, startCompound, startRun, useRun } from "./store";
|
||||
import {
|
||||
CURRENT_FILE,
|
||||
prepareLaunch,
|
||||
resetRuns,
|
||||
selectConfig,
|
||||
sessionsOf,
|
||||
setBeforeLaunchRunner,
|
||||
startCompound,
|
||||
startRun,
|
||||
useRun,
|
||||
} from "./store";
|
||||
import {
|
||||
BUILTIN_TYPES,
|
||||
blankTemplate,
|
||||
@@ -69,6 +79,15 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => disposeTypes());
|
||||
|
||||
it.each([undefined, false])(
|
||||
"respects the launch origin when a configuration requests focus (%s)",
|
||||
async (showOption) => {
|
||||
await startRun(cfg("focused", { focusToolWindow: true }), undefined, undefined, { show: showOption });
|
||||
if (showOption === false) expect(show).not.toHaveBeenCalled();
|
||||
else expect(show).toHaveBeenCalledWith("run", true);
|
||||
},
|
||||
);
|
||||
|
||||
describe("configuration types", () => {
|
||||
it("runs an Application with the chosen JRE and the classpath of the chosen module", () => {
|
||||
const type = typeFor("application")!;
|
||||
@@ -585,6 +604,18 @@ describe("before launch tasks", () => {
|
||||
dispose();
|
||||
disposeBuild();
|
||||
});
|
||||
|
||||
it("forwards the tool-window visibility choice to the before launch runner", async () => {
|
||||
const runner = vi.fn(async () => true);
|
||||
setBeforeLaunchRunner(runner);
|
||||
try {
|
||||
const app = cfg("app", { beforeLaunch: [{ kind: "run", target: "other" }] });
|
||||
expect(await prepareLaunch(app, [], false)).toBe(true);
|
||||
expect(runner).toHaveBeenCalledWith(app, ["app"], false);
|
||||
} finally {
|
||||
setBeforeLaunchRunner(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment variables", () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { showApplicationUpdate } from "./applicationUpdate";
|
||||
import { isUpdatable } from "./updatePolicies";
|
||||
import {
|
||||
configRemoved,
|
||||
debugSession,
|
||||
loadConfigs,
|
||||
persistTemporary,
|
||||
rerunConfig,
|
||||
@@ -355,7 +356,7 @@ function showOutput(s: RunSession) {
|
||||
/** Runs (or reruns, in its tab) a configuration; the output stays in Services instead of opening Run. */
|
||||
function run(r: Resolved) {
|
||||
const debug = serviceDebugger(r.session);
|
||||
if (debug) debug.rerun?.();
|
||||
if (debug) (debug.rerunInPlace ?? debug.rerun)?.();
|
||||
else if (r.session?.run) {
|
||||
const s = r.session.run;
|
||||
void startRun(rerunConfig(s), s.id, s.baseConfig, { show: false });
|
||||
@@ -364,8 +365,8 @@ function run(r: Resolved) {
|
||||
|
||||
function debug(r: Resolved) {
|
||||
const session = serviceDebugger(r.session);
|
||||
if (session) session.rerun?.();
|
||||
else if (r.config) void executeAction("run.debugConfig", r.config);
|
||||
if (session) (session.rerunInPlace ?? session.rerun)?.();
|
||||
else if (r.config) void executeAction("run.debugConfig", { config: r.config, show: false });
|
||||
}
|
||||
|
||||
function actions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
@@ -464,12 +465,13 @@ function RunDetail({ item }: { item: ServiceTreeItem }) {
|
||||
if (r.session?.run) {
|
||||
const execution = r.session;
|
||||
const d = serviceDebugger(execution);
|
||||
const rerun = d ? (d.rerunInPlace ?? d.rerun) : undefined;
|
||||
return (
|
||||
<SessionPanel
|
||||
key={execution.run!.id}
|
||||
session={execution.run!}
|
||||
onRerun={d ? d.rerun : undefined}
|
||||
onDebug={d ? d.rerun : undefined}
|
||||
onRerun={d ? rerun : () => run(r)}
|
||||
onDebug={d ? rerun : () => debugSession(execution.run!.id, { show: false })}
|
||||
onStop={d ? () => void stopServiceSessions([execution]) : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -627,7 +627,7 @@ function newRuntime(config: RunConfig): SessionRuntime {
|
||||
}
|
||||
|
||||
/** Runs a configuration's before launch tasks; false aborts the launch. Installed by the feature. */
|
||||
export type BeforeLaunchRunner = (config: RunConfig, chain: readonly string[]) => Promise<boolean>;
|
||||
export type BeforeLaunchRunner = (config: RunConfig, chain: readonly string[], show?: boolean) => Promise<boolean>;
|
||||
let beforeLaunch: BeforeLaunchRunner | null = null;
|
||||
|
||||
export function setBeforeLaunchRunner(runner: BeforeLaunchRunner | null) {
|
||||
@@ -635,17 +635,25 @@ export function setBeforeLaunchRunner(runner: BeforeLaunchRunner | null) {
|
||||
}
|
||||
|
||||
/** Runs `config`'s before launch tasks (none: true). `chain` holds the configurations already launching. */
|
||||
export async function prepareLaunch(config: RunConfig, chain: readonly string[] = []): Promise<boolean> {
|
||||
export async function prepareLaunch(
|
||||
config: RunConfig,
|
||||
chain: readonly string[] = [],
|
||||
show?: boolean,
|
||||
): Promise<boolean> {
|
||||
if (!config.beforeLaunch?.length || !beforeLaunch) return true;
|
||||
return beforeLaunch(config, [...chain, config.id]);
|
||||
return beforeLaunch(config, [...chain, config.id], show);
|
||||
}
|
||||
|
||||
/** Starts every configuration of a compound (after the compound's own before launch tasks). */
|
||||
export async function startCompound(config: RunConfig, chain: readonly string[] = []): Promise<RunSession[]> {
|
||||
if (!(await prepareLaunch(config, chain))) return [];
|
||||
export async function startCompound(
|
||||
config: RunConfig,
|
||||
chain: readonly string[] = [],
|
||||
show?: boolean,
|
||||
): Promise<RunSession[]> {
|
||||
if (!(await prepareLaunch(config, chain, show))) return [];
|
||||
const children = compoundChildren(config, useRun.getState().configs);
|
||||
const sessions = await Promise.all(
|
||||
children.map((c) => startRun(c, undefined, undefined, { chain: [...chain, config.id] })),
|
||||
children.map((c) => startRun(c, undefined, undefined, { chain: [...chain, config.id], show })),
|
||||
);
|
||||
return sessions.filter((s): s is RunSession => !!s);
|
||||
}
|
||||
@@ -677,7 +685,7 @@ export async function startRun(
|
||||
} = {},
|
||||
): Promise<RunSession | null> {
|
||||
if (config.kind === "compound") {
|
||||
await startCompound(config, options.chain);
|
||||
await startCompound(config, options.chain, options.show);
|
||||
return null;
|
||||
}
|
||||
// IDEA offers only Debug for Remote JVM Debug and the like: there is nothing to run.
|
||||
@@ -692,7 +700,7 @@ export async function startRun(
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!options.launch && !options.prepared && !(await prepareLaunch(config, options.chain))) return null;
|
||||
if (!options.launch && !options.prepared && !(await prepareLaunch(config, options.chain, options.show))) return null;
|
||||
const state = useRun.getState();
|
||||
const busy = (s: RunSession) => isLive(s);
|
||||
let session = reuse ? state.sessions.find((s) => s.id === reuse) : undefined;
|
||||
@@ -789,7 +797,7 @@ export async function startRun(
|
||||
});
|
||||
rt.onExit = options.onExit;
|
||||
try {
|
||||
if (config.focusToolWindow) toolWindowService().show("run", true);
|
||||
if (options.show !== false && config.focusToolWindow) toolWindowService().show("run", true);
|
||||
else if (options.show ?? config.openToolWindow ?? workflowSettings().run.showOnStart)
|
||||
toolWindowService().show("run");
|
||||
} catch {
|
||||
@@ -1142,9 +1150,11 @@ export function rerunSession(sessionId: string) {
|
||||
* `run.debugConfig` entry the Services view and the editor context menu use
|
||||
* (`baseConfig` keeps the original configuration for derived launches).
|
||||
*/
|
||||
export function debugSession(sessionId: string) {
|
||||
export function debugSession(sessionId: string, options: { show?: boolean } = {}) {
|
||||
const session = getSession(sessionId);
|
||||
if (session) void executeAction("run.debugConfig", baseConfigOf(session));
|
||||
if (!session) return;
|
||||
const config = baseConfigOf(session);
|
||||
void executeAction("run.debugConfig", options.show === false ? { config, show: false } : config);
|
||||
}
|
||||
|
||||
/** Paths of the failed tests of a session's last run. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { commands, type RunConfig } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { validateSettings } from "@/features/settings/model/schema";
|
||||
@@ -7,7 +8,13 @@ import { adoptSession, appendConsole, useDebug } from "@/features/debugger/state
|
||||
import { DebugServiceConsole } from "@/features/debugger/ui/DebugConsole";
|
||||
import { runServiceProvider } from "@/features/run/services";
|
||||
import { onRunExited, runtime, startRun, useRun } from "@/features/run/store";
|
||||
import { registerServiceProvider, useDebugRunSessions, type DebugServiceSession } from "@/workbench/contributions";
|
||||
import {
|
||||
provideToolWindowService,
|
||||
registerServiceProvider,
|
||||
useDebugRunSessions,
|
||||
type DebugServiceSession,
|
||||
type ToolWindowService,
|
||||
} from "@/workbench/contributions";
|
||||
import { ServicesTreeView } from "./ServicesTree";
|
||||
import { useServicesTree } from "./tree";
|
||||
|
||||
@@ -109,6 +116,84 @@ it("shows the service port announced by a debug session's application", () => {
|
||||
expect(within(screen.getByRole("tree")).getByText(":8080")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("debugs a service in place without switching to the Debug tool window", async () => {
|
||||
const start = vi.fn();
|
||||
const disposeAction = registerActions([
|
||||
{ id: "run.debugConfig", title: "Debug Configuration", run: ({ arg }) => start(arg) },
|
||||
]);
|
||||
try {
|
||||
render(<ServicesTreeView />);
|
||||
select();
|
||||
fireEvent.click(toolbar().getByRole("button", { name: "Debug" }));
|
||||
await vi.waitFor(() => expect(start).toHaveBeenCalledWith({ config, show: false }));
|
||||
} finally {
|
||||
disposeAction();
|
||||
}
|
||||
});
|
||||
|
||||
it("reruns a debug service in place from the Services panel", () => {
|
||||
const d = summary(1, { rerun: vi.fn(), rerunInPlace: vi.fn() });
|
||||
useDebugRunSessions.setState({ serviceSessions: [d] });
|
||||
render(<ServicesTreeView />);
|
||||
select();
|
||||
fireEvent.click(toolbar().getByRole("button", { name: "Rerun" }));
|
||||
expect(d.rerunInPlace).toHaveBeenCalledOnce();
|
||||
expect(d.rerun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts a debug session in place from a run service's detail toolbar", async () => {
|
||||
const start = vi.fn();
|
||||
const disposeAction = registerActions([
|
||||
{ id: "run.debugConfig", title: "Debug Configuration", run: ({ arg }) => start(arg) },
|
||||
]);
|
||||
const c = { ...config, kind: "shell", command: "echo", debugAdapter: "java-debug" };
|
||||
const s = await startRun(c);
|
||||
expect(s).not.toBeNull();
|
||||
useRun.setState({ configs: [c], sessions: [{ ...s!, config: c, status: "running" as const }] });
|
||||
try {
|
||||
render(<ServicesTreeView />);
|
||||
select();
|
||||
const actions = within(screen.getByRole("toolbar", { name: "Run actions" }));
|
||||
fireEvent.click(actions.getByRole("button", { name: "Debug" }));
|
||||
await vi.waitFor(() => expect(start).toHaveBeenCalledWith({ config: c, show: false }));
|
||||
} finally {
|
||||
disposeAction();
|
||||
}
|
||||
});
|
||||
|
||||
it("reruns a plain run service in place from its detail toolbar", async () => {
|
||||
const c = { ...config, kind: "shell", command: "echo", debugAdapter: null };
|
||||
const s = await startRun(c);
|
||||
expect(s).not.toBeNull();
|
||||
useRun.setState({ configs: [c] });
|
||||
useServicesTree.setState({ expanded: new Set(["run:type:shell"]) });
|
||||
const show = vi.fn();
|
||||
provideToolWindowService({ show } as unknown as ToolWindowService);
|
||||
const previousRuntime = runtime(s!.id);
|
||||
render(<ServicesTreeView />);
|
||||
select();
|
||||
fireEvent.click(within(screen.getByRole("toolbar", { name: "Run actions" })).getByRole("button", { name: "Rerun" }));
|
||||
await vi.waitFor(() => expect(runtime(s!.id)).not.toBe(previousRuntime));
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reruns an attached debugger in place from a run service's detail toolbar", async () => {
|
||||
const c = { ...config, kind: "shell", command: "echo", debugAdapter: null };
|
||||
const s = await startRun(c);
|
||||
expect(s).not.toBeNull();
|
||||
const active = { ...s!, config, status: "running" as const, runId: 7 };
|
||||
runtime(s!.id)!.console.append({ kind: "stdout", text: "Maven application log\n" });
|
||||
useRun.setState({ configs: [config], sessions: [active] });
|
||||
const d = summary(1, { processId: 7, rerun: vi.fn(), rerunInPlace: vi.fn() });
|
||||
useDebugRunSessions.setState({ serviceSessions: [d] });
|
||||
render(<ServicesTreeView />);
|
||||
select();
|
||||
const actions = within(screen.getByRole("toolbar", { name: "Run actions" }));
|
||||
fireEvent.click(actions.getByRole("button", { name: "Debug" }));
|
||||
expect(d.rerunInPlace).toHaveBeenCalledOnce();
|
||||
expect(d.rerun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the other parallel instance alive and then displays it", async () => {
|
||||
adoptSession(1);
|
||||
adoptSession(2);
|
||||
|
||||
@@ -121,9 +121,9 @@ describe("theme tokens", () => {
|
||||
/**
|
||||
* Tokens the palette themes (Solarized, One) take from their light / dark base
|
||||
* on purpose: colours the user assigns (project and data source colours), the
|
||||
* platform's caption close button, and the tint strength.
|
||||
* platform's caption close and macOS window buttons, and the tint strength.
|
||||
*/
|
||||
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|tint-strength$)/;
|
||||
const INHERITED_FROM_BASE = /^--(project-color-|db-color-|caption-close-|fg-caption-close-|mac-window-|tint-strength$)/;
|
||||
|
||||
describe("palette themes", () => {
|
||||
const ISLANDS = ["light", "dark", "dark-hc"];
|
||||
|
||||
@@ -437,7 +437,9 @@ html {
|
||||
}
|
||||
|
||||
/* macOS window buttons drawn by the web title bar in full screen: the system colours in every theme. */
|
||||
:root {
|
||||
:root,
|
||||
:root[data-theme="light"],
|
||||
:root[data-theme="dark-hc"] {
|
||||
--mac-window-close: #ff5f57;
|
||||
--mac-window-minimize: #febc2e;
|
||||
--mac-window-zoom: #28c840;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { commands, onIdeEvent } from "@/platform/ipc";
|
||||
import { commands, isTauri, onIdeEvent } from "@/platform/ipc";
|
||||
import { Icon } from "@/ui";
|
||||
import styles from "./TitleBar.module.css";
|
||||
|
||||
@@ -18,23 +18,31 @@ function useFullscreenWindow(): { fullscreen: boolean; active: boolean } {
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [active, setActive] = useState(() => document.hasFocus());
|
||||
useEffect(() => {
|
||||
// The plain-browser shell and tests have no Tauri window to ask.
|
||||
if (!isTauri()) return;
|
||||
let disposed = false;
|
||||
const offs: (() => void)[] = [];
|
||||
const sync = () => void commands.windowIsFullscreen().then((on) => !disposed && setFullscreen(on));
|
||||
const sync = () =>
|
||||
void commands
|
||||
.windowIsFullscreen()
|
||||
.then((on) => !disposed && setFullscreen(on))
|
||||
.catch(() => {});
|
||||
sync();
|
||||
offs.push(onIdeEvent("windowFullscreenChanged", sync));
|
||||
void import("@tauri-apps/api/window").then(async ({ getCurrentWindow }) => {
|
||||
const w: AppWindow = getCurrentWindow();
|
||||
const listeners = await Promise.all([
|
||||
w.onResized(sync),
|
||||
w.onFocusChanged(({ payload }) => {
|
||||
setActive(payload);
|
||||
sync();
|
||||
}),
|
||||
]);
|
||||
if (disposed) listeners.forEach((off) => off());
|
||||
else offs.push(...listeners);
|
||||
});
|
||||
void import("@tauri-apps/api/window")
|
||||
.then(async ({ getCurrentWindow }) => {
|
||||
const w: AppWindow = getCurrentWindow();
|
||||
const listeners = await Promise.all([
|
||||
w.onResized(sync),
|
||||
w.onFocusChanged(({ payload }) => {
|
||||
setActive(payload);
|
||||
sync();
|
||||
}),
|
||||
]);
|
||||
if (disposed) listeners.forEach((off) => off());
|
||||
else offs.push(...listeners);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
disposed = true;
|
||||
offs.forEach((off) => off());
|
||||
@@ -44,6 +52,7 @@ function useFullscreenWindow(): { fullscreen: boolean; active: boolean } {
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
if (!isTauri()) return;
|
||||
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) => getCurrentWindow().close());
|
||||
}
|
||||
|
||||
|
||||
@@ -1563,6 +1563,8 @@ export interface DebugServiceSession {
|
||||
/** The debuggee's HTTP port announced on its console (not the JDWP attach port), if known. */
|
||||
httpPort?: number;
|
||||
rerun: (() => void) | null;
|
||||
/** Rerun without opening the Debug tool window (the Services panel debugs in place). */
|
||||
rerunInPlace?: (() => void) | null;
|
||||
stop: (options?: { stopProcess?: boolean }) => Promise<void>;
|
||||
show: () => void;
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача