дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(run): add Build Module and Build Artifacts before launch (#2155)
Adds Build Module and Build Artifacts to the Before launch tasks, next to Build Project. A launch now waits for the fresh build result and stops on a compile error, a cancelled or busy build, or a removed module; duplicate tasks are rejected and saved tasks are validated; the Build window keeps its visibility. Co-authored-by: misswell <[email protected]>
Цей коміт міститься в:
1 джерело
5c3feea2a8
коміт
cad33ef119
15 files changed
+291
-22
No files matched your search
@@ -73,7 +73,9 @@ test("a slow module load succeeds before the deadline and cancels the startup ti
|
||||
await gate;
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
const moduleRequest = page.waitForRequest("**/src/main.tsx");
|
||||
await page.goto("/", { waitUntil: "commit" });
|
||||
await moduleRequest;
|
||||
await page.locator("#splash").waitFor({ state: "attached" });
|
||||
await page.clock.runFor(25_000);
|
||||
await expect(page.locator("#boot-error")).toHaveCount(0);
|
||||
|
||||
@@ -294,18 +294,19 @@ test.describe("10,000-line files", () => {
|
||||
await page.keyboard.press("Meta+ArrowUp");
|
||||
await page.keyboard.press("F7");
|
||||
await expect(counter(page)).toHaveText("1 of 42 differences");
|
||||
const first = await rendered(page);
|
||||
if (opts.mode !== "unified") {
|
||||
expect(first.a.words).toContainEqual(["compute", "modified"]);
|
||||
expect(first.b.words).toContainEqual(["computeFast", "modified"]);
|
||||
await expect.poll(async () => (await rendered(page)).a.words).toContainEqual(["compute", "modified"]);
|
||||
await expect.poll(async () => (await rendered(page)).b.words).toContainEqual(["computeFast", "modified"]);
|
||||
}
|
||||
// The last difference is reachable and still highlighted.
|
||||
await page.keyboard.press("Meta+ArrowDown");
|
||||
await page.keyboard.press("Shift+F7");
|
||||
await expect(counter(page)).toHaveText("42 of 42 differences");
|
||||
const last = await rendered(page);
|
||||
if (opts.mode === "unified") expect(last.b.lines).toContain("inserted");
|
||||
else expect(last.b.words).toContainEqual(["computeFast", "modified"]);
|
||||
if (opts.mode === "unified") {
|
||||
await expect.poll(async () => (await rendered(page)).b.lines).toContain("inserted");
|
||||
} else {
|
||||
await expect.poll(async () => (await rendered(page)).b.words).toContainEqual(["computeFast", "modified"]);
|
||||
}
|
||||
await scrollToEnd(page);
|
||||
if (opts.mode !== "unified") {
|
||||
const [a, b] = await topLines(page);
|
||||
|
||||
@@ -6,6 +6,7 @@ for (const theme of ["light", "dark"] as const) {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto(`/e2e/httpHighlight.html?theme=${theme}${extension === "rest" ? "&rest" : ""}`);
|
||||
await expect(page.locator("#json .cm-line").first()).toBeVisible();
|
||||
const token = (text: string) =>
|
||||
page
|
||||
.locator("#http .cm-line span")
|
||||
|
||||
@@ -288,6 +288,42 @@ describe("build feature", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["build.buildModule", { target: "/p/core", activate: false, wait: true }, "build", "/p/core"],
|
||||
["build.buildArtifacts", { activate: false, wait: true }, "artifacts", null],
|
||||
])("awaits %s only when requested and keeps its successful build hidden", async (id, arg, kind, file) => {
|
||||
setContext("build.lastStatus", "success");
|
||||
let finished = false;
|
||||
const run = executeAction(id, arg).then(() => {
|
||||
finished = true;
|
||||
});
|
||||
await act(async () => {});
|
||||
expect(fake.calls.at(-1)).toMatchObject({ kind, file });
|
||||
expect(finished).toBe(false);
|
||||
expect(getContext()["build.lastStatus"]).toBeNull();
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
fake.resolve(result([], true));
|
||||
await run;
|
||||
});
|
||||
expect(finished).toBe(true);
|
||||
expect(getContext()["build.lastStatus"]).toBe("success");
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["build.buildModule", "/p/core"],
|
||||
["build.buildModule", undefined],
|
||||
["build.buildArtifacts", undefined],
|
||||
])("keeps ordinary %s calls nonblocking (%s)", async (id, arg) => {
|
||||
await executeAction(id, arg);
|
||||
expect(useBuildRun.getState().status).toBe("running");
|
||||
expect(fake.calls.at(-1)?.file).toBe(arg ?? null);
|
||||
await act(async () => {
|
||||
fake.resolve(result([], true));
|
||||
});
|
||||
});
|
||||
|
||||
it("starts a build without a workbench", async () => {
|
||||
show.mockImplementation(() => {
|
||||
throw new Error("No workbench");
|
||||
|
||||
@@ -21,6 +21,7 @@ const WHEN = `projectOpen && !${BUILD_RUNNING}`;
|
||||
/** The module to build: the invoker's path (project view) or the active editor's file. */
|
||||
function moduleTarget(arg: unknown): string | null {
|
||||
if (typeof arg === "string" && arg) return arg;
|
||||
if (typeof arg === "object" && arg !== null && "target" in arg && typeof arg.target === "string") return arg.target;
|
||||
return maybeEditorService()?.activePath() ?? null;
|
||||
}
|
||||
|
||||
@@ -79,8 +80,10 @@ export function activate(): () => void {
|
||||
when: WHEN,
|
||||
keywords: ["recompile", "compile file", "make module"],
|
||||
run: ({ arg }) => {
|
||||
const target = moduleTarget(arg);
|
||||
void startBuild("build", target);
|
||||
const options = typeof arg === "object" && arg !== null ? arg : {};
|
||||
const activate = !("activate" in options && options.activate === false);
|
||||
const build = startBuild("build", moduleTarget(arg), activate);
|
||||
if ("wait" in options && options.wait === true) return build.then(() => undefined);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,7 +106,12 @@ export function activate(): () => void {
|
||||
category: "Build",
|
||||
when: WHEN,
|
||||
keywords: ["package", "assemble", "jar", "release"],
|
||||
run: () => void startBuild("artifacts"),
|
||||
run: ({ arg }) => {
|
||||
const options = typeof arg === "object" && arg !== null ? arg : {};
|
||||
const activate = !("activate" in options && options.activate === false);
|
||||
const build = startBuild("artifacts", null, activate);
|
||||
if ("wait" in options && options.wait === true) return build.then(() => undefined);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "build.stop",
|
||||
|
||||
@@ -139,6 +139,7 @@ export async function startBuild(
|
||||
const cancel = () => backend.cancel(request);
|
||||
const previous = useBuildRun.getState().files;
|
||||
useBuildRun.setState({ ...INITIAL, request, kind, file, title: taskTitle, status: "running", files: previous });
|
||||
setContext(BUILD_LAST_STATUS, null);
|
||||
setContext(BUILD_RUNNING, true);
|
||||
if (activate) {
|
||||
try {
|
||||
@@ -280,5 +281,6 @@ export function resetBuildRun() {
|
||||
stopBuild();
|
||||
removeTask(BUILD_TASK_ID);
|
||||
setContext(BUILD_RUNNING, false);
|
||||
setContext(BUILD_LAST_STATUS, null);
|
||||
useBuildRun.setState(INITIAL);
|
||||
}
|
||||
@@ -300,6 +300,13 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi
|
||||
})),
|
||||
},
|
||||
{ label: TASK_LABELS.build, icon: "build-project", run: () => addTask("build") },
|
||||
{
|
||||
label: TASK_LABELS.buildModule,
|
||||
icon: "build-project",
|
||||
disabled: config.module == null,
|
||||
run: () => addTask("buildModule", config.module ?? ""),
|
||||
},
|
||||
{ label: TASK_LABELS.buildArtifacts, icon: "build-rebuild", run: () => addTask("buildArtifacts") },
|
||||
...(config.kind === "dotnet"
|
||||
? [{ label: TASK_LABELS.dotnetBuild, icon: "build-project", run: () => addTask("dotnetBuild") }]
|
||||
: []),
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
*/
|
||||
import { buildCMake, isCMakeBuild } from "./cmake";
|
||||
import { dotnetBuildConfig } from "./dotnetTypes";
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { getContext } from "@/platform/context";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useBuildPresentation } from "@/platform/projectPresentation";
|
||||
import { buildTask } from "./buildTask";
|
||||
import { moduleTaskError } from "./configs";
|
||||
import { tx, txTemplate } from "@/platform/i18n";
|
||||
import type { BeforeLaunchTask, RunConfig } from "@/platform/ipc";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
@@ -14,13 +16,22 @@ import { externalToolConfig } from "./externalTools";
|
||||
import { startRun, useRun } from "./store";
|
||||
|
||||
export const BUILD_ACTION = "build.buildProject";
|
||||
const BUILD_STATUS = "build.lastStatus";
|
||||
|
||||
export type BeforeLaunchKind = "run" | "build" | "tool" | "command" | "toolWindow" | "dotnetBuild";
|
||||
export type BeforeLaunchKind =
|
||||
| "run"
|
||||
| "build"
|
||||
| "buildModule"
|
||||
| "buildArtifacts"
|
||||
| "tool"
|
||||
| "command"
|
||||
| "toolWindow"
|
||||
| "dotnetBuild";
|
||||
|
||||
export const TASK_LABELS: Record<BeforeLaunchKind, string> = {
|
||||
run: "Run Another Configuration",
|
||||
build: "Build Project",
|
||||
buildModule: "Build Module",
|
||||
buildArtifacts: "Build Artifacts",
|
||||
dotnetBuild: "Build .NET project",
|
||||
tool: "Run External Tool",
|
||||
command: "Run Shell Command",
|
||||
@@ -35,6 +46,10 @@ export function taskLabel(task: BeforeLaunchTask, configs: readonly RunConfig[]
|
||||
return `Run '${configs.find((c) => c.id === target)?.name ?? target}'`;
|
||||
case "build":
|
||||
return configs.find((c) => c.id === target)?.name ?? "Build Project";
|
||||
case "buildModule":
|
||||
return target ? `Build Module '${target}'` : "Build Module";
|
||||
case "buildArtifacts":
|
||||
return "Build Artifacts";
|
||||
case "dotnetBuild":
|
||||
return "Build .NET project";
|
||||
case "tool":
|
||||
@@ -73,10 +88,20 @@ async function runTask(
|
||||
return (await runToEnd(dotnetBuildConfig(config), chain, show)) ? null : "Build failed";
|
||||
case "build": {
|
||||
if (target) return buildCMake(target, show === true);
|
||||
if (!getAction(BUILD_ACTION)) return "Building is not available";
|
||||
if (!(await executeAction(BUILD_ACTION, { activate: false }))) return "A build is already running";
|
||||
return getContext()[BUILD_STATUS] === "success" ? null : "Build failed";
|
||||
return buildTask(BUILD_ACTION, { activate: false });
|
||||
}
|
||||
case "buildModule": {
|
||||
const error = moduleTaskError(config, target);
|
||||
if (error) return error;
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root) return "Build did not start";
|
||||
const relative = target.startsWith(root + "/") ? target.slice(root.length + 1) : target;
|
||||
if (!useBuildPresentation.getState().modules.has(relative)) return `Module '${target}' no longer exists`;
|
||||
const path = /^(?:[A-Za-z]:[\\/]|\/)/.test(target) ? target : `${root}/${target}`;
|
||||
return buildTask("build.buildModule", { target: path, activate: false, wait: true });
|
||||
}
|
||||
case "buildArtifacts":
|
||||
return buildTask("build.buildArtifacts", { activate: false, wait: true });
|
||||
case "run": {
|
||||
if (chain.includes(target)) return "The before launch tasks run in a cycle";
|
||||
const other = useRun.getState().configs.find((c) => c.id === target);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerAction } from "@/platform/actions";
|
||||
import { getContext, setContext } from "@/platform/context";
|
||||
import { buildTask } from "./buildTask";
|
||||
|
||||
let dispose: (() => void) | undefined;
|
||||
afterEach(() => {
|
||||
dispose?.();
|
||||
setContext("build.running", false);
|
||||
setContext("build.lastStatus", null);
|
||||
});
|
||||
|
||||
describe("before-launch build results", () => {
|
||||
it.each(["build.buildProject", "build.buildModule", "build.buildArtifacts"])(
|
||||
"awaits fresh results from %s",
|
||||
async (id) => {
|
||||
for (const [status, error] of [
|
||||
["success", null],
|
||||
["failed", "Build failed"],
|
||||
["cancelled", "Build cancelled"],
|
||||
[null, "Build did not start"],
|
||||
] as const) {
|
||||
setContext("build.lastStatus", "success");
|
||||
let finish!: () => void;
|
||||
dispose = registerAction({
|
||||
id,
|
||||
title: "Build",
|
||||
run: () =>
|
||||
new Promise<void>((resolve) => {
|
||||
finish = () => {
|
||||
setContext("build.lastStatus", status);
|
||||
resolve();
|
||||
};
|
||||
}),
|
||||
});
|
||||
let finished = false;
|
||||
const task = buildTask(id, { activate: false, wait: true }).then((value) => {
|
||||
finished = true;
|
||||
return value;
|
||||
});
|
||||
expect(getContext()["build.lastStatus"]).toBeNull();
|
||||
expect(finished).toBe(false);
|
||||
finish();
|
||||
expect(await task).toBe(error);
|
||||
dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("reports an existing build without changing its result", async () => {
|
||||
const run = vi.fn();
|
||||
dispose = registerAction({ id: "build.buildModule", title: "Build", run });
|
||||
setContext("build.running", true);
|
||||
setContext("build.lastStatus", "failed");
|
||||
expect(await buildTask("build.buildModule", {})).toBe("A build is already running");
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(getContext()["build.lastStatus"]).toBe("failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { getContext, setContext } from "@/platform/context";
|
||||
|
||||
/** Dispatches an awaitable build and consumes only its fresh completion status. */
|
||||
export async function buildTask(action: string, arg: unknown): Promise<string | null> {
|
||||
if (!getAction(action)) return "Building is not available";
|
||||
if (getContext()["build.running"]) return "A build is already running";
|
||||
setContext("build.lastStatus", null);
|
||||
if (!(await executeAction(action, arg))) return "A build is already running";
|
||||
switch (getContext()["build.lastStatus"]) {
|
||||
case "success":
|
||||
return null;
|
||||
case "cancelled":
|
||||
return "Build cancelled";
|
||||
case "failed":
|
||||
return "Build failed";
|
||||
default:
|
||||
return "Build did not start";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { getContext } from "@/platform/context";
|
||||
import { buildTask } from "./buildTask";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
|
||||
export function isCMakeBuild(config: RunConfig): boolean {
|
||||
@@ -23,7 +22,5 @@ export function cmakeBuildId(config: RunConfig | undefined, configs: readonly Ru
|
||||
}
|
||||
|
||||
export async function buildCMake(id: string, activate = false): Promise<string | null> {
|
||||
if (!getAction("build.buildProject")) return "Building is not available";
|
||||
if (!(await executeAction("build.buildProject", { cmake: id, activate }))) return "A build is already running";
|
||||
return getContext()["build.lastStatus"] === "success" ? null : "Build failed";
|
||||
return buildTask("build.buildProject", { cmake: id, activate });
|
||||
}
|
||||
@@ -216,6 +216,12 @@ export function duplicateConfig(config: RunConfig, configs: readonly RunConfig[]
|
||||
/** Field errors: `name`, `command`, `env`, `cwd`, `compound`, `beforeLaunch` or a type param key. */
|
||||
export type ConfigErrors = Partial<Record<string, string>>;
|
||||
|
||||
export function moduleTaskError(config: RunConfig, target: string): string | null {
|
||||
if (config.module == null) return "Select a module for the Build Module task";
|
||||
if (config.module !== target) return "The Build Module task uses a different module; remove it and add it again";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateConfig(config: RunConfig, all: readonly RunConfig[] = []): ConfigErrors {
|
||||
const errors: ConfigErrors = {};
|
||||
if (!config.name.trim()) errors.name = "Name is required";
|
||||
@@ -235,7 +241,16 @@ export function validateConfig(config: RunConfig, all: readonly RunConfig[] = []
|
||||
(t.target === config.id || !all.some((c) => c.id === t.target))
|
||||
)
|
||||
errors.beforeLaunch = "A before launch task runs a missing configuration or this one";
|
||||
else if (t.kind !== "build" && t.kind !== "dotnetBuild" && t.kind !== "run" && !(t.target ?? "").trim())
|
||||
else if (t.kind === "buildModule") {
|
||||
const error = moduleTaskError(config, t.target ?? "");
|
||||
if (error) errors.beforeLaunch = error;
|
||||
} else if (
|
||||
t.kind !== "build" &&
|
||||
t.kind !== "buildArtifacts" &&
|
||||
t.kind !== "dotnetBuild" &&
|
||||
t.kind !== "run" &&
|
||||
!(t.target ?? "").trim()
|
||||
)
|
||||
errors.beforeLaunch = "A before launch task is incomplete";
|
||||
}
|
||||
for (const key of Object.keys(config.env)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useBuildPresentation } from "@/platform/projectPresentation";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
@@ -618,6 +619,87 @@ describe("before launch tasks", () => {
|
||||
setBeforeLaunchRunner(null);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([undefined, "", "core"])("validates saved build tasks and module selection (%s)", (module) => {
|
||||
const app = cfg("app", {
|
||||
module,
|
||||
beforeLaunch: [
|
||||
{ kind: "buildArtifacts", target: "" },
|
||||
{ kind: "buildModule", target: module ?? "" },
|
||||
],
|
||||
});
|
||||
expect(validateConfig(app).beforeLaunch).toBe(
|
||||
module == null ? "Select a module for the Build Module task" : undefined,
|
||||
);
|
||||
if (module != null) {
|
||||
expect(validateConfig({ ...app, module: "other" }).beforeLaunch).toContain("different module");
|
||||
}
|
||||
});
|
||||
|
||||
it.each([undefined, "", "core"])("offers Build Module only with a selected module (%s)", (module) => {
|
||||
const config = cfg("app", { module, beforeLaunch: [{ kind: "buildArtifacts", target: "" }] });
|
||||
const changed = vi.fn();
|
||||
render(
|
||||
<ConfigForm config={config} all={[config]} type={undefined} errors={{}} onChange={changed} modifyRequest={0} />,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add task" }));
|
||||
const item = screen.getByRole("menuitem", { name: "Build Module" });
|
||||
expect(item.getAttribute("aria-disabled")).toBe(module == null ? "true" : null);
|
||||
if (module != null) {
|
||||
fireEvent.click(item);
|
||||
expect(changed).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
beforeLaunch: [...config.beforeLaunch!, { kind: "buildModule", target: module }],
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses a removed module before dispatching its build", async () => {
|
||||
const run = vi.fn();
|
||||
const dispose = registerActions([{ id: "build.buildModule", title: "Build Module", run }]);
|
||||
useProject.setState({ project: { root: "/p" } as never });
|
||||
useBuildPresentation.setState({ modules: new Map() });
|
||||
try {
|
||||
const app = cfg("app", { module: "removed", beforeLaunch: [{ kind: "buildModule", target: "removed" }] });
|
||||
expect(await runBeforeLaunch(app, [app.id])).toBe(false);
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: expect.stringContaining("no longer exists") }),
|
||||
);
|
||||
} finally {
|
||||
dispose();
|
||||
useProject.setState({ project: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("runs Build Module and Build Artifacts before launch", async () => {
|
||||
const buildModule = vi.fn(async () => setContext("build.lastStatus", "success"));
|
||||
const artifacts = vi.fn(async () => setContext("build.lastStatus", "success"));
|
||||
const disposeBuild = registerActions([
|
||||
{ id: "build.buildModule", title: "Build Module", run: buildModule },
|
||||
{ id: "build.buildArtifacts", title: "Build Artifacts", run: artifacts },
|
||||
]);
|
||||
useProject.setState({ project: { root: "/p" } as never });
|
||||
useBuildPresentation.setState({ modules: new Map([["core", "maven"]]) });
|
||||
const app = cfg("app", {
|
||||
module: "core",
|
||||
beforeLaunch: [
|
||||
{ kind: "buildModule", target: "core" },
|
||||
{ kind: "buildArtifacts", target: "" },
|
||||
],
|
||||
});
|
||||
expect(await runBeforeLaunch(app, [app.id])).toBe(true);
|
||||
expect(buildModule).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ arg: { target: "/p/core", activate: false, wait: true } }),
|
||||
);
|
||||
expect(artifacts).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ arg: { activate: false, wait: true } }),
|
||||
);
|
||||
useProject.setState({ project: null });
|
||||
useBuildPresentation.setState({ modules: new Map() });
|
||||
disposeBuild();
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment variables", () => {
|
||||
|
||||
@@ -494,6 +494,14 @@
|
||||
"Could not copy. Select the details below and copy them.": "Could not copy. Select the details below and copy them.",
|
||||
"Startup error details": "Startup error details",
|
||||
"Startup did not finish within 30 seconds.": "Startup did not finish within 30 seconds.",
|
||||
"Build Module": "Build Module",
|
||||
"Build Artifacts": "Build Artifacts",
|
||||
"Build Module '{target}'": "Build Module '{target}'",
|
||||
"Build cancelled": "Build cancelled",
|
||||
"Build did not start": "Build did not start",
|
||||
"Select a module for the Build Module task": "Select a module for the Build Module task",
|
||||
"The Build Module task uses a different module; remove it and add it again": "The Build Module task uses a different module; remove it and add it again",
|
||||
"Module '{target}' no longer exists": "Module '{target}' no longer exists",
|
||||
"Choose Projects": "Choose Projects",
|
||||
"Add Selected": "Add Selected",
|
||||
"Projects found": "Projects found",
|
||||
|
||||
@@ -6836,6 +6836,12 @@
|
||||
"Could not copy. Select the details below and copy them.": "无法复制。请选择下方详情并手动复制。",
|
||||
"Startup error details": "启动错误详情",
|
||||
"Startup did not finish within 30 seconds.": "启动未在 30 秒内完成。",
|
||||
"Build Module '{target}'": "构建模块“{target}”",
|
||||
"Build cancelled": "构建已取消",
|
||||
"Build did not start": "构建未启动",
|
||||
"Select a module for the Build Module task": "请为构建模块任务选择模块",
|
||||
"The Build Module task uses a different module; remove it and add it again": "构建模块任务使用了不同的模块,请移除后重新添加",
|
||||
"Module '{target}' no longer exists": "模块“{target}”已不存在",
|
||||
"Choose Projects": "选择项目",
|
||||
"Add Selected": "添加所选项目",
|
||||
"Projects found": "找到的项目",
|
||||
|
||||
Reference in new issue
Заблокувати користувача