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
+9 -1
View File
@@ -1,11 +1,15 @@
> This site is not endorsed by or affiliated with Electronic Arts, or its licensors. Trademarks are the property of their respective owners. Game content and materials copyright Electronic Arts Inc. and its licensors. All Rights Reserved. > This site is not endorsed by or affiliated with Electronic Arts, or its licensors. Trademarks are the property of their respective owners. Game content and materials copyright Electronic Arts Inc. and its licensors. All Rights Reserved.
> >
> RA3 Mod XML is an unofficial fan-made tool. It requires a separately installed RA3 Mod SDK. > RA3 Mod XML is an unofficial player-made tool. It requires a separately installed RA3 Mod SDK.
[**English**](README.md) | [中文](README.zh-CN.md) [**English**](README.md) | [中文](README.zh-CN.md)
# RA3 Mod XML # RA3 Mod XML
> **AI-generated project**
>
> This extension was generated almost entirely by AI. As a result, there may be unexpected bugs or edge cases. Bug reports, corrections, and feedback are very welcome.
A VS Code extension that brings **IntelliSense, navigation, reference tracking, and diagnostics** to XML-based mods for **Command & Conquer: Red Alert 3**. A VS Code extension that brings **IntelliSense, navigation, reference tracking, and diagnostics** to XML-based mods for **Command & Conquer: Red Alert 3**.
It understands the RA3 Mod SDK's XML schema, asset types, references, includes, and vanilla game data — so editing a large mod feels much more like working with a real programming language. It understands the RA3 Mod SDK's XML schema, asset types, references, includes, and vanilla game data — so editing a large mod feels much more like working with a real programming language.
@@ -136,6 +140,10 @@ If the SDK is installed, the extension detects it from the registry and offers
it with one click; otherwise you can set `ra3modxml.sdkPath` manually or use it with one click; otherwise you can set `ra3modxml.sdkPath` manually or use
the `RA3 Mod XML: Configure SDK path…` command. the `RA3 Mod XML: Configure SDK path…` command.
An empty `ra3modxml.sdkPath` is also the default value. Leaving the setting
untouched does **not** suppress the SDK setup hint; only explicitly setting it
to an empty string opts out of SDK features permanently.
## Commands ## Commands
* `RA3 Mod XML: Re-index workspace` * `RA3 Mod XML: Re-index workspace`
+8 -1
View File
@@ -1,11 +1,15 @@
> This site is not endorsed by or affiliated with Electronic Arts, or its licensors. Trademarks are the property of their respective owners. Game content and materials copyright Electronic Arts Inc. and its licensors. All Rights Reserved. > This site is not endorsed by or affiliated with Electronic Arts, or its licensors. Trademarks are the property of their respective owners. Game content and materials copyright Electronic Arts Inc. and its licensors. All Rights Reserved.
> >
> RA3 Mod XML is an unofficial fan-made tool. It requires a separately installed RA3 Mod SDK. > RA3 Mod XML is an unofficial player-made tool. It requires a separately installed RA3 Mod SDK.
[English](README.md) | [**中文**](README.zh-CN.md) [English](README.md) | [**中文**](README.zh-CN.md)
# RA3 Mod XML # RA3 Mod XML
> **AI 生成项目**
>
> 本扩展几乎完全由 AI 生成,因此可能存在意外 bug 或边界情况。欢迎提交 bug 报告、修正与反馈。
一款面向 **《命令与征服:红色警戒 3》** XML 模组的 VS Code 扩展,提供 **IntelliSense、导航、引用追踪与诊断** 能力。 一款面向 **《命令与征服:红色警戒 3》** XML 模组的 VS Code 扩展,提供 **IntelliSense、导航、引用追踪与诊断** 能力。
它理解 RA3 Mod SDK 的 XML schema、资产类型、引用、include 以及原版游戏数据——编辑大型模组的体验会更接近真正的编程语言。 它理解 RA3 Mod SDK 的 XML schema、资产类型、引用、include 以及原版游戏数据——编辑大型模组的体验会更接近真正的编程语言。
@@ -134,6 +138,9 @@
如果已安装 SDK,扩展会从注册表检测到它并提供一键设置;也可以手动设置 如果已安装 SDK,扩展会从注册表检测到它并提供一键设置;也可以手动设置
`ra3modxml.sdkPath`,或使用 `RA3 Mod XML: Configure SDK path…` 命令。 `ra3modxml.sdkPath`,或使用 `RA3 Mod XML: Configure SDK path…` 命令。
`ra3modxml.sdkPath` 的默认值本身就是空字符串。**不修改该设置**不会抑制 SDK
设置提示;只有显式把它设为空字符串,才表示永久禁用 SDK 功能。
## 命令 ## 命令
* `RA3 Mod XML: Re-index workspace`(重新索引工作区) * `RA3 Mod XML: Re-index workspace`(重新索引工作区)
+156
View File
@@ -0,0 +1,156 @@
{
"RA3 Mod XML: caches cleared; rebuilding from scratch…": "RA3 Mod XML: caches cleared; rebuilding from scratch…",
"RA3 Mod XML: index is still building — check the status bar. Most features become available after the XML phase.": "RA3 Mod XML: index is still building — check the status bar. Most features become available after the XML phase.",
"RA3 Mod XML: no index for the active project yet — open a mod XML document to start indexing.": "RA3 Mod XML: no index for the active project yet — open a mod XML document to start indexing.",
"RA3 Mod XML: no index available. Open a workspace that contains Data/Mod.xml, Data/additionalmaps/mapmetadata_*.xml or a mod folder.": "RA3 Mod XML: no index available. Open a workspace that contains Data/Mod.xml, Data/additionalmaps/mapmetadata_*.xml or a mod folder.",
"(stale)": "(stale)",
"RA3 Mod XML index": "RA3 Mod XML index",
"Project: {0}": "Project: {0}",
"Files: {0} ({1} parsed, {2} shallow-scanned, {3} cache hits)": "Files: {0} ({1} parsed, {2} shallow-scanned, {3} cache hits)",
"Assets: {0} ({1} from {2} manifests)": "Assets: {0} ({1} from {2} manifests)",
"References: {0}": "References: {0}",
"Defines: {0} · Streams: {1} · Candidates: {2}": "Defines: {0} · Streams: {1} · Candidates: {2}",
"Phase: {0} · Complete: {1}{2}": "Phase: {0} · Complete: {1}{2}",
"Build #{0} (trigger: {1})": "Build #{0} (trigger: {1})",
"Indexed in {0}s": "Indexed in {0}s",
"XML walk: {0}s · Candidates: {1}s · Art scan: {2}s": "XML walk: {0}s · Candidates: {1}s · Art scan: {2}s",
"0 references": "0 references",
"1 reference": "1 reference",
"{0} references": "{0} references",
"Type: {0}": "Type: {0}",
"RA3 XML · {0}": "RA3 XML · {0}",
"RA3 XML": "RA3 XML",
"Root element of every RA3 asset file": "Root element of every RA3 asset file",
"**Required**": "**Required**",
"References: `{0}`": "References: `{0}`",
"Values: {0}": "Values: {0}",
"Default: `{0}`": "Default: `{0}`",
"Type: `{0}`": "Type: `{0}`",
"Suggest attribute values": "Suggest attribute values",
"Instance join action": "Instance join action",
"Controls how this element merges with the inherited definition: `Replace` or `Remove`.": "Controls how this element merges with the inherited definition: `Replace` or `Remove`.",
"xai namespace": "xai namespace",
"Include type": "Include type",
"xai:joinAction": "xai:joinAction",
"enum": "enum",
"boolean": "boolean",
"Include source": "Include source",
"`{0}` · {1}": "`{0}` · {1}",
"relative": "relative",
"manifest ({0})": "manifest ({0})",
"manifest": "manifest",
"**Type**: {0}": "**Type**: {0}",
"**Source**: {0}": "**Source**: {0}",
"**Origin**: {0}": "**Origin**: {0}",
"Also defined as **{0}** · {1}": "Also defined as **{0}** · {1}",
"{0} · {1}": "{0} · {1}",
"Define": "Define",
"local module": "local module",
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).": "Pipeline-local id in the enclosing GameObject (includes xi:include targets).",
"Suggest content value": "Suggest content value",
"project": "project",
"SDK": "SDK",
"Content is not allowed before the root element": "Content is not allowed before the root element",
"Unterminated comment": "Unterminated comment",
"Unterminated CDATA section": "Unterminated CDATA section",
"Unterminated DOCTYPE": "Unterminated DOCTYPE",
"Unterminated processing instruction": "Unterminated processing instruction",
"Unterminated closing tag": "Unterminated closing tag",
"Unterminated start tag": "Unterminated start tag",
"Malformed markup": "Malformed markup",
"Unexpected closing tag </{0}>": "Unexpected closing tag </{0}>",
"Mismatched closing tag: expected </{0}>, found </{1}>": "Mismatched closing tag: expected </{0}>, found </{1}>",
"Element <{0}> is never closed": "Element <{0}> is never closed",
"Top-level asset <{0}> requires an id attribute": "Top-level asset <{0}> requires an id attribute",
"Unknown element <{0}> (not in the RA3 XSD model)": "Unknown element <{0}> (not in the RA3 XSD model)",
" (based on a partial index)": " (based on a partial index)",
" (index incomplete — may be a false positive)": " (index incomplete — may be a false positive)",
"Include target not found: {0}": "Include target not found: {0}",
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
"**Top-level asset element**": "**Top-level asset element**",
"Attributes: {0} · Children: {1}": "Attributes: {0} · Children: {1}",
"Extends: `{0}`": "Extends: `{0}`",
"Simple type: `{0}`": "Simple type: `{0}`",
"Not found in the bundled XSD model.": "Not found in the bundled XSD model.",
"Namespace/instance attribute.": "Namespace/instance attribute.",
"XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.",
"Unknown attribute for this element.": "Unknown attribute for this element.",
"References assets of type `{0}`": "References assets of type `{0}`",
"Values: `{0}`": "Values: `{0}`",
"May use `$DEFINE` constants": "May use `$DEFINE` constants",
"**Include source**": "**Include source**",
"Include source: `{0}` (not in candidate index)": "Include source: `{0}` (not in candidate index)",
"Index is still building — references cannot be resolved yet.": "Index is still building — references cannot be resolved yet.",
"**Define** `{0}`": "**Define** `{0}`",
"Defined in `{0}:{1}`": "Defined in `{0}:{1}`",
"**1 definition**": "**1 definition**",
"**{0} definitions**": "**{0} definitions**",
"manifest `{0}`": "manifest `{0}`",
"`{0}:{1}`": "`{0}:{1}`",
"- `{0}` · {1}": "- `{0}` · {1}",
"No matching definition of type `{0}` in the current index (may exist in a compiled manifest or vanilla data).": "No matching definition of type `{0}` in the current index (may exist in a compiled manifest or vanilla data).",
"No matching definition of the expected declared type in the current index (may exist in a compiled manifest or vanilla data).": "No matching definition of the expected declared type in the current index (may exist in a compiled manifest or vanilla data).",
"No matching definition in the current index (may exist in a compiled manifest or vanilla data).": "No matching definition in the current index (may exist in a compiled manifest or vanilla data).",
"**Local pipeline id** `{0}`": "**Local pipeline id** `{0}`",
"RA3 Mod XML: no index available yet.": "RA3 Mod XML: no index available yet.",
"RA3 Mod XML: no unreferenced assets found.": "RA3 Mod XML: no unreferenced assets found.",
"{0} unreferenced": "{0} unreferenced",
"Select an asset type": "Select an asset type",
"RA3 Mod XML: no unreferenced {0} assets found.": "RA3 Mod XML: no unreferenced {0} assets found.",
"{0}: {1} unreferenced": "{0}: {1} unreferenced",
"RA3 Mod XML: open an RA3 mod project first to configure the SDK path.": "RA3 Mod XML: open an RA3 mod project first to configure the SDK path.",
"Use detected path": "Use detected path",
"Choose manually…": "Choose manually…",
"Not now": "Not now",
"RA3 Mod XML could not find a valid SDK path. Detected installed SDK: {0}": "RA3 Mod XML could not find a valid SDK path. Detected installed SDK: {0}",
"Choose SDK folder…": "Choose SDK folder…",
"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.": "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.",
"Choose SDK root": "Choose SDK root",
"Choose the RA3 Mod SDK root (should contain Schemas/xsd/CnC3Types.xsd)": "Choose the RA3 Mod SDK root (should contain Schemas/xsd/CnC3Types.xsd)",
"The selected directory is not a usable RA3 Mod SDK (missing {0}). Please choose again.": "The selected directory is not a usable RA3 Mod SDK (missing {0}). Please choose again.",
"that directory": "that directory",
"$(warning) RA3 XML: SDK path does not exist": "$(warning) RA3 XML: SDK path does not exist",
"$(warning) RA3 XML: SDK not configured": "$(warning) RA3 XML: SDK not configured",
"$(warning) RA3 XML: SDK path is invalid": "$(warning) RA3 XML: SDK path is invalid",
"$(warning) RA3 XML: SDK is incomplete": "$(warning) RA3 XML: SDK is incomplete",
"The directory configured in ra3modxml.sdkPath does not exist: {0}. Click to reconfigure, or clear ra3modxml.sdkPath to disable vanilla data features.": "The directory configured in ra3modxml.sdkPath does not exist: {0}. Click to reconfigure, or clear ra3modxml.sdkPath to disable vanilla data features.",
"RA3 Mod SDK path is not configured. Click to set it, or clear ra3modxml.sdkPath to disable vanilla data features.": "RA3 Mod SDK path is not configured. Click to set it, or clear ra3modxml.sdkPath to disable vanilla data features.",
"The directory configured in ra3modxml.sdkPath is not an RA3 Mod SDK root (missing Schemas/xsd/CnC3Types.xsd). Click to reconfigure.": "The directory configured in ra3modxml.sdkPath is not an RA3 Mod SDK root (missing Schemas/xsd/CnC3Types.xsd). Click to reconfigure.",
"RA3 Mod SDK is missing: {0}. Manifest / vanilla source / SDK search path features are unavailable.": "RA3 Mod SDK is missing: {0}. Manifest / vanilla source / SDK search path features are unavailable.",
"RA3 Mod XML: SDK path set to {0}; rebuilding the index…": "RA3 Mod XML: SDK path set to {0}; rebuilding the index…",
"$(sync~spin) RA3 XML: indexing…": "$(sync~spin) RA3 XML: indexing…",
"art cache": "art cache",
"$(error) RA3 XML: indexing failed (stale index kept)": "$(error) RA3 XML: indexing failed (stale index kept)",
"$(error) RA3 XML: indexing failed": "$(error) RA3 XML: indexing failed",
"$(sync~spin) RA3 XML: validating {0} {1}/{2}…": "$(sync~spin) RA3 XML: validating {0} {1}/{2}…",
"RA3 Mod XML cache report": "RA3 Mod XML cache report",
"Projects: {0}": "Projects: {0}",
" builds: #{0} (last trigger: {1})": " builds: #{0} (last trigger: {1})",
" disk cache: {0}": " disk cache: {0}",
"not loaded": "not loaded",
" file: {0}": " file: {0}",
"{0} KB": "{0} KB",
"missing": "missing",
" last load: file={0} keyMatched={1} loaded={2} validated={3} dropped={4} (load {5}ms, validate {6}ms)": " last load: file={0} keyMatched={1} loaded={2} validated={3} dropped={4} (load {5}ms, validate {6}ms)",
" saved after last build: {0}": " saved after last build: {0}",
"yes": "yes",
"no": "no",
" last build: phase={0} assets={1} snapshotHits={2} snapshotFallbacks={3} recordsCacheHits={4} shallowCacheHits={5}": " last build: phase={0} assets={1} snapshotHits={2} snapshotFallbacks={3} recordsCacheHits={4} shallowCacheHits={5}",
"Shared in-memory: {0} record entries · {1} documents ({2} elements) · {3} include resolutions": "Shared in-memory: {0} record entries · {1} documents ({2} elements) · {3} include resolutions",
"$(symbol-misc) RA3 XML: {0} project(s) — open a mod XML to index": "$(symbol-misc) RA3 XML: {0} project(s) — open a mod XML to index",
"$(symbol-misc) RA3 XML: {0} · {1} assets{2}": "$(symbol-misc) RA3 XML: {0} · {1} assets{2}",
"{0} files indexed ({1} parsed, {2} art assets shallow-scanned, {3}s)": "{0} files indexed ({1} parsed, {2} art assets shallow-scanned, {3}s)",
"{0} assets ({1} from {2} manifests)": "{0} assets ({1} from {2} manifests)",
"{0} reference sites": "{0} reference sites",
"{0} defines, {1} streams, {2} include candidates": "{0} defines, {1} streams, {2} include candidates",
"Disk cache: load {0}s, validate {1}s ({2}/{3} ok)": "Disk cache: load {0}s, validate {1}s ({2}/{3} ok)",
"Duplicate id \"{0}\" for <{1}> (also defined on line {2})": "Duplicate id \"{0}\" for <{1}> (also defined on line {2})",
"Duplicate id \"{0}\" for <{1}> (also defined in {2})": "Duplicate id \"{0}\" for <{1}> (also defined in {2})",
"Unknown attribute \"{0}\" for <{1}>": "Unknown attribute \"{0}\" for <{1}>",
"Undefined define \"${0}\"": "Undefined define \"${0}\"",
"Invalid Include type \"{0}\" (expected reference, instance or all)": "Invalid Include type \"{0}\" (expected reference, instance or all)",
"Reference \"{0}\" has no definition of type `{1}` (ids with the same name exist for other types)": "Reference \"{0}\" has no definition of type `{1}` (ids with the same name exist for other types)",
"Reference \"{0}\" has no definition of the expected declared type (ids with the same name exist for other types)": "Reference \"{0}\" has no definition of the expected declared type (ids with the same name exist for other types)",
"Reference \"{0}\" has no matching definition (ids with the same name exist for other types)": "Reference \"{0}\" has no matching definition (ids with the same name exist for other types)",
"Unresolved reference \"{0}\" (not found in the current index)": "Unresolved reference \"{0}\" (not found in the current index)"
}
+156
View File
@@ -0,0 +1,156 @@
{
"RA3 Mod XML: caches cleared; rebuilding from scratch…": "RA3 Mod XML:缓存已清空,正在从零重建…",
"RA3 Mod XML: index is still building — check the status bar. Most features become available after the XML phase.": "RA3 Mod XML:索引仍在构建中——请查看状态栏。大部分功能会在 XML 阶段完成后可用。",
"RA3 Mod XML: no index for the active project yet — open a mod XML document to start indexing.": "RA3 Mod XML:活动项目还没有索引——打开一个 Mod XML 文档即可开始建索引。",
"RA3 Mod XML: no index available. Open a workspace that contains Data/Mod.xml, Data/additionalmaps/mapmetadata_*.xml or a mod folder.": "RA3 Mod XML:当前没有可用索引。请打开包含 Data/Mod.xml、Data/additionalmaps/mapmetadata_*.xml 或 Mod 文件夹的工作区。",
"(stale)": "(已过期)",
"RA3 Mod XML index": "RA3 Mod XML 索引",
"Project: {0}": "项目:{0}",
"Files: {0} ({1} parsed, {2} shallow-scanned, {3} cache hits)": "文件:{0}(解析 {1},浅扫描 {2},缓存命中 {3}",
"Assets: {0} ({1} from {2} manifests)": "资产:{0}(其中 {1} 来自 {2} 个 manifest",
"References: {0}": "引用:{0}",
"Defines: {0} · Streams: {1} · Candidates: {2}": "常量:{0} · 流:{1} · 候选:{2}",
"Phase: {0} · Complete: {1}{2}": "阶段:{0} · 完成:{1}{2}",
"Build #{0} (trigger: {1})": "构建 #{0}(触发原因:{1}",
"Indexed in {0}s": "索引耗时 {0} 秒",
"XML walk: {0}s · Candidates: {1}s · Art scan: {2}s": "XML 遍历:{0} 秒 · 候选扫描:{1} 秒 · 美术扫描:{2} 秒",
"0 references": "0 个引用",
"1 reference": "1 个引用",
"{0} references": "{0} 个引用",
"Type: {0}": "类型:{0}",
"RA3 XML · {0}": "RA3 XML · {0}",
"RA3 XML": "RA3 XML",
"Root element of every RA3 asset file": "每个 RA3 资产文件的根元素",
"**Required**": "**必填**",
"References: `{0}`": "引用:`{0}`",
"Values: {0}": "取值:{0}",
"Default: `{0}`": "默认值:`{0}`",
"Type: `{0}`": "类型:`{0}`",
"Suggest attribute values": "建议属性值",
"Instance join action": "实例合并动作",
"Controls how this element merges with the inherited definition: `Replace` or `Remove`.": "控制该元素如何与继承的定义合并:`Replace` 或 `Remove`。",
"xai namespace": "xai 命名空间",
"Include type": "Include 类型",
"xai:joinAction": "xai:joinAction",
"enum": "枚举",
"boolean": "布尔值",
"Include source": "Include 源",
"`{0}` · {1}": "`{0}` · {1}",
"relative": "相对路径",
"manifest ({0})": "manifest{0}",
"manifest": "manifest",
"**Type**: {0}": "**类型**{0}",
"**Source**: {0}": "**来源**{0}",
"**Origin**: {0}": "**归属**{0}",
"Also defined as **{0}** · {1}": "也定义为 **{0}** · {1}",
"{0} · {1}": "{0} · {1}",
"Define": "定义",
"local module": "局部模块",
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).": "所在 GameObject 中的管线局部 id(包含 xi:include 目标)。",
"Suggest content value": "建议内容值",
"project": "项目",
"SDK": "SDK",
"Content is not allowed before the root element": "根元素之前不允许出现内容",
"Unterminated comment": "注释未结束",
"Unterminated CDATA section": "CDATA 段未结束",
"Unterminated DOCTYPE": "DOCTYPE 未结束",
"Unterminated processing instruction": "处理指令未结束",
"Unterminated closing tag": "结束标签未闭合",
"Unterminated start tag": "开始标签未闭合",
"Malformed markup": "标记格式错误",
"Unexpected closing tag </{0}>": "意外的结束标签 </{0}>",
"Mismatched closing tag: expected </{0}>, found </{1}>": "结束标签不匹配:应为 </{0}>,实际为 </{1}>",
"Element <{0}> is never closed": "元素 <{0}> 从未闭合",
"Top-level asset <{0}> requires an id attribute": "顶层资产 <{0}> 需要 id 属性",
"Unknown element <{0}> (not in the RA3 XSD model)": "未知元素 <{0}>(不在 RA3 XSD 模型中)",
" (based on a partial index)": "(基于部分索引)",
" (index incomplete — may be a false positive)": "(索引不完整——可能是误报)",
"Include target not found: {0}": "找不到 Include 目标:{0}",
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude 元素(W3C XInclude 命名空间)——不属于 RA3 XSD 模型。",
"**Top-level asset element**": "**顶层资产元素**",
"Attributes: {0} · Children: {1}": "属性:{0} · 子元素:{1}",
"Extends: `{0}`": "继承自:`{0}`",
"Simple type: `{0}`": "简单类型:`{0}`",
"Not found in the bundled XSD model.": "内置 XSD 模型中未找到。",
"Namespace/instance attribute.": "命名空间/实例属性。",
"XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude 属性(W3C XInclude 命名空间)——不属于 RA3 XSD 模型。",
"Unknown attribute for this element.": "该元素的未知属性。",
"References assets of type `{0}`": "引用类型为 `{0}` 的资产",
"Values: `{0}`": "取值:`{0}`",
"May use `$DEFINE` constants": "可使用 `$DEFINE` 常量",
"**Include source**": "**Include 源**",
"Include source: `{0}` (not in candidate index)": "Include 源:`{0}`(不在候选索引中)",
"Index is still building — references cannot be resolved yet.": "索引仍在构建中——暂时无法解析引用。",
"**Define** `{0}`": "**常量** `{0}`",
"Defined in `{0}:{1}`": "定义于 `{0}:{1}`",
"**1 definition**": "**1 个定义**",
"**{0} definitions**": "**{0} 个定义**",
"manifest `{0}`": "manifest `{0}`",
"`{0}:{1}`": "`{0}:{1}`",
"- `{0}` · {1}": "- `{0}` · {1}",
"No matching definition of type `{0}` in the current index (may exist in a compiled manifest or vanilla data).": "当前索引中没有类型为 `{0}` 的匹配定义(可能存在于编译后的 manifest 或原版数据中)。",
"No matching definition of the expected declared type in the current index (may exist in a compiled manifest or vanilla data).": "当前索引中没有符合声明类型的匹配定义(可能存在于编译后的 manifest 或原版数据中)。",
"No matching definition in the current index (may exist in a compiled manifest or vanilla data).": "当前索引中没有匹配的定义(可能存在于编译后的 manifest 或原版数据中)。",
"**Local pipeline id** `{0}`": "**局部管线 id** `{0}`",
"RA3 Mod XML: no index available yet.": "RA3 Mod XML:尚无可用索引。",
"RA3 Mod XML: no unreferenced assets found.": "RA3 Mod XML:没有找到未引用的资产。",
"{0} unreferenced": "{0} 个未引用",
"Select an asset type": "选择资产类型",
"RA3 Mod XML: no unreferenced {0} assets found.": "RA3 Mod XML:没有找到未引用的 {0} 类型资产。",
"{0}: {1} unreferenced": "{0}{1} 个未引用",
"RA3 Mod XML: open an RA3 mod project first to configure the SDK path.": "RA3 Mod XML:请先打开 RA3 Mod 项目再配置 SDK 路径。",
"Use detected path": "使用检测到的路径",
"Choose manually…": "手动选择…",
"Not now": "暂时不用",
"RA3 Mod XML could not find a valid SDK path. Detected installed SDK: {0}": "RA3 Mod XML 未找到有效的 SDK 路径。检测到已安装的 SDK{0}",
"Choose SDK folder…": "选择 SDK 文件夹…",
"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.": "RA3 Mod XML 需要 RA3 Mod SDK 路径才能启用原版数据、manifest 与跨文件补全/跳转功能。未设置时插件将以项目模式运行。",
"Choose SDK root": "选择 SDK 根目录",
"Choose the RA3 Mod SDK root (should contain Schemas/xsd/CnC3Types.xsd)": "选择 RA3 Mod SDK 根目录(应包含 Schemas/xsd/CnC3Types.xsd",
"The selected directory is not a usable RA3 Mod SDK (missing {0}). Please choose again.": "所选目录不是可用的 RA3 Mod SDK(缺少 {0})。请重新选择。",
"that directory": "该目录",
"$(warning) RA3 XML: SDK path does not exist": "$(warning) RA3 XMLSDK 路径不存在",
"$(warning) RA3 XML: SDK not configured": "$(warning) RA3 XML:未设置 SDK",
"$(warning) RA3 XML: SDK path is invalid": "$(warning) RA3 XMLSDK 路径无效",
"$(warning) RA3 XML: SDK is incomplete": "$(warning) RA3 XMLSDK 不完整",
"The directory configured in ra3modxml.sdkPath does not exist: {0}. Click to reconfigure, or clear ra3modxml.sdkPath to disable vanilla data features.": "ra3modxml.sdkPath 指向的目录不存在:{0}。点击重新设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。",
"RA3 Mod SDK path is not configured. Click to set it, or clear ra3modxml.sdkPath to disable vanilla data features.": "未配置 RA3 Mod SDK 路径。点击设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。",
"The directory configured in ra3modxml.sdkPath is not an RA3 Mod SDK root (missing Schemas/xsd/CnC3Types.xsd). Click to reconfigure.": "ra3modxml.sdkPath 指向的目录不是 RA3 Mod SDK 根目录(缺少 Schemas/xsd/CnC3Types.xsd)。点击重新设置。",
"RA3 Mod SDK is missing: {0}. Manifest / vanilla source / SDK search path features are unavailable.": "RA3 Mod SDK 缺少:{0}。manifest / 原版源码 / SDK 搜索路径等功能不可用。",
"RA3 Mod XML: SDK path set to {0}; rebuilding the index…": "RA3 Mod XMLSDK 路径已设置为 {0},正在重建索引…",
"$(sync~spin) RA3 XML: indexing…": "$(sync~spin) RA3 XML:正在建索引…",
"art cache": "美术缓存",
"$(error) RA3 XML: indexing failed (stale index kept)": "$(error) RA3 XML:索引失败(保留旧索引)",
"$(error) RA3 XML: indexing failed": "$(error) RA3 XML:索引失败",
"$(sync~spin) RA3 XML: validating {0} {1}/{2}…": "$(sync~spin) RA3 XML:正在校验 {0} {1}/{2}…",
"RA3 Mod XML cache report": "RA3 Mod XML 缓存报告",
"Projects: {0}": "项目数:{0}",
" builds: #{0} (last trigger: {1})": " 构建次数:#{0}(上次触发:{1})",
" disk cache: {0}": " 磁盘缓存:{0}",
"not loaded": "未加载",
" file: {0}": " 文件:{0}",
"{0} KB": "{0} KB",
"missing": "缺失",
" last load: file={0} keyMatched={1} loaded={2} validated={3} dropped={4} (load {5}ms, validate {6}ms)": " 最近加载:file={0} keyMatched={1} loaded={2} validated={3} dropped={4}(加载 {5}ms,校验 {6}ms",
" saved after last build: {0}": " 上次构建后已保存:{0}",
"yes": "是",
"no": "否",
" last build: phase={0} assets={1} snapshotHits={2} snapshotFallbacks={3} recordsCacheHits={4} shallowCacheHits={5}": " 最近构建:阶段={0} 资产={1} snapshotHits={2} snapshotFallbacks={3} recordsCacheHits={4} shallowCacheHits={5}",
"Shared in-memory: {0} record entries · {1} documents ({2} elements) · {3} include resolutions": "共享内存:{0} 条记录 · {1} 个文档({2} 个元素)· {3} 条 include 解析",
"$(symbol-misc) RA3 XML: {0} project(s) — open a mod XML to index": "$(symbol-misc) RA3 XML{0} 个项目——打开一个 Mod XML 开始建索引",
"$(symbol-misc) RA3 XML: {0} · {1} assets{2}": "$(symbol-misc) RA3 XML{0} · {1} 个资产{2}",
"{0} files indexed ({1} parsed, {2} art assets shallow-scanned, {3}s)": "{0} 个文件已索引(解析 {1},浅扫描美术资产 {2},耗时 {3} 秒)",
"{0} assets ({1} from {2} manifests)": "{0} 个资产(其中 {1} 来自 {2} 个 manifest",
"{0} reference sites": "{0} 个引用位置",
"{0} defines, {1} streams, {2} include candidates": "{0} 个常量,{1} 条流,{2} 个 include 候选",
"Disk cache: load {0}s, validate {1}s ({2}/{3} ok)": "磁盘缓存:加载 {0} 秒,校验 {1} 秒({2}/{3} 正常)",
"Duplicate id \"{0}\" for <{1}> (also defined on line {2})": "重复 id \"{0}\"<{1}>(也定义在第 {2} 行)",
"Duplicate id \"{0}\" for <{1}> (also defined in {2})": "重复 id \"{0}\"<{1}>(也定义在 {2}",
"Unknown attribute \"{0}\" for <{1}>": "未知属性 \"{0}\"<{1}>",
"Undefined define \"${0}\"": "未定义常量 \"${0}\"",
"Invalid Include type \"{0}\" (expected reference, instance or all)": "无效的 Include 类型 \"{0}\"(应为 reference、instance 或 all",
"Reference \"{0}\" has no definition of type `{1}` (ids with the same name exist for other types)": "引用 \"{0}\" 没有类型为 `{1}` 的定义(其他类型存在同名 id)",
"Reference \"{0}\" has no definition of the expected declared type (ids with the same name exist for other types)": "引用 \"{0}\" 没有符合声明类型的定义(其他类型存在同名 id)",
"Reference \"{0}\" has no matching definition (ids with the same name exist for other types)": "引用 \"{0}\" 没有匹配的定义(其他类型存在同名 id)",
"Unresolved reference \"{0}\" (not found in the current index)": "无法解析的引用 \"{0}\"(当前索引中未找到)"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ra3-mod-xml", "name": "ra3-mod-xml",
"version": "0.1.18", "version": "0.1.21",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ra3-mod-xml", "name": "ra3-mod-xml",
"version": "0.1.18", "version": "0.1.21",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-xml-parser": "^4.5.0" "fast-xml-parser": "^4.5.0"
+19 -18
View File
@@ -1,10 +1,11 @@
{ {
"name": "ra3-mod-xml", "name": "ra3-mod-xml",
"displayName": "RA3 Mod XML", "displayName": "%ra3modxml.displayName%",
"description": "Red Alert 3 Mod XML tooling: syntax highlighting, completions, reference navigation and diagnostics for SAGE/BinaryAssetBuilder XML.", "description": "%ra3modxml.description%",
"version": "0.1.20", "version": "0.1.21",
"publisher": "ra3-mod-xml", "publisher": "ra3-mod-xml",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"l10n": "./l10n",
"engines": { "engines": {
"vscode": "^1.85.0" "vscode": "^1.85.0"
}, },
@@ -42,17 +43,17 @@
} }
], ],
"configuration": { "configuration": {
"title": "RA3 Mod XML", "title": "%ra3modxml.configuration.title%",
"properties": { "properties": {
"ra3modxml.sdkPath": { "ra3modxml.sdkPath": {
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Path to the RA3 Mod SDK root. Used to resolve DATA:/ART:/AUDIO: includes and to index vanilla SageXml sources. Leave empty to disable vanilla SDK features (project-only mode)." "description": "%ra3modxml.sdkPath.description%"
}, },
"ra3modxml.indexSageXml": { "ra3modxml.indexSageXml": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"description": "Index vanilla game XML sources shipped with the SDK (SageXml) so completions/references can resolve vanilla asset ids." "description": "%ra3modxml.indexSageXml.description%"
}, },
"ra3modxml.reportUnresolvedReferences": { "ra3modxml.reportUnresolvedReferences": {
"type": "string", "type": "string",
@@ -62,12 +63,12 @@
"none" "none"
], ],
"default": "warning", "default": "warning",
"description": "Severity for attribute references (CommandSet=..., inheritFrom=...) that cannot be resolved in the index. The index covers mod XML sources, SDK SageXml sources and compiled manifests." "description": "%ra3modxml.reportUnresolvedReferences.description%"
}, },
"ra3modxml.diagnoseUnknownElements": { "ra3modxml.diagnoseUnknownElements": {
"type": "boolean", "type": "boolean",
"default": true, "default": true,
"description": "Report element/attribute names that are not known from the bundled XSD model." "description": "%ra3modxml.diagnoseUnknownElements.description%"
}, },
"ra3modxml.definitionMode": { "ra3modxml.definitionMode": {
"type": "string", "type": "string",
@@ -76,7 +77,7 @@
"project-only" "project-only"
], ],
"default": "all", "default": "all",
"description": "Go-to-definition candidates: 'all' lists the mod definition together with any vanilla (SDK/manifest) definitions (mod first); 'project-only' jumps directly to the mod definition and skips vanilla candidates when the id is defined in the project." "description": "%ra3modxml.definitionMode.description%"
}, },
"ra3modxml.additionalDataSearchPaths": { "ra3modxml.additionalDataSearchPaths": {
"type": "array", "type": "array",
@@ -84,38 +85,38 @@
"type": "string" "type": "string"
}, },
"default": [], "default": [],
"description": "Extra absolute directories appended to the DATA: search path (checked after the project folders)." "description": "%ra3modxml.additionalDataSearchPaths.description%"
} }
} }
}, },
"commands": [ "commands": [
{ {
"command": "ra3modxml.reindex", "command": "ra3modxml.reindex",
"title": "RA3 Mod XML: Re-index workspace" "title": "%ra3modxml.command.reindex.title%"
}, },
{ {
"command": "ra3modxml.openIndexReport", "command": "ra3modxml.openIndexReport",
"title": "RA3 Mod XML: Show index report" "title": "%ra3modxml.command.openIndexReport.title%"
}, },
{ {
"command": "ra3modxml.clearCache", "command": "ra3modxml.clearCache",
"title": "RA3 Mod XML: Clear caches and rebuild" "title": "%ra3modxml.command.clearCache.title%"
}, },
{ {
"command": "ra3modxml.configureSdkPath", "command": "ra3modxml.configureSdkPath",
"title": "RA3 Mod XML: Configure SDK path…" "title": "%ra3modxml.command.configureSdkPath.title%"
}, },
{ {
"command": "ra3modxml.showCacheReport", "command": "ra3modxml.showCacheReport",
"title": "RA3 Mod XML: Show cache report" "title": "%ra3modxml.command.showCacheReport.title%"
}, },
{ {
"command": "ra3modxml.findUnreferencedAssets", "command": "ra3modxml.findUnreferencedAssets",
"title": "RA3 Mod XML: Find unreferenced assets" "title": "%ra3modxml.command.findUnreferencedAssets.title%"
}, },
{ {
"command": "ra3modxml.findUnreferencedAssetsOfType", "command": "ra3modxml.findUnreferencedAssetsOfType",
"title": "RA3 Mod XML: Find unreferenced assets of this type" "title": "%ra3modxml.command.findUnreferencedAssetsOfType.title%"
} }
] ]
}, },
@@ -134,7 +135,7 @@
"generate-model": "node tools/xsd-to-model.mjs", "generate-model": "node tools/xsd-to-model.mjs",
"pretest": "tsc", "pretest": "tsc",
"test": "node --test \"test/*.test.mjs\"", "test": "node --test \"test/*.test.mjs\"",
"package": "npm run build && vsce package" "package": "npm run build && vsce package --no-rewrite-relative-links"
}, },
"dependencies": { "dependencies": {
"fast-xml-parser": "^4.5.0" "fast-xml-parser": "^4.5.0"
+18
View File
@@ -0,0 +1,18 @@
{
"ra3modxml.displayName": "RA3 Mod XML",
"ra3modxml.description": "Red Alert 3 Mod XML tooling: syntax highlighting, completions, reference navigation and diagnostics for SAGE/BinaryAssetBuilder XML.",
"ra3modxml.configuration.title": "RA3 Mod XML",
"ra3modxml.sdkPath.description": "Path to the RA3 Mod SDK root. Used to resolve DATA:/ART:/AUDIO: includes and to index vanilla SageXml sources. Leave empty to disable vanilla SDK features (project-only mode).",
"ra3modxml.indexSageXml.description": "Index vanilla game XML sources shipped with the SDK (SageXml) so completions/references can resolve vanilla asset ids.",
"ra3modxml.reportUnresolvedReferences.description": "Severity for attribute references (CommandSet=..., inheritFrom=...) that cannot be resolved in the index. The index covers mod XML sources, SDK SageXml sources and compiled manifests.",
"ra3modxml.diagnoseUnknownElements.description": "Report element/attribute names that are not known from the bundled XSD model.",
"ra3modxml.definitionMode.description": "Go-to-definition candidates: 'all' lists the mod definition together with any vanilla (SDK/manifest) definitions (mod first); 'project-only' jumps directly to the mod definition and skips vanilla candidates when the id is defined in the project.",
"ra3modxml.additionalDataSearchPaths.description": "Extra absolute directories appended to the DATA: search path (checked after the project folders).",
"ra3modxml.command.reindex.title": "RA3 Mod XML: Re-index workspace",
"ra3modxml.command.openIndexReport.title": "RA3 Mod XML: Show index report",
"ra3modxml.command.clearCache.title": "RA3 Mod XML: Clear caches and rebuild",
"ra3modxml.command.configureSdkPath.title": "RA3 Mod XML: Configure SDK path…",
"ra3modxml.command.showCacheReport.title": "RA3 Mod XML: Show cache report",
"ra3modxml.command.findUnreferencedAssets.title": "RA3 Mod XML: Find unreferenced assets…",
"ra3modxml.command.findUnreferencedAssetsOfType.title": "RA3 Mod XML: Find unreferenced assets of this type"
}
+18
View File
@@ -0,0 +1,18 @@
{
"ra3modxml.displayName": "RA3 Mod XML",
"ra3modxml.description": "红警 3 Mod XML 工具:为 SAGE/BinaryAssetBuilder XML 提供语法高亮、自动补全、引用跳转与诊断。",
"ra3modxml.configuration.title": "RA3 Mod XML",
"ra3modxml.sdkPath.description": "RA3 Mod SDK 根目录路径。用于解析 DATA:/ART:/AUDIO: include,并索引原版 SageXml 源码。留空可禁用 SDK 原版功能(仅项目模式)。",
"ra3modxml.indexSageXml.description": "索引 SDK 附带的原版游戏 XML 源码(SageXml),使补全/引用可以解析原版资产 id。",
"ra3modxml.reportUnresolvedReferences.description": "索引中无法解析的属性引用(CommandSet=...、inheritFrom=...)的诊断级别。索引覆盖 mod XML 源码、SDK SageXml 源码和编译后的 manifest。",
"ra3modxml.diagnoseUnknownElements.description": "报告不在内置 XSD 模型中的元素/属性名。",
"ra3modxml.definitionMode.description": "跳转定义候选项:'all' 同时列出 mod 定义与原版(SDK/manifest)定义(mod 优先);'project-only' 在项目内已定义该 id 时直接跳转 mod 定义并跳过原版候选。",
"ra3modxml.additionalDataSearchPaths.description": "追加到 DATA: 搜索路径末尾的额外绝对目录(在项目文件夹之后检查)。",
"ra3modxml.command.reindex.title": "RA3 Mod XML: 重建索引",
"ra3modxml.command.openIndexReport.title": "RA3 Mod XML: 显示索引报告",
"ra3modxml.command.clearCache.title": "RA3 Mod XML: 清空缓存并重建",
"ra3modxml.command.configureSdkPath.title": "RA3 Mod XML: 配置 SDK 路径…",
"ra3modxml.command.showCacheReport.title": "RA3 Mod XML: 显示缓存报告",
"ra3modxml.command.findUnreferencedAssets.title": "RA3 Mod XML: 查找未引用的资产…",
"ra3modxml.command.findUnreferencedAssetsOfType.title": "RA3 Mod XML: 查找该类型的未引用资产"
}
+45 -16
View File
@@ -20,6 +20,7 @@ import {
Ra3SemanticTokensProvider, Ra3SemanticTokensProvider,
RA3_SEMANTIC_TOKENS_LEGEND, RA3_SEMANTIC_TOKENS_LEGEND,
} from "./features/semanticTokens"; } from "./features/semanticTokens";
import { t } from "./localize";
const XML_SELECTOR: vscode.DocumentSelector = [{ language: "xml" }]; const XML_SELECTOR: vscode.DocumentSelector = [{ language: "xml" }];
/** Safety-net refresh interval while a rebuild is running. */ /** 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", () => { vscode.commands.registerCommand("ra3modxml.clearCache", () => {
ws.clearCaches(); ws.clearCaches();
void vscode.window.showInformationMessage( 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 (!idx) {
if (ws.isBuilding) { if (ws.isBuilding) {
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
"RA3 Mod XML: index is still building — check the status bar. " + t(
"Most features become available after the XML phase.", "RA3 Mod XML: index is still building — check the status bar. Most features become available after the XML phase.",
),
); );
return; return;
} }
if (ws.getProjectRoots().length) { if (ws.getProjectRoots().length) {
void vscode.window.showInformationMessage( 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; return;
} }
void vscode.window.showInformationMessage( 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; return;
} }
const s = idx.stats; const s = idx.stats;
const stale = idx.stale ? " (stale)" : ""; const stale = idx.stale ? ` ${t("(stale)")}` : "";
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
`RA3 Mod XML index\n` + [
`Project: ${s.projectDir}\n` + t("RA3 Mod XML index"),
`Files: ${s.indexedFiles} (${s.parsedFiles} parsed, ${s.shallowScannedFiles} shallow-scanned, ${s.shallowCacheHits + s.recordsCacheHits} cache hits)\n` + t("Project: {0}", s.projectDir),
`Assets: ${s.assetCount} (${s.manifestAssetCount} from ${s.manifestFiles} manifests)\n` + t(
`References: ${s.referenceCount}\n` + "Files: {0} ({1} parsed, {2} shallow-scanned, {3} cache hits)",
`Defines: ${s.defineCount} · Streams: ${s.streams} · Candidates: ${s.sourceCandidates}\n` + s.indexedFiles,
`Phase: ${s.phase} · Complete: ${s.complete}${stale}\n` + s.parsedFiles,
`Build #${ws.buildCount} (trigger: ${ws.lastTrigger})\n` + s.shallowScannedFiles,
`Indexed in ${(s.elapsedMs / 1000).toFixed(1)}s\n` + s.shallowCacheHits + s.recordsCacheHits,
`XML walk: ${(s.walkMs / 1000).toFixed(1)}s · Candidates: ${(s.candidatesMs / 1000).toFixed(1)}s · Art scan: ${(s.artScanMs / 1000).toFixed(1)}s`, ),
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 }, { modal: false },
); );
}), }),
+4 -3
View File
@@ -10,6 +10,7 @@ import {
type ShowReferencesArgs, type ShowReferencesArgs,
} from "./references"; } from "./references";
import type { ModWorkspace } from "../workspace"; import type { ModWorkspace } from "../workspace";
import { t } from "../localize";
/** Never build a DOM for huge files just to show counts (w3x safety). */ /** Never build a DOM for huge files just to show counts (w3x safety). */
const MAX_CODELENS_TEXT = 4 * 1024 * 1024; const MAX_CODELENS_TEXT = 4 * 1024 * 1024;
@@ -124,10 +125,10 @@ export class Ra3CodeLensProvider implements vscode.CodeLensProvider {
new vscode.CodeLens(range, { new vscode.CodeLens(range, {
title: title:
count === 0 count === 0
? "0 references" ? t("0 references")
: count === 1 : count === 1
? "1 reference" ? t("1 reference")
: `${count} references`, : t("{0} references", count),
command: "ra3modxml.showReferences", command: "ra3modxml.showReferences",
arguments: [args], arguments: [args],
}), }),
+90 -32
View File
@@ -17,6 +17,7 @@ import {
} from "../indexer/logicalTree"; } from "../indexer/logicalTree";
import type { ModWorkspace } from "../workspace"; import type { ModWorkspace } from "../workspace";
import type { ModIndex, AssetDef } from "../indexer/types"; import type { ModIndex, AssetDef } from "../indexer/types";
import { t } from "../localize";
const MAX_VALUE_ITEMS = 400; const MAX_VALUE_ITEMS = 400;
@@ -79,9 +80,9 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
const docText = const docText =
child.doc || child.doc ||
(info?.kind === "complex" ? info.doc : "") || (info?.kind === "complex" ? info.doc : "") ||
(type ? `Type: ${type}` : ""); (type ? t("Type: {0}", type) : "");
item.documentation = docText ? new vscode.MarkdownString(docText) : undefined; 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); item.insertText = this.elementSnippet(child.name, type, ctx.element == null);
items.push(item); items.push(item);
} }
@@ -93,7 +94,11 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
): { name: string; type: string | null; doc: string }[] { ): { name: string; type: string | null; doc: string }[] {
if (!parent) { if (!parent) {
return [ 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); const parentType = resolveElementType(parent);
@@ -160,12 +165,16 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
item.sortText = attr.required ? "0" + attr.name : "1" + attr.name; item.sortText = attr.required ? "0" + attr.name : "1" + attr.name;
const md = new vscode.MarkdownString(); const md = new vscode.MarkdownString();
if (attr.doc) md.appendMarkdown(attr.doc + "\n\n"); if (attr.doc) md.appendMarkdown(attr.doc + "\n\n");
if (attr.required) md.appendMarkdown(`**Required** \n`); if (attr.required) md.appendMarkdown(`${t("**Required**")} \n`);
if (attr.refType) md.appendMarkdown(`References: \`${attr.refType}\` \n`); if (attr.refType) {
md.appendMarkdown(`${t("References: `{0}`", attr.refType)} \n`);
}
if (attr.enumValues.length) if (attr.enumValues.length)
md.appendMarkdown(`Values: ${attr.enumValues.join(", ")} \n`); md.appendMarkdown(`${t("Values: {0}", attr.enumValues.join(", "))} \n`);
if (attr.default != null) md.appendMarkdown(`Default: \`${attr.default}\` \n`); if (attr.default != null) {
md.appendMarkdown(`Type: \`${attr.type ?? "string"}\``); md.appendMarkdown(`${t("Default: `{0}`", attr.default)} \n`);
}
md.appendMarkdown(t("Type: `{0}`", attr.type ?? "string"));
item.documentation = md; item.documentation = md;
const value = this.attributeValuePlaceholder(attr, el); const value = this.attributeValuePlaceholder(attr, el);
item.insertText = new vscode.SnippetString( item.insertText = new vscode.SnippetString(
@@ -174,7 +183,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
if (value.trigger) { if (value.trigger) {
item.command = { item.command = {
command: "editor.action.triggerSuggest", command: "editor.action.triggerSuggest",
title: "Suggest attribute values", title: t("Suggest attribute values"),
}; };
} }
items.push(item); items.push(item);
@@ -187,13 +196,15 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
j.insertText = new vscode.SnippetString( j.insertText = new vscode.SnippetString(
layout.prefix + 'xai:joinAction="$1"', layout.prefix + 'xai:joinAction="$1"',
); );
j.detail = "Instance join action"; j.detail = t("Instance join action");
j.documentation = new vscode.MarkdownString( 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 = { j.command = {
command: "editor.action.triggerSuggest", command: "editor.action.triggerSuggest",
title: "Suggest attribute values", title: t("Suggest attribute values"),
}; };
items.push(j); items.push(j);
} }
@@ -203,7 +214,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
ns.insertText = new vscode.SnippetString( ns.insertText = new vscode.SnippetString(
layout.prefix + 'xmlns:xai="uri:ea.com:eala:asset:instance"', layout.prefix + 'xmlns:xai="uri:ea.com:eala:asset:instance"',
); );
ns.detail = "xai namespace"; ns.detail = t("xai namespace");
items.push(ns); items.push(ns);
} }
return items; return items;
@@ -312,7 +323,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
// Include type / source // Include type / source
if (isInclude && attrName === "type") { if (isInclude && attrName === "type") {
return ["reference", "instance", "all"].map((v) => 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") { if (isInclude && attrName === "source") {
@@ -321,7 +332,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
} }
if (attrName === "xai:joinaction" || attrName === "joinaction") { if (attrName === "xai:joinaction" || attrName === "joinaction") {
return ["Replace", "Remove"].map((v) => 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 return attrInfo.enumValues
.filter((v) => v.toLowerCase().startsWith(prefix.toLowerCase())) .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) { if (attrInfo?.isBoolean) {
return ["true", "false"] return ["true", "false"]
.filter((v) => v.startsWith(prefix.toLowerCase())) .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) { if (idx && attrInfo?.allowsDefine) {
return this.defineItems(idx, prefix, make); return this.defineItems(idx, prefix, make);
@@ -412,7 +429,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
make( make(
v, v,
vscode.CompletionItemKind.EnumMember, vscode.CompletionItemKind.EnumMember,
attrInfo.type ?? "enum", attrInfo.type ?? t("enum"),
undefined, undefined,
range, range,
append ? ` ${v}` : v, append ? ` ${v}` : v,
@@ -436,10 +453,14 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
a.source.localeCompare(b.source), a.source.localeCompare(b.source),
); );
const items = candidates.map((c) => { 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.detail = c.path;
item.documentation = new vscode.MarkdownString( item.documentation = new vscode.MarkdownString(
`\`${c.prefix ?? "relative"}\` · ${c.path}`, t("`{0}` · {1}", c.prefix ?? t("relative"), c.path),
); );
return item; return item;
}); });
@@ -520,19 +541,30 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
); );
const items = top.map(({ def }) => { const items = top.map(({ def }) => {
const originLabel = (d: AssetDef) => 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 origin = originLabel(def);
const doc = new vscode.MarkdownString(); const doc = new vscode.MarkdownString();
doc.appendCodeblock(def.id); doc.appendCodeblock(def.id);
doc.appendMarkdown(`**Type**: ${def.type} \n`); doc.appendMarkdown(`${t("**Type**: {0}", def.type)} \n`);
if (def.manifestSource) doc.appendMarkdown(`**Source**: ${def.manifestSource} \n`); if (def.manifestSource) {
doc.appendMarkdown(`**Origin**: ${origin}`); doc.appendMarkdown(`${t("**Source**: {0}", def.manifestSource)} \n`);
}
doc.appendMarkdown(t("**Origin**: {0}", origin));
for (const extra of byId.get(def.id.toLowerCase())?.extras ?? []) { for (const extra of byId.get(def.id.toLowerCase())?.extras ?? []) {
doc.appendMarkdown( 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); return this.limitItems(items, byId.size);
} }
@@ -557,7 +589,12 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
if (seen.has(dedupe)) continue; if (seen.has(dedupe)) continue;
seen.add(dedupe); seen.add(dedupe);
const label = `$${def.name}`; 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; item.insertText = label;
items.push(item); items.push(item);
} }
@@ -580,8 +617,10 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
make( make(
id, id,
vscode.CompletionItemKind.Value, vscode.CompletionItemKind.Value,
"local module", t("local module"),
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).", 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); if (isList) return this.listEnumItems(info, rawPrefix, seg, valueRange, make);
return info.enumValues return info.enumValues
.filter((v) => v.toLowerCase().startsWith(seg.token.toLowerCase())) .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) { if (idx && info.allowsDefine) {
return this.defineItems(idx, seg.token, make); 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); item.insertText = this.elementSnippet(child.name, child.type, !typedOpen);
const type = child.type; const type = child.type;
const info = type ? model.typeInfo(type) : undefined; 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 : ""); const doc = child.doc || (info?.kind === "complex" ? info.doc : "");
if (doc) item.documentation = new vscode.MarkdownString(doc); if (doc) item.documentation = new vscode.MarkdownString(doc);
if (info?.kind === "simple" && this.simpleContentValueKind(info)) { if (info?.kind === "simple" && this.simpleContentValueKind(info)) {
item.command = { item.command = {
command: "editor.action.triggerSuggest", command: "editor.action.triggerSuggest",
title: "Suggest content value", title: t("Suggest content value"),
}; };
} }
items.push(item); items.push(item);
@@ -736,6 +781,19 @@ interface ScoredDef {
score: number; 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 { function compareScoredDefs(a: ScoredDef, b: ScoredDef): number {
return a.score - b.score || a.def.id.localeCompare(b.def.id); 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 * as vscode from "vscode";
import { dirname } from "node:path"; 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 { resolveElementType } from "../language/typeContext";
import { resolveSource, buildSearchPaths } from "../indexer/includeResolver"; import { resolveSource, buildSearchPaths } from "../indexer/includeResolver";
import { validateSdkPath } from "../sdk"; import { validateSdkPath } from "../sdk";
@@ -16,6 +20,7 @@ import {
} from "../indexer/refs"; } from "../indexer/refs";
import type { LogicalElement } from "../indexer/logicalTree"; import type { LogicalElement } from "../indexer/logicalTree";
import { scopePathKey } from "../indexer/localScope"; import { scopePathKey } from "../indexer/localScope";
import { t } from "../localize";
export class Ra3Diagnostics { export class Ra3Diagnostics {
private collection: vscode.DiagnosticCollection; 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),
new vscode.Position(err.line, err.character + 1), new vscode.Position(err.line, err.character + 1),
), ),
err.message, this.parseErrorMessage(err),
vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Error,
"xml-syntax", "xml-syntax",
), ),
@@ -79,6 +84,42 @@ export class Ra3Diagnostics {
this.collection.set(document.uri, diags); 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 { clear(uri: vscode.Uri): void {
this.collection.delete(uri); this.collection.delete(uri);
} }
@@ -120,7 +161,7 @@ export class Ra3Diagnostics {
diags.push( diags.push(
this.diag( this.diag(
range, range,
`Top-level asset <${local}> requires an id attribute`, t("Top-level asset <{0}> requires an id attribute", local),
vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Error,
"missing-id", "missing-id",
), ),
@@ -136,7 +177,12 @@ export class Ra3Diagnostics {
diags.push( diags.push(
this.diag( this.diag(
where, 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, vscode.DiagnosticSeverity.Error,
"duplicate-id", "duplicate-id",
), ),
@@ -166,7 +212,7 @@ export class Ra3Diagnostics {
diags.push( diags.push(
this.diag( this.diag(
range, range,
`Unknown element <${local}> (not in the RA3 XSD model)`, t("Unknown element <{0}> (not in the RA3 XSD model)", local),
vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Warning,
"unknown-element", "unknown-element",
), ),
@@ -187,16 +233,16 @@ export class Ra3Diagnostics {
continue; continue;
} }
if (settings.diagnoseUnknownElements && !knownNames.has(aName)) { if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
diags.push( diags.push(
this.diag( this.diag(
new vscode.Range( new vscode.Range(
document.positionAt(attr.nameStart), document.positionAt(attr.nameStart),
document.positionAt(attr.nameEnd), 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( diags.push(
this.diag( this.diag(
range, range,
`Duplicate id "${id}" for <${type}> (also defined in ${other.file})` + t(
(provisional ? " (based on a partial index)" : ""), 'Duplicate id "{0}" for <{1}> (also defined in {2})',
id,
type,
other.file,
) + (provisional ? t(" (based on a partial index)") : ""),
vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Error,
"duplicate-id", "duplicate-id",
), ),
@@ -296,8 +346,8 @@ export class Ra3Diagnostics {
diags.push( diags.push(
this.diag( this.diag(
range, range,
`Undefined define "$${m[1]}"` + t('Undefined define "${0}"', m[1]) +
(provisional ? " (index incomplete — may be a false positive)" : ""), (provisional ? t(" (index incomplete — may be a false positive)") : ""),
vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Warning,
code, code,
), ),
@@ -318,20 +368,18 @@ export class Ra3Diagnostics {
const attrRef = model const attrRef = model
.attributesOfType(elType) .attributesOfType(elType)
.find((a) => a.name === attrName); .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 code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
const baseMessage = anyDef const baseMessage = unresolvedReferenceMessage(
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)` value,
: `Unresolved reference "${value}" (not found in the current index)`; anyDef,
attrRef?.refType ?? null,
attrRef?.isRef ?? false,
);
diags.push( diags.push(
this.diag( this.diag(
range, range,
provisional provisional
? `${baseMessage} (index incomplete — may be a false positive)` ? baseMessage + t(" (index incomplete — may be a false positive)")
: baseMessage, : baseMessage,
severity === "warning" severity === "warning"
? vscode.DiagnosticSeverity.Warning ? vscode.DiagnosticSeverity.Warning
@@ -379,8 +427,8 @@ export class Ra3Diagnostics {
diags.push( diags.push(
this.diag( this.diag(
range, range,
`Undefined define "$${m[1]}"` + t('Undefined define "${0}"', m[1]) +
(provisional ? " (index incomplete — may be a false positive)" : ""), (provisional ? t(" (index incomplete — may be a false positive)") : ""),
vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Warning,
code, code,
), ),
@@ -398,16 +446,18 @@ export class Ra3Diagnostics {
(idx.local?.assetsById.has(value.toLowerCase()) ?? false) || (idx.local?.assetsById.has(value.toLowerCase()) ?? false) ||
idx.assetsById.has(value.toLowerCase()); idx.assetsById.has(value.toLowerCase());
const refType = info.refType; const refType = info.refType;
const expected = refType ? `of type \`${refType}\`` : "of the expected declared type";
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference"; const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
const baseMessage = anyDef const baseMessage = unresolvedReferenceMessage(
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)` value,
: `Unresolved reference "${value}" (not found in the current index)`; anyDef,
refType ?? null,
!refType,
);
diags.push( diags.push(
this.diag( this.diag(
range, range,
provisional provisional
? `${baseMessage} (index incomplete — may be a false positive)` ? baseMessage + t(" (index incomplete — may be a false positive)")
: baseMessage, : baseMessage,
severity === "warning" severity === "warning"
? vscode.DiagnosticSeverity.Warning ? vscode.DiagnosticSeverity.Warning
@@ -432,7 +482,10 @@ export class Ra3Diagnostics {
document.positionAt(typeAttr.valueStart), document.positionAt(typeAttr.valueStart),
document.positionAt(typeAttr.valueEnd), 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, vscode.DiagnosticSeverity.Error,
"include-type", "include-type",
), ),
@@ -462,7 +515,7 @@ export class Ra3Diagnostics {
document.positionAt(sourceAttr.valueStart), document.positionAt(sourceAttr.valueStart),
document.positionAt(sourceAttr.valueEnd), document.positionAt(sourceAttr.valueEnd),
), ),
`Include target not found: ${sourceAttr.value}`, t("Include target not found: {0}", sourceAttr.value),
vscode.DiagnosticSeverity.Warning, vscode.DiagnosticSeverity.Warning,
"include-not-found", "include-not-found",
), ),
@@ -488,6 +541,34 @@ function localName(tag: string): string {
return idx >= 0 ? tag.slice(idx + 1) : tag; 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 { function tagRange(document: vscode.TextDocument, el: XmlElement): vscode.Range {
return new vscode.Range( return new vscode.Range(
document.positionAt(el.start), document.positionAt(el.start),
+89 -43
View File
@@ -19,6 +19,7 @@ import {
import { scopePathKey, type DocumentScope } from "../indexer/localScope"; import { scopePathKey, type DocumentScope } from "../indexer/localScope";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { buildSearchPaths, resolveSource } from "../indexer/includeResolver"; import { buildSearchPaths, resolveSource } from "../indexer/includeResolver";
import { t } from "../localize";
export class Ra3HoverProvider implements vscode.HoverProvider { export class Ra3HoverProvider implements vscode.HoverProvider {
constructor(private ws: ModWorkspace) {} constructor(private ws: ModWorkspace) {}
@@ -68,7 +69,9 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
const md = new vscode.MarkdownString(); const md = new vscode.MarkdownString();
md.appendCodeblock(`<${name}>`, "xml"); md.appendCodeblock(`<${name}>`, "xml");
md.appendMarkdown( 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); return new vscode.Hover(md);
} }
@@ -76,19 +79,25 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
const info = type ? model.typeInfo(type) : undefined; const info = type ? model.typeInfo(type) : undefined;
const md = new vscode.MarkdownString(); const md = new vscode.MarkdownString();
md.appendCodeblock(`<${name}>`, "xml"); 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?.kind === "complex") {
if (info.doc) md.appendMarkdown(`${info.doc} \n`); if (info.doc) md.appendMarkdown(`${info.doc} \n`);
md.appendMarkdown( 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") { } else if (info?.kind === "simple") {
md.appendMarkdown(`Simple type: \`${type}\``); md.appendMarkdown(t("Simple type: `{0}`", type ?? ""));
} else if (type) { } else if (type) {
md.appendMarkdown(`Type: \`${type}\``); md.appendMarkdown(t("Type: `{0}`", type));
} else { } 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); return new vscode.Hover(md);
} }
@@ -104,26 +113,38 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
md.appendCodeblock(`${attrName}=""`, "xml"); md.appendCodeblock(`${attrName}=""`, "xml");
if (!attr) { if (!attr) {
if (/^(xmlns|xai:)/.test(attrName)) { if (/^(xmlns|xai:)/.test(attrName)) {
md.appendMarkdown(`Namespace/instance attribute.`); md.appendMarkdown(t("Namespace/instance attribute."));
return new vscode.Hover(md); return new vscode.Hover(md);
} }
if (!model.isXsdElementName(el.name)) { if (!model.isXsdElementName(el.name)) {
md.appendMarkdown( 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); 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); return new vscode.Hover(md);
} }
if (attr.doc) md.appendMarkdown(`${attr.doc} \n`); if (attr.doc) md.appendMarkdown(`${attr.doc} \n`);
if (attr.required) md.appendMarkdown(`**Required** \n`); if (attr.required) md.appendMarkdown(`${t("**Required**")} \n`);
if (attr.refType) md.appendMarkdown(`References assets of type \`${attr.refType}\` \n`); if (attr.refType) {
md.appendMarkdown(
`${t("References assets of type `{0}`", attr.refType)} \n`,
);
}
if (attr.enumValues.length) if (attr.enumValues.length)
md.appendMarkdown(`Values: \`${attr.enumValues.join("`, `")}\` \n`); md.appendMarkdown(
if (attr.default != null) md.appendMarkdown(`Default: \`${attr.default}\` \n`); `${t("Values: `{0}`", attr.enumValues.join("`, `"))} \n`,
if (attr.allowsDefine) md.appendMarkdown(`May use \`$DEFINE\` constants \n`); );
md.appendMarkdown(`Type: \`${attr.type ?? "string"}\``); 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); return new vscode.Hover(md);
} }
@@ -164,17 +185,19 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
).path ).path
: null; : null;
if (resolved) { if (resolved) {
md.appendMarkdown(`**Include source** \n`); md.appendMarkdown(`${t("**Include source**")} \n`);
md.appendCodeblock(resolved); md.appendCodeblock(resolved);
return new vscode.Hover(md); return new vscode.Hover(md);
} }
const cand = idx?.sourceCandidates.find((c) => c.source === value); const cand = idx?.sourceCandidates.find((c) => c.source === value);
if (cand) { if (cand) {
md.appendMarkdown(`**Include source** \n`); md.appendMarkdown(`${t("**Include source**")} \n`);
md.appendCodeblock(cand.path); md.appendCodeblock(cand.path);
return new vscode.Hover(md); 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); return new vscode.Hover(md);
} }
@@ -191,7 +214,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
if (!idx) { if (!idx) {
if (isReferenceAttributeOfType(elType, attrName)) { if (isReferenceAttributeOfType(elType, attrName)) {
md.appendMarkdown( 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); return new vscode.Hover(md);
} }
@@ -204,12 +227,14 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
const attrRef = model const attrRef = model
.attributesOfType(elType) .attributesOfType(elType)
.find((a) => a.name === attrName); .find((a) => a.name === attrName);
const expected = attrRef?.refType return this.noDefinitionHover(
? ` of type \`${attrRef.refType}\`` attrRef?.refType
: attrRef?.isRef ? "typed"
? " of the expected declared type" : attrRef?.isRef
: ""; ? "untyped"
return this.noDefinitionHover(expected); : "generic",
attrRef?.refType ?? undefined,
);
} }
} }
@@ -235,16 +260,16 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
if (!isReferenceContentType(elType)) return null; if (!isReferenceContentType(elType)) return null;
if (!idx) { if (!idx) {
const md = new vscode.MarkdownString(); 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); return new vscode.Hover(md);
} }
const targets = resolveContentReferenceTargets(idx, elType, value); const targets = resolveContentReferenceTargets(idx, elType, value);
if (targets.length) return this.definitionsHover(targets, document); if (targets.length) return this.definitionsHover(targets, document);
const info = elType ? model.typeInfo(elType) : undefined; const info = elType ? model.typeInfo(elType) : undefined;
const refType = info?.kind === "simple" ? info.refType : null; const refType = info?.kind === "simple" ? info.refType : null;
return this.noDefinitionHover( return this.noDefinitionHover(refType ? "typed" : "untyped", refType ?? undefined);
refType ? ` of type \`${refType}\`` : " of the expected declared type",
);
} }
private defineHover( private defineHover(
@@ -254,10 +279,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
if (!defs?.length) return null; if (!defs?.length) return null;
const d = defs[0]; const d = defs[0];
const md = new vscode.MarkdownString(); const md = new vscode.MarkdownString();
md.appendMarkdown(`**Define** \`$${d.name}\` \n`); md.appendMarkdown(`${t("**Define** `{0}`", `$${d.name}`)} \n`);
md.appendCodeblock(d.value); md.appendCodeblock(d.value);
const rel = relativePath(document, d.file); 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); return new vscode.Hover(md);
} }
@@ -266,23 +291,44 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
document: vscode.TextDocument, document: vscode.TextDocument,
): vscode.Hover { ): vscode.Hover {
const md2 = new vscode.MarkdownString(); 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)) { for (const { def: d } of targets.slice(0, 8)) {
const loc = const loc =
d.origin === "manifest" d.origin === "manifest"
? `manifest \`${d.manifestSource ?? d.file}\`` ? t("manifest `{0}`", d.manifestSource ?? d.file)
: `\`${relativePath(document, d.file)}:${d.line}\``; : t("`{0}:{1}`", relativePath(document, d.file), d.line);
md2.appendMarkdown(`- \`${d.type}\` · ${loc} \n`); md2.appendMarkdown(`${t("- `{0}` · {1}", d.type, loc)} \n`);
} }
return new vscode.Hover(md2); 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(); const md = new vscode.MarkdownString();
md.appendMarkdown( if (kind === "typed" && refType) {
`No matching definition${expected} in the current index` + md.appendMarkdown(
" (may exist in a compiled manifest or vanilla data).", 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); return new vscode.Hover(md);
} }
@@ -301,10 +347,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
const lineMap = scope.lineMaps.get(scopePathKey(target.sourceFile)); const lineMap = scope.lineMaps.get(scopePathKey(target.sourceFile));
const line = lineMap ? lineMap.positionAt(idAttr.valueStart).line + 1 : 0; const line = lineMap ? lineMap.positionAt(idAttr.valueStart).line + 1 : 0;
const md = new vscode.MarkdownString(); 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}>`); md.appendCodeblock(`<${target.name}>`);
const rel = relativePath(document, target.sourceFile); 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); return new vscode.Hover(md);
} }
} }
+8 -1
View File
@@ -24,6 +24,7 @@ import {
import { scopePathKey, type DocumentScope } from "../indexer/localScope"; import { scopePathKey, type DocumentScope } from "../indexer/localScope";
import type { ModWorkspace } from "../workspace"; import type { ModWorkspace } from "../workspace";
import type { AssetDef, ModIndex } from "../indexer/types"; import type { AssetDef, ModIndex } from "../indexer/types";
import { t } from "../localize";
function searchPathsFor(idx: ModIndex): SearchPaths { function searchPathsFor(idx: ModIndex): SearchPaths {
return buildSearchPaths(idx.sdkDir, idx.projectDir); return buildSearchPaths(idx.sdkDir, idx.projectDir);
@@ -404,7 +405,13 @@ export class Ra3DocumentSymbolProvider implements vscode.DocumentSymbolProvider
document.positionAt(define.end), document.positionAt(define.end),
); );
symbols.push( 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 { findElementAt, parseXml } from "../language/xmlParser";
import { unreferencedByType } from "../indexer/referenceIndex"; import { unreferencedByType } from "../indexer/referenceIndex";
import type { ModWorkspace } from "../workspace"; import type { ModWorkspace } from "../workspace";
import { t } from "../localize";
interface TypePickItem extends vscode.QuickPickItem { interface TypePickItem extends vscode.QuickPickItem {
type: string; type: string;
@@ -26,7 +27,7 @@ export async function findUnreferencedAssets(
const idx = ws.activeIndex(); const idx = ws.activeIndex();
if (!ws.isRa3Workspace() || !idx) { if (!ws.isRa3Workspace() || !idx) {
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
"RA3 Mod XML: no index available yet.", t("RA3 Mod XML: no index available yet."),
); );
return; return;
} }
@@ -36,18 +37,18 @@ export async function findUnreferencedAssets(
if (!type) { if (!type) {
if (!byType.size) { if (!byType.size) {
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
"RA3 Mod XML: no unreferenced assets found.", t("RA3 Mod XML: no unreferenced assets found."),
); );
return; return;
} }
const pickedType = await vscode.window.showQuickPick<TypePickItem>( const pickedType = await vscode.window.showQuickPick<TypePickItem>(
[...byType.entries()].map(([t, defs]) => ({ [...byType.entries()].map(([typeName, defs]) => ({
label: t, label: typeName,
description: `${defs.length} unreferenced`, description: t("{0} unreferenced", defs.length),
type: t, type: typeName,
})), })),
{ {
placeHolder: "Select an asset type", placeHolder: t("Select an asset type"),
matchOnDescription: true, matchOnDescription: true,
}, },
); );
@@ -58,7 +59,7 @@ export async function findUnreferencedAssets(
const defs = byType.get(type) ?? []; const defs = byType.get(type) ?? [];
if (!defs.length) { if (!defs.length) {
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
`RA3 Mod XML: no unreferenced ${type} assets found.`, t("RA3 Mod XML: no unreferenced {0} assets found.", type),
); );
return; return;
} }
@@ -70,7 +71,7 @@ export async function findUnreferencedAssets(
line: d.line, line: d.line,
})), })),
{ {
placeHolder: `${type}: ${defs.length} unreferenced`, placeHolder: t("{0}: {1} unreferenced", type, defs.length),
matchOnDescription: true, matchOnDescription: true,
}, },
); );
+48 -12
View File
@@ -53,6 +53,10 @@ export interface XmlElement {
export interface XmlParseError { export interface XmlParseError {
message: string; 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; offset: number;
line: number; line: number;
character: number; character: number;
@@ -249,9 +253,21 @@ export function parseXml(text: string): XmlDocument {
let i = 0; let i = 0;
const n = text.length; 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); 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) { while (i < n) {
@@ -261,7 +277,11 @@ export function parseXml(text: string): XmlDocument {
// text before the root element - ignore unless it is non-whitespace // text before the root element - ignore unless it is non-whitespace
const between = text.slice(i, lt); const between = text.slice(i, lt);
if (between.trim() !== "") { 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; i = lt;
@@ -270,7 +290,7 @@ export function parseXml(text: string): XmlDocument {
if (text.startsWith("<!--", i)) { if (text.startsWith("<!--", i)) {
const close = text.indexOf("-->", i + 4); const close = text.indexOf("-->", i + 4);
if (close < 0) { if (close < 0) {
err("Unterminated comment", i); err("unterminated-comment", "Unterminated comment", i);
break; break;
} }
i = close + 3; i = close + 3;
@@ -280,7 +300,7 @@ export function parseXml(text: string): XmlDocument {
if (text.startsWith("<![CDATA[", i)) { if (text.startsWith("<![CDATA[", i)) {
const close = text.indexOf("]]>", i + 9); const close = text.indexOf("]]>", i + 9);
if (close < 0) { if (close < 0) {
err("Unterminated CDATA section", i); err("unterminated-cdata", "Unterminated CDATA section", i);
break; break;
} }
i = close + 3; i = close + 3;
@@ -290,7 +310,7 @@ export function parseXml(text: string): XmlDocument {
if (text.startsWith("<!DOCTYPE", i) || text.startsWith("<!doctype", i)) { if (text.startsWith("<!DOCTYPE", i) || text.startsWith("<!doctype", i)) {
const close = text.indexOf(">", i); const close = text.indexOf(">", i);
if (close < 0) { if (close < 0) {
err("Unterminated DOCTYPE", i); err("unterminated-doctype", "Unterminated DOCTYPE", i);
break; break;
} }
i = close + 1; i = close + 1;
@@ -300,7 +320,11 @@ export function parseXml(text: string): XmlDocument {
if (text.startsWith("<?", i)) { if (text.startsWith("<?", i)) {
const close = text.indexOf("?>", i + 2); const close = text.indexOf("?>", i + 2);
if (close < 0) { if (close < 0) {
err("Unterminated processing instruction", i); err(
"unterminated-processing-instruction",
"Unterminated processing instruction",
i,
);
break; break;
} }
if (i === 0 && /^<\?xml\s/i.test(text.slice(i, close + 2))) { 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)) { if (text.startsWith("</", i)) {
const gt = text.indexOf(">", i + 2); const gt = text.indexOf(">", i + 2);
if (gt < 0) { if (gt < 0) {
err("Unterminated closing tag", i); err("unterminated-closing-tag", "Unterminated closing tag", i);
break; break;
} }
const name = text.slice(i + 2, gt).trim(); const name = text.slice(i + 2, gt).trim();
const top = stack[stack.length - 1]; const top = stack[stack.length - 1];
if (!top) { if (!top) {
err(`Unexpected closing tag </${name}>`, i); err(
"unexpected-closing-tag",
`Unexpected closing tag </${name}>`,
i,
{ name },
);
} else if (top.name !== 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 // recover: find the matching element on the stack if possible
let idx = stack.length - 1; let idx = stack.length - 1;
while (idx >= 0 && stack[idx].name !== name) idx--; while (idx >= 0 && stack[idx].name !== name) idx--;
@@ -343,13 +377,13 @@ export function parseXml(text: string): XmlDocument {
} }
// opening tag // opening tag
if (text[i + 1] === "!" || text[i + 1] === "?") { if (text[i + 1] === "!" || text[i + 1] === "?") {
err("Malformed markup", i); err("malformed-markup", "Malformed markup", i);
i++; i++;
continue; continue;
} }
const gt = findTagEnd(text, i + 1); const gt = findTagEnd(text, i + 1);
if (gt < 0) { 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 // Recovery while typing: an attribute value whose closing quote has not
// been typed yet makes the scanner run to EOF. End the malformed start // 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 // 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) { for (const el of stack) {
const pos = lineMap.positionAt(el.start); const pos = lineMap.positionAt(el.start);
errors.push({ errors.push({
code: "element-never-closed",
message: `Element <${el.name}> is never closed`, message: `Element <${el.name}> is never closed`,
params: { name: el.name },
offset: el.start, offset: el.start,
line: pos.line, line: pos.line,
character: pos.character, character: pos.character,
+63
View File
@@ -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
View File
@@ -5,6 +5,7 @@ import {
validateSdkPath, validateSdkPath,
type SdkValidation, type SdkValidation,
} from "./sdk"; } from "./sdk";
import { t } from "./localize";
/** /**
* Non-intrusive SDK path guidance: a status-bar hint plus a one-time prompt * 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(); const ws = this.getWs();
if (!ws) { if (!ws) {
void vscode.window.showInformationMessage( 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; return;
} }
@@ -79,26 +80,35 @@ export class SdkSetup {
(detectedValidation.status === "ok" || (detectedValidation.status === "ok" ||
detectedValidation.status === "partial") 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( 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); await applySdkPath(detectedValidation.path);
} else if (pick === "手动选择…") { } else if (pick === chooseManually) {
await this.chooseAndApply(); await this.chooseAndApply();
} }
return; return;
} }
const chooseSdkFolder = t("Choose SDK folder…");
const pick = await vscode.window.showWarningMessage( const pick = await vscode.window.showWarningMessage(
"RA3 Mod XML 需要 RA3 Mod SDK 路径才能启用原版数据、manifest 与跨文件补全/跳转功能。未设置时插件将以项目模式运行。", t(
"选择 SDK 文件夹…", "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> { private async chooseAndApply(): Promise<void> {
@@ -106,17 +116,20 @@ export class SdkSetup {
canSelectFiles: false, canSelectFiles: false,
canSelectFolders: true, canSelectFolders: true,
canSelectMany: false, canSelectMany: false,
openLabel: "选择 SDK 根目录", openLabel: t("Choose SDK root"),
title: "选择 RA3 Mod SDK 根目录(应包含 Schemas/xsd/CnC3Types.xsd", title: t(
"Choose the RA3 Mod SDK root (should contain Schemas/xsd/CnC3Types.xsd)",
),
}); });
const dir = picked?.[0]?.fsPath; const dir = picked?.[0]?.fsPath;
if (!dir) return; if (!dir) return;
const validation = validateSdkPath(dir); const validation = validateSdkPath(dir);
if (validation.status === "missing" || validation.status === "not-sdk") { if (validation.status === "missing" || validation.status === "not-sdk") {
void vscode.window.showErrorMessage( void vscode.window.showErrorMessage(
`所选目录不是可用的 RA3 Mod SDK(缺少 ${ t(
validation.missing.join("、") || "该目录" "The selected directory is not a usable RA3 Mod SDK (missing {0}). Please choose again.",
})。请重新选择。`, validation.missing.join(", ") || t("that directory"),
),
); );
return; return;
} }
@@ -127,23 +140,35 @@ export class SdkSetup {
function statusBarText(validation: SdkValidation): string { function statusBarText(validation: SdkValidation): string {
if (validation.status === "missing") { if (validation.status === "missing") {
return validation.path return validation.path
? "$(warning) RA3 XML: SDK 路径不存在" ? t("$(warning) RA3 XML: SDK path does not exist")
: "$(warning) RA3 XML: 未设置 SDK"; : t("$(warning) RA3 XML: SDK not configured");
} }
if (validation.status === "not-sdk") return "$(warning) RA3 XML: SDK 路径无效"; if (validation.status === "not-sdk") {
return "$(warning) RA3 XML: SDK 不完整"; return t("$(warning) RA3 XML: SDK path is invalid");
}
return t("$(warning) RA3 XML: SDK is incomplete");
} }
function describeSdkValidation(validation: SdkValidation): string { function describeSdkValidation(validation: SdkValidation): string {
if (validation.status === "missing") { if (validation.status === "missing") {
return validation.path return validation.path
? `ra3modxml.sdkPath 指向的目录不存在:${validation.path}。点击重新设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。` ? t(
: "未配置 RA3 Mod SDK 路径。点击设置;或将 ra3modxml.sdkPath 清空以禁用原版数据功能。"; "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") { 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( function isExplicitlyConfigured(
@@ -163,6 +188,6 @@ async function applySdkPath(path: string): Promise<void> {
.getConfiguration("ra3modxml") .getConfiguration("ra3modxml")
.update("sdkPath", path, vscode.ConfigurationTarget.Global); .update("sdkPath", path, vscode.ConfigurationTarget.Global);
void vscode.window.showInformationMessage( void vscode.window.showInformationMessage(
`RA3 Mod XML: SDK 路径已设置为 ${path},正在重建索引…`, t("RA3 Mod XML: SDK path set to {0}; rebuilding the index…", path),
); );
} }
+99 -28
View File
@@ -38,6 +38,7 @@ import {
findProjectRootForFile, findProjectRootForFile,
findProjectRootUpward, findProjectRootUpward,
} from "./projectRoot"; } from "./projectRoot";
import { t } from "./localize";
const REBUILD_DEBOUNCE_MS = 1500; const REBUILD_DEBOUNCE_MS = 1500;
/** Log a disk-cache validation progress line every N validated records. */ /** 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 // stamp matches the current disk state, otherwise the fast build
// could publish an index built from stale records. // could publish an index built from stale records.
const pendingArt = await this.seedRecordsFromDisk(state); 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(); this.statusBar.show();
const indexer = new ModIndexer({ const indexer = new ModIndexer({
projectDir: state.root, projectDir: state.root,
@@ -488,7 +489,7 @@ export class ModWorkspace {
// records before phase B so it can trust the cache instead of // records before phase B so it can trust the cache instead of
// re-scanning 2.6 GB of models. // re-scanning 2.6 GB of models.
if (!phaseIndex.complete && pendingArt.length) { 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); this.publishIndex(state, finalIndex, epochAtStart);
@@ -504,12 +505,11 @@ export class ModWorkspace {
// Keep the last good snapshot (marked stale) instead of disabling // Keep the last good snapshot (marked stale) instead of disabling
// the extension entirely; a later rebuild can recover. // the extension entirely; a later rebuild can recover.
state.index.stale = true; state.index.stale = true;
this.statusBar.text = this.statusBar.text = t("$(error) RA3 XML: indexing failed (stale index kept)");
"$(error) RA3 XML: indexing failed (stale index kept)";
this.statusBar.tooltip = err instanceof Error ? err.message : String(err); this.statusBar.tooltip = err instanceof Error ? err.message : String(err);
this.onIndexUpdate?.(); this.onIndexUpdate?.();
} else { } 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); this.statusBar.tooltip = err instanceof Error ? err.message : String(err);
} }
} finally { } finally {
@@ -612,7 +612,12 @@ export class ModWorkspace {
const start = Date.now(); const start = Date.now();
let lastLogCount = 0; let lastLogCount = 0;
const onProgress = (done: number): void => { 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(); this.statusBar.show();
if (done - lastLogCount >= CACHE_PROGRESS_LOG_EVERY) { if (done - lastLogCount >= CACHE_PROGRESS_LOG_EVERY) {
lastLogCount = done; lastLogCount = done;
@@ -699,31 +704,66 @@ export class ModWorkspace {
/** Human-readable cache status for the `ra3modxml.showCacheReport` command. */ /** Human-readable cache status for the `ra3modxml.showCacheReport` command. */
async cacheReport(): Promise<string> { async cacheReport(): Promise<string> {
const lines: string[] = ["RA3 Mod XML cache report"]; const lines: string[] = [t("RA3 Mod XML cache report")];
lines.push(`Projects: ${this.states.size}`); lines.push(t("Projects: {0}", this.states.size));
for (const st of this.states.values()) { for (const st of this.states.values()) {
lines.push(`Project: ${st.root}`); lines.push(t("Project: {0}", st.root));
lines.push(` builds: #${st.buildCount} (last trigger: ${st.lastTrigger})`); lines.push(
lines.push(` disk cache: ${st.diskCache?.path ?? "not loaded"}`); 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) { if (st.diskCache) {
const status = await st.diskCache.status(); const status = await st.diskCache.status();
lines.push( 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( 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) { if (st.index) {
const s = st.index.stats; const s = st.index.stats;
lines.push( 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( 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"); return lines.join("\n");
} }
@@ -760,14 +800,17 @@ export class ModWorkspace {
} }
const building = [...this.states.values()].find((s) => s.building); const building = [...this.states.values()].find((s) => s.building);
if (building) { if (building) {
this.statusBar.text = "$(sync~spin) RA3 XML: indexing…"; this.statusBar.text = t("$(sync~spin) RA3 XML: indexing…");
this.statusBar.show(); this.statusBar.show();
return; return;
} }
const st = this.activeState(); const st = this.activeState();
const idx = st?.index; const idx = st?.index;
if (!idx || !st) { 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()] this.statusBar.tooltip = [...this.states.values()]
.map((s) => s.root) .map((s) => s.root)
.join("\n"); .join("\n");
@@ -775,16 +818,44 @@ export class ModWorkspace {
return; return;
} }
const s = idx.stats; const s = idx.stats;
const stale = idx.stale ? " (stale)" : ""; const stale = idx.stale ? ` ${t("(stale)")}` : "";
this.statusBar.text = `$(symbol-misc) RA3 XML: ${basename(st.root)} · ${formatCount(s.assetCount)} assets${stale}`; this.statusBar.text = t(
this.statusBar.tooltip = "$(symbol-misc) RA3 XML: {0} · {1} assets{2}",
`${st.root}\n` + basename(st.root),
`${s.indexedFiles} files indexed (${s.parsedFiles} parsed, ${s.shallowScannedFiles} art assets shallow-scanned, ${(s.elapsedMs / 1000).toFixed(1)}s)\n` + formatCount(s.assetCount),
`${s.assetCount} assets (${s.manifestAssetCount} from ${s.manifestFiles} manifests)\n` + stale,
`${s.referenceCount} reference sites\n` + );
`${s.defineCount} defines, ${s.streams} streams, ${s.sourceCandidates} include candidates\n` + this.statusBar.tooltip = [
`Phase: ${s.phase} · Complete: ${s.complete}${stale}\n` + st.root,
`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)`; 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(); this.statusBar.show();
} }
+36
View File
@@ -0,0 +1,36 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
// Minimal vscode shim without l10n: exercises the fallback path that unit
// tests and extension hosts without a loaded bundle use.
const require = createRequire(import.meta.url);
const Module = require("module");
const origResolve = Module._resolveFilename;
Module._resolveFilename = function (request, ...args) {
if (request === "vscode") return "vscode-stub";
return origResolve.call(this, request, ...args);
};
require.cache["vscode-stub"] = {
id: "vscode-stub",
filename: "vscode-stub",
loaded: true,
exports: {},
};
const { t, tN } = require("../out/localize.js");
test("t fallback substitutes positional placeholders", () => {
assert.equal(t("Hello {0}!", "World"), "Hello World!");
assert.equal(t("{0} and {1}", "A", 2), "A and 2");
assert.equal(t("No placeholders"), "No placeholders");
});
test("tN fallback substitutes named placeholders", () => {
assert.equal(tN("Hello {name}", { name: "Codex" }), "Hello Codex");
assert.equal(
tN("Keep {missing}", {}),
"Keep {missing}",
"unknown placeholders stay untouched",
);
});