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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-27 12:09:51 +08:00
1 джерело beab7992f9
коміт 56fdc005a8
29 files changed
+1592 -111

No files matched your search

+34
Переглянути файл
@@ -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();
});
});
+52 -37
Переглянути файл
@@ -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>
</>