Add worktree to workspace when joining a remote worktree

Antonio Scandurra created

Change summary

zed-rpc/proto/zed.proto | 11 +++++
zed-rpc/src/peer.rs     | 30 +++++++----------
zed-rpc/src/proto.rs    |  7 +--
zed/src/workspace.rs    | 12 +++++-
zed/src/worktree.rs     | 72 ++++++++++++++++++++++++++++++++++++++++--
5 files changed, 103 insertions(+), 29 deletions(-)

Detailed changes

zed-rpc/proto/zed.proto 🔗

@@ -53,7 +53,16 @@ message OpenBufferResponse {
 }
 
 message Worktree {
-    repeated string paths = 1;
+    string root_name = 1;
+    repeated Entry entries = 2;
+}
+
+message Entry {
+    bool is_dir = 1;
+    string path = 2;
+    uint64 inode = 3;
+    bool is_symlink = 4;
+    bool is_ignored = 5;
 }
 
 message Buffer {

zed-rpc/src/peer.rs 🔗

@@ -341,23 +341,19 @@ mod tests {
             smol::spawn(server.handle_messages(server_conn_id2)).detach();
 
             // define the expected requests and responses
-            let request1 = proto::OpenWorktree {
-                worktree_id: 101,
-                access_token: "first-worktree-access-token".to_string(),
+            let request1 = proto::Auth {
+                user_id: 1,
+                access_token: "token-1".to_string(),
             };
-            let response1 = proto::OpenWorktreeResponse {
-                worktree: Some(proto::Worktree {
-                    paths: vec!["path/one".to_string()],
-                }),
+            let response1 = proto::AuthResponse {
+                credentials_valid: true,
             };
-            let request2 = proto::OpenWorktree {
-                worktree_id: 102,
-                access_token: "second-worktree-access-token".to_string(),
+            let request2 = proto::Auth {
+                user_id: 2,
+                access_token: "token-2".to_string(),
             };
-            let response2 = proto::OpenWorktreeResponse {
-                worktree: Some(proto::Worktree {
-                    paths: vec!["path/two".to_string(), "path/three".to_string()],
-                }),
+            let response2 = proto::AuthResponse {
+                credentials_valid: false,
             };
             let request3 = proto::OpenBuffer {
                 worktree_id: 102,
@@ -386,7 +382,7 @@ mod tests {
 
             // on the server, respond to two requests for each client
             let mut open_buffer_rx = server.add_message_handler::<proto::OpenBuffer>().await;
-            let mut open_worktree_rx = server.add_message_handler::<proto::OpenWorktree>().await;
+            let mut auth_rx = server.add_message_handler::<proto::Auth>().await;
             let (mut server_done_tx, mut server_done_rx) = oneshot::channel::<()>();
             smol::spawn({
                 let request1 = request1.clone();
@@ -398,11 +394,11 @@ mod tests {
                 let response3 = response3.clone();
                 let response4 = response4.clone();
                 async move {
-                    let msg = open_worktree_rx.recv().await.unwrap();
+                    let msg = auth_rx.recv().await.unwrap();
                     assert_eq!(msg.payload, request1);
                     server.respond(msg, response1.clone()).await.unwrap();
 
-                    let msg = open_worktree_rx.recv().await.unwrap();
+                    let msg = auth_rx.recv().await.unwrap();
                     assert_eq!(msg.payload, request2.clone());
                     server.respond(msg, response2.clone()).await.unwrap();
 

zed-rpc/src/proto.rs 🔗

@@ -132,10 +132,9 @@ mod tests {
             }
             .into_envelope(3, None);
 
-            let message2 = ShareWorktree {
-                worktree: Some(Worktree {
-                    paths: vec!["ok".to_string()],
-                }),
+            let message2 = OpenBuffer {
+                worktree_id: 1,
+                path: "path".to_string(),
             }
             .into_envelope(5, None);
 

zed/src/workspace.rs 🔗

@@ -705,7 +705,7 @@ impl Workspace {
         let rpc = self.rpc.clone();
         let executor = cx.background_executor().clone();
 
-        let task = cx.spawn(|_, cx| async move {
+        let task = cx.spawn(|this, mut cx| async move {
             let connection_id = rpc.connect_to_server(&cx, &executor).await?;
 
             let worktree_url = cx
@@ -725,7 +725,15 @@ impl Workspace {
                     },
                 )
                 .await?;
-            log::info!("joined worktree: {:?}", open_worktree_response);
+            let worktree = open_worktree_response
+                .worktree
+                .ok_or_else(|| anyhow!("empty worktree"))?;
+            this.update(&mut cx, |workspace, cx| {
+                let worktree = cx.add_model(|cx| Worktree::remote(worktree, cx));
+                cx.observe_model(&worktree, |_, _, cx| cx.notify());
+                workspace.worktrees.insert(worktree);
+                cx.notify();
+            });
 
             surf::Result::Ok(())
         });

zed/src/worktree.rs 🔗

@@ -48,6 +48,7 @@ enum ScanState {
 
 pub enum Worktree {
     Local(LocalWorktree),
+    Remote(RemoteWorktree),
 }
 
 impl Entity for Worktree {
@@ -59,6 +60,10 @@ impl Worktree {
         Worktree::Local(LocalWorktree::new(path, cx))
     }
 
+    pub fn remote(worktree: proto::Worktree, cx: &mut ModelContext<Worktree>) -> Self {
+        Worktree::Remote(RemoteWorktree::new(worktree, cx))
+    }
+
     pub fn as_local(&self) -> Option<&LocalWorktree> {
         if let Worktree::Local(worktree) = self {
             Some(worktree)
@@ -78,6 +83,7 @@ impl Worktree {
     pub fn snapshot(&self) -> Snapshot {
         match self {
             Worktree::Local(worktree) => worktree.snapshot(),
+            Worktree::Remote(worktree) => worktree.snapshot.clone(),
         }
     }
 
@@ -88,6 +94,7 @@ impl Worktree {
     ) -> impl Future<Output = Result<History>> {
         match self {
             Worktree::Local(worktree) => worktree.load_history(path, cx),
+            Worktree::Remote(worktree) => todo!(),
         }
     }
 
@@ -99,6 +106,7 @@ impl Worktree {
     ) -> impl Future<Output = Result<()>> {
         match self {
             Worktree::Local(worktree) => worktree.save(path, content, cx),
+            Worktree::Remote(worktree) => todo!(),
         }
     }
 }
@@ -109,6 +117,7 @@ impl Deref for Worktree {
     fn deref(&self) -> &Self::Target {
         match self {
             Worktree::Local(worktree) => &worktree.snapshot,
+            Worktree::Remote(worktree) => &worktree.snapshot,
         }
     }
 }
@@ -137,7 +146,7 @@ struct FileHandleState {
 }
 
 impl LocalWorktree {
-    pub fn new(path: impl Into<Arc<Path>>, cx: &mut ModelContext<Worktree>) -> Self {
+    fn new(path: impl Into<Arc<Path>>, cx: &mut ModelContext<Worktree>) -> Self {
         let abs_path = path.into();
         let (scan_state_tx, scan_state_rx) = smol::channel::unbounded();
         let id = cx.model_id();
@@ -309,14 +318,22 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Task<anyhow::Result<(u64, String)>> {
         self.rpc = Some(client.clone());
+        let root_name = self.root_name.clone();
         let snapshot = self.snapshot();
         cx.spawn(|_this, cx| async move {
-            let paths = cx
+            let entries = cx
                 .background_executor()
                 .spawn(async move {
                     snapshot
-                        .paths()
-                        .map(|path| path.to_string_lossy().to_string())
+                        .entries
+                        .cursor::<(), ()>()
+                        .map(|entry| proto::Entry {
+                            is_dir: entry.is_dir(),
+                            path: entry.path.to_string_lossy().to_string(),
+                            inode: entry.inode,
+                            is_symlink: entry.is_symlink,
+                            is_ignored: entry.is_ignored,
+                        })
                         .collect()
                 })
                 .await;
@@ -325,7 +342,7 @@ impl LocalWorktree {
                 .request(
                     connection_id,
                     proto::ShareWorktree {
-                        worktree: Some(proto::Worktree { paths }),
+                        worktree: Some(proto::Worktree { root_name, entries }),
                     },
                 )
                 .await?;
@@ -350,6 +367,50 @@ impl fmt::Debug for LocalWorktree {
     }
 }
 
+pub struct RemoteWorktree {
+    snapshot: Snapshot,
+}
+
+impl RemoteWorktree {
+    fn new(worktree: proto::Worktree, cx: &mut ModelContext<Worktree>) -> Self {
+        let id = cx.model_id();
+        let root_char_bag: CharBag = worktree
+            .root_name
+            .chars()
+            .map(|c| c.to_ascii_lowercase())
+            .collect();
+        let mut entries = SumTree::new();
+        entries.extend(
+            worktree.entries.into_iter().map(|entry| {
+                let kind = if entry.is_dir {
+                    EntryKind::Dir
+                } else {
+                    let mut char_bag = root_char_bag.clone();
+                    char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
+                    EntryKind::File(char_bag)
+                };
+                Entry {
+                    kind,
+                    path: Path::new(&entry.path).into(),
+                    inode: entry.inode,
+                    is_symlink: entry.is_symlink,
+                    is_ignored: entry.is_ignored,
+                }
+            }),
+            &(),
+        );
+        let snapshot = Snapshot {
+            id,
+            scan_id: 0,
+            abs_path: Path::new("").into(),
+            root_name: worktree.root_name,
+            ignores: Default::default(),
+            entries,
+        };
+        Self { snapshot }
+    }
+}
+
 #[derive(Clone)]
 pub struct Snapshot {
     id: usize,
@@ -1367,6 +1428,7 @@ impl WorktreeHandle for ModelHandle<Worktree> {
                     }
                 })
             }
+            Worktree::Remote(tree) => todo!(),
         }
     }