Conservatively report fs events that occurred during initial worktree scan

Max Brunsfeld and Nathan Sobo created

Co-authored-by: Nathan Sobo <nathan@zed.dev>

Change summary

crates/project/src/project.rs  |  5 ++
crates/project/src/worktree.rs | 60 +++++++++++++++++++++++++++---------
2 files changed, 49 insertions(+), 16 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -4604,7 +4604,10 @@ impl Project {
                                             typ: match change {
                                                 PathChange::Added => lsp::FileChangeType::CREATED,
                                                 PathChange::Removed => lsp::FileChangeType::DELETED,
-                                                PathChange::Updated => lsp::FileChangeType::CHANGED,
+                                                PathChange::Updated
+                                                | PathChange::AddedOrUpdated => {
+                                                    lsp::FileChangeType::CHANGED
+                                                }
                                             },
                                         })
                                         .collect(),

crates/project/src/worktree.rs 🔗

@@ -610,8 +610,6 @@ impl LocalWorktree {
                     }
                 }));
 
-                cx.emit(Event::UpdatedEntries(Default::default()));
-
                 if !updated_repos.is_empty() {
                     cx.emit(Event::UpdatedGitRepositories(updated_repos));
                 }
@@ -2072,6 +2070,7 @@ pub enum PathChange {
     Added,
     Removed,
     Updated,
+    AddedOrUpdated,
 }
 
 impl Entry {
@@ -2283,21 +2282,37 @@ impl BackgroundScanner {
 
         futures::pin_mut!(events_rx);
 
-        while let Some(mut events) = events_rx.next().await {
+        // Process any events that occurred while performing the initial scan. These
+        // events can't be reported as precisely, because there is no snapshot of the
+        // worktree before they occurred.
+        if let Some(mut events) = events_rx.next().await {
             while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) {
                 events.extend(additional_events);
             }
-
             if self.notify.unbounded_send(ScanState::Updating).is_err() {
-                break;
+                return;
             }
-
-            if !self.process_events(events).await {
-                break;
+            if !self.process_events(events, true).await {
+                return;
+            }
+            if self.notify.unbounded_send(ScanState::Idle).is_err() {
+                return;
             }
+        }
 
+        // Continue processing events until the worktree is dropped.
+        while let Some(mut events) = events_rx.next().await {
+            while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) {
+                events.extend(additional_events);
+            }
+            if self.notify.unbounded_send(ScanState::Updating).is_err() {
+                return;
+            }
+            if !self.process_events(events, false).await {
+                return;
+            }
             if self.notify.unbounded_send(ScanState::Idle).is_err() {
-                break;
+                return;
             }
         }
     }
@@ -2508,7 +2523,11 @@ impl BackgroundScanner {
         Ok(())
     }
 
-    async fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
+    async fn process_events(
+        &mut self,
+        mut events: Vec<fsevent::Event>,
+        received_before_initialized: bool,
+    ) -> bool {
         events.sort_unstable_by(|a, b| a.path.cmp(&b.path));
         events.dedup_by(|a, b| a.path.starts_with(&b.path));
 
@@ -2632,7 +2651,7 @@ impl BackgroundScanner {
 
         self.update_ignore_statuses().await;
         self.update_git_repositories();
-        self.build_change_set(prev_snapshot, event_paths);
+        self.build_change_set(prev_snapshot, event_paths, received_before_initialized);
         self.snapshot.lock().scan_completed();
         true
     }
@@ -2749,7 +2768,12 @@ impl BackgroundScanner {
         snapshot.entries_by_id.edit(entries_by_id_edits, &());
     }
 
-    fn build_change_set(&self, old_snapshot: Snapshot, event_paths: Vec<Arc<Path>>) {
+    fn build_change_set(
+        &self,
+        old_snapshot: Snapshot,
+        event_paths: Vec<Arc<Path>>,
+        received_before_initialized: bool,
+    ) {
         let new_snapshot = self.snapshot.lock();
         let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>();
         let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>();
@@ -2777,7 +2801,13 @@ impl BackgroundScanner {
                                 old_paths.next(&());
                             }
                             Ordering::Equal => {
-                                if old_entry.mtime != new_entry.mtime {
+                                if received_before_initialized {
+                                    // If the worktree was not fully initialized when this event was generated,
+                                    // we can't know whether this entry was added during the scan or whether
+                                    // it was merely updated.
+                                    change_set
+                                        .insert(old_entry.path.clone(), PathChange::AddedOrUpdated);
+                                } else if old_entry.mtime != new_entry.mtime {
                                     change_set.insert(old_entry.path.clone(), PathChange::Updated);
                                 }
                                 old_paths.next(&());
@@ -3604,7 +3634,7 @@ mod tests {
                 let len = rng.gen_range(0..=events.len());
                 let to_deliver = events.drain(0..len).collect::<Vec<_>>();
                 log::info!("Delivering events: {:#?}", to_deliver);
-                smol::block_on(scanner.process_events(to_deliver));
+                smol::block_on(scanner.process_events(to_deliver, false));
                 scanner.snapshot().check_invariants();
             } else {
                 events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap());
@@ -3616,7 +3646,7 @@ mod tests {
             }
         }
         log::info!("Quiescing: {:#?}", events);
-        smol::block_on(scanner.process_events(events));
+        smol::block_on(scanner.process_events(events, false));
         scanner.snapshot().check_invariants();
 
         let (notify_tx, _notify_rx) = mpsc::unbounded();