import { test } from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; // Minimal vscode shim so the compiled semantic tokens provider can run under // plain node. Only the APIs used by the provider call path are implemented. class Range { constructor(startLine, startCharacter, endLine, endCharacter) { this.start = { line: startLine, character: startCharacter }; this.end = { line: endLine, character: endCharacter }; } } class SemanticTokensLegend { constructor(tokenTypes) { this.tokenTypes = tokenTypes; } } class SemanticTokens { constructor(data) { this.data = data; } } class SemanticTokensBuilder { constructor(legend) { this.legend = legend; this.pushed = []; } push(range, tokenType) { this.pushed.push({ range, tokenType }); } build() { return new SemanticTokens(new Uint32Array(this.pushed.length * 5)); } } const require = createRequire(import.meta.url); const Module = require("module"); const origResolve = Module._resolveFilename; Module._resolveFilename = function (request, ...args) { if (request === "vscode") return "vscode-stub"; return origResolve.call(this, request, ...args); }; require.cache["vscode-stub"] = { id: "vscode-stub", filename: "vscode-stub", loaded: true, exports: { Range, SemanticTokens, SemanticTokensBuilder, SemanticTokensLegend, }, }; const { parseXml } = require("../out/language/xmlParser.js"); const { buildSemanticTokenRanges } = require("../out/language/semanticTokens.js"); const { Ra3SemanticTokensProvider } = require("../out/features/semanticTokens.js"); function lineStartsOf(text) { const starts = [0]; for (let i = 0; i < text.length; i++) { if (text.charCodeAt(i) === 10) starts.push(i + 1); } return starts; } function sliceAt(text, token) { const starts = lineStartsOf(text); const offset = starts[token.line] + token.startChar; return text.slice(offset, offset + token.length); } function makeDocument(text) { const lineStarts = lineStartsOf(text); return { getText: () => text, offsetAt: (pos) => lineStarts[pos.line] + pos.character, positionAt: (offset) => { let lo = 0; let hi = lineStarts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (lineStarts[mid] <= offset) lo = mid; else hi = mid - 1; } return { line: lo, character: offset - lineStarts[lo] }; }, }; } const MALFORMED = `\n { const doc = parseXml(MALFORMED); assert.ok(doc.errors.length > 0); const tokens = buildSemanticTokenRanges(doc, MALFORMED); // Element names (opening and closing tags). assert.ok(tokens.some((t) => t.tokenType === "type" && sliceAt(MALFORMED, t) === "AssetDeclaration")); assert.ok(tokens.some((t) => t.tokenType === "type" && sliceAt(MALFORMED, t) === "LocomotorTemplate")); assert.ok(tokens.some((t) => t.tokenType === "type" && sliceAt(MALFORMED, t) === "Other")); assert.ok( tokens.some( (t) => t.tokenType === "type" && sliceAt(MALFORMED, t) === "LocomotorTemplate" && t.line === 3, ), ); // Attribute names. assert.ok(tokens.some((t) => t.tokenType === "property" && sliceAt(MALFORMED, t) === "Surfaces")); assert.ok(tokens.some((t) => t.tokenType === "property" && sliceAt(MALFORMED, t) === "id")); // Attribute values: closed quotes include both quotes; the unterminated // Surfaces value keeps the opening quote and everything up to the recovered // line end (the `>` is still inside the unclosed string). const strings = tokens.filter((t) => t.tokenType === "string").map((t) => sliceAt(MALFORMED, t)); assert.ok(strings.includes('"x"')); assert.ok(strings.includes('"GROUND>')); // Tokens are sorted by position for the vscode encoder. for (let i = 1; i < tokens.length; i++) { const a = tokens[i - 1]; const b = tokens[i]; assert.ok(a.line < b.line || (a.line === b.line && a.startChar <= b.startChar)); } }); test("well-formed XML gets no semantic fallback tokens", async () => { const text = `\n \n`; const provider = new Ra3SemanticTokensProvider(); const result = await provider.provideDocumentSemanticTokens(makeDocument(text), {}); assert.equal(result.data.length, 0); }); test("malformed XML gets semantic fallback tokens", async () => { const provider = new Ra3SemanticTokensProvider(); const result = await provider.provideDocumentSemanticTokens( makeDocument(MALFORMED), {}, ); assert.ok(result.data.length > 0); });