0.1.13 improve completion
This commit is contained in:
@@ -1064,3 +1064,217 @@ stale=true 并触发 follow-up。处理(按用户建议的扩展名白名单
|
||||
输出通道同步增加 `walk / candidates / art` 耗时分解,便于下次直接定位慢在哪一段。
|
||||
|
||||
版本 **0.1.8 → 0.1.9**。
|
||||
|
||||
---
|
||||
|
||||
## 十八、问题分析(第十三轮,2026-08-04):bit-flag 列表补全的三层问题
|
||||
|
||||
### 现象
|
||||
|
||||
对 `xs:list` 枚举(bit flag,如 `CreateObject@Disposition`、
|
||||
`LocomotorTemplate@Surfaces`):
|
||||
|
||||
1. 刚打开引号时(`Disposition="`)能补全;
|
||||
2. 输入第一项后再输入空格,**不触发**补全;
|
||||
3. 已经闭合的 `Disposition="RANDOM_FORCE RELATIVE_ANGLE"` 想在中间插入或末尾
|
||||
追加 flag,**不触发**补全。
|
||||
|
||||
### 根因(三个独立问题)
|
||||
|
||||
**1. 空格没有注册为补全触发字符**
|
||||
|
||||
`extension.ts` 注册 provider 时只传了 `< " = : . /`。VS Code 只在输入 word
|
||||
字符或已注册触发字符时自动弹出补全;空格两者都不是,所以“打 flag → 空格”
|
||||
永远不会自动弹出。刚打开引号能补全正是因为 `"` 已注册。
|
||||
|
||||
**2. 多行未闭合引号下,解析恢复丢失后续行属性**
|
||||
|
||||
解析器对未闭合开始标签的恢复策略是“截到第一个换行”(`xmlParser.ts`),因此
|
||||
像用户示例这样属性逐行书写的标签,`<CreateObject` 之后各行的 `Options` /
|
||||
`Disposition` 全部丢失,`startTagEnd` 停在 `<CreateObject` 行尾。光标在后续
|
||||
行时 `analyzeContext` 走 `content` 分支——实测 `kind: content, attrs: []`。
|
||||
也就是说,**按用户贴出的原文状态,当前代码其实并不会补全 Disposition**;
|
||||
观察到“能补全”的编辑状态里引号/`>` 多半已闭合。另外恢复分支没有把恢复出的
|
||||
元素挂到父元素下(`parent = null`),即使补上上下文分析,类型解析也会退回
|
||||
全局映射(`CreateObject` → `GameObjectWeakRef`)而找不到 `Disposition`。
|
||||
|
||||
**3. 闭合引号内“插入/追加 flag”的体验与范围问题**
|
||||
|
||||
- 光标在完整值末尾(`...RELATIVE_ANGLE|"`)时,当前 token 恰好等于完整枚举
|
||||
值,`startsWith` 过滤只剩它自己 → 看起来“没有补全”;
|
||||
- 替换范围 bug:引号闭合时 `endOffset` 固定取整个值的末尾而不是光标位置。
|
||||
实测光标在 `RANDOM_FORCE | RELATIVE_ANGLE` 中间时 range 为 `(28..42)`,
|
||||
选中任意 flag 会删掉 `RELATIVE_ANGLE` 及之后的内容;
|
||||
- 光标恰好贴在闭合引号后面(`"|`)时,`offset <= quoteEnd` 把引号算进
|
||||
prefix(`RELATIVE_ANGLE"`),返回 0 项。
|
||||
|
||||
### 修复
|
||||
|
||||
1. **触发字符**:`registerCompletionItemProvider` 增加 `" "`(空格)。副作用:
|
||||
属性之间按空格会弹属性名补全(加分项),文本内容按空格会弹子元素补全。
|
||||
2. **多行未闭合标签的补全**:
|
||||
- 解析器给恢复出的元素打 `recoveredStartTag` 标记,并**补挂父链**
|
||||
(与正常分支一致,`parent.children.push` + `el.parent = parent`);
|
||||
- `analyzeContext` 在光标越过 `startTagEnd` 且元素带标记时,把
|
||||
`text.slice(tagStart, cursor)` 作为部分标签重新 `parseTag` 一次,
|
||||
再走同一套 start-tag 分类。全局解析恢复策略不变,后续文档解析/诊断
|
||||
不受影响。
|
||||
- 引号判定从 `offset <= quoteEnd` 改为 `offset < quoteEnd`:光标在闭合
|
||||
引号之后进入 attribute-name 上下文。
|
||||
3. **list 补全范围与过滤**(`completion.ts`):
|
||||
- list 值替换范围改为 `min(cursor, valueEnd)`,只覆盖当前段;非 list
|
||||
保持整值替换;
|
||||
- 排除列表中已出现的 flag(空格后只推荐剩余项);
|
||||
- 追加模式:当前段已是完整枚举值且没有其它枚举以它为前缀时,给出零宽
|
||||
range、`insertText = " FLAG"`,可直接在闭合值末尾/列表中间追加;
|
||||
前缀保护是必要的——实测 820 个 list 枚举里有 10569 对严格前缀关系
|
||||
(如 `CAN_ATTACK` → `CAN_ATTACK_WALLS`)。
|
||||
|
||||
### 举一反三的测试(98 → 107 全绿)
|
||||
|
||||
- `xmlParser.test.mjs`:恢复元素带 `recoveredStartTag` 标记、正常元素不带;
|
||||
- `context.test.mjs`:用户示例的多行未闭合引号(`Disposition="`)仍为
|
||||
attribute-value 且 prefix 正确;输入 flag + 空格后 prefix 含完整值;闭合
|
||||
引号之后为 attribute-name;
|
||||
- `completion.test.mjs`:
|
||||
- 空格后只推荐未使用的 flag(10 项,不含 GROUND),range 零宽在光标处;
|
||||
- 列表中间插入不会删掉尾部 flag(range 止于光标);
|
||||
- 闭合值末尾完整 flag → 追加模式(`insertText: " WATER"`);
|
||||
- `CAN_ATTACK` 有更长变体时保持前缀过滤,不进入追加模式;
|
||||
- 多行未闭合 `Disposition="RANDOM_FORCE ` 经完整 provider 链路返回剩余
|
||||
flag(不含 RANDOM_FORCE)。
|
||||
|
||||
版本 **0.1.9 → 0.1.10**。
|
||||
|
||||
---
|
||||
|
||||
## 十九、问题分析(第十四轮,2026-08-04):属性补全的插入体验
|
||||
|
||||
### 现象
|
||||
|
||||
写完一个属性值并关闭引号后,会立刻触发下一个属性的补全菜单(由 `"` 触发字符
|
||||
带来,方便)。但按 Enter 接受补全时不会补空格,结果属性与上一个属性的闭合
|
||||
引号贴在一起:
|
||||
|
||||
```xml
|
||||
Disposition="RANDOM_FORCE RELATIVE_ANGLE"Count="$1"
|
||||
```
|
||||
|
||||
连续接受会变成 `...RELATIVE_ANGLE"Count="$1"CreateFX="$1"DestinationPlayer="$1"`。
|
||||
|
||||
另外提出两个功能请求:
|
||||
|
||||
1. 新属性自动参考临近属性的缩进(很多 XML 的属性统一换行缩进);
|
||||
2. 补全的 `$1` 占位符在允许时变成更有意义的值(数字 → 数字、角度 → 角度、
|
||||
时间 → 时间),顺带提示值的格式。
|
||||
|
||||
### 修复
|
||||
|
||||
**1. 插入布局(`completion.ts` 新增 `attributeInsertLayout`)**
|
||||
|
||||
- 光标紧贴上一个属性的闭合引号时,插入文本前补一个空格(inline 布局);
|
||||
- 临近属性是“一行一个”布局(相邻属性之间的原文含换行)时,插入
|
||||
`\n + 上一个属性的缩进`;
|
||||
- 用户已经回车换行时,用临近缩进替换当前行已有的空白(对齐);
|
||||
- inline 风格的文件里用户手动换行,则保留用户自己打的缩进,不强改;
|
||||
- `xai:joinAction` / `xmlns:xai` 辅助项同样享受前缀与触发。
|
||||
|
||||
**2. 类型化默认值(`completion.ts` 新增 `attributeValuePlaceholder`)**
|
||||
|
||||
- 引用 / 枚举 / list / 布尔 / `inheritFrom` / `Include@source` / `id` 保留
|
||||
`$1` 占位并自动触发值补全(这些值靠候选选择,不能瞎猜);
|
||||
- 标量属性优先用 XSD `default`(如 `Count="1"`),没有默认值时按类型给示例:
|
||||
`Angle → 0d`、`Time → 0s`、`Percentage → 100%`、`Velocity → 0.0`、
|
||||
`SageReal/float → 0.0`、`SageInt/unsigned → 0`;
|
||||
- 填了具体默认值后不再弹空的 suggest 窗口;`allowsDefine` 的数值属性现在也
|
||||
直接给数值示例(`$DEFINE` 仍可在值内手动触发补全)。
|
||||
|
||||
### 举一反三的测试(107 → 111 全绿)
|
||||
|
||||
- 闭合引号后接受属性 → ` Count="1"`(空格),range 零宽在光标处;
|
||||
- 一行一个属性 → `\n Count="1"`(换行 + 缩进);
|
||||
- 已在新行 → range 覆盖当前行空白,插入 ` Count="1"` 对齐;
|
||||
- 标量默认值:`Count="1"`(XSD 默认)、`FadeTime="0s"`、`DispositionAngle="0d"`;
|
||||
- 建议类属性:`CreateFX="$1"`、`Options="$1"`、`DisabledWhileBusy="$1"` 均带
|
||||
trigger 命令;具体默认值不带。
|
||||
|
||||
版本 **0.1.10 → 0.1.11**。
|
||||
|
||||
---
|
||||
|
||||
## 二十、问题分析(第十五轮,2026-08-04):属性补全的缩进叠加与 `$1` 占位符
|
||||
|
||||
### 现象
|
||||
|
||||
连续接受属性补全时,缩进不是稳定对齐,而是逐行递增。用户分步实测(0.1.12):
|
||||
|
||||
- 关闭引号 → 属性候选菜单 → 直接 Enter:第一次补全 `Count="1"` 就落在
|
||||
6 个 Tab(`Disposition` 是 3 个 Tab);
|
||||
- 按空格再次触发 → Enter:`CreateFX="$1"` 落在 9 个 Tab;
|
||||
- 再 Enter(CreateFX 带触发命令,菜单自动重开):`DestinationPlayer="$1"`
|
||||
落在 12 个 Tab。
|
||||
|
||||
```xml
|
||||
<CreateObject
|
||||
Options="IGNORE_ALL_OBJECTS"
|
||||
Disposition="RANDOM_FORCE RELATIVE_ANGLE ABSOLUTE_ANGLE"
|
||||
Count="1"
|
||||
CreateFX="$1"
|
||||
DestinationPlayer="$1"
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
VS Code 在插入**含换行的补全文本**时,会给新行套用当前行的基础缩进,并与
|
||||
补全文本里嵌入的缩进**相加**(而不是替换):
|
||||
|
||||
```text
|
||||
我们插入 \n + 3 Tab → 落盘 = 当前行 3 Tab + 我们 3 Tab = 6 Tab
|
||||
下一行:当前行 6 Tab + 我们 3 Tab = 9 Tab
|
||||
再下一行:9 + 3 = 12 Tab
|
||||
```
|
||||
|
||||
与实测的 6 / 9 / 12 完全一致。关键证据是**第一次补全就已多缩进**:0.1.12
|
||||
第一次只插入 `\n` + 3 个 Tab(锚点是首个独占一行的 `Options`),落盘却是
|
||||
6 个——问题不在我们读取了谁的缩进,而在于补全文本自带的缩进被编辑器叠加。
|
||||
|
||||
排查过程中还发现一个放大因素并已修复:`attributeInsertLayout` 原先以
|
||||
“最后一个被解析出的属性”为锚点,用户在自动缩进的新行上输入的半截属性名
|
||||
(`C`、`D`…,`hasValue = false`)也会被当成锚点,把编辑器自动缩进抄进补全
|
||||
行;即使叠加根因修掉,这个因素也会让缩进更容易跑偏。
|
||||
|
||||
### 修复(0.1.13)
|
||||
|
||||
1. **换行时只插入 `\n`,不再嵌入缩进**:编辑器自动补当前行的基础缩进
|
||||
(3 Tab),叠加量为 0,后续行稳定在 3 Tab;已在新行时仍显式替换为规范
|
||||
缩进(该路径不插入换行,不受叠加影响)。
|
||||
2. **锚点只用完整属性**(`hasValue` 为真),并优先取**第一个独占一行的完整
|
||||
属性**作为规范缩进;半截属性名不参与缩进计算,整行内联时才回退到最后一个
|
||||
完整属性。
|
||||
3. **`$1` 改为真正的 snippet 占位符**:属性名补全统一用 `SnippetString`,
|
||||
文档中不再出现字面 `$1`;接受补全后光标落在引号内的占位处,弹出的也是值
|
||||
补全菜单。`Count="1"` 这类具体默认值同样用 snippet(无占位符,光标落在
|
||||
闭合引号后)。
|
||||
4. **尾随空格**:插入换行时,若上一个属性与光标之间只有空白(例如为触发补全
|
||||
按的空格),把这段空白一并纳入替换范围,不再残留尾随空格。
|
||||
5. **调试日志**:`ModWorkspace.log()` 输出到 “RA3 Mod XML” 输出通道;
|
||||
attribute-name 补全每次记录 `existing / range / prefix`(JSON 转义),用于
|
||||
对比“我们插入的内容”与“落盘的内容”,定位编辑器侧改写。
|
||||
|
||||
### 测试环境说明
|
||||
|
||||
当前单测(node + vscode stub)**不能复现 VS Code 的 suggest 弹窗、snippet
|
||||
缩进与 auto-indent 行为**,这类问题只能靠实机 + 输出通道日志确认。后续若要
|
||||
自动化,需要引入 `@vscode/test-electron` 做扩展宿主集成测试(本期未做,记录
|
||||
为候选)。
|
||||
|
||||
### 测试(111 → 113 全绿)
|
||||
|
||||
- 新行上输入半截属性名(行缩进 20 个空格)→ 补全仍用规范缩进,range 覆盖
|
||||
整段自动缩进与已输入字符;
|
||||
- 为触发补全按的空格被新行替换吞掉,不再残留尾随空格;
|
||||
- 属性名补全断言改为 `insertText.value`(SnippetString),换行插入断言为
|
||||
`\nCount="1"`(缩进由编辑器提供)。
|
||||
|
||||
版本 **0.1.11 → 0.1.13**(0.1.12 为中间版本,仅含锚点与尾随空格修复,
|
||||
未解决叠加;0.1.13 为最终修复)。
|
||||
|
||||
+45
-4
@@ -1,6 +1,6 @@
|
||||
# 调研结论与实施计划(已按最新代码同步更新)
|
||||
|
||||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-01)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮等。
|
||||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-04)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)等。
|
||||
|
||||
## 一、调研结论(带证据)
|
||||
|
||||
@@ -132,10 +132,16 @@ test/
|
||||
11. **`xs:list` 建模与多值补全**:list 简单类型继承 itemType 的枚举 / refType / isRef /
|
||||
allowsDefine 并标记 `isList`(`LocomotorSurfaceBitFlags`、`KindOfBitFlags` 等 79 个
|
||||
类型、317 处属性声明受益);补全只对“最后一个空格段”过滤,替换范围只覆盖当前段,
|
||||
支持 `Surfaces="GROUND ` 之后继续输入 `W` 提示 `WATER`。
|
||||
支持 `Surfaces="GROUND ` 之后继续输入 `W` 提示 `WATER`。第十三轮(2026-08-04)
|
||||
补全触发与编辑体验:空格注册为触发字符;列表过滤排除已出现的 flag;当前段已是
|
||||
完整枚举值且没有更长变体时进入“追加模式”(零宽 range + `insertText=" FLAG"`,
|
||||
可直接在闭合值末尾/中间追加);替换范围止于光标,中间插入不会删除尾部 flag。
|
||||
12. **未闭合引号的行尾恢复**:起始标签扫描到 EOF 且引号未闭合时,在第一个换行处截断
|
||||
标签并继续解析,未闭合只影响当前行(仍上报 `Unterminated start tag`),后续元素
|
||||
的补全 / hover / 诊断不中断。
|
||||
的补全 / hover / 诊断不中断。第十三轮补充:恢复出的元素带 `recoveredStartTag`
|
||||
标记并补挂父链;补全上下文对“光标在恢复元素内但越过 `startTagEnd`”的情况按
|
||||
`text.slice(tagStart, cursor)` 重新解析部分标签,使多行书写的未闭合属性
|
||||
(如 `Disposition="`)仍可获得 attribute-value 补全,且不影响全局解析。
|
||||
13. **语义 token 兜底高亮**:TextMate 对未闭合引号会把后续内容当字符串吞掉(任何
|
||||
XML 编辑器皆然);扩展注册 `DocumentSemanticTokensProvider`,仅当解析报错时用
|
||||
语义 token 覆盖标签名 / 属性名 / 属性值(标准 token 类型 `type` / `property` /
|
||||
@@ -214,6 +220,19 @@ test/
|
||||
(`AttachModuleId` / `ModuleId` / `AutoResolveBody` 等)在最近 GameObject
|
||||
子树内解析;未命中不新增诊断(保守策略,避免跨文件误报)。
|
||||
顶层 `<Include type="all">` 暂不并入逻辑树(保留为后续扩展)。
|
||||
26. **属性补全的插入布局与类型化默认值(第十四轮,2026-08-04)**:
|
||||
`attributeInsertLayout` 按临近属性的排版决定插入方式——贴引号时补空格、
|
||||
一行一个属性时补换行 + 缩进、已在新行时用临近缩进替换当前行空白、
|
||||
inline 风格的手动换行保留用户缩进;`attributeValuePlaceholder` 对引用/
|
||||
枚举/list/布尔等建议类属性保留 `$1` + 自动触发,对标量属性填 XSD 默认值
|
||||
或类型示例(`0d` / `0s` / `100%` / `0.0` / `0`),具体默认值不再弹空
|
||||
suggest。
|
||||
第十五轮(2026-08-04)最终结论:VS Code 插入含换行的补全文本时会把当前
|
||||
行基础缩进与文本内嵌缩进相加(3+3=6、6+3=9…),因此换行前缀只插入 `\n`、
|
||||
缩进交给编辑器;同时半截属性名(`hasValue=false`)不再作为缩进锚点,改用
|
||||
第一个独占一行的完整属性作为规范缩进,插入换行时顺带吞掉触发补全留下的
|
||||
尾随空格;属性名补全改用 `SnippetString`(`$1` 成为真正占位符),并新增
|
||||
输出通道调试日志。
|
||||
|
||||
## 三、实施步骤
|
||||
|
||||
@@ -250,6 +269,15 @@ test/
|
||||
+ Poid 局部作用域补全/悬停/跳转;测试 92 → 98。2026-08-04 补充构建期
|
||||
闸门:`getScope` 在重建进行中只返回 parse-only scope,避免与 indexer
|
||||
抢盘(版本 0.1.9)。
|
||||
16. [x] bit-flag 列表补全修复(第十三轮,2026-08-04):空格触发字符、多行
|
||||
未闭合标签的部分标签重解析(`recoveredStartTag` 标记 + 恢复元素父链)、
|
||||
闭合值内追加模式与中间插入范围修复;测试 98 → 107(版本 0.1.10)。
|
||||
17. [x] 属性补全插入体验(第十四轮,2026-08-04):闭合引号后自动补空格、
|
||||
临近属性缩进对齐、标量属性类型化默认值;测试 107 → 111(版本 0.1.11)。
|
||||
18. [x] 属性补全缩进叠加修复(第十五轮,2026-08-04):换行前缀只插入 `\n`
|
||||
(编辑器自动补基础缩进,避免 3+3=6 式叠加)、完整属性锚点 + 首个独占一行
|
||||
属性为规范缩进、`$1` 改为 SnippetString 占位符、输出通道调试日志、尾随
|
||||
空格吞除;测试 111 → 113(版本 0.1.12–0.1.13)。
|
||||
|
||||
## 四、验证结果(实测)
|
||||
|
||||
@@ -259,7 +287,20 @@ test/
|
||||
| GenEvoTest | 66 文件 | 首次 ~2.6s / 二次 ~0.4s | 35,502(manifest 35,322,w3x 浅扫 38) | 2 个流、73 个 Define;二次构建 0 重扫 / 38 缓存命中 |
|
||||
| Corona | 8,976 文件 | 首次 ~250s / 信任二次 ~2s / 强制 ~5-25s | 64,868(manifest 35,322,w3x 浅扫 4,829) | 3 个流、183 个 Define、0 诊断;二次构建 statSync 0、resolveHits 15,333 |
|
||||
|
||||
单元测试覆盖:XML 解析(自闭合/容错/偏移/未闭合引号行尾恢复)、补全上下文(未闭合引号仍为 attribute-value、list 多值分段)、补全集成(vscode stub 下 `LocomotorTemplate@Surfaces` 未闭合引号枚举补全、空格后第二段过滤与替换范围)、语义 token(标签/属性/值范围、合法文档返回空、malformed 返回兜底 token)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化、属性级 refType、外来命名空间判定、`xs:list` 枚举继承与 `isList` 标记)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、模块 `id` 定义点、Poid 局部引用、`xi:include` 不校验、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
|
||||
单元测试覆盖:XML 解析(自闭合/容错/偏移/未闭合引号行尾恢复 + recovered
|
||||
标记)、补全上下文(未闭合引号仍为 attribute-value、多行未闭合引号、list 多值
|
||||
分段、闭合引号后为 attribute-name)、补全集成(vscode stub 下
|
||||
`LocomotorTemplate@Surfaces` 未闭合引号枚举补全、空格后第二段过滤与替换范围、
|
||||
空格后排除已用 flag、中间插入范围止于光标、闭合值末尾追加模式、`CAN_ATTACK`
|
||||
前缀保护、多行未闭合 `Disposition` 完整链路、闭合引号后补空格、一行一个属性
|
||||
换行缩进、新行缩进对齐、标量类型化默认值)、语义 token(标签/属性/值范围、
|
||||
合法文档返回空、malformed 返回兜底 token)、include 解析(BAB 顺序、SDK 根
|
||||
优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器
|
||||
(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、
|
||||
`childTypeOf`、大小写规范化、属性级 refType、外来命名空间判定、`xs:list`
|
||||
枚举继承与 `isList` 标记)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、
|
||||
模块 `id` 定义点、Poid 局部引用、`xi:include` 不校验、`Side="Allies"` 命中
|
||||
manifest 的 `PlayerTemplate`)。
|
||||
|
||||
> 注:D: 盘移动硬盘已恢复连接;Corona 已在第八 / 九轮按上述新数据回归。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user