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", "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.2", "version": "0.2.3",
"publisher": "wrnexus", "publisher": "wrnexus",
"private": true, "private": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
+12 -8
View File
@@ -2,7 +2,7 @@
"use strict"; "use strict";
const vscode = require("vscode"); 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 // 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)
@@ -115,13 +115,17 @@ function activate(context) {
context.subscriptions.push( context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider("wrn", { vscode.languages.registerDocumentFormattingEditProvider("wrn", {
provideDocumentFormattingEdits(document, options) { provideDocumentFormattingEdits(document, options) {
if (!vscode.workspace.getConfiguration("wrnexus").get("format.enable", true)) return []; const source = document.getText();
const formatted = formatWrn(document.getText(), options);
if (formatted === document.getText()) return []; const formatted = formatWrn(source, {
const end = document.positionAt(document.getText().length); tabSize: options.tabSize,
return [ insertSpaces: options.insertSpaces,
vscode.TextEdit.replace(new vscode.Range(new vscode.Position(0, 0), end), formatted), printWidth: 100,
]; });
const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length));
return [vscode.TextEdit.replace(range, formatted)];
}, },
}), }),
vscode.languages.registerCompletionItemProvider( vscode.languages.registerCompletionItemProvider(
+239 -120
View File
@@ -17,152 +17,271 @@ const VOID_ELEMENTS = new Set([
"wbr", "wbr",
]); ]);
/** Count structural curly braces while ignoring quoted strings and line comments. */ function findOpeningTagEnd(value) {
function curlyDelta(line, state) {
let delta = 0;
let quote = null; let quote = null;
let escaped = false;
for (let index = 0; index < line.length; index++) { for (let index = 0; index < value.length; index += 1) {
const char = line[index]; const char = value[index];
const next = line[index + 1];
if (state.blockComment) { if (quote !== null) {
if (char === "*" && next === "/") { if (char === quote && value[index - 1] !== "\\") {
state.blockComment = false; quote = null;
index++;
} }
continue; continue;
} }
if (state.template) {
if (!escaped && char === "`") state.template = false; if (char === '"' || char === "'") {
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 === "'") {
quote = char; quote = char;
} else if (char === "{") { continue;
delta++; }
} else if (char === "}") {
delta--; if (char === ">") {
return index;
} }
} }
return delta;
return -1;
} }
/** Return the net nesting introduced by HTML-like tags on this line. */ function parseAttributes(value) {
function htmlDelta(line) { const attributes = [];
let delta = 0; const pattern =
const tags = line.matchAll(/<\s*(\/?)\s*([A-Za-z][\w:-]*)\b[^>]*>/g); /[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/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++;
}
return delta;
}
function leadingClosers(line) { let match;
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;
}
function formatOpeningTag(source, unit, baseDepth) { while ((match = pattern.exec(value)) !== null) {
if ( attributes.push(match[0].trim());
!source.startsWith("<") ||
source.startsWith("</") ||
source.startsWith("<!--") ||
source.length <= 100
) {
return source;
} }
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) { if (!match) {
return source; return null;
} }
const tag = match[1]; const tagName = match[1];
const rawAttributes = match[2].trim(); const attributes = parseAttributes(match[2].trim());
const selfClosing = match[3] === "/"; const selfClosing = match[3] === "/";
const inlineClosing = remainder === `</${tagName}>`;
if (!rawAttributes) { return {
return source; tagName,
} attributes,
selfClosing,
const attributes = []; inlineClosing,
const pattern = /(?:[^\s"'=<>`]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'))?/g; remainder,
};
for (const attribute of rawAttributes.matchAll(pattern)) {
attributes.push(attribute[0]);
}
if (attributes.length < 2) {
return source;
}
const base = unit.repeat(baseDepth);
const child = unit.repeat(baseDepth + 1);
return [
`${base}<${tag}`,
...attributes.map((attribute) => `${child}${attribute}`),
`${base}${selfClosing ? "/>" : ">"}`,
].join("\n");
} }
/** function formatOpeningTag(value, unit, depth, printWidth = 100) {
* Format WRN source conservatively: normalize structural indentation and const parsed = parseOpeningTag(value);
* 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; if (!parsed) {
const state = { blockComment: false, template: false }; return {
const formatted = lines.map((original) => { lines: [`${unit.repeat(depth)}${value.trim()}`],
const line = original.trimEnd(); opensElement: false,
if (!line.trim()) return ""; };
}
// Preserve multiline template-string content because its whitespace can be data. const baseIndent = unit.repeat(depth);
const wasTemplate = state.template; const attributeIndent = unit.repeat(depth + 1);
const trimmed = line.trimStart(); const normalizedSingleLine = value.replace(/\s+/g, " ").trim();
const indent = Math.max(0, depth - leadingClosers(trimmed));
const formattedLine = formatOpeningTag(trimmed, unit, indent);
const output = wasTemplate const shouldBreak =
? line parsed.attributes.length > 1 ||
: formattedLine.startsWith(unit.repeat(indent)) normalizedSingleLine.length > printWidth ||
? formattedLine value.includes("\n");
: unit.repeat(indent) + formattedLine;
depth = Math.max(0, depth + curlyDelta(trimmed, state) + htmlDelta(trimmed));
return output;
});
return formatted.join("\n") + (hadFinalNewline ? "\n" : ""); const opensElement =
!parsed.selfClosing &&
!parsed.inlineClosing &&
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
if (!shouldBreak) {
return {
lines: [`${baseIndent}${normalizedSingleLine}`],
opensElement,
};
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.map(
(attribute) => `${attributeIndent}${attribute}`,
),
];
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}`);
}
}
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.