feat(run): share Java preparation and add incremental build policies (#2156)

Run and Debug now share one Java preparation path: save documents, build with the configured policy, resolve the module classpath and JDK, and stop on a failed build. Incremental Make is the default; Rebuild, NoBuild and Build-no-error-check are persisted on the run configuration, and the form offers only what a build tool supports. Delegated Gradle goes through the same path, Debug reports save failures, Maven snapshots honour Rebuild, and scoped diagnostics are retained so incremental Maven cannot erase compilation errors.

Co-authored-by: misswell <[email protected]>
Цей коміт міститься в:
misswellтаmisswell authored and GitHub committed 2026-11-09 03:18:13 +08:00
1 джерело 43bcd66b2b
коміт 9f9a2d8cf0
41 files changed
+868 -136

No files matched your search

+22 -2
Переглянути файл
@@ -772,11 +772,11 @@ export const commands = {
buildRun: (kind: BuildKind, file: string | null, cmake: string | null, request: RequestId, clear: string[], onEvent: Channel<BuildEvent>) => typedError<BuildResult, IdeError>(__TAURI_INVOKE("build_run", { kind, file, cmake, request, clear, onEvent })),
/**
* Cancellable with `cancel_request(request)`.
* Builds the workspace (when `build`), then resolves the runtime (or test)
* Builds according to `policy`, then resolves the runtime (or test)
* classpath and the JDK of the module at `module` (absolute, or relative to the project root).
* A module with Kotlin / Groovy / Scala sources is built by its build system, whose class directories come first.
*/
javaResolveLaunch: (module: string, test: boolean, build: boolean, request: RequestId) => typedError<JavaLaunchInfo, IdeError>(__TAURI_INVOKE("java_resolve_launch", { module, test, build, request })),
javaResolveLaunch: (module: string, test: boolean, policy: BuildPolicy, request: RequestId) => typedError<JavaLaunchInfo, IdeError>(__TAURI_INVOKE("java_resolve_launch", { module, test, policy, request })),
/** Port of java-debug's DAP server inside the jdtls serving `module` (started on demand). */
javaDebugServer: (module: string) => typedError<number, IdeError>(__TAURI_INVOKE("java_debug_server", { module })),
/** Incremental build of the jdtls workspace serving `module` (e.g. before HotSwap). */
@@ -2131,6 +2131,22 @@ export type BuildModel = {
gradle?: GradleModel[],
};
/** How a Java launch builds the project before starting (IDEA's Before launch › Build). */
export type BuildPolicy =
/** Build the way this project's backend does it: incremental when possible. */
"automatic" |
/** Incremental build (IDEA's "Build"/Make). */
"make" |
/** Clean the output, then build (IDEA's Rebuild). */
"rebuild" |
/** Do not build; still resolve the module, JDK and classpath. */
"noBuild" |
/**
* Incremental build; a project with compile errors may still start with its last
* successfully compiled classes (IDEA's "Build, no error check").
*/
"makeAndContinueOnErrors";
export type BuildResult = {
system: BuildSystem,
title: string,
@@ -7038,6 +7054,8 @@ export type RunExtras_Deserialize = {
/** "Save console output to file" (overwritten on each run). */
outputFile?: string | null,
beforeLaunch?: BeforeLaunchTask[],
/** Before launch build for JVM launches; unset keeps the project's default (incremental). */
buildPolicy?: BuildPolicy | null,
/** Compound configurations: ids of the configurations started together. */
compound?: string[],
showOnStdout?: boolean,
@@ -7080,6 +7098,8 @@ export type RunExtras_Serialize = {
/** "Save console output to file" (overwritten on each run). */
outputFile?: string | null,
beforeLaunch?: BeforeLaunchTask[],
/** Before launch build for JVM launches; unset keeps the project's default (incremental). */
buildPolicy?: BuildPolicy | null,
/** Compound configurations: ids of the configurations started together. */
compound?: string[],
showOnStdout?: boolean,
+25 -1
Переглянути файл
@@ -40,6 +40,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => {
return { ...real, commands: { ...real.commands, ...cmds } };
});
const saveAll = vi.hoisted(() => vi.fn(async () => undefined));
const editor = vi.hoisted(() => ({
current: null as null | { ctx: { path: string; language: string }; view: { state: unknown } },
}));
@@ -55,7 +56,7 @@ vi.mock("@/workbench/contributions", async (importOriginal) => {
...real,
maybeEditorService: () => ({
openFile: async (path: string, o?: { focus?: boolean }) => void opened.push({ path, focus: o?.focus }),
saveAll: async () => undefined,
saveAll,
}),
};
});
@@ -438,3 +439,26 @@ it("stops the waiting Python container if attaching fails", async () => {
});
expect(ipc.runStop).toHaveBeenCalledWith(9);
});
it("reports Java Debug save failures and does not start the adapter", async () => {
const notify = vi.fn();
provideNotificationService({ notify });
useProject.setState({ project: { root: "/p" } as never });
saveAll.mockRejectedValueOnce(new Error("save failed"));
await executeAction("debug.startConfig", {
id: "main",
name: "Main",
kind: "application",
command: "java",
args: ["a.Main"],
cwd: null,
module: "",
env: {},
detected: false,
isTest: false,
debugAdapter: "java-debug",
});
expect(ipc.debugStart).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ level: "error", body: "save failed" }));
useProject.setState({ project: null });
});
+15 -2
Переглянути файл
@@ -78,7 +78,7 @@ describe("Java debugging (java-debug through jdtls)", () => {
expect(isJavaDebuggable({ ...app, kind: "maven" })).toBe(false);
const l = await javaLaunchConfig(app, "/p");
expect(invoked).toEqual([
["javaResolveLaunch", ["api", false, true, expect.any(Number)]],
["javaResolveLaunch", ["api", false, "automatic", expect.any(Number)]],
["javaDebugServer", ["api"]],
]);
expect(l).toMatchObject({ adapter: "java-debug", request: "launch", cwd: "/p/api", args: [], env: { A: "1" } });
@@ -178,7 +178,7 @@ it.each(["sibling", "/outside/modules"])("debugs from %s with the main class mod
for (const module of ["api", undefined]) {
invoked.length = 0;
const launch = await javaLaunchConfig({ ...app, module, cwd }, "/p");
expect(invoked).toContainEqual(["javaResolveLaunch", ["api", false, true, expect.any(Number)]]);
expect(invoked).toContainEqual(["javaResolveLaunch", ["api", false, "automatic", expect.any(Number)]]);
expect(invoked).toContainEqual(["javaDebugServer", ["api"]]);
expect(launch.cwd).toBe(cwd.startsWith("/") ? cwd : `/p/${cwd}`);
}
@@ -208,3 +208,16 @@ describe("naming the Java project jdtls imported", () => {
expect(await javaProjectName("/p")).toBe("p");
});
});
it("reports a save failure before starting Java Debug", async () => {
invoked.length = 0;
const saveAll = vi.fn().mockRejectedValue(new Error("save failed"));
await expect(
javaLaunchConfig(app, "/p", {
request: 1,
signal: new AbortController().signal,
saveAll,
}),
).rejects.toThrow("save failed");
expect(invoked).toEqual([]);
});
+8 -5
Переглянути файл
@@ -266,11 +266,14 @@ async function startJavaConfig(config: RunConfig, root: string, origin: RunConfi
};
signal.addEventListener("abort", cancel, { once: true });
try {
await maybeEditorService()
?.saveAll()
.catch(() => undefined);
signal.throwIfAborted();
return await javaLaunchConfig(config, root, { request, signal, report });
return await javaLaunchConfig(config, root, {
request,
signal,
report,
saveAll: async () => {
await maybeEditorService()?.saveAll();
},
});
} finally {
signal.removeEventListener("abort", cancel);
}
+4 -1
Переглянути файл
@@ -15,7 +15,7 @@ import {
prepareJavaLaunch,
type JavaPreparation,
} from "@/platform/javaLaunch";
import { notifications } from "@/workbench/contributions";
import { maybeEditorService, notifications } from "@/workbench/contributions";
export const JAVA_ADAPTER = "java-debug";
export const KOTLIN_ADAPTER = "kotlin-debug-adapter";
@@ -119,6 +119,9 @@ export async function javaLaunchConfig(
preparation ?? {
request: nextRequestId(),
signal: new AbortController().signal,
saveAll: async () => {
await maybeEditorService()?.saveAll();
},
},
);
if (info.errorCount > 0 && !preparation?.report)
+56 -5
Переглянути файл
@@ -3,10 +3,10 @@
* common ones, the options added through "Modify options" (⌥M) and the
* Before launch list.
*/
import { parseJavaArgs } from "@/platform/javaLaunch";
import { isJvmApplication, parseJavaArgs } from "@/platform/javaLaunch";
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
import clsx from "clsx";
import { isTauri, type BeforeLaunchTask, type RunConfig } from "@/platform/ipc";
import { isTauri, type BeforeLaunchTask, type BuildPolicy, type RunConfig } from "@/platform/ipc";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { relativePath, useProject } from "@/platform/project";
import { isRemoteWindow } from "@/platform/remote";
@@ -42,7 +42,9 @@ import {
parseEnv,
type ConfigErrors,
} from "./configs";
import { gradleContainsModule } from "./gradleRun";
import { EnvDialog } from "./EnvDialog";
import { useBuildModel } from "@/platform/buildModel";
import { withTarget } from "./remote";
import { COMPOUND, formParams, terminalApplies, withParams } from "./types";
import { TomcatSettings } from "./TomcatSettings";
@@ -76,12 +78,19 @@ export type OptionId =
| "showOnStdout"
| "showOnStderr"
| "beforeLaunch"
| "buildPolicy"
| "openToolWindow"
| "focusToolWindow"
| "isTest"
| "emulateTerminal";
const VALUE_OPTIONS: ReadonlySet<OptionId> = new Set(["stdinFile", "envFiles", "outputFile", "beforeLaunch"]);
const VALUE_OPTIONS: ReadonlySet<OptionId> = new Set([
"stdinFile",
"envFiles",
"outputFile",
"beforeLaunch",
"buildPolicy",
]);
const OPTION_LABELS: Record<OptionId, string> = {
allowParallel: "Allow multiple instances",
@@ -91,6 +100,7 @@ const OPTION_LABELS: Record<OptionId, string> = {
showOnStdout: "Show console when a message is printed to stdout",
showOnStderr: "Show console when a message is printed to stderr",
beforeLaunch: "Add before launch task",
buildPolicy: "Build",
openToolWindow: "Open run/debug tool window when started",
focusToolWindow: "Focus run/debug tool window when started",
isTest: "Show results in the test runner",
@@ -101,7 +111,11 @@ const OPTION_GROUPS: ReadonlyArray<{ label: string; options: OptionId[]; process
{ label: "Operating System", options: ["allowParallel", "stdinFile", "envFiles", "emulateTerminal"], process: true },
{ label: "Logs", options: ["outputFile", "showOnStdout", "showOnStderr"], process: true },
{ label: "Test", options: ["isTest"], process: true },
{ label: "Before Launch", options: ["beforeLaunch", "openToolWindow", "focusToolWindow"], process: false },
{
label: "Before Launch",
options: ["buildPolicy", "beforeLaunch", "openToolWindow", "focusToolWindow"],
process: false,
},
];
/** Whether the option currently has a (non-default) value. */
@@ -121,6 +135,8 @@ export function optionSet(config: RunConfig, id: OptionId, openByDefault: boolea
return !!config.showOnStderr;
case "beforeLaunch":
return !!config.beforeLaunch?.length;
case "buildPolicy":
return (config.buildPolicy ?? "automatic") !== "automatic";
case "openToolWindow":
return config.openToolWindow ?? openByDefault;
case "focusToolWindow":
@@ -145,6 +161,8 @@ export function optionPatch(id: OptionId, on: boolean): Partial<RunConfig> {
return { outputFile: null };
case "beforeLaunch":
return { beforeLaunch: [] };
case "buildPolicy":
return { buildPolicy: on ? "automatic" : null };
case "showOnStdout":
return { showOnStdout: on };
case "showOnStderr":
@@ -160,6 +178,14 @@ export function optionPatch(id: OptionId, on: boolean): Partial<RunConfig> {
}
}
const BUILD_POLICY_OPTIONS: SelectOption[] = [
{ value: "automatic", label: "Automatic (incremental)" },
{ value: "make", label: "Build (Make)" },
{ value: "rebuild", label: "Rebuild" },
{ value: "noBuild", label: "No build" },
{ value: "makeAndContinueOnErrors", label: "Build, no error check" },
];
export interface ConfigFormProps {
config: RunConfig;
/** Every configuration of the dialog (compound members, before launch targets, name checks). */
@@ -222,6 +248,17 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
const tx = useTx();
const txTemplate = useTxTemplate();
const root = useProject((s) => s.project?.root ?? null);
const gradle = useBuildModel((s) => s.model?.gradle);
const runUsing = useProject((s) => s.settings?.languages?.java?.gradle?.runUsing);
const delegated =
isJvmApplication(config) &&
runUsing !== "ide" &&
gradle?.some((g) => config.module == null || gradleContainsModule(g, root ?? "", config.module));
const buildPolicyOptions = BUILD_POLICY_OPTIONS.filter((option) => {
if (config.kind === "gradle" || delegated)
return option.value !== "noBuild" && option.value !== "makeAndContinueOnErrors";
return config.kind !== "maven" || option.value !== "noBuild";
});
const [shown, setShown] = useState<ReadonlySet<OptionId>>(() => new Set());
const [argsText, setArgsText] = useState<string | null>(null);
const [envText, setEnvText] = useState<string | null>(null);
@@ -239,6 +276,7 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
}
const compound = config.kind === COMPOUND;
const buildPolicyApplies = (c: RunConfig) => isJvmApplication(c) || c.kind === "maven" || c.kind === "gradle";
// Compound and debug-only types (Remote JVM Debug) start no process of their own.
const noProcess = compound || !!type?.debugOnly;
const params = compound ? {} : formParams(config, type);
@@ -275,7 +313,9 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
...g.options
.filter(
(id) =>
(!compound || id === "beforeLaunch") && (id !== "emulateTerminal" || terminal || !!config.emulateTerminal),
(!compound || id === "beforeLaunch") &&
(id !== "emulateTerminal" || terminal || !!config.emulateTerminal) &&
(id !== "buildPolicy" || buildPolicyApplies(config)),
)
.map((id): MenuItem => ({
label: OPTION_LABELS[id],
@@ -629,6 +669,17 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
</>
)}
{isShown("buildPolicy") && buildPolicyApplies(config) && (
<Field label="Build" hint="Before launch build for this Java configuration">
<Select
value={config.buildPolicy ?? "automatic"}
options={buildPolicyOptions}
onChange={(v) => update({ buildPolicy: v === "automatic" ? null : (v as BuildPolicy) })}
aria-label="Build"
/>
</Field>
)}
{isShown("beforeLaunch") && (
<Field label="Before launch" error={errors.beforeLaunch} top>
<div className={styles.taskList} role="list" aria-label={tx("Before launch tasks")}>
+2 -2
Переглянути файл
@@ -131,7 +131,7 @@ const boot = {
describe("Spring Boot application updates", () => {
it("processes resources and publishes them to the resolved runtime output", async () => {
await updateTomcat(boot, "resources");
expect(mocks.resolve).toHaveBeenCalledWith("app", false, false, 1);
expect(mocks.resolve).toHaveBeenCalledWith("app", false, "noBuild", 1);
expect(mocks.runBuild).toHaveBeenCalledWith({ id: "build" });
expect(mocks.publish).toHaveBeenCalledWith(boot.config, ["/p/app/bin/main"], "a.Main");
expect(mocks.update).not.toHaveBeenCalled();
@@ -139,7 +139,7 @@ describe("Spring Boot application updates", () => {
});
it("compiles and invokes the existing scoped HotSwap action in debug", async () => {
await updateTomcat(boot, "classes");
expect(mocks.resolve).toHaveBeenCalledWith("app", false, true, 1);
expect(mocks.resolve).toHaveBeenCalledWith("app", false, "make", 1);
expect(mocks.action).toHaveBeenCalledWith("debug.reloadChangedClasses", { configId: "boot" });
});
it("explains compiled classes require restart without a debugger", async () => {
+6 -1
Переглянути файл
@@ -43,7 +43,12 @@ async function updateSpringBoot(config: RunConfig, mode: UpdateMode, debugging:
const parsed = parseJavaArgs(config.args);
if (!parsed.mainClass) throw new Error(tx("The Spring Boot configuration has no main class"));
const info = await call(
commands.javaResolveLaunch(module, parsed.classpath === "test", mode === "classes", nextRequestId()),
commands.javaResolveLaunch(
module,
parsed.classpath === "test",
mode === "classes" ? "make" : "noBuild",
nextRequestId(),
),
);
if (info.build === "failed" || info.build === "withErrors" || info.build === "cancelled")
throw new Error(tx("Application update build failed"));
+4 -2
Переглянути файл
@@ -2,9 +2,9 @@
* Before launch tasks (IDEA's "Before launch" list): run in order before a
* configuration starts; the first failure aborts the launch.
*/
import { useProject } from "@/platform/project";
import { buildCMake, isCMakeBuild } from "./cmake";
import { dotnetBuildConfig } from "./dotnetTypes";
import { useProject } from "@/platform/project";
import { useBuildPresentation } from "@/platform/projectPresentation";
import { buildTask } from "./buildTask";
import { moduleTaskError } from "./configs";
@@ -88,7 +88,9 @@ async function runTask(
return (await runToEnd(dotnetBuildConfig(config), chain, show)) ? null : "Build failed";
case "build": {
if (target) return buildCMake(target, show === true);
return buildTask(BUILD_ACTION, { activate: false });
const error = await buildTask(BUILD_ACTION, { activate: false });
if (error) return error;
return null;
}
case "buildModule": {
const error = moduleTaskError(config, target);
+44
Переглянути файл
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { applyBuildPolicyToCommand } from "./buildCommand";
const cfg = (extra: Partial<RunConfig>): RunConfig => ({
id: "m",
name: "m",
kind: "maven",
command: "mvn",
args: ["-B", "spring-boot:run"],
cwd: null,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
...extra,
});
describe("build policy for build-tool commands", () => {
it("puts clean first for Maven and Gradle Rebuild", () => {
expect(applyBuildPolicyToCommand({ ...cfg({ buildPolicy: "rebuild" }), kind: "maven" }).args).toEqual([
"clean",
"-B",
"spring-boot:run",
]);
expect(
applyBuildPolicyToCommand({ ...cfg({ kind: "gradle", args: ["bootRun"] }), buildPolicy: "rebuild" }).args,
).toEqual(["clean", "bootRun"]);
});
it("lets Maven continue on compile errors only when asked", () => {
expect(applyBuildPolicyToCommand({ ...cfg({ buildPolicy: "makeAndContinueOnErrors" }) }).args).toContain(
"-Dmaven.compiler.failOnError=false",
);
expect(applyBuildPolicyToCommand({ ...cfg({ buildPolicy: "make" }) }).args).not.toContain(
"-Dmaven.compiler.failOnError=false",
);
});
it("leaves Automatic and NoBuild commands alone", () => {
for (const buildPolicy of ["automatic", "noBuild"] as const)
expect(applyBuildPolicyToCommand(cfg({ buildPolicy }))).toEqual(cfg({ buildPolicy }));
});
});
+22
Переглянути файл
@@ -0,0 +1,22 @@
/**
* Build policies for configurations whose command is itself a build tool (Maven,
* Gradle). Application/Spring Boot configurations build through `prepareJavaLaunch`
* instead; this only rewrites the command of an already chosen build-tool launch.
*/
import type { RunConfig } from "@/platform/ipc";
const MAVEN = new Set(["maven"]);
const GRADLE = new Set(["gradle"]);
export function applyBuildPolicyToCommand(config: RunConfig): RunConfig {
const rebuild = config.buildPolicy === "rebuild";
if (rebuild && !config.args.includes("clean") && (MAVEN.has(config.kind) || GRADLE.has(config.kind))) {
// Maven and Gradle both accept the clean task before their other goals.
return { ...config, args: ["clean", ...config.args] };
}
if (config.buildPolicy === "makeAndContinueOnErrors" && MAVEN.has(config.kind)) {
const flag = "-Dmaven.compiler.failOnError=false";
return config.args.includes(flag) ? config : { ...config, args: [...config.args, flag] };
}
return config;
}
+12
Переглянути файл
@@ -265,3 +265,15 @@ describe("delegation across independent builds", () => {
expect(run!.args.at(-1)).toBe(":app:openideaRun");
});
});
it("uses the same Rebuild preparation for delegated Run and Debug", () => {
const app = {
...applicationConfig({ className: "a.Main" }, "app/src/main/java/a/Main.java", []),
buildPolicy: "rebuild" as const,
};
const run = delegatedGradleConfig(app, "app", ctx)!;
expect(run.args[0]).toBe("clean");
const debug = jdwpLaunch(run)!;
expect(debug.args.filter((a) => a === "clean")).toHaveLength(1);
expect(debug.args).toContain(DELEGATED_DEBUG);
});
+13 -6
Переглянути файл
@@ -10,8 +10,10 @@ import TESTS_SCRIPT from "./gradleTests.gradle?raw";
import BOOT_SCRIPT from "./gradleBoot.gradle?raw";
import { useBuildModel } from "@/platform/buildModel";
import { call, commands, isTauri, type GradleModel, type RunConfig } from "@/platform/ipc";
import { findJavaModule, isJvmApplication, parseJavaArgs } from "@/platform/javaLaunch";
import { isJvmApplication, parseJavaArgs, prepareJavaModule } from "@/platform/javaLaunch";
import { useProject } from "@/platform/project";
import { maybeEditorService } from "@/workbench/contributions";
import { applyBuildPolicyToCommand } from "./buildCommand";
/** System property that marks a delegated run (read by `jdwpLaunch` to add the debug flag). */
export const DELEGATED_MAIN = "-Dopenidea.run.main=";
@@ -21,6 +23,10 @@ const TASK = "openideaRun";
const isAbsolute = (p: string) => p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
const absolute = (root: string, p: string) => (isAbsolute(p) ? p : p ? `${root}/${p}` : root);
export function gradleContainsModule(gradle: GradleModel, root: string, module: string): boolean {
return gradle.projects.some((p) => absolute(root, p.dir) === absolute(root, module));
}
export interface DelegationContext {
/** Project root. */
root: string;
@@ -74,7 +80,7 @@ export function delegatedGradleConfig(config: RunConfig, module: string, ctx: De
// A Gradle run from here on: not an Application again (it would be delegated twice), and
// without the Application's params (Gradle's own VM-options init script would repeat them).
const { params: _params, ...rest } = config;
return {
return applyBuildPolicyToCommand({
...rest,
kind: "gradle",
command: ctx.gradle.command,
@@ -83,7 +89,7 @@ export function delegatedGradleConfig(config: RunConfig, module: string, ctx: De
// The Gradle JVM's JAVA_HOME first; the configuration's own environment wins.
env: { ...ctx.gradle.env, ...config.env },
module: module,
};
});
}
/**
@@ -148,8 +154,9 @@ export async function gradleDelegation(config: RunConfig): Promise<RunConfig | n
if (!project?.root || settings?.languages?.java?.gradle?.runUsing === "ide") return null;
const { model, gradleOffline } = useBuildModel.getState();
if (!model?.gradle?.length) return null;
const module = config.module ?? (await findJavaModule(config));
if (module === null) return null;
const module = await prepareJavaModule(config, async () => {
await maybeEditorService()?.saveAll();
});
const dir = absolute(project.root, module);
// A composite can report a sibling's module; prefer a linked root that contains it.
const ownerDepth = (g: GradleModel) => {
@@ -158,7 +165,7 @@ export async function gradleDelegation(config: RunConfig): Promise<RunConfig | n
return path === root || path.startsWith(`${root}/`) ? root.split("/").length : 0;
};
const gradle = model.gradle
.filter((g) => g.projects.some((p) => absolute(project.root, p.dir) === dir))
.filter((g) => gradleContainsModule(g, project.root, module))
.sort((a, b) => ownerDepth(b) - ownerDepth(a))[0];
if (!gradle) return null;
const ctx = {
+12 -1
Переглянути файл
@@ -137,7 +137,18 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = [], sho
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;
let delegated: RunConfig | null;
try {
delegated = getAction("debug.attach") ? await gradleDelegation(config) : null;
} catch (error) {
notifications().notify({
group: "run",
level: "error",
title: `Cannot run '${config.name}'`,
body: error instanceof Error ? error.message : String(error),
});
return;
}
if (delegated) {
await debugWithAgent(delegated, show);
return;
+6
Переглянути файл
@@ -366,6 +366,12 @@ describe("debugging build tool configurations", () => {
expect(out.env.MAVEN_OPTS).toMatch(AGENT);
});
it("honours Rebuild on a Maven debug command", () => {
const out = jdwpLaunch(cfg({ id: "r", kind: "maven", args: ["spring-boot:run"], buildPolicy: "rebuild" }))!;
expect(out.args[0]).toBe("clean");
expect(out.args.at(-1)).toMatch(/^-Dspring-boot\.run\.jvmArguments=-agentlib:jdwp=/);
});
it("keeps plain Maven builds out of the debugger unless set to the Java debugger", () => {
expect(jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["exec:exec"] }))).toBeNull();
const chosen = jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["package"], debugAdapter: "java-debug" }))!;
+2
Переглянути файл
@@ -11,6 +11,7 @@
import { executeAction } from "@/platform/actions";
import type { RunConfig } from "@/platform/ipc";
import { isJvmApplication, JDWP_AGENT, jdwpListeningPort } from "@/platform/javaLaunch";
import { applyBuildPolicyToCommand } from "./buildCommand";
import { startRun, useRun } from "./store";
import { DELEGATED_DEBUG, DELEGATED_MAIN } from "./gradleRun";
@@ -38,6 +39,7 @@ function withProperty(args: readonly string[], key: string, value: string): stri
/** The configuration with a waiting JDWP agent, or null when it starts no debuggable JVM. */
export function jdwpLaunch(config: RunConfig): RunConfig | null {
config = applyBuildPolicyToCommand(config);
if (config.kind === "tomcat") return { ...config, params: { ...config.params, tomcatDebug: "true" } };
const args = config.args;
// A single source file (`java Hi.java`): the agent goes before the file. Compiled first
+70 -6
Переглянути файл
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { IpcError, type RunConfig } from "@/platform/ipc";
import { findJavaModule, prepareJavaLaunch } from "@/platform/javaLaunch";
import { useProject } from "@/platform/project";
import { findJavaModule, prepareJavaLaunch, prepareJavaModule } from "@/platform/javaLaunch";
import { _resetI18nForTests, setLocale } from "@/platform/i18n";
import { provideNotificationService } from "@/workbench/contributions";
import { applicationConfig } from "./application";
@@ -72,7 +73,7 @@ afterEach(() => {
it.each(["sibling", "/outside/modules"])("runs from %s using the stored module's classpath", async (cwd) => {
const session = await startRun({ ...config, module: "app", cwd });
expect(session?.status).toBe("running");
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, true, expect.any(Number));
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
expect(ipc.runStart).toHaveBeenCalledWith(
expect.objectContaining({ cwd, args: ["-cp", "/p/app/target/classes", "a.Main"] }),
expect.anything(),
@@ -80,10 +81,59 @@ it.each(["sibling", "/outside/modules"])("runs from %s using the stored module's
expect(ipc.findFiles).not.toHaveBeenCalled();
});
const launchInfo = (build: "succeeded" | "failed" | "withErrors") => ({
build,
projectRoot: "/p",
classPaths: ["/p/app/target/classes"],
modulePaths: [],
javaHome: null,
javaExec: "java",
shortenCommandLine: null,
errors: build === "succeeded" ? [] : [{ path: "/p/app/src/Main.java", line: 1, column: 1, message: "boom" }],
errorCount: build === "succeeded" ? 0 : 1,
hints: [],
});
it.each(["noBuild", "rebuild"] as const)("passes the %s build policy to the resolver", async (buildPolicy) => {
await startRun({ ...config, module: "app", buildPolicy });
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, buildPolicy, expect.any(Number));
});
it("always applies the launch policy after a Before launch Build Project", async () => {
useProject.setState({ project: { root: "/p" } as never });
await startRun({ ...config, module: "app" });
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
useProject.setState({ project: null });
});
it("does not start a configuration whose build failed", async () => {
ipc.javaResolveLaunch.mockResolvedValueOnce({ status: "ok", data: launchInfo("failed") });
const session = await startRun({ ...config, module: "app", buildPolicy: "make" });
expect(session?.status).toBe("failed");
expect(ipc.runStart).not.toHaveBeenCalled();
});
it("starts with the last compiled classes for makeAndContinueOnErrors", async () => {
ipc.javaResolveLaunch.mockResolvedValueOnce({ status: "ok", data: launchInfo("withErrors") });
const session = await startRun({ ...config, module: "app", buildPolicy: "makeAndContinueOnErrors" });
expect(session?.status).toBe("running");
expect(ipc.runStart).toHaveBeenCalled();
});
it.each([undefined, "automatic", "make", "rebuild"] as const)(
"does not launch scoped compilation-error bytecode under %s",
async (buildPolicy) => {
ipc.javaResolveLaunch.mockResolvedValueOnce({ status: "ok", data: launchInfo("withErrors") });
const session = await startRun({ ...config, module: "app", buildPolicy });
expect(session?.status).toBe("failed");
expect(ipc.runStart).not.toHaveBeenCalled();
},
);
it("finds the source module for an old saved configuration without a module", async () => {
await startRun({ ...config, cwd: "/outside/modules" });
expect(ipc.findFiles).toHaveBeenCalledWith("Main.java", 1000);
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, true, expect.any(Number));
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ cwd: "/outside/modules" }), expect.anything());
});
@@ -96,7 +146,7 @@ it.each([["-cp", "lib/*"], []])("colours Spring Boot runs without classpath reso
expect.objectContaining({ args: ["-Dspring.output.ansi.enabled=always", ...args] }),
expect.anything(),
);
expect(ipc.javaResolveLaunch).not.toHaveBeenCalled();
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
expect(boot.args).toBe(args);
expect(args).not.toContain("-Dspring.output.ansi.enabled=always");
});
@@ -105,14 +155,14 @@ it.each(["never", "detect", "always"])("preserves an explicit Spring Boot consol
const args = [`-Dspring.output.ansi.enabled=${value}`, "-cp", "lib/*", "a.Main"];
await startRun({ ...config, kind: "spring-boot", args });
expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ args }), expect.anything());
expect(ipc.javaResolveLaunch).not.toHaveBeenCalled();
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
});
it("keeps plain Java runs with an explicit classpath free of Spring Boot options", async () => {
const args = ["-cp", "lib/*", "a.Main"];
await startRun({ ...config, args });
expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ args }), expect.anything());
expect(ipc.javaResolveLaunch).not.toHaveBeenCalled();
expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, "automatic", expect.any(Number));
});
it("adds the Spring Boot console option once when resolving the module classpath", async () => {
@@ -197,3 +247,17 @@ it("translates import recovery guidance for a root Spring Boot configuration", a
),
).rejects.toThrow("Java 模块 '.' 尚未导入");
});
it("does not resolve or launch when saving fails", async () => {
const saveAll = vi.fn().mockRejectedValue(new Error("save failed"));
await expect(
prepareJavaLaunch(config, {
request: 1,
signal: new AbortController().signal,
saveAll,
}),
).rejects.toThrow("save failed");
await expect(prepareJavaModule(config, saveAll)).rejects.toThrow("save failed");
expect(ipc.javaResolveLaunch).not.toHaveBeenCalled();
expect(ipc.runStart).not.toHaveBeenCalled();
});
+61
Переглянути файл
@@ -1,3 +1,4 @@
import { useBuildModel } from "@/platform/buildModel";
import { useBuildPresentation } from "@/platform/projectPresentation";
import { useProject } from "@/platform/project";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -116,6 +117,22 @@ describe("configuration types", () => {
expect(formParams(saved)).toMatchObject({ mainClass: "a.Main", module: "." });
});
it("sets the before launch build policy of a Java configuration", async () => {
const type = typeFor("application")!;
const config = withParams(newConfigOfType(type, []), {
...newConfigOfType(type, []).params,
mainClass: "a.Main",
});
const changed = vi.fn();
render(<ConfigForm config={config} all={[config]} type={type} errors={{}} onChange={changed} modifyRequest={0} />);
fireEvent.click(screen.getByRole("button", { name: /Modify options/ }));
fireEvent.click(await screen.findByText("Build"));
expect(screen.getByRole("button", { name: "Build" }).textContent).toContain("Automatic");
fireEvent.click(screen.getByRole("button", { name: "Build" }));
fireEvent.click(await screen.findByRole("option", { name: "Rebuild" }));
expect(changed).toHaveBeenCalledWith(expect.objectContaining({ buildPolicy: "rebuild" }));
});
it.each(["spring-boot", "tomcat"])("edits shared update policies in the %s form", async (kind) => {
const type = typeFor(kind)!;
const original = newConfigOfType(type, []);
@@ -700,6 +717,16 @@ describe("before launch tasks", () => {
useBuildPresentation.setState({ modules: new Map() });
disposeBuild();
});
it("shows the build policy for a Maven configuration", async () => {
const type = typeFor("maven")!;
const config = newConfigOfType(type, []);
const changed = vi.fn();
render(<ConfigForm config={config} all={[config]} type={type} errors={{}} onChange={changed} modifyRequest={0} />);
fireEvent.click(screen.getByRole("button", { name: /Modify options/ }));
fireEvent.click(await screen.findByText("Build"));
expect(screen.getByRole("button", { name: "Build" })).toBeTruthy();
});
});
describe("environment variables", () => {
@@ -866,3 +893,37 @@ describe("new Gradle wrapper default", () => {
},
);
});
it.each(["maven", "gradle", "delegated", "absolute", "legacy"])(
"offers only supported build policies for %s",
(tool) => {
const kind = tool === "maven" || tool === "gradle" ? tool : "application";
const config = cfg("main", { kind, module: tool === "legacy" ? null : "app", buildPolicy: "make" });
const previous = useProject.getState();
if (kind === "application") {
useProject.setState({ project: { root: "/work" } as never, settings: null });
useBuildModel.setState({
model: { gradle: [{ projects: [{ dir: tool === "absolute" ? "/work/app" : "app" }] }] } as never,
});
}
try {
render(
<ConfigForm
config={config}
all={[config]}
type={typeFor(kind)}
errors={{}}
onChange={vi.fn()}
modifyRequest={0}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Build" }));
expect(screen.queryByRole("option", { name: "No build" })).toBeNull();
expect(screen.queryByRole("option", { name: "Build, no error check" }) !== null).toBe(tool === "maven");
expect(screen.getByRole("option", { name: "Rebuild" })).toBeTruthy();
} finally {
useBuildModel.setState({ model: null });
useProject.setState(previous);
}
},
);
+10 -3
Переглянути файл
@@ -19,9 +19,9 @@ import {
type TestCaseReport,
} from "@/platform/ipc";
import {
consoleVmArgs,
findJavaModule,
isJvmApplication,
JavaBuildFailedError,
parseJavaArgs,
prepareJavaLaunch,
} from "@/platform/javaLaunch";
@@ -30,6 +30,7 @@ import { useProject } from "@/platform/project";
import { executeAction } from "@/platform/actions";
import { workflowSettings } from "@/platform/workflowSettings";
import { maybeEditorService, notifications, toolWindowService, useDebugRunSessions } from "@/workbench/contributions";
import { applyBuildPolicyToCommand } from "./buildCommand";
import { ConsoleModel } from "./console";
import { jvmFrameTarget } from "./links";
import { confirmRerun, confirmTerminate } from "./dialogs";
@@ -851,6 +852,8 @@ export async function startRun(
};
try {
let launch = options.launch ?? config;
// A Maven/Gradle configuration's own command is its build; Rebuild adds clean.
if (!options.launch) launch = applyBuildPolicyToCommand(launch);
// "Build and run using: Gradle": the module's Gradle build compiles and launches it.
const delegated = isJvmApplication(launch) ? await gradleDelegation(launch) : null;
if (runtimes.get(sessionId)?.generation !== generation) return getSession(sessionId) ?? null;
@@ -931,7 +934,8 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
say(`No main class in '${config.name}': expected java [options] -cp \${classpath} <main class> [args]\n`, "stderr");
return null;
}
if (!parsed.classpath) return { ...config, args: [...consoleVmArgs(config), ...config.args] };
// Always prepare, exactly like Debug: even an explicit classpath needs the module, the
// project JDK and the selected build, and both modes must share the same result.
const rt = runtime(sessionId)!;
const request = { id: nextRequestId(), controller: new AbortController() };
rt.resolveRequest = request;
@@ -940,6 +944,9 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
request: request.id,
signal: request.controller.signal,
report: say,
saveAll: async () => {
await maybeEditorService()?.saveAll();
},
});
// No installed JDK is new enough for the module: offer to download one.
const jdkHint = info.hints.find((h) => downloadJdkActions(h).length);
@@ -959,7 +966,7 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
if (runtimes.get(sessionId) === rt && rt.resolveRequest === request) patchSession(sessionId, { stopped: true });
return null;
}
say(`Cannot resolve the classpath: ${message}\n`, "stderr");
say(e instanceof JavaBuildFailedError ? `${message}\n` : `Cannot resolve the classpath: ${message}\n`, "stderr");
// jdtls can't start without a newer JDK: "Download JDK 25".
const actions = downloadJdkActions(message);
notifications().notify({
+1 -1
Переглянути файл
@@ -278,7 +278,7 @@ describe("updating a running Tomcat deployment", () => {
it("compiles the selected WAR module and copies its outputs instead of rebuilding the WAR", async () => {
const c = config({ deployment: "artifact", artifact: "maven:web/pom.xml" });
expect(await syncTomcatOutputs(c, false)).toBe(true);
expect(commands.javaResolveLaunch).toHaveBeenCalledWith("web", false, true, expect.anything());
expect(commands.javaResolveLaunch).toHaveBeenCalledWith("web", false, "make", expect.anything());
expect(commands.runTomcatSync).toHaveBeenCalledWith(c, "web", ["/p/web/target/classes", "/m2/lib.jar"], false);
});
+3 -1
Переглянути файл
@@ -161,7 +161,9 @@ export async function syncTomcatOutputs(config: RunConfig, resourcesOnly: boolea
const artifacts = await call(commands.runTomcatArtifacts());
const artifact = p.artifact ? artifacts.find((a) => a.id === p.artifact) : artifacts[0];
if (!artifact) return false;
const info = await call(commands.javaResolveLaunch(artifact.module, false, true, nextRequestId())).catch(() => null);
const info = await call(commands.javaResolveLaunch(artifact.module, false, "make", nextRequestId())).catch(
() => null,
);
if (!info) return false;
if (info.build === "failed" || info.build === "withErrors" || info.build === "cancelled")
throw new Error(tx("Application update build failed"));
+30 -1
Переглянути файл
@@ -243,6 +243,34 @@ describe("catalogs", () => {
const COMPLETE = ["zh"];
/** Written in another script, so a text equal to its English source was not translated. */
const OWN_SCRIPT = new Set(["zh", "ko"]);
/** Product names intentionally kept as written in every locale. */
const SAME_AS_ENGLISH = new Set([
"Docker",
"Shell",
"Java",
"Python",
"C/C++",
"C#",
"Go",
"Rust",
"Kotlin",
"PHP",
"SQL",
"JavaScript",
"TypeScript",
"HTML",
"CSS",
"JSON",
"YAML",
"XML",
"Maven",
"Gradle",
"npm",
"pnpm",
"Yarn",
"Cargo",
"CMake",
]);
const translated = SUPPORTED_LOCALES.filter((locale) => locale !== "en").sort();
it("exist for exactly the supported locales", () => {
@@ -259,7 +287,8 @@ describe("catalogs", () => {
it("translate source text into text different from English, in locales with another script", () => {
for (const [locale, catalog] of Object.entries(texts))
if (OWN_SCRIPT.has(locale))
for (const [source, text] of Object.entries(catalog)) expect(text, `${locale}:${source}`).not.toBe(source);
for (const [source, text] of Object.entries(catalog))
if (!SAME_AS_ENGLISH.has(source)) expect(text, `${locale}:${source}`).not.toBe(source);
});
it("keep the placeholders of source-text templates", () => {
+43 -7
Переглянути файл
@@ -4,7 +4,15 @@
* `${classpath}` (or `${testClasspath}`) and the `java` launcher are resolved
* through jdtls when the configuration starts.
*/
import { call, commands, IpcError, type JavaLaunchInfo, type RunConfig, type RequestId } from "@/platform/ipc";
import {
call,
commands,
IpcError,
type BuildPolicy,
type JavaLaunchInfo,
type RunConfig,
type RequestId,
} from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { useBuildPresentation } from "@/platform/projectPresentation";
@@ -170,25 +178,47 @@ export async function resolveJavaModule(config: RunConfig): Promise<string> {
return module;
}
/** Saves before resolving the module, for direct and delegated Run and Debug. */
export async function prepareJavaModule(config: RunConfig, saveAll?: () => Promise<void>, signal?: AbortSignal) {
signal?.throwIfAborted();
await saveAll?.();
signal?.throwIfAborted();
const module = await resolveJavaModule(config);
signal?.throwIfAborted();
return module;
}
/** Shared, cancellable build and classpath preparation for Run and Debug. */
export interface JavaPreparation {
request: RequestId;
signal: AbortSignal;
report?: (text: string, kind?: "system" | "stderr") => void;
/** Before launch build; unset reads the run configuration (default: automatic/incremental). */
policy?: BuildPolicy;
/** Saves open documents first, as IDEA's Before launch does. */
saveAll?: () => Promise<void>;
}
/** The build the launch would do: the configuration's policy, else incremental. */
export function buildPolicyOf(config: RunConfig): BuildPolicy {
return config.buildPolicy ?? "automatic";
}
/** A build stopped the launch: the message is ready for the console and the notification. */
export class JavaBuildFailedError extends Error {}
export async function prepareJavaLaunch(config: RunConfig, preparation: JavaPreparation) {
const { signal, report } = preparation;
const policy = preparation.policy ?? buildPolicyOf(config);
signal.throwIfAborted();
const parsed = parseJavaArgs(config.args);
if (!parsed.mainClass) throw new Error(`No main class in '${config.name}'`);
report?.(tx`Building and resolving the classpath of ${parsed.mainClass}…` + "\n");
const started = performance.now();
const module = await resolveJavaModule(config);
signal.throwIfAborted();
const module = await prepareJavaModule(config, preparation.saveAll, signal);
const moduleMs = performance.now() - started;
const info = await call(
commands.javaResolveLaunch(module, parsed.classpath === "test", true, preparation.request),
commands.javaResolveLaunch(module, parsed.classpath === "test", policy, preparation.request),
).catch((error: unknown) => {
if (
error instanceof IpcError &&
@@ -204,11 +234,17 @@ export async function prepareJavaLaunch(config: RunConfig, preparation: JavaPrep
});
signal.throwIfAborted();
console.info("Java launch preparation", { module, moduleMs, resolveMs: performance.now() - started - moduleMs });
if (info.build === "withErrors")
report?.(tx("Build completed with errors; launching with the last compiled classes.") + "\n", "stderr");
if (info.build === "failed") report?.(tx("Build failed; launching with the last compiled classes.") + "\n", "stderr");
const errors = formatCompileErrors(info);
if (errors) report?.(errors, "stderr");
// IDEA's default: a failed build (or one with compile errors) does not start the program;
// only "Build, no error check" continues with the last compiled classes.
if ((info.build === "failed" || info.build === "withErrors") && policy !== "makeAndContinueOnErrors") {
const reason =
info.build === "failed"
? tx`Build failed; '${config.name}' was not started.`
: tx`Build has compile errors; '${config.name}' was not started.`;
throw new JavaBuildFailedError(reason);
}
if (parsed.classpath && !info.classPaths.length)
throw new Error(tx("The classpath is empty: is the module imported by the Java language server?"));
return { module, info, resolved: resolveJavaCommand(config, info) };
+8
Переглянути файл
@@ -540,5 +540,13 @@
"dev,integration": "dev,integration",
"Comma-separated profiles (-P), combined with Maven tool window choices; !id excludes a profile. A run configuration’s own -P replaces these profiles.": "Comma-separated profiles (-P), combined with Maven tool window choices; !id excludes a profile. A run configuration’s own -P replaces these profiles.",
"Active profiles": "Active profiles",
"Build failed; '{name}' was not started.": "Build failed; '{name}' was not started.",
"Build has compile errors; '{name}' was not started.": "Build has compile errors; '{name}' was not started.",
"Automatic (incremental)": "Automatic (incremental)",
"Build (Make)": "Build (Make)",
"Rebuild": "Rebuild",
"No build": "No build",
"Build, no error check": "Build, no error check",
"Before launch build for this Java configuration": "Before launch build for this Java configuration",
"the bundle of {gemfile} has no `debug` gem. Add `gem \"debug\"` to the Gemfile and run `bundle install`": "the bundle of {gemfile} has no `debug` gem. Add `gem \"debug\"` to the Gemfile and run `bundle install`"
}
-2
Переглянути файл
@@ -3488,8 +3488,6 @@
"What should the agent do?": "¿Qué debe hacer el agente?",
"New worktree": "Nuevo árbol de trabajo",
"Building and resolving the classpath of {name}…": "Compilando y resolviendo el classpath de {name}…",
"Build completed with errors; launching with the last compiled classes.": "La compilación terminó con errores; iniciando con las últimas clases compiladas.",
"Build failed; launching with the last compiled classes.": "La compilación falló; iniciando con las últimas clases compiladas.",
"The classpath is empty: is the module imported by the Java language server?": "El classpath está vacío: ¿el servidor de lenguaje Java importó el módulo?",
"Launch cancelled.": "Inicio cancelado.",
"Starting debugger for {name}…": "Iniciando depurador para {name}…",
-2
Переглянути файл
@@ -3354,8 +3354,6 @@
"What should the agent do?": "Que doit faire l'agent ?",
"New worktree": "Nouveau worktree",
"Building and resolving the classpath of {name}…": "Compilation et résolution du classpath de {name}…",
"Build completed with errors; launching with the last compiled classes.": "Build terminé avec des erreurs ; lancement avec les dernières classes compilées.",
"Build failed; launching with the last compiled classes.": "Échec du build ; lancement avec les dernières classes compilées.",
"The classpath is empty: is the module imported by the Java language server?": "Le classpath est vide : le module est-il importé par le serveur de langage Java ?",
"Launch cancelled.": "Lancement annulé.",
"Starting debugger for {name}…": "Démarrage du débogueur pour {name}…",
-2
Переглянути файл
@@ -3543,8 +3543,6 @@
"What should the agent do?": "에이전트가 무엇을 해야 하나요?",
"New worktree": "새 워크트리",
"Building and resolving the classpath of {name}…": "{name}의 클래스 경로를 빌드하고 해결하는 중…",
"Build completed with errors; launching with the last compiled classes.": "빌드가 오류와 함께 완료되었습니다. 마지막으로 컴파일된 클래스로 실행합니다.",
"Build failed; launching with the last compiled classes.": "빌드에 실패했습니다. 마지막으로 컴파일된 클래스로 실행합니다.",
"The classpath is empty: is the module imported by the Java language server?": "클래스 경로가 비어 있습니다. Java 언어 서버에서 모듈을 가져왔습니까?",
"Launch cancelled.": "실행이 취소되었습니다.",
"Starting debugger for {name}…": "{name}의 디버거 시작 중…",
-2
Переглянути файл
@@ -3460,8 +3460,6 @@
"What should the agent do?": "Co agent powinien zrobić?",
"New worktree": "Nowe worktree",
"Building and resolving the classpath of {name}…": "Kompilowanie i ustalanie ścieżki klas (classpath) dla {name}…",
"Build completed with errors; launching with the last compiled classes.": "Kompilacja zakończona z błędami; uruchamianie z ostatnio skompilowanymi klasami.",
"Build failed; launching with the last compiled classes.": "Kompilacja nie powiodła się; uruchamianie z ostatnio skompilowanymi klasami.",
"The classpath is empty: is the module imported by the Java language server?": "Ścieżka klas jest pusta: czy moduł został zaimportowany przez serwer językowy Java?",
"Launch cancelled.": "Uruchomienie anulowane.",
"Starting debugger for {name}…": "Uruchamianie debugera dla {name}…",
-2
Переглянути файл
@@ -3461,8 +3461,6 @@
"What should the agent do?": "O que o agente deve fazer?",
"New worktree": "Novo worktree",
"Building and resolving the classpath of {name}…": "Compilando e resolvendo o classpath de {name}…",
"Build completed with errors; launching with the last compiled classes.": "Build concluído com erros; iniciando com as últimas classes compiladas.",
"Build failed; launching with the last compiled classes.": "Falha no build; iniciando com as últimas classes compiladas.",
"The classpath is empty: is the module imported by the Java language server?": "O classpath está vazio: o módulo foi importado pelo servidor de linguagem Java?",
"Launch cancelled.": "Inicialização cancelada.",
"Starting debugger for {name}…": "Iniciando o depurador para {name}…",
+21 -4
Переглянути файл
@@ -1581,7 +1581,6 @@
"Open in New Window": "在新窗口中打开",
"Open in Terminal": "在终端中打开",
"Open in editor": "在编辑器中打开",
"Open main menu with a lone Alt press": "单独按下 Alt 打开主菜单",
"Open moved files in editor": "在编辑器中打开已移动的文件",
"Open new tabs at the end": "在末尾打开新标签页",
"Open on GitHub": "在 GitHub 上打开",
@@ -3768,8 +3767,6 @@
"What should the agent do?": "让 agent 做点什么?",
"New worktree": "新 worktree",
"Building and resolving the classpath of {name}…": "正在构建并解析 {name} 的类路径…",
"Build completed with errors; launching with the last compiled classes.": "构建完成但存在错误;使用上次编译的类启动。",
"Build failed; launching with the last compiled classes.": "构建失败;使用上次编译的类启动。",
"The classpath is empty: is the module imported by the Java language server?": "类路径为空:Java 语言服务器是否已导入此模块?",
"Launch cancelled.": "启动已取消。",
"Starting debugger for {name}…": "正在启动 {name} 的调试器…",
@@ -5088,7 +5085,6 @@
"Only one of these runs at a time. Automatic uses the first one available: {order}.": "这些服务器同一时间只运行一个。自动模式使用第一个可用的:{order}。",
"Configure the settings related to version control used in your project": "配置项目中使用的版本控制相关设置",
"Use non-modal commit interface": "使用非模态提交界面",
"When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "关闭后,Alt 组合键仍然可用,但单独按下 Alt 不会聚焦主菜单",
"When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "关闭时,本地更改和贮藏作为 Git 工具窗口的标签页显示,提交时打开对话框。",
"Clear initial commit message": "清除初始提交消息",
"Show options before Update": "更新前显示选项",
@@ -6876,5 +6872,26 @@
"Select Maven toolchains.xml": "选择 Maven toolchains.xml",
"dev,integration": "开发,集成",
"Comma-separated profiles (-P), combined with Maven tool window choices; !id excludes a profile. A run configuration’s own -P replaces these profiles.": "以逗号分隔的 profiles(-P),与 Maven 工具窗口中的选择合并;!id 排除指定 profile。运行配置自身的 -P 会替代这些 profiles。",
"Build failed; '{name}' was not started.": "构建失败;'{name}' 未启动。",
"Build has compile errors; '{name}' was not started.": "构建存在编译错误;'{name}' 未启动。",
"Automatic (incremental)": "自动(增量)",
"Build (Make)": "构建(Make)",
"Rebuild": "重新构建",
"No build": "不构建",
"Build, no error check": "构建,不检查错误",
"Before launch build for this Java configuration": "此 Java 配置启动前的构建方式",
"Docker": "Docker",
"Shell": "Shell",
"C/C++": "C/C++",
"Go": "Go",
"Rust": "Rust",
"Python": "Python",
"JavaScript": "JavaScript",
"Java": "Java",
"Add Service ({kind})": "添加服务({kind})",
"Others": "其他",
"Servers": "服务器",
"Open main menu with a lone Alt press": "单独按下 Alt 打开主菜单",
"When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "关闭后,Alt 组合键仍然可用,但单独按下 Alt 不会聚焦主菜单",
"the bundle of {gemfile} has no `debug` gem. Add `gem \"debug\"` to the Gemfile and run `bundle install`": "{gemfile} 的 bundle 中没有 `debug` gem。请在 Gemfile 中添加 `gem \"debug\"`,然后运行 `bundle install`"
}