@@ -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(¶ms, 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(¶ms, 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>,