diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7f28af0c40c7715d9c5599c780a8576b83bbb221..fee7cc445e91984c0facac81c81c14289890b9dc 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use rpc::proto::{self, update_followers::update_view}; +use rpc::proto::{self, update_view}; use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f0743c5b4fa02ba4952051eb7a76cd8c004dd058..dbf1218ccd0f90e5c537160303561e7717287993 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -556,21 +556,6 @@ message UpdateFollowers { View create_view = 4; UpdateView update_view = 5; } - - message UpdateActiveView { - optional uint64 id = 1; - } - - message UpdateView { - uint64 id = 1; - oneof variant { - Editor editor = 2; - } - - message Editor { - Anchor scroll_top = 1; - } - } } message Unfollow { @@ -580,10 +565,30 @@ message Unfollow { // Entities +message UpdateActiveView { + optional uint64 id = 1; + optional uint32 leader_id = 2; +} + +message UpdateView { + uint64 id = 1; + optional uint32 leader_id = 2; + + oneof variant { + Editor editor = 3; + } + + message Editor { + Anchor scroll_top = 1; + } +} + message View { uint64 id = 1; + optional uint32 leader_id = 2; + oneof variant { - Editor editor = 2; + Editor editor = 3; } message Editor { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index fe56cea2e299c763fc29b9605ebd5c1015728a3a..771d82e0173b2f07d27e481380b5a3bdfe5a2e70 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -678,17 +678,21 @@ impl Server { request: TypedEnvelope, ) -> tide::Result { let leader_id = ConnectionId(request.payload.leader_id); + let follower_id = request.sender_id; if !self .state() - .project_connection_ids(request.payload.project_id, request.sender_id)? + .project_connection_ids(request.payload.project_id, follower_id)? .contains(&leader_id) { Err(anyhow!("no such peer"))?; } - let response = self + let mut response = self .peer .forward_request(request.sender_id, leader_id, request.payload) .await?; + response + .views + .retain(|view| view.leader_id != Some(follower_id.0)); Ok(response) } @@ -716,9 +720,18 @@ impl Server { let connection_ids = self .state() .project_connection_ids(request.payload.project_id, request.sender_id)?; + let leader_id = request + .payload + .variant + .as_ref() + .and_then(|variant| match variant { + proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + }); for follower_id in &request.payload.follower_ids { let follower_id = ConnectionId(*follower_id); - if connection_ids.contains(&follower_id) { + if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id { self.peer .forward_send(request.sender_id, follower_id, request.payload.clone())?; } @@ -4265,9 +4278,6 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4328,6 +4338,108 @@ mod tests { ); } + #[gpui::test(iterations = 10)] + async fn test_peers_following_each_other(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() + .downcast::() + .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, "2.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a + .update(cx_a, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); + workspace + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + 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 + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + workspace.open_path((worktree_id, "3.txt"), cx) + }) + .await + .unwrap(); + + // Ensure peers following each other doesn't cause an infinite loop. + cx_a.foreground().run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "3.txt").into()) + ); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c5d1dfbf6412c9ad8703622dcf9d1f89077b5b3..78a59e9d91daaa8d0c4e216e65fe69a69eb0f0fb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -264,10 +264,10 @@ pub trait FollowableItem: Item { &self, event: &Self::Event, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &mut self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; } @@ -279,10 +279,10 @@ pub trait FollowableItemHandle: ItemHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; } @@ -300,13 +300,13 @@ impl FollowableItemHandle for ViewHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option { + ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_message(message, cx)) @@ -403,6 +403,7 @@ impl ItemHandle for ViewHandle { proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), }), cx, ); @@ -441,12 +442,11 @@ impl ItemHandle for ViewHandle { .and_then(|i| i.to_update_message(event, cx)) { workspace.update_followers( - proto::update_followers::Variant::UpdateView( - proto::update_followers::UpdateView { - id: item.id() as u64, - variant: Some(message), - }, - ), + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -628,7 +628,7 @@ struct FollowerState { #[derive(Debug)] enum FollowerItem { - Loading(Vec), + Loading(Vec), Loaded(Box), } @@ -1110,7 +1110,7 @@ impl Workspace { fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { - self.active_pane = pane; + self.active_pane = pane.clone(); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); @@ -1119,11 +1119,10 @@ impl Workspace { } self.update_followers( - proto::update_followers::Variant::UpdateActiveView( - proto::update_followers::UpdateActiveView { - id: self.active_item(cx).map(|item| item.id() as u64), - }, - ), + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + leader_id: self.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -1520,14 +1519,22 @@ impl Workspace { Ok(proto::FollowResponse { active_view_id, views: this - .items(cx) - .filter_map(|item| { - let id = item.id() as u64; - let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx)?; - Some(proto::View { - id, - variant: Some(variant), + .panes() + .iter() + .flat_map(|pane| { + let leader_id = this.leader_for_pane(pane).map(|id| id.0); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let id = item.id() as u64; + let item = item.to_followable_item_handle(cx)?; + let variant = item.to_state_message(cx)?; + Some(proto::View { + id, + leader_id, + variant: Some(variant), + }) + } }) }) .collect(), @@ -1705,6 +1712,18 @@ impl Workspace { None } + fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + self.follower_states_by_leader + .iter() + .find_map(|(leader_id, state)| { + if state.contains_key(pane) { + Some(*leader_id) + } else { + None + } + }) + } + fn update_leader_state( &mut self, leader_id: PeerId,