import { expect, test } from "bun:test"; import { fileURLToPath } from "node:url"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { completionItems, definitionLocation, documentDiagnostics, documentSymbols, formatDocument, positionAt, symbolLocations, virtualTypeScriptDocument, workspaceCompletionItems, clearWorkspaceIndexCache, extractComponentRefactor, htmlToWrn, } from "../src/index.ts"; const document = { uri: "file:///Counter.wrn", text: `component Counter {\nstate count = 0\nview { }\n}`, }; test("provides editor-neutral language features", () => { expect(documentDiagnostics(document)).toEqual([]); expect(completionItems().some((item) => item.label === "state")).toBe(true); expect(documentSymbols(document).map((item) => item.name)).toContain("count"); const reference = positionAt(document.text, document.text.lastIndexOf("count")); expect(definitionLocation(document, reference)).not.toBeNull(); expect(symbolLocations(document, reference)).toHaveLength(3); expect(formatDocument(document)).toBeArray(); }); test("indexes workspace components, contracts, routes, translations, schemas and classes", () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-workspace-")); try { for (const directory of ["components", "pages", "locales", "db"]) mkdirSync(join(root, "app", directory), { recursive: true }); mkdirSync(join(root, "app", "pages", "users"), { recursive: true }); writeFileSync( join(root, "app", "components", "Card.wrn"), `component Card { props { title: string } outputs { close() } view { } }`, ); writeFileSync( join(root, "app", "pages", "users", "[id].wrn"), `page User { view {

User

} }`, { flag: "w" }, ); writeFileSync( join(root, "app", "locales", "en.json"), JSON.stringify({ user: { title: "User" } }), ); writeFileSync(join(root, "app", "db", "schema.ts"), `export const UserSchema = {};`); const labels = workspaceCompletionItems(root).map((item) => item.label); for (const label of [ "Card", "title", "@close", "slot:body", "/users/:id", "user.title", "UserSchema", "text-red-500", ]) expect(labels).toContain(label); } finally { rmSync(root, { recursive: true, force: true }); } }); test("workspace indexing is cached and skips dependency and generated trees", () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-lsp-bounded-")); try { mkdirSync(join(root, "app", "pages"), { recursive: true }); mkdirSync(join(root, "app", "node_modules", "large-package"), { recursive: true }); mkdirSync(join(root, "app", ".wrnexus"), { recursive: true }); writeFileSync(join(root, "app", "pages", "index.wrn"), `page Home { view {

Home

} }`); writeFileSync( join(root, "app", "node_modules", "large-package", "secret.ts"), `export const DependencySchema = {};`, ); writeFileSync(join(root, "app", ".wrnexus", "generated.ts"), `export const CacheSchema = {};`); const first = workspaceCompletionItems(root); const second = workspaceCompletionItems(root); expect(second).toBe(first); expect(first.map((item) => item.label)).not.toContain("DependencySchema"); expect(first.map((item) => item.label)).not.toContain("CacheSchema"); writeFileSync(join(root, "app", "pages", "later.wrn"), `page Later { view {

Later

} }`); expect(workspaceCompletionItems(root).map((item) => item.label)).not.toContain("/later"); clearWorkspaceIndexCache(root); expect(workspaceCompletionItems(root).map((item) => item.label)).toContain("/later"); } finally { clearWorkspaceIndexCache(root); rmSync(root, { recursive: true, force: true }); } }); test("provides safe component extraction and HTML conversion refactors", () => { const doc = { uri: "file:///project/app/pages/index.wrn", version: 2, text: "
Hello
", }; const range = { start: { line: 0, character: 0 }, end: { line: 0, character: doc.text.length } }; const edit = extractComponentRefactor(doc, range, "Greeting"); expect(edit.documentChanges).toHaveLength(3); expect(JSON.stringify(edit)).toContain(""); expect(htmlToWrn(``, "Imported")).toContain( `@click="save()"`, ); }); test("provides a mapped TypeScript virtual document and expression type diagnostics", () => { const typed = { uri: "file:///Typed.wrn", text: `component Typed {\nprops { count: number }\nstate label: string = count\nview {

{label}

}\n}`, }; const virtual = virtualTypeScriptDocument(typed); expect(virtual.languageId).toBe("typescript"); expect(virtual.uri).toEndWith(".wrn.ts"); expect(virtual.text).toContain("declare const count: Readonly"); expect(virtual.mappings.length).toBeGreaterThan(0); expect(documentDiagnostics(typed).some((item) => String(item.code).startsWith("WRN-TYPE-"))).toBe( true, ); }); test("reports compiler and accessibility diagnostics", () => { const diagnostics = documentDiagnostics({ uri: "file:///bad.wrn", text: `page Bad {\n view {\n \n }\n}`, }); expect(diagnostics.some((item) => String(item.code).startsWith("WRN-A11Y"))).toBe(true); }); test("skips compiler graph creation for oversized WRN documents", () => { const diagnostics = documentDiagnostics({ uri: "file:///large.wrn", text: `page Large { view {

${"x".repeat(1_048_576)}

} }`, }); expect(diagnostics).toHaveLength(1); expect(diagnostics[0]?.code).toBe("WRN-LSP-FILE-SIZE"); }); function packet(value: unknown): Uint8Array { const body = JSON.stringify(value); return new TextEncoder().encode(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`); } test("serves initialize over standard LSP stdio framing", async () => { const process = Bun.spawn( ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))], { stdin: "pipe", stdout: "pipe", stderr: "pipe", }, ); process.stdin.write(packet({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })); await process.stdin.flush(); const reader = process.stdout.getReader(); let output = ""; while (!output.includes('"id":1')) { const chunk = await reader.read(); if (chunk.done) break; output += new TextDecoder().decode(chunk.value); } expect(output).toContain("WRNexus Language Server"); expect(output).toContain("documentFormattingProvider"); expect(output).toContain("wrnexusVirtualTypeScript"); process.kill(); await process.exited; }); test("coalesces rapid document changes into one pending diagnostic analysis", async () => { const process = Bun.spawn( ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))], { stdin: "pipe", stdout: "pipe", stderr: "pipe" }, ); const uri = "file:///rapid.wrn"; process.stdin.write( packet({ jsonrpc: "2.0", method: "textDocument/didOpen", params: { textDocument: { uri, version: 1, text: `page Rapid { view {

1

} }` } }, }), ); for (let version = 2; version <= 50; version++) { process.stdin.write( packet({ jsonrpc: "2.0", method: "textDocument/didChange", params: { textDocument: { uri, version }, contentChanges: [{ text: `page Rapid { view {

${version}

} }` }], }, }), ); } process.stdin.write( packet({ jsonrpc: "2.0", id: 99, method: "wrnexus/serverStatus", params: {} }), ); await process.stdin.flush(); const reader = process.stdout.getReader(); let output = ""; while (!output.includes('"id":99')) { const chunk = await reader.read(); if (chunk.done) break; output += new TextDecoder().decode(chunk.value); } expect(output).toContain('"pendingDiagnostics":1'); expect(output).toContain('"openDocuments":1'); 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; }); test("advertises and serves folding ranges and linked editing ranges over the wire", async () => { const process = Bun.spawn( ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))], { stdin: "pipe", stdout: "pipe", stderr: "pipe" }, ); const uri = "file:///fold.wrn"; const text = `page A { view {
x
} } `; 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'); const initReply = output.slice(output.indexOf('"id":1')); expect(initReply).toContain('"foldingRangeProvider":true'); expect(initReply).toContain('"linkedEditingRangeProvider":true'); process.stdin.write( packet({ jsonrpc: "2.0", method: "textDocument/didOpen", params: { textDocument: { uri, version: 1, text } }, }), ); process.stdin.write( packet({ jsonrpc: "2.0", id: 2, method: "textDocument/foldingRange", params: { textDocument: { uri } }, }), ); await process.stdin.flush(); await readUntil('"id":2'); const foldReply = output.slice(output.indexOf('"id":2')); expect(foldReply).toContain('"result":['); process.stdin.write( packet({ jsonrpc: "2.0", id: 3, method: "textDocument/linkedEditingRange", params: { textDocument: { uri }, position: { line: 2, character: 6 } }, }), ); await process.stdin.flush(); await readUntil('"id":3'); const linkedReply = output.slice(output.indexOf('"id":3')); expect(linkedReply).toContain('"ranges"'); process.kill(); await process.exited; }); test("serves wrn/tagComplete over the wire", async () => { const process = Bun.spawn( ["bun", "run", fileURLToPath(new URL("../src/server.ts", import.meta.url))], { stdin: "pipe", stdout: "pipe", stderr: "pipe" }, ); const uri = "file:///tag-complete.wrn"; const text = `page A { view {
} } `; 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 } }, }), ); process.stdin.write( packet({ jsonrpc: "2.0", id: 2, method: "wrn/tagComplete", params: { textDocument: { uri }, position: { line: 2, character: 9 } }, }), ); await process.stdin.flush(); await readUntil('"id":2'); const reply = output.slice(output.indexOf('"id":2')); expect(reply).toContain('"result":"$0
"'); process.kill(); await process.exited; });