дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(build): link missing tool and JDK errors to recovery (#2335)
Цей коміт міститься в:
1 джерело
e2f22de824
коміт
0b1f723b9a
11 files changed
+260
-8
No files matched your search
@@ -23,7 +23,7 @@ import {
|
||||
type RunConfig,
|
||||
type SyncProgress,
|
||||
} from "@/platform/ipc";
|
||||
import { downloadJdkActions } from "@/platform/jdk";
|
||||
import { buildToolRecovery } from "@/platform/buildToolRecovery";
|
||||
import { useBuildModel } from "@/platform/buildModel";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { editorService, notifications } from "@/workbench/contributions";
|
||||
@@ -454,9 +454,8 @@ export async function syncTool(tool: BuildTool, buildDir: string | null = null):
|
||||
group: "run",
|
||||
level: "warning",
|
||||
title: tool === "maven" ? "Maven sync finished with errors" : "Gradle sync failed",
|
||||
body: error,
|
||||
...buildToolRecovery(error),
|
||||
key: `build-sync-${tool}`,
|
||||
actions: downloadJdkActions(error),
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isCancelled(e))
|
||||
@@ -464,7 +463,7 @@ export async function syncTool(tool: BuildTool, buildDir: string | null = null):
|
||||
group: "run",
|
||||
level: "error",
|
||||
title: tool === "maven" ? "Cannot reload Maven projects" : "Cannot reload Gradle projects",
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
...buildToolRecovery(e instanceof Error ? e.message : String(e)),
|
||||
key: `build-sync-${tool}`,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
prepareJavaLaunch,
|
||||
} from "@/platform/javaLaunch";
|
||||
import { downloadJdkActions } from "@/platform/jdk";
|
||||
import { buildToolRecovery } from "@/platform/buildToolRecovery";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
@@ -930,7 +931,7 @@ export async function startRun(
|
||||
group: "run",
|
||||
level: "error",
|
||||
title: `Error running '${remoteLabel(config)}'`,
|
||||
body: message,
|
||||
...buildToolRecovery(message),
|
||||
});
|
||||
}
|
||||
return getSession(sessionId) ?? null;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
|
||||
const executeAction = vi.hoisted(() => vi.fn());
|
||||
vi.mock("./actions", async (original) => ({
|
||||
...(await original<typeof import("./actions")>()),
|
||||
executeAction,
|
||||
}));
|
||||
import { buildToolRecovery } from "./buildToolRecovery";
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
it.each([
|
||||
["cannot start mvn: No such file or directory (os error 2)", "maven"],
|
||||
["command not found: mvn", "maven"],
|
||||
["command not found: /opt/apache maven/bin/mvn", "maven"],
|
||||
["command not found: C:\\Maven\\bin\\mvn.cmd", "maven"],
|
||||
["cannot start gradle: No such file or directory (os error 2)", "gradle"],
|
||||
["command not found: gradle", "gradle"],
|
||||
["command not found: C:\\Gradle\\bin\\gradle.bat", "gradle"],
|
||||
["command not found: ./gradlew", "gradle"],
|
||||
])("links missing build tool to its settings: %s", (message, tool) => {
|
||||
const result = buildToolRecovery(message);
|
||||
expect(result.body).toContain(`${tool === "maven" ? "Maven" : "Gradle"} could not be started`);
|
||||
expect(result.body).toContain(message);
|
||||
expect(result.actions).toHaveLength(1);
|
||||
result.actions[0]!.run();
|
||||
expect(executeAction).toHaveBeenCalledWith("file.settings", `build.${tool}`);
|
||||
});
|
||||
|
||||
it("links the reported Gradle import rejection to the Gradle JVM picker and its JDK download", () => {
|
||||
const message =
|
||||
"Nov 9, 2026, 3:49:11 PM Can't use Java 27.0.1.0.3 and Gradle 9.7.1 to import Gradle project kotlin-gradle.";
|
||||
const result = buildToolRecovery(message);
|
||||
expect(result.body).toContain("Download JDK");
|
||||
expect(result.actions[0]!.label).toBe("Choose Gradle JVM");
|
||||
result.actions[0]!.run();
|
||||
expect(executeAction).toHaveBeenCalledWith("file.settings", "build.gradle");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"command not found: npm",
|
||||
"Maven tests failed",
|
||||
"Gradle build failed",
|
||||
"cannot start mvn: Permission denied (os error 13)",
|
||||
])("leaves unrelated failures alone: %s", (message) => {
|
||||
expect(buildToolRecovery(message)).toEqual({ body: message, actions: [] });
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { executeAction } from "./actions";
|
||||
import { tx } from "./i18n";
|
||||
import { downloadJdkActions } from "./jdk";
|
||||
|
||||
/** Recovery shared by build sync, run configurations and language-server import errors. */
|
||||
export function buildToolRecovery(message: string) {
|
||||
const actions = downloadJdkActions(message);
|
||||
const incompatible = /Can't use Java .* and Gradle .* to import Gradle project/i.test(message);
|
||||
const missing = /command not found: ([^\r\n]+)|cannot start ([^\r\n]+?): [^\r\n]*\(os error [23]\)/i.exec(message);
|
||||
const command = (missing?.[1] ?? missing?.[2] ?? "").trim().replace(/^["'`]|["'`]$/g, "");
|
||||
const namedTool = /(?:^|[/\\])(?:mvn|mvnw|mvnd)(?:\.(?:cmd|bat|exe))?$/i.test(command)
|
||||
? "maven"
|
||||
: /(?:^|[/\\])gradlew?(?:\.(?:bat|exe))?$/i.test(command)
|
||||
? "gradle"
|
||||
: undefined;
|
||||
const recoveryTool = incompatible ? "gradle" : namedTool;
|
||||
if (!recoveryTool) return { body: message, actions };
|
||||
const label =
|
||||
recoveryTool === "maven"
|
||||
? tx("Open Maven Settings")
|
||||
: incompatible
|
||||
? tx("Choose Gradle JVM")
|
||||
: tx("Open Gradle Settings");
|
||||
actions.push({ label, run: () => void executeAction("file.settings", `build.${recoveryTool}`) });
|
||||
const hint = incompatible
|
||||
? tx(
|
||||
"Choose a JDK compatible with this Gradle version in Gradle JVM. Download JDK is available in the JVM selector.",
|
||||
)
|
||||
: recoveryTool === "maven"
|
||||
? tx(
|
||||
"Maven could not be started. Configure Maven home in Maven settings, install Maven on PATH, or use a project with a Maven wrapper.",
|
||||
)
|
||||
: tx(
|
||||
"Gradle could not be started. Configure a local installation in Gradle settings, install Gradle on PATH, or use a project with a Gradle wrapper.",
|
||||
);
|
||||
return { body: `${hint}\n\n${message}`, actions };
|
||||
}
|
||||
@@ -735,5 +735,11 @@
|
||||
"Select a .NET project": "Select a .NET project",
|
||||
"Project evaluation failed": "Project evaluation failed",
|
||||
"dotnet exited with code {0}": "dotnet exited with code {0}",
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "Microsoft.Testing.Platform was not found in the restored project."
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "Microsoft.Testing.Platform was not found in the restored project.",
|
||||
"Open Maven Settings": "Open Maven Settings",
|
||||
"Open Gradle Settings": "Open Gradle Settings",
|
||||
"Choose Gradle JVM": "Choose Gradle JVM",
|
||||
"Choose a JDK compatible with this Gradle version in Gradle JVM. Download JDK is available in the JVM selector.": "Choose a JDK compatible with this Gradle version in Gradle JVM. Download JDK is available in the JVM selector.",
|
||||
"Maven could not be started. Configure Maven home in Maven settings, install Maven on PATH, or use a project with a Maven wrapper.": "Maven could not be started. Configure Maven home in Maven settings, install Maven on PATH, or use a project with a Maven wrapper.",
|
||||
"Gradle could not be started. Configure a local installation in Gradle settings, install Gradle on PATH, or use a project with a Gradle wrapper.": "Gradle could not be started. Configure a local installation in Gradle settings, install Gradle on PATH, or use a project with a Gradle wrapper."
|
||||
}
|
||||
@@ -7069,5 +7069,11 @@
|
||||
"Run test": "运行测试",
|
||||
"Project evaluation failed": "项目求值失败",
|
||||
"dotnet exited with code {0}": "dotnet 已退出,退出码 {0}",
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "还原后的项目中未找到 Microsoft.Testing.Platform。"
|
||||
"Microsoft.Testing.Platform was not found in the restored project.": "还原后的项目中未找到 Microsoft.Testing.Platform。",
|
||||
"Open Maven Settings": "打开 Maven 设置",
|
||||
"Open Gradle Settings": "打开 Gradle 设置",
|
||||
"Choose Gradle JVM": "选择 Gradle JVM",
|
||||
"Choose a JDK compatible with this Gradle version in Gradle JVM. Download JDK is available in the JVM selector.": "请在 Gradle JVM 中选择与此 Gradle 版本兼容的 JDK。JVM 选择器中提供下载 JDK 选项。",
|
||||
"Maven could not be started. Configure Maven home in Maven settings, install Maven on PATH, or use a project with a Maven wrapper.": "无法启动 Maven。请在 Maven 设置中配置 Maven 主目录,将 Maven 安装到 PATH,或使用带有 Maven wrapper 的项目。",
|
||||
"Gradle could not be started. Configure a local installation in Gradle settings, install Gradle on PATH, or use a project with a Gradle wrapper.": "无法启动 Gradle。请在 Gradle 设置中配置本地安装,将 Gradle 安装到 PATH,或使用带有 Gradle wrapper 的项目。"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { emitLocalIdeEvent } from "@/platform/ipc";
|
||||
import { setLocale } from "@/platform/i18n";
|
||||
import { registerStatusItem, registerToolWindow, type StatusItemContribution } from "./contributions";
|
||||
import { orderStatusItems, StatusBar, TaskStatus } from "./StatusBar";
|
||||
import { orderTitleItems } from "./TitleBar";
|
||||
@@ -200,6 +201,27 @@ describe("notifications", () => {
|
||||
uninstall();
|
||||
});
|
||||
|
||||
it.each(["en", "zh"])("adds Gradle JVM recovery before translating backend messages (%s)", async (locale) => {
|
||||
await setLocale(locale);
|
||||
const uninstall = installNotifyEvents();
|
||||
try {
|
||||
act(() =>
|
||||
emitLocalIdeEvent({
|
||||
type: "notify",
|
||||
level: "error",
|
||||
title: "jdtls",
|
||||
body: "Can't use Java 27.0.1.0.3 and Gradle 9.7.1 to import Gradle project kotlin-gradle.",
|
||||
}),
|
||||
);
|
||||
const item = useNotifications.getState().items[0]!;
|
||||
expect(item.actions?.map((a) => a.label)).toEqual([locale === "zh" ? "选择 Gradle JVM" : "Choose Gradle JVM"]);
|
||||
expect(item.body).toContain(locale === "zh" ? "下载 JDK" : "Download JDK");
|
||||
} finally {
|
||||
uninstall();
|
||||
await setLocale("en");
|
||||
}
|
||||
});
|
||||
|
||||
it("the Notifications tool window lists history, marks it read and hides balloons", () => {
|
||||
const dispose = registerToolWindow({
|
||||
id: NOTIFICATIONS_TOOL_WINDOW,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import type { BuildModel, ProjectInfo, RunConfig } from "@/platform/ipc";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ sync: vi.fn(), start: vi.fn(), notify: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
isTauri: () => true,
|
||||
commands: {
|
||||
...real.commands,
|
||||
buildSync: mocks.sync,
|
||||
runStart: mocks.start,
|
||||
runConfigs: async () => ({ status: "ok", data: [] }),
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@tauri-apps/api/core", async (original) => ({
|
||||
...(await original<typeof import("@tauri-apps/api/core")>()),
|
||||
Channel: class {
|
||||
onmessage: unknown = null;
|
||||
},
|
||||
}));
|
||||
|
||||
import { useProject } from "@/platform/project";
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
import { resetBuild, syncTool, useBuild } from "../features/build-tools/store";
|
||||
import { resetRuns, startRun } from "../features/run/store";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useProject.setState({ project: { root: "/p", trusted: true } as ProjectInfo });
|
||||
useBuild.setState({ root: "/p", syncing: {}, model: { maven: null, gradle: [] } });
|
||||
provideNotificationService({ notify: mocks.notify });
|
||||
});
|
||||
afterEach(() => {
|
||||
resetBuild();
|
||||
resetRuns();
|
||||
});
|
||||
|
||||
it.each(["maven", "gradle"] as const)("offers settings for %s sync failures, returned or thrown", async (tool) => {
|
||||
const command = tool === "maven" ? "mvn" : "gradle";
|
||||
const message = `cannot start ${command}: No such file or directory (os error 2)`;
|
||||
mocks.sync.mockRejectedValueOnce(new Error(message));
|
||||
await syncTool(tool);
|
||||
expect(mocks.notify).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
actions: [expect.objectContaining({ label: `Open ${tool === "maven" ? "Maven" : "Gradle"} Settings` })],
|
||||
body: expect.stringContaining(message),
|
||||
}),
|
||||
);
|
||||
const model = (tool === "maven"
|
||||
? { maven: { syncError: message }, gradle: [] }
|
||||
: { maven: null, gradle: [{ syncError: message }] }) as unknown as BuildModel;
|
||||
mocks.sync.mockResolvedValueOnce({ status: "ok", data: model });
|
||||
await syncTool(tool);
|
||||
expect(mocks.notify).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
actions: [expect.objectContaining({ label: `Open ${tool === "maven" ? "Maven" : "Gradle"} Settings` })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["maven", "gradle"] as const)("offers settings when a %s run cannot start", async (kind) => {
|
||||
const command = kind === "maven" ? "mvn" : "gradle";
|
||||
const config: RunConfig = {
|
||||
id: kind,
|
||||
name: `${command} test`,
|
||||
kind,
|
||||
command,
|
||||
args: ["test"],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
};
|
||||
mocks.start.mockRejectedValueOnce(new Error(`command not found: ${command}`));
|
||||
await startRun(config, undefined, undefined, { prepared: true, show: false });
|
||||
expect(mocks.start).toHaveBeenCalledOnce();
|
||||
expect(mocks.notify).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
actions: [expect.objectContaining({ label: `Open ${kind === "maven" ? "Maven" : "Gradle"} Settings` })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, onIdeEvent } from "@/platform/ipc";
|
||||
import { buildToolRecovery } from "@/platform/buildToolRecovery";
|
||||
import { txTemplate } from "@/platform/i18n";
|
||||
import { useProject } from "@/platform/project";
|
||||
import type { NotificationService } from "./contributions";
|
||||
@@ -202,6 +203,14 @@ export function installNotifyEvents(): () => void {
|
||||
return onIdeEvent(
|
||||
"notify",
|
||||
// Backend text may carry values (paths, names); `{name}` catalog templates translate it.
|
||||
(e) => void notify({ level: e.level, title: txTemplate(e.title), body: e.body ? txTemplate(e.body) : undefined }),
|
||||
(e) => {
|
||||
const recovery = e.body ? buildToolRecovery(e.body) : undefined;
|
||||
void notify({
|
||||
level: e.level,
|
||||
title: txTemplate(e.title),
|
||||
...recovery,
|
||||
body: recovery ? txTemplate(recovery.body) : undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача