Skip to content

fix(editor): stop external file syncs from stranding the dirty marker (#3165) - #3185

Merged
wgqqqqq merged 1 commit into
GCWing:mainfrom
xiechimon:fix/3165-external-sync-dirty-bracket
Sep 22, 2026
Merged

wgqqqqq merged 1 commit into
GCWing:mainfrom
xiechimon:fix/3165-external-sync-dirty-bracket

Conversation

@xiechimon

Copy link
Copy Markdown
Contributor

问题

Fixes #3165。外部程序(AI 代理、git checkout、脚本)改写已打开的文件后,编辑器缓冲区会正确刷新,但标签页仍然挂着「已修改」标记,而用户没有做过任何编辑。

根因

脏状态链路把「程序化同步」当成「用户编辑」处理,共三处:

  1. MonacoModelManager.setupContentChangeListener() 对每次 setValue 都重算 isDirty 并广播 monaco-model-dirty-changed,磁盘同步写入也会把模型翻成脏;
  2. CodeEditor.applyDiskSnapshotToEditor() 的清理 markAsSaved 被推迟到 queueMicrotask,组件中途卸载就会跳过清理,saved 元数据停留在过期状态;
  3. updateModelContent(markAsSaved=true) 清了标志但不广播任何事件,监听 dirty 事件的消费方保持旧的脏点。

改动

  • MonacoModelManager 新增 beginExternalSync()/endExternalSync() 括号:括号打开期间内容变更跳过脏重算与瞬时广播;用深度计数器实现,不配对的 endExternalSync 是 no-op,抑制态不会卡死。
  • updateModelContent(markAsSaved=true):setValue 包进括号,并补发 dirty-changed(false) 广播,让干净状态和脏状态一样对事件消费方可见。
  • CodeEditor:applyExternalContentToModel 的 setValue 包进括号;磁盘同步与编码切换重载两条路径改为同步调用 markAsSaved,去掉 microtask 延迟。

issue 建议 2(脏状态单一来源化)是跨组件重构,改动面大,本 PR 不包含,可后续单独讨论。

验证

  • 新测试 src/web-ui/src/tools/editor/services/MonacoModelManager.test.ts(5 用例,用 setMonacoRuntime 搭 fake runtime):在 base commit 上先跑为红(3 fail:缺广播 + 缺 bracket API),修复后转绿;含对照组用例确认括号外的真实用户编辑仍会标脏。
  • npx vitest run src/tools/editor:2431 passed 全绿。
  • pnpm run type-check:改动文件 0 错误(预存在的 3 个 @/generated/api 错误需要 cargo 生成步骤,与本 PR 无关)。
  • eslint 改动文件通过。

Testing level: fully tested(本地)。

AI-assisted 说明:实现与测试由 AI 编码代理协助完成,以上验证均在本地 checkout 实跑。

External writes to an open file (AI agent, git checkout, scripts) refreshed
the buffer correctly but left the tab showing "modified": the global content
change listener recomputed isDirty on every setValue with no way to tell a
disk sync apart from a user edit, and the clean-up markAsSaved ran in a
microtask that an unmount race could skip entirely.

- MonacoModelManager gains a beginExternalSync/endExternalSync bracket: while
  open, content changes skip the dirty recompute and the transient
  dirty-changed broadcast; unbalanced endExternalSync calls are no-ops so
  suppression can never get stuck on.
- updateModelContent(markAsSaved=true) brackets its setValue and broadcasts
  dirty-changed(false), making the clean transition visible to consumers the
  same way setValue made the dirty one visible.
- CodeEditor brackets the programmatic setValue in
  applyExternalContentToModel and settles markAsSaved synchronously in the
  disk-sync and encoding-reload paths instead of deferring to a microtask.

Closes GCWing#3165
Copilot AI lite review requested due to automatic review settings September 21, 2026 20:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wgqqqqq wgqqqqq self-assigned this Sep 22, 2026
@wgqqqqq
wgqqqqq merged commit ce048f9 into GCWing:main Sep 22, 2026
9 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.

[Bug]外部程序修改已打开文件后,编辑器仍显示「已修改」标记(用户未手动编辑)

3 participants