first commit
This commit is contained in:
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run WrNexus Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}/../../examples/basic-app"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/src/**/*.js"],
|
||||
"preLaunchTask": "build:compiler"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build:compiler",
|
||||
"detail": "Bundle @wrnexus/compiler to src/compiler.cjs (powers diagnostics).",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"build",
|
||||
"../../packages/compiler/src/index.ts",
|
||||
"--target=node",
|
||||
"--format=cjs",
|
||||
"--outfile=src/compiler.cjs"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.vscode/**
|
||||
test/**
|
||||
*.vsix
|
||||
**/*.ts
|
||||
**/*.map
|
||||
**/tsconfig.json
|
||||
package-lock.json
|
||||
bun.lock
|
||||
../../**
|
||||
node_modules/**
|
||||
@@ -0,0 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial Marketplace package for WrNexus `.wrn` language support.
|
||||
- Added syntax highlighting, grammar injections, snippets, completions, and compiler-backed diagnostics.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 WrNexus contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
# WrNexus for VS Code
|
||||
|
||||
Language support for [WrNexus](https://github.com/) `.wrn` files — the SSR-first,
|
||||
Bun-powered full-stack framework.
|
||||
|
||||
## Features
|
||||
|
||||
- **Syntax highlighting** ("color-full code"). Keywords (`page`, `component`,
|
||||
`state`, `view`, `api`, `realtime`, …) plus **embedded highlighting** for the
|
||||
languages inside each block:
|
||||
- `view { … }` → HTML, with `{expr}` interpolation and `{t:key}` translations
|
||||
and `@event="…"` bindings picked out.
|
||||
- `style { … }` → CSS.
|
||||
- `functions { … }`, `api … { … }`, `ssr`/`client`, `realtime on(…) { … }` → TypeScript.
|
||||
- **WrNexus attributes in distinct colors.** A grammar injection recolors the
|
||||
framework's own attributes so they pop even inside plain HTML: `@event`
|
||||
bindings and `{t:…}` in one accent, runtime directives (`data-component`,
|
||||
`data-for`, `data-show`, `data-text`, `data-scope`, `data-slot`, `data-on-*`,
|
||||
`data-wire-*`) in another. Default colors ship with the extension; change them
|
||||
under `editor.tokenColorCustomizations` (scopes
|
||||
`entity.other.attribute-name.wrn.directive` and `…wire.event`).
|
||||
- **Diagnostics.** Parse errors from the real `@wrnexus/compiler` are shown inline
|
||||
as you type, anchored to the exact offset.
|
||||
- **Snippets.** `page`, `component`, `view`, `state`, `props`, `seo`, `api`,
|
||||
`ssr`, `client`, `realtime`, `functions`, `style`, plus view helpers `mount`,
|
||||
`for`, `show`, `t`.
|
||||
- **Completions.** Block keywords at file scope, `data-*` runtime attributes and
|
||||
`@event` bindings inside a `view`, and HTTP methods after `api`.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ---------------------------- | ------- | ----------------------------------- |
|
||||
| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. |
|
||||
|
||||
## Building the diagnostics engine
|
||||
|
||||
Diagnostics use a bundled copy of the compiler. Regenerate it after changing
|
||||
`@wrnexus/compiler`:
|
||||
|
||||
```bash
|
||||
bun run build:compiler # writes src/compiler.cjs
|
||||
```
|
||||
|
||||
The extension still highlights, snippets and completes without the bundle; only
|
||||
inline error reporting requires it.
|
||||
|
||||
## Development
|
||||
|
||||
Open this folder in VS Code and press <kbd>F5</kbd> to launch an Extension
|
||||
Development Host, then open any `.wrn` file.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Support
|
||||
|
||||
For bugs, feature requests, and publishing issues, open an issue in the WrNexus repository:
|
||||
|
||||
https://github.com/wrnexus/wrnexus/issues
|
||||
|
||||
Please include your VS Code version, extension version, operating system, and a small `.wrn` sample when reporting language support problems.
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect width="32" height="32" rx="6" fill="#0b1021"/>
|
||||
<path d="M6 11l3 10 3-8 3 8 3-10" fill="none" stroke="#5eead4" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="24" cy="12" r="2.4" fill="#f472b6"/>
|
||||
<path d="M24 14.5v6" stroke="#f472b6" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//"
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["<", ">"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
||||
{ "open": "'", "close": "'", "notIn": ["string"] },
|
||||
{ "open": "<", "close": ">" }
|
||||
],
|
||||
"autoCloseBefore": ";:.,=}])> \n\t",
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"],
|
||||
["<", ">"]
|
||||
],
|
||||
"colorizedBracketPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*//\\s*#region\\b",
|
||||
"end": "^\\s*//\\s*#endregion\\b"
|
||||
}
|
||||
},
|
||||
"onEnterRules": [
|
||||
{
|
||||
"beforeText": "^\\s*//.*$",
|
||||
"action": { "indent": "none", "appendText": "// " }
|
||||
}
|
||||
],
|
||||
"wordPattern": "[A-Za-z_][A-Za-z0-9_-]*"
|
||||
}
|
||||
Generated
+3811
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"name": "wrnexus",
|
||||
"displayName": "WrNexus (.wrn)",
|
||||
"description": "Syntax highlighting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
||||
"version": "0.1.0",
|
||||
"publisher": "wrnexus",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wrnexus/wrnexus.git",
|
||||
"directory": "editors/vscode"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/wrnexus/wrnexus/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wrnexus/wrnexus#readme",
|
||||
"galleryBanner": {
|
||||
"color": "#0b1021",
|
||||
"theme": "dark"
|
||||
},
|
||||
"pricing": "Free",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Snippets",
|
||||
"Linters"
|
||||
],
|
||||
"keywords": [
|
||||
"wire",
|
||||
"wrnexus",
|
||||
"wrn",
|
||||
"full-stack",
|
||||
"ssr",
|
||||
"bun"
|
||||
],
|
||||
"main": "./src/extension.js",
|
||||
"activationEvents": [
|
||||
"onLanguage:wrn"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "wrn",
|
||||
"aliases": [
|
||||
"WrNexus",
|
||||
"wrn"
|
||||
],
|
||||
"extensions": [
|
||||
".wrn"
|
||||
],
|
||||
"configuration": "./language-configuration.json",
|
||||
"icon": {
|
||||
"light": "./icons/wrn.svg",
|
||||
"dark": "./icons/wrn.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "wrn",
|
||||
"scopeName": "source.wrn",
|
||||
"path": "./syntaxes/wrn.tmLanguage.json",
|
||||
"embeddedLanguages": {
|
||||
"meta.embedded.block.html": "html",
|
||||
"meta.embedded.block.css": "css",
|
||||
"meta.embedded.block.ts": "typescript",
|
||||
"meta.embedded.expr.wrn": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scopeName": "wrn.attributes.injection",
|
||||
"path": "./syntaxes/wrn.injection.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"source.wrn"
|
||||
]
|
||||
}
|
||||
],
|
||||
"snippets": [
|
||||
{
|
||||
"language": "wrn",
|
||||
"path": "./snippets/wrn.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "WrNexus",
|
||||
"properties": {
|
||||
"wrnexus.diagnostics.enable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show parse errors for .wrn files as you type."
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurationDefaults": {
|
||||
"editor.tokenColorCustomizations": {
|
||||
"textMateRules": [
|
||||
{
|
||||
"scope": "entity.other.attribute-name.wrn.directive",
|
||||
"settings": {
|
||||
"foreground": "#5eead4",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "entity.other.attribute-name.wrn.event",
|
||||
"settings": {
|
||||
"foreground": "#f472b6",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "punctuation.definition.keyword.wrn",
|
||||
"settings": {
|
||||
"foreground": "#f472b6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "keyword.control.i18n.wrn",
|
||||
"settings": {
|
||||
"foreground": "#f472b6",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "string.other.i18n-key.wrn",
|
||||
"settings": {
|
||||
"foreground": "#5eead4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "constant.language.http-method.wrn",
|
||||
"settings": {
|
||||
"foreground": "#f472b6",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs",
|
||||
"validate": "node test/validate.mjs",
|
||||
"vscode:prepublish": "bun run build:compiler",
|
||||
"package": "vsce package --no-dependencies",
|
||||
"publish": "vsce publish --no-dependencies",
|
||||
"publish:azure": "vsce publish --no-dependencies --azure-credential"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "^3.9.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"Page": {
|
||||
"prefix": "page",
|
||||
"body": [
|
||||
"page ${1:Home} {",
|
||||
" layout = \"${2:public}\"",
|
||||
"",
|
||||
" seo {",
|
||||
" title = \"${3:Home}\"",
|
||||
" description = \"${4:A WrNexus page.}\"",
|
||||
" }",
|
||||
"",
|
||||
" view {",
|
||||
" <h1>$5</h1>",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"description": "A WrNexus page (a route) with layout, seo and view."
|
||||
},
|
||||
"Component": {
|
||||
"prefix": "component",
|
||||
"body": [
|
||||
"component ${1:Counter} {",
|
||||
" props {",
|
||||
" ${2:start} = ${3:0}",
|
||||
" }",
|
||||
"",
|
||||
" state ${4:count} = ${2:start}",
|
||||
"",
|
||||
" view {",
|
||||
" <button @click=\"${4:count}++\">{${4:count}}</button>",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"description": "A reusable, prop-driven component."
|
||||
},
|
||||
"View block": {
|
||||
"prefix": "view",
|
||||
"body": ["view {", " $0", "}"],
|
||||
"description": "The server-rendered HTML view block."
|
||||
},
|
||||
"State declaration": {
|
||||
"prefix": "state",
|
||||
"body": ["state ${1:count} = ${2:0}"],
|
||||
"description": "Reactive state seeded into the scope."
|
||||
},
|
||||
"Props block": {
|
||||
"prefix": "props",
|
||||
"body": ["props {", " ${1:label} = ${2:\"Label\"}", "}"],
|
||||
"description": "Component props with typed defaults."
|
||||
},
|
||||
"SEO block": {
|
||||
"prefix": "seo",
|
||||
"body": ["seo {", " title = \"${1:Title}\"", " description = \"${2:Description}\"", "}"],
|
||||
"description": "Per-page SEO metadata."
|
||||
},
|
||||
"API route": {
|
||||
"prefix": "api",
|
||||
"body": [
|
||||
"api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {",
|
||||
" $0",
|
||||
" return Response.json({ ok: true });",
|
||||
"}"
|
||||
],
|
||||
"description": "A colocated API route handler."
|
||||
},
|
||||
"Functions block": {
|
||||
"prefix": "functions",
|
||||
"body": ["functions {", " $0", "}"],
|
||||
"description": "Shared helper functions for this file."
|
||||
},
|
||||
"Style block": {
|
||||
"prefix": "style",
|
||||
"body": ["style {", " .${1:box} {", " $0", " }", "}"],
|
||||
"description": "Scoped CSS inlined with the page."
|
||||
},
|
||||
"SSR data block": {
|
||||
"prefix": "ssr",
|
||||
"body": ["ssr {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
|
||||
"description": "Server-side data fetching block."
|
||||
},
|
||||
"Client data block": {
|
||||
"prefix": "client",
|
||||
"body": ["client {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
|
||||
"description": "Client-side data fetching block."
|
||||
},
|
||||
"Realtime block": {
|
||||
"prefix": "realtime",
|
||||
"body": ["realtime ${1:chat} {", " on ${2:message}(${3:data}) {", " $0", " }", "}"],
|
||||
"description": "A realtime channel with event handlers."
|
||||
},
|
||||
"Component mount": {
|
||||
"prefix": "mount",
|
||||
"body": ["<div data-component=\"${1:counter}\" ${2:start=\"0\"}></div>"],
|
||||
"description": "Mount a component in a view."
|
||||
},
|
||||
"List rendering (data-for)": {
|
||||
"prefix": "for",
|
||||
"body": ["<${1:li} data-for=\"${2:item} in ${3:items}\" data-text=\"${2:item}\"></${1:li}>"],
|
||||
"description": "Repeat an element for each item."
|
||||
},
|
||||
"Conditional (data-show)": {
|
||||
"prefix": "show",
|
||||
"body": ["<div data-show=\"${1:condition}\">$0</div>"],
|
||||
"description": "Toggle visibility reactively."
|
||||
},
|
||||
"Translation": {
|
||||
"prefix": "t",
|
||||
"body": ["{t:${1:key}}"],
|
||||
"description": "Localized text via the i18n dictionary."
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
// @ts-check
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
|
||||
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
||||
// defensively so the rest of the extension (highlighting, snippets, completion)
|
||||
// still works even if the bundle is missing.
|
||||
let compiler = null;
|
||||
try {
|
||||
compiler = require("./compiler.cjs");
|
||||
} catch (err) {
|
||||
console.warn("[wrnexus] compiler bundle not found; diagnostics disabled.", err && err.message);
|
||||
}
|
||||
|
||||
/** Top-level block keywords offered at file scope. */
|
||||
const BLOCK_KEYWORDS = [
|
||||
["page", "page ${1:Name} {\n\t$0\n}", "A route page"],
|
||||
["component", "component ${1:Name} {\n\t$0\n}", "A reusable, prop-driven component"],
|
||||
["layout", 'layout = "${1:public}"', "Select a layout for this page"],
|
||||
["state", "state ${1:count} = ${2:0}", "Reactive state seeded into the scope"],
|
||||
["props", 'props {\n\t${1:label} = ${2:"Label"}\n}', "Component props with typed defaults"],
|
||||
["view", "view {\n\t$0\n}", "Server-rendered HTML"],
|
||||
["seo", 'seo {\n\ttitle = "$1"\n\tdescription = "$2"\n}', "Per-page SEO metadata"],
|
||||
["api", "api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {\n\t$0\n}", "Colocated API route"],
|
||||
["ssr", "ssr {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}", "Server-side data block"],
|
||||
[
|
||||
"client",
|
||||
"client {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}",
|
||||
"Client-side data block",
|
||||
],
|
||||
[
|
||||
"realtime",
|
||||
"realtime ${1:chat} {\n\ton ${2:message}(${3:data}) {\n\t\t$0\n\t}\n}",
|
||||
"Realtime channel",
|
||||
],
|
||||
["functions", "functions {\n\t$0\n}", "Shared helper functions"],
|
||||
["style", "style {\n\t$0\n}", "Scoped CSS"],
|
||||
];
|
||||
|
||||
/** `data-*` attributes understood by the reactive runtime. */
|
||||
const DATA_ATTRS = [
|
||||
["data-component", 'data-component="$1"', "Mount a component by name"],
|
||||
["data-for", 'data-for="${1:item} in ${2:items}"', "Repeat this element per item"],
|
||||
["data-show", 'data-show="${1:condition}"', "Toggle visibility reactively"],
|
||||
["data-text", 'data-text="${1:expr}"', "Bind text content to an expression"],
|
||||
["data-scope", 'data-scope="${1:key}: ${2:value}"', "Declare a local reactive scope"],
|
||||
["data-slot", 'data-slot="${1:name}"', "Fill a named <slot> on a component"],
|
||||
];
|
||||
|
||||
/** Client event bindings. */
|
||||
const EVENTS = [
|
||||
"click",
|
||||
"input",
|
||||
"change",
|
||||
"submit",
|
||||
"keydown",
|
||||
"keyup",
|
||||
"focus",
|
||||
"blur",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
|
||||
context.subscriptions.push(diagnostics);
|
||||
|
||||
const timers = new Map();
|
||||
const runDiagnostics = (doc) => {
|
||||
if (doc.languageId !== "wrn") return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus").get("diagnostics.enable", true)) {
|
||||
diagnostics.delete(doc.uri);
|
||||
return;
|
||||
}
|
||||
if (!compiler || typeof compiler.compileWireFile !== "function") return;
|
||||
|
||||
const text = doc.getText();
|
||||
/** @type {vscode.Diagnostic[]} */
|
||||
const found = [];
|
||||
try {
|
||||
compiler.compileWireFile(text);
|
||||
} catch (err) {
|
||||
found.push(toDiagnostic(doc, err));
|
||||
}
|
||||
diagnostics.set(doc.uri, found);
|
||||
};
|
||||
|
||||
const schedule = (doc) => {
|
||||
const key = doc.uri.toString();
|
||||
clearTimeout(timers.get(key));
|
||||
timers.set(
|
||||
key,
|
||||
setTimeout(() => {
|
||||
timers.delete(key);
|
||||
runDiagnostics(doc);
|
||||
}, 250),
|
||||
);
|
||||
};
|
||||
|
||||
// Lint on open, edit and save; clear on close.
|
||||
vscode.workspace.textDocuments.forEach(runDiagnostics);
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidOpenTextDocument(runDiagnostics),
|
||||
vscode.workspace.onDidChangeTextDocument((e) => schedule(e.document)),
|
||||
vscode.workspace.onDidSaveTextDocument(runDiagnostics),
|
||||
vscode.workspace.onDidCloseTextDocument((doc) => diagnostics.delete(doc.uri)),
|
||||
);
|
||||
|
||||
// Completions.
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
"wrn",
|
||||
{ provideCompletionItems: provideCompletions },
|
||||
"-",
|
||||
"@",
|
||||
"{",
|
||||
":",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a thrown ParseError to a VS Code diagnostic. The compiler encodes the
|
||||
* failure position as `... at offset <N>` in the message; we resolve it to a
|
||||
* range. Errors without an offset (e.g. "Unexpected end of input") anchor to the
|
||||
* end of the document.
|
||||
* @param {vscode.TextDocument} doc
|
||||
* @param {unknown} err
|
||||
* @returns {vscode.Diagnostic}
|
||||
*/
|
||||
function toDiagnostic(doc, err) {
|
||||
const message = err && err.message ? String(err.message) : "Failed to parse .wrn file";
|
||||
const match = /offset\s+(\d+)/.exec(message);
|
||||
let range;
|
||||
if (match) {
|
||||
const offset = Number(match[1]);
|
||||
const start = doc.positionAt(offset);
|
||||
const wordRange = doc.getWordRangeAtPosition(start);
|
||||
range = wordRange || new vscode.Range(start, doc.positionAt(offset + 1));
|
||||
} else {
|
||||
const last = doc.lineAt(Math.max(0, doc.lineCount - 1));
|
||||
range = new vscode.Range(last.range.start, last.range.end);
|
||||
}
|
||||
const diag = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
|
||||
diag.source = "wrn";
|
||||
return diag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {vscode.TextDocument} document
|
||||
* @param {vscode.Position} position
|
||||
* @returns {vscode.CompletionItem[]}
|
||||
*/
|
||||
function provideCompletions(document, position) {
|
||||
const line = document.lineAt(position).text;
|
||||
const before = line.slice(0, position.character);
|
||||
const full = document.getText();
|
||||
const upto = document.offsetAt(position);
|
||||
const inView = isInsideBlock(full, upto, "view");
|
||||
|
||||
// `{t:` — offer nothing structured, just let the user type the key.
|
||||
// `@` inside a view — event bindings.
|
||||
if (inView && /@[A-Za-z-]*$/.test(before)) {
|
||||
return EVENTS.map((ev) => {
|
||||
const item = new vscode.CompletionItem(ev, vscode.CompletionItemKind.Event);
|
||||
item.insertText = new vscode.SnippetString(`${ev}="$0"`);
|
||||
item.detail = "wrn event binding";
|
||||
// Replace the `@`-less part already typed.
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// `data-` inside a view — reactive attributes.
|
||||
if (inView && /(^|\s)data-[A-Za-z-]*$/.test(before)) {
|
||||
return DATA_ATTRS.map(([label, snippet, doc]) => {
|
||||
const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Property);
|
||||
item.insertText = new vscode.SnippetString(String(snippet));
|
||||
item.documentation = new vscode.MarkdownString(String(doc));
|
||||
item.detail = "wrn runtime attribute";
|
||||
const dashIdx = before.lastIndexOf("data-");
|
||||
item.range = new vscode.Range(position.line, dashIdx, position.line, position.character);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// Top-level block keywords, when not inside a view/style/functions body.
|
||||
if (!inView && !isInsideBlock(full, upto, "style") && !isInsideBlock(full, upto, "functions")) {
|
||||
return BLOCK_KEYWORDS.map(([label, snippet, doc]) => {
|
||||
const item = new vscode.CompletionItem(String(label), vscode.CompletionItemKind.Keyword);
|
||||
item.insertText = new vscode.SnippetString(String(snippet));
|
||||
item.documentation = new vscode.MarkdownString(String(doc));
|
||||
item.detail = "wrn block";
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Very small heuristic: is `offset` inside a `<name> { ... }` block? Finds the
|
||||
* nearest preceding `name {` opener and checks braces don't balance before the
|
||||
* cursor. Good enough to gate view/style/functions-aware completions.
|
||||
* @param {string} text
|
||||
* @param {number} offset
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInsideBlock(text, offset, name) {
|
||||
const re = new RegExp("\\b" + name + "\\b\\s*(?:[^\\n{]*)\\{", "g");
|
||||
let opener = -1;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const bracePos = m.index + m[0].length - 1;
|
||||
if (bracePos >= offset) break;
|
||||
opener = bracePos;
|
||||
}
|
||||
if (opener === -1) return false;
|
||||
// Count braces between the opener and the cursor; still open => inside.
|
||||
let depth = 0;
|
||||
for (let i = opener; i < offset && i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (c === "{") depth++;
|
||||
else if (c === "}") depth--;
|
||||
}
|
||||
return depth > 0;
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Wire attributes",
|
||||
"scopeName": "wrn.attributes.injection",
|
||||
"injectionSelector": "L:meta.tag -comment -string",
|
||||
"patterns": [{ "include": "#wire-event" }, { "include": "#wire-directive" }],
|
||||
"repository": {
|
||||
"wire-event": {
|
||||
"match": "(@)([A-Za-z][A-Za-z0-9_-]*)",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.definition.keyword.wrn" },
|
||||
"2": { "name": "entity.other.attribute-name.wrn.event" }
|
||||
}
|
||||
},
|
||||
"wire-directive": {
|
||||
"match": "\\b(data-(?:component|scope|for|text|show|slot|on-[A-Za-z0-9_-]+|wire-[A-Za-z0-9_-]+|wrnexus-[A-Za-z0-9_-]+))\\b",
|
||||
"name": "entity.other.attribute-name.wrn.directive"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Wire",
|
||||
"scopeName": "source.wrn",
|
||||
"patterns": [{ "include": "#comments" }, { "include": "#declaration" }, { "include": "#blocks" }],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.double-slash.wrn",
|
||||
"match": "//.*$"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"declaration": {
|
||||
"match": "\\b(page|component)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)?",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.wrn keyword.control.wrn" },
|
||||
"2": { "name": "entity.name.type.wrn" }
|
||||
}
|
||||
},
|
||||
|
||||
"blocks": {
|
||||
"patterns": [
|
||||
{ "include": "#view-block" },
|
||||
{ "include": "#style-block" },
|
||||
{ "include": "#seo-block" },
|
||||
{ "include": "#props-block" },
|
||||
{ "include": "#realtime-block" },
|
||||
{ "include": "#mode-block" },
|
||||
{ "include": "#api-block" },
|
||||
{ "include": "#functions-block" },
|
||||
{ "include": "#state-decl" },
|
||||
{ "include": "#layout-decl" },
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
},
|
||||
|
||||
"layout-decl": {
|
||||
"match": "\\b(layout)\\b\\s*(=)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "keyword.operator.assignment.wrn" }
|
||||
}
|
||||
},
|
||||
|
||||
"state-decl": {
|
||||
"begin": "\\b(state)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(=)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "variable.other.wrn" },
|
||||
"3": { "name": "keyword.operator.assignment.wrn" }
|
||||
},
|
||||
"end": "$",
|
||||
"contentName": "meta.embedded.expr.wrn",
|
||||
"patterns": [{ "include": "source.ts" }]
|
||||
},
|
||||
|
||||
"props-block": {
|
||||
"begin": "\\b(props)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{
|
||||
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(=)",
|
||||
"captures": {
|
||||
"1": { "name": "variable.parameter.wrn" },
|
||||
"2": { "name": "keyword.operator.assignment.wrn" }
|
||||
}
|
||||
},
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
},
|
||||
|
||||
"seo-block": {
|
||||
"begin": "\\b(seo)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{
|
||||
"match": "\\b([A-Za-z_][A-Za-z0-9_-]*)\\s*(=)",
|
||||
"captures": {
|
||||
"1": { "name": "support.type.property-name.wrn" },
|
||||
"2": { "name": "keyword.operator.assignment.wrn" }
|
||||
}
|
||||
},
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
},
|
||||
|
||||
"style-block": {
|
||||
"begin": "\\b(style)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.css",
|
||||
"patterns": [{ "include": "#css-braces" }, { "include": "source.css" }]
|
||||
},
|
||||
|
||||
"css-braces": {
|
||||
"begin": "\\{",
|
||||
"end": "\\}",
|
||||
"contentName": "meta.embedded.block.css",
|
||||
"patterns": [{ "include": "#css-braces" }, { "include": "source.css" }]
|
||||
},
|
||||
|
||||
"functions-block": {
|
||||
"begin": "\\b(functions)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }]
|
||||
},
|
||||
|
||||
"ts-braces": {
|
||||
"begin": "\\{",
|
||||
"end": "\\}",
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }]
|
||||
},
|
||||
|
||||
"api-block": {
|
||||
"begin": "\\b(api)\\b\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "constant.language.http-method.wrn" },
|
||||
"3": { "name": "string.unquoted.route.wrn" },
|
||||
"4": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }]
|
||||
},
|
||||
|
||||
"mode-block": {
|
||||
"begin": "\\b(ssr|client)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{
|
||||
"begin": "\\b(api)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "entity.name.function.wrn" },
|
||||
"3": { "name": "constant.language.http-method.wrn" },
|
||||
"4": { "name": "string.unquoted.route.wrn" },
|
||||
"5": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }]
|
||||
},
|
||||
{ "include": "#functions-block" }
|
||||
]
|
||||
},
|
||||
|
||||
"realtime-block": {
|
||||
"begin": "\\b(realtime)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "entity.name.function.wrn" },
|
||||
"3": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{
|
||||
"begin": "\\b(on)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(\\([^)]*\\))\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "entity.name.function.event.wrn" },
|
||||
"3": { "name": "variable.parameter.wrn" },
|
||||
"4": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.ts",
|
||||
"patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"view-block": {
|
||||
"begin": "\\b(view)\\b\\s*(\\{)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.control.wrn" },
|
||||
"2": { "name": "punctuation.definition.block.begin.wrn" }
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.block.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.block.html",
|
||||
"patterns": [
|
||||
{ "include": "#interpolation" },
|
||||
{ "include": "#wire-event" },
|
||||
{ "include": "text.html.basic" }
|
||||
]
|
||||
},
|
||||
|
||||
"wire-event": {
|
||||
"match": "(@)([A-Za-z][A-Za-z0-9_-]*)(=)",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.definition.keyword.wrn" },
|
||||
"2": { "name": "entity.other.attribute-name.event.wrn" },
|
||||
"3": { "name": "keyword.operator.assignment.wrn" }
|
||||
}
|
||||
},
|
||||
|
||||
"interpolation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.interpolation.i18n.wrn",
|
||||
"match": "(\\{)(t:)([^}]*)(\\})",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.section.interpolation.begin.wrn" },
|
||||
"2": { "name": "keyword.control.i18n.wrn" },
|
||||
"3": { "name": "string.other.i18n-key.wrn" },
|
||||
"4": { "name": "punctuation.section.interpolation.end.wrn" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "meta.interpolation.wrn",
|
||||
"begin": "\\{",
|
||||
"end": "\\}",
|
||||
"beginCaptures": {
|
||||
"0": { "name": "punctuation.section.interpolation.begin.wrn" }
|
||||
},
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.section.interpolation.end.wrn" }
|
||||
},
|
||||
"contentName": "meta.embedded.expr.wrn",
|
||||
"patterns": [{ "include": "#interp-braces" }, { "include": "source.ts" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"interp-braces": {
|
||||
"begin": "\\{",
|
||||
"end": "\\}",
|
||||
"contentName": "meta.embedded.expr.wrn",
|
||||
"patterns": [{ "include": "#interp-braces" }, { "include": "source.ts" }]
|
||||
},
|
||||
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.wrn",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [{ "name": "constant.character.escape.wrn", "match": "\\\\." }]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.wrn",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [{ "name": "constant.character.escape.wrn", "match": "\\\\." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Sanity checks for the VS Code extension assets. Run with `node test/validate.mjs`.
|
||||
// Verifies the JSON contribution files parse and that the bundled compiler both
|
||||
// accepts valid .wrn and reports an offset for invalid .wrn (what diagnostics need).
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, "..");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
let failures = 0;
|
||||
const ok = (name) => console.log(` ok ${name}`);
|
||||
const bad = (name, detail) => {
|
||||
failures++;
|
||||
console.error(` FAIL ${name}${detail ? " — " + detail : ""}`);
|
||||
};
|
||||
|
||||
// 1. All JSON contribution files parse.
|
||||
for (const rel of [
|
||||
"package.json",
|
||||
"language-configuration.json",
|
||||
"syntaxes/wrn.tmLanguage.json",
|
||||
"snippets/wrn.json",
|
||||
]) {
|
||||
try {
|
||||
JSON.parse(readFileSync(join(root, rel), "utf8"));
|
||||
ok(`json parses: ${rel}`);
|
||||
} catch (e) {
|
||||
bad(`json parses: ${rel}`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Grammar wires up the right scope + embedded languages.
|
||||
const grammar = JSON.parse(readFileSync(join(root, "syntaxes/wrn.tmLanguage.json"), "utf8"));
|
||||
grammar.scopeName === "source.wrn" ? ok("grammar scopeName") : bad("grammar scopeName");
|
||||
|
||||
// 3. Bundled compiler: valid source compiles, invalid source throws with an offset.
|
||||
try {
|
||||
const compiler = require(join(root, "src/compiler.cjs"));
|
||||
const good = `page Home {\n view { <h1>Hi</h1> }\n}`;
|
||||
compiler.compileWireFile(good);
|
||||
ok("compiler accepts valid .wrn");
|
||||
|
||||
const badSrc = `page Home {\n view { <h1>Hi</h2> }\n}`;
|
||||
let threw = false;
|
||||
try {
|
||||
compiler.compileWireFile(badSrc);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
/offset\s+\d+|end of input/.test(e.message)
|
||||
? ok("compiler reports a locatable error")
|
||||
: bad("compiler reports a locatable error", e.message);
|
||||
}
|
||||
if (!threw) bad("compiler rejects invalid .wrn");
|
||||
} catch (e) {
|
||||
bad("compiler bundle loads", e.message);
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? "\nAll extension checks passed." : `\n${failures} check(s) failed.`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
Binary file not shown.
Reference in New Issue
Block a user