release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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";
|
||||
Reference in New Issue
Block a user