Eagerly populate child entries when copying a directory via RPC

Antonio Scandurra created

Change summary

crates/project/src/fs.rs       | 42 ++++++++++++++++++-------
crates/project/src/project.rs  | 29 ++++++++++++----
crates/project/src/worktree.rs | 60 +++++++++++++++++++++++++++---------
crates/rpc/proto/zed.proto     |  3 +
4 files changed, 98 insertions(+), 36 deletions(-)

Detailed changes

crates/project/src/fs.rs 🔗

@@ -15,7 +15,12 @@ use text::Rope;
 pub trait Fs: Send + Sync {
     async fn create_dir(&self, path: &Path) -> Result<()>;
     async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
-    async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
+    async fn copy(
+        &self,
+        source: &Path,
+        target: &Path,
+        options: CopyOptions,
+    ) -> Result<Vec<PathBuf>>;
     async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
     async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
@@ -91,15 +96,21 @@ impl Fs for RealFs {
         Ok(())
     }
 
-    async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+    async fn copy(
+        &self,
+        source: &Path,
+        target: &Path,
+        options: CopyOptions,
+    ) -> Result<Vec<PathBuf>> {
         if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
             if options.ignore_if_exists {
-                return Ok(());
+                return Ok(Default::default());
             } else {
                 return Err(anyhow!("{target:?} already exists"));
             }
         }
 
+        let mut paths = vec![target.to_path_buf()];
         let metadata = smol::fs::metadata(source).await?;
         let _ = smol::fs::remove_dir_all(target).await;
         if metadata.is_dir() {
@@ -109,15 +120,17 @@ impl Fs for RealFs {
                 if let Ok(child) = child {
                     let child_source_path = child.path();
                     let child_target_path = target.join(child.file_name());
-                    self.copy(&child_source_path, &child_target_path, options)
-                        .await?;
+                    paths.extend(
+                        self.copy(&child_source_path, &child_target_path, options)
+                            .await?,
+                    );
                 }
             }
         } else {
             smol::fs::copy(source, target).await?;
         }
 
-        Ok(())
+        Ok(paths)
     }
 
     async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
@@ -547,7 +560,12 @@ impl Fs for FakeFs {
         Ok(())
     }
 
-    async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+    async fn copy(
+        &self,
+        source: &Path,
+        target: &Path,
+        options: CopyOptions,
+    ) -> Result<Vec<PathBuf>> {
         let source = normalize_path(source);
         let target = normalize_path(target);
 
@@ -557,7 +575,7 @@ impl Fs for FakeFs {
 
         if !options.overwrite && state.entries.contains_key(&target) {
             if options.ignore_if_exists {
-                return Ok(());
+                return Ok(Default::default());
             } else {
                 return Err(anyhow!("{target:?} already exists"));
             }
@@ -570,15 +588,15 @@ impl Fs for FakeFs {
             }
         }
 
-        let mut events = Vec::new();
+        let mut paths = Vec::new();
         for (relative_path, entry) in new_entries {
             let new_path = normalize_path(&target.join(relative_path));
-            events.push(new_path.clone());
+            paths.push(new_path.clone());
             state.entries.insert(new_path, entry);
         }
 
-        state.emit_event(&events).await;
-        Ok(())
+        state.emit_event(&paths).await;
+        Ok(paths)
     }
 
     async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {

crates/project/src/project.rs 🔗

@@ -784,7 +784,7 @@ impl Project {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
+    ) -> Option<Task<Result<(Entry, Vec<Entry>)>>> {
         let worktree = self.worktree_for_entry(entry_id, cx)?;
         let new_path = new_path.into();
         if self.is_local() {
@@ -809,15 +809,24 @@ impl Project {
                 let entry = response
                     .entry
                     .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote().unwrap().insert_entry(
+                let (entry, child_entries) = worktree.update(&mut cx, |worktree, cx| {
+                    let worktree = worktree.as_remote().unwrap();
+                    let root_entry =
+                        worktree.insert_entry(entry, response.worktree_scan_id as usize, cx);
+                    let mut child_entries = Vec::new();
+                    for entry in response.child_entries {
+                        child_entries.push(worktree.insert_entry(
                             entry,
                             response.worktree_scan_id as usize,
                             cx,
-                        )
-                    })
-                    .await
+                        ));
+                    }
+                    (root_entry, child_entries)
+                });
+                Ok((
+                    entry.await?,
+                    futures::future::try_join_all(child_entries).await?,
+                ))
             }))
         }
     }
@@ -4039,6 +4048,7 @@ impl Project {
             .await?;
         Ok(proto::ProjectEntryResponse {
             entry: Some((&entry).into()),
+            child_entries: Default::default(),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -4067,6 +4077,7 @@ impl Project {
             .await?;
         Ok(proto::ProjectEntryResponse {
             entry: Some((&entry).into()),
+            child_entries: Default::default(),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -4083,7 +4094,7 @@ impl Project {
                 .ok_or_else(|| anyhow!("worktree not found"))
         })?;
         let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
-        let entry = worktree
+        let (entry, child_entries) = worktree
             .update(&mut cx, |worktree, cx| {
                 let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
                 worktree
@@ -4095,6 +4106,7 @@ impl Project {
             .await?;
         Ok(proto::ProjectEntryResponse {
             entry: Some((&entry).into()),
+            child_entries: child_entries.iter().map(Into::into).collect(),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -4122,6 +4134,7 @@ impl Project {
             .await?;
         Ok(proto::ProjectEntryResponse {
             entry: None,
+            child_entries: Default::default(),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }

crates/project/src/worktree.rs 🔗

@@ -779,7 +779,7 @@ impl LocalWorktree {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Task<Result<Entry>>> {
+    ) -> Option<Task<Result<(Entry, Vec<Entry>)>>> {
         let old_path = self.entry_for_id(entry_id)?.path.clone();
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
@@ -794,23 +794,38 @@ impl LocalWorktree {
         });
 
         Some(cx.spawn(|this, mut cx| async move {
-            copy.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut().unwrap().refresh_entry(
-                        new_path.clone(),
-                        abs_new_path,
-                        None,
-                        cx,
-                    )
-                })
-                .await?;
+            let copied_paths = copy.await?;
+            let (entry, child_entries) = this.update(&mut cx, |this, cx| {
+                let this = this.as_local_mut().unwrap();
+                let root_entry =
+                    this.refresh_entry(new_path.clone(), abs_new_path.clone(), None, cx);
+
+                let mut child_entries = Vec::new();
+                for copied_path in copied_paths {
+                    if copied_path != abs_new_path {
+                        let relative_copied_path = copied_path.strip_prefix(this.abs_path())?;
+                        child_entries.push(this.refresh_entry(
+                            relative_copied_path.into(),
+                            copied_path,
+                            None,
+                            cx,
+                        ));
+                    }
+                }
+
+                anyhow::Ok((root_entry, child_entries))
+            })?;
+            let (entry, child_entries) = (
+                entry.await?,
+                futures::future::try_join_all(child_entries).await?,
+            );
+
             this.update(&mut cx, |this, cx| {
                 this.poll_snapshot(cx);
                 this.as_local().unwrap().broadcast_snapshot()
             })
             .await;
-            Ok(entry)
+            Ok((entry, child_entries))
         }))
     }
 
@@ -1202,8 +1217,23 @@ impl Snapshot {
     }
 
     fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
-        if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) {
-            self.entries_by_path.remove(&PathKey(entry.path), &());
+        if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) {
+            self.entries_by_path = {
+                let mut cursor = self.entries_by_path.cursor();
+                let mut new_entries_by_path =
+                    cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
+                while let Some(entry) = cursor.item() {
+                    if entry.path.starts_with(&removed_entry.path) {
+                        self.entries_by_id.remove(&entry.id, &());
+                        cursor.next(&());
+                    } else {
+                        break;
+                    }
+                }
+                new_entries_by_path.push_tree(cursor.suffix(&()), &());
+                new_entries_by_path
+            };
+
             true
         } else {
             false

crates/rpc/proto/zed.proto 🔗

@@ -224,7 +224,8 @@ message DeleteProjectEntry {
 
 message ProjectEntryResponse {
     Entry entry = 1;
-    uint64 worktree_scan_id = 2;
+    repeated Entry child_entries = 2;
+    uint64 worktree_scan_id = 3;
 }
 
 message AddProjectCollaborator {