Skip to content

Fix stale diagnostic error markers in file tree#49333

Merged
SomeoneToIgnore merged 4 commits intozed-industries:mainfrom
ArthurDEV44:fix/stale-diagnostic-markers-file-tree
Mar 23, 2026
Merged

Fix stale diagnostic error markers in file tree#49333
SomeoneToIgnore merged 4 commits intozed-industries:mainfrom
ArthurDEV44:fix/stale-diagnostic-markers-file-tree

Conversation

@ArthurDEV44
Copy link
Copy Markdown
Contributor

@ArthurDEV44 ArthurDEV44 commented Feb 17, 2026

Summary

Fixes #48289
Closes #52021

Diagnostic error markers (red/yellow dots) in the project panel file tree persisted after errors were resolved. Three root causes:

  • WorktreeUpdatedEntries ignored — when files changed on disk (e.g. yarn install), stale diagnostic summaries were never cleared. Added invalidate_diagnostic_summaries_for_updated_entries() to clear summaries for Removed/Updated/AddedOrUpdated paths.
  • Missing DiagnosticsUpdated emission on server stopstop_local_language_server() cleared summaries and sent proto messages but never emitted LspStoreEvent::DiagnosticsUpdated, so the project panel never refreshed.
  • Buffer reload not handled — reloading a buffer from disk did not clear stale summaries. Added BufferEvent::Reloaded handler.

All three paths also send zeroed UpdateDiagnosticSummary proto messages to downstream collab clients.

Test plan

  • ./script/clippy passes
  • cargo test -p project -p project_panel -p worktree passes (319 tests, 0 failures)
  • 4 new tests added:
    • test_diagnostic_summaries_cleared_on_worktree_entry_removal
    • test_diagnostic_summaries_cleared_on_worktree_entry_update
    • test_diagnostic_summaries_cleared_on_server_restart
    • test_diagnostic_summaries_cleared_on_buffer_reload
  • Manual testing: error markers clear when files change on disk
  • Manual testing: error markers clear on LSP restart

Release Notes:

  • Fixed stale diagnostic data persisting after file reloads, server restarts and FS entry removals

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Feb 17, 2026
@zed-community-bot zed-community-bot bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Feb 17, 2026
@SomeoneToIgnore SomeoneToIgnore self-assigned this Feb 24, 2026
Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Sorry for coming late, the diagnostics part is relatively hard at places and somehow I failed to express what I found odd in the PR for some time.

#48289 mentions removing files and language server restarts, those parts are covered good in the PR.

The rest, buffer reload and edited, seem a bit out of place, I've left some comments.

Overall, seems that there's indeed some invalidation gist here that needs solving, it's the way to solve it has to be slightly different.

@ArthurDEV44
Copy link
Copy Markdown
Contributor Author

Good call @SomeoneToIgnore, I'll hoist downstream_client.clone() before the loop in both on_buffer_reloaded and invalidate_diagnostic_summaries_for_updated_entries.

(Same pattern exists in stop_local_language_server at line 10745, but I'll at least fix it in my new code.)

You're right, on_buffer_edited already calls pull_workspace_diagnostics (line 7877), so open buffers are covered.

I was trying to handle files not open in any editor that still have stale diagnostic_summaries (e.g. after yarn install modifies files on disk). But thinking about it more, this is the server's job: Zed already sends workspace/didChangeWatchedFiles via update_local_worktree_language_servers, and the server can then push new diagnostics or send workspace/diagnostic/refresh (handled at line 1149).

I'll drop Updated and AddedOrUpdated and keep only Removed, deleted files have no server-driven recovery path.

The scenario: user has a file open with diagnostic errors, does "Revert File" (or the file changes externally). If the resulting content is identical to what's in the buffer, BufferEvent::Reloaded fires unconditionally (buffer.rs:1668), but Edited does not: did_edit (buffer.rs:2845) early-returns when apply_diff produces no text changes.

Since on_buffer_event ignores Reloaded (_ => {} at line 4429), on_buffer_edited never runs, pull_workspace_diagnostics is never triggered, and stale summaries persist in the file tree.

Instead of clearing summaries directly, I could handle Reloaded in on_buffer_event and just call pull_workspace_diagnostics for the relevant servers, same as on_buffer_edited does. That keeps the server in charge. Would that work?

@SomeoneToIgnore
Copy link
Copy Markdown
Contributor

Thank you, all ideas sounds very reasonable — let's start small and ensure language servers do their part of the job.

One small note that a reload scenario is described pretty well, which means we can try and create a test for this use case specifically.

Diagnostic error markers (red/yellow dots) in the project panel persisted
after errors were resolved. Three root causes were identified and fixed:

1. WorktreeUpdatedEntries was ignored in LspStore — when files changed on
   disk (e.g. after `yarn install`), stale diagnostic summaries were never
   cleared. Added `invalidate_diagnostic_summaries_for_updated_entries()`
   to clear summaries for Removed/Updated/AddedOrUpdated paths.

2. stop_local_language_server cleared diagnostic summaries and sent proto
   messages but never emitted `LspStoreEvent::DiagnosticsUpdated`, so the
   project panel was never notified to refresh its indicators.

3. Buffer reloads (Revert File, external changes) did not clear stale
   diagnostic summaries. Added `BufferEvent::Reloaded` handler that clears
   summaries for the reloaded buffer's path.

All three paths also send zeroed `UpdateDiagnosticSummary` proto messages
to downstream collab clients.

Closes zed-industries#48289
@ArthurDEV44 ArthurDEV44 force-pushed the fix/stale-diagnostic-markers-file-tree branch from aa8fd4c to 2776853 Compare March 19, 2026 15:16
@ArthurDEV44
Copy link
Copy Markdown
Contributor Author

Applied the requested changes :

1. Hoisted downstream_client.clone() before the loop in invalidate_diagnostic_summaries_for_updated_entries, and moved the if let Some(...) check outside the inner server_id loop since it doesn't change between iterations.

2. Removed Updated/AddedOrUpdated from invalidate_diagnostic_summaries_for_updated_entries, only Removed triggers summary clearing now.

3. Rewrote on_buffer_reloaded to call pull_workspace_diagnostics for each relevant language server instead of clearing summaries directly.

4. Rewrote the buffer reload test with a fake LSP that has workspace diagnostics support, verifying the pull is triggered after reload.

One thing to flag: apply_workspace_diagnostic_report skips open buffers (line 12250 "diagnostics from a document pull should win over diagnostics from a workspace pull"). So for the specific scenario where a buffer is open and reloaded without content changes, pull_workspace_diagnostics is triggered but its response won't update that file's summaries. In practice the server should handle this through didChangeWatchedFiles or document-level pulls from the editor, but wanted to call it out.

Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Thank you, I now have mostly style nits and one large question here: it seems more reasonable to update buffer's diagnostics, not the workspace (many buffers) when a single buffer is reloaded?

LspStore has pub fn pull_diagnostics_for_buffer which should suffice for such cases?
Workspace diagnostics refresh can be, again, by the server's request or other, open editors' logic.

That should also help with the notes of the regular buffer's pull prevalence over workspace diagnostics: hopefully the new code structure removes such shenanigans out of this particular test case.
As a downside, we'd be better storing some HashMap<BufferId, Task<()>> and removing that on buffer invalidation — .detach() is bad in this (and many more) cases as we loose the cancellation control.

…simplify

- on_buffer_reloaded: use pull_diagnostics_for_buffer (document-level)
  instead of pull_workspace_diagnostics. Store the task in
  buffer_reload_tasks HashMap for cancellation control, clean up
  on buffer invalidation.
- Rename invalidate_diagnostic_summaries_for_updated_entries to
  invalidate_diagnostic_summaries_for_removed_entries.
- Replace match with .filter() on PathChange::Removed.
- Merge double iteration on summaries_by_server_id into one loop.
- Replace .log_err() with .ok() on downstream client send.
- Rewrite test to use DocumentDiagnosticRequest (document-level pull).
@ArthurDEV44
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback.

Here's what changed:

  • on_buffer_reloaded now calls pull_diagnostics_for_buffer instead of pull_workspace_diagnostics. The task is stored in a HashMap<BufferId, Task> on LspStore (not detached), and cleaned up in the observe_release callback when the buffer refcount drops to zero.
  • Renamed to invalidate_diagnostic_summaries_for_removed_entries.
  • Replaced the match with .iter().filter(|(_, _, change)| *change == PathChange::Removed).
  • Merged the two iterations on summaries_by_server_id into one loop.
  • .log_err().ok().
  • Test rewritten around DocumentDiagnosticRequest since we're doing a document-level pull now.

The upstream commit b9dbc86 causes FakeFs::save to trigger
automatic buffer reloads via FS events. Remove the explicit
buffer.reload() call — the FS event handles it.
Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Thank you, this seems quite clean compared to the original version.

@SomeoneToIgnore SomeoneToIgnore merged commit 45ca651 into zed-industries:main Mar 23, 2026
31 checks passed
AmaanBilwar pushed a commit to AmaanBilwar/zed that referenced this pull request Mar 23, 2026
## Summary

Fixes zed-industries#48289
Closes zed-industries#52021

Diagnostic error markers (red/yellow dots) in the project panel file
tree persisted after errors were resolved. Three root causes:

- **WorktreeUpdatedEntries ignored** — when files changed on disk (e.g.
`yarn install`), stale diagnostic summaries were never cleared. Added
`invalidate_diagnostic_summaries_for_updated_entries()` to clear
summaries for `Removed`/`Updated`/`AddedOrUpdated` paths.
- **Missing DiagnosticsUpdated emission on server stop** —
`stop_local_language_server()` cleared summaries and sent proto messages
but never emitted `LspStoreEvent::DiagnosticsUpdated`, so the project
panel never refreshed.
- **Buffer reload not handled** — reloading a buffer from disk did not
clear stale summaries. Added `BufferEvent::Reloaded` handler.

All three paths also send zeroed `UpdateDiagnosticSummary` proto
messages to downstream collab clients.

## Test plan

- [x] `./script/clippy` passes
- [x] `cargo test -p project -p project_panel -p worktree` passes (319
tests, 0 failures)
- [x] 4 new tests added:
  - `test_diagnostic_summaries_cleared_on_worktree_entry_removal`
  - `test_diagnostic_summaries_cleared_on_worktree_entry_update`
  - `test_diagnostic_summaries_cleared_on_server_restart`
  - `test_diagnostic_summaries_cleared_on_buffer_reload`
- [x] Manual testing: error markers clear when files change on disk
- [x] Manual testing: error markers clear on LSP restart

Release Notes:

- Fixed stale diagnostic data persisting after file reloads, server
restarts and FS entry removals
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions

Projects

None yet

2 participants