feat(language-server): add tag folding and linked editing

This commit is contained in:
2026-08-18 21:10:57 +05:30
parent bd0c1317ff
commit d9e8f5be82
5 changed files with 149 additions and 2 deletions
@@ -3,6 +3,7 @@ import {
htmlCompletions,
htmlFoldingRanges,
htmlHover,
htmlLinkedEditingRanges,
htmlTagComplete,
} from "../src/html-service.ts";
@@ -172,3 +173,25 @@ test("merging ranks WRNexus entries above HTML and drops exact collisions", () =
const sorted = [...merged].sort((a, b) => (a.sortText ?? "").localeCompare(b.sortText ?? ""));
expect(sorted[0]!.label).toBe("Card");
});
test("linked editing returns both the opening and closing tag names", () => {
const text = `page A {
view {
<div>x</div>
}
}
`;
const ranges = htmlLinkedEditingRanges(doc(text), positionOf(text, "<di"));
expect(ranges).not.toBeNull();
expect(ranges).toHaveLength(2);
});
test("linked editing returns null outside a view block", () => {
const text = `page A {
functions {
function go() { }
}
}
`;
expect(htmlLinkedEditingRanges(doc(text), positionOf(text, "func"))).toBeNull();
});
@@ -305,3 +305,69 @@ test("didClose drops the region cache so a reopened document at the same version
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 {
<div>x</div>
}
}
`;
const reader = process.stdout.getReader();
let output = "";
async function readUntil(marker: string): Promise<void> {
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;
});