Skip to content

fix(markdown): prompt on external disk changes while the document is dirty - #611

Closed
ONEGAYI wants to merge 1 commit into
cweijan:mainfrom
ONEGAYI:fix/markdown-external-change-backstop
Closed

ONEGAYI wants to merge 1 commit into
cweijan:mainfrom
ONEGAYI:fix/markdown-external-change-backstop

Conversation

@ONEGAYI

@ONEGAYI ONEGAYI commented Sep 16, 2026

Copy link
Copy Markdown

Problem

VS Code never reloads a dirty text document when its file changes on disk — no reload, no event. The markdown editor applies webview input to the document via applyEdit and only writes the file on save, so the document is dirty almost constantly. As a result:

  • external writers (formatters, AI agents, other editors) silently vanish from an open editor view, and
  • the next manual save silently clobbers their changes with the stale buffer.

This is very visible when an agent edits files while the user has them open in the WYSIWYG markdown editor.

Fix

The Handler already owns a FileSystemWatcher whose fileChange events were never consumed by anyone — this PR consumes them for the markdown editor:

  • Pure decision core (src/service/markdown/externalChangeGuard.ts): strict UTF-8 normalization (non-UTF-8 files are left alone — VS Code may decode documents with other encodings, and adopting a mojibake interpretation would corrupt them) and shouldAskAboutDiskChange: ask iff the document is dirty, the disk text differs from every current/recent buffer form (echo suppression), and the user has not already declined that exact text.
  • Prompt: "Load disk version" / "Keep my edits" (ext.markdown.externalFileChange, localized in all 10 package.nls files).
    • Load re-reads the disk right before applying (never a stale snapshot), clears the pending sync, applyEdit + document.save() + pushes the content to the webview. Same contract as VS Code's own conflict flow: local unsaved edits are discarded.
    • Keep (and dismissing the prompt via Esc) records the declined text — asked once per distinct disk content, so streaming writers don't spam.
  • Save guard: workspace.onWillSaveTextDocument + event.waitUntil parks saves of the document while a decision is pending, so autosave or a reflexive Ctrl+S cannot silently overwrite the change the prompt is asking about.
  • Echo suppression for focus-outside saves: recent pre-flush document snapshots (bounded ring) are treated as buffer forms.
  • A failed save after loading warns instead of failing silently (ext.markdown.externalSaveFailed).
  • Unit tests for the decision core: npm run test:unit (node:test; 10 cases incl. GBK/UTF-16 rejection, echo orderings, emptied disk). Note: requires Node ≥ 22.6 for native TS type stripping.

Known limitations (deliberately out of scope)

  • Same file open in two panels → duplicate prompts, and a Load in one panel overwrites the other panel's unsaved edits (pre-existing shared-document model, made visible by this feature)
  • Watcher blind spots: non-workspace virtual schemes have no watcher; file names containing glob metacharacters (report[1].md) silently fail to match; deletes/renames produce no prompt
  • Millisecond read-modify-write races remain around Load (no atomic file API exists)
  • If the prompt is relegated to the notification center and ignored, saves of that document wait until it is answered

Validation

  • npm run test:unit — 10/10 pass
  • npm run build — pass
  • npx eslint on changed files — clean

Design reference: ONEGAYI/vscode-markdown-editor-hardened (external-change-watcher), adapted to this extension's handler/sync model with two review rounds of hardening (UTF-8 strictness, prompt serialization, save-guard deadlock avoidance, echo rings).

Note: this PR touches the import block of markdownEditorProvider.ts and adds the same two config lines (test script / tsconfig exclude) as #610 — expect a trivial merge conflict if that lands first.

…dirty

VS Code silently refuses to reload a dirty text document when its file
changes on disk (no event fires). The markdown editor applies webview
input to the document via applyEdit and only writes the file on save, so
the document is dirty almost constantly: external writers (formatters,
AI agents, other editors) vanish from an open editor view, and the next
save clobbers their changes with the stale buffer.

The Handler already owned a FileSystemWatcher whose fileChange events
were never consumed. This change consumes them:

- add src/service/markdown/externalChangeGuard.ts: pure decision core
  (strict UTF-8 normalization — non-UTF-8 files are left alone — and
  shouldAskAboutDiskChange: ask iff dirty, disk differs from every
  current/recent buffer form, and the text was not already declined)
- on divergence, prompt 'Load disk version' / 'Keep my edits'
  (ext.markdown.externalFileChange, localized in all 10 package.nls
  files). Load re-reads the disk right before applying, clears the
  pending sync, applies the edit, saves and pushes the content to the
  webview; Keep (and prompt dismissal) records the declined text —
  asked once per distinct disk content, so streaming writers do not spam
- saves are parked via workspace.onWillSaveTextDocument while a
  decision is pending, so autosave or a reflexive Ctrl+S cannot
  silently overwrite the change the prompt is asking about
- recent pre-flush document snapshots suppress echo prompts when a
  save was triggered with focus outside the webview
- a failed save after loading warns instead of failing silently
  (ext.markdown.externalSaveFailed, all 10 locales)
- unit tests for the decision core (npm run test:unit, node:test)

Known limitations (deliberately out of scope): duplicate prompts when
the same file is open in two panels (pre-existing shared-document
model); watcher blind spots for non-workspace virtual schemes, file
names containing glob metacharacters, and deletes/renames; millisecond
read-modify-write races around Load; a save hangs until the user
answers a prompt they relegated to the notification center. The
unit-test script requires Node >= 22.6 (native TS type stripping).

Design reference: ONEGAYI/vscode-markdown-editor-hardened
(external-change-watcher).
ONEGAYI added a commit to ONEGAYI/vscode-office that referenced this pull request Sep 17, 2026
- 进行中工作索引:上游待合并 PR 增补 cweijan#613(列表 marker 上游移植);
  fork issues 行改写为已全部修复关闭,逐条标注修复提交
  (e166a00 / a117f6b / 8638b2f / 1fbe850)及 v4.3.0-fork.1 交付
- 架构关键事实:外部变更兜底的 watcher 覆盖与双面板限制改为
  已在 fork 内修复(1fbe850),上游侧待 cweijan#611 合并后发 PR,
  避免后续误读为待修
@ONEGAYI

ONEGAYI commented Oct 7, 2026

Copy link
Copy Markdown
Author

Closing due to inactivity — no review feedback after ~3 weeks. The commits remain available via refs/pull/611/head in this PR if anyone wants to revisit. Thanks!

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