release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
completionItems,
|
||||
definitionLocation,
|
||||
documentDiagnostics,
|
||||
documentSymbols,
|
||||
formatDocument,
|
||||
hover,
|
||||
symbolLocations,
|
||||
wordAt,
|
||||
virtualTypeScriptDocument,
|
||||
workspaceCompletionItems,
|
||||
extractComponentRefactor,
|
||||
htmlToWrn,
|
||||
type TextDocument,
|
||||
} from "./index.ts";
|
||||
|
||||
type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any };
|
||||
const documents = new Map<string, TextDocument>();
|
||||
const diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const MAX_OPEN_DOCUMENTS = 256;
|
||||
const DIAGNOSTIC_DEBOUNCE_MS = 300;
|
||||
let buffer = Buffer.alloc(0);
|
||||
let workspaceRoot = process.cwd();
|
||||
|
||||
function rootFromUri(uri?: string): string {
|
||||
if (!uri?.startsWith("file://")) return workspaceRoot;
|
||||
return decodeURIComponent(uri.slice(7)).replace(/^\/([A-Za-z]:)/, "$1");
|
||||
}
|
||||
|
||||
function send(value: unknown): void {
|
||||
const body = Buffer.from(JSON.stringify(value));
|
||||
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
||||
process.stdout.write(body);
|
||||
}
|
||||
function result(id: JsonRpc["id"], value: unknown): void {
|
||||
send({ jsonrpc: "2.0", id, result: value });
|
||||
}
|
||||
function publish(document: TextDocument): void {
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "textDocument/publishDiagnostics",
|
||||
params: { uri: document.uri, diagnostics: documentDiagnostics(document) },
|
||||
});
|
||||
}
|
||||
|
||||
function clearDiagnosticTimer(uri: string): void {
|
||||
const timer = diagnosticTimers.get(uri);
|
||||
if (timer) clearTimeout(timer);
|
||||
diagnosticTimers.delete(uri);
|
||||
}
|
||||
|
||||
function schedulePublish(document: TextDocument): void {
|
||||
clearDiagnosticTimer(document.uri);
|
||||
const expectedVersion = document.version;
|
||||
diagnosticTimers.set(
|
||||
document.uri,
|
||||
setTimeout(() => {
|
||||
diagnosticTimers.delete(document.uri);
|
||||
const current = documents.get(document.uri);
|
||||
if (current && current.version === expectedVersion) publish(current);
|
||||
}, DIAGNOSTIC_DEBOUNCE_MS),
|
||||
);
|
||||
}
|
||||
|
||||
function rememberDocument(document: TextDocument): void {
|
||||
documents.delete(document.uri);
|
||||
documents.set(document.uri, document);
|
||||
while (documents.size > MAX_OPEN_DOCUMENTS) {
|
||||
const oldest = documents.keys().next().value;
|
||||
if (typeof oldest !== "string") break;
|
||||
documents.delete(oldest);
|
||||
clearDiagnosticTimer(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
async function handle(message: JsonRpc): Promise<void> {
|
||||
const params = message.params ?? {};
|
||||
switch (message.method) {
|
||||
case "initialize":
|
||||
workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
|
||||
result(message.id, {
|
||||
serverInfo: { name: "WRNexus Language Server", version: "0.8.0" },
|
||||
capabilities: {
|
||||
textDocumentSync: 1,
|
||||
documentFormattingProvider: true,
|
||||
completionProvider: { triggerCharacters: ["<", "@", ":", "."] },
|
||||
hoverProvider: true,
|
||||
definitionProvider: true,
|
||||
referencesProvider: true,
|
||||
renameProvider: { prepareProvider: true },
|
||||
documentSymbolProvider: true,
|
||||
codeActionProvider: {
|
||||
codeActionKinds: ["quickfix", "refactor.extract", "refactor.rewrite"],
|
||||
},
|
||||
experimental: { wrnexusVirtualTypeScript: true },
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "initialized":
|
||||
break;
|
||||
case "shutdown":
|
||||
for (const timer of diagnosticTimers.values()) clearTimeout(timer);
|
||||
diagnosticTimers.clear();
|
||||
result(message.id, null);
|
||||
break;
|
||||
case "exit":
|
||||
process.exit(0);
|
||||
break;
|
||||
case "textDocument/didOpen": {
|
||||
const item = params.textDocument;
|
||||
const document = { uri: item.uri, text: item.text, version: item.version };
|
||||
rememberDocument(document);
|
||||
publish(document);
|
||||
break;
|
||||
}
|
||||
case "textDocument/didChange": {
|
||||
const existing = documents.get(params.textDocument.uri);
|
||||
const text = params.contentChanges?.at(-1)?.text;
|
||||
if (existing && typeof text === "string") {
|
||||
existing.text = text;
|
||||
existing.version = params.textDocument.version;
|
||||
schedulePublish(existing);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "textDocument/didClose":
|
||||
clearDiagnosticTimer(params.textDocument.uri);
|
||||
documents.delete(params.textDocument.uri);
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "textDocument/publishDiagnostics",
|
||||
params: { uri: params.textDocument.uri, diagnostics: [] },
|
||||
});
|
||||
break;
|
||||
case "wrnexus/serverStatus":
|
||||
result(message.id, {
|
||||
openDocuments: documents.size,
|
||||
pendingDiagnostics: diagnosticTimers.size,
|
||||
memory: process.memoryUsage(),
|
||||
});
|
||||
break;
|
||||
case "textDocument/formatting": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(
|
||||
message.id,
|
||||
document
|
||||
? formatDocument(document, params.options?.tabSize, params.options?.insertSpaces)
|
||||
: [],
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "textDocument/completion":
|
||||
result(message.id, [...completionItems(), ...workspaceCompletionItems(workspaceRoot)]);
|
||||
break;
|
||||
case "textDocument/documentSymbol": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? documentSymbols(document) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/hover": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? hover(document, params.position) : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/definition": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? definitionLocation(document, params.position) : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/references": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? symbolLocations(document, params.position) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/prepareRename": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? (wordAt(document.text, params.position)?.range ?? null) : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/rename": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const edits = document
|
||||
? symbolLocations(document, params.position).map(({ range }) => ({
|
||||
range,
|
||||
newText: params.newName,
|
||||
}))
|
||||
: [];
|
||||
result(message.id, document ? { changes: { [document.uri]: edits } } : null);
|
||||
break;
|
||||
}
|
||||
case "textDocument/codeAction": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (!document) {
|
||||
result(message.id, []);
|
||||
break;
|
||||
}
|
||||
const actions: any[] = documentDiagnostics(document)
|
||||
.filter((item) => item.code === "WRNA11Y001")
|
||||
.map((item) => ({
|
||||
title: "Add empty alt attribute",
|
||||
kind: "quickfix",
|
||||
diagnostics: [item],
|
||||
edit: {
|
||||
changes: {
|
||||
[document.uri]: [
|
||||
{ range: { start: item.range.end, end: item.range.end }, newText: ' alt=""' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}));
|
||||
const selected = document.text.slice(
|
||||
document.text
|
||||
.split(/\r?\n/)
|
||||
.slice(0, params.range.start.line)
|
||||
.reduce((n, line) => n + line.length + 1, 0) + params.range.start.character,
|
||||
document.text
|
||||
.split(/\r?\n/)
|
||||
.slice(0, params.range.end.line)
|
||||
.reduce((n, line) => n + line.length + 1, 0) + params.range.end.character,
|
||||
);
|
||||
if (selected.trim().startsWith("<")) {
|
||||
try {
|
||||
actions.push({
|
||||
title: "Extract selection to WRN component",
|
||||
kind: "refactor.extract",
|
||||
edit: extractComponentRefactor(document, params.range, "ExtractedComponent"),
|
||||
});
|
||||
actions.push({
|
||||
title: "Convert selected HTML to WRN page",
|
||||
kind: "refactor.rewrite",
|
||||
edit: {
|
||||
changes: {
|
||||
[document.uri]: [
|
||||
{ range: params.range, newText: htmlToWrn(selected, "ImportedPage") },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
/* selection is not safely convertible */
|
||||
}
|
||||
}
|
||||
result(message.id, actions);
|
||||
break;
|
||||
}
|
||||
case "wrnexus/virtualDocument": {
|
||||
const document = documents.get(params.textDocument?.uri ?? params.uri);
|
||||
result(message.id, document ? virtualTypeScriptDocument(document) : null);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (message.id !== undefined) result(message.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
function consume(): void {
|
||||
while (true) {
|
||||
const end = buffer.indexOf("\r\n\r\n");
|
||||
if (end < 0) return;
|
||||
const header = buffer.subarray(0, end).toString();
|
||||
const length = Number(/Content-Length:\s*(\d+)/i.exec(header)?.[1]);
|
||||
if (!Number.isFinite(length)) {
|
||||
buffer = Buffer.alloc(0);
|
||||
return;
|
||||
}
|
||||
const bodyStart = end + 4;
|
||||
if (buffer.length < bodyStart + length) return;
|
||||
const body = buffer.subarray(bodyStart, bodyStart + length).toString();
|
||||
buffer = buffer.subarray(bodyStart + length);
|
||||
void handle(JSON.parse(body));
|
||||
}
|
||||
}
|
||||
process.stdin.on("data", (chunk) => {
|
||||
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
||||
consume();
|
||||
});
|
||||
process.stdin.resume();
|
||||
Reference in New Issue
Block a user