Emit loaded events for lazily loaded paths in worktree

Max Brunsfeld created

Change summary

crates/project/src/worktree.rs       | 30 +++++++++++++++++----
crates/project/src/worktree_tests.rs | 42 ++++++++++++++++++++++++++++-
2 files changed, 64 insertions(+), 8 deletions(-)

Detailed changes

crates/project/src/worktree.rs 🔗

@@ -2759,6 +2759,10 @@ impl EntryKind {
         )
     }
 
+    pub fn is_unloaded(&self) -> bool {
+        matches!(self, EntryKind::UnloadedDir)
+    }
+
     pub fn is_file(&self) -> bool {
         matches!(self, EntryKind::File(_))
     }
@@ -3773,6 +3777,7 @@ impl BackgroundScanner {
         let mut changes = Vec::new();
         let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>();
         let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>();
+        let mut last_newly_loaded_dir_path = None;
         old_paths.next(&());
         new_paths.next(&());
         for path in event_paths {
@@ -3820,20 +3825,33 @@ impl BackgroundScanner {
                                     changes.push((old_entry.path.clone(), old_entry.id, Removed));
                                     changes.push((new_entry.path.clone(), new_entry.id, Added));
                                 } else if old_entry != new_entry {
-                                    changes.push((new_entry.path.clone(), new_entry.id, Updated));
+                                    if old_entry.kind.is_unloaded() {
+                                        last_newly_loaded_dir_path = Some(&new_entry.path);
+                                        changes.push((
+                                            new_entry.path.clone(),
+                                            new_entry.id,
+                                            Loaded,
+                                        ));
+                                    } else {
+                                        changes.push((
+                                            new_entry.path.clone(),
+                                            new_entry.id,
+                                            Updated,
+                                        ));
+                                    }
                                 }
                                 old_paths.next(&());
                                 new_paths.next(&());
                             }
                             Ordering::Greater => {
+                                let is_newly_loaded = self.phase == InitialScan
+                                    || last_newly_loaded_dir_path
+                                        .as_ref()
+                                        .map_or(false, |dir| new_entry.path.starts_with(&dir));
                                 changes.push((
                                     new_entry.path.clone(),
                                     new_entry.id,
-                                    if self.phase == InitialScan {
-                                        Loaded
-                                    } else {
-                                        Added
-                                    },
+                                    if is_newly_loaded { Loaded } else { Added },
                                 ));
                                 new_paths.next(&());
                             }

crates/project/src/worktree_tests.rs 🔗

@@ -15,6 +15,7 @@ use serde_json::json;
 use std::{
     env,
     fmt::Write,
+    mem,
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -313,6 +314,21 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
         .await;
 
+    let tree_updates = Arc::new(Mutex::new(Vec::new()));
+    tree.update(cx, |_, cx| {
+        let tree_updates = tree_updates.clone();
+        cx.subscribe(&tree, move |_, _, event, _| {
+            if let Event::UpdatedEntries(update) = event {
+                tree_updates.lock().extend(
+                    update
+                        .iter()
+                        .map(|(path, _, change)| (path.clone(), *change)),
+                );
+            }
+        })
+        .detach();
+    });
+
     // The symlinked directories are not scanned by default.
     tree.read_with(cx, |tree, _| {
         assert_eq!(
@@ -365,6 +381,14 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
             ]
         );
     });
+    assert_eq!(
+        mem::take(&mut *tree_updates.lock()),
+        &[
+            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
+            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
+            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+        ]
+    );
 
     // Expand a subdirectory of one of the symlinked directories.
     tree.read_with(cx, |tree, _| {
@@ -396,6 +420,21 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
             ]
         );
     });
+
+    assert_eq!(
+        mem::take(&mut *tree_updates.lock()),
+        &[
+            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+            (
+                Path::new("deps/dep-dir3/src/e.rs").into(),
+                PathChange::Loaded
+            ),
+            (
+                Path::new("deps/dep-dir3/src/f.rs").into(),
+                PathChange::Loaded
+            )
+        ]
+    );
 }
 
 #[gpui::test]
@@ -1114,7 +1153,6 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
                     Ok(ix) | Err(ix) => ix,
                 };
                 match change_type {
-                    PathChange::Loaded => entries.insert(ix, entry.unwrap()),
                     PathChange::Added => entries.insert(ix, entry.unwrap()),
                     PathChange::Removed => drop(entries.remove(ix)),
                     PathChange::Updated => {
@@ -1123,7 +1161,7 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
                         assert_eq!(existing_entry.path, entry.path);
                         *existing_entry = entry;
                     }
-                    PathChange::AddedOrUpdated => {
+                    PathChange::AddedOrUpdated | PathChange::Loaded => {
                         let entry = entry.unwrap();
                         if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
                             *entries.get_mut(ix).unwrap() = entry;