Skip to content

dev → main: 递归分组同步层级修复(issue #356)+ 剪贴板 / 分隔条 / 更新链路等 9 项修复 - #357

Merged
feigeCode merged 10 commits into
mainfrom
dev
Oct 7, 2026
Merged

feigeCode merged 10 commits into
mainfrom
dev

Conversation

@feigeCode

Copy link
Copy Markdown
Owner

dev → main:递归分组同步层级修复 + 一批 UI / 发布 / 更新修复

本批 10 个提交,46 个文件,+4364 / −335。主线是 issue #356 的云同步与个人同步层级修复,其余为沿路修复与经验沉淀。

主线:递归分组的父子层级随同步载荷传递(issue #356)

现象

在 A 电脑递归创建分组 A / B,把同步数据应用到 B 电脑后嵌套被拍平:A、B 变成两个并列分组。云同步与个人同步(git / WebDAV / 目录)两条链路都能复现。

根因

同步载荷 WorkspacePlainData 只带 name / color / icon / sort_order,父子关系从未进入载荷;本地 parent_id 是机器内自增 ID,跨设备无意义。下载侧 apply 阶段一律写 parent_id: None,update_from_cloud 也刻意不写 parent_id——于是层级在同步入口就丢了,而不是在某一侧算错。

修复

  • 载荷新增 format_version + parent_cloud_id,用「版本号 + 父云 ID」三态表达层级:0 / 缺省 = 旧载荷(无意见,保持本地层级)、1 + None = 明确的根分组、1 + Some(id) = 指定父分组。旧载荷因此仍可区分,不会被当成「空层级」覆盖本地树。
  • 父分组的 cloud_id 尚未落库时不冒充根分组(按旧载荷上传),避免在别的设备上永久拍平,随后由补传逻辑自愈。
  • 上传侧按祖先优先排序(cycle-safe),保证父分组先拿到 cloud_id,子分组才能写入稳定引用;个人同步导出顺序同步收敛为 钥匙串 → 分组 → 连接。
  • 下载侧不改 apply 语义,改由收尾的 reconcile_parent_links / finalize_pass 统一对齐父子关系:顺序无关、幂等、只写真实变化,且不动 updated_at(避免多余上传)。
  • 旧客户端留下的无层级载荷:不拍平本地嵌套,并在父分组已有 cloud_id 且未被本轮计划覆盖时补一次带层级的上传。
  • 云同步与个人同步两条链路一起修,共用同一份载荷与解密入口。

测试

  • 新增引擎级端到端用例 6 个(内存假云端 + 可多开的「设备」,走完整 SyncEngine::sync()):上传三层嵌套的云端引用、侧边栏顺序反序仍父先上传、另一台设备还原 A>B>C 嵌套、旧载荷升级不拍平、旧载荷更新更晚不拍平、云端移到根分组时清除本地父关系。
  • 另有载荷三态往返、祖先优先排序(含父引用成环)、收尾对齐的幂等 / 父引用未落地 / 根分组清除、个人同步端到端递归嵌套等 11+ 个用例。
  • 突变验证(确认用例确实卡住修复点):上传不解析父引用 → 6 个里 4 个失败;禁用收尾对齐 → 2 个失败;去掉祖先优先排序 → 1 个失败;还原后全绿。

其余 9 个提交

提交 内容
53ef9a372 fix(update): Linux 应用内更新不再对运行中的可执行文件试写
01b400146 fix(db_view): SQL 补全字段不再串到文档里其他语句的表(issue #329)
f3ec3660a chore(deps): gpui-kit 切到 b450f35c1,gutter 标记随滚动对齐(issue #330)
24abe96da fix(release): Windows MSI 快捷方式不再引用图标表,图标取自 exe 内嵌资源(issue #325)
0bd7ee2c7 fix(ui): 表头列宽抓取区骑到列边界上,不再要求像素级对准(issue #333)
cdad30638 fix(clipboard): 复制整行后粘贴丢字段,剪贴板 TSV 改为成对转义(issue #355)
cb2259076 docs(agents): 沉淀「剪贴板 TSV 必须成对转义」的经验
88a063119 fix(ui): 面板分隔条与值表列宽抓取区都骑到边界上(issue #333)
ec3412d10 docs(agents): 沉淀「拖拽分隔条抓取区必须骑在边界两侧」的经验(issue #333)

这批修复各自的定向验证在各自的会话里完成:cargo test -p one-core --lib、相关 crate 的 check / 定向测试,以及各 issue 对应的复现步骤。本次合批只做了下面的通用验证,没有逐条复跑每个提交的定向用例。

本批通用验证

$ git merge-tree --write-tree --name-only origin/main origin/dev
f05ade73d7417703b5cd4471a1c32d89c2459c03        # exit 0,无冲突行,可干净合并

$ cargo check --workspace --all-targets
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 19s

$ cargo test -p one-core --lib
test result: ok. 763 passed; 0 failed; 0 ignored

$ node --test script/test-release-packaging.mjs
ℹ tests 31 / pass 31 / fail 0

无新增 clippy 告警(cargo clippy -p one-core --all-targets 对本批新增代码零命中)。

install_linux 的前置检查会对自己(正在运行的可执行文件)open(O_WRONLY),
Linux 内核对正在执行的映像必定返回 ETXTBSY(Text file busy,os error 26),
导致所有非便携版 Linux 的应用内更新必然失败,并把用户引向「权限」这个错误
方向(issue #353:便携目录已给足写权限仍报 os error 26)。

替换链路是 rename(target→backup) + rename(staging→target),只依赖目录写权限、
从不写目标文件,因此前置检查改为探测目标所在目录是否可写。附带修正:目标文件
只读但目录可写时不再被误拒(旧实现在 macOS 上同样以 Permission denied 拒绝更新,
而 rename 覆盖只读文件是允许的)。

验证:
- cargo check -p main --tests:通过
- cargo test -p main --bin navop update:68 passed / 0 failed
- 变异验证:临时把实现改回 open(O_WRONLY),read_only_target_file_is_not_rejected 转红
- Linux 专属断言(正在运行的映像、只读目录)需在 Linux 上执行:本机 macOS 不复现
  ETXTBSY,aarch64 交叉检查因缺 aarch64-linux-gnu-gcc 未完成
补全用的符号表此前由整篇文档的 token 构建,而「当前语句是否有 FROM」的判断却是
按分号分句收窄的:两条路径口径不一致,多语句文档里 `select * from users where `
会把文档中其他语句的表也当成当前语句的表,候选里混进别的表的字段;跨表出现同名
列时裸列名还会被迫退化成 `table.column`,用户不得不加别名(issue #329 附的截图
就是这种混排)。点号补全(`l.`)同样会解析到上一条语句的别名。

改为按分号界取当前语句的 token 子切片,符号表与上下文推断都只用它:列候选和点号
别名解析随之一起收窄,上一条语句的别名不再在当前语句里解析出字段。原先散落在
`current_statement_has_from_keyword` 里的边界计算提取为 `current_statement_bounds`,
两条路径共用同一口径。行内(ghost text)补全走同一套数据源,一并收窄,避免同一类
串扰。

分号划分对 `DELIMITER` 之类的存储过程体并不精确,这与旧的 `has_from` 判断口径一致;
收窄只会少给候选,不会给错候选。

验证:
- cargo test -p db_view --lib:872 passed / 0 failed(含新增回归测试
  statement_scope_provider_tests,覆盖多语句字段列表、多语句限定列、跨语句别名
  三种场景)
- 变异验证:临时把符号表改回整篇文档 token,新增测试转红(多语句下出现
  users.id / logs.id / message)
- cargo clippy -p db_view --lib --tests:本次改动文件无新增告警
修复在 fork 的 fix/gutter-lane-scroll 分支,已推送。根因:gutter lane overlay
用内容空间坐标放置标记,却没有像文本元素那样叠加纵向滚动平移,所以滚动后每个
▶ 执行按钮都停在未滚动时的行位置,点它执行的是另一条语句(偏移量等于滚动距离)。

验证:gpui-kit 侧新增回归测试修前红修后绿,`cargo test -p gpui-base --lib` 1320
通过、`cargo test -p gpui-component --lib` 609 通过、clippy/fmt 干净;navop 侧按
新 rev 编译并跑 `cargo test -p db_view`,872 通过。rquickjs 与各 gpui-kit crate
仍锁定同一 revision。
issue #325 报告桌面上的 Navop 快捷方式图标退化成通用白纸。仓库里没有任何隐藏快捷
方式箭头或桌面图标的功能,现象落回安装包给出的快捷方式图标上:navop.wxs 的两个
<Shortcut> 都带 Icon="NavopIcon",即引用 Icon 表。MSI 会把 Icon 表里的图标另存成
一份独立文件作为快捷方式的图标来源(文档还要求这类图标与目标扩展名一致,而我们
是 .ico 对 navop.exe),这份文件一旦被清理或图标缓存失效,快捷方式就只能显示通用
白纸图标;而 navop.exe 自己其实已内嵌同一份图标(main/build.rs 的 set_icon)。

因此两个快捷方式一律不再引用 Icon 表,图标改由目标 exe 的内嵌资源提供,与
file_association.rs 的 DefaultIcon = "<exe>,0" 保持同一口径。控制面板「应用和功能」
的图标仍走 Icon 表,<Icon Id="NavopIcon"> 与 ARPPRODUCTICON 原样保留。

契约收紧:test-release-packaging.mjs 新增用例断言两个 <Shortcut> 不带 Icon=、卸载项
图标仍走 Icon 表、且 build.rs 确实内嵌 resources/windows/navop.ico,保证「留空」不是
「没图标」;validate-windows-msi.ps1 新增 Assert-MsiEmptyValue 并直接校验安装产物里
Shortcut.Icon_ 为空,把这条规则钉在 Windows release 阶段。

验证:
- node --test script/test-release-packaging.mjs:31 passed / 0 failed(含新增用例)
- 变异验证:把 Icon="NavopIcon" 临时加回 Desktop 快捷方式,新增用例转红;还原后转绿
- validate-windows-msi.ps1 未在本机执行:macOS 无 pwsh、无 WindowsInstaller COM,
  只在 Windows release CI 里跑;MSI 装完快捷方式图标的真实表现需 Windows 实机验收
以前是一条 2px、整条缩在左侧列内、贴着右缘的命中带,比指针热区还窄:
鼠标稍微一动,光标和悬停高亮就一起消失,得像素级对准才拉得动列宽。

改成以列边界为中心、两侧各留 4px 的抓取区:
- one_ui 数据表格(表数据 / MongoDB 共用):边界两侧的列各渲染自己那半边
- 表对象列表、用户列表的表头:同样跨边界取两半
- 表设计器表头:列与列之间隔着 gap_3,改用以列边界为中心的整条抓取区
- 单元格预览面板的分隔条:同样改成跨边界抓取区

回归测试:one_ui 4 条、表对象列表 4 条、表设计器 3 条、预览面板 3 条。
可编辑表格与查询结果共用「\t 分列、\n 分行」的剪贴板格式,但两侧各自手写:
复制侧 join("\t") 原样写值,粘贴侧 lines() + split('\t') 裸切。字段本身含
\t / \n / \r / " 时,复制那一刻就已经在剪贴板上被切成多个字段,粘贴侧无从还
原——表现为「剪贴板里字段是全的,粘进去少了几个」:含制表符的字段会多切出一
列顶掉后面的列,含换行的字段会串到下一行。CSV 导出那条路早有 RFC 4180 转义,
剪贴板链路只是没跟上。

修复:
- 新增 one_ui::edit_table::tsv 统一编解码(escape_tsv_field /
  encode_tsv_row(s) / parse_tsv_rows),四个读写点全部改走它:
  EditTableState::action_copy / action_paste(Ctrl/Cmd+C、V),以及
  CopyFormatter::format_tsv / EditorTableDelegate 右键粘贴
- 字段含 \t / \n / \r / " 才加引号并双写内部 ",普通值保持原样(粘到 Excel
  也是标准格式);只有整段文本含 \t、换行或 "" 才按引号规则解转义,这样从别
  处粘单值(JSON 片段、C:\new\test 这类路径)依旧原样写入
- \r\n 算一个换行、末尾换行不补空行,与旧的 str::lines() 行为对齐

已知遗留:值恰为字面量 \N 与 SQL NULL 标记仍无法区分(粘贴侧没有 NULL 语
义),本次未改动。

回归测试:
- clipboard_tsv_round_trip_is_lossless:真实复制产出 → 真实解码;旧规则下可
  精确复现「字段含制表符 → 多切一列」「字段含换行 → 多一行且值错位」
- clipboard_tsv_escapes_fields_with_separators
- table_data_clipboard_sites_use_the_shared_codec(契约测试,钉住两个读写点不
  许退回裸切)
- one_ui tsv 单测 10 条:往返、引号/换行/制表符/回车/空值、单值原样、契约

验证:cargo test -p db_view --lib copy_format(20)、cargo test -p one-ui --lib
tsv(10)、cargo test -p db_view --lib(884)、cargo test -p one-ui --lib
(114)、cargo clippy -p one-ui -p db_view --all-targets(无 error、改动文件无
新增告警)、cargo check -p main(Finished)。
issue #333 的根因是「抓取区整条缩在边界一侧」:鼠标必须像素级停在那几个像素
里,稍微偏一点光标就退回默认状态。上一提交修好了三处表头,这次补齐同类站点。

one_ui::resize_handle(SQL 结果面板、数据库/Mongo/Redis 侧边栏、终端工具坞共用)
- 旧实现:Left/Right 两种形态是 1px 宽 + 单侧 padding,实测热区只有 4px,
  且整条贴在面板一侧;`left(1px)`/`right(1px)` 还让分隔线不在边界上。
- 现在:热区宽度取 `Resize::hit_area()`(分隔线 + 两侧各 4px),用负边距跨到
  边框外(`right_0() + mr(-4)` / `left_0() + ml(-4)` / `top_0() + mt(-4)`),
  分隔线用 `justify_center`/`items_center` 留在原来的位置上,视觉零变化。
- 实测边界:`HandlePlacement::Left` 热区 [边界-5, 边界+4]、`Right` [边界-4,
  边界+5]、`Axis::Vertical` [边界-4, 边界+5];分隔线仍压在面板那一像素边框上。

redis_view 值表表头(List/Set/ZSet/Hash 共用)
- 旧实现:6px 整条缩在左列右缘里,从边界右侧靠过来完全没有反馈。
- 现在与 db_view 表头一致:一条边界由左右两个单元格各渲染一半(各 4px),
  两半都留在自己的单元格内——单元格带 `overflow_hidden`,跨列的绝对定位会被
  裁掉、也会被后画的兄弟节点抢走鼠标。合计 8px 跨边界,分隔线位置不变。

踩到的坑(影响所有同类改动):分隔条容器默认是 `display: block`,此时
`justify_center`/`items_center` 静默失效,细线会被 block 布局丢到盒子起点;
必须显式 `.flex()`。

验证
- `cargo test -p one-ui --lib` 117 通过(新增 resize_handle_tests 3 个用例,
  分别覆盖三种形态:热区两侧各铺够、分隔线停在边界、边界两侧按下都能拖动;
  变异验证:把负边距改成 0,三条全红)。
- `cargo test -p redis_view --lib` 77 通过(新增 4 个用例:两半首尾相接跨边界、
  分隔线停在边界、左右两侧按下都能拖动第 0 列;变异验证:抓取区退回 6px 单边
  或去掉右列那一半,对应用例转红)。
- `cargo clippy -p one-ui -p redis_view --all-targets` 改动文件无新增告警;
  `cargo check -p main --all-targets` 通过。
分组(工作空间)的父子关系此前从不进入同步载荷,只有 name/color/icon/sort_order
会上传,而 local parent_id 是机器内自增 ID。于是另一台电脑应用同步数据后,
A/B 两层嵌套会变成两个并列分组。

- WorkspacePlainData 增加 format_version 与 parent_cloud_id,用「版本号 + 父云 ID」
  三态表达层级:0/缺省 = 旧载荷(无意见,保持本地层级),1 + None = 明确的根分组,
  1 + Some(id) = 指定父分组。旧载荷因此仍可区分,不会把本地树当成空层级覆盖。
- 父分组的 cloud_id 尚未落库时不冒充根分组(上传为旧载荷),避免在别的设备上
  永久拍平;随后由补传逻辑自愈。
- 上传侧按祖先优先排序(cycle-safe),保证父分组先拿到 cloud_id,子分组才能写入
  稳定引用;个人同步导出顺序同时收敛为 钥匙串 → 分组 → 连接。
- 下载侧 apply 阶段不写 parent_id,改由收尾的 reconcile_parent_links / finalize_pass
  统一对齐:顺序无关、幂等、只写真实变化,且不动 updated_at(避免多余上传)。
- 旧客户端留下的无层级载荷不再拍平本地嵌套,并在父分组已有 cloud_id 时自动补一次
  带层级的上传(受「未被本轮计划覆盖」约束,避免往返抖动)。
- 云同步与个人同步(git/WebDAV/目录)两条链路一起修,共用同一份载荷与解密入口。

回归测试:载荷三态往返、祖先优先排序、收尾对齐的幂等/过期父引用/根分组清除、
个人同步端到端递归嵌套还原,以及新增的引擎级端到端用例(假云端 + 双设备)。
cargo test -p one-core --lib 763 passed;三项突变验证确认用例确实卡住修复点。
@feigeCode
feigeCode merged commit 6166eb7 into main Oct 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant