release: Vs Code New Extension with layout Support
This commit is contained in:
@@ -46,7 +46,7 @@
|
|||||||
"folding": {
|
"folding": {
|
||||||
"offSide": false,
|
"offSide": false,
|
||||||
"markers": {
|
"markers": {
|
||||||
"start": "^\\s*(?:page|component|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$",
|
"start": "^\\s*(?:page|component|layout|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$",
|
||||||
"end": "^\\s*\\}\\s*$"
|
"end": "^\\s*\\}\\s*$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"displayName": "WRNexus Language Support",
|
"displayName": "WRNexus Language Support",
|
||||||
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"publisher": "wrnexus",
|
"publisher": "wrnexus",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
|||||||
@@ -56,6 +56,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"WRN layout": {
|
||||||
|
"prefix": ["wrn-layout", "layout"],
|
||||||
|
"description": "Create a WRN layout",
|
||||||
|
"body": [
|
||||||
|
"layout ${1:LayoutName} {",
|
||||||
|
" view {",
|
||||||
|
" <main>",
|
||||||
|
" {content}",
|
||||||
|
" </main>",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
"WRN state": {
|
"WRN state": {
|
||||||
"prefix": ["wrn-state", "state"],
|
"prefix": ["wrn-state", "state"],
|
||||||
"description": "Create reactive state",
|
"description": "Create reactive state",
|
||||||
|
|||||||
+103
-26
@@ -274,10 +274,10 @@ function parse(source) {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const opener = lx.next();
|
const opener = lx.next();
|
||||||
if (opener.type !== "ident" || opener.value !== "page" && opener.value !== "component") {
|
if (opener.type !== "ident" || !["page", "component", "layout"].includes(opener.value)) {
|
||||||
throw new ParseError(`Expected 'page' or 'component' but got '${opener.value || opener.type}' at offset ${opener.pos}`);
|
throw new ParseError(`Expected 'page', 'component', or 'layout' but got '${opener.value || opener.type}' at offset ${opener.pos}`);
|
||||||
}
|
}
|
||||||
const kind = opener.value === "component" ? "component" : "page";
|
const kind = opener.value;
|
||||||
const name = expect("ident").value;
|
const name = expect("ident").value;
|
||||||
expect("lbrace");
|
expect("lbrace");
|
||||||
let layout;
|
let layout;
|
||||||
@@ -448,12 +448,12 @@ function parse(source) {
|
|||||||
function parseHtmlView(src, pos) {
|
function parseHtmlView(src, pos) {
|
||||||
let i = pos;
|
let i = pos;
|
||||||
const isNameStart = (c) => /[A-Za-z_]/.test(c);
|
const isNameStart = (c) => /[A-Za-z_]/.test(c);
|
||||||
const isNamePart = (c) => /[A-Za-z0-9_:.[\]%-]/.test(c);
|
const isTagNamePart = (c) => /[A-Za-z0-9_$:.-]/.test(c);
|
||||||
const isAttributeNamePart = (c, next) => {
|
const isAttributeNamePart = (c, next) => {
|
||||||
if (c === "/") {
|
if (c === "/") {
|
||||||
return next !== ">";
|
return next !== ">";
|
||||||
}
|
}
|
||||||
return isNamePart(c);
|
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
|
||||||
};
|
};
|
||||||
const isWs2 = (c) => c === " " || c === "\t" || c === `
|
const isWs2 = (c) => c === " " || c === "\t" || c === `
|
||||||
` || c === "\r";
|
` || c === "\r";
|
||||||
@@ -491,9 +491,21 @@ function parseHtmlView(src, pos) {
|
|||||||
i++;
|
i++;
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
const readName = () => {
|
const readTagName = () => {
|
||||||
if (i >= src.length || !isNameStart(src[i]))
|
if (i >= src.length || !isNameStart(src[i])) {
|
||||||
return fail("Expected a tag or attribute name");
|
return fail("Expected a tag name");
|
||||||
|
}
|
||||||
|
const start = i++;
|
||||||
|
while (i < src.length && isTagNamePart(src[i])) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return src.slice(start, i);
|
||||||
|
};
|
||||||
|
const readAttributeName = () => {
|
||||||
|
const first = src[i];
|
||||||
|
if (i >= src.length || !isNameStart(first) && first !== ":" && first !== "$") {
|
||||||
|
return fail("Expected an attribute name");
|
||||||
|
}
|
||||||
const start = i++;
|
const start = i++;
|
||||||
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
|
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
|
||||||
i++;
|
i++;
|
||||||
@@ -502,7 +514,7 @@ function parseHtmlView(src, pos) {
|
|||||||
};
|
};
|
||||||
const parseTag = () => {
|
const parseTag = () => {
|
||||||
i++;
|
i++;
|
||||||
const tag = readName();
|
const tag = readTagName();
|
||||||
const attrs = [];
|
const attrs = [];
|
||||||
for (;; ) {
|
for (;; ) {
|
||||||
skipWs();
|
skipWs();
|
||||||
@@ -519,7 +531,7 @@ function parseHtmlView(src, pos) {
|
|||||||
}
|
}
|
||||||
if (c === "@") {
|
if (c === "@") {
|
||||||
i++;
|
i++;
|
||||||
const name2 = readName();
|
const name2 = readAttributeName();
|
||||||
skipWs();
|
skipWs();
|
||||||
if (src[i] !== "=")
|
if (src[i] !== "=")
|
||||||
return fail(`Expected '=' after @${name2}`);
|
return fail(`Expected '=' after @${name2}`);
|
||||||
@@ -528,7 +540,7 @@ function parseHtmlView(src, pos) {
|
|||||||
attrs.push({ name: name2, value: readQuoted(), event: true });
|
attrs.push({ name: name2, value: readQuoted(), event: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const name = readName();
|
const name = readAttributeName();
|
||||||
skipWs();
|
skipWs();
|
||||||
if (src[i] === "=") {
|
if (src[i] === "=") {
|
||||||
i++;
|
i++;
|
||||||
@@ -546,7 +558,7 @@ function parseHtmlView(src, pos) {
|
|||||||
return fail(`Expected </${tag}>`);
|
return fail(`Expected </${tag}>`);
|
||||||
i += 2;
|
i += 2;
|
||||||
skipWs();
|
skipWs();
|
||||||
const close = readName();
|
const close = readTagName();
|
||||||
if (close !== tag)
|
if (close !== tag)
|
||||||
return fail(`Mismatched </${close}>, expected </${tag}>`);
|
return fail(`Mismatched </${close}>, expected </${tag}>`);
|
||||||
skipWs();
|
skipWs();
|
||||||
@@ -674,6 +686,9 @@ function parseHtmlView(src, pos) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ../../packages/compiler/src/codegen.ts
|
// ../../packages/compiler/src/codegen.ts
|
||||||
|
function isComponentTag(tag) {
|
||||||
|
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||||
|
}
|
||||||
function attrEscape(value) {
|
function attrEscape(value) {
|
||||||
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||||
}
|
}
|
||||||
@@ -804,21 +819,30 @@ function bakeLoopAttr(raw) {
|
|||||||
return out + escLit(attrEscape(raw.slice(last)));
|
return out + escLit(attrEscape(raw.slice(last)));
|
||||||
}
|
}
|
||||||
function renderLoopBody(node) {
|
function renderLoopBody(node) {
|
||||||
if (node.type === "text")
|
if (node.type === "text") {
|
||||||
return bakeLoopText(node.value);
|
return bakeLoopText(node.value);
|
||||||
if (node.type === "each")
|
}
|
||||||
|
if (node.type === "each") {
|
||||||
return compileEachExpr(node);
|
return compileEachExpr(node);
|
||||||
if (node.type === "if")
|
}
|
||||||
|
if (node.type === "if") {
|
||||||
return compileIfExpr(node);
|
return compileIfExpr(node);
|
||||||
const attrs = node.attrs.map((a) => {
|
}
|
||||||
const name = a.event ? eventAttribute(a.name) : a.name;
|
const componentTag = isComponentTag(node.tag);
|
||||||
if (a.boolean)
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
||||||
|
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
||||||
|
if (attr.boolean) {
|
||||||
return escLit(` ${name}`);
|
return escLit(` ${name}`);
|
||||||
return escLit(` ${name}="`) + bakeLoopAttr(a.value) + escLit(`"`);
|
}
|
||||||
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
||||||
}).join("");
|
}).join("");
|
||||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase()))
|
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
const inner = node.children.map(renderLoopBody).join("");
|
||||||
|
if (componentTag) {
|
||||||
|
return escLit(`<div data-component="${attrEscape(node.tag)}"`) + attrs + escLit(">") + inner + escLit("</div>");
|
||||||
|
}
|
||||||
|
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||||
|
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
||||||
|
}
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||||
}
|
}
|
||||||
function compileEachExpr(node) {
|
function compileEachExpr(node) {
|
||||||
@@ -862,6 +886,9 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
|||||||
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
||||||
return `\x00WRNEACH${loops.length - 1}\x00`;
|
return `\x00WRNEACH${loops.length - 1}\x00`;
|
||||||
}
|
}
|
||||||
|
if (isComponentTag(node.tag)) {
|
||||||
|
return renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive);
|
||||||
|
}
|
||||||
const apiName = attrValue(node.attrs, "api");
|
const apiName = attrValue(node.attrs, "api");
|
||||||
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
||||||
if (apiName && !apiBinding) {
|
if (apiName && !apiBinding) {
|
||||||
@@ -888,6 +915,35 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
|||||||
}) : node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
}) : node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
||||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
||||||
}
|
}
|
||||||
|
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
||||||
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component");
|
||||||
|
const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
||||||
|
return `<div data-component="${attrEscape(node.tag)}"${renderAttrs(attrs, undefined, reactive)}>${inner}</div>`;
|
||||||
|
}
|
||||||
|
function renderNestedComponentInvocation(node, ctx) {
|
||||||
|
let bindIndex = 0;
|
||||||
|
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => {
|
||||||
|
if (attr.event) {
|
||||||
|
return ` ${eventAttribute(attr.name)}="${compileAttrValue(attr.value, ctx)}"`;
|
||||||
|
}
|
||||||
|
if (attr.boolean) {
|
||||||
|
return ` ${attr.name}`;
|
||||||
|
}
|
||||||
|
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`;
|
||||||
|
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
||||||
|
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
||||||
|
}).join("");
|
||||||
|
const loops = loopVarsOf(node);
|
||||||
|
const childCtx = loops.length > 0 ? {
|
||||||
|
...ctx,
|
||||||
|
loopVars: new Set([...ctx.loopVars ?? [], ...loops])
|
||||||
|
} : ctx;
|
||||||
|
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||||
|
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
||||||
|
}
|
||||||
function ssrMarker(bindings, binding) {
|
function ssrMarker(bindings, binding) {
|
||||||
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
||||||
bindings.push({ marker, ...binding });
|
bindings.push({ marker, ...binding });
|
||||||
@@ -1000,8 +1056,9 @@ async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<strin
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
function generate(ast) {
|
function generate(ast) {
|
||||||
if (ast.kind === "component")
|
if (ast.kind === "component" || ast.kind === "layout") {
|
||||||
return generateComponent(ast);
|
return generateComponent(ast);
|
||||||
|
}
|
||||||
const out = [];
|
const out = [];
|
||||||
const ssrBindings = [];
|
const ssrBindings = [];
|
||||||
const csrBindings = [];
|
const csrBindings = [];
|
||||||
@@ -1230,6 +1287,8 @@ function compileText(raw, ctx) {
|
|||||||
out += escLit(`{${expr}}`);
|
out += escLit(`{${expr}}`);
|
||||||
} else if (exprRefsState(expr, ctx.stateNames)) {
|
} else if (exprRefsState(expr, ctx.stateNames)) {
|
||||||
out += escLit(`<span data-text="${attrEscape(expr)}">`) + `\${__wireHtml(${ctx.resolveExpr(expr)})}` + escLit(`</span>`);
|
out += escLit(`<span data-text="${attrEscape(expr)}">`) + `\${__wireHtml(${ctx.resolveExpr(expr)})}` + escLit(`</span>`);
|
||||||
|
} else if (expr === "content") {
|
||||||
|
out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
|
||||||
} else {
|
} else {
|
||||||
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
|
||||||
}
|
}
|
||||||
@@ -1262,6 +1321,9 @@ function renderComponentNode(node, ctx) {
|
|||||||
if (node.type === "each" || node.type === "if") {
|
if (node.type === "each" || node.type === "if") {
|
||||||
throw new Error("Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).");
|
throw new Error("Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).");
|
||||||
}
|
}
|
||||||
|
if (isComponentTag(node.tag)) {
|
||||||
|
return renderNestedComponentInvocation(node, ctx);
|
||||||
|
}
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const staticClasses = [];
|
const staticClasses = [];
|
||||||
const conditionalClasses = [];
|
const conditionalClasses = [];
|
||||||
@@ -1309,10 +1371,18 @@ function renderComponentNode(node, ctx) {
|
|||||||
}
|
}
|
||||||
function generateComponent(ast) {
|
function generateComponent(ast) {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ? [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
default: '""'
|
||||||
|
},
|
||||||
|
...ast.props
|
||||||
|
] : ast.props;
|
||||||
const stateNames = new Set(ast.states.map((s) => s.name));
|
const stateNames = new Set(ast.states.map((s) => s.name));
|
||||||
const nameRefs = new Map;
|
const nameRefs = new Map;
|
||||||
for (const p of ast.props)
|
for (const p of effectiveProps) {
|
||||||
nameRefs.set(p.name, safeRef(p.name));
|
nameRefs.set(p.name, safeRef(p.name));
|
||||||
|
}
|
||||||
for (const s of ast.states)
|
for (const s of ast.states)
|
||||||
nameRefs.set(s.name, safeRef(s.name));
|
nameRefs.set(s.name, safeRef(s.name));
|
||||||
const resolveExpr = (expr) => {
|
const resolveExpr = (expr) => {
|
||||||
@@ -1331,9 +1401,12 @@ ${styles.map(styleEscape).join(`
|
|||||||
`)}
|
`)}
|
||||||
</style>`) : "";
|
</style>`) : "";
|
||||||
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view);
|
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view);
|
||||||
const scopeKeys = [...ast.props.map((p) => p.name), ...ast.states.map((s) => s.name)];
|
const scopeKeys = [
|
||||||
|
...effectiveProps.map((prop) => prop.name),
|
||||||
|
...ast.states.map((state) => state.name)
|
||||||
|
];
|
||||||
const decls = [];
|
const decls = [];
|
||||||
for (const prop of ast.props) {
|
for (const prop of effectiveProps) {
|
||||||
decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`);
|
decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`);
|
||||||
}
|
}
|
||||||
for (const state of ast.states) {
|
for (const state of ast.states) {
|
||||||
@@ -1343,7 +1416,11 @@ ${styles.map(styleEscape).join(`
|
|||||||
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });
|
const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });
|
||||||
` : needsScope ? ` const __scope = "";
|
` : needsScope ? ` const __scope = "";
|
||||||
` : "";
|
` : "";
|
||||||
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
if (ast.kind === "layout") {
|
||||||
|
out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
|
||||||
|
} else {
|
||||||
|
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
||||||
|
}
|
||||||
out.push(`function __coerce(v: any, def: any): any {
|
out.push(`function __coerce(v: any, def: any): any {
|
||||||
if (v === undefined || v === null) return def;
|
if (v === undefined || v === null) return def;
|
||||||
if (typeof def === "number") return Number(v);
|
if (typeof def === "number") return Number(v);
|
||||||
|
|||||||
@@ -33,6 +33,20 @@ const BLOCK_COMPLETIONS = [
|
|||||||
"}",
|
"}",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "layout",
|
||||||
|
detail: "WRN layout",
|
||||||
|
documentation: "Create a reusable WRN layout.",
|
||||||
|
snippet: [
|
||||||
|
"layout ${1:LayoutName} {",
|
||||||
|
" view {",
|
||||||
|
" <main>",
|
||||||
|
" {content}",
|
||||||
|
" </main>",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "seo",
|
label: "seo",
|
||||||
detail: "SEO metadata block",
|
detail: "SEO metadata block",
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const vscode = require("vscode");
|
||||||
|
|
||||||
|
const COMPONENT_DECLARATION =
|
||||||
|
/^\s*(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/gm;
|
||||||
|
|
||||||
|
function getTagAtPosition(document, position) {
|
||||||
|
const range = document.getWordRangeAtPosition(
|
||||||
|
position,
|
||||||
|
/[A-Za-z_$][\w$]*/,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.getText(range);
|
||||||
|
|
||||||
|
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = document.lineAt(position.line).text;
|
||||||
|
const offset = document.offsetAt(position);
|
||||||
|
const lineStart = document.offsetAt(
|
||||||
|
new vscode.Position(position.line, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
const characterOffset = offset - lineStart;
|
||||||
|
const before = line.slice(0, characterOffset);
|
||||||
|
const after = line.slice(characterOffset);
|
||||||
|
|
||||||
|
const insideTag =
|
||||||
|
before.lastIndexOf("<") > before.lastIndexOf(">") &&
|
||||||
|
after.includes(">");
|
||||||
|
|
||||||
|
if (!insideTag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
range,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findDeclaration(name) {
|
||||||
|
const files = await vscode.workspace.findFiles(
|
||||||
|
"**/*.wrn",
|
||||||
|
"**/{node_modules,dist,.wrnexus,.git}/**",
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
for (const uri of files) {
|
||||||
|
let document;
|
||||||
|
|
||||||
|
try {
|
||||||
|
document = await vscode.workspace.openTextDocument(uri);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = document.getText();
|
||||||
|
|
||||||
|
COMPONENT_DECLARATION.lastIndex = 0;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = COMPONENT_DECLARATION.exec(source)) !== null) {
|
||||||
|
const declarationName = match[2];
|
||||||
|
|
||||||
|
if (declarationName !== name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameOffset =
|
||||||
|
match.index +
|
||||||
|
match[0].indexOf(declarationName);
|
||||||
|
|
||||||
|
const start = document.positionAt(nameOffset);
|
||||||
|
const end = document.positionAt(
|
||||||
|
nameOffset + declarationName.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
matches.push(
|
||||||
|
new vscode.Location(
|
||||||
|
uri,
|
||||||
|
new vscode.Range(start, end),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function provideDefinition(document, position) {
|
||||||
|
const tag = getTagAtPosition(document, position);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await findDeclaration(tag.name);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.length === 1 ? matches[0] : matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerDefinitionProvider(context) {
|
||||||
|
const disposable = vscode.languages.registerDefinitionProvider(
|
||||||
|
{
|
||||||
|
language: "wrn",
|
||||||
|
scheme: "file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provideDefinition,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
context.subscriptions.push(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
findDeclaration,
|
||||||
|
getTagAtPosition,
|
||||||
|
provideDefinition,
|
||||||
|
registerDefinitionProvider,
|
||||||
|
};
|
||||||
@@ -0,0 +1,761 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const vscode = require("vscode");
|
||||||
|
|
||||||
|
const COLLECTION_NAME = "wrnexus";
|
||||||
|
const WRN_LANGUAGE_ID = "wrn";
|
||||||
|
|
||||||
|
const TOP_LEVEL_PATTERN =
|
||||||
|
/^\s*(page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/;
|
||||||
|
|
||||||
|
const VALID_TOP_LEVEL_KINDS = new Set([
|
||||||
|
"page",
|
||||||
|
"component",
|
||||||
|
"layout",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const VALID_MEMBERS = {
|
||||||
|
page: new Set([
|
||||||
|
"layout",
|
||||||
|
"state",
|
||||||
|
"view",
|
||||||
|
"seo",
|
||||||
|
"style",
|
||||||
|
"functions",
|
||||||
|
"api",
|
||||||
|
"ssr",
|
||||||
|
"client",
|
||||||
|
"realtime",
|
||||||
|
]),
|
||||||
|
|
||||||
|
component: new Set([
|
||||||
|
"props",
|
||||||
|
"state",
|
||||||
|
"view",
|
||||||
|
"style",
|
||||||
|
"functions",
|
||||||
|
]),
|
||||||
|
|
||||||
|
layout: new Set([
|
||||||
|
"props",
|
||||||
|
"state",
|
||||||
|
"view",
|
||||||
|
"style",
|
||||||
|
"functions",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
function createDiagnostic(
|
||||||
|
document,
|
||||||
|
startOffset,
|
||||||
|
endOffset,
|
||||||
|
message,
|
||||||
|
severity = vscode.DiagnosticSeverity.Error,
|
||||||
|
code,
|
||||||
|
) {
|
||||||
|
const diagnostic = new vscode.Diagnostic(
|
||||||
|
new vscode.Range(
|
||||||
|
document.positionAt(startOffset),
|
||||||
|
document.positionAt(endOffset),
|
||||||
|
),
|
||||||
|
message,
|
||||||
|
severity,
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostic.source = "WRNexus";
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
diagnostic.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineDiagnostic(
|
||||||
|
document,
|
||||||
|
lineNumber,
|
||||||
|
message,
|
||||||
|
severity = vscode.DiagnosticSeverity.Error,
|
||||||
|
code,
|
||||||
|
) {
|
||||||
|
const line = document.lineAt(lineNumber);
|
||||||
|
|
||||||
|
const diagnostic = new vscode.Diagnostic(
|
||||||
|
line.range,
|
||||||
|
message,
|
||||||
|
severity,
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostic.source = "WRNexus";
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
diagnostic.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripComments(source) {
|
||||||
|
return source.replace(/<!--[\s\S]*?-->/g, (comment) =>
|
||||||
|
comment.replace(/[^\n]/g, " "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTopLevelDeclaration(document, source) {
|
||||||
|
const match = TOP_LEVEL_PATTERN.exec(source);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
const firstMeaningfulLine = source
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.findIndex((line) => line.trim().length > 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
diagnostic: lineDiagnostic(
|
||||||
|
document,
|
||||||
|
Math.max(0, firstMeaningfulLine),
|
||||||
|
"A .wrn file must start with `page`, `component`, or `layout`.",
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-invalid-root",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: match[1],
|
||||||
|
name: match[2],
|
||||||
|
match,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBalancedCharacters(document, source) {
|
||||||
|
const diagnostics = [];
|
||||||
|
const stack = [];
|
||||||
|
|
||||||
|
let quote = null;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
const pairs = {
|
||||||
|
"}": "{",
|
||||||
|
"]": "[",
|
||||||
|
")": "(",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < source.length; index += 1) {
|
||||||
|
const character = source[index];
|
||||||
|
|
||||||
|
if (quote !== null) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === quote) {
|
||||||
|
quote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === '"' || character === "'") {
|
||||||
|
quote = character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
source.startsWith("<!--", index)
|
||||||
|
) {
|
||||||
|
const commentEnd = source.indexOf("-->", index + 4);
|
||||||
|
|
||||||
|
if (commentEnd === -1) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
index,
|
||||||
|
Math.min(source.length, index + 4),
|
||||||
|
"Unclosed HTML comment.",
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-unclosed-comment",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = commentEnd + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
character === "{" ||
|
||||||
|
character === "[" ||
|
||||||
|
character === "("
|
||||||
|
) {
|
||||||
|
stack.push({
|
||||||
|
character,
|
||||||
|
offset: index,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
character === "}" ||
|
||||||
|
character === "]" ||
|
||||||
|
character === ")"
|
||||||
|
) {
|
||||||
|
const expectedOpening = pairs[character];
|
||||||
|
const opening = stack.pop();
|
||||||
|
|
||||||
|
if (!opening || opening.character !== expectedOpening) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
index,
|
||||||
|
index + 1,
|
||||||
|
`Unexpected \`${character}\`.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-unexpected-closing",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const opening of stack) {
|
||||||
|
const expectedClosing =
|
||||||
|
opening.character === "{"
|
||||||
|
? "}"
|
||||||
|
: opening.character === "["
|
||||||
|
? "]"
|
||||||
|
: ")";
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
opening.offset,
|
||||||
|
opening.offset + 1,
|
||||||
|
`Missing closing \`${expectedClosing}\`.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-missing-closing",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quote !== null) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
Math.max(0, source.length - 1),
|
||||||
|
source.length,
|
||||||
|
`Unclosed ${quote === '"' ? "double" : "single"} quote.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-unclosed-string",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateHtmlTags(document, source) {
|
||||||
|
const diagnostics = [];
|
||||||
|
const stack = [];
|
||||||
|
|
||||||
|
const voidElements = new Set([
|
||||||
|
"area",
|
||||||
|
"base",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"embed",
|
||||||
|
"hr",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"param",
|
||||||
|
"source",
|
||||||
|
"track",
|
||||||
|
"wbr",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cleaned = stripComments(source);
|
||||||
|
const tagPattern =
|
||||||
|
/<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = tagPattern.exec(cleaned)) !== null) {
|
||||||
|
const completeTag = match[0];
|
||||||
|
const tagName = match[1];
|
||||||
|
const lowerTag = tagName.toLowerCase();
|
||||||
|
|
||||||
|
const isClosing = completeTag.startsWith("</");
|
||||||
|
const isSelfClosing = completeTag.endsWith("/>");
|
||||||
|
const isVoid = voidElements.has(lowerTag);
|
||||||
|
|
||||||
|
if (isClosing) {
|
||||||
|
const last = stack.pop();
|
||||||
|
|
||||||
|
if (!last) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
match.index,
|
||||||
|
match.index + completeTag.length,
|
||||||
|
`Unexpected closing tag </${tagName}>.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-unexpected-html-close",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.tagName !== tagName) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
match.index,
|
||||||
|
match.index + completeTag.length,
|
||||||
|
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-mismatched-html-tag",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelfClosing && !isVoid) {
|
||||||
|
stack.push({
|
||||||
|
tagName,
|
||||||
|
offset: match.index,
|
||||||
|
length: completeTag.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of stack) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
tag.offset,
|
||||||
|
tag.offset + tag.length,
|
||||||
|
`Missing closing tag </${tag.tagName}>.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-missing-html-close",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRootBodyRange(source, rootMatch) {
|
||||||
|
const openingBrace =
|
||||||
|
rootMatch.index + rootMatch[0].lastIndexOf("{");
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let quote = null;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let index = openingBrace;
|
||||||
|
index < source.length;
|
||||||
|
index += 1
|
||||||
|
) {
|
||||||
|
const character = source[index];
|
||||||
|
|
||||||
|
if (quote !== null) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === quote) {
|
||||||
|
quote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === '"' || character === "'") {
|
||||||
|
quote = character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "{") {
|
||||||
|
depth += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "}") {
|
||||||
|
depth -= 1;
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
return {
|
||||||
|
start: openingBrace + 1,
|
||||||
|
end: index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: openingBrace + 1,
|
||||||
|
end: source.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRootMembers(source, bodyStart, bodyEnd) {
|
||||||
|
const members = [];
|
||||||
|
let index = bodyStart;
|
||||||
|
let depth = 0;
|
||||||
|
let quote = null;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
while (index < bodyEnd) {
|
||||||
|
const character = source[index];
|
||||||
|
|
||||||
|
if (quote !== null) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === quote) {
|
||||||
|
quote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === '"' || character === "'") {
|
||||||
|
quote = character;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.startsWith("<!--", index)) {
|
||||||
|
const end = source.indexOf("-->", index + 4);
|
||||||
|
index = end === -1 ? bodyEnd : end + 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "{") {
|
||||||
|
depth += 1;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "}") {
|
||||||
|
depth = Math.max(0, depth - 1);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
depth === 0 &&
|
||||||
|
/[A-Za-z_]/.test(character)
|
||||||
|
) {
|
||||||
|
const start = index;
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
while (
|
||||||
|
index < bodyEnd &&
|
||||||
|
/[A-Za-z0-9_-]/.test(source[index])
|
||||||
|
) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = source.slice(start, index);
|
||||||
|
|
||||||
|
members.push({
|
||||||
|
name,
|
||||||
|
start,
|
||||||
|
end: index,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRootMembers(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
rootKind,
|
||||||
|
rootMatch,
|
||||||
|
) {
|
||||||
|
const diagnostics = [];
|
||||||
|
const allowed = VALID_MEMBERS[rootKind];
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||||
|
const members = findRootMembers(
|
||||||
|
source,
|
||||||
|
bodyRange.start,
|
||||||
|
bodyRange.end,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const member of members) {
|
||||||
|
if (allowed.has(member.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
member.start,
|
||||||
|
member.end,
|
||||||
|
`Unknown ${rootKind} member \`${member.name}\`.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-unknown-member",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequiredView(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
rootKind,
|
||||||
|
rootMatch,
|
||||||
|
) {
|
||||||
|
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||||
|
const body = source.slice(bodyRange.start, bodyRange.end);
|
||||||
|
|
||||||
|
if (/\bview\s*\{/.test(body)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
rootMatch.index,
|
||||||
|
rootMatch.index + rootMatch[0].length,
|
||||||
|
`The ${rootKind} \`${rootMatch[2]}\` does not contain a \`view { ... }\` block.`,
|
||||||
|
vscode.DiagnosticSeverity.Warning,
|
||||||
|
"wrn-missing-view",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLayoutUsage(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
rootKind,
|
||||||
|
) {
|
||||||
|
const diagnostics = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
rootKind !== "page" &&
|
||||||
|
/^\s*layout\s*=/m.test(source)
|
||||||
|
) {
|
||||||
|
const match = /^\s*layout\s*=/m.exec(source);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
match.index,
|
||||||
|
match.index + match[0].length,
|
||||||
|
"`layout = \"...\"` is only valid inside a page.",
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-invalid-layout-member",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDocument(document) {
|
||||||
|
if (document.languageId !== WRN_LANGUAGE_ID) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = document.getText();
|
||||||
|
|
||||||
|
if (!source.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagnostics = [];
|
||||||
|
const declaration = findTopLevelDeclaration(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (declaration.diagnostic) {
|
||||||
|
diagnostics.push(declaration.diagnostic);
|
||||||
|
diagnostics.push(
|
||||||
|
...validateBalancedCharacters(document, source),
|
||||||
|
);
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VALID_TOP_LEVEL_KINDS.has(declaration.kind)) {
|
||||||
|
diagnostics.push(
|
||||||
|
createDiagnostic(
|
||||||
|
document,
|
||||||
|
declaration.match.index,
|
||||||
|
declaration.match.index +
|
||||||
|
declaration.match[0].length,
|
||||||
|
`Unsupported WRN declaration \`${declaration.kind}\`.`,
|
||||||
|
vscode.DiagnosticSeverity.Error,
|
||||||
|
"wrn-invalid-kind",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
...validateBalancedCharacters(document, source),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
...validateHtmlTags(document, source),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
...validateRootMembers(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
declaration.kind,
|
||||||
|
declaration.match,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
...validateRequiredView(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
declaration.kind,
|
||||||
|
declaration.match,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.push(
|
||||||
|
...validateLayoutUsage(
|
||||||
|
document,
|
||||||
|
source,
|
||||||
|
declaration.kind,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerDiagnostics(context) {
|
||||||
|
const collection =
|
||||||
|
vscode.languages.createDiagnosticCollection(
|
||||||
|
COLLECTION_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
const timers = new Map();
|
||||||
|
|
||||||
|
const update = (document) => {
|
||||||
|
if (document.languageId !== WRN_LANGUAGE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTimer = timers.get(
|
||||||
|
document.uri.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (previousTimer) {
|
||||||
|
clearTimeout(previousTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
timers.delete(document.uri.toString());
|
||||||
|
|
||||||
|
collection.set(
|
||||||
|
document.uri,
|
||||||
|
validateDocument(document),
|
||||||
|
);
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
timers.set(document.uri.toString(), timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const document of vscode.workspace.textDocuments) {
|
||||||
|
update(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
|
collection,
|
||||||
|
|
||||||
|
vscode.workspace.onDidOpenTextDocument(update),
|
||||||
|
|
||||||
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||||
|
update(event.document);
|
||||||
|
}),
|
||||||
|
|
||||||
|
vscode.workspace.onDidSaveTextDocument(update),
|
||||||
|
|
||||||
|
vscode.workspace.onDidCloseTextDocument(
|
||||||
|
(document) => {
|
||||||
|
const key = document.uri.toString();
|
||||||
|
const timer = timers.get(key);
|
||||||
|
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timers.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.delete(document.uri);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
{
|
||||||
|
dispose() {
|
||||||
|
for (const timer of timers.values()) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
timers.clear();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
registerDiagnostics,
|
||||||
|
validateBalancedCharacters,
|
||||||
|
validateDocument,
|
||||||
|
validateHtmlTags,
|
||||||
|
validateRootMembers,
|
||||||
|
};
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
const vscode = require("vscode");
|
const vscode = require("vscode");
|
||||||
const { formatWrn } = require("./formatter");
|
const { formatWrn } = require("./formatter");
|
||||||
const { registerCompletionProvider } = require("./completion");
|
const { registerCompletionProvider } = require("./completion");
|
||||||
|
const { registerDefinitionProvider } = require("./definition");
|
||||||
|
const { registerDiagnostics } = require("./diagnostics");
|
||||||
|
|
||||||
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
||||||
// defensively so the rest of the extension (highlighting, snippets, completion)
|
// defensively so the rest of the extension (highlighting, snippets, completion)
|
||||||
@@ -68,7 +70,9 @@ const EVENTS = [
|
|||||||
* @param {vscode.ExtensionContext} context
|
* @param {vscode.ExtensionContext} context
|
||||||
*/
|
*/
|
||||||
function activate(context) {
|
function activate(context) {
|
||||||
|
registerDiagnostics(context);
|
||||||
registerCompletionProvider(context);
|
registerCompletionProvider(context);
|
||||||
|
registerDefinitionProvider(context);
|
||||||
|
|
||||||
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
|
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
|
||||||
context.subscriptions.push(diagnostics);
|
context.subscriptions.push(diagnostics);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"declaration": {
|
"declaration": {
|
||||||
"match": "\\b(page|component)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)?",
|
"match": "\\b(page|component|layout)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)?",
|
||||||
"captures": {
|
"captures": {
|
||||||
"1": { "name": "storage.type.wrn keyword.control.wrn" },
|
"1": { "name": "storage.type.wrn keyword.control.wrn" },
|
||||||
"2": { "name": "entity.name.type.wrn" }
|
"2": { "name": "entity.name.type.wrn" }
|
||||||
|
|||||||
Binary file not shown.
@@ -111,7 +111,7 @@ export function runGenerate(
|
|||||||
): void {
|
): void {
|
||||||
const type = typeArg ? ALIASES[typeArg] : undefined;
|
const type = typeArg ? ALIASES[typeArg] : undefined;
|
||||||
if (!type || !name) {
|
if (!type || !name) {
|
||||||
console.error("Usage: wrnexus generate <page|component|api|schema> <name>");
|
console.error("Usage: wrnexus generate <page|component|layout|api|schema> <name>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user