дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(lsp): Drop language servers that leave the list from the status bar (#125)
Removing a language server (disabled in settings via stop_unlisted, or a fallback detached when its preferred server comes back) only emitted a stopped state, and the status bar list only refreshed for unknown servers, so the removed server stayed until the project was reopened. Both paths now go through forget(), which drops the server and emits IdeEvent::LanguageServerRemoved; the frontend drops the entry and ignores stale lsp_servers results that still contain it. Stopped servers no longer count towards "+N". Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
ac426c8881
коміт
5b63a2b95c
6 files changed
+96
-6
No files matched your search
@@ -39,6 +39,36 @@ describe("server status", () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(useServers.getState().servers.get(id)).toMatchObject({ status: "missing", message: "install jdtls" });
|
||||
});
|
||||
|
||||
it("drops a server the backend removed, even if an older snapshot still lists it", async () => {
|
||||
useProject.setState({ project: { name: "p", root: "/p", isGit: false, settings: {} } as never });
|
||||
dispose = installServerEvents();
|
||||
const info = (id: number, name: string): ServerInfo => ({
|
||||
id: id as ServerId,
|
||||
name,
|
||||
languages: ["python"],
|
||||
root: "/p",
|
||||
status: "ready",
|
||||
pid: null,
|
||||
});
|
||||
pending.splice(0).forEach((resolve) => resolve([info(11, "basedpyright"), info(12, "pyright")]));
|
||||
await vi.waitFor(() => expect(useServers.getState().servers.size).toBe(2));
|
||||
expect(serversSummary([...useServers.getState().servers.values()])?.text).toBe("basedpyright +1");
|
||||
|
||||
// The fallback stops and leaves the list; a snapshot taken before that answers late.
|
||||
const status = (id: number, name: string, status: ServerInfo["status"]) =>
|
||||
emitLocalIdeEvent({ type: "languageServer", server: id as ServerId, name, status, message: null });
|
||||
status(13, "ty", "starting");
|
||||
status(12, "pyright", "stopped");
|
||||
emitLocalIdeEvent({ type: "languageServerRemoved", server: 12 as ServerId });
|
||||
expect(useServers.getState().servers.has(12 as ServerId)).toBe(false);
|
||||
pending.splice(0).forEach((resolve) => resolve([info(11, "basedpyright"), info(12, "pyright"), info(13, "ty")]));
|
||||
await vi.waitFor(() => expect(useServers.getState().servers.get(13 as ServerId)?.languages).toEqual(["python"]));
|
||||
expect([...useServers.getState().servers.keys()]).toEqual([11, 13]);
|
||||
|
||||
emitLocalIdeEvent({ type: "languageServerRemoved", server: 13 as ServerId });
|
||||
expect(serversSummary([...useServers.getState().servers.values()])?.text).toBe("basedpyright");
|
||||
});
|
||||
});
|
||||
|
||||
describe("serversSummary", () => {
|
||||
@@ -54,4 +84,10 @@ describe("serversSummary", () => {
|
||||
expect(serversSummary([entry(1, "vtsls", "missing"), entry(2, "typescript-language-server", "ready")])?.tone).toBe("ok");
|
||||
expect(serversSummary([entry(1, "vtsls", "missing")])?.tone).toBe("error");
|
||||
});
|
||||
it("doesn't count stopped servers next to running ones", () => {
|
||||
const text = (...servers: ReturnType<typeof entry>[]) => serversSummary(servers)?.text;
|
||||
expect(text(entry(1, "basedpyright", "ready"), entry(2, "pyright", "stopped"))).toBe("basedpyright");
|
||||
expect(text(entry(1, "basedpyright", "ready"), entry(2, "ty", "starting"))).toBe("basedpyright +1");
|
||||
expect(serversSummary([entry(2, "pyright", "stopped")])).toMatchObject({ text: "pyright", tone: "idle" });
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Language server status: `lsp_servers` snapshot + `languageServer` events. */
|
||||
/** Language server status: `lsp_servers` snapshot + `languageServer` / `languageServerRemoved` events. */
|
||||
import { getAction } from "@/platform/actions";
|
||||
import { create } from "zustand";
|
||||
import type { ServerId, ServerInfo, ServerStatus } from "@/bindings";
|
||||
@@ -26,6 +26,8 @@ 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>();
|
||||
/** Event count when each server left the list, so an older snapshot doesn't bring it back. */
|
||||
const removedAt = new Map<ServerId, number>();
|
||||
|
||||
function notifyMissing(e: ServerEntry) {
|
||||
if (notifiedMissing.has(e.name)) return;
|
||||
@@ -66,6 +68,7 @@ export async function refreshServers() {
|
||||
const prev = useServers.getState().servers;
|
||||
const map = new Map<ServerId, ServerEntry>();
|
||||
for (const s of list) {
|
||||
if ((removedAt.get(s.id) ?? 0) > since) continue;
|
||||
const p = prev.get(s.id);
|
||||
const newer = p && (lastEvent.get(s.id) ?? 0) > since;
|
||||
map.set(s.id, {
|
||||
@@ -104,6 +107,16 @@ export function installServerEvents(): () => void {
|
||||
upsertServer({ id: e.server, name: e.name, status: e.status, message: e.message });
|
||||
if (!known) void refreshServers();
|
||||
});
|
||||
const offRemoved = onIdeEvent("languageServerRemoved", (e) => {
|
||||
removedAt.set(e.server, ++eventSeq);
|
||||
lastEvent.delete(e.server);
|
||||
if (!useServers.getState().servers.has(e.server)) return;
|
||||
useServers.setState((s) => {
|
||||
const servers = new Map(s.servers);
|
||||
servers.delete(e.server);
|
||||
return { servers };
|
||||
});
|
||||
});
|
||||
let root = useProject.getState().project?.root ?? null;
|
||||
void refreshServers();
|
||||
const unsub = useProject.subscribe((s) => {
|
||||
@@ -115,6 +128,7 @@ export function installServerEvents(): () => void {
|
||||
});
|
||||
return () => {
|
||||
off();
|
||||
offRemoved();
|
||||
unsub();
|
||||
};
|
||||
}
|
||||
@@ -148,7 +162,9 @@ export function serversSummary(servers: readonly ServerEntry[]): { text: string;
|
||||
);
|
||||
const busy = servers.filter((s) => s.status === "starting" || s.status === "busy");
|
||||
const tone = bad.length ? "error" : busy.length ? "busy" : servers.some((s) => s.status === "ready") ? "ok" : "idle";
|
||||
const names = [...new Set(servers.map((s) => s.name))];
|
||||
// A stopped server (one a preferred server took over from) isn't worth naming next to running ones.
|
||||
const active = servers.filter((s) => s.status !== "stopped");
|
||||
const names = [...new Set((active.length ? active : servers).map((s) => s.name))];
|
||||
const text = names.length === 1 ? names[0]! : `${names[0]} +${names.length - 1}`;
|
||||
return { text, tone };
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача