Updated the editor

This commit is contained in:
2026-07-14 12:25:26 +05:30
parent d15017aee3
commit fef4218a07
5 changed files with 252 additions and 129 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
"version": "0.2.2",
"version": "0.2.3",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+12 -8
View File
@@ -2,7 +2,7 @@
"use strict";
const vscode = require("vscode");
const { formatWrn } = require("./formatter.js");
const { formatWrn } = require("./formatter");
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
// defensively so the rest of the extension (highlighting, snippets, completion)
@@ -115,13 +115,17 @@ function activate(context) {
context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider("wrn", {
provideDocumentFormattingEdits(document, options) {
if (!vscode.workspace.getConfiguration("wrnexus").get("format.enable", true)) return [];
const formatted = formatWrn(document.getText(), options);
if (formatted === document.getText()) return [];
const end = document.positionAt(document.getText().length);
return [
vscode.TextEdit.replace(new vscode.Range(new vscode.Position(0, 0), end), formatted),
];
const source = document.getText();
const formatted = formatWrn(source, {
tabSize: options.tabSize,
insertSpaces: options.insertSpaces,
printWidth: 100,
});
const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length));
return [vscode.TextEdit.replace(range, formatted)];
},
}),
vscode.languages.registerCompletionItemProvider(
+234 -115
View File
@@ -17,152 +17,271 @@ const VOID_ELEMENTS = new Set([
"wbr",
]);
/** Count structural curly braces while ignoring quoted strings and line comments. */
function curlyDelta(line, state) {
let delta = 0;
function findOpeningTagEnd(value) {
let quote = null;
let escaped = false;
for (let index = 0; index < line.length; index++) {
const char = line[index];
const next = line[index + 1];
for (let index = 0; index < value.length; index += 1) {
const char = value[index];
if (state.blockComment) {
if (char === "*" && next === "/") {
state.blockComment = false;
index++;
if (quote !== null) {
if (char === quote && value[index - 1] !== "\\") {
quote = null;
}
continue;
}
if (state.template) {
if (!escaped && char === "`") state.template = false;
escaped = !escaped && char === "\\";
if (char !== "\\") escaped = false;
continue;
}
if (quote) {
if (!escaped && char === quote) quote = null;
escaped = !escaped && char === "\\";
if (char !== "\\") escaped = false;
continue;
}
if (char === "/" && next === "*") {
state.blockComment = true;
index++;
} else if (char === "/" && next === "/") {
break;
} else if (char === "`") {
state.template = true;
} else if (char === '"' || char === "'") {
if (char === '"' || char === "'") {
quote = char;
} else if (char === "{") {
delta++;
} else if (char === "}") {
delta--;
}
}
return delta;
continue;
}
/** Return the net nesting introduced by HTML-like tags on this line. */
function htmlDelta(line) {
let delta = 0;
const tags = line.matchAll(/<\s*(\/?)\s*([A-Za-z][\w:-]*)\b[^>]*>/g);
for (const match of tags) {
const full = match[0];
const closing = match[1] === "/";
const name = match[2].toLowerCase();
if (closing) delta--;
else if (!VOID_ELEMENTS.has(name) && !/\/\s*>$/.test(full)) delta++;
if (char === ">") {
return index;
}
return delta;
}
function leadingClosers(line) {
let count = /^\s*}/.test(line) ? 1 : 0;
const tags = line.match(/^\s*((?:<\/\s*[A-Za-z][\w:-]*\s*>\s*)+)/);
if (tags) count += [...tags[1].matchAll(/<\//g)].length;
return count;
return -1;
}
function formatOpeningTag(source, unit, baseDepth) {
if (
!source.startsWith("<") ||
source.startsWith("</") ||
source.startsWith("<!--") ||
source.length <= 100
) {
return source;
function parseAttributes(value) {
const attributes = [];
const pattern =
/[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
let match;
while ((match = pattern.exec(value)) !== null) {
attributes.push(match[0].trim());
}
const match = /^<([A-Za-z][\w:-]*)([\s\S]*?)(\/?)>$/.exec(source);
return attributes;
}
function parseOpeningTag(value) {
const endIndex = findOpeningTagEnd(value);
if (endIndex === -1) {
return null;
}
const openingPart = value.slice(0, endIndex + 1);
const remainder = value.slice(endIndex + 1).trim();
const match = /^<([A-Za-z][\w:-]*)([\s\S]*?)(\/?)>$/.exec(openingPart);
if (!match) {
return source;
return null;
}
const tag = match[1];
const rawAttributes = match[2].trim();
const tagName = match[1];
const attributes = parseAttributes(match[2].trim());
const selfClosing = match[3] === "/";
const inlineClosing = remainder === `</${tagName}>`;
if (!rawAttributes) {
return source;
return {
tagName,
attributes,
selfClosing,
inlineClosing,
remainder,
};
}
const attributes = [];
const pattern = /(?:[^\s"'=<>`]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'))?/g;
function formatOpeningTag(value, unit, depth, printWidth = 100) {
const parsed = parseOpeningTag(value);
for (const attribute of rawAttributes.matchAll(pattern)) {
attributes.push(attribute[0]);
if (!parsed) {
return {
lines: [`${unit.repeat(depth)}${value.trim()}`],
opensElement: false,
};
}
if (attributes.length < 2) {
return source;
const baseIndent = unit.repeat(depth);
const attributeIndent = unit.repeat(depth + 1);
const normalizedSingleLine = value.replace(/\s+/g, " ").trim();
const shouldBreak =
parsed.attributes.length > 1 ||
normalizedSingleLine.length > printWidth ||
value.includes("\n");
const opensElement =
!parsed.selfClosing &&
!parsed.inlineClosing &&
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
if (!shouldBreak) {
return {
lines: [`${baseIndent}${normalizedSingleLine}`],
opensElement,
};
}
const base = unit.repeat(baseDepth);
const child = unit.repeat(baseDepth + 1);
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.map(
(attribute) => `${attributeIndent}${attribute}`,
),
];
return [
`${base}<${tag}`,
...attributes.map((attribute) => `${child}${attribute}`),
`${base}${selfClosing ? "/>" : ">"}`,
].join("\n");
if (parsed.inlineClosing) {
lines.push(`${baseIndent}></${parsed.tagName}>`);
} else if (parsed.selfClosing) {
lines.push(`${baseIndent}/>`);
} else {
lines.push(`${baseIndent}>`);
if (parsed.remainder) {
lines.push(`${baseIndent}${parsed.remainder}`);
}
}
/**
* Format WRN source conservatively: normalize structural indentation and
* trailing whitespace without rewriting expressions, HTML, CSS, or JS.
*/
function formatWrn(text, options = {}) {
const tabSize = Math.max(1, Number(options.tabSize) || 2);
const unit = options.insertSpaces === false ? "\t" : " ".repeat(tabSize);
const hadFinalNewline = /\r?\n$/.test(text);
const lines = text.replace(/\r\n/g, "\n").split("\n");
if (hadFinalNewline) lines.pop();
let depth = 0;
const state = { blockComment: false, template: false };
const formatted = lines.map((original) => {
const line = original.trimEnd();
if (!line.trim()) return "";
// Preserve multiline template-string content because its whitespace can be data.
const wasTemplate = state.template;
const trimmed = line.trimStart();
const indent = Math.max(0, depth - leadingClosers(trimmed));
const formattedLine = formatOpeningTag(trimmed, unit, indent);
const output = wasTemplate
? line
: formattedLine.startsWith(unit.repeat(indent))
? formattedLine
: unit.repeat(indent) + formattedLine;
depth = Math.max(0, depth + curlyDelta(trimmed, state) + htmlDelta(trimmed));
return output;
});
return formatted.join("\n") + (hadFinalNewline ? "\n" : "");
return {
lines,
opensElement,
};
}
module.exports = { formatWrn };
function isMultilineOpeningTagStart(value) {
if (!value.startsWith("<")) {
return false;
}
if (
value.startsWith("</") ||
value.startsWith("<!--") ||
value.startsWith("<!") ||
value.startsWith("<?")
) {
return false;
}
return findOpeningTagEnd(value) === -1;
}
function isClosingTag(value) {
return /^<\/[A-Za-z][\w:-]*\s*>/.test(value);
}
function isInlineElement(value) {
return /^<([A-Za-z][\w:-]*)\b[^>]*>[\s\S]*<\/\1\s*>$/.test(value);
}
function isWrnBlockClosing(value) {
return value === "}" || value.startsWith("} ");
}
function isWrnBlockOpening(value) {
if (!value.endsWith("{")) {
return false;
}
return !value.startsWith("{");
}
function formatWrn(source, options = {}) {
const unit =
options.insertSpaces === false
? "\t"
: " ".repeat(options.tabSize || 4);
const printWidth = options.printWidth || 100;
const inputLines = source.replace(/\r\n/g, "\n").split("\n");
const output = [];
let wrnDepth = 0;
let htmlDepth = 0;
let index = 0;
let previousWasBlank = false;
while (index < inputLines.length) {
const originalLine = inputLines[index];
const trimmed = originalLine.trim();
if (trimmed === "") {
if (!previousWasBlank && output.length > 0) {
output.push("");
}
previousWasBlank = true;
index += 1;
continue;
}
previousWasBlank = false;
let value = trimmed;
if (isMultilineOpeningTagStart(value)) {
const collected = [value];
let cursor = index + 1;
while (cursor < inputLines.length) {
const nextPart = inputLines[cursor].trim();
collected.push(nextPart);
const joined = collected.join(" ");
if (findOpeningTagEnd(joined) !== -1) {
break;
}
cursor += 1;
}
value = collected.join(" ");
index = cursor;
}
if (isWrnBlockClosing(value)) {
wrnDepth = Math.max(0, wrnDepth - 1);
}
if (isClosingTag(value)) {
htmlDepth = Math.max(0, htmlDepth - 1);
}
const depth = wrnDepth + htmlDepth;
if (
value.startsWith("<") &&
!value.startsWith("</") &&
!value.startsWith("<!--") &&
!isInlineElement(value)
) {
const formattedTag = formatOpeningTag(
value,
unit,
depth,
printWidth,
);
output.push(...formattedTag.lines);
if (formattedTag.opensElement) {
htmlDepth += 1;
}
} else {
output.push(`${unit.repeat(depth)}${value}`);
}
if (isWrnBlockOpening(value)) {
wrnDepth += 1;
}
index += 1;
}
while (output.length > 0 && output[output.length - 1] === "") {
output.pop();
}
return `${output.join("\n")}\n`;
}
module.exports = {
formatOpeningTag,
formatWrn,
};
Binary file not shown.
Binary file not shown.