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