feat(language-server): add tag folding and linked editing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// WRN editor language server source hash: 6a4f4c8e370f4063c863f89ca0dd57923fdb2b940a3e63980984ed2ff45f7af0
|
||||
// WRN editor language server source hash: 1317c4e318ab938fffab3dd5b65655b0e220a81c481c9fa92f23706ede60303c
|
||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||
// @bun @bun-cjs
|
||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||
@@ -195960,6 +195960,16 @@ function htmlHover(document, position) {
|
||||
const contents = markdown(result.contents);
|
||||
return contents ? { contents } : null;
|
||||
}
|
||||
function htmlFoldingRanges(document) {
|
||||
return service.getFoldingRanges(htmlDocument(document)).map((range) => ({ startLine: range.startLine, endLine: range.endLine }));
|
||||
}
|
||||
function htmlLinkedEditingRanges(document, position) {
|
||||
if (!isInsideHtml(document, offsetAt2(document.text, position)))
|
||||
return null;
|
||||
const virtual = htmlDocument(document);
|
||||
const ranges = service.findLinkedEditingRanges(virtual, position, service.parseHTMLDocument(virtual));
|
||||
return ranges && ranges.length ? ranges : null;
|
||||
}
|
||||
function mergeCompletions(wrnexus, html) {
|
||||
const taken = new Set(wrnexus.map((item) => item.label));
|
||||
return [
|
||||
@@ -196066,6 +196076,8 @@ async function handle(message) {
|
||||
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"]
|
||||
},
|
||||
hoverProvider: true,
|
||||
foldingRangeProvider: true,
|
||||
linkedEditingRangeProvider: true,
|
||||
definitionProvider: true,
|
||||
referencesProvider: true,
|
||||
renameProvider: { prepareProvider: true },
|
||||
@@ -196169,6 +196181,17 @@ async function handle(message) {
|
||||
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
||||
break;
|
||||
}
|
||||
case "textDocument/foldingRange": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? htmlFoldingRanges(document) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/linkedEditingRange": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const ranges = document ? htmlLinkedEditingRanges(document, params.position) : null;
|
||||
result(message.id, ranges ? { ranges } : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/definition": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? definitionLocation(document, params.position) : null);
|
||||
|
||||
@@ -77,6 +77,22 @@ export function htmlFoldingRanges(
|
||||
.map((range) => ({ startLine: range.startLine, endLine: range.endLine }));
|
||||
}
|
||||
|
||||
/** Ranges of the opening and closing tag names, so renaming one renames both. */
|
||||
export function htmlLinkedEditingRanges(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
): Array<{ start: Position; end: Position }> | null {
|
||||
if (!isInsideHtml(document, offsetAt(document.text, position))) return null;
|
||||
|
||||
const virtual = htmlDocument(document);
|
||||
const ranges = service.findLinkedEditingRanges(
|
||||
virtual,
|
||||
position,
|
||||
service.parseHTMLDocument(virtual),
|
||||
);
|
||||
return ranges && ranges.length ? ranges : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects `<Name attr="x" /` just before `position` and completes the `>`.
|
||||
*
|
||||
|
||||
@@ -19,7 +19,13 @@ import {
|
||||
htmlToWrn,
|
||||
type TextDocument,
|
||||
} from "./index.ts";
|
||||
import { htmlCompletions, htmlHover, mergeCompletions } from "./html-service.ts";
|
||||
import {
|
||||
htmlCompletions,
|
||||
htmlFoldingRanges,
|
||||
htmlHover,
|
||||
htmlLinkedEditingRanges,
|
||||
mergeCompletions,
|
||||
} from "./html-service.ts";
|
||||
import { clearHtmlRegionCache } from "./html-regions.ts";
|
||||
|
||||
type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any };
|
||||
@@ -117,6 +123,8 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"],
|
||||
},
|
||||
hoverProvider: true,
|
||||
foldingRangeProvider: true,
|
||||
linkedEditingRangeProvider: true,
|
||||
definitionProvider: true,
|
||||
referencesProvider: true,
|
||||
renameProvider: { prepareProvider: true },
|
||||
@@ -223,6 +231,17 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
||||
break;
|
||||
}
|
||||
case "textDocument/foldingRange": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? htmlFoldingRanges(document) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/linkedEditingRange": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const ranges = document ? htmlLinkedEditingRanges(document, params.position) : null;
|
||||
result(message.id, ranges ? { ranges } : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/definition": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? definitionLocation(document, params.position) : null);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user