Updated the editor
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.
Reference in New Issue
Block a user