release: WRNexusJS 0.2.32
This commit is contained in:
@@ -20,15 +20,26 @@ const BLOCK_COMPLETIONS = [
|
||||
{
|
||||
label: "component",
|
||||
detail: "WRN component",
|
||||
documentation: "Create a reusable WRN component.",
|
||||
documentation:
|
||||
"Create a reusable WRN component with state, functions, lifecycle hooks, watchers, and a view.",
|
||||
snippet: [
|
||||
"component ${1:ComponentName} {",
|
||||
" props {",
|
||||
' ${2:title} = "${3:Title}"',
|
||||
" state ${2:ready} = false",
|
||||
"",
|
||||
" functions {",
|
||||
" function ${3:initialize}() {",
|
||||
" $0",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" lifecycle {",
|
||||
" mount {",
|
||||
" ${3:initialize}()",
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" view {",
|
||||
" $0",
|
||||
" <div></div>",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
@@ -47,9 +58,16 @@ const BLOCK_COMPLETIONS = [
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "props",
|
||||
detail: "Component or layout props block",
|
||||
documentation: "Declare values accepted by a component or layout.",
|
||||
snippet: ["props {", ' ${1:title} = "${2:Title}"', "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "seo",
|
||||
detail: "SEO metadata block",
|
||||
documentation: "Declare title, description, and other page metadata.",
|
||||
snippet: [
|
||||
"seo {",
|
||||
' title = "${1:Page title}"',
|
||||
@@ -60,13 +78,84 @@ const BLOCK_COMPLETIONS = [
|
||||
{
|
||||
label: "view",
|
||||
detail: "WRN view block",
|
||||
documentation: "Declare the HTML view rendered by the page, component, or layout.",
|
||||
snippet: ["view {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "state",
|
||||
detail: "Reactive state declaration",
|
||||
documentation: "Declare reactive state owned by the current component, layout, or page.",
|
||||
snippet: 'state ${1:name} = ${2:"value"}',
|
||||
},
|
||||
{
|
||||
label: "functions",
|
||||
detail: "Browser component functions block",
|
||||
documentation:
|
||||
"Declare functions that can be called from events, lifecycle hooks, and watchers.",
|
||||
snippet: ["functions {", " function ${1:handler}(${2}) {", " $0", " }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "function",
|
||||
detail: "WRN component function",
|
||||
documentation: "Declare a browser-side function inside a functions block.",
|
||||
snippet: ["function ${1:name}(${2}) {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "lifecycle",
|
||||
detail: "Component lifecycle block",
|
||||
documentation: "Declare mount, update, and unmount hooks for a component or layout.",
|
||||
snippet: [
|
||||
"lifecycle {",
|
||||
" mount {",
|
||||
" $1",
|
||||
" }",
|
||||
"",
|
||||
" update {",
|
||||
" $2",
|
||||
" }",
|
||||
"",
|
||||
" unmount {",
|
||||
" $0",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "mount",
|
||||
detail: "Lifecycle mount hook",
|
||||
documentation: "Runs once after the component is connected and hydrated.",
|
||||
snippet: ["mount {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "update",
|
||||
detail: "Lifecycle update hook",
|
||||
documentation: "Runs once after a batch of reactive state changes.",
|
||||
snippet: ["update {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "unmount",
|
||||
detail: "Lifecycle unmount hook",
|
||||
documentation:
|
||||
"Runs before the component is removed. Use it to remove global listeners and release resources.",
|
||||
snippet: ["unmount {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "watch",
|
||||
detail: "Reactive state watcher",
|
||||
documentation:
|
||||
"Run code when one declared state value changes. The watcher receives `value` and `previous`.",
|
||||
snippet: ["watch ${1:stateName} {", " console.log(value, previous)", " $0", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "style",
|
||||
detail: "Scoped style block",
|
||||
documentation: "Declare styles for the current WRN declaration.",
|
||||
snippet: ["style {", " $0", "}"].join("\n"),
|
||||
},
|
||||
];
|
||||
|
||||
const ATTRIBUTE_COMPLETIONS = [
|
||||
@@ -76,6 +165,19 @@ const ATTRIBUTE_COMPLETIONS = [
|
||||
["@submit", "Submit event handler", '@submit="${1:handler()}"'],
|
||||
["@focus", "Focus event handler", '@focus="${1:handler()}"'],
|
||||
["@blur", "Blur event handler", '@blur="${1:handler()}"'],
|
||||
["@keydown", "Keyboard key-down event handler", '@keydown="${1:handler()}"'],
|
||||
["@keyup", "Keyboard key-up event handler", '@keyup="${1:handler()}"'],
|
||||
["@mouseenter", "Pointer enter event handler", '@mouseenter="${1:handler()}"'],
|
||||
["@mouseleave", "Pointer leave event handler", '@mouseleave="${1:handler()}"'],
|
||||
["@window:scroll", "Window scroll event handler", '@window:scroll="${1:handler()}"'],
|
||||
["@window:resize", "Window resize event handler", '@window:resize="${1:handler()}"'],
|
||||
["@window:keydown", "Window key-down event handler", '@window:keydown="${1:handler()}"'],
|
||||
["@document:click", "Document click event handler", '@document:click="${1:handler()}"'],
|
||||
[
|
||||
"@document:visibilitychange",
|
||||
"Document visibility-change event handler",
|
||||
'@document:visibilitychange="${1:handler()}"',
|
||||
],
|
||||
["data-show", "Conditional visibility", 'data-show="${1:condition}"'],
|
||||
["data-for", "Reactive loop", 'data-for="${1:item} in ${2:items}"'],
|
||||
["class:", "Conditional CSS class", 'class:${1:border-indigo-500}="${2:condition}"'],
|
||||
@@ -90,6 +192,11 @@ const CONTEXT_COMPLETIONS = [
|
||||
["ctx.locals", "Request-local data"],
|
||||
];
|
||||
|
||||
const WATCH_VALUE_COMPLETIONS = [
|
||||
["value", "Current watcher state value"],
|
||||
["previous", "Previous watcher state value"],
|
||||
];
|
||||
|
||||
function completionKindFor(label) {
|
||||
if (label.startsWith("@")) {
|
||||
return vscode.CompletionItemKind.Event;
|
||||
@@ -99,15 +206,47 @@ function completionKindFor(label) {
|
||||
return vscode.CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
if (label === "function" || label === "functions") {
|
||||
return vscode.CompletionItemKind.Function;
|
||||
}
|
||||
|
||||
return vscode.CompletionItemKind.Keyword;
|
||||
}
|
||||
|
||||
function createCompletion(label, detail, snippet) {
|
||||
function createCompletion(label, detail, snippet, documentation) {
|
||||
const item = new vscode.CompletionItem(label, completionKindFor(label));
|
||||
|
||||
item.detail = detail;
|
||||
item.insertText = new vscode.SnippetString(snippet || label);
|
||||
item.documentation = new vscode.MarkdownString(detail);
|
||||
|
||||
item.documentation = new vscode.MarkdownString(documentation || detail);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function createVariableCompletion(label, detail, insertion = label) {
|
||||
const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Variable);
|
||||
|
||||
item.detail = detail;
|
||||
item.insertText = insertion;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function createFunctionCompletion(name, parameters = []) {
|
||||
const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);
|
||||
|
||||
item.detail = `WRN component function${
|
||||
parameters.length > 0 ? ` (${parameters.join(", ")})` : ""
|
||||
}`;
|
||||
|
||||
item.documentation = new vscode.MarkdownString(
|
||||
`Call the browser function \`${name}\` declared in the current \`functions { ... }\` block.`,
|
||||
);
|
||||
|
||||
const placeholders = parameters.map((parameter, index) => `\${${index + 1}:${parameter}}`);
|
||||
|
||||
item.insertText = new vscode.SnippetString(`${name}(${placeholders.join(", ")})`);
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -118,6 +257,7 @@ function getCurrentOpeningTag(document, position) {
|
||||
);
|
||||
|
||||
const lastOpen = textBeforeCursor.lastIndexOf("<");
|
||||
|
||||
const lastClose = textBeforeCursor.lastIndexOf(">");
|
||||
|
||||
if (lastOpen > lastClose) {
|
||||
@@ -129,6 +269,7 @@ function getCurrentOpeningTag(document, position) {
|
||||
|
||||
function extractRouteParams(document) {
|
||||
const fileName = document.fileName.replace(/\\/g, "/");
|
||||
|
||||
const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)];
|
||||
|
||||
return matches.map((match) => match[1]);
|
||||
@@ -136,13 +277,195 @@ function extractRouteParams(document) {
|
||||
|
||||
function extractStates(document) {
|
||||
const source = document.getText();
|
||||
|
||||
const matches = [...source.matchAll(/^\s*state\s+([A-Za-z_$][\w$]*)\s*=/gm)];
|
||||
|
||||
return matches.map((match) => match[1]);
|
||||
return [...new Set(matches.map((match) => match[1]))];
|
||||
}
|
||||
|
||||
function extractProps(document) {
|
||||
const source = document.getText();
|
||||
const propsBlockPattern = /\bprops\s*\{([\s\S]*?)\}/g;
|
||||
|
||||
const props = new Set();
|
||||
let blockMatch;
|
||||
|
||||
while ((blockMatch = propsBlockPattern.exec(source)) !== null) {
|
||||
const body = blockMatch[1];
|
||||
|
||||
for (const match of body.matchAll(/^\s*([A-Za-z_$][\w$]*)\s*=/gm)) {
|
||||
props.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...props];
|
||||
}
|
||||
|
||||
function extractFunctions(document) {
|
||||
const source = document.getText();
|
||||
const functions = [];
|
||||
const seen = new Set();
|
||||
|
||||
const pattern = /(?:^|\s)(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/g;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
const name = match[1];
|
||||
|
||||
if (seen.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(name);
|
||||
|
||||
const parameters = match[2]
|
||||
.split(",")
|
||||
.map((parameter) => parameter.trim())
|
||||
.filter(Boolean)
|
||||
.map((parameter) => parameter.replace(/=.*$/, "").trim());
|
||||
|
||||
functions.push({
|
||||
name,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
function sourceBeforePosition(document, position) {
|
||||
return document.getText(new vscode.Range(new vscode.Position(0, 0), position));
|
||||
}
|
||||
|
||||
function isInsideNamedBlock(document, position, blockName) {
|
||||
const source = sourceBeforePosition(document, position);
|
||||
|
||||
const tokenPattern = new RegExp(`\\b${blockName}\\s*\\{|\\{|\\}`, "g");
|
||||
|
||||
const stack = [];
|
||||
let match;
|
||||
|
||||
while ((match = tokenPattern.exec(source)) !== null) {
|
||||
const token = match[0];
|
||||
|
||||
if (token.startsWith(blockName)) {
|
||||
stack.push(blockName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "{") {
|
||||
stack.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "}") {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return stack.includes(blockName);
|
||||
}
|
||||
|
||||
function isInsideLifecycle(document, position) {
|
||||
return isInsideNamedBlock(document, position, "lifecycle");
|
||||
}
|
||||
|
||||
function isInsideFunctions(document, position) {
|
||||
return isInsideNamedBlock(document, position, "functions");
|
||||
}
|
||||
|
||||
function isInsideWatch(document, position) {
|
||||
const source = sourceBeforePosition(document, position);
|
||||
|
||||
const watchMatches = [...source.matchAll(/\bwatch\s+[A-Za-z_$][\w$]*\s*\{/g)];
|
||||
|
||||
if (watchMatches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const latest = watchMatches[watchMatches.length - 1];
|
||||
|
||||
const tail = source.slice(latest.index);
|
||||
let depth = 0;
|
||||
|
||||
for (const character of tail) {
|
||||
if (character === "{") {
|
||||
depth += 1;
|
||||
} else if (character === "}") {
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return depth > 0;
|
||||
}
|
||||
|
||||
function isAfterWatchKeyword(document, position) {
|
||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||
|
||||
return /^\s*watch\s+[A-Za-z0-9_$]*$/.test(linePrefix);
|
||||
}
|
||||
|
||||
function addBlockCompletions(items, document, position) {
|
||||
const insideLifecycle = isInsideLifecycle(document, position);
|
||||
|
||||
const insideFunctions = isInsideFunctions(document, position);
|
||||
|
||||
for (const completion of BLOCK_COMPLETIONS) {
|
||||
if (insideLifecycle && !["mount", "update", "unmount"].includes(completion.label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (insideFunctions && completion.label !== "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideLifecycle && ["mount", "update", "unmount"].includes(completion.label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideFunctions && completion.label === "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(
|
||||
createCompletion(
|
||||
completion.label,
|
||||
completion.detail,
|
||||
completion.snippet,
|
||||
completion.documentation,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addStateCompletions(items, document, position) {
|
||||
const states = extractStates(document);
|
||||
const afterWatch = isAfterWatchKeyword(document, position);
|
||||
|
||||
for (const state of states) {
|
||||
const item = createVariableCompletion(
|
||||
state,
|
||||
afterWatch ? "Declared WRN state available for watching" : "WRN reactive state",
|
||||
);
|
||||
|
||||
if (afterWatch) {
|
||||
item.sortText = `0-${state}`;
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
function addFunctionCompletions(items, document) {
|
||||
for (const fn of extractFunctions(document)) {
|
||||
items.push(createFunctionCompletion(fn.name, fn.parameters));
|
||||
}
|
||||
}
|
||||
|
||||
function provideCompletionItems(document, position) {
|
||||
const items = [];
|
||||
|
||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||
|
||||
const openingTag = getCurrentOpeningTag(document, position);
|
||||
@@ -151,10 +474,10 @@ function provideCompletionItems(document, position) {
|
||||
for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) {
|
||||
items.push(createCompletion(label, detail, snippet));
|
||||
}
|
||||
} else if (isAfterWatchKeyword(document, position)) {
|
||||
addStateCompletions(items, document, position);
|
||||
} else {
|
||||
for (const completion of BLOCK_COMPLETIONS) {
|
||||
items.push(createCompletion(completion.label, completion.detail, completion.snippet));
|
||||
}
|
||||
addBlockCompletions(items, document, position);
|
||||
}
|
||||
|
||||
if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) {
|
||||
@@ -164,31 +487,27 @@ function provideCompletionItems(document, position) {
|
||||
}
|
||||
|
||||
for (const param of extractRouteParams(document)) {
|
||||
const item = new vscode.CompletionItem(param, vscode.CompletionItemKind.Variable);
|
||||
items.push(createVariableCompletion(param, `Route parameter from [${param}].wrn`));
|
||||
|
||||
item.detail = `Route parameter from [${param}].wrn`;
|
||||
item.insertText = param;
|
||||
|
||||
items.push(item);
|
||||
|
||||
const fullItem = new vscode.CompletionItem(
|
||||
`ctx.params.${param}`,
|
||||
vscode.CompletionItemKind.Variable,
|
||||
items.push(
|
||||
createVariableCompletion(`ctx.params.${param}`, `Route parameter from [${param}].wrn`),
|
||||
);
|
||||
|
||||
fullItem.detail = `Route parameter from [${param}].wrn`;
|
||||
fullItem.insertText = `ctx.params.${param}`;
|
||||
|
||||
items.push(fullItem);
|
||||
}
|
||||
|
||||
for (const state of extractStates(document)) {
|
||||
const item = new vscode.CompletionItem(state, vscode.CompletionItemKind.Variable);
|
||||
if (!isAfterWatchKeyword(document, position)) {
|
||||
addStateCompletions(items, document, position);
|
||||
}
|
||||
|
||||
item.detail = "WRN state";
|
||||
item.insertText = state;
|
||||
for (const prop of extractProps(document)) {
|
||||
items.push(createVariableCompletion(prop, "WRN component or layout prop"));
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
addFunctionCompletions(items, document);
|
||||
|
||||
if (isInsideWatch(document, position)) {
|
||||
for (const [label, detail] of WATCH_VALUE_COMPLETIONS) {
|
||||
items.push(createVariableCompletion(label, detail));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -196,7 +515,10 @@ function provideCompletionItems(document, position) {
|
||||
|
||||
function registerCompletionProvider(context) {
|
||||
const provider = vscode.languages.registerCompletionItemProvider(
|
||||
{ language: "wrn", scheme: "file" },
|
||||
{
|
||||
language: "wrn",
|
||||
scheme: "file",
|
||||
},
|
||||
{
|
||||
provideCompletionItems,
|
||||
},
|
||||
@@ -205,13 +527,17 @@ function registerCompletionProvider(context) {
|
||||
".",
|
||||
"<",
|
||||
" ",
|
||||
"(",
|
||||
);
|
||||
|
||||
context.subscriptions.push(provider);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractFunctions,
|
||||
extractProps,
|
||||
extractRouteParams,
|
||||
extractStates,
|
||||
provideCompletionItems,
|
||||
registerCompletionProvider,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user