Skip to content

feat(editor): 未保存草稿保护、保存冲突检测与内置查找替换 - #600

Open
nyantused-cpun wants to merge 1 commit into
omdsh-dev:mainfrom
nyantused-cpun:feat/editor-draft-protection
Open

feat(editor): 未保存草稿保护、保存冲突检测与内置查找替换#600
nyantused-cpun wants to merge 1 commit into
omdsh-dev:mainfrom
nyantused-cpun:feat/editor-draft-protection

Conversation

@nyantused-cpun

Copy link
Copy Markdown

背景

三项都在「编辑器 ↔ 磁盘」边界上,不改 ctx.betterSidebar 的服务方法签名,不碰 DSH 源码。设计文档:docs/plans/2026-09-09-editor-draft-protection-design.md

1. 未保存草稿保护(新增 src/client/editor-dirty.ts

实施前:脏稿确认只挂在手动刷新按钮上;关标签页(closeTab)、删文件(closePathTabs)都不问,也没有 beforeunload——草稿直接消失。

  • EditorHost 把工具栏报告里已有的 dirty 登记进模块级登记表(不新增 viewer 契约),卸载即清除,所以条目不会比草稿活得更久。
  • 三条路径共用 confirmDiscardDraft:标签页 X / 中键 / 右键菜单「关闭(其他/左/右)」→ Sidebar.actions.closeTab;文件树删除 → closePathTabs(文案由调用方传入,tree-mutations.ts 保持无 i18n 依赖);浏览器刷新/关页 → beforeunload(按 dirtyCount() 全量判断,因为整个页面要走了)。
  • 确认后立即清除条目,避免「连续关闭多个脏 tab」重复弹窗;拒绝删除时标签页保留。

2. 保存的乐观并发门 + 并发临时文件互删修复

实施前:模型/另一个标签页/外部编辑器改了文件后,旧草稿 Ctrl/Cmd+S 会静默覆盖;且临时文件名固定为 ${path}.dsh-sidebar-tmp-${process.pid},同一路径两次保存并发时先完成者的 rm(tmp) 会删掉后完成者的临时文件(ENOENT)。

  • 协议(两字段都可省,向后兼容):fs.read 返回 mtimeMsfs.write 接受 expectedMtimeMs,基线不符 → 409 fs-conflict写临时文件之前判定,不留垃圾);成功返回新基线 mtimeMs
  • 编辑器收到 fs-conflict保留草稿、显示警告横幅 +「重新载入」(FileViewerProps.onReload → 已有的、会先确认脏稿的刷新路径)。
  • 临时文件改为 .${basename}.dsh-write-${randomUUID()}.tmp(与 fs-operations.ts 上传路由同款),每次保存有自己的临时兄弟文件。

3. 内置查找/替换

@codemirror/search 此前在 dependencies 里但 src/ 零引用(白装)。接入 search({ top: true }) + searchKeymapCtrl/Cmd+FCtrl/Cmd+HCtrl/Cmd+GCtrl/Cmd+D),面板文案经 EditorState.phrases.of(...) 接入本插件词典(CodeMirror 默认只有英文),工具栏新增放大镜按钮(预览态下先切编辑态)。

验证

  • tsc --noEmit ✅、eslint src tests --max-warnings=0
  • 新增 21 例测试:editor-unsaved-guard.spec.tsx(8) / fs-write-route.spec.ts(5) / text-editor-conflict.spec.tsx(4) / text-editor-search.spec.tsx(4)
  • 19 个语言词典同步 12 个新 key(tests/locales.spec.ts 的键集相等守卫通过)
  • 全量 vitest:1342 passed / 4 failed。4 个失败均为本机环境所致,与本次改动无关:3 个 Windows 符号链接 EPERM(需开发者模式/管理员;CI 在 ubuntu 恒绿),1 个 caps status entries 在全量并发下 5s 超时(单独跑 2.1s 通过)。设置 GIT_AUTHOR_*/GIT_COMMITTER_* 后 smoke 的 revert/cherry-pick 两项由失败转为通过。
  • 并发用例在 Windows 上显式允许 EPERM(平台 rename 行为)但拒绝 ENOENT(互删的特征),并断言无临时文件残留。

已知取舍

mtime 基线不是内容哈希(同毫秒并发写仍可能双双通过,此时以最后一次 rename 为准);外部 viewer 不参与脏稿保护(只有内置文本编辑器报告 dirty);跨会话切换不弹窗;不做跨文件搜索(按仓库理念留给生态插件)。

三项编辑器/磁盘边界上的改动,均不改变 ctx.betterSidebar 的服务方法签名:

1. 未保存草稿保护(新增 src/client/editor-dirty.ts)
   - EditorHost 把工具栏的 dirty 登记进模块级登记表,卸载即清除
   - 关标签页(X / 中键 / 右键菜单)、删文件(closePathTabs)、
     页面刷新/关闭(beforeunload)三条路径共用一次确认
2. 保存的乐观并发门
   - fs.read 返回 mtimeMs;fs.write 接受 expectedMtimeMs,基线不符
     返回 409 fs-conflict 而不是覆盖对方字节;成功回报新基线
   - 编辑器显示冲突横幅 +「重新载入」,草稿不丢
   - 顺带修复:临时文件改为随机命名兄弟文件,同一路径的并发保存
     不再互删(原固定名 .dsh-sidebar-tmp-<pid>)
3. 内置查找/替换
   - 接入 @codemirror/search(依赖此前白装)+ searchKeymap,
     面板文案经 EditorState.phrases 接入本插件词典

测试:新增 21 例(guard 8 / fs.write 路由 5 / 冲突 4 / 查找 4);
19 个语言词典同步 12 个新 key。设计见
docs/plans/2026-09-09-editor-draft-protection-design.md。
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