summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/lsp/src/diagnostics.test.ts73
-rw-r--r--packages/lsp/src/diagnostics.ts34
2 files changed, 106 insertions, 1 deletions
diff --git a/packages/lsp/src/diagnostics.test.ts b/packages/lsp/src/diagnostics.test.ts
index 7f0365b..70b1ffa 100644
--- a/packages/lsp/src/diagnostics.test.ts
+++ b/packages/lsp/src/diagnostics.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { DiagnosticsStore } from "./diagnostics.js";
+import { type Diagnostic, DiagnosticsStore } from "./diagnostics.js";
describe("diagnostics", () => {
it("formats diagnostics with severity, location, and message", () => {
@@ -84,4 +84,75 @@ describe("diagnostics", () => {
// Other URIs are untouched.
expect(store.format("file:///project/b.ts")).toBe("");
});
+
+ it("bounds pushDiagnostics — oldest background file is evicted past the cap (Bug 3 remainder)", () => {
+ // Language servers scan the workspace and push diagnostics for files the
+ // agent never opened. These never enter the client's openDocuments LRU,
+ // so without an independent cap the pushDiagnostics map grows forever.
+ const store = new DiagnosticsStore();
+ const CAP = 100;
+
+ const diag = (uri: string): readonly Diagnostic[] => [
+ {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+ severity: 1,
+ message: `err in ${uri}`,
+ },
+ ];
+
+ // Push `CAP` distinct background files (at the cap — none evicted yet).
+ for (let i = 0; i < CAP; i++) {
+ store.setPushDiagnostics({ uri: `file:///bg/file${i}.ts`, diagnostics: diag(`file${i}`) });
+ }
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(true);
+ expect(store.format("file:///bg/file0.ts")).toContain("err in file0");
+
+ // One past the cap → the least-recently-pushed (file0) is evicted.
+ store.setPushDiagnostics({
+ uri: "file:///bg/file100.ts",
+ diagnostics: diag("file100"),
+ });
+
+ // file0 (oldest) is gone — no didClose, no retained diagnostics, no flag.
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(false);
+ expect(store.format("file:///bg/file0.ts")).toBe("");
+ expect(store.getMerged("file:///bg/file0.ts")).toHaveLength(0);
+
+ // The newest (file100) and a middle entry are retained.
+ expect(store.hasReceivedPush("file:///bg/file100.ts")).toBe(true);
+ expect(store.format("file:///bg/file100.ts")).toContain("err in file100");
+ expect(store.format("file:///bg/file50.ts")).toContain("err in file50");
+ });
+
+ it("re-pushing a URI refreshes its LRU position so it is not evicted", () => {
+ const store = new DiagnosticsStore();
+ const CAP = 100;
+
+ const diag = (uri: string): readonly Diagnostic[] => [
+ {
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+ severity: 1,
+ message: uri,
+ },
+ ];
+
+ for (let i = 0; i < CAP; i++) {
+ store.setPushDiagnostics({ uri: `file:///bg/file${i}.ts`, diagnostics: diag(`file${i}`) });
+ }
+
+ // Re-push file0 (an actively-edited file gets re-pushed on each change):
+ // this should move it to the tail (most-recently-used), NOT leave it at
+ // the head where it would be the first eviction candidate.
+ store.setPushDiagnostics({ uri: "file:///bg/file0.ts", diagnostics: diag("file0-updated") });
+
+ // Now push one past the cap. file1 (now the oldest untouched) should be
+ // evicted; file0 (refreshed) must survive.
+ store.setPushDiagnostics({ uri: "file:///bg/file100.ts", diagnostics: diag("file100") });
+
+ expect(store.hasReceivedPush("file:///bg/file0.ts")).toBe(true);
+ expect(store.format("file:///bg/file0.ts")).toContain("file0-updated");
+
+ expect(store.hasReceivedPush("file:///bg/file1.ts")).toBe(false);
+ expect(store.format("file:///bg/file1.ts")).toBe("");
+ });
});
diff --git a/packages/lsp/src/diagnostics.ts b/packages/lsp/src/diagnostics.ts
index 09ec1ae..cce261f 100644
--- a/packages/lsp/src/diagnostics.ts
+++ b/packages/lsp/src/diagnostics.ts
@@ -35,10 +35,44 @@ export class DiagnosticsStore {
private pushDiagnostics = new Map<string, readonly Diagnostic[]>();
private pullDiagnostics = new Map<string, readonly Diagnostic[]>();
private pushReceived = new Set<string>();
+ /**
+ * Bounded push-diagnostics set: once more than this many URIs have cached
+ * push diagnostics, the least-recently-pushed is evicted (just deleted —
+ * no didClose, since these are often background-scanned files the agent
+ * never opened). Language servers (tsserver, rust-analyzer, …) scan the
+ * workspace and emit textDocument/publishDiagnostics for files the client
+ * never touched; those never enter the client's openDocuments LRU, so
+ * without this cap the map grew without bound over a long session.
+ */
+ private static readonly MAX_PUSH_DIAGNOSTICS = 100;
setPushDiagnostics(params: PublishDiagnosticsParams): void {
+ // Delete-then-set to refresh LRU recency: a URI re-pushed (e.g. an
+ // actively edited file) moves to the tail (most-recently-used), so
+ // background-scanned files pushed once drift to the head and are evicted
+ // first. Mirrors the openDocuments LRU in client.ts. (A plain `set` on
+ // an existing key updates the value but leaves its insertion position —
+ // and thus its eviction priority — unchanged.)
+ this.pushDiagnostics.delete(params.uri);
this.pushDiagnostics.set(params.uri, params.diagnostics);
this.pushReceived.add(params.uri);
+ this.evictPushIfOverCap();
+ }
+
+ /**
+ * If the push-diagnostics set exceeds MAX_PUSH_DIAGNOSTICS, drop the
+ * least-recently-pushed entry (the first key in insertion order) from
+ * both `pushDiagnostics` and `pushReceived`. No didClose is sent: these
+ * entries are frequently for files the agent never opened, and even for
+ * an opened file the next push re-caches it.
+ */
+ private evictPushIfOverCap(): void {
+ while (this.pushDiagnostics.size > DiagnosticsStore.MAX_PUSH_DIAGNOSTICS) {
+ const oldest = this.pushDiagnostics.keys().next().value;
+ if (oldest === undefined) break;
+ this.pushDiagnostics.delete(oldest);
+ this.pushReceived.delete(oldest);
+ }
}
setPullDiagnostics(uri: string, report: DocumentDiagnosticReport): void {