дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(lsp): Run jdtls on a JDK 21+ and offer Install in the server popup (#2)
* fix(lsp): Run jdtls on a JDK 21+ and offer Install in the server popup jdtls refuses to start on Java < 21, and its launcher took whatever JAVA_HOME / PATH pointed at, so it crash-looped with a Python traceback when that was an older JDK (#1). Pick the first detected JDK 21+ and pass it via JAVA_HOME; projects still compile against their own runtimes. When none is found, report a clear message instead of launching, and rescan on restart so a newly installed JDK is picked up. JDK detection now also covers the java on PATH and Windows vendor directories. The Language Servers status popup showed the manual install hint for missing servers; it now has an Install button that runs tools.install, which opens the Language Servers settings page for tools the IDE cannot install itself. Fixes #1 Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu * fix(lsp): Address review of the jdtls runtime check - Cache the JDK scan and rescan at most every 10s while no JDK 21+ is found, refreshing the shared list instead of discarding it; skip duplicate Windows Program Files roots. - Check `launch_error` in the manager (inside the blocking launch task) rather than in ProcessLauncher, and skip such servers in batch inspection. - Keep the install hint visible next to the Install button; when the tool list failed to load, try the install so the error is reported. - Don't let a server snapshot requested before a status event overwrite that newer status (left jdtls showing "Starting…" while missing).
Цей коміт міститься в:
1 джерело
3591b52034
коміт
5e3e1f939d
15 files changed
+284
-38
No files matched your search
@@ -1,7 +1,8 @@
|
||||
/** Status bar widget listing language servers, with a popup to restart them. */
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { BarButton, IconButton, Popup } from "@/ui";
|
||||
import { getAction, executeAction } from "@/platform/actions";
|
||||
import { BarButton, Button, IconButton, Popup } from "@/ui";
|
||||
import { relativePath, useProject } from "@/platform/project";
|
||||
import { STATUS_LABEL, restartServer, serversSummary, useServers } from "./servers";
|
||||
import styles from "./ServerStatus.module.css";
|
||||
@@ -13,6 +14,8 @@ export function ServerStatus() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const summary = serversSummary(servers);
|
||||
// features/tools owns installing; without it, fall back to the manual hint text.
|
||||
const canInstall = !!getAction("tools.install");
|
||||
if (!summary) return null;
|
||||
return (
|
||||
<>
|
||||
@@ -43,13 +46,26 @@ export function ServerStatus() {
|
||||
<span className={styles.name}>{s.name}</span>
|
||||
<span className={styles.status}>{STATUS_LABEL[s.status]}</span>
|
||||
<span className={styles.spacer} />
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
size="small"
|
||||
title={`Restart ${s.name}`}
|
||||
disabled={s.status === "missing" || s.status === "starting"}
|
||||
onClick={() => void restartServer(s.id)}
|
||||
/>
|
||||
{s.status === "missing" && canInstall ? (
|
||||
<Button
|
||||
size="small"
|
||||
aria-label={`Install ${s.name}`}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
void executeAction("tools.install", s.name);
|
||||
}}
|
||||
>
|
||||
Install
|
||||
</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}>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ServerId, ServerInfo } from "@/bindings";
|
||||
|
||||
const pending = vi.hoisted(() => [] as ((data: ServerInfo[]) => void)[]);
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const ipc = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...ipc,
|
||||
isTauri: () => true,
|
||||
commands: {
|
||||
...ipc.commands,
|
||||
lspServers: () =>
|
||||
new Promise((resolve) => {
|
||||
pending.push((data) => resolve({ status: "ok", data }));
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { emitLocalIdeEvent } = await import("@/platform/ipc");
|
||||
const { useProject } = await import("@/platform/project");
|
||||
const { installServerEvents, useServers } = await import("./servers");
|
||||
|
||||
describe("server status", () => {
|
||||
let dispose = () => {};
|
||||
afterEach(() => dispose());
|
||||
|
||||
it("keeps a status that arrived after the snapshot was requested", async () => {
|
||||
useProject.setState({ project: { name: "p", root: "/p", isGit: false, settings: {} } as never });
|
||||
dispose = installServerEvents();
|
||||
const id = 1 as ServerId;
|
||||
const info: ServerInfo = { id, name: "jdtls", languages: [], root: "/p", status: "starting", pid: null };
|
||||
// The first event requests a snapshot; the server reports missing before it answers.
|
||||
emitLocalIdeEvent({ type: "languageServer", server: id, name: "jdtls", status: "starting", message: null });
|
||||
emitLocalIdeEvent({ type: "languageServer", server: id, name: "jdtls", status: "missing", message: "install jdtls" });
|
||||
pending.splice(0).forEach((resolve) => resolve([info]));
|
||||
await vi.waitFor(() => expect(useServers.getState().servers.get(id)?.languages).toEqual([]));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(useServers.getState().servers.get(id)).toMatchObject({ status: "missing", message: "install jdtls" });
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,10 @@ export const useServers = create<ServersState>(() => ({ servers: new Map() }));
|
||||
|
||||
const notifiedMissing = new Set<string>();
|
||||
|
||||
/** Event count at each server's latest event, so a snapshot taken earlier doesn't undo it. */
|
||||
let eventSeq = 0;
|
||||
const lastEvent = new Map<ServerId, number>();
|
||||
|
||||
function notifyMissing(e: ServerEntry) {
|
||||
if (notifiedMissing.has(e.name)) return;
|
||||
notifiedMissing.add(e.name);
|
||||
@@ -57,18 +61,22 @@ export async function refreshServers() {
|
||||
}
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
const since = eventSeq;
|
||||
const list: ServerInfo[] = await call(commands.lspServers());
|
||||
const prev = useServers.getState().servers;
|
||||
const map = new Map<ServerId, ServerEntry>();
|
||||
for (const s of list)
|
||||
for (const s of list) {
|
||||
const p = prev.get(s.id);
|
||||
const newer = p && (lastEvent.get(s.id) ?? 0) > since;
|
||||
map.set(s.id, {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
status: newer ? p.status : s.status,
|
||||
languages: s.languages,
|
||||
root: s.root,
|
||||
message: prev.get(s.id)?.message ?? null,
|
||||
message: p?.message ?? null,
|
||||
});
|
||||
}
|
||||
useServers.setState({ servers: map });
|
||||
for (const e of map.values()) if (e.status === "missing") notifyMissing(e);
|
||||
} catch {
|
||||
@@ -92,6 +100,7 @@ export async function restartServer(id: ServerId) {
|
||||
export function installServerEvents(): () => void {
|
||||
const off = onIdeEvent("languageServer", (e) => {
|
||||
const known = useServers.getState().servers.has(e.server);
|
||||
lastEvent.set(e.server, ++eventSeq);
|
||||
upsertServer({ id: e.server, name: e.name, status: e.status, message: e.message });
|
||||
if (!known) void refreshServers();
|
||||
});
|
||||
|
||||
Reference in new issue
Заблокувати користувача