mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Avoid infinite loop when collaborators follow each other
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
parent
13a2dacc60
commit
3e0bc979c3
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -678,17 +678,21 @@ impl Server {
|
||||
request: TypedEnvelope<proto::Follow>,
|
||||
) -> tide::Result<proto::FollowResponse> {
|
||||
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::<Editor>()
|
||||
.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::<Editor>()
|
||||
.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();
|
||||
|
@ -264,10 +264,10 @@ pub trait FollowableItem: Item {
|
||||
&self,
|
||||
event: &Self::Event,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::update_followers::update_view::Variant>;
|
||||
) -> Option<proto::update_view::Variant>;
|
||||
fn apply_update_message(
|
||||
&mut self,
|
||||
message: proto::update_followers::update_view::Variant,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
@ -279,10 +279,10 @@ pub trait FollowableItemHandle: ItemHandle {
|
||||
&self,
|
||||
event: &dyn Any,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::update_followers::update_view::Variant>;
|
||||
) -> Option<proto::update_view::Variant>;
|
||||
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<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||
&self,
|
||||
event: &dyn Any,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::update_followers::update_view::Variant> {
|
||||
) -> Option<proto::update_view::Variant> {
|
||||
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<T: Item> ItemHandle for ViewHandle<T> {
|
||||
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<T: Item> ItemHandle for ViewHandle<T> {
|
||||
.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<proto::update_followers::update_view::Variant>),
|
||||
Loading(Vec<proto::update_view::Variant>),
|
||||
Loaded(Box<dyn FollowableItemHandle>),
|
||||
}
|
||||
|
||||
@ -1110,7 +1110,7 @@ impl Workspace {
|
||||
|
||||
fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
|
||||
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<Pane>) -> Option<PeerId> {
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user