Allow guests to create files from the project panel

Max Brunsfeld and Nathan Sobo created

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

Change summary

Cargo.lock                                |   4 
crates/collab/src/rpc.rs                  |  68 ++++++++++
crates/gpui/src/executor.rs               |  16 +-
crates/project/Cargo.toml                 |   2 
crates/project/src/project.rs             |  72 +++++++++++
crates/project/src/worktree.rs            | 162 ++++++++++++++----------
crates/project_panel/src/project_panel.rs |  21 +-
crates/rpc/proto/zed.proto                | 135 ++++++++++++--------
crates/rpc/src/proto.rs                   |   8 +
9 files changed, 349 insertions(+), 139 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -63,9 +63,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.42"
+version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
 
 [[package]]
 name = "arrayref"

crates/collab/src/rpc.rs 🔗

@@ -126,6 +126,7 @@ impl Server {
             .add_request_handler(Server::forward_project_request::<proto::PerformRename>)
             .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::update_buffer)
             .add_message_handler(Server::update_buffer_file)
             .add_message_handler(Server::buffer_reloaded)
@@ -1808,6 +1809,73 @@ mod tests {
             .await;
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_worktree_manipulation(
+        executor: Arc<Deterministic>,
+        cx_a: &mut TestAppContext,
+        cx_b: &mut TestAppContext,
+    ) {
+        executor.forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+
+        // Share a project as client A
+        fs.insert_tree(
+            "/dir",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "a.txt": "a-contents",
+                "b.txt": "b-contents",
+            }),
+        )
+        .await;
+
+        let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+        let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+        let worktree_a =
+            project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+        let worktree_b =
+            project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
+
+        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
+                    .paths()
+                    .map(|p| p.to_string_lossy())
+                    .collect::<Vec<_>>(),
+                [".zed.toml", "a.txt", "b.txt", "c.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", "c.txt"]
+            );
+        });
+    }
+
     #[gpui::test(iterations = 10)]
     async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();

crates/gpui/src/executor.rs 🔗

@@ -360,6 +360,14 @@ impl Deterministic {
 
         self.state.lock().now = new_now;
     }
+
+    pub fn forbid_parking(&self) {
+        use rand::prelude::*;
+
+        let mut state = self.state.lock();
+        state.forbid_parking = true;
+        state.rng = StdRng::seed_from_u64(state.seed);
+    }
 }
 
 impl Drop for Timer {
@@ -507,14 +515,8 @@ impl Foreground {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn forbid_parking(&self) {
-        use rand::prelude::*;
-
         match self {
-            Self::Deterministic { executor, .. } => {
-                let mut state = executor.state.lock();
-                state.forbid_parking = true;
-                state.rng = StdRng::seed_from_u64(state.seed);
-            }
+            Self::Deterministic { executor, .. } => executor.forbid_parking(),
             _ => panic!("this method can only be called on a deterministic executor"),
         }
     }

crates/project/Cargo.toml 🔗

@@ -29,7 +29,7 @@ settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 util = { path = "../util" }
 aho-corasick = "0.7"
-anyhow = "1.0.38"
+anyhow = "1.0.57"
 async-trait = "0.1"
 futures = "0.3"
 ignore = "0.4"

crates/project/src/project.rs 🔗

@@ -36,9 +36,11 @@ use std::{
     cell::RefCell,
     cmp::{self, Ordering},
     convert::TryInto,
+    ffi::OsString,
     hash::Hash,
     mem,
     ops::Range,
+    os::unix::{ffi::OsStrExt, prelude::OsStringExt},
     path::{Component, Path, PathBuf},
     rc::Rc,
     sync::{
@@ -259,6 +261,7 @@ impl Project {
         client.add_model_message_handler(Self::handle_update_buffer);
         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_apply_additional_edits_for_completion);
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_reload_buffers);
@@ -686,6 +689,47 @@ impl Project {
             .map(|worktree| worktree.read(cx).id())
     }
 
+    pub fn create_file(
+        &mut self,
+        project_path: impl Into<ProjectPath>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<Entry>>> {
+        let project_path = project_path.into();
+        let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+
+        if self.is_local() {
+            Some(worktree.update(cx, |worktree, cx| {
+                worktree.as_local_mut().unwrap().write_file(
+                    project_path.path,
+                    Default::default(),
+                    cx,
+                )
+            }))
+        } else {
+            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::CreateProjectEntry {
+                        worktree_id: project_path.worktree_id.to_proto(),
+                        project_id,
+                        path: project_path.path.as_os_str().as_bytes().to_vec(),
+                        is_directory: false,
+                    })
+                    .await?;
+                worktree.update(&mut cx, |worktree, _| {
+                    let worktree = worktree.as_remote_mut().unwrap();
+                    worktree.snapshot.insert_entry(
+                        response
+                            .entry
+                            .ok_or_else(|| anyhow!("missing entry in response"))?,
+                    )
+                })
+            }))
+        }
+    }
+
     pub fn can_share(&self, cx: &AppContext) -> bool {
         self.is_local() && self.visible_worktrees(cx).next().is_some()
     }
@@ -3733,6 +3777,34 @@ impl Project {
         })
     }
 
+    async fn handle_create_project_entry(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::CreateProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::CreateProjectEntryResponse> {
+        let entry = this
+            .update(&mut cx, |this, cx| {
+                let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+                let worktree = this
+                    .worktree_for_id(worktree_id, cx)
+                    .ok_or_else(|| anyhow!("worktree not found"))?;
+                worktree.update(cx, |worktree, cx| {
+                    let worktree = worktree.as_local_mut().unwrap();
+                    if envelope.payload.is_directory {
+                        unimplemented!("can't yet create directories");
+                    } else {
+                        let path = PathBuf::from(OsString::from_vec(envelope.payload.path));
+                        anyhow::Ok(worktree.write_file(path, Default::default(), cx))
+                    }
+                })
+            })?
+            .await?;
+        Ok(proto::CreateProjectEntryResponse {
+            entry: Some((&entry).into()),
+        })
+    }
+
     async fn handle_update_diagnostic_summary(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,

crates/project/src/worktree.rs 🔗

@@ -42,6 +42,7 @@ use std::{
     fmt,
     future::Future,
     ops::{Deref, DerefMut},
+    os::unix::prelude::{OsStrExt, OsStringExt},
     path::{Path, PathBuf},
     sync::{atomic::AtomicUsize, Arc},
     time::{Duration, SystemTime},
@@ -623,13 +624,15 @@ impl LocalWorktree {
         let handle = cx.handle();
         let path = Arc::from(path);
         let abs_path = self.absolutize(&path);
-        let background_snapshot = self.background_snapshot.clone();
         let fs = self.fs.clone();
         cx.spawn(|this, mut cx| async move {
             let text = fs.load(&abs_path).await?;
             // Eagerly populate the snapshot with an updated entry for the loaded file
-            let entry =
-                refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path, None).await?;
+            let entry = this
+                .update(&mut cx, |this, _| {
+                    this.as_local().unwrap().refresh_entry(path, abs_path, None)
+                })
+                .await?;
             this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
             Ok((
                 File {
@@ -653,7 +656,7 @@ impl LocalWorktree {
         let buffer = buffer_handle.read(cx);
         let text = buffer.as_rope().clone();
         let version = buffer.version();
-        let save = self.save(path, text, cx);
+        let save = self.write_file(path, text, cx);
         let handle = cx.handle();
         cx.as_mut().spawn(|mut cx| async move {
             let entry = save.await?;
@@ -673,7 +676,7 @@ impl LocalWorktree {
         })
     }
 
-    pub fn save(
+    pub fn write_file(
         &self,
         path: impl Into<Arc<Path>>,
         text: Rope,
@@ -681,22 +684,21 @@ impl LocalWorktree {
     ) -> Task<Result<Entry>> {
         let path = path.into();
         let abs_path = self.absolutize(&path);
-        let background_snapshot = self.background_snapshot.clone();
-        let fs = self.fs.clone();
-        let save = cx.background().spawn(async move {
-            fs.save(&abs_path, &text).await?;
-            refresh_entry(
-                fs.as_ref(),
-                &background_snapshot,
-                path.clone(),
-                &abs_path,
-                None,
-            )
-            .await
+        let save = cx.background().spawn({
+            let fs = self.fs.clone();
+            let abs_path = abs_path.clone();
+            async move { fs.save(&abs_path, &text).await }
         });
 
         cx.spawn(|this, mut cx| async move {
-            let entry = save.await?;
+            save.await?;
+            let entry = this
+                .update(&mut cx, |this, _| {
+                    this.as_local_mut()
+                        .unwrap()
+                        .refresh_entry(path, abs_path, None)
+                })
+                .await?;
             this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
             Ok(entry)
         })
@@ -712,28 +714,68 @@ impl LocalWorktree {
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
-        let background_snapshot = self.background_snapshot.clone();
-        let fs = self.fs.clone();
-        let rename = cx.background().spawn(async move {
-            fs.rename(&abs_old_path, &abs_new_path, Default::default())
-                .await?;
-            refresh_entry(
-                fs.as_ref(),
-                &background_snapshot,
-                new_path.clone(),
-                &abs_new_path,
-                Some(old_path),
-            )
-            .await
+        let rename = cx.background().spawn({
+            let fs = self.fs.clone();
+            let abs_new_path = abs_new_path.clone();
+            async move {
+                fs.rename(&abs_old_path, &abs_new_path, Default::default())
+                    .await
+            }
         });
 
         cx.spawn(|this, mut cx| async move {
-            let entry = rename.await?;
+            rename.await?;
+            let entry = this
+                .update(&mut cx, |this, _| {
+                    this.as_local_mut().unwrap().refresh_entry(
+                        new_path.clone(),
+                        abs_new_path,
+                        Some(old_path),
+                    )
+                })
+                .await?;
             this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
             Ok(entry)
         })
     }
 
+    fn refresh_entry(
+        &self,
+        path: Arc<Path>,
+        abs_path: PathBuf,
+        old_path: Option<Arc<Path>>,
+    ) -> impl Future<Output = Result<Entry>> {
+        let root_char_bag;
+        let next_entry_id;
+        let fs = self.fs.clone();
+        let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone());
+        let snapshot = self.background_snapshot.clone();
+        {
+            let snapshot = snapshot.lock();
+            root_char_bag = snapshot.root_char_bag;
+            next_entry_id = snapshot.next_entry_id.clone();
+        }
+        async move {
+            let entry = Entry::new(
+                path,
+                &fs.metadata(&abs_path)
+                    .await?
+                    .ok_or_else(|| anyhow!("could not read saved file metadata"))?,
+                &next_entry_id,
+                root_char_bag,
+            );
+            let mut snapshot = snapshot.lock();
+            if let Some(old_path) = old_path {
+                snapshot.remove_path(&old_path);
+            }
+            let entry = snapshot.insert_entry(entry, fs.as_ref());
+            if let Some(tx) = shared_snapshots_tx {
+                tx.send(snapshot.clone()).await.ok();
+            }
+            Ok(entry)
+        }
+    }
+
     pub fn register(
         &mut self,
         project_id: u64,
@@ -914,6 +956,21 @@ impl Snapshot {
         self.entries_by_id.get(&entry_id, &()).is_some()
     }
 
+    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(
+            PathEntry {
+                id: entry.id,
+                path: entry.path.clone(),
+                is_ignored: entry.is_ignored,
+                scan_id: 0,
+            },
+            &(),
+        );
+        self.entries_by_path.insert_or_replace(entry.clone(), &());
+        Ok(entry)
+    }
+
     pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
         let mut entries_by_path_edits = Vec::new();
         let mut entries_by_id_edits = Vec::new();
@@ -1437,7 +1494,7 @@ impl language::File for File {
             Worktree::Local(worktree) => {
                 let rpc = worktree.client.clone();
                 let project_id = worktree.share.as_ref().map(|share| share.project_id);
-                let save = worktree.save(self.path.clone(), text, cx);
+                let save = worktree.write_file(self.path.clone(), text, cx);
                 cx.background().spawn(async move {
                     let entry = save.await?;
                     if let Some(project_id) = project_id {
@@ -2155,35 +2212,6 @@ impl BackgroundScanner {
     }
 }
 
-async fn refresh_entry(
-    fs: &dyn Fs,
-    snapshot: &Mutex<LocalSnapshot>,
-    path: Arc<Path>,
-    abs_path: &Path,
-    old_path: Option<Arc<Path>>,
-) -> Result<Entry> {
-    let root_char_bag;
-    let next_entry_id;
-    {
-        let snapshot = snapshot.lock();
-        root_char_bag = snapshot.root_char_bag;
-        next_entry_id = snapshot.next_entry_id.clone();
-    }
-    let entry = Entry::new(
-        path,
-        &fs.metadata(abs_path)
-            .await?
-            .ok_or_else(|| anyhow!("could not read saved file metadata"))?,
-        &next_entry_id,
-        root_char_bag,
-    );
-    let mut snapshot = snapshot.lock();
-    if let Some(old_path) = old_path {
-        snapshot.remove_path(&old_path);
-    }
-    Ok(snapshot.insert_entry(entry, fs))
-}
-
 fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
     let mut result = root_char_bag;
     result.extend(
@@ -2421,7 +2449,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
         Self {
             id: entry.id.to_proto(),
             is_dir: entry.is_dir(),
-            path: entry.path.to_string_lossy().to_string(),
+            path: entry.path.as_os_str().as_bytes().to_vec(),
             inode: entry.inode,
             mtime: Some(entry.mtime.into()),
             is_symlink: entry.is_symlink,
@@ -2439,10 +2467,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
                 EntryKind::Dir
             } else {
                 let mut char_bag = root_char_bag.clone();
-                char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
+                char_bag.extend(
+                    String::from_utf8_lossy(&entry.path)
+                        .chars()
+                        .map(|c| c.to_ascii_lowercase()),
+                );
                 EntryKind::File(char_bag)
             };
-            let path: Arc<Path> = Arc::from(Path::new(&entry.path));
+            let path: Arc<Path> = PathBuf::from(OsString::from_vec(entry.path)).into();
             Ok(Entry {
                 id: ProjectEntryId::from_proto(entry.id),
                 kind,

crates/project_panel/src/project_panel.rs 🔗

@@ -273,27 +273,19 @@ impl ProjectPanel {
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
         let edit_state = self.edit_state.take()?;
         cx.focus_self();
+
         let worktree = self
             .project
             .read(cx)
             .worktree_for_id(edit_state.worktree_id, cx)?;
-
-        // TODO - implement this for remote projects
-        if !worktree.read(cx).is_local() {
-            return None;
-        }
-
         let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?;
         let filename = self.filename_editor.read(cx).text(cx);
 
         if edit_state.new_file {
             let new_path = entry.path.join(filename);
-            let save = worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_local()
-                    .unwrap()
-                    .save(new_path, Default::default(), cx)
-            });
+            let save = self.project.update(cx, |project, cx| {
+                project.create_file((edit_state.worktree_id, new_path), cx)
+            })?;
             Some(cx.spawn(|this, mut cx| async move {
                 let new_entry = save.await?;
                 this.update(&mut cx, |this, cx| {
@@ -303,6 +295,11 @@ 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 🔗

@@ -36,57 +36,63 @@ message Envelope {
         RegisterWorktree register_worktree = 28;
         UnregisterWorktree unregister_worktree = 29;
         UpdateWorktree update_worktree = 31;
-        UpdateDiagnosticSummary update_diagnostic_summary = 32;
-        StartLanguageServer start_language_server = 33;
-        UpdateLanguageServer update_language_server = 34;
-
-        OpenBufferById open_buffer_by_id = 35;
-        OpenBufferByPath open_buffer_by_path = 36;
-        OpenBufferResponse open_buffer_response = 37;
-        UpdateBuffer update_buffer = 38;
-        UpdateBufferFile update_buffer_file = 39;
-        SaveBuffer save_buffer = 40;
-        BufferSaved buffer_saved = 41;
-        BufferReloaded buffer_reloaded = 42;
-        ReloadBuffers reload_buffers = 43;
-        ReloadBuffersResponse reload_buffers_response = 44;
-        FormatBuffers format_buffers = 45;
-        FormatBuffersResponse format_buffers_response = 46;
-        GetCompletions get_completions = 47;
-        GetCompletionsResponse get_completions_response = 48;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50;
-        GetCodeActions get_code_actions = 51;
-        GetCodeActionsResponse get_code_actions_response = 52;
-        ApplyCodeAction apply_code_action = 53;
-        ApplyCodeActionResponse apply_code_action_response = 54;
-        PrepareRename prepare_rename = 55;
-        PrepareRenameResponse prepare_rename_response = 56;
-        PerformRename perform_rename = 57;
-        PerformRenameResponse perform_rename_response = 58;
-        SearchProject search_project = 59;
-        SearchProjectResponse search_project_response = 60;
-
-        GetChannels get_channels = 61;
-        GetChannelsResponse get_channels_response = 62;
-        JoinChannel join_channel = 63;
-        JoinChannelResponse join_channel_response = 64;
-        LeaveChannel leave_channel = 65;
-        SendChannelMessage send_channel_message = 66;
-        SendChannelMessageResponse send_channel_message_response = 67;
-        ChannelMessageSent channel_message_sent = 68;
-        GetChannelMessages get_channel_messages = 69;
-        GetChannelMessagesResponse get_channel_messages_response = 70;
-
-        UpdateContacts update_contacts = 71;
-
-        GetUsers get_users = 72;
-        GetUsersResponse get_users_response = 73;
-
-        Follow follow = 74;
-        FollowResponse follow_response = 75;
-        UpdateFollowers update_followers = 76;
-        Unfollow unfollow = 77;
+
+        CreateProjectEntry create_project_entry = 32;
+        CreateProjectEntryResponse create_project_entry_response = 33;
+        RenameProjectEntry rename_project_entry = 34;
+        DeleteProjectEntry delete_project_entry = 35;
+
+        UpdateDiagnosticSummary update_diagnostic_summary = 36;
+        StartLanguageServer start_language_server = 37;
+        UpdateLanguageServer update_language_server = 38;
+
+        OpenBufferById open_buffer_by_id = 39;
+        OpenBufferByPath open_buffer_by_path = 40;
+        OpenBufferResponse open_buffer_response = 41;
+        UpdateBuffer update_buffer = 42;
+        UpdateBufferFile update_buffer_file = 43;
+        SaveBuffer save_buffer = 44;
+        BufferSaved buffer_saved = 45;
+        BufferReloaded buffer_reloaded = 46;
+        ReloadBuffers reload_buffers = 47;
+        ReloadBuffersResponse reload_buffers_response = 48;
+        FormatBuffers format_buffers = 49;
+        FormatBuffersResponse format_buffers_response = 50;
+        GetCompletions get_completions = 51;
+        GetCompletionsResponse get_completions_response = 52;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54;
+        GetCodeActions get_code_actions = 55;
+        GetCodeActionsResponse get_code_actions_response = 56;
+        ApplyCodeAction apply_code_action = 57;
+        ApplyCodeActionResponse apply_code_action_response = 58;
+        PrepareRename prepare_rename = 59;
+        PrepareRenameResponse prepare_rename_response = 60;
+        PerformRename perform_rename = 61;
+        PerformRenameResponse perform_rename_response = 62;
+        SearchProject search_project = 63;
+        SearchProjectResponse search_project_response = 64;
+
+        GetChannels get_channels = 65;
+        GetChannelsResponse get_channels_response = 66;
+        JoinChannel join_channel = 67;
+        JoinChannelResponse join_channel_response = 68;
+        LeaveChannel leave_channel = 69;
+        SendChannelMessage send_channel_message = 70;
+        SendChannelMessageResponse send_channel_message_response = 71;
+        ChannelMessageSent channel_message_sent = 72;
+        GetChannelMessages get_channel_messages = 73;
+        GetChannelMessagesResponse get_channel_messages_response = 74;
+
+        UpdateContacts update_contacts = 75;
+
+        GetUsers get_users = 76;
+        GetUsersResponse get_users_response = 77;
+
+        Follow follow = 78;
+        FollowResponse follow_response = 79;
+        UpdateFollowers update_followers = 80;
+        Unfollow unfollow = 81;
     }
 }
 
@@ -158,6 +164,31 @@ message UpdateWorktree {
     repeated uint64 removed_entries = 5;
 }
 
+message CreateProjectEntry {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    bytes path = 3;
+    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;
+}
+
+message DeleteProjectEntry {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string path = 3;
+}
+
 message AddProjectCollaborator {
     uint64 project_id = 1;
     Collaborator collaborator = 2;
@@ -642,7 +673,7 @@ message File {
 message Entry {
     uint64 id = 1;
     bool is_dir = 2;
-    string path = 3;
+    bytes path = 3;
     uint64 inode = 4;
     Timestamp mtime = 5;
     bool is_symlink = 6;

crates/rpc/src/proto.rs 🔗

@@ -147,6 +147,9 @@ messages!(
     (BufferReloaded, Foreground),
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
+    (CreateProjectEntry, Foreground),
+    (CreateProjectEntryResponse, Foreground),
+    (DeleteProjectEntry, Foreground),
     (Error, Foreground),
     (Follow, Foreground),
     (FollowResponse, Foreground),
@@ -194,6 +197,7 @@ messages!(
     (ReloadBuffers, Foreground),
     (ReloadBuffersResponse, Foreground),
     (RemoveProjectCollaborator, Foreground),
+    (RenameProjectEntry, Foreground),
     (SaveBuffer, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
@@ -219,6 +223,7 @@ request_messages!(
         ApplyCompletionAdditionalEdits,
         ApplyCompletionAdditionalEditsResponse
     ),
+    (CreateProjectEntry, CreateProjectEntryResponse),
     (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
@@ -257,6 +262,9 @@ entity_messages!(
     ApplyCompletionAdditionalEdits,
     BufferReloaded,
     BufferSaved,
+    CreateProjectEntry,
+    RenameProjectEntry,
+    DeleteProjectEntry,
     Follow,
     FormatBuffers,
     GetCodeActions,