This commit is contained in:
2026-08-11 12:01:25 +02:00
parent dbc2c99d8d
commit 2a049c7e92
20 changed files with 1045 additions and 227 deletions
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+8 -1
View File
@@ -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,
),
);
}
}
+10 -9
View File
@@ -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,
},
);