Files
WRNexusJS/packages/language-server/src/server.ts
T
Clintchiz 72e4d3eceb
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-04 12:19:09 +05:30

326 lines
11 KiB
TypeScript

#!/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 internalDiagnostic(document: TextDocument, error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return {
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
severity: 1,
code: "WRN-LSP-INTERNAL",
source: "wrnexus",
message: `WRNexus language analysis failed safely: ${message}`,
};
}
function safeDocumentDiagnostics(document: TextDocument) {
try {
return documentDiagnostics(document);
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
return [internalDiagnostic(document, error)];
}
}
function publish(document: TextDocument): void {
send({
jsonrpc: "2.0",
method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(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.3" },
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[] = safeDocumentDiagnostics(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);
let message: JsonRpc;
try {
message = JSON.parse(body) as JsonRpc;
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] invalid JSON-RPC payload: ${error instanceof Error ? error.message : String(error)}\n`,
);
continue;
}
void handle(message).catch((error) => {
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
process.stderr.write(`[wrnexus-lsp] request failed: ${detail}\n`);
if (message.id !== undefined) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32603, message: "WRNexus language server request failed", data: detail },
});
}
});
}
}
process.stdin.on("data", (chunk) => {
try {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
consume();
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] input processing failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
}
});
process.stdin.resume();