0.1.21
This commit is contained in:
+45
-16
@@ -20,6 +20,7 @@ import {
|
||||
Ra3SemanticTokensProvider,
|
||||
RA3_SEMANTIC_TOKENS_LEGEND,
|
||||
} from "./features/semanticTokens";
|
||||
import { t } from "./localize";
|
||||
|
||||
const XML_SELECTOR: vscode.DocumentSelector = [{ language: "xml" }];
|
||||
/** Safety-net refresh interval while a rebuild is running. */
|
||||
@@ -203,7 +204,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
vscode.commands.registerCommand("ra3modxml.clearCache", () => {
|
||||
ws.clearCaches();
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: caches cleared; rebuilding from scratch…",
|
||||
t("RA3 Mod XML: caches cleared; rebuilding from scratch…"),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -220,35 +221,63 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
if (!idx) {
|
||||
if (ws.isBuilding) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: index is still building — check the status bar. " +
|
||||
"Most features become available after the XML phase.",
|
||||
t(
|
||||
"RA3 Mod XML: index is still building — check the status bar. Most features become available after the XML phase.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (ws.getProjectRoots().length) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: no index for the active project yet — open a mod XML document to start indexing.",
|
||||
t(
|
||||
"RA3 Mod XML: no index for the active project yet — open a mod XML document to start indexing.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: no index available. Open a workspace that contains Data/Mod.xml, Data/additionalmaps/mapmetadata_*.xml or a mod folder.",
|
||||
t(
|
||||
"RA3 Mod XML: no index available. Open a workspace that contains Data/Mod.xml, Data/additionalmaps/mapmetadata_*.xml or a mod folder.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const s = idx.stats;
|
||||
const stale = idx.stale ? " (stale)" : "";
|
||||
const stale = idx.stale ? ` ${t("(stale)")}` : "";
|
||||
void vscode.window.showInformationMessage(
|
||||
`RA3 Mod XML index\n` +
|
||||
`Project: ${s.projectDir}\n` +
|
||||
`Files: ${s.indexedFiles} (${s.parsedFiles} parsed, ${s.shallowScannedFiles} shallow-scanned, ${s.shallowCacheHits + s.recordsCacheHits} cache hits)\n` +
|
||||
`Assets: ${s.assetCount} (${s.manifestAssetCount} from ${s.manifestFiles} manifests)\n` +
|
||||
`References: ${s.referenceCount}\n` +
|
||||
`Defines: ${s.defineCount} · Streams: ${s.streams} · Candidates: ${s.sourceCandidates}\n` +
|
||||
`Phase: ${s.phase} · Complete: ${s.complete}${stale}\n` +
|
||||
`Build #${ws.buildCount} (trigger: ${ws.lastTrigger})\n` +
|
||||
`Indexed in ${(s.elapsedMs / 1000).toFixed(1)}s\n` +
|
||||
`XML walk: ${(s.walkMs / 1000).toFixed(1)}s · Candidates: ${(s.candidatesMs / 1000).toFixed(1)}s · Art scan: ${(s.artScanMs / 1000).toFixed(1)}s`,
|
||||
[
|
||||
t("RA3 Mod XML index"),
|
||||
t("Project: {0}", s.projectDir),
|
||||
t(
|
||||
"Files: {0} ({1} parsed, {2} shallow-scanned, {3} cache hits)",
|
||||
s.indexedFiles,
|
||||
s.parsedFiles,
|
||||
s.shallowScannedFiles,
|
||||
s.shallowCacheHits + s.recordsCacheHits,
|
||||
),
|
||||
t(
|
||||
"Assets: {0} ({1} from {2} manifests)",
|
||||
s.assetCount,
|
||||
s.manifestAssetCount,
|
||||
s.manifestFiles,
|
||||
),
|
||||
t("References: {0}", s.referenceCount),
|
||||
t(
|
||||
"Defines: {0} · Streams: {1} · Candidates: {2}",
|
||||
s.defineCount,
|
||||
s.streams,
|
||||
s.sourceCandidates,
|
||||
),
|
||||
t("Phase: {0} · Complete: {1}{2}", s.phase, s.complete, stale),
|
||||
t("Build #{0} (trigger: {1})", ws.buildCount, ws.lastTrigger),
|
||||
t("Indexed in {0}s", (s.elapsedMs / 1000).toFixed(1)),
|
||||
t(
|
||||
"XML walk: {0}s · Candidates: {1}s · Art scan: {2}s",
|
||||
(s.walkMs / 1000).toFixed(1),
|
||||
(s.candidatesMs / 1000).toFixed(1),
|
||||
(s.artScanMs / 1000).toFixed(1),
|
||||
),
|
||||
].join("\n"),
|
||||
{ modal: false },
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ShowReferencesArgs,
|
||||
} from "./references";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import { t } from "../localize";
|
||||
|
||||
/** Never build a DOM for huge files just to show counts (w3x safety). */
|
||||
const MAX_CODELENS_TEXT = 4 * 1024 * 1024;
|
||||
@@ -124,10 +125,10 @@ export class Ra3CodeLensProvider implements vscode.CodeLensProvider {
|
||||
new vscode.CodeLens(range, {
|
||||
title:
|
||||
count === 0
|
||||
? "0 references"
|
||||
? t("0 references")
|
||||
: count === 1
|
||||
? "1 reference"
|
||||
: `${count} references`,
|
||||
? t("1 reference")
|
||||
: t("{0} references", count),
|
||||
command: "ra3modxml.showReferences",
|
||||
arguments: [args],
|
||||
}),
|
||||
|
||||
+90
-32
@@ -17,6 +17,7 @@ import {
|
||||
} from "../indexer/logicalTree";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex, AssetDef } from "../indexer/types";
|
||||
import { t } from "../localize";
|
||||
|
||||
const MAX_VALUE_ITEMS = 400;
|
||||
|
||||
@@ -79,9 +80,9 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
const docText =
|
||||
child.doc ||
|
||||
(info?.kind === "complex" ? info.doc : "") ||
|
||||
(type ? `Type: ${type}` : "");
|
||||
(type ? t("Type: {0}", type) : "");
|
||||
item.documentation = docText ? new vscode.MarkdownString(docText) : undefined;
|
||||
item.detail = type ? `RA3 XML · ${type}` : "RA3 XML";
|
||||
item.detail = type ? t("RA3 XML · {0}", type) : t("RA3 XML");
|
||||
item.insertText = this.elementSnippet(child.name, type, ctx.element == null);
|
||||
items.push(item);
|
||||
}
|
||||
@@ -93,7 +94,11 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
): { name: string; type: string | null; doc: string }[] {
|
||||
if (!parent) {
|
||||
return [
|
||||
{ name: "AssetDeclaration", type: null, doc: "Root element of every RA3 asset file" },
|
||||
{
|
||||
name: "AssetDeclaration",
|
||||
type: null,
|
||||
doc: t("Root element of every RA3 asset file"),
|
||||
},
|
||||
];
|
||||
}
|
||||
const parentType = resolveElementType(parent);
|
||||
@@ -160,12 +165,16 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
item.sortText = attr.required ? "0" + attr.name : "1" + attr.name;
|
||||
const md = new vscode.MarkdownString();
|
||||
if (attr.doc) md.appendMarkdown(attr.doc + "\n\n");
|
||||
if (attr.required) md.appendMarkdown(`**Required** \n`);
|
||||
if (attr.refType) md.appendMarkdown(`References: \`${attr.refType}\` \n`);
|
||||
if (attr.required) md.appendMarkdown(`${t("**Required**")} \n`);
|
||||
if (attr.refType) {
|
||||
md.appendMarkdown(`${t("References: `{0}`", attr.refType)} \n`);
|
||||
}
|
||||
if (attr.enumValues.length)
|
||||
md.appendMarkdown(`Values: ${attr.enumValues.join(", ")} \n`);
|
||||
if (attr.default != null) md.appendMarkdown(`Default: \`${attr.default}\` \n`);
|
||||
md.appendMarkdown(`Type: \`${attr.type ?? "string"}\``);
|
||||
md.appendMarkdown(`${t("Values: {0}", attr.enumValues.join(", "))} \n`);
|
||||
if (attr.default != null) {
|
||||
md.appendMarkdown(`${t("Default: `{0}`", attr.default)} \n`);
|
||||
}
|
||||
md.appendMarkdown(t("Type: `{0}`", attr.type ?? "string"));
|
||||
item.documentation = md;
|
||||
const value = this.attributeValuePlaceholder(attr, el);
|
||||
item.insertText = new vscode.SnippetString(
|
||||
@@ -174,7 +183,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
if (value.trigger) {
|
||||
item.command = {
|
||||
command: "editor.action.triggerSuggest",
|
||||
title: "Suggest attribute values",
|
||||
title: t("Suggest attribute values"),
|
||||
};
|
||||
}
|
||||
items.push(item);
|
||||
@@ -187,13 +196,15 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
j.insertText = new vscode.SnippetString(
|
||||
layout.prefix + 'xai:joinAction="$1"',
|
||||
);
|
||||
j.detail = "Instance join action";
|
||||
j.detail = t("Instance join action");
|
||||
j.documentation = new vscode.MarkdownString(
|
||||
"Controls how this element merges with the inherited definition: `Replace` or `Remove`.",
|
||||
t(
|
||||
"Controls how this element merges with the inherited definition: `Replace` or `Remove`.",
|
||||
),
|
||||
);
|
||||
j.command = {
|
||||
command: "editor.action.triggerSuggest",
|
||||
title: "Suggest attribute values",
|
||||
title: t("Suggest attribute values"),
|
||||
};
|
||||
items.push(j);
|
||||
}
|
||||
@@ -203,7 +214,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
ns.insertText = new vscode.SnippetString(
|
||||
layout.prefix + 'xmlns:xai="uri:ea.com:eala:asset:instance"',
|
||||
);
|
||||
ns.detail = "xai namespace";
|
||||
ns.detail = t("xai namespace");
|
||||
items.push(ns);
|
||||
}
|
||||
return items;
|
||||
@@ -312,7 +323,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
// Include type / source
|
||||
if (isInclude && attrName === "type") {
|
||||
return ["reference", "instance", "all"].map((v) =>
|
||||
make(v, vscode.CompletionItemKind.EnumMember, "Include type"),
|
||||
make(v, vscode.CompletionItemKind.EnumMember, t("Include type")),
|
||||
);
|
||||
}
|
||||
if (isInclude && attrName === "source") {
|
||||
@@ -321,7 +332,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
}
|
||||
if (attrName === "xai:joinaction" || attrName === "joinaction") {
|
||||
return ["Replace", "Remove"].map((v) =>
|
||||
make(v, vscode.CompletionItemKind.EnumMember, "xai:joinAction"),
|
||||
make(v, vscode.CompletionItemKind.EnumMember, t("xai:joinAction")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -348,12 +359,18 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
}
|
||||
return attrInfo.enumValues
|
||||
.filter((v) => v.toLowerCase().startsWith(prefix.toLowerCase()))
|
||||
.map((v) => make(v, vscode.CompletionItemKind.EnumMember, attrInfo.type ?? "enum"));
|
||||
.map((v) =>
|
||||
make(
|
||||
v,
|
||||
vscode.CompletionItemKind.EnumMember,
|
||||
attrInfo.type ?? t("enum"),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (attrInfo?.isBoolean) {
|
||||
return ["true", "false"]
|
||||
.filter((v) => v.startsWith(prefix.toLowerCase()))
|
||||
.map((v) => make(v, vscode.CompletionItemKind.Value, "boolean"));
|
||||
.map((v) => make(v, vscode.CompletionItemKind.Value, t("boolean")));
|
||||
}
|
||||
if (idx && attrInfo?.allowsDefine) {
|
||||
return this.defineItems(idx, prefix, make);
|
||||
@@ -412,7 +429,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
make(
|
||||
v,
|
||||
vscode.CompletionItemKind.EnumMember,
|
||||
attrInfo.type ?? "enum",
|
||||
attrInfo.type ?? t("enum"),
|
||||
undefined,
|
||||
range,
|
||||
append ? ` ${v}` : v,
|
||||
@@ -436,10 +453,14 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
a.source.localeCompare(b.source),
|
||||
);
|
||||
const items = candidates.map((c) => {
|
||||
const item = make(c.source, vscode.CompletionItemKind.File, "Include source");
|
||||
const item = make(
|
||||
c.source,
|
||||
vscode.CompletionItemKind.File,
|
||||
t("Include source"),
|
||||
);
|
||||
item.detail = c.path;
|
||||
item.documentation = new vscode.MarkdownString(
|
||||
`\`${c.prefix ?? "relative"}\` · ${c.path}`,
|
||||
t("`{0}` · {1}", c.prefix ?? t("relative"), c.path),
|
||||
);
|
||||
return item;
|
||||
});
|
||||
@@ -520,19 +541,30 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
);
|
||||
const items = top.map(({ def }) => {
|
||||
const originLabel = (d: AssetDef) =>
|
||||
d.origin === "manifest" ? `manifest (${d.manifestSource ?? ""})` : d.origin;
|
||||
d.origin === "manifest"
|
||||
? d.manifestSource
|
||||
? t("manifest ({0})", d.manifestSource)
|
||||
: t("manifest")
|
||||
: originLabelText(d.origin);
|
||||
const origin = originLabel(def);
|
||||
const doc = new vscode.MarkdownString();
|
||||
doc.appendCodeblock(def.id);
|
||||
doc.appendMarkdown(`**Type**: ${def.type} \n`);
|
||||
if (def.manifestSource) doc.appendMarkdown(`**Source**: ${def.manifestSource} \n`);
|
||||
doc.appendMarkdown(`**Origin**: ${origin}`);
|
||||
doc.appendMarkdown(`${t("**Type**: {0}", def.type)} \n`);
|
||||
if (def.manifestSource) {
|
||||
doc.appendMarkdown(`${t("**Source**: {0}", def.manifestSource)} \n`);
|
||||
}
|
||||
doc.appendMarkdown(t("**Origin**: {0}", origin));
|
||||
for (const extra of byId.get(def.id.toLowerCase())?.extras ?? []) {
|
||||
doc.appendMarkdown(
|
||||
`\n\nAlso defined as **${extra.type}** · ${originLabel(extra)}`,
|
||||
`\n\n${t("Also defined as **{0}** · {1}", extra.type, originLabel(extra))}`,
|
||||
);
|
||||
}
|
||||
return make(def.id, vscode.CompletionItemKind.Value, `${def.type} · ${origin}`, doc.value);
|
||||
return make(
|
||||
def.id,
|
||||
vscode.CompletionItemKind.Value,
|
||||
t("{0} · {1}", def.type, origin),
|
||||
doc.value,
|
||||
);
|
||||
});
|
||||
return this.limitItems(items, byId.size);
|
||||
}
|
||||
@@ -557,7 +589,12 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
if (seen.has(dedupe)) continue;
|
||||
seen.add(dedupe);
|
||||
const label = `$${def.name}`;
|
||||
const item = make(label, vscode.CompletionItemKind.Constant, "Define", def.value);
|
||||
const item = make(
|
||||
label,
|
||||
vscode.CompletionItemKind.Constant,
|
||||
t("Define"),
|
||||
def.value,
|
||||
);
|
||||
item.insertText = label;
|
||||
items.push(item);
|
||||
}
|
||||
@@ -580,8 +617,10 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
make(
|
||||
id,
|
||||
vscode.CompletionItemKind.Value,
|
||||
"local module",
|
||||
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).",
|
||||
t("local module"),
|
||||
t(
|
||||
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -674,7 +713,13 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
if (isList) return this.listEnumItems(info, rawPrefix, seg, valueRange, make);
|
||||
return info.enumValues
|
||||
.filter((v) => v.toLowerCase().startsWith(seg.token.toLowerCase()))
|
||||
.map((v) => make(v, vscode.CompletionItemKind.EnumMember, elType ?? "enum"));
|
||||
.map((v) =>
|
||||
make(
|
||||
v,
|
||||
vscode.CompletionItemKind.EnumMember,
|
||||
elType ?? t("enum"),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (idx && info.allowsDefine) {
|
||||
return this.defineItems(idx, seg.token, make);
|
||||
@@ -708,13 +753,13 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
item.insertText = this.elementSnippet(child.name, child.type, !typedOpen);
|
||||
const type = child.type;
|
||||
const info = type ? model.typeInfo(type) : undefined;
|
||||
item.detail = type ? `RA3 XML · ${type}` : "RA3 XML";
|
||||
item.detail = type ? t("RA3 XML · {0}", type) : t("RA3 XML");
|
||||
const doc = child.doc || (info?.kind === "complex" ? info.doc : "");
|
||||
if (doc) item.documentation = new vscode.MarkdownString(doc);
|
||||
if (info?.kind === "simple" && this.simpleContentValueKind(info)) {
|
||||
item.command = {
|
||||
command: "editor.action.triggerSuggest",
|
||||
title: "Suggest content value",
|
||||
title: t("Suggest content value"),
|
||||
};
|
||||
}
|
||||
items.push(item);
|
||||
@@ -736,6 +781,19 @@ interface ScoredDef {
|
||||
score: number;
|
||||
}
|
||||
|
||||
function originLabelText(origin: AssetDef["origin"]): string {
|
||||
switch (origin) {
|
||||
case "project":
|
||||
return t("project");
|
||||
case "sdk":
|
||||
return t("SDK");
|
||||
case "manifest":
|
||||
return t("manifest");
|
||||
default:
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
|
||||
function compareScoredDefs(a: ScoredDef, b: ScoredDef): number {
|
||||
return a.score - b.score || a.def.id.localeCompare(b.def.id);
|
||||
}
|
||||
|
||||
+117
-36
@@ -1,6 +1,10 @@
|
||||
import * as vscode from "vscode";
|
||||
import { dirname } from "node:path";
|
||||
import { LineMap, type XmlElement } from "../language/xmlParser";
|
||||
import {
|
||||
LineMap,
|
||||
type XmlElement,
|
||||
type XmlParseError,
|
||||
} from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import { resolveSource, buildSearchPaths } from "../indexer/includeResolver";
|
||||
import { validateSdkPath } from "../sdk";
|
||||
@@ -16,6 +20,7 @@ import {
|
||||
} from "../indexer/refs";
|
||||
import type { LogicalElement } from "../indexer/logicalTree";
|
||||
import { scopePathKey } from "../indexer/localScope";
|
||||
import { t } from "../localize";
|
||||
|
||||
export class Ra3Diagnostics {
|
||||
private collection: vscode.DiagnosticCollection;
|
||||
@@ -57,7 +62,7 @@ export class Ra3Diagnostics {
|
||||
new vscode.Position(err.line, err.character),
|
||||
new vscode.Position(err.line, err.character + 1),
|
||||
),
|
||||
err.message,
|
||||
this.parseErrorMessage(err),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"xml-syntax",
|
||||
),
|
||||
@@ -79,6 +84,42 @@ export class Ra3Diagnostics {
|
||||
this.collection.set(document.uri, diags);
|
||||
}
|
||||
|
||||
private parseErrorMessage(err: XmlParseError): string {
|
||||
switch (err.code) {
|
||||
case "content-before-root":
|
||||
return t("Content is not allowed before the root element");
|
||||
case "unterminated-comment":
|
||||
return t("Unterminated comment");
|
||||
case "unterminated-cdata":
|
||||
return t("Unterminated CDATA section");
|
||||
case "unterminated-doctype":
|
||||
return t("Unterminated DOCTYPE");
|
||||
case "unterminated-processing-instruction":
|
||||
return t("Unterminated processing instruction");
|
||||
case "unterminated-closing-tag":
|
||||
return t("Unterminated closing tag");
|
||||
case "unterminated-start-tag":
|
||||
return t("Unterminated start tag");
|
||||
case "malformed-markup":
|
||||
return t("Malformed markup");
|
||||
case "unexpected-closing-tag":
|
||||
return t(
|
||||
"Unexpected closing tag </{0}>",
|
||||
err.params?.name ?? "",
|
||||
);
|
||||
case "mismatched-closing-tag":
|
||||
return t(
|
||||
"Mismatched closing tag: expected </{0}>, found </{1}>",
|
||||
err.params?.expected ?? "",
|
||||
err.params?.found ?? "",
|
||||
);
|
||||
case "element-never-closed":
|
||||
return t("Element <{0}> is never closed", err.params?.name ?? "");
|
||||
default:
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
|
||||
clear(uri: vscode.Uri): void {
|
||||
this.collection.delete(uri);
|
||||
}
|
||||
@@ -120,7 +161,7 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Top-level asset <${local}> requires an id attribute`,
|
||||
t("Top-level asset <{0}> requires an id attribute", local),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"missing-id",
|
||||
),
|
||||
@@ -136,7 +177,12 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
where,
|
||||
`Duplicate id "${idAttr.value}" for <${local}> (also defined on line ${prev.line})`,
|
||||
t(
|
||||
'Duplicate id "{0}" for <{1}> (also defined on line {2})',
|
||||
idAttr.value,
|
||||
local,
|
||||
prev.line,
|
||||
),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"duplicate-id",
|
||||
),
|
||||
@@ -166,7 +212,7 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Unknown element <${local}> (not in the RA3 XSD model)`,
|
||||
t("Unknown element <{0}> (not in the RA3 XSD model)", local),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"unknown-element",
|
||||
),
|
||||
@@ -187,16 +233,16 @@ export class Ra3Diagnostics {
|
||||
continue;
|
||||
}
|
||||
if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
|
||||
diags.push(
|
||||
this.diag(
|
||||
new vscode.Range(
|
||||
document.positionAt(attr.nameStart),
|
||||
document.positionAt(attr.nameEnd),
|
||||
diags.push(
|
||||
this.diag(
|
||||
new vscode.Range(
|
||||
document.positionAt(attr.nameStart),
|
||||
document.positionAt(attr.nameEnd),
|
||||
),
|
||||
t('Unknown attribute "{0}" for <{1}>', aName, local),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"unknown-attribute",
|
||||
),
|
||||
`Unknown attribute "${aName}" for <${local}>`,
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"unknown-attribute",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,8 +304,12 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Duplicate id "${id}" for <${type}> (also defined in ${other.file})` +
|
||||
(provisional ? " (based on a partial index)" : ""),
|
||||
t(
|
||||
'Duplicate id "{0}" for <{1}> (also defined in {2})',
|
||||
id,
|
||||
type,
|
||||
other.file,
|
||||
) + (provisional ? t(" (based on a partial index)") : ""),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"duplicate-id",
|
||||
),
|
||||
@@ -296,8 +346,8 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Undefined define "$${m[1]}"` +
|
||||
(provisional ? " (index incomplete — may be a false positive)" : ""),
|
||||
t('Undefined define "${0}"', m[1]) +
|
||||
(provisional ? t(" (index incomplete — may be a false positive)") : ""),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
code,
|
||||
),
|
||||
@@ -318,20 +368,18 @@ export class Ra3Diagnostics {
|
||||
const attrRef = model
|
||||
.attributesOfType(elType)
|
||||
.find((a) => a.name === attrName);
|
||||
const expected = attrRef?.refType
|
||||
? `of type \`${attrRef.refType}\``
|
||||
: attrRef?.isRef
|
||||
? "of the expected declared type"
|
||||
: "matching";
|
||||
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
|
||||
const baseMessage = anyDef
|
||||
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
|
||||
: `Unresolved reference "${value}" (not found in the current index)`;
|
||||
const baseMessage = unresolvedReferenceMessage(
|
||||
value,
|
||||
anyDef,
|
||||
attrRef?.refType ?? null,
|
||||
attrRef?.isRef ?? false,
|
||||
);
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
provisional
|
||||
? `${baseMessage} (index incomplete — may be a false positive)`
|
||||
? baseMessage + t(" (index incomplete — may be a false positive)")
|
||||
: baseMessage,
|
||||
severity === "warning"
|
||||
? vscode.DiagnosticSeverity.Warning
|
||||
@@ -379,8 +427,8 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Undefined define "$${m[1]}"` +
|
||||
(provisional ? " (index incomplete — may be a false positive)" : ""),
|
||||
t('Undefined define "${0}"', m[1]) +
|
||||
(provisional ? t(" (index incomplete — may be a false positive)") : ""),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
code,
|
||||
),
|
||||
@@ -398,16 +446,18 @@ export class Ra3Diagnostics {
|
||||
(idx.local?.assetsById.has(value.toLowerCase()) ?? false) ||
|
||||
idx.assetsById.has(value.toLowerCase());
|
||||
const refType = info.refType;
|
||||
const expected = refType ? `of type \`${refType}\`` : "of the expected declared type";
|
||||
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
|
||||
const baseMessage = anyDef
|
||||
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
|
||||
: `Unresolved reference "${value}" (not found in the current index)`;
|
||||
const baseMessage = unresolvedReferenceMessage(
|
||||
value,
|
||||
anyDef,
|
||||
refType ?? null,
|
||||
!refType,
|
||||
);
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
provisional
|
||||
? `${baseMessage} (index incomplete — may be a false positive)`
|
||||
? baseMessage + t(" (index incomplete — may be a false positive)")
|
||||
: baseMessage,
|
||||
severity === "warning"
|
||||
? vscode.DiagnosticSeverity.Warning
|
||||
@@ -432,7 +482,10 @@ export class Ra3Diagnostics {
|
||||
document.positionAt(typeAttr.valueStart),
|
||||
document.positionAt(typeAttr.valueEnd),
|
||||
),
|
||||
`Invalid Include type "${typeAttr.value}" (expected reference, instance or all)`,
|
||||
t(
|
||||
'Invalid Include type "{0}" (expected reference, instance or all)',
|
||||
typeAttr.value,
|
||||
),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"include-type",
|
||||
),
|
||||
@@ -462,7 +515,7 @@ export class Ra3Diagnostics {
|
||||
document.positionAt(sourceAttr.valueStart),
|
||||
document.positionAt(sourceAttr.valueEnd),
|
||||
),
|
||||
`Include target not found: ${sourceAttr.value}`,
|
||||
t("Include target not found: {0}", sourceAttr.value),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"include-not-found",
|
||||
),
|
||||
@@ -488,6 +541,34 @@ function localName(tag: string): string {
|
||||
return idx >= 0 ? tag.slice(idx + 1) : tag;
|
||||
}
|
||||
|
||||
function unresolvedReferenceMessage(
|
||||
value: string,
|
||||
anyDef: boolean,
|
||||
refType: string | null,
|
||||
isRef: boolean,
|
||||
): string {
|
||||
if (anyDef) {
|
||||
if (refType) {
|
||||
return t(
|
||||
'Reference "{0}" has no definition of type `{1}` (ids with the same name exist for other types)',
|
||||
value,
|
||||
refType,
|
||||
);
|
||||
}
|
||||
if (isRef) {
|
||||
return t(
|
||||
'Reference "{0}" has no definition of the expected declared type (ids with the same name exist for other types)',
|
||||
value,
|
||||
);
|
||||
}
|
||||
return t(
|
||||
'Reference "{0}" has no matching definition (ids with the same name exist for other types)',
|
||||
value,
|
||||
);
|
||||
}
|
||||
return t('Unresolved reference "{0}" (not found in the current index)', value);
|
||||
}
|
||||
|
||||
function tagRange(document: vscode.TextDocument, el: XmlElement): vscode.Range {
|
||||
return new vscode.Range(
|
||||
document.positionAt(el.start),
|
||||
|
||||
+89
-43
@@ -19,6 +19,7 @@ import {
|
||||
import { scopePathKey, type DocumentScope } from "../indexer/localScope";
|
||||
import { dirname } from "node:path";
|
||||
import { buildSearchPaths, resolveSource } from "../indexer/includeResolver";
|
||||
import { t } from "../localize";
|
||||
|
||||
export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
constructor(private ws: ModWorkspace) {}
|
||||
@@ -68,7 +69,9 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendCodeblock(`<${name}>`, "xml");
|
||||
md.appendMarkdown(
|
||||
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
|
||||
t(
|
||||
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
|
||||
),
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
@@ -76,19 +79,25 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
const info = type ? model.typeInfo(type) : undefined;
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendCodeblock(`<${name}>`, "xml");
|
||||
if (model.isTopLevelElement(name)) md.appendMarkdown(`**Top-level asset element** \n`);
|
||||
if (model.isTopLevelElement(name)) {
|
||||
md.appendMarkdown(`${t("**Top-level asset element**")} \n`);
|
||||
}
|
||||
if (info?.kind === "complex") {
|
||||
if (info.doc) md.appendMarkdown(`${info.doc} \n`);
|
||||
md.appendMarkdown(
|
||||
`Attributes: ${info.attributes.length} · Children: ${info.children.length} \n`,
|
||||
`${t(
|
||||
"Attributes: {0} · Children: {1}",
|
||||
info.attributes.length,
|
||||
info.children.length,
|
||||
)} \n`,
|
||||
);
|
||||
if (info.base) md.appendMarkdown(`Extends: \`${info.base}\``);
|
||||
if (info.base) md.appendMarkdown(t("Extends: `{0}`", info.base));
|
||||
} else if (info?.kind === "simple") {
|
||||
md.appendMarkdown(`Simple type: \`${type}\``);
|
||||
md.appendMarkdown(t("Simple type: `{0}`", type ?? ""));
|
||||
} else if (type) {
|
||||
md.appendMarkdown(`Type: \`${type}\``);
|
||||
md.appendMarkdown(t("Type: `{0}`", type));
|
||||
} else {
|
||||
md.appendMarkdown("Not found in the bundled XSD model.");
|
||||
md.appendMarkdown(t("Not found in the bundled XSD model."));
|
||||
}
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
@@ -104,26 +113,38 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
md.appendCodeblock(`${attrName}=""`, "xml");
|
||||
if (!attr) {
|
||||
if (/^(xmlns|xai:)/.test(attrName)) {
|
||||
md.appendMarkdown(`Namespace/instance attribute.`);
|
||||
md.appendMarkdown(t("Namespace/instance attribute."));
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
if (!model.isXsdElementName(el.name)) {
|
||||
md.appendMarkdown(
|
||||
`XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.`,
|
||||
t(
|
||||
"XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.",
|
||||
),
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
md.appendMarkdown("Unknown attribute for this element.");
|
||||
md.appendMarkdown(t("Unknown attribute for this element."));
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
if (attr.doc) md.appendMarkdown(`${attr.doc} \n`);
|
||||
if (attr.required) md.appendMarkdown(`**Required** \n`);
|
||||
if (attr.refType) md.appendMarkdown(`References assets of type \`${attr.refType}\` \n`);
|
||||
if (attr.required) md.appendMarkdown(`${t("**Required**")} \n`);
|
||||
if (attr.refType) {
|
||||
md.appendMarkdown(
|
||||
`${t("References assets of type `{0}`", attr.refType)} \n`,
|
||||
);
|
||||
}
|
||||
if (attr.enumValues.length)
|
||||
md.appendMarkdown(`Values: \`${attr.enumValues.join("`, `")}\` \n`);
|
||||
if (attr.default != null) md.appendMarkdown(`Default: \`${attr.default}\` \n`);
|
||||
if (attr.allowsDefine) md.appendMarkdown(`May use \`$DEFINE\` constants \n`);
|
||||
md.appendMarkdown(`Type: \`${attr.type ?? "string"}\``);
|
||||
md.appendMarkdown(
|
||||
`${t("Values: `{0}`", attr.enumValues.join("`, `"))} \n`,
|
||||
);
|
||||
if (attr.default != null) {
|
||||
md.appendMarkdown(`${t("Default: `{0}`", attr.default)} \n`);
|
||||
}
|
||||
if (attr.allowsDefine) {
|
||||
md.appendMarkdown(`${t("May use `$DEFINE` constants")} \n`);
|
||||
}
|
||||
md.appendMarkdown(t("Type: `{0}`", attr.type ?? "string"));
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
|
||||
@@ -164,17 +185,19 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
).path
|
||||
: null;
|
||||
if (resolved) {
|
||||
md.appendMarkdown(`**Include source** \n`);
|
||||
md.appendMarkdown(`${t("**Include source**")} \n`);
|
||||
md.appendCodeblock(resolved);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
const cand = idx?.sourceCandidates.find((c) => c.source === value);
|
||||
if (cand) {
|
||||
md.appendMarkdown(`**Include source** \n`);
|
||||
md.appendMarkdown(`${t("**Include source**")} \n`);
|
||||
md.appendCodeblock(cand.path);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
md.appendMarkdown(`Include source: \`${value}\` (not in candidate index)`);
|
||||
md.appendMarkdown(
|
||||
t("Include source: `{0}` (not in candidate index)", value),
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
|
||||
@@ -191,7 +214,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
if (!idx) {
|
||||
if (isReferenceAttributeOfType(elType, attrName)) {
|
||||
md.appendMarkdown(
|
||||
"Index is still building — references cannot be resolved yet.",
|
||||
t("Index is still building — references cannot be resolved yet."),
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
@@ -204,12 +227,14 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
const attrRef = model
|
||||
.attributesOfType(elType)
|
||||
.find((a) => a.name === attrName);
|
||||
const expected = attrRef?.refType
|
||||
? ` of type \`${attrRef.refType}\``
|
||||
: attrRef?.isRef
|
||||
? " of the expected declared type"
|
||||
: "";
|
||||
return this.noDefinitionHover(expected);
|
||||
return this.noDefinitionHover(
|
||||
attrRef?.refType
|
||||
? "typed"
|
||||
: attrRef?.isRef
|
||||
? "untyped"
|
||||
: "generic",
|
||||
attrRef?.refType ?? undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,16 +260,16 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
if (!isReferenceContentType(elType)) return null;
|
||||
if (!idx) {
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown("Index is still building — references cannot be resolved yet.");
|
||||
md.appendMarkdown(
|
||||
t("Index is still building — references cannot be resolved yet."),
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
const targets = resolveContentReferenceTargets(idx, elType, value);
|
||||
if (targets.length) return this.definitionsHover(targets, document);
|
||||
const info = elType ? model.typeInfo(elType) : undefined;
|
||||
const refType = info?.kind === "simple" ? info.refType : null;
|
||||
return this.noDefinitionHover(
|
||||
refType ? ` of type \`${refType}\`` : " of the expected declared type",
|
||||
);
|
||||
return this.noDefinitionHover(refType ? "typed" : "untyped", refType ?? undefined);
|
||||
}
|
||||
|
||||
private defineHover(
|
||||
@@ -254,10 +279,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
if (!defs?.length) return null;
|
||||
const d = defs[0];
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown(`**Define** \`$${d.name}\` \n`);
|
||||
md.appendMarkdown(`${t("**Define** `{0}`", `$${d.name}`)} \n`);
|
||||
md.appendCodeblock(d.value);
|
||||
const rel = relativePath(document, d.file);
|
||||
md.appendMarkdown(`Defined in \`${rel}:${d.line}\``);
|
||||
md.appendMarkdown(t("Defined in `{0}:{1}`", rel, d.line));
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
|
||||
@@ -266,23 +291,44 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
document: vscode.TextDocument,
|
||||
): vscode.Hover {
|
||||
const md2 = new vscode.MarkdownString();
|
||||
md2.appendMarkdown(`**${targets.length} definition${targets.length > 1 ? "s" : ""}** \n`);
|
||||
md2.appendMarkdown(
|
||||
`${targets.length === 1 ? t("**1 definition**") : t("**{0} definitions**", targets.length)} \n`,
|
||||
);
|
||||
for (const { def: d } of targets.slice(0, 8)) {
|
||||
const loc =
|
||||
d.origin === "manifest"
|
||||
? `manifest \`${d.manifestSource ?? d.file}\``
|
||||
: `\`${relativePath(document, d.file)}:${d.line}\``;
|
||||
md2.appendMarkdown(`- \`${d.type}\` · ${loc} \n`);
|
||||
? t("manifest `{0}`", d.manifestSource ?? d.file)
|
||||
: t("`{0}:{1}`", relativePath(document, d.file), d.line);
|
||||
md2.appendMarkdown(`${t("- `{0}` · {1}", d.type, loc)} \n`);
|
||||
}
|
||||
return new vscode.Hover(md2);
|
||||
}
|
||||
|
||||
private noDefinitionHover(expected: string): vscode.Hover {
|
||||
private noDefinitionHover(
|
||||
kind: "typed" | "untyped" | "generic",
|
||||
refType?: string,
|
||||
): vscode.Hover {
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown(
|
||||
`No matching definition${expected} in the current index` +
|
||||
" (may exist in a compiled manifest or vanilla data).",
|
||||
);
|
||||
if (kind === "typed" && refType) {
|
||||
md.appendMarkdown(
|
||||
t(
|
||||
"No matching definition of type `{0}` in the current index (may exist in a compiled manifest or vanilla data).",
|
||||
refType,
|
||||
),
|
||||
);
|
||||
} else if (kind === "untyped") {
|
||||
md.appendMarkdown(
|
||||
t(
|
||||
"No matching definition of the expected declared type in the current index (may exist in a compiled manifest or vanilla data).",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
md.appendMarkdown(
|
||||
t(
|
||||
"No matching definition in the current index (may exist in a compiled manifest or vanilla data).",
|
||||
),
|
||||
);
|
||||
}
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
|
||||
@@ -301,10 +347,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
const lineMap = scope.lineMaps.get(scopePathKey(target.sourceFile));
|
||||
const line = lineMap ? lineMap.positionAt(idAttr.valueStart).line + 1 : 0;
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown(`**Local pipeline id** \`${value}\` \n`);
|
||||
md.appendMarkdown(`${t("**Local pipeline id** `{0}`", value)} \n`);
|
||||
md.appendCodeblock(`<${target.name}>`);
|
||||
const rel = relativePath(document, target.sourceFile);
|
||||
md.appendMarkdown(`Defined in \`${rel}:${line}\``);
|
||||
md.appendMarkdown(t("Defined in `{0}:{1}`", rel, line));
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { scopePathKey, type DocumentScope } from "../indexer/localScope";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { AssetDef, ModIndex } from "../indexer/types";
|
||||
import { t } from "../localize";
|
||||
|
||||
function searchPathsFor(idx: ModIndex): SearchPaths {
|
||||
return buildSearchPaths(idx.sdkDir, idx.projectDir);
|
||||
@@ -404,7 +405,13 @@ export class Ra3DocumentSymbolProvider implements vscode.DocumentSymbolProvider
|
||||
document.positionAt(define.end),
|
||||
);
|
||||
symbols.push(
|
||||
new vscode.DocumentSymbol(`$${name}`, "Define", vscode.SymbolKind.Constant, range, range),
|
||||
new vscode.DocumentSymbol(
|
||||
`$${name}`,
|
||||
t("Define"),
|
||||
vscode.SymbolKind.Constant,
|
||||
range,
|
||||
range,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { relative } from "node:path";
|
||||
import { findElementAt, parseXml } from "../language/xmlParser";
|
||||
import { unreferencedByType } from "../indexer/referenceIndex";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import { t } from "../localize";
|
||||
|
||||
interface TypePickItem extends vscode.QuickPickItem {
|
||||
type: string;
|
||||
@@ -26,7 +27,7 @@ export async function findUnreferencedAssets(
|
||||
const idx = ws.activeIndex();
|
||||
if (!ws.isRa3Workspace() || !idx) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: no index available yet.",
|
||||
t("RA3 Mod XML: no index available yet."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -36,18 +37,18 @@ export async function findUnreferencedAssets(
|
||||
if (!type) {
|
||||
if (!byType.size) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: no unreferenced assets found.",
|
||||
t("RA3 Mod XML: no unreferenced assets found."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const pickedType = await vscode.window.showQuickPick<TypePickItem>(
|
||||
[...byType.entries()].map(([t, defs]) => ({
|
||||
label: t,
|
||||
description: `${defs.length} unreferenced`,
|
||||
type: t,
|
||||
[...byType.entries()].map(([typeName, defs]) => ({
|
||||
label: typeName,
|
||||
description: t("{0} unreferenced", defs.length),
|
||||
type: typeName,
|
||||
})),
|
||||
{
|
||||
placeHolder: "Select an asset type",
|
||||
placeHolder: t("Select an asset type"),
|
||||
matchOnDescription: true,
|
||||
},
|
||||
);
|
||||
@@ -58,7 +59,7 @@ export async function findUnreferencedAssets(
|
||||
const defs = byType.get(type) ?? [];
|
||||
if (!defs.length) {
|
||||
void vscode.window.showInformationMessage(
|
||||
`RA3 Mod XML: no unreferenced ${type} assets found.`,
|
||||
t("RA3 Mod XML: no unreferenced {0} assets found.", type),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -70,7 +71,7 @@ export async function findUnreferencedAssets(
|
||||
line: d.line,
|
||||
})),
|
||||
{
|
||||
placeHolder: `${type}: ${defs.length} unreferenced`,
|
||||
placeHolder: t("{0}: {1} unreferenced", type, defs.length),
|
||||
matchOnDescription: true,
|
||||
},
|
||||
);
|
||||
|
||||
+48
-12
@@ -53,6 +53,10 @@ export interface XmlElement {
|
||||
|
||||
export interface XmlParseError {
|
||||
message: string;
|
||||
/** Stable machine-readable id used by the UI layer for localization. */
|
||||
code: string;
|
||||
/** Dynamic values referenced by the localized message. */
|
||||
params?: Record<string, string>;
|
||||
offset: number;
|
||||
line: number;
|
||||
character: number;
|
||||
@@ -249,9 +253,21 @@ export function parseXml(text: string): XmlDocument {
|
||||
let i = 0;
|
||||
const n = text.length;
|
||||
|
||||
const err = (message: string, offset: number) => {
|
||||
const err = (
|
||||
code: string,
|
||||
message: string,
|
||||
offset: number,
|
||||
params?: Record<string, string>,
|
||||
) => {
|
||||
const pos = lineMap.positionAt(offset);
|
||||
errors.push({ message, offset, line: pos.line, character: pos.character });
|
||||
errors.push({
|
||||
code,
|
||||
message,
|
||||
params,
|
||||
offset,
|
||||
line: pos.line,
|
||||
character: pos.character,
|
||||
});
|
||||
};
|
||||
|
||||
while (i < n) {
|
||||
@@ -261,7 +277,11 @@ export function parseXml(text: string): XmlDocument {
|
||||
// text before the root element - ignore unless it is non-whitespace
|
||||
const between = text.slice(i, lt);
|
||||
if (between.trim() !== "") {
|
||||
err("Content is not allowed before the root element", i);
|
||||
err(
|
||||
"content-before-root",
|
||||
"Content is not allowed before the root element",
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
i = lt;
|
||||
@@ -270,7 +290,7 @@ export function parseXml(text: string): XmlDocument {
|
||||
if (text.startsWith("<!--", i)) {
|
||||
const close = text.indexOf("-->", i + 4);
|
||||
if (close < 0) {
|
||||
err("Unterminated comment", i);
|
||||
err("unterminated-comment", "Unterminated comment", i);
|
||||
break;
|
||||
}
|
||||
i = close + 3;
|
||||
@@ -280,7 +300,7 @@ export function parseXml(text: string): XmlDocument {
|
||||
if (text.startsWith("<![CDATA[", i)) {
|
||||
const close = text.indexOf("]]>", i + 9);
|
||||
if (close < 0) {
|
||||
err("Unterminated CDATA section", i);
|
||||
err("unterminated-cdata", "Unterminated CDATA section", i);
|
||||
break;
|
||||
}
|
||||
i = close + 3;
|
||||
@@ -290,7 +310,7 @@ export function parseXml(text: string): XmlDocument {
|
||||
if (text.startsWith("<!DOCTYPE", i) || text.startsWith("<!doctype", i)) {
|
||||
const close = text.indexOf(">", i);
|
||||
if (close < 0) {
|
||||
err("Unterminated DOCTYPE", i);
|
||||
err("unterminated-doctype", "Unterminated DOCTYPE", i);
|
||||
break;
|
||||
}
|
||||
i = close + 1;
|
||||
@@ -300,7 +320,11 @@ export function parseXml(text: string): XmlDocument {
|
||||
if (text.startsWith("<?", i)) {
|
||||
const close = text.indexOf("?>", i + 2);
|
||||
if (close < 0) {
|
||||
err("Unterminated processing instruction", i);
|
||||
err(
|
||||
"unterminated-processing-instruction",
|
||||
"Unterminated processing instruction",
|
||||
i,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (i === 0 && /^<\?xml\s/i.test(text.slice(i, close + 2))) {
|
||||
@@ -313,15 +337,25 @@ export function parseXml(text: string): XmlDocument {
|
||||
if (text.startsWith("</", i)) {
|
||||
const gt = text.indexOf(">", i + 2);
|
||||
if (gt < 0) {
|
||||
err("Unterminated closing tag", i);
|
||||
err("unterminated-closing-tag", "Unterminated closing tag", i);
|
||||
break;
|
||||
}
|
||||
const name = text.slice(i + 2, gt).trim();
|
||||
const top = stack[stack.length - 1];
|
||||
if (!top) {
|
||||
err(`Unexpected closing tag </${name}>`, i);
|
||||
err(
|
||||
"unexpected-closing-tag",
|
||||
`Unexpected closing tag </${name}>`,
|
||||
i,
|
||||
{ name },
|
||||
);
|
||||
} else if (top.name !== name) {
|
||||
err(`Mismatched closing tag: expected </${top.name}>, found </${name}>`, i);
|
||||
err(
|
||||
"mismatched-closing-tag",
|
||||
`Mismatched closing tag: expected </${top.name}>, found </${name}>`,
|
||||
i,
|
||||
{ expected: top.name, found: name },
|
||||
);
|
||||
// recover: find the matching element on the stack if possible
|
||||
let idx = stack.length - 1;
|
||||
while (idx >= 0 && stack[idx].name !== name) idx--;
|
||||
@@ -343,13 +377,13 @@ export function parseXml(text: string): XmlDocument {
|
||||
}
|
||||
// opening tag
|
||||
if (text[i + 1] === "!" || text[i + 1] === "?") {
|
||||
err("Malformed markup", i);
|
||||
err("malformed-markup", "Malformed markup", i);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const gt = findTagEnd(text, i + 1);
|
||||
if (gt < 0) {
|
||||
err("Unterminated start tag", i);
|
||||
err("unterminated-start-tag", "Unterminated start tag", i);
|
||||
// Recovery while typing: an attribute value whose closing quote has not
|
||||
// been typed yet makes the scanner run to EOF. End the malformed start
|
||||
// tag at the first line break (or EOF) so the rest of the document is
|
||||
@@ -399,7 +433,9 @@ export function parseXml(text: string): XmlDocument {
|
||||
for (const el of stack) {
|
||||
const pos = lineMap.positionAt(el.start);
|
||||
errors.push({
|
||||
code: "element-never-closed",
|
||||
message: `Element <${el.name}> is never closed`,
|
||||
params: { name: el.name },
|
||||
offset: el.start,
|
||||
line: pos.line,
|
||||
character: pos.character,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as vscode from "vscode";
|
||||
|
||||
type L10n = {
|
||||
t(message: string, ...args: Array<string | number | boolean>): string;
|
||||
t(
|
||||
message: string,
|
||||
args: Record<string, string | number | boolean>,
|
||||
): string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Thin wrapper around VS Code's built-in `l10n.t`.
|
||||
*
|
||||
* The fallback keeps the message unchanged when no bundle is loaded (for
|
||||
* example in unit tests or when the extension runs with the default English
|
||||
* locale), so the same call sites work in production and tests.
|
||||
*/
|
||||
export function t(
|
||||
message: string,
|
||||
...args: Array<string | number | boolean>
|
||||
): string {
|
||||
const l10n = (vscode as unknown as { l10n?: L10n }).l10n;
|
||||
if (!l10n?.t) return formatIndexed(message, args);
|
||||
try {
|
||||
return l10n.t(message, ...args);
|
||||
} catch {
|
||||
return formatIndexed(message, args);
|
||||
}
|
||||
}
|
||||
|
||||
/** Named-placeholder variant of {@link t}. */
|
||||
export function tN(
|
||||
message: string,
|
||||
args: Record<string, string | number | boolean>,
|
||||
): string {
|
||||
const l10n = (vscode as unknown as { l10n?: L10n }).l10n;
|
||||
if (!l10n?.t) return formatNamed(message, args);
|
||||
try {
|
||||
return l10n.t(message, args);
|
||||
} catch {
|
||||
return formatNamed(message, args);
|
||||
}
|
||||
}
|
||||
|
||||
function formatIndexed(
|
||||
message: string,
|
||||
args: Array<string | number | boolean>,
|
||||
): string {
|
||||
return message.replace(/\{(\d+)\}/g, (match, index: string) => {
|
||||
const value = args[Number(index)];
|
||||
return value === undefined ? match : String(value);
|
||||
});
|
||||
}
|
||||
|
||||
function formatNamed(
|
||||
message: string,
|
||||
args: Record<string, string | number | boolean>,
|
||||
): string {
|
||||
return message.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name: string) => {
|
||||
const value = args[name];
|
||||
return value === undefined ? match : String(value);
|
||||
});
|
||||
}
|
||||
+50
-25
@@ -5,6 +5,7 @@ import {
|
||||
validateSdkPath,
|
||||
type SdkValidation,
|
||||
} from "./sdk";
|
||||
import { t } from "./localize";
|
||||
|
||||
/**
|
||||
* Non-intrusive SDK path guidance: a status-bar hint plus a one-time prompt
|
||||
@@ -32,7 +33,7 @@ export class SdkSetup {
|
||||
const ws = this.getWs();
|
||||
if (!ws) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: 打开 RA3 Mod 项目后即可配置 SDK 路径。",
|
||||
t("RA3 Mod XML: open an RA3 mod project first to configure the SDK path."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -79,26 +80,35 @@ export class SdkSetup {
|
||||
(detectedValidation.status === "ok" ||
|
||||
detectedValidation.status === "partial")
|
||||
) {
|
||||
const useDetected = t("Use detected path");
|
||||
const chooseManually = t("Choose manually…");
|
||||
const notNow = t("Not now");
|
||||
const pick = await vscode.window.showWarningMessage(
|
||||
`RA3 Mod XML 未找到有效的 SDK 路径。检测到已安装的 SDK:${detectedValidation.path}`,
|
||||
"使用检测到的路径",
|
||||
"手动选择…",
|
||||
"暂时不用",
|
||||
t(
|
||||
"RA3 Mod XML could not find a valid SDK path. Detected installed SDK: {0}",
|
||||
detectedValidation.path,
|
||||
),
|
||||
useDetected,
|
||||
chooseManually,
|
||||
notNow,
|
||||
);
|
||||
if (pick === "使用检测到的路径") {
|
||||
if (pick === useDetected) {
|
||||
await applySdkPath(detectedValidation.path);
|
||||
} else if (pick === "手动选择…") {
|
||||
} else if (pick === chooseManually) {
|
||||
await this.chooseAndApply();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const chooseSdkFolder = t("Choose SDK folder…");
|
||||
const pick = await vscode.window.showWarningMessage(
|
||||
"RA3 Mod XML 需要 RA3 Mod SDK 路径才能启用原版数据、manifest 与跨文件补全/跳转功能。未设置时插件将以项目模式运行。",
|
||||
"选择 SDK 文件夹…",
|
||||
"暂时不用",
|
||||
t(
|
||||
"RA3 Mod XML needs the RA3 Mod SDK path to enable vanilla data, manifests and cross-file completion/navigation. Without it, the extension runs in project-only mode.",
|
||||
),
|
||||
chooseSdkFolder,
|
||||
t("Not now"),
|
||||
);
|
||||
if (pick === "选择 SDK 文件夹…") await this.chooseAndApply();
|
||||
if (pick === chooseSdkFolder) await this.chooseAndApply();
|
||||
}
|
||||
|
||||
private async chooseAndApply(): Promise<void> {
|
||||
@@ -106,17 +116,20 @@ export class SdkSetup {
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: "选择 SDK 根目录",
|
||||
title: "选择 RA3 Mod SDK 根目录(应包含 Schemas/xsd/CnC3Types.xsd)",
|
||||
openLabel: t("Choose SDK root"),
|
||||
title: t(
|
||||
"Choose the RA3 Mod SDK root (should contain Schemas/xsd/CnC3Types.xsd)",
|
||||
),
|
||||
});
|
||||
const dir = picked?.[0]?.fsPath;
|
||||
if (!dir) return;
|
||||
const validation = validateSdkPath(dir);
|
||||
if (validation.status === "missing" || validation.status === "not-sdk") {
|
||||
void vscode.window.showErrorMessage(
|
||||
`所选目录不是可用的 RA3 Mod SDK(缺少 ${
|
||||
validation.missing.join("、") || "该目录"
|
||||
})。请重新选择。`,
|
||||
t(
|
||||
"The selected directory is not a usable RA3 Mod SDK (missing {0}). Please choose again.",
|
||||
validation.missing.join(", ") || t("that directory"),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -127,23 +140,35 @@ export class SdkSetup {
|
||||
function statusBarText(validation: SdkValidation): string {
|
||||
if (validation.status === "missing") {
|
||||
return validation.path
|
||||
? "$(warning) RA3 XML: SDK 路径不存在"
|
||||
: "$(warning) RA3 XML: 未设置 SDK";
|
||||
? t("$(warning) RA3 XML: SDK path does not exist")
|
||||
: t("$(warning) RA3 XML: SDK not configured");
|
||||
}
|
||||
if (validation.status === "not-sdk") return "$(warning) RA3 XML: SDK 路径无效";
|
||||
return "$(warning) RA3 XML: SDK 不完整";
|
||||
if (validation.status === "not-sdk") {
|
||||
return t("$(warning) RA3 XML: SDK path is invalid");
|
||||
}
|
||||
return t("$(warning) RA3 XML: SDK is incomplete");
|
||||
}
|
||||
|
||||
function describeSdkValidation(validation: SdkValidation): string {
|
||||
if (validation.status === "missing") {
|
||||
return validation.path
|
||||
? `ra3modxml.sdkPath 指向的目录不存在:${validation.path}。点击重新设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。`
|
||||
: "未配置 RA3 Mod SDK 路径。点击设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。";
|
||||
? t(
|
||||
"The directory configured in ra3modxml.sdkPath does not exist: {0}. Click to reconfigure, or clear ra3modxml.sdkPath to disable vanilla data features.",
|
||||
validation.path,
|
||||
)
|
||||
: t(
|
||||
"RA3 Mod SDK path is not configured. Click to set it, or clear ra3modxml.sdkPath to disable vanilla data features.",
|
||||
);
|
||||
}
|
||||
if (validation.status === "not-sdk") {
|
||||
return "ra3modxml.sdkPath 指向的目录不是 RA3 Mod SDK 根目录(缺少 Schemas/xsd/CnC3Types.xsd)。点击重新设置。";
|
||||
return t(
|
||||
"The directory configured in ra3modxml.sdkPath is not an RA3 Mod SDK root (missing Schemas/xsd/CnC3Types.xsd). Click to reconfigure.",
|
||||
);
|
||||
}
|
||||
return `RA3 Mod SDK 缺少:${validation.missing.join("、")}。manifest / 原版源码 / SDK 搜索路径等功能不可用。`;
|
||||
return t(
|
||||
"RA3 Mod SDK is missing: {0}. Manifest / vanilla source / SDK search path features are unavailable.",
|
||||
validation.missing.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitlyConfigured(
|
||||
@@ -163,6 +188,6 @@ async function applySdkPath(path: string): Promise<void> {
|
||||
.getConfiguration("ra3modxml")
|
||||
.update("sdkPath", path, vscode.ConfigurationTarget.Global);
|
||||
void vscode.window.showInformationMessage(
|
||||
`RA3 Mod XML: SDK 路径已设置为 ${path},正在重建索引…`,
|
||||
t("RA3 Mod XML: SDK path set to {0}; rebuilding the index…", path),
|
||||
);
|
||||
}
|
||||
|
||||
+99
-28
@@ -38,6 +38,7 @@ import {
|
||||
findProjectRootForFile,
|
||||
findProjectRootUpward,
|
||||
} from "./projectRoot";
|
||||
import { t } from "./localize";
|
||||
|
||||
const REBUILD_DEBOUNCE_MS = 1500;
|
||||
/** Log a disk-cache validation progress line every N validated records. */
|
||||
@@ -463,7 +464,7 @@ export class ModWorkspace {
|
||||
// stamp matches the current disk state, otherwise the fast build
|
||||
// could publish an index built from stale records.
|
||||
const pendingArt = await this.seedRecordsFromDisk(state);
|
||||
this.statusBar.text = "$(sync~spin) RA3 XML: indexing…";
|
||||
this.statusBar.text = t("$(sync~spin) RA3 XML: indexing…");
|
||||
this.statusBar.show();
|
||||
const indexer = new ModIndexer({
|
||||
projectDir: state.root,
|
||||
@@ -488,7 +489,7 @@ export class ModWorkspace {
|
||||
// records before phase B so it can trust the cache instead of
|
||||
// re-scanning 2.6 GB of models.
|
||||
if (!phaseIndex.complete && pendingArt.length) {
|
||||
await this.validateAndSeedCache(state, pendingArt, "art cache");
|
||||
await this.validateAndSeedCache(state, pendingArt, t("art cache"));
|
||||
}
|
||||
});
|
||||
this.publishIndex(state, finalIndex, epochAtStart);
|
||||
@@ -504,12 +505,11 @@ export class ModWorkspace {
|
||||
// Keep the last good snapshot (marked stale) instead of disabling
|
||||
// the extension entirely; a later rebuild can recover.
|
||||
state.index.stale = true;
|
||||
this.statusBar.text =
|
||||
"$(error) RA3 XML: indexing failed (stale index kept)";
|
||||
this.statusBar.text = t("$(error) RA3 XML: indexing failed (stale index kept)");
|
||||
this.statusBar.tooltip = err instanceof Error ? err.message : String(err);
|
||||
this.onIndexUpdate?.();
|
||||
} else {
|
||||
this.statusBar.text = "$(error) RA3 XML: indexing failed";
|
||||
this.statusBar.text = t("$(error) RA3 XML: indexing failed");
|
||||
this.statusBar.tooltip = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
} finally {
|
||||
@@ -612,7 +612,12 @@ export class ModWorkspace {
|
||||
const start = Date.now();
|
||||
let lastLogCount = 0;
|
||||
const onProgress = (done: number): void => {
|
||||
this.statusBar.text = `$(sync~spin) RA3 XML: validating ${label} ${done}/${total}…`;
|
||||
this.statusBar.text = t(
|
||||
"$(sync~spin) RA3 XML: validating {0} {1}/{2}…",
|
||||
label,
|
||||
done,
|
||||
total,
|
||||
);
|
||||
this.statusBar.show();
|
||||
if (done - lastLogCount >= CACHE_PROGRESS_LOG_EVERY) {
|
||||
lastLogCount = done;
|
||||
@@ -699,31 +704,66 @@ export class ModWorkspace {
|
||||
|
||||
/** Human-readable cache status for the `ra3modxml.showCacheReport` command. */
|
||||
async cacheReport(): Promise<string> {
|
||||
const lines: string[] = ["RA3 Mod XML cache report"];
|
||||
lines.push(`Projects: ${this.states.size}`);
|
||||
const lines: string[] = [t("RA3 Mod XML cache report")];
|
||||
lines.push(t("Projects: {0}", this.states.size));
|
||||
for (const st of this.states.values()) {
|
||||
lines.push(`Project: ${st.root}`);
|
||||
lines.push(` builds: #${st.buildCount} (last trigger: ${st.lastTrigger})`);
|
||||
lines.push(` disk cache: ${st.diskCache?.path ?? "not loaded"}`);
|
||||
lines.push(t("Project: {0}", st.root));
|
||||
lines.push(
|
||||
t(" builds: #{0} (last trigger: {1})", st.buildCount, st.lastTrigger),
|
||||
);
|
||||
lines.push(t(" disk cache: {0}", st.diskCache?.path ?? t("not loaded")));
|
||||
if (st.diskCache) {
|
||||
const status = await st.diskCache.status();
|
||||
lines.push(
|
||||
` file: ${status?.exists ? `${(status.sizeBytes / 1024).toFixed(1)} KB` : "missing"}`,
|
||||
t(
|
||||
" file: {0}",
|
||||
status?.exists
|
||||
? t("{0} KB", (status.sizeBytes / 1024).toFixed(1))
|
||||
: t("missing"),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
` last load: file=${st.diskCacheStats.fileExists} keyMatched=${st.diskCacheStats.keyMatched} loaded=${st.diskCacheStats.loaded} validated=${st.diskCacheStats.validated} dropped=${st.diskCacheStats.dropped} (load ${st.diskCacheStats.loadMs}ms, validate ${st.diskCacheStats.validateMs}ms)`,
|
||||
t(
|
||||
" last load: file={0} keyMatched={1} loaded={2} validated={3} dropped={4} (load {5}ms, validate {6}ms)",
|
||||
st.diskCacheStats.fileExists,
|
||||
st.diskCacheStats.keyMatched,
|
||||
st.diskCacheStats.loaded,
|
||||
st.diskCacheStats.validated,
|
||||
st.diskCacheStats.dropped,
|
||||
st.diskCacheStats.loadMs,
|
||||
st.diskCacheStats.validateMs,
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
t(
|
||||
" saved after last build: {0}",
|
||||
st.diskSaved ? t("yes") : t("no"),
|
||||
),
|
||||
);
|
||||
lines.push(` saved after last build: ${st.diskSaved}`);
|
||||
}
|
||||
if (st.index) {
|
||||
const s = st.index.stats;
|
||||
lines.push(
|
||||
` last build: phase=${s.phase} assets=${s.assetCount} snapshotHits=${s.snapshotHits} snapshotFallbacks=${s.snapshotFallbacks} recordsCacheHits=${s.recordsCacheHits} shallowCacheHits=${s.shallowCacheHits}`,
|
||||
t(
|
||||
" last build: phase={0} assets={1} snapshotHits={2} snapshotFallbacks={3} recordsCacheHits={4} shallowCacheHits={5}",
|
||||
s.phase,
|
||||
s.assetCount,
|
||||
s.snapshotHits,
|
||||
s.snapshotFallbacks,
|
||||
s.recordsCacheHits,
|
||||
s.shallowCacheHits,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
`Shared in-memory: ${this.recordsCache.size} record entries · ${this.documentCache.size} documents (${this.documentCache.elements} elements) · ${this.resolveCache.size} include resolutions`,
|
||||
t(
|
||||
"Shared in-memory: {0} record entries · {1} documents ({2} elements) · {3} include resolutions",
|
||||
this.recordsCache.size,
|
||||
this.documentCache.size,
|
||||
this.documentCache.elements,
|
||||
this.resolveCache.size,
|
||||
),
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -760,14 +800,17 @@ export class ModWorkspace {
|
||||
}
|
||||
const building = [...this.states.values()].find((s) => s.building);
|
||||
if (building) {
|
||||
this.statusBar.text = "$(sync~spin) RA3 XML: indexing…";
|
||||
this.statusBar.text = t("$(sync~spin) RA3 XML: indexing…");
|
||||
this.statusBar.show();
|
||||
return;
|
||||
}
|
||||
const st = this.activeState();
|
||||
const idx = st?.index;
|
||||
if (!idx || !st) {
|
||||
this.statusBar.text = `$(symbol-misc) RA3 XML: ${this.states.size} project(s) — open a mod XML to index`;
|
||||
this.statusBar.text = t(
|
||||
"$(symbol-misc) RA3 XML: {0} project(s) — open a mod XML to index",
|
||||
this.states.size,
|
||||
);
|
||||
this.statusBar.tooltip = [...this.states.values()]
|
||||
.map((s) => s.root)
|
||||
.join("\n");
|
||||
@@ -775,16 +818,44 @@ export class ModWorkspace {
|
||||
return;
|
||||
}
|
||||
const s = idx.stats;
|
||||
const stale = idx.stale ? " (stale)" : "";
|
||||
this.statusBar.text = `$(symbol-misc) RA3 XML: ${basename(st.root)} · ${formatCount(s.assetCount)} assets${stale}`;
|
||||
this.statusBar.tooltip =
|
||||
`${st.root}\n` +
|
||||
`${s.indexedFiles} files indexed (${s.parsedFiles} parsed, ${s.shallowScannedFiles} art assets shallow-scanned, ${(s.elapsedMs / 1000).toFixed(1)}s)\n` +
|
||||
`${s.assetCount} assets (${s.manifestAssetCount} from ${s.manifestFiles} manifests)\n` +
|
||||
`${s.referenceCount} reference sites\n` +
|
||||
`${s.defineCount} defines, ${s.streams} streams, ${s.sourceCandidates} include candidates\n` +
|
||||
`Phase: ${s.phase} · Complete: ${s.complete}${stale}\n` +
|
||||
`Disk cache: load ${(st.diskCacheStats.loadMs / 1000).toFixed(1)}s, validate ${(st.diskCacheStats.validateMs / 1000).toFixed(1)}s (${st.diskCacheStats.validated}/${st.diskCacheStats.loaded} ok)`;
|
||||
const stale = idx.stale ? ` ${t("(stale)")}` : "";
|
||||
this.statusBar.text = t(
|
||||
"$(symbol-misc) RA3 XML: {0} · {1} assets{2}",
|
||||
basename(st.root),
|
||||
formatCount(s.assetCount),
|
||||
stale,
|
||||
);
|
||||
this.statusBar.tooltip = [
|
||||
st.root,
|
||||
t(
|
||||
"{0} files indexed ({1} parsed, {2} art assets shallow-scanned, {3}s)",
|
||||
s.indexedFiles,
|
||||
s.parsedFiles,
|
||||
s.shallowScannedFiles,
|
||||
(s.elapsedMs / 1000).toFixed(1),
|
||||
),
|
||||
t(
|
||||
"{0} assets ({1} from {2} manifests)",
|
||||
s.assetCount,
|
||||
s.manifestAssetCount,
|
||||
s.manifestFiles,
|
||||
),
|
||||
t("{0} reference sites", s.referenceCount),
|
||||
t(
|
||||
"{0} defines, {1} streams, {2} include candidates",
|
||||
s.defineCount,
|
||||
s.streams,
|
||||
s.sourceCandidates,
|
||||
),
|
||||
t("Phase: {0} · Complete: {1}{2}", s.phase, s.complete, stale),
|
||||
t(
|
||||
"Disk cache: load {0}s, validate {1}s ({2}/{3} ok)",
|
||||
(st.diskCacheStats.loadMs / 1000).toFixed(1),
|
||||
(st.diskCacheStats.validateMs / 1000).toFixed(1),
|
||||
st.diskCacheStats.validated,
|
||||
st.diskCacheStats.loaded,
|
||||
),
|
||||
].join("\n");
|
||||
this.statusBar.show();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user