дзеркало
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
@@ -268,6 +268,12 @@ impl Handler for ClientHandler {
|
||||
"window/showMessage" => self.show_message(&mgr, ¶ms),
|
||||
"window/logMessage" => {
|
||||
let text = params.get("message").and_then(Value::as_str).unwrap_or_default();
|
||||
if self.server.name == "jdtls"
|
||||
&& text.contains("Can't use Java ")
|
||||
&& text.contains(" to import Gradle project ")
|
||||
{
|
||||
self.show_message(&mgr, &json!({ "type": 1, "message": text.lines().next().unwrap_or(text) }));
|
||||
}
|
||||
match params.get("type").and_then(Value::as_u64) {
|
||||
Some(1) if self.server.name == "jdtls" && text.contains(crate::jdtls::reserved_names::IMPORT_FAILURE) => {
|
||||
tracing::warn!("[{}] {text}", self.server.name);
|
||||
|
||||
@@ -5606,6 +5606,39 @@ async fn jdtls_import_failure_names_reserved_device_files_on_windows() {
|
||||
let _ = std::fs::remove_file(&nul);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn gradle_incompatible_jdk_log_is_surfaced_once_across_message_channels() {
|
||||
let h = harness_for("java", "java", &[("jdtls", incremental_caps())], &[]);
|
||||
let s = h.launcher.state("jdtls");
|
||||
h.open("A.java", "class A {}");
|
||||
h.wait_status("jdtls", ServerStatus::Ready).await;
|
||||
let failure = "Can't use Java 27.0.1.0.3 and Gradle 9.7.1 to import Gradle project kotlin-gradle.";
|
||||
for _ in 0..2 {
|
||||
s.peer()
|
||||
.notify(
|
||||
"window/logMessage",
|
||||
json!({ "type": 1, "message": format!("{failure}\njava.lang.Exception: details stay in the log") }),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
eventually(|| h.events.lock().iter().any(|e| matches!(e, IdeEvent::Notify { body: Some(b), .. } if b == failure)))
|
||||
.await;
|
||||
s.peer().notify("window/showMessage", json!({ "type": 1, "message": failure })).unwrap();
|
||||
s.peer().notify("window/logMessage", json!({ "type": 1, "message": "unrelated internal error" })).unwrap();
|
||||
s.peer().notify("window/showMessage", json!({ "type": 2, "message": "after" })).unwrap();
|
||||
eventually(|| h.events.lock().iter().any(|e| matches!(e, IdeEvent::Notify { body: Some(b), .. } if b == "after")))
|
||||
.await;
|
||||
let events = h.events.lock();
|
||||
let messages: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
IdeEvent::Notify { body: Some(body), .. } => Some(body.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(messages, vec![failure, "after"]);
|
||||
}
|
||||
|
||||
/// Pull Members Up / Push Members Down: the extension's answers, refusals and edits.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn java_hierarchy_refactorings_use_the_extension_commands() {
|
||||
|
||||
@@ -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
Заблокувати користувача