Files
WRNexusJS/packages/language-server/src/index.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

239 lines
7.4 KiB
TypeScript

import { diagnose, formatWrn, type WrnDiagnostic } from "@wrnexus/syntax";
import { checkWrnSource, virtualTypeScriptModule } from "@wrnexus/typecheck";
export interface Position {
line: number;
character: number;
}
export interface Range {
start: Position;
end: Position;
}
export interface TextDocument {
uri: string;
text: string;
version?: number;
}
export const WRN_COMPLETIONS = [
"page",
"component",
"layout",
"props",
"outputs",
"state",
"computed",
"effect",
"watch",
"lifecycle",
"load",
"action",
"api",
"realtime",
"view",
"style",
"runtime",
"hydrate",
] as const;
export function offsetAt(text: string, position: Position): number {
const lines = text.split(/\r?\n/);
let offset = 0;
for (let line = 0; line < Math.min(position.line, lines.length); line++)
offset += (lines[line]?.length ?? 0) + 1;
return Math.min(text.length, offset + Math.max(0, position.character));
}
export function positionAt(text: string, requestedOffset: number): Position {
const offset = Math.max(0, Math.min(text.length, requestedOffset));
const prefix = text.slice(0, offset);
const lines = prefix.split(/\r?\n/);
return { line: lines.length - 1, character: lines.at(-1)?.length ?? 0 };
}
export function wordAt(text: string, position: Position): { word: string; range: Range } | null {
const offset = offsetAt(text, position);
const left = text.slice(0, offset).match(/[A-Za-z_$][\w$]*$/)?.[0] ?? "";
const right = text.slice(offset).match(/^[\w$]*/)?.[0] ?? "";
const word = left + right;
if (!word) return null;
const start = offset - left.length;
return {
word,
range: { start: positionAt(text, start), end: positionAt(text, start + word.length) },
};
}
function diagnosticRange(diagnostic: WrnDiagnostic): Range {
const start = diagnostic.position ?? { line: 1, column: 1, offset: 0 };
return {
start: { line: Math.max(0, start.line - 1), character: Math.max(0, start.column - 1) },
end: {
line: Math.max(0, start.line - 1),
character: Math.max(1, start.column - 1 + Math.max(1, diagnostic.received?.length ?? 1)),
},
};
}
export function documentDiagnostics(document: TextDocument) {
if (Buffer.byteLength(document.text, "utf8") > 1_048_576) {
return [
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
severity: 2,
code: "WRN-LSP-FILE-SIZE",
source: "wrnexus",
message: "Type analysis is disabled because this WRN document exceeds 1 MiB.",
},
];
}
const syntax = diagnose(document.text, { file: document.uri, accessibility: true }).map(
(diagnostic) => ({
range: diagnosticRange(diagnostic),
severity: diagnostic.severity === "error" ? 1 : diagnostic.severity === "warning" ? 2 : 3,
code: diagnostic.code,
source: "wrnexus",
message: diagnostic.message,
}),
);
if (syntax.some((diagnostic) => diagnostic.severity === 1)) return syntax;
const types = checkWrnSource(document.text, { filePath: documentPath(document.uri) }).map(
(diagnostic) => ({
range: {
start: {
line: Math.max(0, diagnostic.line - 1),
character: Math.max(0, diagnostic.column - 1),
},
end: {
line: Math.max(0, diagnostic.line - 1),
character: Math.max(1, diagnostic.column - 1 + Math.max(1, diagnostic.length)),
},
},
severity: diagnostic.category === "error" ? 1 : diagnostic.category === "warning" ? 2 : 3,
code: diagnostic.code,
source: "wrnexus-types",
message: diagnostic.message,
}),
);
const seen = new Set(
syntax.map((item) => `${item.code}:${item.range.start.line}:${item.range.start.character}`),
);
return [
...syntax,
...types.filter(
(item) => !seen.has(`${item.code}:${item.range.start.line}:${item.range.start.character}`),
),
];
}
function documentPath(uri: string): string {
if (!uri.startsWith("file://")) return uri;
const value = decodeURIComponent(uri.slice("file://".length));
return /^\/[A-Za-z]:\//.test(value) ? value.slice(1) : value;
}
/** TypeScript representation consumed by editor TypeScript plugins and safe refactoring tools. */
export function virtualTypeScriptDocument(document: TextDocument): {
uri: string;
languageId: "typescript";
text: string;
mappings: Array<{
virtualStartLine: number;
virtualEndLine: number;
sourceStartLine: number;
sourceStartColumn: number;
}>;
} {
const virtual = virtualTypeScriptModule(document.text, documentPath(document.uri));
return {
uri: `${document.uri}.ts`,
languageId: "typescript",
text: virtual.code,
mappings: virtual.mappings,
};
}
export function formatDocument(document: TextDocument, tabSize = 4, insertSpaces = true) {
const formatted = formatWrn(document.text, { tabSize, insertSpaces });
if (formatted === document.text) return [];
return [
{
range: {
start: { line: 0, character: 0 },
end: positionAt(document.text, document.text.length),
},
newText: formatted,
},
];
}
export function documentSymbols(document: TextDocument) {
const pattern =
/\b(page|component|layout|state|computed|watch|effect|load|action|api)\s+([A-Za-z_$][\w$]*)/g;
return [...document.text.matchAll(pattern)].map((match) => {
const name = match[2]!;
const start = match.index! + match[0].lastIndexOf(name);
const range = {
start: positionAt(document.text, start),
end: positionAt(document.text, start + name.length),
};
return {
name,
kind: ["page", "component", "layout"].includes(match[1]!) ? 5 : 13,
range,
selectionRange: range,
};
});
}
export function symbolLocations(document: TextDocument, position: Position) {
const selected = wordAt(document.text, position);
if (!selected) return [];
const pattern = new RegExp(
`(?<![A-Za-z0-9_$])${selected.word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`,
"g",
);
return [...document.text.matchAll(pattern)].map((match) => ({
uri: document.uri,
range: {
start: positionAt(document.text, match.index!),
end: positionAt(document.text, match.index! + selected.word.length),
},
}));
}
export function definitionLocation(document: TextDocument, position: Position) {
const selected = wordAt(document.text, position);
if (!selected) return null;
const declaration = new RegExp(
`\\b(?:state|computed|page|component|layout)\\s+${selected.word}\\b|\\b${selected.word}\\s*(?=[:?])`,
).exec(document.text);
if (!declaration) return null;
const start = declaration.index + declaration[0].lastIndexOf(selected.word);
return {
uri: document.uri,
range: {
start: positionAt(document.text, start),
end: positionAt(document.text, start + selected.word.length),
},
};
}
export function hover(document: TextDocument, position: Position) {
const selected = wordAt(document.text, position);
if (!selected) return null;
const declaration = new RegExp(
`\\b(state|computed|prop|page|component|layout)\\s+${selected.word}\\b`,
).exec(document.text);
if (!declaration) return null;
return {
contents: { kind: "markdown", value: `\`\`\`wrn\n${declaration[0]}\n\`\`\`` },
range: selected.range,
};
}
export function completionItems() {
return WRN_COMPLETIONS.map((label) => ({ label, kind: 14, detail: "WRNexus language keyword" }));
}
export * from "./workspace.ts";