Allow guests to rename stuff

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/collab/src/rpc.rs                  | 33 ++++++++++++++--
crates/project/src/project.rs             | 50 ++++++++++++++++++++++--
crates/project/src/worktree.rs            |  8 ++++
crates/project_panel/src/project_panel.rs |  5 --
crates/rpc/proto/zed.proto                | 20 ++++-----
crates/rpc/src/proto.rs                   |  9 ++--
crates/sum_tree/src/sum_tree.rs           | 17 ++++++++
7 files changed, 113 insertions(+), 29 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -127,6 +127,7 @@ impl Server {
             .add_request_handler(Server::forward_project_request::<proto::ReloadBuffers>)
             .add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
             .add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
+            .add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
             .add_request_handler(Server::update_buffer)
             .add_message_handler(Server::update_buffer_file)
             .add_message_handler(Server::buffer_reloaded)
@@ -1810,7 +1811,7 @@ mod tests {
     }
 
     #[gpui::test(iterations = 10)]
-    async fn test_worktree_manipulation(
+    async fn test_fs_operations(
         executor: Arc<Deterministic>,
         cx_a: &mut TestAppContext,
         cx_b: &mut TestAppContext,
@@ -1848,14 +1849,12 @@ mod tests {
         let worktree_b =
             project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
 
-        project_b
+        let entry = project_b
             .update(cx_b, |project, cx| {
                 project.create_file((worktree_id, "c.txt"), cx).unwrap()
             })
             .await
             .unwrap();
-
-        executor.run_until_parked();
         worktree_a.read_with(cx_a, |worktree, _| {
             assert_eq!(
                 worktree
@@ -1874,6 +1873,32 @@ mod tests {
                 [".zed.toml", "a.txt", "b.txt", "c.txt"]
             );
         });
+
+        project_b
+            .update(cx_b, |project, cx| {
+                project.rename_entry(entry.id, Path::new("d.txt"), cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+        worktree_a.read_with(cx_a, |worktree, _| {
+            assert_eq!(
+                worktree
+                    .paths()
+                    .map(|p| p.to_string_lossy())
+                    .collect::<Vec<_>>(),
+                [".zed.toml", "a.txt", "b.txt", "d.txt"]
+            );
+        });
+        worktree_b.read_with(cx_b, |worktree, _| {
+            assert_eq!(
+                worktree
+                    .paths()
+                    .map(|p| p.to_string_lossy())
+                    .collect::<Vec<_>>(),
+                [".zed.toml", "a.txt", "b.txt", "d.txt"]
+            );
+        });
     }
 
     #[gpui::test(iterations = 10)]

crates/project/src/project.rs 🔗

@@ -262,6 +262,7 @@ impl Project {
         client.add_model_message_handler(Self::handle_update_diagnostic_summary);
         client.add_model_message_handler(Self::handle_update_worktree);
         client.add_model_request_handler(Self::handle_create_project_entry);
+        client.add_model_request_handler(Self::handle_rename_project_entry);
         client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_reload_buffers);
@@ -736,9 +737,9 @@ impl Project {
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<Task<Result<Entry>>> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let new_path = new_path.into();
         if self.is_local() {
-            let worktree = self.worktree_for_entry(entry_id, cx)?;
-
             worktree.update(cx, |worktree, cx| {
                 worktree
                     .as_local_mut()
@@ -746,7 +747,27 @@ impl Project {
                     .rename_entry(entry_id, new_path, cx)
             })
         } else {
-            todo!()
+            let client = self.client.clone();
+            let project_id = self.remote_id().unwrap();
+
+            Some(cx.spawn_weak(|_, mut cx| async move {
+                let response = client
+                    .request(proto::RenameProjectEntry {
+                        project_id,
+                        entry_id: entry_id.to_proto(),
+                        new_path: new_path.as_os_str().as_bytes().to_vec(),
+                    })
+                    .await?;
+                worktree.update(&mut cx, |worktree, _| {
+                    let worktree = worktree.as_remote_mut().unwrap();
+                    worktree.snapshot.remove_entry(entry_id);
+                    worktree.snapshot.insert_entry(
+                        response
+                            .entry
+                            .ok_or_else(|| anyhow!("missing entry in response"))?,
+                    )
+                })
+            }))
         }
     }
 
@@ -3802,7 +3823,7 @@ impl Project {
         envelope: TypedEnvelope<proto::CreateProjectEntry>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
-    ) -> Result<proto::CreateProjectEntryResponse> {
+    ) -> Result<proto::ProjectEntryResponse> {
         let entry = this
             .update(&mut cx, |this, cx| {
                 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
@@ -3820,7 +3841,26 @@ impl Project {
                 })
             })?
             .await?;
-        Ok(proto::CreateProjectEntryResponse {
+        Ok(proto::ProjectEntryResponse {
+            entry: Some((&entry).into()),
+        })
+    }
+
+    async fn handle_rename_project_entry(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::RenameProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let entry = this
+            .update(&mut cx, |this, cx| {
+                let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+                let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
+                this.rename_entry(entry_id, new_path, cx)
+                    .ok_or_else(|| anyhow!("invalid entry"))
+            })?
+            .await?;
+        Ok(proto::ProjectEntryResponse {
             entry: Some((&entry).into()),
         })
     }

crates/project/src/worktree.rs 🔗

@@ -956,6 +956,14 @@ impl Snapshot {
         self.entries_by_id.get(&entry_id, &()).is_some()
     }
 
+    pub(crate) fn remove_entry(&mut self, entry_id: ProjectEntryId) -> Option<Entry> {
+        if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) {
+            self.entries_by_path.remove(&PathKey(entry.path), &())
+        } else {
+            None
+        }
+    }
+
     pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
         let entry = Entry::try_from((&self.root_char_bag, entry))?;
         self.entries_by_id.insert_or_replace(

crates/project_panel/src/project_panel.rs 🔗

@@ -295,11 +295,6 @@ impl ProjectPanel {
                 Ok(())
             }))
         } else {
-            // TODO - implement this for remote projects
-            if !worktree.read(cx).is_local() {
-                return None;
-            }
-
             let old_path = entry.path.clone();
             let new_path = if let Some(parent) = old_path.parent() {
                 parent.join(filename)

crates/rpc/proto/zed.proto 🔗

@@ -38,9 +38,9 @@ message Envelope {
         UpdateWorktree update_worktree = 31;
 
         CreateProjectEntry create_project_entry = 32;
-        CreateProjectEntryResponse create_project_entry_response = 33;
-        RenameProjectEntry rename_project_entry = 34;
-        DeleteProjectEntry delete_project_entry = 35;
+        RenameProjectEntry rename_project_entry = 33;
+        DeleteProjectEntry delete_project_entry = 34;
+        ProjectEntryResponse project_entry_response = 35;
 
         UpdateDiagnosticSummary update_diagnostic_summary = 36;
         StartLanguageServer start_language_server = 37;
@@ -171,16 +171,10 @@ message CreateProjectEntry {
     bool is_directory = 4;
 }
 
-message CreateProjectEntryResponse {
-    Entry entry = 1;
-}
-
 message RenameProjectEntry {
     uint64 project_id = 1;
-    uint64 old_worktree_id = 2;
-    string old_path = 3;
-    uint64 new_worktree_id = 4;
-    string new_path = 5;
+    uint64 entry_id = 2;
+    bytes new_path = 3;
 }
 
 message DeleteProjectEntry {
@@ -189,6 +183,10 @@ message DeleteProjectEntry {
     string path = 3;
 }
 
+message ProjectEntryResponse {
+    Entry entry = 1;
+}
+
 message AddProjectCollaborator {
     uint64 project_id = 1;
     Collaborator collaborator = 2;

crates/rpc/src/proto.rs 🔗

@@ -148,7 +148,6 @@ messages!(
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
     (CreateProjectEntry, Foreground),
-    (CreateProjectEntryResponse, Foreground),
     (DeleteProjectEntry, Foreground),
     (Error, Foreground),
     (Follow, Foreground),
@@ -177,8 +176,6 @@ messages!(
     (JoinChannelResponse, Foreground),
     (JoinProject, Foreground),
     (JoinProjectResponse, Foreground),
-    (StartLanguageServer, Foreground),
-    (UpdateLanguageServer, Foreground),
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
     (OpenBufferById, Background),
@@ -190,6 +187,7 @@ messages!(
     (PerformRenameResponse, Background),
     (PrepareRename, Background),
     (PrepareRenameResponse, Background),
+    (ProjectEntryResponse, Foreground),
     (RegisterProjectResponse, Foreground),
     (Ping, Foreground),
     (RegisterProject, Foreground),
@@ -204,6 +202,7 @@ messages!(
     (SendChannelMessage, Foreground),
     (SendChannelMessageResponse, Foreground),
     (ShareProject, Foreground),
+    (StartLanguageServer, Foreground),
     (Test, Foreground),
     (Unfollow, Foreground),
     (UnregisterProject, Foreground),
@@ -214,6 +213,7 @@ messages!(
     (UpdateContacts, Foreground),
     (UpdateDiagnosticSummary, Foreground),
     (UpdateFollowers, Foreground),
+    (UpdateLanguageServer, Foreground),
     (UpdateWorktree, Foreground),
 );
 
@@ -223,7 +223,7 @@ request_messages!(
         ApplyCompletionAdditionalEdits,
         ApplyCompletionAdditionalEditsResponse
     ),
-    (CreateProjectEntry, CreateProjectEntryResponse),
+    (CreateProjectEntry, ProjectEntryResponse),
     (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
@@ -246,6 +246,7 @@ request_messages!(
     (RegisterProject, RegisterProjectResponse),
     (RegisterWorktree, Ack),
     (ReloadBuffers, ReloadBuffersResponse),
+    (RenameProjectEntry, ProjectEntryResponse),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
     (SendChannelMessage, SendChannelMessageResponse),

crates/sum_tree/src/sum_tree.rs 🔗

@@ -502,6 +502,23 @@ impl<T: KeyedItem> SumTree<T> {
         replaced
     }
 
+    pub fn remove(&mut self, key: &T::Key, cx: &<T::Summary as Summary>::Context) -> Option<T> {
+        let mut removed = None;
+        *self = {
+            let mut cursor = self.cursor::<T::Key>();
+            let mut new_tree = cursor.slice(key, Bias::Left, cx);
+            if let Some(item) = cursor.item() {
+                if item.key() == *key {
+                    removed = Some(item.clone());
+                    cursor.next(cx);
+                }
+            }
+            new_tree.push_tree(cursor.suffix(cx), cx);
+            new_tree
+        };
+        removed
+    }
+
     pub fn edit(
         &mut self,
         mut edits: Vec<Edit<T>>,