feat(language-server): add tag folding and linked editing
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||||
// @bun @bun-cjs
|
// @bun @bun-cjs
|
||||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||||
@@ -195960,6 +195960,16 @@ function htmlHover(document, position) {
|
|||||||
const contents = markdown(result.contents);
|
const contents = markdown(result.contents);
|
||||||
return contents ? { contents } : null;
|
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) {
|
function mergeCompletions(wrnexus, html) {
|
||||||
const taken = new Set(wrnexus.map((item) => item.label));
|
const taken = new Set(wrnexus.map((item) => item.label));
|
||||||
return [
|
return [
|
||||||
@@ -196066,6 +196076,8 @@ async function handle(message) {
|
|||||||
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"]
|
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"]
|
||||||
},
|
},
|
||||||
hoverProvider: true,
|
hoverProvider: true,
|
||||||
|
foldingRangeProvider: true,
|
||||||
|
linkedEditingRangeProvider: true,
|
||||||
definitionProvider: true,
|
definitionProvider: true,
|
||||||
referencesProvider: true,
|
referencesProvider: true,
|
||||||
renameProvider: { prepareProvider: true },
|
renameProvider: { prepareProvider: true },
|
||||||
@@ -196169,6 +196181,17 @@ async function handle(message) {
|
|||||||
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
||||||
break;
|
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": {
|
case "textDocument/definition": {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
result(message.id, document ? definitionLocation(document, params.position) : null);
|
result(message.id, document ? definitionLocation(document, params.position) : null);
|
||||||
|
|||||||
@@ -77,6 +77,22 @@ export function htmlFoldingRanges(
|
|||||||
.map((range) => ({ startLine: range.startLine, endLine: range.endLine }));
|
.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 `>`.
|
* Detects `<Name attr="x" /` just before `position` and completes the `>`.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ import {
|
|||||||
htmlToWrn,
|
htmlToWrn,
|
||||||
type TextDocument,
|
type TextDocument,
|
||||||
} from "./index.ts";
|
} 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";
|
import { clearHtmlRegionCache } from "./html-regions.ts";
|
||||||
|
|
||||||
type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any };
|
type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any };
|
||||||
@@ -117,6 +123,8 @@ async function handle(message: JsonRpc): Promise<void> {
|
|||||||
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"],
|
triggerCharacters: ["<", "@", ":", ".", " ", "=", '"', "/"],
|
||||||
},
|
},
|
||||||
hoverProvider: true,
|
hoverProvider: true,
|
||||||
|
foldingRangeProvider: true,
|
||||||
|
linkedEditingRangeProvider: true,
|
||||||
definitionProvider: true,
|
definitionProvider: true,
|
||||||
referencesProvider: true,
|
referencesProvider: true,
|
||||||
renameProvider: { prepareProvider: 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));
|
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
||||||
break;
|
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": {
|
case "textDocument/definition": {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
result(message.id, document ? definitionLocation(document, params.position) : null);
|
result(message.id, document ? definitionLocation(document, params.position) : null);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
htmlCompletions,
|
htmlCompletions,
|
||||||
htmlFoldingRanges,
|
htmlFoldingRanges,
|
||||||
htmlHover,
|
htmlHover,
|
||||||
|
htmlLinkedEditingRanges,
|
||||||
htmlTagComplete,
|
htmlTagComplete,
|
||||||
} from "../src/html-service.ts";
|
} 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 ?? ""));
|
const sorted = [...merged].sort((a, b) => (a.sortText ?? "").localeCompare(b.sortText ?? ""));
|
||||||
expect(sorted[0]!.label).toBe("Card");
|
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();
|
process.kill();
|
||||||
await process.exited;
|
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