From 682fee633d924e4ab9833048cfdac48d06e5f469 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:14:38 +0800 Subject: [PATCH] fix(vfs): report files deleted on macOS again (#1435) fix(vfs): report files deleted on macOS again Since the watcher queues raw notify events (#1412), deleting a file on macOS was never reported, so its symbols stayed in the index and the tree kept it (the project smoke tests failed for every language). FSEvents delivers a deleted file's coalesced history flags as create, remove, then modify events. Create + remove merged into a transient path, the following modify turned it back into a creation, and a creation of a path that no longer exists is dropped. A modify no longer revives a transient path; only a create does, and the disk decides as before. --- crates/ide-vfs/src/watch.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/ide-vfs/src/watch.rs b/crates/ide-vfs/src/watch.rs index 6cdd1af38..253d7a826 100644 --- a/crates/ide-vfs/src/watch.rs +++ b/crates/ide-vfs/src/watch.rs @@ -404,6 +404,9 @@ fn merge(prev: Option, next: Change) -> Change { match (prev, next) { (None, n) => n, (Some(Vcs), _) | (_, Vcs) => Vcs, + // FSEvents reports a deleted file's history as create, remove, then modify: a + // modify can't bring back a path that went away, only a create can. + (Some(Transient), Modified) => Transient, (Some(Created | Transient), Modified | Created) => Created, (Some(Created | Transient), Removed) => Transient, (Some(Removed), Created | Modified) => Modified, @@ -630,7 +633,7 @@ impl EventFilter { #[cfg(test)] mod tests { use super::*; - use notify::event::{CreateKind, DataChange, RemoveKind}; + use notify::event::{CreateKind, DataChange, MetadataKind, RemoveKind}; fn ev(kind: EventKind, paths: Vec) -> Event { Event { kind, paths, attrs: Default::default() } @@ -788,6 +791,24 @@ mod tests { assert_eq!(batch.ignored, vec![root.join("debug.log")], "ignored files are reported apart"); } + /// What FSEvents delivers when a file is deleted: its coalesced history flags as + /// create, remove, then modify events, all for a path that no longer exists. + #[test] + fn fsevents_deletion_with_history_flags_is_a_removal() { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); + let filter = EventFilter::new(root.clone(), &[]); + let gone = root.join("src/Fresh.java"); + let batch = filter.batch(&[ + ev(EventKind::Create(CreateKind::File), vec![gone.clone()]), + ev(EventKind::Remove(RemoveKind::File), vec![gone.clone()]), + ev(EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)), vec![gone.clone()]), + ev(EventKind::Modify(ModifyKind::Data(DataChange::Content)), vec![gone.clone()]), + ]); + assert_eq!(batch.removed, vec![gone]); + assert!(batch.created.is_empty() && batch.modified.is_empty()); + } + #[test] fn atomic_save_temp_files_are_not_reported() { let tmp = tempfile::tempdir().unwrap();