дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(java): Download JDK, and one-click "Download JDK N" where a JDK is missing (#61)
Root cause: the IDE only detected installed JDKs and had no way to get one, so any launch needing a newer JDK than the machine has (latest jdtls needs JDK 25) dead-ended in an error. Fix: IDEA-style Download JDK (Eclipse Temurin via the Adoptium API, sha256-verified, reusing ide-tools' download/verify code; Windows zip, Linux tar.gz, macOS Contents/Home) into <app-data>/tools/jdks, registered with JDK discovery and rescanned after each download. "Download JDK…" in the Project SDK and Language Server JDK pickers; a one-click `java.downloadJdk` "Download JDK N" on "needs JDK N" errors, which also repoints a too-old language server JDK and restarts jdtls. Siblings: language server status popup, jdtls launch notification, run console classpath error, debug launch, run configurations without a JDK ≥ the module's Java version, Maven/Gradle sync failures, single-file Java run without `java` on PATH. Verified in the app on macOS: vendor/version/location queries, the Download JDK dialog from the Language Server JDK picker, a real Temurin 25.0.5 download that runs. Not verified on Windows/Linux. Fixes #49 https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
beab7992f9
коміт
56fdc005a8
29 files changed
+1592
-111
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { registerAction } from "@/platform/actions";
|
||||
import { ServerStatus } from "./ServerStatus";
|
||||
import { useServers, type ServerEntry } from "./servers";
|
||||
|
||||
const ISSUE_49 =
|
||||
"jdtls needs JDK 25 or newer to run (found JDK 8, 17, 21). Download JDK 25, or pick an installed one in Settings › Languages & Frameworks › Java › Language Server › JDK (it restarts automatically); the project can still target an older Java version.";
|
||||
|
||||
const server = (status: ServerEntry["status"], message: string | null): ServerEntry =>
|
||||
({ id: 1, name: "jdtls", status, message, languages: ["Java"], root: null }) as unknown as ServerEntry;
|
||||
|
||||
afterEach(() => useServers.setState({ servers: new Map() }));
|
||||
|
||||
describe("Language server status popup", () => {
|
||||
it("offers Download JDK N when jdtls can't start for want of a JDK", () => {
|
||||
const run = vi.fn();
|
||||
const off = registerAction({ id: "java.downloadJdk", title: "Download JDK…", run: ({ arg }) => run(arg) });
|
||||
useServers.setState({ servers: new Map([[1 as never, server("misconfigured", ISSUE_49)]]) });
|
||||
render(<ServerStatus />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /Language servers/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Download JDK 25" }));
|
||||
expect(run).toHaveBeenCalledWith({ version: 25, languageServer: true });
|
||||
off();
|
||||
});
|
||||
|
||||
it("keeps Restart for other failures", () => {
|
||||
useServers.setState({ servers: new Map([[1 as never, server("crashed", "exit code 1")]]) });
|
||||
render(<ServerStatus />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /Language servers/ }));
|
||||
expect(screen.queryByRole("button", { name: /Download JDK/ })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Restart jdtls" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { useMemo, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { getAction, executeAction } from "@/platform/actions";
|
||||
import { BarButton, Button, IconButton, Popup, T } from "@/ui";
|
||||
import { downloadJdkActions } from "@/platform/jdk";
|
||||
import { relativePath, useProject } from "@/platform/project";
|
||||
import { STATUS_LABEL, restartServer, serversSummary, useServers } from "./servers";
|
||||
import styles from "./ServerStatus.module.css";
|
||||
@@ -41,45 +42,59 @@ export function ServerStatus() {
|
||||
<div className={styles.header}>
|
||||
<T>Language Servers</T>
|
||||
</div>
|
||||
{servers.map((s) => (
|
||||
<div key={s.id} className={styles.server}>
|
||||
<div className={styles.line}>
|
||||
<span className={clsx(styles.dot, styles[toneOf(s.status)])} aria-hidden />
|
||||
<span className={styles.name}>{s.name}</span>
|
||||
<span className={styles.status}>{STATUS_LABEL[s.status]}</span>
|
||||
<span className={styles.spacer} />
|
||||
{s.status === "missing" && canInstall ? (
|
||||
<Button
|
||||
size="small"
|
||||
aria-label={`Install ${s.name}`}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
void executeAction("tools.install", s.name);
|
||||
}}
|
||||
>
|
||||
<T>Install</T>
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
size="small"
|
||||
title={`Restart ${s.name}`}
|
||||
disabled={s.status === "missing" || s.status === "starting"}
|
||||
onClick={() => void restartServer(s.id)}
|
||||
/>
|
||||
{servers.map((s) => {
|
||||
// jdtls without a JDK new enough to run it: "Download JDK 25".
|
||||
const [jdkFix] = s.status === "misconfigured" ? downloadJdkActions(s.message) : [];
|
||||
return (
|
||||
<div key={s.id} className={styles.server}>
|
||||
<div className={styles.line}>
|
||||
<span className={clsx(styles.dot, styles[toneOf(s.status)])} aria-hidden />
|
||||
<span className={styles.name}>{s.name}</span>
|
||||
<span className={styles.status}>{STATUS_LABEL[s.status]}</span>
|
||||
<span className={styles.spacer} />
|
||||
{jdkFix ? (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
jdkFix.run();
|
||||
}}
|
||||
>
|
||||
{jdkFix.label}
|
||||
</Button>
|
||||
) : s.status === "missing" && canInstall ? (
|
||||
<Button
|
||||
size="small"
|
||||
aria-label={`Install ${s.name}`}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
void executeAction("tools.install", s.name);
|
||||
}}
|
||||
>
|
||||
<T>Install</T>
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
size="small"
|
||||
title={`Restart ${s.name}`}
|
||||
disabled={s.status === "missing" || s.status === "starting"}
|
||||
onClick={() => void restartServer(s.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(s.languages.length > 0 || s.root) && (
|
||||
<div className={styles.meta}>
|
||||
{s.languages.join(", ")}
|
||||
{s.root && s.root !== root ? ` · ${relativePath(root, s.root) || s.root}` : ""}
|
||||
</div>
|
||||
)}
|
||||
{s.message && (
|
||||
<div className={clsx(styles.message, s.status === "missing" && styles.install)}>{s.message}</div>
|
||||
)}
|
||||
</div>
|
||||
{(s.languages.length > 0 || s.root) && (
|
||||
<div className={styles.meta}>
|
||||
{s.languages.join(", ")}
|
||||
{s.root && s.root !== root ? ` · ${relativePath(root, s.root) || s.root}` : ""}
|
||||
</div>
|
||||
)}
|
||||
{s.message && (
|
||||
<div className={clsx(styles.message, s.status === "missing" && styles.install)}>{s.message}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popup>
|
||||
</>
|
||||
|
||||
Reference in new issue
Заблокувати користувача