799 lines
22 KiB
JavaScript
799 lines
22 KiB
JavaScript
"use strict";
|
|
|
|
const vscode = require("vscode");
|
|
|
|
const BLOCK_COMPLETIONS = [
|
|
{
|
|
label: "import",
|
|
detail: "WRN server module import",
|
|
documentation:
|
|
"Import a package or module before the page, component, or layout declaration for server rendering.",
|
|
snippet: 'import { ${1:appUrl} } from "${2:@wrnexus/helpers}";\n\n$0',
|
|
},
|
|
{
|
|
label: "page",
|
|
detail: "WRN page",
|
|
documentation: "Create a WRN page declaration.",
|
|
snippet: [
|
|
"page ${1:PageName} {",
|
|
' layout = "${2:default}"',
|
|
"",
|
|
" view {",
|
|
" $0",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "component",
|
|
detail: "WRN component",
|
|
documentation:
|
|
"Create a reusable WRN component with state, functions, lifecycle hooks, watchers, and a view.",
|
|
snippet: [
|
|
"component ${1:ComponentName} {",
|
|
" state ${2:ready} = false",
|
|
"",
|
|
" functions {",
|
|
" function ${3:initialize}() {",
|
|
" $0",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" lifecycle {",
|
|
" mount {",
|
|
" ${3:initialize}()",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" view {",
|
|
" <div></div>",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "layout",
|
|
detail: "WRN layout",
|
|
documentation: "Create a reusable WRN layout.",
|
|
snippet: [
|
|
"layout ${1:LayoutName} {",
|
|
" view {",
|
|
" <main>",
|
|
" {content}",
|
|
" </main>",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "types",
|
|
detail: "TypeScript type declarations block",
|
|
documentation: "Declare interfaces and type aliases used by props, state, and functions.",
|
|
snippet: [
|
|
"types {",
|
|
" interface ${1:Item} {",
|
|
" ${2:id}: ${3:string}",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "props",
|
|
detail: "Component or layout props block",
|
|
documentation: "Declare values accepted by a component or layout.",
|
|
snippet: ["props {", ' ${1:title}: ${2:string} = "${3:Title}"', "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "seo",
|
|
detail: "SEO metadata block",
|
|
documentation: "Declare title, description, and other page metadata.",
|
|
snippet: [
|
|
"seo {",
|
|
' title = "${1:Page title}"',
|
|
' description = "${2:Page description}"',
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
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:string} = ${3:"value"}',
|
|
},
|
|
{
|
|
label: "runtime",
|
|
detail: "WRN execution target",
|
|
documentation: "Choose whether this declaration executes on the server, client, or both.",
|
|
snippet: 'runtime = "${1|universal,server,client|}"',
|
|
},
|
|
{
|
|
label: "hydrate",
|
|
detail: "WRN hydration strategy",
|
|
documentation: "Choose when client interactivity is initialized for this declaration.",
|
|
snippet: 'hydrate = "${1|load,idle,visible,interaction,none|}"',
|
|
},
|
|
{
|
|
label: "computed",
|
|
detail: "Derived reactive values block",
|
|
documentation:
|
|
"Declare values that are recomputed only when their reactive dependencies change.",
|
|
snippet: ["computed {", " ${1:displayName} = ${2:firstName + ' ' + lastName}", "}"].join(
|
|
"\n",
|
|
),
|
|
},
|
|
{
|
|
label: "effect",
|
|
detail: "Reactive side-effect block",
|
|
documentation: "Run browser-side code whenever its referenced reactive values change.",
|
|
snippet: ["effect {", " ${1:console.log(value)}", "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "security",
|
|
detail: "Route security metadata block",
|
|
documentation: "Declare authentication, authorization, CSRF, and rate-limit policy metadata.",
|
|
snippet: [
|
|
"security {",
|
|
' auth = "${1|required,optional,public|}"',
|
|
' csrf = "${2:true}"',
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "load",
|
|
detail: "Typed data-loading block",
|
|
documentation: "Load data on the server or client with an explicit execution boundary.",
|
|
snippet: ["load ${1|server,client|} {", " ${2:return {}}", "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "action",
|
|
detail: "Named server action",
|
|
documentation: "Declare a callable mutation with an explicit name and arguments.",
|
|
snippet: ["action ${1:save}(${2:input}) {", " $0", "}"].join("\n"),
|
|
},
|
|
{
|
|
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:value}: ${3:unknown}): ${4:void} {",
|
|
" $0",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "function",
|
|
detail: "WRN component function",
|
|
documentation: "Declare a browser-side function inside a functions block.",
|
|
snippet: ["function ${1:name}(${2:value}: ${3:unknown}): ${4:void} {", " $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"),
|
|
},
|
|
{
|
|
label: "import type",
|
|
detail: "WRN v0.6 TypeScript type import",
|
|
snippet: 'import type { ${1:PublicUser} } from "${2:@/types/user.ts}"\n\n$0',
|
|
},
|
|
{
|
|
label: "outputs",
|
|
detail: "Typed callable component outputs",
|
|
snippet: [
|
|
"outputs {",
|
|
" ${1:confirm}(payload: ${2:ConfirmPayload})",
|
|
" ${3:cancel}()",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "client function",
|
|
detail: "Browser-only typed function",
|
|
snippet: ["client ${1|,async |}function ${2:name}(${3}): ${4:void} {", " $0", "}"].join(
|
|
"\n",
|
|
),
|
|
},
|
|
{
|
|
label: "server function",
|
|
detail: "Server-only typed function",
|
|
snippet: ["server ${1|,async |}function ${2:name}(${3}): ${4:void} {", " $0", "}"].join(
|
|
"\n",
|
|
),
|
|
},
|
|
{
|
|
label: "shared function",
|
|
detail: "Server-and-browser typed function",
|
|
snippet: ["shared function ${1:name}(${2}): ${3:void} {", " $0", "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "state block",
|
|
detail: "Grouped typed reactive state",
|
|
snippet: ["state {", ' ${1:name}: ${2:string} = ${3:""}', "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "client state",
|
|
detail: "Browser-only state block",
|
|
snippet: ["client state {", " ${1:menuOpen}: boolean = false", "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "server state",
|
|
detail: "Server-only state block",
|
|
snippet: ["server state {", " ${1:sessionId}: string | null = null", "}"].join("\n"),
|
|
},
|
|
{
|
|
label: "global store",
|
|
detail: "Request-scoped global store",
|
|
snippet: [
|
|
"global store ${1:UserStore} {",
|
|
" state {",
|
|
" ${2:user}: ${3:PublicUser | null} = null",
|
|
" }",
|
|
"",
|
|
" computed {",
|
|
" ${4:authenticated}: boolean = ${2:user} !== null",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "page store",
|
|
detail: "Route-scoped page store",
|
|
snippet: [
|
|
"page store ${1:PageStore} {",
|
|
" state {",
|
|
" ${2:loading}: boolean = false",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "apis",
|
|
detail: "Page-level API call declarations block",
|
|
documentation:
|
|
"Declare the API calls available to this page as named entries, each callable via api.<name>() from functions and the `api=` view binding.",
|
|
snippet: [
|
|
"apis {",
|
|
" ${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
|
|
" request {",
|
|
" body {",
|
|
" ${4:name}?: ${5:string}",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" response {",
|
|
" return ${6:data}",
|
|
" }",
|
|
"",
|
|
" error {",
|
|
" return ${7:null}",
|
|
" }",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "apis entry",
|
|
detail: "Single API call declaration",
|
|
documentation:
|
|
"Declare a single named API call inside an apis { } block: <name> <METHOD> <path> { request/response/error }.",
|
|
snippet: [
|
|
"${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
|
|
" request {",
|
|
" body {",
|
|
" ${4:name}?: ${5:string}",
|
|
" }",
|
|
" }",
|
|
"",
|
|
" response {",
|
|
" return ${6:data}",
|
|
" }",
|
|
"",
|
|
" error {",
|
|
" return ${7:null}",
|
|
" }",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
{
|
|
label: "persist",
|
|
detail: "Include-only store persistence",
|
|
snippet: [
|
|
"persist {",
|
|
' storage = "${1|memory,session,local|}"',
|
|
' include = ["${2:field}"]',
|
|
" version = ${3:1}",
|
|
"}",
|
|
].join("\n"),
|
|
},
|
|
];
|
|
|
|
const ATTRIBUTE_COMPLETIONS = [
|
|
["@click", "Click event handler", '@click="${1:handler()}"'],
|
|
["@change", "Change event handler", '@change="${1:handler()}"'],
|
|
["@input", "Input event handler", '@input="${1:handler()}"'],
|
|
["@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}"'],
|
|
];
|
|
|
|
const CONTEXT_COMPLETIONS = [
|
|
["ctx.params", "Dynamic route parameters"],
|
|
["ctx.query", "URL query parameters"],
|
|
["ctx.request", "Current Request object"],
|
|
["ctx.user", "Authenticated user"],
|
|
["ctx.session", "Current session"],
|
|
["ctx.locals", "Request-local data"],
|
|
["ctx.tenant", "Resolved tenant context"],
|
|
["ctx.tracer", "Request tracing interface"],
|
|
];
|
|
|
|
const WATCH_VALUE_COMPLETIONS = [
|
|
["value", "Current watcher state value"],
|
|
["previous", "Previous watcher state value"],
|
|
];
|
|
|
|
function completionKindFor(label) {
|
|
if (label.startsWith("@")) {
|
|
return vscode.CompletionItemKind.Event;
|
|
}
|
|
|
|
if (label.startsWith("class:") || label.startsWith("data-")) {
|
|
return vscode.CompletionItemKind.Property;
|
|
}
|
|
|
|
if (label === "function" || label === "functions") {
|
|
return vscode.CompletionItemKind.Function;
|
|
}
|
|
|
|
return vscode.CompletionItemKind.Keyword;
|
|
}
|
|
|
|
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(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;
|
|
}
|
|
|
|
function getCurrentOpeningTag(document, position) {
|
|
const textBeforeCursor = document.getText(
|
|
new vscode.Range(new vscode.Position(position.line, 0), position),
|
|
);
|
|
|
|
const lastOpen = textBeforeCursor.lastIndexOf("<");
|
|
|
|
const lastClose = textBeforeCursor.lastIndexOf(">");
|
|
|
|
if (lastOpen > lastClose) {
|
|
return textBeforeCursor.slice(lastOpen);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function extractRouteParams(document) {
|
|
const fileName = document.fileName.replace(/\\/g, "/");
|
|
|
|
const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)];
|
|
|
|
return matches.map((match) => match[1]);
|
|
}
|
|
|
|
function extractStates(document) {
|
|
const source = document.getText();
|
|
|
|
const matches = [...source.matchAll(/^\s*state\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=\r\n]+)?\s*=/gm)];
|
|
|
|
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*:\s*[^=\r\n]+)?(?:\s*=|\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(/=.*$/, "")
|
|
.replace(/\??\s*:\s*[\s\S]+$/, "")
|
|
.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;
|
|
}
|
|
|
|
/**
|
|
* Whether an offset sits inside a `view { }` block.
|
|
*
|
|
* The language server owns completion there and returns a merged list, so this
|
|
* provider stands down to avoid VS Code concatenating two independent lists.
|
|
* Quotes are only tracked inside a tag: `<p>it's</p>` would otherwise open a
|
|
* string that never closes.
|
|
*/
|
|
function isInsideViewBlock(text, offset) {
|
|
const pattern = /\bview\s*\{/g;
|
|
let match;
|
|
while ((match = pattern.exec(text))) {
|
|
const start = match.index + match[0].length;
|
|
let depth = 1;
|
|
let inTag = false;
|
|
let quote = null;
|
|
let index = start;
|
|
for (; index < text.length && depth > 0; index += 1) {
|
|
const char = text[index];
|
|
if (quote) {
|
|
if (char === quote) quote = null;
|
|
continue;
|
|
}
|
|
if (inTag && (char === '"' || char === "'")) quote = char;
|
|
else if (char === "<") inTag = true;
|
|
else if (char === ">") inTag = false;
|
|
else if (char === "{") depth += 1;
|
|
else if (char === "}") depth -= 1;
|
|
}
|
|
if (offset >= start && offset <= index) return true;
|
|
pattern.lastIndex = index;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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) {
|
|
if (isInsideViewBlock(document.getText(), document.offsetAt(position))) return [];
|
|
|
|
const items = [];
|
|
|
|
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
|
|
|
const openingTag = getCurrentOpeningTag(document, position);
|
|
|
|
if (openingTag !== null) {
|
|
for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) {
|
|
items.push(createCompletion(label, detail, snippet));
|
|
}
|
|
} else if (isAfterWatchKeyword(document, position)) {
|
|
addStateCompletions(items, document, position);
|
|
} else {
|
|
addBlockCompletions(items, document, position);
|
|
}
|
|
|
|
if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) {
|
|
for (const [label, detail] of CONTEXT_COMPLETIONS) {
|
|
items.push(createCompletion(label, detail, label));
|
|
}
|
|
}
|
|
|
|
for (const param of extractRouteParams(document)) {
|
|
items.push(createVariableCompletion(param, `Route parameter from [${param}].wrn`));
|
|
|
|
items.push(
|
|
createVariableCompletion(`ctx.params.${param}`, `Route parameter from [${param}].wrn`),
|
|
);
|
|
}
|
|
|
|
if (!isAfterWatchKeyword(document, position)) {
|
|
addStateCompletions(items, document, position);
|
|
}
|
|
|
|
for (const prop of extractProps(document)) {
|
|
items.push(createVariableCompletion(prop, "WRN component or layout prop"));
|
|
}
|
|
|
|
addFunctionCompletions(items, document);
|
|
|
|
if (isInsideWatch(document, position)) {
|
|
for (const [label, detail] of WATCH_VALUE_COMPLETIONS) {
|
|
items.push(createVariableCompletion(label, detail));
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function registerCompletionProvider(context) {
|
|
const provider = vscode.languages.registerCompletionItemProvider(
|
|
{
|
|
language: "wrn",
|
|
},
|
|
{
|
|
provideCompletionItems,
|
|
},
|
|
"@",
|
|
":",
|
|
".",
|
|
"<",
|
|
" ",
|
|
"(",
|
|
);
|
|
|
|
context.subscriptions.push(provider);
|
|
}
|
|
|
|
module.exports = {
|
|
extractFunctions,
|
|
extractProps,
|
|
extractRouteParams,
|
|
extractStates,
|
|
isInsideViewBlock,
|
|
provideCompletionItems,
|
|
registerCompletionProvider,
|
|
};
|