diff --git a/packages/language-server/test/language-server.test.ts b/packages/language-server/test/language-server.test.ts index 6b48d781..4ec65bd9 100644 --- a/packages/language-server/test/language-server.test.ts +++ b/packages/language-server/test/language-server.test.ts @@ -217,3 +217,91 @@ test("coalesces rapid document changes into one pending diagnostic analysis", as process.kill(); await process.exited; }); + +test("didClose drops the region cache so a reopened document at the same version is rescanned", async () => { + // Regression coverage for the didClose wiring in server.ts: without the + // clearHtmlRegionCache(uri) call there, a document that closes and reopens + // at version 1 (a common restart point) matches the stale cache entry from + // the prior session. Open first WITHOUT a view block (caching "no HTML + // here" for this uri/version), close, then reopen the SAME uri at the SAME + // version WITH a view block covering the same offset: correct behaviour + // rescans and finds it, a stale cache still says "no HTML here" and + // suppresses the completions entirely. + const process = Bun.spawn( + ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))], + { stdin: "pipe", stdout: "pipe", stderr: "pipe" }, + ); + const uri = "file:///reopen.wrn"; + const withoutView = `page A { + functions { + x + } +} +`; + const withView = `page A { + view { + < + } +} +`; + const position = { line: 2, character: 5 }; + const reader = process.stdout.getReader(); + let output = ""; + async function readUntil(marker: string): Promise { + while (!output.includes(marker)) { + const chunk = await reader.read(); + if (chunk.done) break; + output += new TextDecoder().decode(chunk.value); + } + } + + process.stdin.write(packet({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })); + await process.stdin.flush(); + await readUntil('"id":1'); + + process.stdin.write( + packet({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { textDocument: { uri, version: 1, text: withoutView } }, + }), + ); + process.stdin.write( + packet({ + jsonrpc: "2.0", + id: 2, + method: "textDocument/completion", + params: { textDocument: { uri }, position }, + }), + ); + await process.stdin.flush(); + await readUntil('"id":2'); + const firstReply = output.slice(output.indexOf('"id":2')); + expect(firstReply).not.toContain('"label":"div"'); + + process.stdin.write( + packet({ jsonrpc: "2.0", method: "textDocument/didClose", params: { textDocument: { uri } } }), + ); + process.stdin.write( + packet({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { textDocument: { uri, version: 1, text: withView } }, + }), + ); + process.stdin.write( + packet({ + jsonrpc: "2.0", + id: 3, + method: "textDocument/completion", + params: { textDocument: { uri }, position }, + }), + ); + await process.stdin.flush(); + await readUntil('"id":3'); + const secondReply = output.slice(output.indexOf('"id":3')); + expect(secondReply).toContain('"label":"div"'); + + process.kill(); + await process.exited; +});