修代码补全

This commit is contained in:
2026-08-01 15:47:51 +02:00
parent 429ea367f3
commit 45fa2b9a27
18 changed files with 802 additions and 37 deletions
+145
View File
@@ -0,0 +1,145 @@
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 =
`<AssetDeclaration>\n <LocomotorTemplate id="x" Surfaces="GROUND>\n <Other/>\n</LocomotorTemplate>\n</AssetDeclaration>`;
test("fallback tokens cover names, attributes and values", () => {
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 = `<AssetDeclaration>\n <LocomotorTemplate id="x" Surfaces="GROUND"/>\n</AssetDeclaration>`;
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);
});