mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
fix(editor): show usage counts when the project finishes loading at launch (#1809)
Usage counts asked while a language server loads the project come back incomplete and were re-asked only after a busy spell of 1.5 s or more, seen from the editor's own mount. An editor restored at launch mounts while its server starts, so it missed the spell's start, and a short load (vtsls loads a small project in about a second) never counted. Server spells are now followed once from app start, and the spell right after a start counts as loading whatever its length; later short spells are still ignored so request progress (jdtls) doesn't loop. The implementations gutter shares the same settle signal. Co-authored-by: jaw52 <[email protected]>
This commit is contained in:
2 files changed
+123
-31
No files matched your search
@@ -16,46 +16,95 @@ vi.mock("@/platform/ipc", async (orig) => {
|
||||
|
||||
const { onServerSettled, featureBackoff, installAvailabilityEvents } = await import("./availability");
|
||||
|
||||
const status = (s: string) => {
|
||||
for (const fn of listeners) fn({ type: "languageServer", server: 1, status: s } as unknown as IdeEvent);
|
||||
const status = (s: string, server = 1) => {
|
||||
for (const fn of [...listeners]) fn({ type: "languageServer", server, status: s } as unknown as IdeEvent);
|
||||
};
|
||||
|
||||
describe("onServerSettled", () => {
|
||||
// Spells are followed once for the whole app: each test uses its own server.
|
||||
const offs: (() => void)[] = [];
|
||||
const settled = (fn: () => void) => offs.push(onServerSettled(fn));
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
listeners.clear();
|
||||
for (const off of offs.splice(0)) off();
|
||||
});
|
||||
|
||||
it("re-runs a callback that keeps the server busy only once", () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
onServerSettled(fn);
|
||||
status("starting");
|
||||
status("ready");
|
||||
settled(fn);
|
||||
status("starting", 1);
|
||||
status("ready", 1);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
status("busy");
|
||||
status("busy", 1);
|
||||
vi.advanceTimersByTime(2000);
|
||||
status("ready");
|
||||
status("ready", 1);
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
// Indexing that starts later is a new settle.
|
||||
vi.advanceTimersByTime(10_000);
|
||||
status("busy");
|
||||
status("busy", 1);
|
||||
vi.advanceTimersByTime(5000);
|
||||
status("ready");
|
||||
status("ready", 1);
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("ignores short busy spells", () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
onServerSettled(fn);
|
||||
status("busy");
|
||||
settled(fn);
|
||||
status("busy", 2);
|
||||
vi.advanceTimersByTime(100);
|
||||
status("ready");
|
||||
status("ready", 2);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-runs after the project loads right after a start, however briefly", () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
settled(fn);
|
||||
status("starting", 3);
|
||||
status("ready", 3);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
// vtsls loads a small project in about a second; answers given meanwhile were partial.
|
||||
status("busy", 3);
|
||||
vi.advanceTimersByTime(1000);
|
||||
status("ready", 3);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
// Later short spells are requests again.
|
||||
status("busy", 3);
|
||||
vi.advanceTimersByTime(100);
|
||||
status("ready", 3);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("tells a callback added while the project loads that it has", () => {
|
||||
vi.useFakeTimers();
|
||||
settled(() => {});
|
||||
status("starting", 4);
|
||||
status("ready", 4);
|
||||
status("busy", 4);
|
||||
// An editor restored at launch mounts now.
|
||||
const fn = vi.fn();
|
||||
settled(fn);
|
||||
vi.advanceTimersByTime(800);
|
||||
status("ready", 4);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ignores a short spell that begins well after a start", () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
settled(fn);
|
||||
status("starting", 5);
|
||||
status("ready", 5);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
status("busy", 5);
|
||||
vi.advanceTimersByTime(100);
|
||||
status("ready", 5);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("featureBackoff", () => {
|
||||
|
||||
@@ -68,38 +68,81 @@ const LONG_BUSY_MS = 1500;
|
||||
/** A busy spell starting this soon after `fn` ran may be `fn`'s own requests. */
|
||||
const ECHO_MS = 2000;
|
||||
|
||||
/** A busy spell starting this soon after a start is the server loading the project. */
|
||||
const LOADING_AFTER_START_MS = 5000;
|
||||
|
||||
type Settle = (server: number, spellStart: number, loading: boolean) => void;
|
||||
|
||||
/** Servers starting (`-Infinity`) or busy since when. */
|
||||
const spells = new Map<number, number>();
|
||||
/** When servers became ready after starting, until their next spell ends. */
|
||||
const readyAfterStart = new Map<number, number>();
|
||||
const settles = new Set<Settle>();
|
||||
let tracking = false;
|
||||
|
||||
/**
|
||||
* Follows every server's spells from app start, so an editor that mounts while its
|
||||
* server starts or loads (one restored at launch) still learns when that ends.
|
||||
*/
|
||||
function trackSpells() {
|
||||
if (tracking || !isTauri()) return;
|
||||
tracking = true;
|
||||
onIdeEvent("languageServer", (e) => {
|
||||
if (e.status === "starting") {
|
||||
spells.set(e.server, -Infinity);
|
||||
readyAfterStart.delete(e.server);
|
||||
} else if (e.status === "busy") {
|
||||
if (!spells.has(e.server)) spells.set(e.server, Date.now());
|
||||
} else if (e.status === "ready") {
|
||||
const t = spells.get(e.server);
|
||||
spells.delete(e.server);
|
||||
if (t === undefined) return;
|
||||
const startedAt = readyAfterStart.get(e.server);
|
||||
if (t === -Infinity) readyAfterStart.set(e.server, Date.now());
|
||||
else readyAfterStart.delete(e.server);
|
||||
const loading = startedAt !== undefined && t - startedAt < LOADING_AFTER_START_MS;
|
||||
for (const settle of settles) settle(e.server, t, loading);
|
||||
} else {
|
||||
spells.delete(e.server);
|
||||
readyAfterStart.delete(e.server);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `fn` when a server becomes ready after starting or after a long busy
|
||||
* spell (indexing), when earlier answers may have been incomplete. Short busy
|
||||
* spells are ignored: jdtls reports progress for the very requests a caller
|
||||
* sends, and re-asking on each of them would never stop. For the same reason
|
||||
* a long spell that began right after `fn` ran (slow requests `fn` sent, or
|
||||
* indexing right after start) re-runs it only once in a row.
|
||||
* indexing right after start) re-runs it only once in a row. The spell right
|
||||
* after a start counts whatever its length: it is the project loading (vtsls
|
||||
* loads a small one in about a second), and answers given meanwhile are partial.
|
||||
*/
|
||||
export function onServerSettled(fn: () => void): () => void {
|
||||
if (!isTauri()) return () => {};
|
||||
const since = new Map<number, number>();
|
||||
trackSpells();
|
||||
const fired = new Map<number, { at: number; echoed: boolean }>();
|
||||
return onIdeEvent("languageServer", (e) => {
|
||||
if (e.status === "starting") {
|
||||
since.set(e.server, -Infinity);
|
||||
fired.delete(e.server);
|
||||
} else if (e.status === "busy") {
|
||||
if (!since.has(e.server)) since.set(e.server, Date.now());
|
||||
} else if (e.status === "ready") {
|
||||
const t = since.get(e.server);
|
||||
since.delete(e.server);
|
||||
if (t === undefined || Date.now() - t < LONG_BUSY_MS) return;
|
||||
const last = fired.get(e.server);
|
||||
const echo = !!last && t - last.at < ECHO_MS;
|
||||
if (echo && last.echoed) return;
|
||||
fired.set(e.server, { at: Date.now(), echoed: echo });
|
||||
fn();
|
||||
} else since.delete(e.server);
|
||||
const settle: Settle = (server, t, loading) => {
|
||||
if (!loading && Date.now() - t < LONG_BUSY_MS) return;
|
||||
const last = fired.get(server);
|
||||
const echo = !!last && t - last.at < ECHO_MS;
|
||||
if (echo && last.echoed) return;
|
||||
fired.set(server, { at: Date.now(), echoed: echo });
|
||||
fn();
|
||||
};
|
||||
settles.add(settle);
|
||||
const offStarting = onIdeEvent("languageServer", (e) => {
|
||||
if (e.status === "starting") fired.delete(e.server);
|
||||
});
|
||||
return () => {
|
||||
settles.delete(settle);
|
||||
offStarting();
|
||||
};
|
||||
}
|
||||
|
||||
export function installAvailabilityEvents(): () => void {
|
||||
trackSpells();
|
||||
return onIdeEvent("languageServer", (e) => {
|
||||
if (e.status === "ready") clearBackoffs();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user