0.1.24 fix inheritFrom="type:id"
This commit is contained in:
@@ -777,6 +777,69 @@ test("simple-content value completion works before the closing tag is typed", as
|
||||
assert.equal(item.range.end.character, pos.character);
|
||||
});
|
||||
|
||||
test("asset value completion keeps a typed Type: prefix the user already typed", async () => {
|
||||
const def = {
|
||||
type: "AudioEvent",
|
||||
id: "BaseSoundEffect",
|
||||
file: "Sounds.xml",
|
||||
line: 1,
|
||||
origin: "sdk",
|
||||
};
|
||||
const idx = {
|
||||
assets: new Map([["AudioEvent", new Map([["basesoundeffect", [def]]])]]),
|
||||
assetsById: new Map([["basesoundeffect", [def]]]),
|
||||
};
|
||||
|
||||
// Qualified input: the typed "AudioEvent:" prefix must be kept, so the
|
||||
// completed value is "AudioEvent:BaseSoundEffect" and the replacement
|
||||
// range still covers only the current segment.
|
||||
const qualifiedText =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="X" inheritFrom="AudioEvent:Base\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const qualifiedLine = qualifiedText.split("\n")[1];
|
||||
const qualifiedPos = new Position(1, qualifiedLine.length);
|
||||
const qualifiedItems = await makeProvider(idx).provideCompletionItems(
|
||||
makeDocument(qualifiedText),
|
||||
qualifiedPos,
|
||||
token,
|
||||
);
|
||||
const qualifiedLabels = qualifiedItems.map((i) => i.label);
|
||||
assert.ok(
|
||||
qualifiedLabels.includes("AudioEvent:BaseSoundEffect"),
|
||||
"qualified label offered for AudioEvent:Base",
|
||||
);
|
||||
assert.ok(
|
||||
!qualifiedLabels.includes("BaseSoundEffect"),
|
||||
"the bare id is not offered when a type prefix was typed",
|
||||
);
|
||||
const qualifiedItem = qualifiedItems.find(
|
||||
(i) => i.label === "AudioEvent:BaseSoundEffect",
|
||||
);
|
||||
assert.equal(qualifiedItem.insertText, "AudioEvent:BaseSoundEffect");
|
||||
assert.equal(
|
||||
qualifiedItem.range.start.character,
|
||||
qualifiedLine.lastIndexOf('"') + 1,
|
||||
);
|
||||
assert.equal(qualifiedItem.range.end.character, qualifiedPos.character);
|
||||
|
||||
// Plain input stays plain: no prefix typed -> bare id, unchanged behavior.
|
||||
const plainText =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="X" inheritFrom="Base\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const plainLine = plainText.split("\n")[1];
|
||||
const plainPos = new Position(1, plainLine.length);
|
||||
const plainItems = await makeProvider(idx).provideCompletionItems(
|
||||
makeDocument(plainText),
|
||||
plainPos,
|
||||
token,
|
||||
);
|
||||
const plainItem = plainItems.find((i) => i.label === "BaseSoundEffect");
|
||||
assert.ok(plainItem, "bare id offered without a type prefix");
|
||||
assert.equal(plainItem.insertText, "BaseSoundEffect");
|
||||
});
|
||||
|
||||
test("simpleContent complex child inserts a value pair and triggers suggest", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
|
||||
@@ -239,6 +239,63 @@ test("Ctrl+click on simple-content text jumps to the definition", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("qualified Type:Id inheritFrom is diagnosed and navigated as resolved", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration xmlns="uri:ea.com:eala:asset">\n` +
|
||||
` <AudioEvent id="BaseSoundEffect"/>\n` +
|
||||
` <AudioEvent id="X" inheritFrom="AudioEvent:BaseSoundEffect"/>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const def = {
|
||||
type: "AudioEvent",
|
||||
id: "BaseSoundEffect",
|
||||
file: URI,
|
||||
line: 1,
|
||||
origin: "project",
|
||||
};
|
||||
const idx = makeIdx([def]);
|
||||
const scope = await makeScope(text, idx);
|
||||
|
||||
// Diagnostics: the manifest-style qualified value must not be reported as
|
||||
// an unresolved reference (the reported FutureTank scenario).
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const diagnostics = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: false,
|
||||
reportUnresolvedReferences: "warning",
|
||||
},
|
||||
});
|
||||
diagnostics["collection"] = collection;
|
||||
await diagnostics.update(makeDocument(text));
|
||||
const messages = collection.last.diags.map((d) => d.message);
|
||||
assert.ok(
|
||||
!messages.some((m) => m.includes("AudioEvent:BaseSoundEffect")),
|
||||
"qualified inheritFrom is not unresolved",
|
||||
);
|
||||
|
||||
// Ctrl+click on the qualified value jumps to the plain-id definition.
|
||||
const provider = new Ra3DefinitionProvider({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: { definitionMode: "all" },
|
||||
indexer: null,
|
||||
});
|
||||
const line = text.split("\n")[2];
|
||||
const pos = new Position(2, line.indexOf("AudioEvent:BaseSoundEffect") + 8);
|
||||
const locations = await provider.provideDefinition(makeDocument(text), pos, {});
|
||||
assert.ok(locations && locations.length === 1, "qualified reference resolves");
|
||||
const defLine = text.split("\n")[1];
|
||||
const defStartChar =
|
||||
defLine.indexOf('id="BaseSoundEffect"') + 'id="'.length;
|
||||
assert.equal(locations[0].range.start.line, 1);
|
||||
assert.equal(locations[0].range.start.character, defStartChar);
|
||||
assert.equal(
|
||||
locations[0].range.end.character,
|
||||
defStartChar + "BaseSoundEffect".length,
|
||||
);
|
||||
});
|
||||
|
||||
test("hover on simpleContent complex content shows the referenced definition", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
|
||||
@@ -299,6 +299,81 @@ test("manifest assets sharing an id keep every type in assetsById", async () =>
|
||||
assert.ok(airfield?.some((d) => d.type === "W3DContainer"), "W3DContainer retained");
|
||||
});
|
||||
|
||||
test("qualified Type:Id inheritFrom resolves against an instance-included SageXml definition", async () => {
|
||||
const tmp = fs.mkdtempSync(join(os.tmpdir(), "ra3-qualified-ref-"));
|
||||
try {
|
||||
const projectDir = join(tmp, "project");
|
||||
const sdkDir = join(tmp, "sdk");
|
||||
fs.mkdirSync(join(projectDir, "Data"), { recursive: true });
|
||||
fs.mkdirSync(join(sdkDir, "SageXml", "Sounds"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(projectDir, "Data", "Mod.xml"),
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Includes>
|
||||
<Include type="all" source="Units.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(projectDir, "Data", "Units.xml"),
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Includes>
|
||||
<Include type="instance" source="DATA:SageXml/Sounds/BaseSoundEffect.xml" />
|
||||
</Includes>
|
||||
<AudioEvent id="ALL_FutureTank_ArmPrimaryWeapon" inheritFrom="AudioEvent:BaseSoundEffect" />
|
||||
</AssetDeclaration>`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(sdkDir, "SageXml", "Sounds", "BaseSoundEffect.xml"),
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<AudioEvent id="BaseSoundEffect" />
|
||||
</AssetDeclaration>`,
|
||||
);
|
||||
|
||||
const indexer = new ModIndexer({
|
||||
projectDir,
|
||||
sdkDir,
|
||||
builtmodsDirs: [],
|
||||
indexSageXml: false,
|
||||
additionalDataSearchPaths: [],
|
||||
walker: new CachedDirectoryWalker(),
|
||||
});
|
||||
const idx = await indexer.build();
|
||||
|
||||
assert.ok(
|
||||
!idx.diagnostics.some((d) => d.code === "include-not-found"),
|
||||
"the DATA:SageXml instance include resolves",
|
||||
);
|
||||
const defs = idx.assetsById.get("basesoundeffect");
|
||||
assert.ok(
|
||||
defs?.some((d) => d.type === "AudioEvent"),
|
||||
"the SageXml definition is indexed through the instance include",
|
||||
);
|
||||
|
||||
const targets = resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"AudioEvent",
|
||||
"inheritFrom",
|
||||
"AudioEvent:BaseSoundEffect",
|
||||
);
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.id, "BaseSoundEffect");
|
||||
assert.equal(targets[0].def.type, "AudioEvent");
|
||||
|
||||
const sageDef = defs.find((d) => d.type === "AudioEvent");
|
||||
const sites = idx.references.get(assetDefKey(sageDef));
|
||||
assert.ok(
|
||||
sites?.some((s) => /Units\.xml$/.test(s.file)),
|
||||
"the qualified inheritFrom lands in the reverse index (FAR / CodeLens)",
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("build publishes an immutable XML phase before art scanning", async () => {
|
||||
let phaseA;
|
||||
const indexer = new ModIndexer({
|
||||
|
||||
@@ -157,6 +157,42 @@ test("records extracted from XML resolve through the reference index", () => {
|
||||
assert.equal(csSites[0].kind, "attr");
|
||||
});
|
||||
|
||||
test("qualified Type:Id reference records resolve to plain-id definitions", () => {
|
||||
const def = makeDef("AudioEvent", "BaseSoundEffect", "C:/sdk/Sounds.xml", 2);
|
||||
const lookup = {
|
||||
assets: new Map(),
|
||||
assetsById: new Map([["basesoundeffect", [def]]]),
|
||||
};
|
||||
const records = {
|
||||
assets: [],
|
||||
defines: [],
|
||||
includes: [],
|
||||
rootXiIncludes: [],
|
||||
nestedXiIncludes: [],
|
||||
references: [
|
||||
{
|
||||
kind: "attr",
|
||||
refType: null,
|
||||
selfType: "AudioEvent",
|
||||
value: "AudioEvent:BaseSoundEffect",
|
||||
line: 3,
|
||||
start: 10,
|
||||
end: 40,
|
||||
},
|
||||
],
|
||||
};
|
||||
const map = buildReferenceIndex(
|
||||
[{ file: "C:/mod/AudioEvent.xml", records }],
|
||||
lookup,
|
||||
);
|
||||
|
||||
const sites = map.get(assetDefKey(def));
|
||||
assert.equal(sites?.length, 1);
|
||||
assert.equal(sites[0].file, "C:/mod/AudioEvent.xml");
|
||||
assert.equal(sites[0].start, 10);
|
||||
assert.equal(sites[0].end, 40);
|
||||
});
|
||||
|
||||
test("referenceSitesForDefinition unions manifest-source sites onto the SageXml source file", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "ra3-refindex-"));
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isReferenceAttributeOfType,
|
||||
isReferenceContentType,
|
||||
isReferenceTargetType,
|
||||
normalizeReferenceId,
|
||||
resolveContentReferenceTargets,
|
||||
resolveReferenceTargets,
|
||||
resolveReferenceTargetsForType,
|
||||
@@ -346,6 +347,130 @@ test("simpleContent complex types resolve as typed content references", () => {
|
||||
assert.equal(subsoundTargets[0].def.type, "AudioEvent");
|
||||
});
|
||||
|
||||
test("normalizeReferenceId strips a manifest-style Type: prefix", () => {
|
||||
assert.equal(normalizeReferenceId("BaseSoundEffect"), "BaseSoundEffect");
|
||||
assert.equal(
|
||||
normalizeReferenceId("AudioEvent:BaseSoundEffect"),
|
||||
"BaseSoundEffect",
|
||||
);
|
||||
// Art-asset manifest names can carry a subtype segment; the referenceable
|
||||
// id is still the last colon segment.
|
||||
assert.equal(
|
||||
normalizeReferenceId("W3dContainer:W3DContainer:ABC_SKN"),
|
||||
"ABC_SKN",
|
||||
);
|
||||
// A trailing colon has no id yet; keep the raw value so a half-typed
|
||||
// qualified value never matches anything.
|
||||
assert.equal(normalizeReferenceId("AudioEvent:"), "AudioEvent:");
|
||||
});
|
||||
|
||||
test("qualified Type:Id inheritFrom values resolve to plain-id definitions", () => {
|
||||
const def = {
|
||||
type: "AudioEvent",
|
||||
id: "BaseSoundEffect",
|
||||
file: "Sounds.xml",
|
||||
line: 1,
|
||||
origin: "sdk",
|
||||
};
|
||||
const idx = {
|
||||
assetsById: new Map([["basesoundeffect", [def]]]),
|
||||
assets: new Map(),
|
||||
defines: new Map(),
|
||||
};
|
||||
|
||||
// The reported scenario: <AudioEvent inheritFrom="AudioEvent:BaseSoundEffect"/>.
|
||||
const qualified = resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"AudioEvent",
|
||||
"inheritFrom",
|
||||
"AudioEvent:BaseSoundEffect",
|
||||
);
|
||||
assert.equal(qualified.length, 1);
|
||||
assert.equal(qualified[0].def.id, "BaseSoundEffect");
|
||||
|
||||
// Plain ids keep working unchanged.
|
||||
assert.equal(
|
||||
resolveReferenceTargetsForType(idx, "AudioEvent", "inheritFrom", "BaseSoundEffect")
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
|
||||
// A wrong type prefix is still filtered by selfType: the AudioEvent def
|
||||
// must never satisfy a GameObject inheritFrom.
|
||||
assert.equal(
|
||||
resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"GameObject",
|
||||
"inheritFrom",
|
||||
"GameObject:BaseSoundEffect",
|
||||
).length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("qualified Type:Id values resolve for typed attributes and content refs", () => {
|
||||
const audioEvent = {
|
||||
type: "AudioEvent",
|
||||
id: "JAP_Refinery_Select",
|
||||
file: "SoundEffects.xml",
|
||||
line: 1,
|
||||
origin: "sdk",
|
||||
};
|
||||
const playerTemplate = {
|
||||
type: "PlayerTemplate",
|
||||
id: "Allies",
|
||||
file: "PlayerTemplates.xml",
|
||||
line: 1,
|
||||
origin: "manifest",
|
||||
};
|
||||
const audioFile = {
|
||||
type: "AudioFile",
|
||||
id: "Shared",
|
||||
file: "Audio.xml",
|
||||
line: 1,
|
||||
origin: "project",
|
||||
};
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
["jap_refinery_select", [audioEvent]],
|
||||
["allies", [playerTemplate]],
|
||||
["shared", [audioFile]],
|
||||
]),
|
||||
assets: new Map(),
|
||||
defines: new Map(),
|
||||
};
|
||||
|
||||
// SoundOrEvaEvent@Sound refType is BaseAudioEventInfo; the concrete
|
||||
// "AudioEvent:" prefix must survive normalization and the type filter.
|
||||
const soundTargets = resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"SoundOrEvaEvent",
|
||||
"Sound",
|
||||
"AudioEvent:JAP_Refinery_Select",
|
||||
);
|
||||
assert.equal(soundTargets.length, 1);
|
||||
assert.equal(soundTargets[0].def.type, "AudioEvent");
|
||||
|
||||
// Side="PlayerTemplate:Allies" style attribute.
|
||||
const sideTargets = resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"SideSound",
|
||||
"Side",
|
||||
"PlayerTemplate:Allies",
|
||||
);
|
||||
assert.equal(sideTargets.length, 1);
|
||||
assert.equal(sideTargets[0].def.type, "PlayerTemplate");
|
||||
|
||||
// Simple-content references use the same convention.
|
||||
const contentTargets = resolveContentReferenceTargets(
|
||||
idx,
|
||||
"AudioFileRefWithWeight",
|
||||
"AudioFile:Shared",
|
||||
);
|
||||
assert.equal(contentTargets.length, 1);
|
||||
assert.equal(contentTargets[0].def.type, "AudioFile");
|
||||
});
|
||||
|
||||
test("untyped and pipeline-local content is not a global reference", () => {
|
||||
// Generic AssetReference content is used for shader constants and model
|
||||
// sub-object names, not global asset ids; Poid is pipeline-local.
|
||||
|
||||
Reference in New Issue
Block a user