Start work on an integration test for following

Max Brunsfeld created

Change summary

crates/server/src/rpc.rs          | 180 ++++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs |   4 
2 files changed, 177 insertions(+), 7 deletions(-)

Detailed changes

crates/server/src/rpc.rs 🔗

@@ -112,6 +112,7 @@ impl Server {
             .add_request_handler(Server::join_channel)
             .add_message_handler(Server::leave_channel)
             .add_request_handler(Server::send_channel_message)
+            .add_request_handler(Server::follow)
             .add_request_handler(Server::get_channel_messages);
 
         Arc::new(server)
@@ -669,6 +670,25 @@ impl Server {
         Ok(())
     }
 
+    async fn follow(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::Follow>,
+    ) -> tide::Result<proto::FollowResponse> {
+        let leader_id = ConnectionId(request.payload.leader_id);
+        if !self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)?
+            .contains(&leader_id)
+        {
+            Err(anyhow!("no such peer"))?;
+        }
+        let response = self
+            .peer
+            .forward_request(request.sender_id, leader_id, request.payload)
+            .await?;
+        Ok(response)
+    }
+
     async fn get_channels(
         self: Arc<Server>,
         request: TypedEnvelope<proto::GetChannels>,
@@ -1016,7 +1036,7 @@ mod tests {
         self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
         ToOffset, ToggleCodeActions, Undo,
     };
-    use gpui::{executor, ModelHandle, TestAppContext};
+    use gpui::{executor, ModelHandle, TestAppContext, ViewHandle};
     use language::{
         tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
         LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
@@ -1028,7 +1048,7 @@ mod tests {
         fs::{FakeFs, Fs as _},
         search::SearchQuery,
         worktree::WorktreeHandle,
-        DiagnosticSummary, Project, ProjectPath,
+        DiagnosticSummary, Project, ProjectPath, WorktreeId,
     };
     use rand::prelude::*;
     use rpc::PeerId;
@@ -1046,7 +1066,7 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Settings, Workspace, WorkspaceParams};
+    use workspace::{Settings, SplitDirection, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -3225,7 +3245,7 @@ mod tests {
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "main.rs").into(), cx)
+                workspace.open_path((worktree_id, "main.rs"), cx)
             })
             .await
             .unwrap()
@@ -3459,7 +3479,7 @@ mod tests {
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "one.rs").into(), cx)
+                workspace.open_path((worktree_id, "one.rs"), cx)
             })
             .await
             .unwrap()
@@ -4148,6 +4168,80 @@ mod tests {
         }
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        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;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap();
+        let editor_a2 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap();
+
+        // Client B opens an editor.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let editor_b1 = workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap();
+
+        // Client B starts following client A.
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                let leader_id = project_b.read(cx).collaborators().keys().next().unwrap();
+                workspace.follow(*leader_id, cx)
+            })
+            .await
+            .unwrap();
+    }
+
     #[gpui::test(iterations = 100)]
     async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
         cx.foreground().forbid_parking();
@@ -4477,6 +4571,7 @@ mod tests {
                 client,
                 peer_id,
                 user_store,
+                language_registry: Arc::new(LanguageRegistry::test()),
                 project: Default::default(),
                 buffers: Default::default(),
             };
@@ -4541,6 +4636,7 @@ mod tests {
         client: Arc<Client>,
         pub peer_id: PeerId,
         pub user_store: ModelHandle<UserStore>,
+        language_registry: Arc<LanguageRegistry>,
         project: Option<ModelHandle<Project>>,
         buffers: HashSet<ModelHandle<language::Buffer>>,
     }
@@ -4568,6 +4664,80 @@ mod tests {
             while authed_user.next().await.unwrap().is_none() {}
         }
 
+        async fn build_local_project(
+            &mut self,
+            fs: Arc<FakeFs>,
+            root_path: impl AsRef<Path>,
+            cx: &mut TestAppContext,
+        ) -> (ModelHandle<Project>, WorktreeId) {
+            let project = cx.update(|cx| {
+                Project::local(
+                    self.client.clone(),
+                    self.user_store.clone(),
+                    self.language_registry.clone(),
+                    fs,
+                    cx,
+                )
+            });
+            self.project = Some(project.clone());
+            let (worktree, _) = project
+                .update(cx, |p, cx| {
+                    p.find_or_create_local_worktree(root_path, true, cx)
+                })
+                .await
+                .unwrap();
+            worktree
+                .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+                .await;
+            project
+                .update(cx, |project, _| project.next_remote_id())
+                .await;
+            (project, worktree.read_with(cx, |tree, _| tree.id()))
+        }
+
+        async fn build_remote_project(
+            &mut self,
+            project_id: u64,
+            cx: &mut TestAppContext,
+        ) -> ModelHandle<Project> {
+            let project = Project::remote(
+                project_id,
+                self.client.clone(),
+                self.user_store.clone(),
+                self.language_registry.clone(),
+                FakeFs::new(cx.background()),
+                &mut cx.to_async(),
+            )
+            .await
+            .unwrap();
+            self.project = Some(project.clone());
+            project
+        }
+
+        fn build_workspace(
+            &self,
+            project: &ModelHandle<Project>,
+            cx: &mut TestAppContext,
+        ) -> ViewHandle<Workspace> {
+            let (window_id, _) = cx.add_window(|cx| EmptyView);
+            cx.add_view(window_id, |cx| {
+                let fs = project.read(cx).fs().clone();
+                Workspace::new(
+                    &WorkspaceParams {
+                        fs,
+                        project: project.clone(),
+                        user_store: self.user_store.clone(),
+                        languages: self.language_registry.clone(),
+                        channel_list: cx.add_model(|cx| {
+                            ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
+                        }),
+                        client: self.client.clone(),
+                    },
+                    cx,
+                )
+            })
+        }
+
         fn simulate_host(
             mut self,
             project: ModelHandle<Project>,

crates/workspace/src/workspace.rs 🔗

@@ -825,11 +825,11 @@ impl Workspace {
 
     pub fn open_path(
         &mut self,
-        path: ProjectPath,
+        path: impl Into<ProjectPath>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
         let pane = self.active_pane().downgrade();
-        let task = self.load_path(path, cx);
+        let task = self.load_path(path.into(), cx);
         cx.spawn(|this, mut cx| async move {
             let (project_entry_id, build_item) = task.await?;
             let pane = pane