This commit is contained in:
2026-08-11 02:20:06 +02:00
parent 36eaaafa01
commit dbc2c99d8d
45 changed files with 3904 additions and 661 deletions
+252 -63
View File
@@ -325,69 +325,6 @@ hover 同时显示 `No matching definition of the expected declared type...`。
---
## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href`/`xpointer` 误报未知属性
### 问题
AttachTest `Allied Vehicle\Guardian Tank\GameObject.xml` 第 249 行附近:
```xml
<xi:include
href="DATA:Includes/HeadlightDraw2.xml"
xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)"/>
```
报两条 `Unknown attribute "href" / "xpointer" for <include>``unknown-attribute`),
hover 同时显示 `Unknown attribute for this element.`。
这个元素属于 **W3C XInclude 命名空间**`xmlns:xi="http://www.w3.org/2001/XInclude"`),
并不是 EA `uri:ea.com:eala:asset` XSD 的一部分。同一行在第二轮“问题 C”处理过
(嵌套 `xi:include` 的索引与导航),但那轮没有覆盖 unknown-attribute 诊断,属于遗留缺口。
### 根因
诊断的属性校验没有像元素校验那样排除外来命名空间:
- 元素校验已有 `!el.name.startsWith("xi:")` 守卫(所以 `<include>` 本身不报 unknown element);
- 属性校验只跳过 `xmlns*` / `xai:` / `xi:` 前缀的属性名,而 `href`、`xpointer` 是不带
前缀的普通属性名;
- `<xi:include>` 解析类型为 null(XSD 模型不含该元素),knownAttrs 为空 → 任何属性
都被判为 unknown。
### 修复
1. `schemaModel` 新增两个纯函数:
- `isXsdElementName``xi:` 前缀元素不属于 EA XSD 模型;
- `isXsdAttributeName`:EA XSD 属性不带命名空间前缀,带前缀(`xai:`、`xi:`、
`xlink:`、`xml:`、`xsi:`、`xmlns:*`)的都是命名空间机制,不做 schema 校验。
2. `diagnostics``xi:` 前缀元素整体跳过 schema 校验(元素与属性都不再误报);
前缀属性名统一跳过。
3. `hover``xi:include` 元素/属性给出 XInclude 说明;`href` 值悬停像
`<Include source>` 一样解析目标文件(Ctrl+点击跳转此前已可用)。
### 验证
- 真实文件全量扫描:0 未知元素、0 未知属性(修复前 `href`/`xpointer` 两条必现);
- 新增测试:`isXsdElementName` / `isXsdAttributeName` 断言;`xi:include` 解析类型为
null 且不参与校验;全量 39/39 通过。
### 后续(架构方向,待确认)
用户提出“先展开 `xi:include`(类比 C++ 宏展开),再处理 mod XML 解析”。该方向与第二轮
遗留的“虚拟合并”开放项一致,设计要点:
- 构建**逻辑树**而非文本拼接:把目标文件选中内容(`xpointer` 子集)作为子节点拼入父
元素,节点保留源文件与原始偏移,避免文本级拼接导致的偏移断裂;
- 展开范围:`xi:include` 与 EA `<Include type="all">`(内容合并);`instance` /
`reference` 是可见性 / 编译产物语义,不拼树;`inheritFrom` + `joinAction` 是属性级
继承合并,不是宏展开;
- 收益:跨 include 的上下文类型解析、包含内容的结构校验、以及后续“GameObject 内模块
id 局部作用域”(HeadlightDraw2 的模块也是该 GameObject 的模块);
- 风险:include 环 / 深度限制、大文件性能、`xpointer` 仅支持现有子集形式
`/n:Name/child::*`)。
---
## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href` / `xpointer` 被误报为未知属性
### 问题
@@ -1624,3 +1561,255 @@ WarheadTemplate="..."` 引用。该问题在移动硬盘重连 + 重新打开工
只要文件被打开并触发 CodeLens / FAR,不一致就会被检测并定向修复。
版本 **0.1.17 → 0.1.18**。
---
## 二十五、问题分析(2026-08-07):属性补全换行误判与补全项重复
### 现象
1. 属性名补全的“自动换行”在属性已经位于自己单独一行时仍会再插一个换行:
例如在 one-per-line 标签中间插入新属性(光标行已有半截属性名,后面还有
其他属性)时,接受补全会多出一个空行。正确规则是:只有“同一行上的第二个
属性”才换行;光标已经在自己单独一行时不应换行。
2. 属性值补全出现两条完全相同的值,例如 `ProjectileNugget@WarheadTemplate`
中 `AlliedCommandoDesertEaglesWarhead` 出现两遍。
### 根因
1. `attributeInsertLayout` 判断“是否已在新行”时用的是标签内**最后一个完整
属性**的结束位置,而没考虑它是否位于光标之前。在 one-per-line 标签中间
插入时,光标后面的属性会让 `alreadyOnNewLine` 误判为 false,于是再次插入
`\n`。同理,在第一个属性之前的新行上补全也会因“面前没有完整属性”而误换行。
2. `assetIdItems` 只按 `(类型, id, 文件, 行)` 去重。同一个 ID 可以同时出现在
当前文档局部 overlay(未保存文本的行号与磁盘不同)与全局索引中,也可以
同时出现在项目 XML 与编译 manifest 中——不同文件/行号不会被去重,于是同一
个值出现两条。
### 修复
1. **换行判定只看光标之前的属性**:`attributeInsertLayout` 先过滤出结束位置
在光标之前的完整属性,再判断光标是否与它们同处一行;没有前置属性时,用
元素名与光标之间是否有换行判断是否已在自己一行。已在新行时不再插入换行,
one-per-line 风格下仍用规范缩进替换当前行空白;同一行第二个属性仍按原有
规则换行;元素名同一行补首个属性时(文件为 one-per-line)仍保留换行行为。
2. **值补全按 id 去重**`assetIdItems` 改为先按 `(类型, id, 文件, 行)` 去掉
同一份定义,再按 id(大小写不敏感)合并为一个补全项,保留分数最高的定义
local > project > sdk/manifest),其余定义在文档说明中列出
(“Also defined as …”)。`defineItems` 同步改为按 define 名去重,局部定义
优先。
### 验证(169 → 173 全绿)
- one-per-line 标签中间插入:不再插入换行,range 覆盖当前行空白与半截属性名;
- 第一个属性之前的新行补全:不换行,按规范缩进对齐;
- 元素名同一行补首个属性(one-per-line 文件):仍换行;
- 同一 ID 同时存在于局部 overlay / 全局索引 / manifest:只出现一个补全项,
文档中列出其它定义位置。
版本 **0.1.19 → 0.1.20**。
---
## 二十六、问题分析(2026-08-08):磁盘缓存校验阻塞与日志可观测性
### 现象
清缓存 / 冷启动时“validating cache”耗时很长,但输出通道只有构建开始和结束
两行,看不到校验花了多久;构建日志显示 `done in 1.6s`、索引已完整,但 VS Code
状态栏仍停留在“indexing”。
### 根因
1. `seedRecordsFromDisk` 在构建前 `await loadValidated()`,对全部 8,976 条缓存
记录逐文件 stat(机械盘可达数十秒),且这段耗时没有任何日志;构建计时从
`runBuild` 才开始,日志里自然看不到。
2. `publishIndex` 发布最终快照时 `state.building` 仍为 true`updateStatusBar()`
只会显示“indexing…”;`finally` 把 `building` 置 false 后没有再次刷新状态栏,
于是状态栏一直停在 indexing。
### 修复
1. **校验仍是快速构建的前置条件**:`DiskRecordsCache` 拆为 `load()`(读 +
gunzip + JSON,快)与 `validate()`(逐文件 stat)。冷启动**先校验后构建**:
只有 stat 与当前磁盘一致的记录才播种进共享 recordsCache,过期条目在构建时
重新读取。曾尝试“先快速构建、构建后后台校验”,但快速路径
`trustUnchanged=true`)会直接信任未校验的 recordsCache 条目,可能发布
过期 index;后台校验只能事后发现 stat 可见的变化,stat 不可见变化(同
size/mtime/ctime 的重写)无法事后发现。同时并发校验会与索引器抢机械盘
I/O,实测把信任构建从 1.6s 拖到 25swalk 17s / candidates 6.3s),因此
该方案已放弃,恢复“校验通过才允许快速构建”的不变量。
进一步优化为**分阶段校验**:full XML 记录先校验(约一半,~15s),构建随即
开始并发布 phase Ashallow 美术记录先以 `validated:false` 预播种,phase A
的 `readDocument` 只把它当“待扫描登记”用(不消费记录、不 stat 2.6GB 模型),
在 phase A 发布回调里校验并标记 `validated:true`phase B 直接命中缓存。
结果:phase A 可用时间从 ~34s 降到 ~16s,最终完成时间基本不变,正确性不变。
2. **校验进度可视化**`validate()` 增加 `onProgress` 回调,状态栏显示
`validating cache N/M…` / `validating art cache N/M…`,输出通道每校验
1000 条输出一行进度,避免长时间无反馈。
3. **日志补齐计时**:新增 `[disk-cache] loaded … in Xs`、
`[disk-cache] validated … in Xs (dropped=N)`、`[disk-cache] saved … in Xs`
以及 `[build] wall time …`(含缓存加载的总耗时);`DiskCacheLoadStats`
增加 `loadMs` / `validateMs`cacheReport 与状态栏 tooltip 一并展示;
输出通道所有日志行统一自动加本地 `HH:mm:ss.mmm` 时间戳,方便对照
watcher / build / disk-cache 事件的先后顺序。
4. **状态栏修复**`finally` 中 `building = false` 后调用 `updateStatusBar()`
构建完成后不再卡在 indexing;校验阶段状态栏直接显示
`validating cache…` / `validating art cache…` 及计数。
### 验证(173 → 176 全绿)
- `load()` 不做 stat 校验,`validate()` 返回 `kept` / `invalidKeys` 与
validated / dropped / validateMs 统计,`onProgress` 单调递增到总数;
- 变更 / 缺失条目被报告并触发重建,有效条目保留;
- 未校验的 shallow 条目在 phase A 只登记不消费,phase B stat 校验后命中缓存
不再重扫;
- 全量 176 个测试通过,esbuild 产物已更新。
---
## 二十七、问题分析(2026-08-10):CodeLens 与 FAR 定义合并路径不一致
### 现象
Corona 项目中 `WeaponTemplate` 不再显示 CodeLens 引用计数(或显示 0),但
右键菜单 Find All References 仍能查到引用。
### 根因
CodeLens 只查“全局 index 中当前文件这一条定义”的引用桶
`referenceSitesForDefinition`),而 FAR 会把**当前文档的 local overlay** 与
全局同名定义合并后再收集引用(`definitionsForReference` +
`collectReferenceSites`)。当文件未进全局 include 流(standalone / 片段文件)
或定义只存在于 local overlay 时,FAR 能通过全局同名定义找到引用,CodeLens
却按本地文件 key 精确查表得到 0/空,表现就是“FAR 可用、CodeLens 消失”。
### 修复
1. `Ra3CodeLensProvider` 改为 async,通过 `ws.getCodeLensScope(document)` 取
merged index(当前文档 local overlay + 全局 index),并用与 FAR 相同的
`definitionsForReference` + `collectReferenceSites` 计算计数;
2. `showReferencesForDef`(点击 lens 打开的 references peek)同步改为同一
逻辑,保证显示的数字与打开的 peek 严格一致;
3. CodeLens 的 records-desync 自愈改用 `recordsSyncSurfaceFor(document)`
与 FAR 一样按文档所属项目定向修复,而不是活动项目。
### 刷新体验与 0 显示
- CodeLens 使用轻量 `getCodeLensScope`(只解析当前文档 + 挂全局 index,不
展开 include 链),快照发布后的 `editor.action.codeLens.refresh` 即时返回
新计数;
- Provider 实现 `onDidChangeCodeLenses` 事件,`onIndexUpdate` 在每次快照
发布时主动 fire,VS Code 立即重新查询(不依赖 refresh 命令是否生效);
- **全局重试定时器**`onBuildStart` 启动一个 2s 间隔的定时器,只要
`ws.isBuilding()` 为 true 就重新 fire CodeLens 刷新;构建结束即停止。
用于兜底 VS Code 对单次 refresh 事件的合并/延迟,保证 phase A 的计数
不会等到 final 才上屏。定时器为全局单实例、仅构建期存在,不随文档数
放大;
- 日志:每次快照发布记录 `[codelens] refresh (project/phase/assets/...)`
首个快照前每个文档只记一次 `[codelens] suppressed`scope 异常和超过
250ms 的慢 provider 调用也会记录。refresh 事件频率等于快照发布次数
(低频),不会按每次 VS Code 查询记录,避免大项目刷屏;
- 在**第一个全局快照发布前**`stats.indexedFiles === 0` 的本地-only index
不渲染任何 CodeLens,避免冷启动期间满屏误导性的 0 references
- 一旦存在真实快照,“0 references”仍按设计显示(参考目标类型 0 也显示,
点击可打开空 peek 作为“未引用”信号)。
### 重新评估(2026-08-10
- **与 FAR 的一致性**:CodeLens 只渲染当前文档的顶层资产,cheap scope 的
overlay 已覆盖这些资产;反向索引的引用站点挂在**每个匹配定义**上,因此
当前文档定义 + 全局同名定义的并集与 FAR 的 full-scope 并集在计数上一致。
仅存在于 include 链、且不在全局 index 中的定义没有反向引用桶,full scope
也不会多出站点,故不构成计数差异。
- **0 显示语义**:按文档需求“参考目标类型 0 也显示”,隐藏逻辑收窄为
“尚无全局快照”,避免小项目 phase A 后仍被隐藏。
- **已知边界**CodeLens 计数与“从该 id 发起 FAR”完全一致,因此同名 id
跨类型时会把各类型定义的反向站点合并计数——这是 FAR 既有语义,CodeLens
与其保持一致,不再按类型收窄。
### 验证(175 → 178 全绿)
- CodeLens 与 FAR 共享定义合并路径后,standalone 文件中 WeaponTemplate
的计数与 FAR 结果一致;
- 点击 lens 打开的 peek 与计数一致;
- 尚无全局快照时不渲染 CodeLens,快照存在后 0 references 仍显示;
- `onDidChangeCodeLenses` 在 refresh 时触发;测试 shim 不再提供
`indexForDocument`/`activeIndex`,若实现回退到旧的 index 查找方式会直接
测试失败;
- 原有 manifest 源引用归并、0 引用显示、desync 自愈测试全部保持。
---
## 二十八、问题分析(2026-08-10):manifest 源地址被 mod 同名 DATA 路径遮蔽
### 现象
Corona `Data\Allied\Units\AlliedCommandoTech1.xml` 中
`Template="AlliedCommandoDesertEagles"` 的 Ctrl+点击有两个候选:
- mod 定义:正常;
- 原版 manifest 定义:`manifestSource` 是 `DATA:globaldata/weapon.xml`
但它没有跳到 `SageXml\globaldata\weapon.xml`,而是打开 mod 自己的
`Data\globaldata\weapon.xml`915 字节的 Include 汇总文件,不含该 id)。
### 根因
`manifestSource` 记录的是**原版 manifest 编译时该资产的源地址**,不是
“当前 mod 按 BAB Include 规则会命中哪个文件”。旧代码在
`src/features/navigation.ts` 的 `assetDefLocation()` 里用
`resolveSource(src, null, searchPathsFor(idx))` 解析它,而
`searchPathsFor(idx)` 是当前项目的 BAB 搜索顺序——项目 `Data` 在
`SageXml` 之前。于是只要 mod 同名遮蔽了 `DATA:globaldata/weapon.xml`
manifest 候选就会被劫持到 mod 文件。
同一语义混淆也存在于 `src/indexer/referenceIndex.ts` 的
`referenceSitesForDefinition()`:它用当前项目搜索路径判断 manifest 定义
是否对应某个源码文件,同样会被遮蔽路径带偏。
### 实测证据
- `static.manifest` 中确有
`WeaponTemplate:AlliedCommandoDesertEagles`
`sourceFileName = "DATA:globaldata/weapon.xml"`
- `Data\globaldata\weapon.xml`mod)与
`SageXml\globaldata\weapon.xml`(原版)都存在,后者 277 KB,
该 id 在第 1093 行;
- 当前 BAB 顺序解析返回 mod 文件;只按 `[SDK根, SDK\SageXml]` 解析则返回
`SageXml\globaldata\weapon.xml`
- 对 `static.manifest` 全部 DATA 源扫描:1874 个都存在于 `SageXml`
其中 172 个被 Corona `Data` 同名遮蔽。说明这是普遍现象,不是个别文件。
### 修复
1. `src/indexer/includeResolver.ts` 新增 `buildVanillaSearchPaths(sdkDir)`
DATA 只搜 `[SDK根, SDK\SageXml]`ART / AUDIO 同理只搜 SDK 目录
(当前 SDK 基本没有 art/audio 源码,保持“找不到就 manifest-only”)。
2. `src/features/navigation.ts` 的 manifest 定义跳转改用 vanilla-only 路径;
普通 `<Include>` / `xi:include` 仍使用当前项目 BAB 顺序,不受影响。
3. `src/indexer/referenceIndex.ts` 的 manifest 源归并同步改用 vanilla-only
路径,避免把 manifest 引用错误归并到 mod 同名文件。
4. 边界处理:
- `SageXml` 源文件缺失(用户删除/改名):manifest 候选保持
manifest-only,不跳到 mod 遮蔽文件;
- 源文件存在但 id 已被移除(用户修改 SageXml):跳到该文件顶部,
不做虚假的精确定位;
- 只要 `SageXml` 中仍有该 id,就精确跳转(与既有问题 C 的行为一致)。
- 当前实现不读取 `ra3modxml.indexSageXml`manifest 导航始终尝试解析
SageXml 源码(这只影响“跳到哪里”,不影响是否把 SageXml 纳入索引);
如需让导航也跟随该设置,可后续加开关。
### 测试(178 → 184 全绿)
- `includeResolver.test.mjs``buildVanillaSearchPaths` 结构断言;mod 同名
遮蔽时普通 BAB 解析命中 mod、vanilla-only 命中 SageXmlvanilla 源缺失时
即使 mod 遮蔽也返回 null;
- `referenceIndex.test.mjs`manifest 源归并只命中 SageXml 文件,同名 mod
文件不继承引用站点;
- `contentFeatures.test.mjs`Ctrl+点击 manifest 定义命中 SageXml 而非 mod
遮蔽文件;SageXml 源缺失时不跳 mod;文件存在但 id 被删时降到文件顶部。
> 备注:ART/AUDIO 源映射按用户意见不作为本轮目标;`buildVanillaSearchPaths`
> 已包含对应 SDK 目录,将来若有源码可直接复用。
+4 -3
View File
@@ -86,9 +86,10 @@ ModIndex.references Map<定义 key, ReferenceSite[]>
- 点击执行 `ra3modxml.showReferences``editor.action.showReferences`
打开 references peek,结果与计数完全一致(不含定义本身);
- 计数除了当前定义自己的反向索引桶,还并入“manifestSource 可解析到当前
文件”的 manifest 定义桶:manifest 资产有对应 SageXml 源码时,引用直接
视作 SageXml 源码对该 asset 的引用(Go to Definition 同样把 manifest
定义映射到 SageXml 源码);
文件”的 manifest 定义桶:`manifestSource` 按 vanilla-only 搜索路径
SDK 根 + `SageXml`)解析,mod 同名 DATA 路径不会被视为源码;manifest
资产有对应 SageXml 源码时,引用直接视作 SageXml 源码对该 asset 的引用
Go to Definition 同样把 manifest 定义映射到 SageXml 源码);
- 索引重建完成后自动 `editor.action.codeLens.refresh`,计数不会停留在旧值。
## 五、未引用资产
+78 -7
View File
@@ -1,6 +1,6 @@
# 调研结论与实施计划(已按最新代码同步更新)
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-05)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)、simple-content 元素文本引用(补全 / hover / 跳转 / 诊断 / Find All References)、语义引用索引 / CodeLens 引用计数 / 未引用资产命令等。
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-10)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)、simple-content 元素文本引用(补全 / hover / 跳转 / 诊断 / Find All References)、语义引用索引 / CodeLens 引用计数 / 未引用资产命令、属性补全换行判定与按 id 去重、manifest 源地址按 vanilla-only 解析(避免 mod 同名 DATA 路径遮蔽)等。
## 一、调研结论(带证据)
@@ -69,7 +69,10 @@
```
src/
extension.ts 激活入口(provider 注册、索引调度、诊断调度)
workspace.ts 项目检测(Data/Mod.xml / mod.babproj)、索引生命周期、状态栏、重建防抖
projectRoot.ts 项目根发现(向上 / 容器向下 / 单文件,Data/Mod.xml、
mapmetadata_*.xml、*.babproj 标记,纯 TS 可单测)
workspace.ts 多项目状态(按文档就近选项目、惰性索引、全局串行构建队列、
共享缓存、按项目磁盘缓存、watchers、状态栏、重建防抖)
settings.ts 配置读取(sdkPath、indexSageXml、definitionMode 等)
language/
xmlParser.ts 带源码偏移的轻量 XML 解析器(格式错误定位、容错)
@@ -81,7 +84,8 @@ src/
schema-model.json 由 tools/xsd-to-model.mjs 生成
asset-types.json 由 tools/extract-asset-types.mjs 生成(TypeId 哈希→类型名)
indexer/
includeResolver.ts Include 路径解析(纯 TSBAB /data /art /audio 顺序)
includeResolver.ts Include 路径解析(纯 TSBAB /data /art /audio 顺序)+
manifest 源 vanilla-only 解析(SDK 根 + SageXml
existence.ts 文件集存在性快照(目录枚举 Set,替代逐路径 statSync
manifestParser.ts .manifest 二进制解析 + 类型/ID 推导(纯 TS)
fileScanner.ts 目录遍历缓存 + Include source 候选收集
@@ -112,15 +116,24 @@ tools/
extract-asset-types.mjs OpenSAGE AssetType.cs → asset-types.json
test/
fixtures/minimod 样例 Modinclude 各种情形、同名 ID、嵌套 xi:include、manifest 回退)
*.test.mjs 14 个测试文件(xmlParser / context / completion / semanticTokens /
*.test.mjs 16 个测试文件(xmlParser / context / completion / semanticTokens /
includeResolver / manifestParser / indexer / schemaModel / refs /
typeContext / manifestTypes / referenceIndex / codeLens /
referenceProvider
referenceProvider / projectRoot / workspaceMulti
```
### 关键设计决策
1. **语言激活范围**:不劫持 `*.xml`通过 `workspaceContains:**/Data/Mod.xml``**/*.babproj` 激活;语法高亮为**纯注入** grammar(不声明 `language`,避免覆盖内置 XML 语法)。
1. **语言激活范围与项目检测**:不劫持 `*.xml`激活条件含 `onLanguage:xml`
`workspaceContains:Mod.xml``additionalmaps/mapmetadata_*.xml`
`**/Data/Mod.xml``**/Data/additionalmaps/mapmetadata_*.xml``**/*.babproj`
语法高亮为**纯注入** grammar(不声明 `language`,避免覆盖内置 XML 语法)。
项目根通过 `src/projectRoot.ts` 发现:工作区文件夹向上最多 12 层、容器文件夹
向下浅扫最多 3 层(跳过 Data/Art/builtmods/.git 等)、打开的 XML 文件向上,
任一 `Data/Mod.xml``Data/additionalmaps/mapmetadata_*.xml``*.babproj`
标记命中即算项目根(大小写不敏感、最近命中者优先)。多项目按文档就近选择:
单个项目打开时立即建索引;容器/多项目时惰性建索引(活动文档所属项目先建,
其他在文档打开/首次请求时建),构建经全局串行队列避免并发写共享缓存。
2. **索引范围与默认值**:索引“项目 Data + additionalmaps + 沿 include 可达的 SageXml 原版源码”;SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(可配置)。`reference` include 解析为 `builtmods` 下对应 manifest(惰性解析、按文件缓存),manifest 缺失/无效时回退到占位 XML。
**美术资产(.w3x**`<Include type="all">` / `ART:` 指向的 `.w3x`(及内容嗅探为
XML 的未知扩展名文件)按其顶层资产入库(`W3DContainer` / `W3DMesh` /
@@ -352,6 +365,62 @@ test/
对 stat 匹配的 full XML 做内容哈希校验;打开文档时比较 records 哈希,
不一致则定向 invalidate + `records-desync` 重建自愈;磁盘缓存 v2 → v3;
测试 147 → 151;分析见 `docs/analysis-issues.md` 二十四。
24. [x] 多项目支持(2026-08-07):新增纯模块 `src/projectRoot.ts`(向上 12 层 /
容器向下 3 层 / 单文件向上,`Data/Mod.xml``mapmetadata_*.xml``*.babproj`
标记,大小写不敏感、最近优先、跳过 Data/Art/builtmods/.git 等目录);
`ModWorkspace` 改为多项目状态——按文档就近选项目、单项目立即索引 /
多项目惰性索引(活动文档所属项目先建)、全局串行构建队列保护共享缓存、
磁盘缓存按项目分文件、watcher 事件按路径归属调度、workspace 文件夹变化
重检、激活事件补 mapmetadata 与打开 Data 文件夹场景;测试 151 → 168。
25. [x] 属性补全换行判定与值补全去重(2026-08-07):`attributeInsertLayout`
只按光标之前的完整属性判断“是否已在新行”,one-per-line 标签中间插入或
首属性新行补全不再多插换行,同一行第二个属性仍按原规则换行;
`assetIdItems` 按 id 去重(局部 overlay / 全局索引 / manifest 同一 ID
只给一项,其余定义列入文档说明),`defineItems` 同步按名去重;
测试 168 → 173;版本 0.1.20;分析见 `docs/analysis-issues.md` 二十五。
26. [x] 磁盘缓存可观测性、分阶段校验与进度显示(2026-08-08):
`DiskRecordsCache` 拆为 `load()`(读 + gunzip + JSON)与 `validate()`(逐文件
stat,带进度回调);冷启动**先校验 XML/full 记录再构建**,美术/shallow
记录先以 `validated:false` 预播种(phase A 只登记不消费,避免 stat 2.6GB
模型),在 phase A 发布后的回调里校验并进入 phase B——phase A 可用时间
从 ~34s 提前到 ~16s,且不牺牲“未校验缓存不可信”的正确性(曾尝试构建后
后台校验,既有 I/O 争用又无法事后发现 stat 不可见变化,已放弃);
校验进度写入状态栏(`validating cache N/M…`)并每 1000 条输出一行日志;
`DiskCacheLoadStats` 增加 `loadMs` / `validateMs`,输出通道新增
`[disk-cache] loaded / validated / saved` 计时与 `[build] wall time`
(含缓存加载的总耗时),cacheReport 与状态栏 tooltip 展示校验耗时;
输出通道所有日志行自动加本地 `HH:mm:ss.mmm` 时间戳(`ModWorkspace.log`);
修复构建完成后状态栏仍显示 indexing(`building` 置 false 后补一次
`updateStatusBar()`);测试 173 → 175;分析见
`docs/analysis-issues.md` 二十六。
27. [x] CodeLens 与 FAR 使用同一套定义合并路径(2026-08-10):CodeLens
改为通过 `getScope(document)` 取 merged index,并用
`definitionsForReference`(文档 local overlay + 全局同名定义)+
`collectReferenceSites` 计算计数,与 Find All References 严格一致;
点击 lens 打开的 references peek`showReferencesForDef`)同步改为
同一逻辑,修复“FAR 有引用但 WeaponTemplate 等 CodeLens 不显示/为 0”
的 standalone / 未进全局流文件场景;`scheduleRebuildIfRecordsDesync`
在 CodeLens 中也改用 `recordsSyncSurfaceFor(document)`(按文档所属
项目自愈)。补充:CodeLens 改用轻量 `getCodeLensScope`(只解析当前
文档 + 挂全局索引,不展开 include 链),快照发布后计数即时刷新;
仅在尚无全局快照(`stats.indexedFiles === 0`)时不渲染 CodeLens
快照存在后“0 references”仍按设计显示;新增 `onDidChangeCodeLenses`
事件在每次快照发布时主动通知 VS Code 重新查询(不再只依赖 refresh
命令);输出通道增加 `[codelens] refresh`(快照发布时低频记录)、
`[codelens] suppressed`(首个快照前每个文档只记一次)、scope 异常与
超过 250ms 的慢调用记录;另加**全局重试定时器**:构建期间每 2s 重新
fire 一次 CodeLens 刷新(`onBuildStart` 启动、`!isBuilding` 停止),
避免 VS Code 合并/漏掉单次 refresh 事件导致 phase A 计数迟迟不出现;
定时器只在构建期存在,构建结束即清除;测试 175 → 178;分析见
`docs/analysis-issues.md` 二十七。
28. [x] manifest 源地址按 vanilla-only 解析(2026-08-10):新增
`buildVanillaSearchPaths(sdkDir)``manifestSource` 只按
`[SDK根, SDK\SageXml]`(ART/AUDIO 同理)解析,不再使用当前项目 BAB
顺序;修复 mod 同名 `DATA:globaldata/weapon.xml` 遮蔽导致 manifest
定义跳不到 SageXml 的问题;`referenceIndex` 的 manifest 源归并同步
修正;SageXml 源缺失时保持 manifest-only,文件存在但 id 被删时降级
到文件顶部;测试 178 → 184;分析见 `docs/analysis-issues.md`
二十八。
## 四、验证结果(实测)
@@ -369,7 +438,9 @@ test/
前缀保护、多行未闭合 `Disposition` 完整链路、闭合引号后补空格、一行一个属性
换行缩进、新行缩进对齐、标量类型化默认值)、语义 token(标签/属性/值范围、
合法文档返回空、malformed 返回兜底 token)、include 解析(BAB 顺序、SDK 根
优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器
优先于 SageXmlmanifest 源 vanilla-onlymod 同名遮蔽仍命中 SageXml、
源缺失保持 manifest-only、id 被删降级文件顶部)、manifest 二进制解析
(合成 v5 样本、类型/ID 推导)、索引器
(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、
`childTypeOf`、大小写规范化、属性级 refType、外来命名空间判定、`xs:list`
枚举继承与 `isList` 标记)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`
+16
View File
@@ -64,6 +64,14 @@ XML 之间的组织靠 `<Include>` 标签,共有三种语义:
- `<Include>` 目标文件找不到、Include 循环;
- `$DEFINE` 未定义。
**补充(工作区/项目检测,2026-08-07**
- 项目根不要求工作区精确匹配 `Data/Mod.xml`:从工作区文件夹向上最多 12 层、
从打开的 XML 文件向上、以及从“包含多个 mod 的容器文件夹”向下浅扫最多 3 层
均可发现项目根(`Data/Mod.xml``Data/additionalmaps/mapmetadata_*.xml`
`*.babproj` 任一标记命中即可,大小写不敏感、最近命中优先)。
- 多项目同时打开时按文档就近选择项目;单项目打开立即建索引,容器/多项目采用
惰性索引(活动文档所属项目先建,其他在文档打开或首次请求时建),构建串行执行。
**补充(manifest 解析,支持 include reference 后的补全/导航/诊断)**
-`Mod.xml`(或其他文件)用 `<Include type="reference" source="DATA:static.xml" />` 引用占位文件时,实际内容来自 SDK `builtmods` 下对应的已编译二进制 manifest(`static.manifest` / `global.manifest` / `audio.manifest`)。
@@ -71,6 +79,11 @@ XML 之间的组织靠 `<Include>` 标签,共有三种语义:
- **代码补全**:例如 reference 了 `audio.xml` 后,所有音频资产 ID 都能出现在引用型属性(如 `AudioEventRef`)的补全里;
- **引用导航/悬停**:能定位资产来自哪个 manifest、哪个源文件;
- **诊断**:能把“引用了 manifest 中的 ID”识别为已解析,而不是误报未解析引用。
- manifest 的 `sourceFileName`(如 `DATA:globaldata/weapon.xml`)是**原版编译
时的源地址**,按 vanilla-only 搜索路径(SDK 根 + `SageXml`)解析,不能用当前
mod 的 BAB 顺序解析——否则 mod 同名 DATA 路径会遮蔽 SageXml 源码。ART/AUDIO
源码默认不映射(SDK 基本不提供);SageXml 源缺失时保持 manifest-only
文件存在但 id 被删时降级到文件顶部。
- manifest 为二进制格式,解析逻辑参考 OpenSAGE `ManifestFile.cs`(用户已在本工作区 `OpenSAGE/` 克隆并切到指定 commit)。关键格式要点:
- 头部含版本(5/6/7)、端序标志、各缓冲区大小、资产数量;
- 每个资产条目含 `TypeId`(哈希)、`NameOffset``SourceFileNameOffset` 等;
@@ -116,6 +129,9 @@ XML 之间的组织靠 `<Include>` 标签,共有三种语义:
## 四、验收标准
- 在 AttachTest / GenEvoTest 上开箱即用(高亮、补全、跳转、诊断)。
- 打开 mod 的 `Data` 文件夹、`Data` 子文件夹、仅含 mapmetadata 的项目、
单个 XML 文件、以及“内部包含多个 mod”的容器文件夹时均能正确发现项目根;
多项目打开时各自索引与功能互不串扰。
- 在 Corona 规模的目录上不卡 UI:索引在后台执行、保存文件后增量更新。
- 纯解析/索引核心不依赖 VS Code API,可被其他工具复用。
- 可用 `vsce package` 打出可安装的 `.vsix`