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

Open
#3,165 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
65/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
typescript
Domain
frontend

Research direction

Start in src/web-ui/src/tools/editor/services/MonacoModelManager.ts, especially setupContentChangeListener() and updateModelContent(), then trace the external-sync path through CodeEditor.tsx and applyDiskSnapshotToEditor(). Compare the manager-level dirty state, component-level hasChanges state, and emitted events. Done means an external file refresh leaves the buffer and disk consistent without stale dirty indicators or an unnecessary unsaved prompt.

Written by the indexing model from the issue text.

Description

问题描述

当外部程序(AI 代理 / 脚本 / git checkout)修改了当前已在编辑器中打开的文件时,编辑器会正确地把可见缓冲区刷新为新磁盘内容,但标签页/编辑器仍显示「已修改 / 未保存更改」标记——即便用户从未手动输入任何内容,且此时缓冲区内容已与磁盘一致。

复现步骤

  1. 在编辑器中打开一个文件,保持标签页打开。
  2. 用外部程序修改磁盘上的同一文件(例如 AI 代理写文件,或 git checkout)。
  3. 编辑器检测到变更,并把可见内容刷新为最新磁盘内容。
  4. 观察:标签页/编辑器仍显示「已修改」标记。

期望行为

外部变更被同步、缓冲区与磁盘一致后,「已修改 / 脏」标记应被清除。

实际行为

脏标记残留(或短暂亮起),导致用户即使没有任何未保存编辑,也不得不「关闭而不保存」。

环境

  • 操作系统:Windows
  • OpenBitFun 版本:最新版(反馈时刚完成升级)
  • 触发条件:外部程序写入已打开的文件

根因分析(代码级)

1. 全局脏标记监听器没有「程序化同步」抑制

src/web-ui/src/tools/editor/services/MonacoModelManager.tssetupContentChangeListener()

const listener = model.onDidChangeContent(() => {
  const metadata = this.modelMetadata.get(uriString);
  if (metadata) {
    const currentVersionId = model.getAlternativeVersionId();
    metadata.isDirty = this.documentModels.has(uriString)
      ? model.getValue() !== metadata.originalContent
      : currentVersionId !== metadata.savedVersionId;
    // 同时广播 monaco-model-dirty-changed 和 ModelContentChanged
  }
});

这里没有任何「正在同步」的防护。每一次 model.setValue()——包括外部磁盘同步——都会把 isDirty 重新计算为 true

相比之下,src/web-ui/src/tools/editor/components/CodeEditor.tsx(约第 913 行)的组件级监听器是有防护的:

model.onDidChangeContent(() => {
  if (isLoadingContentRef.current) {
    return;  // 加载期间被抑制
  }
  // ...
  setHasChanges(changed);
});

因此同一次外部同步下,两套脏状态(hasChanges 状态 vs metadata.isDirty)会得出不一致的结果。

2. 脏标记同步置位、异步清除

CodeEditor.tsxapplyDiskSnapshotToEditor() 调用 applyExternalContentToModel(),后者执行 model.setValue(fileContent)(经全局监听器同步置脏),随后才在 queueMicrotask 里通过 monacoModelManager.markAsSaved(modelKey) 清除。

3. updateModelContent 默认 markAsSaved = false

MonacoModelManager.updateModelContent(filePath, content, markAsSaved = false)

model.setValue(content);        // 全局监听器把 isDirty 置为 true
if (metadata) {
  if (markAsSaved) {            // 默认 false → 永远不会清除
    metadata.savedVersionId = /* ... */;
    metadata.originalContent = content;
    metadata.isDirty = false;
  }
  // ...
}

任何把磁盘内容同步进已打开模型、但没有传 markAsSaved=true 的调用方,都会让模型永久处于脏状态。

4. 事件不对称

setValue 会广播 ModelContentChanged(标签页 / 文件列表的消费方据此把条目标脏),但 markAsSaved() 只派发 monaco-model-dirty-changed,不会发出对应的「内容已同步」事件。因此消费方会保留过期的脏标记。

修复建议

  1. 在 manager 层增加「正在同步」标志,让程序化 setValue 不标脏(或当内容是磁盘真值时,让 updateModelContent 默认采用「已保存」语义)。
  2. 让脏状态单一来源化——UI 读取 metadata.isDirty,而不是维护一套平行的 hasChanges
  3. 外部同步时同步地清除脏标记,并广播一个对应的「内容已同步 / 已保存」事件给标签页 / 文件列表消费方。
Dominant language
Rust
Stars
2.3k
Forks
236
Avg merge
3h 7m
Merged PRs (30d)
596

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from GCWing/OpenBitFun

All issues in GCWing/OpenBitFun

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.