fix(watcher): close deleted open documents

This commit is contained in:
2026-05-20 00:22:30 -04:00
parent 62fc80c70f
commit 14749a6449
3 changed files with 59 additions and 1 deletions

View File

@@ -247,6 +247,14 @@ export class LspClient {
return uri;
}
closeDocument(uri: string): void {
this.versions.delete(uri);
this.diagnostics.delete(uri);
this.conn.sendNotification("textDocument/didClose", {
textDocument: { uri },
});
}
// Send Raw LSP Request - Passthrough used by the command dispatcher.
sendRequest<R = unknown>(method: string, params: unknown): Promise<R> {
return this.conn.sendRequest(method, params) as Promise<R>;

View File

@@ -20,6 +20,7 @@ import {
const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
const WATCHER_READY_TIMEOUT_MS = 5000;
const FILE_CHANGE_DELETED = 3;
// Client Entry - One LspClient per (server.id, rootDir), plus the bookkeeping
// needed to keep files in sync and evict on idleness.
@@ -152,6 +153,11 @@ async function waitForWatcherReady(entry: ClientEntry): Promise<void> {
function forwardEvents(entry: ClientEntry, events: FileEvent[]): void {
try {
for (const event of events) {
if (event.type !== FILE_CHANGE_DELETED || !entry.opened.has(event.uri)) continue;
entry.client.closeDocument(event.uri);
entry.opened.delete(event.uri);
}
if (process.env.LSP_DEBUG) {
log(`watcher fire`, entry.server.id, JSON.stringify(events));
}

View File

@@ -32,7 +32,7 @@ interface DiagResult {
[serverId: string]: { diagnostics?: { message: string }[] };
}
describe("watcher: gopls picks up externally-created files", { skip: skip ?? undefined }, () => {
describe("watcher: gopls picks up external file changes", { skip: skip ?? undefined }, () => {
let tmpDir: string;
let mainFile: string;
let helperFile: string;
@@ -122,4 +122,48 @@ describe("watcher: gopls picks up externally-created files", { skip: skip ?? und
`Expected diagnostic to clear after creating helper.go, still got: ${JSON.stringify(finalDiags)}`,
);
});
it("closes an opened file when it is deleted externally", async () => {
fs.writeFileSync(
helperFile,
"package main\n\nfunc Helper(x int) {}\n",
);
await runCliJson([helperFile, "diagnostics", '{"timeoutMs":3000}'], env);
await pollUntil(
async () =>
(await runCliJson(
[mainFile, "diagnostics", '{"timeoutMs":3000}'],
env,
)) as DiagResult,
(r) => {
const diags = r["gopls"]?.diagnostics ?? [];
return diags.some((d) => d.message.includes("not enough arguments"));
},
15000,
500,
);
fs.rmSync(helperFile);
const result = await pollUntil(
async () =>
(await runCliJson(
[mainFile, "diagnostics", '{"timeoutMs":3000}'],
env,
)) as DiagResult,
(r) => {
const diags = r["gopls"]?.diagnostics ?? [];
return diags.some((d) => d.message.toLowerCase().includes("undefined"));
},
15000,
500,
);
const finalDiags = result["gopls"]?.diagnostics ?? [];
assert.ok(
finalDiags.some((d) => d.message.toLowerCase().includes("undefined")),
`Expected undefined-symbol diagnostic after deleting opened helper.go, got: ${JSON.stringify(finalDiags)}`,
);
});
});