From be51a5831109149f766243675e75ca30de3cf34d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 May 2022 17:24:00 -0700 Subject: [PATCH 01/26] Start work on requesting to join projects Co-authored-by: Nathan Sobo --- assets/themes/cave-dark.json | 36 +- assets/themes/cave-light.json | 36 +- assets/themes/dark.json | 36 +- assets/themes/light.json | 36 +- assets/themes/solarized-dark.json | 36 +- assets/themes/solarized-light.json | 36 +- assets/themes/sulphurpool-dark.json | 36 +- assets/themes/sulphurpool-light.json | 36 +- crates/client/src/client.rs | 6 +- crates/client/src/user.rs | 8 +- crates/collab/src/rpc.rs | 218 +++++++----- crates/collab/src/rpc/store.rs | 310 +++++++----------- crates/contacts_panel/Cargo.toml | 1 + .../src/contact_notification.rs | 177 ++-------- crates/contacts_panel/src/contacts_panel.rs | 45 ++- .../src/join_project_notification.rs | 71 ++++ crates/contacts_panel/src/notifications.rs | 112 +++++++ crates/gpui/src/presenter.rs | 8 +- crates/project/src/project.rs | 167 ++++++---- crates/rpc/proto/zed.proto | 33 +- crates/rpc/src/proto.rs | 8 +- crates/theme/src/theme.rs | 3 +- crates/workspace/src/workspace.rs | 49 +-- crates/zed/src/zed.rs | 23 +- styles/src/styleTree/contactsPanel.ts | 16 +- 25 files changed, 660 insertions(+), 883 deletions(-) create mode 100644 crates/contacts_panel/src/join_project_notification.rs create mode 100644 crates/contacts_panel/src/notifications.rs diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 13eb8f247367069dfa3c1ec04e19dd11e3690c8e..347a8d90b9b86abde5e2c0d4ce791e685c76af0a 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#8b8792", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#26232a", - "hover": { - "background": "#5852603d" - }, - "active": { - "background": "#5852605c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 097d2755aa256b6d5942e0737408b3a25cfc28c7..89acf414ab2764905ca4770760f680a88a715cd0 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#585260", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#e2dfe7", - "hover": { - "background": "#8b87921f" - }, - "active": { - "background": "#8b87922e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index c4e33765ce10a32e4f60794b06396f94a15b46dc..e91642bb25b512760e7600e7d5c6f5bb5c9c5799 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#9c9c9c", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#1c1c1c", - "hover": { - "background": "#232323" - }, - "active": { - "background": "#2b2b2b" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 1cff4df3ad5148a4ce60d883c2b1863013484575..99c1bab730d3b7df7eea1fbc51e5f841ebab15b4 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#474747", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#f8f8f8", - "hover": { - "background": "#eaeaea" - }, - "active": { - "background": "#e3e3e3" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 8606c78ca06fccbcc214505d664540314bb8a8fb..8a75aa02dd184792b3f8dfdfaf31d74295356d4c 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#93a1a1", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#073642", - "hover": { - "background": "#586e753d" - }, - "active": { - "background": "#586e755c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index fb8e781ea0d9c8361c8a76f118f6ffa19c35feaf..84bd0762ec07789bd8570dd78f605061857d06e3 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#586e75", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#eee8d5", - "hover": { - "background": "#93a1a11f" - }, - "active": { - "background": "#93a1a12e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 607cecf990c3853624f682c82091ff92ed8bc226..716e81d6a298fff34f4cbeb39a65f4878829d884 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#979db4", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#293256", - "hover": { - "background": "#5e66873d" - }, - "active": { - "background": "#5e66875c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index d66382331ae8c43419772ddd017b635b94a230ed..09bb2127df7f8080c7058fd0fe5abd9f449b2d0d 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#5e6687", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#dfe2f1", - "hover": { - "background": "#979db41f" - }, - "active": { - "background": "#979db42e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 75d5b459e134d97b61d01e949e164ab80baf9d89..0fc0f97949bfdc73d1921ab23c514ef11b29a18b 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1106,7 +1106,7 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); client.add_model_message_handler( - move |model: ModelHandle, _: TypedEnvelope, _, cx| { + move |model: ModelHandle, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), @@ -1135,8 +1135,8 @@ mod tests { let subscription3 = model3.update(cx, |_, cx| client.add_model_for_remote_entity(3, cx)); drop(subscription3); - server.send(proto::UnshareProject { project_id: 1 }); - server.send(proto::UnshareProject { project_id: 2 }); + server.send(proto::JoinProject { project_id: 1 }); + server.send(proto::JoinProject { project_id: 2 }); done_rx1.next().await.unwrap(); done_rx2.next().await.unwrap(); } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 84254da73a5bb0815acf3b66bf0cef112088f030..97cf225b5fa13c5945c30db38fc57af1c0016009 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -17,6 +17,12 @@ pub struct User { pub avatar: Option>, } +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.github_login == other.github_login + } +} + #[derive(Debug)] pub struct Contact { pub user: Arc, @@ -27,7 +33,6 @@ pub struct Contact { #[derive(Debug)] pub struct ProjectMetadata { pub id: u64, - pub is_shared: bool, pub worktree_root_names: Vec, pub guests: Vec>, } @@ -560,7 +565,6 @@ impl Contact { projects.push(ProjectMetadata { id: project.id, worktree_root_names: project.worktree_root_names.clone(), - is_shared: project.is_shared, guests, }); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1e7384c2c3dd91c2591ee46725f0ab73c7c99aa3..19d8ebc779835945258aeb723cb955a67b6b4866 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -66,6 +66,11 @@ impl Response { self.server.peer.respond(self.receipt, payload)?; Ok(()) } + + fn into_receipt(self) -> Receipt { + self.responded.store(true, SeqCst); + self.receipt + } } pub struct Server { @@ -115,10 +120,9 @@ impl Server { .add_request_handler(Server::ping) .add_request_handler(Server::register_project) .add_message_handler(Server::unregister_project) - .add_request_handler(Server::share_project) - .add_message_handler(Server::unshare_project) .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) + .add_message_handler(Server::respond_to_join_project_request) .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) @@ -336,12 +340,10 @@ impl Server { let removed_connection = self.store_mut().await.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { - if let Some(share) = project.share { - broadcast(connection_id, share.guests.keys().copied(), |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - } + broadcast(connection_id, project.guests.keys().copied(), |conn_id| { + self.peer + .send(conn_id, proto::UnregisterProject { project_id }) + }); } for (project_id, peer_ids) in removed_connection.guest_project_ids { @@ -402,20 +404,20 @@ impl Server { Ok(()) } - async fn share_project( - self: Arc, - request: TypedEnvelope, - response: Response, - ) -> Result<()> { - let user_id = { - let mut state = self.store_mut().await; - state.share_project(request.payload.project_id, request.sender_id)?; - state.user_id_for_connection(request.sender_id)? - }; - self.update_user_contacts(user_id).await?; - response.send(proto::Ack {})?; - Ok(()) - } + // async fn share_project( + // self: Arc, + // request: TypedEnvelope, + // response: Response, + // ) -> Result<()> { + // let user_id = { + // let mut state = self.store_mut().await; + // state.share_project(request.payload.project_id, request.sender_id)?; + // state.user_id_for_connection(request.sender_id)? + // }; + // self.update_user_contacts(user_id).await?; + // response.send(proto::Ack {})?; + // Ok(()) + // } async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; @@ -447,24 +449,6 @@ impl Server { Ok(()) } - async fn unshare_project( - self: Arc, - request: TypedEnvelope, - ) -> Result<()> { - let project_id = request.payload.project_id; - let project; - { - let mut state = self.store_mut().await; - project = state.unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, project.connection_ids, |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - } - self.update_user_contacts(project.host_user_id).await?; - Ok(()) - } - async fn join_project( self: Arc, request: TypedEnvelope, @@ -473,9 +457,12 @@ impl Server { let project_id = request.payload.project_id; let host_user_id; let guest_user_id; + let host_connection_id; { let state = self.store().await; - host_user_id = state.project(project_id)?.host_user_id; + let project = state.project(project_id)?; + host_user_id = project.host_user_id; + host_connection_id = project.host_connection_id; guest_user_id = state.user_id_for_connection(request.sender_id)?; }; @@ -488,22 +475,71 @@ impl Server { return Err(anyhow!("no such project"))?; } + self.store_mut().await.request_join_project( + guest_user_id, + project_id, + response.into_receipt(), + )?; + self.peer.send( + host_connection_id, + proto::RequestJoinProject { + project_id, + requester_id: guest_user_id.to_proto(), + }, + )?; + Ok(()) + } + + async fn respond_to_join_project_request( + self: Arc, + request: TypedEnvelope, + ) -> Result<()> { + let host_user_id; + { - let state = &mut *self.store_mut().await; - let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; - let share = joined.project.share()?; - let peer_count = share.guests.len(); + let mut state = self.store_mut().await; + let project_id = request.payload.project_id; + let project = state.project(project_id)?; + if project.host_connection_id != request.sender_id { + Err(anyhow!("no such connection"))?; + } + + host_user_id = project.host_user_id; + let guest_user_id = UserId::from_proto(request.payload.requester_id); + + if !request.payload.allow { + let receipts = state + .deny_join_project_request(request.sender_id, guest_user_id, project_id) + .ok_or_else(|| anyhow!("no such request"))?; + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline {}, + )), + }, + )?; + } + return Ok(()); + } + + let (receipts_with_replica_ids, project) = state + .accept_join_project_request(request.sender_id, guest_user_id, project_id) + .ok_or_else(|| anyhow!("no such request"))?; + + let peer_count = project.guests.len(); let mut collaborators = Vec::with_capacity(peer_count); collaborators.push(proto::Collaborator { - peer_id: joined.project.host_connection_id.0, + peer_id: project.host_connection_id.0, replica_id: 0, - user_id: joined.project.host_user_id.to_proto(), + user_id: project.host_user_id.to_proto(), }); - let worktrees = share + let worktrees = project .worktrees .iter() .filter_map(|(id, shared_worktree)| { - let worktree = joined.project.worktrees.get(&id)?; + let worktree = project.worktrees.get(&id)?; Some(proto::Worktree { id: *id, root_name: worktree.root_name.clone(), @@ -517,8 +553,8 @@ impl Server { scan_id: shared_worktree.scan_id, }) }) - .collect(); - for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { + .collect::>(); + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &project.guests { if *peer_conn_id != request.sender_id { collaborators.push(proto::Collaborator { peer_id: peer_conn_id.0, @@ -527,30 +563,41 @@ impl Server { }); } } - broadcast( - request.sender_id, - joined.project.connection_ids(), - |conn_id| { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id, - collaborator: Some(proto::Collaborator { - peer_id: request.sender_id.0, - replica_id: joined.replica_id as u32, - user_id: guest_user_id.to_proto(), - }), - }, - ) - }, - ); - response.send(proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - })?; + for conn_id in project.connection_ids() { + for (receipt, replica_id) in &receipts_with_replica_ids { + if conn_id != receipt.sender_id { + self.peer.send( + conn_id, + proto::AddProjectCollaborator { + project_id, + collaborator: Some(proto::Collaborator { + peer_id: receipt.sender_id.0, + replica_id: *replica_id as u32, + user_id: guest_user_id.to_proto(), + }), + }, + )?; + } + } + } + + for (receipt, replica_id) in receipts_with_replica_ids { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Accept( + proto::join_project_response::Accept { + worktrees: worktrees.clone(), + replica_id: replica_id as u32, + collaborators: collaborators.clone(), + language_servers: project.language_servers.clone(), + }, + )), + }, + )?; + } } + self.update_user_contacts(host_user_id).await?; Ok(()) } @@ -599,6 +646,7 @@ impl Server { Worktree { root_name: request.payload.root_name.clone(), visible: request.payload.visible, + ..Default::default() }, )?; @@ -2782,9 +2830,6 @@ mod tests { let worktree = store .project(project_id) .unwrap() - .share - .as_ref() - .unwrap() .worktrees .get(&worktree_id.to_proto()) .unwrap(); @@ -5055,7 +5100,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", false, vec![])]), + ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5077,7 +5122,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec![])]), + ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5093,7 +5138,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_a", true, vec![("a", vec!["user_b"])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5112,8 +5157,8 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_a", true, vec![("a", vec!["user_b"])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) @@ -5135,7 +5180,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) @@ -5151,7 +5196,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", false, vec![]) ] ) @@ -5174,14 +5219,14 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) }); } - fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { + fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> { user_store .contacts() .iter() @@ -5192,7 +5237,6 @@ mod tests { .map(|p| { ( p.worktree_root_names[0].as_str(), - p.is_shared, p.guests.iter().map(|p| p.github_login.as_str()).collect(), ) }) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 07103204e5553d7f6a19f96209261c706fafb18c..765162936c61d9f4f97cc5564b80baf200f1429b 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,7 +1,7 @@ use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; -use rpc::{proto, ConnectionId}; +use rpc::{proto, ConnectionId, Receipt}; use std::{collections::hash_map, path::PathBuf}; use tracing::instrument; @@ -17,31 +17,24 @@ pub struct Store { struct ConnectionState { user_id: UserId, projects: HashSet, + requested_projects: HashSet, channels: HashSet, } pub struct Project { pub host_connection_id: ConnectionId, pub host_user_id: UserId, - pub share: Option, + pub guests: HashMap, + pub join_requests: HashMap>>, + pub active_replica_ids: HashSet, pub worktrees: HashMap, pub language_servers: Vec, } +#[derive(Default)] pub struct Worktree { pub root_name: String, pub visible: bool, -} - -#[derive(Default)] -pub struct ProjectShare { - pub guests: HashMap, - pub active_replica_ids: HashSet, - pub worktrees: HashMap, -} - -#[derive(Default)] -pub struct WorktreeShare { pub entries: HashMap, pub diagnostic_summaries: BTreeMap, pub scan_id: u64, @@ -62,18 +55,6 @@ pub struct RemovedConnectionState { pub contact_ids: HashSet, } -pub struct JoinedProject<'a> { - pub replica_id: ReplicaId, - pub project: &'a Project, -} - -pub struct SharedProject {} - -pub struct UnsharedProject { - pub connection_ids: Vec, - pub host_user_id: UserId, -} - pub struct LeftProject { pub connection_ids: Vec, pub host_user_id: UserId, @@ -93,7 +74,7 @@ impl Store { let mut shared_projects = 0; for project in self.projects.values() { registered_projects += 1; - if project.share.is_some() { + if !project.guests.is_empty() { shared_projects += 1; } } @@ -112,6 +93,7 @@ impl Store { ConnectionState { user_id, projects: Default::default(), + requested_projects: Default::default(), channels: Default::default(), }, ); @@ -275,18 +257,15 @@ impl Store { if project.host_user_id == user_id { metadata.push(proto::ProjectMetadata { id: project_id, - is_shared: project.share.is_some(), worktree_root_names: project .worktrees .values() .map(|worktree| worktree.root_name.clone()) .collect(), guests: project - .share - .iter() - .flat_map(|share| { - share.guests.values().map(|(_, user_id)| user_id.to_proto()) - }) + .guests + .values() + .map(|(_, user_id)| user_id.to_proto()) .collect(), }); } @@ -307,7 +286,9 @@ impl Store { Project { host_connection_id, host_user_id, - share: None, + guests: Default::default(), + join_requests: Default::default(), + active_replica_ids: Default::default(), worktrees: Default::default(), language_servers: Default::default(), }, @@ -332,10 +313,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { project.worktrees.insert(worktree_id, worktree); - if let Ok(share) = project.share_mut() { - share.worktrees.insert(worktree_id, Default::default()); - } - Ok(()) } else { Err(anyhow!("no such project"))? @@ -356,11 +333,9 @@ impl Store { host_connection.projects.remove(&project_id); } - if let Some(share) = &project.share { - for guest_connection in share.guests.keys() { - if let Some(connection) = self.connections.get_mut(&guest_connection) { - connection.projects.remove(&project_id); - } + for guest_connection in project.guests.keys() { + if let Some(connection) = self.connections.get_mut(&guest_connection) { + connection.projects.remove(&project_id); } } @@ -391,64 +366,7 @@ impl Store { .worktrees .remove(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; - - let mut guest_connection_ids = Vec::new(); - if let Ok(share) = project.share_mut() { - guest_connection_ids.extend(share.guests.keys()); - share.worktrees.remove(&worktree_id); - } - - Ok((worktree, guest_connection_ids)) - } - - pub fn share_project( - &mut self, - project_id: u64, - connection_id: ConnectionId, - ) -> Result { - if let Some(project) = self.projects.get_mut(&project_id) { - if project.host_connection_id == connection_id { - let mut share = ProjectShare::default(); - for worktree_id in project.worktrees.keys() { - share.worktrees.insert(*worktree_id, Default::default()); - } - project.share = Some(share); - return Ok(SharedProject {}); - } - } - Err(anyhow!("no such project"))? - } - - pub fn unshare_project( - &mut self, - project_id: u64, - acting_connection_id: ConnectionId, - ) -> Result { - let project = if let Some(project) = self.projects.get_mut(&project_id) { - project - } else { - return Err(anyhow!("no such project"))?; - }; - - if project.host_connection_id != acting_connection_id { - return Err(anyhow!("not your project"))?; - } - - let connection_ids = project.connection_ids(); - if let Some(share) = project.share.take() { - for connection_id in share.guests.into_keys() { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.projects.remove(&project_id); - } - } - - Ok(UnsharedProject { - connection_ids, - host_user_id: project.host_user_id, - }) - } else { - Err(anyhow!("project is not shared"))? - } + Ok((worktree, project.guest_connection_ids())) } pub fn update_diagnostic_summary( @@ -464,7 +382,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -495,35 +412,77 @@ impl Store { Err(anyhow!("no such project"))? } - pub fn join_project( + pub fn request_join_project( &mut self, - connection_id: ConnectionId, - user_id: UserId, + requester_id: UserId, project_id: u64, - ) -> Result { + receipt: Receipt, + ) -> Result<()> { let connection = self .connections - .get_mut(&connection_id) + .get_mut(&receipt.sender_id) .ok_or_else(|| anyhow!("no such connection"))?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; + connection.requested_projects.insert(project_id); + project + .join_requests + .entry(requester_id) + .or_default() + .push(receipt); + Ok(()) + } - let share = project.share_mut()?; - connection.projects.insert(project_id); + pub fn deny_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> Option>> { + let project = self.projects.get_mut(&project_id)?; + if responder_connection_id != project.host_connection_id { + return None; + } - let mut replica_id = 1; - while share.active_replica_ids.contains(&replica_id) { - replica_id += 1; + let receipts = project.join_requests.remove(&requester_id)?; + for receipt in &receipts { + let requester_connection = self.connections.get_mut(&receipt.sender_id)?; + requester_connection.requested_projects.remove(&project_id); } - share.active_replica_ids.insert(replica_id); - share.guests.insert(connection_id, (replica_id, user_id)); + Some(receipts) + } - Ok(JoinedProject { - replica_id, - project: &self.projects[&project_id], - }) + pub fn accept_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> Option<(Vec<(Receipt, ReplicaId)>, &Project)> { + let project = self.projects.get_mut(&project_id)?; + if responder_connection_id != project.host_connection_id { + return None; + } + + let receipts = project.join_requests.remove(&requester_id)?; + let mut receipts_with_replica_ids = Vec::new(); + for receipt in receipts { + let requester_connection = self.connections.get_mut(&receipt.sender_id)?; + requester_connection.requested_projects.remove(&project_id); + requester_connection.projects.insert(project_id); + let mut replica_id = 1; + while project.active_replica_ids.contains(&replica_id) { + replica_id += 1; + } + project.active_replica_ids.insert(replica_id); + project + .guests + .insert(receipt.sender_id, (replica_id, requester_id)); + receipts_with_replica_ids.push((receipt, replica_id)); + } + + Some((receipts_with_replica_ids, project)) } pub fn leave_project( @@ -535,15 +494,11 @@ impl Store { .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - let share = project - .share - .as_mut() - .ok_or_else(|| anyhow!("project is not shared"))?; - let (replica_id, _) = share + let (replica_id, _) = project .guests .remove(&connection_id) .ok_or_else(|| anyhow!("cannot leave a project before joining it"))?; - share.active_replica_ids.remove(&replica_id); + project.active_replica_ids.remove(&replica_id); if let Some(connection) = self.connections.get_mut(&connection_id) { connection.projects.remove(&project_id); @@ -566,7 +521,6 @@ impl Store { ) -> Result> { let project = self.write_project(project_id, connection_id)?; let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -611,12 +565,7 @@ impl Store { .get(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -634,12 +583,7 @@ impl Store { .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -653,28 +597,21 @@ impl Store { for project_id in &connection.projects { let project = &self.projects.get(&project_id).unwrap(); if project.host_connection_id != *connection_id { - assert!(project - .share - .as_ref() - .unwrap() - .guests - .contains_key(connection_id)); + assert!(project.guests.contains_key(connection_id)); } - if let Some(share) = project.share.as_ref() { - for (worktree_id, worktree) in share.worktrees.iter() { - let mut paths = HashMap::default(); - for entry in worktree.entries.values() { - let prev_entry = paths.insert(&entry.path, entry); - assert_eq!( - prev_entry, - None, - "worktree {:?}, duplicate path for entries {:?} and {:?}", - worktree_id, - prev_entry.unwrap(), - entry - ); - } + for (worktree_id, worktree) in project.worktrees.iter() { + let mut paths = HashMap::default(); + for entry in worktree.entries.values() { + let prev_entry = paths.insert(&entry.path, entry); + assert_eq!( + prev_entry, + None, + "worktree {:?}, duplicate path for entries {:?} and {:?}", + worktree_id, + prev_entry.unwrap(), + entry + ); } } } @@ -702,21 +639,19 @@ impl Store { let host_connection = self.connections.get(&project.host_connection_id).unwrap(); assert!(host_connection.projects.contains(project_id)); - if let Some(share) = &project.share { - for guest_connection_id in share.guests.keys() { - let guest_connection = self.connections.get(guest_connection_id).unwrap(); - assert!(guest_connection.projects.contains(project_id)); - } - assert_eq!(share.active_replica_ids.len(), share.guests.len(),); - assert_eq!( - share.active_replica_ids, - share - .guests - .values() - .map(|(replica_id, _)| *replica_id) - .collect::>(), - ); + for guest_connection_id in project.guests.keys() { + let guest_connection = self.connections.get(guest_connection_id).unwrap(); + assert!(guest_connection.projects.contains(project_id)); } + assert_eq!(project.active_replica_ids.len(), project.guests.len(),); + assert_eq!( + project.active_replica_ids, + project + .guests + .values() + .map(|(replica_id, _)| *replica_id) + .collect::>(), + ); } for (channel_id, channel) in &self.channels { @@ -730,38 +665,15 @@ impl Store { impl Project { pub fn guest_connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share.guests.keys().copied().collect() - } else { - Vec::new() - } + self.guests.keys().copied().collect() } pub fn connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share - .guests - .keys() - .copied() - .chain(Some(self.host_connection_id)) - .collect() - } else { - vec![self.host_connection_id] - } - } - - pub fn share(&self) -> Result<&ProjectShare> { - Ok(self - .share - .as_ref() - .ok_or_else(|| anyhow!("worktree is not shared"))?) - } - - fn share_mut(&mut self) -> Result<&mut ProjectShare> { - Ok(self - .share - .as_mut() - .ok_or_else(|| anyhow!("worktree is not shared"))?) + self.guests + .keys() + .copied() + .chain(Some(self.host_connection_id)) + .collect() } } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index de49f070b94338cd64e5cf29056fd7c1bb2088ec..b6d7bf63fc67897dac2c1fc34281fbed4df04d73 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -13,6 +13,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } picker = { path = "../picker" } +project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs index 6369f70ce0244fdd0a1a8540d4a91a0d86c0acbc..7df0f7cad96b0453145d895d88ef0e66ab32e031 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/contacts_panel/src/contact_notification.rs @@ -1,13 +1,11 @@ +use crate::notifications::render_user_notification; use client::{ContactEvent, ContactEventKind, UserStore}; use gpui::{ - elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, + elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext, + View, ViewContext, }; -use settings::Settings; use workspace::Notification; -use crate::render_icon_button; - impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); pub fn init(cx: &mut MutableAppContext) { @@ -33,9 +31,6 @@ pub enum Event { Dismiss, } -enum Decline {} -enum Accept {} - impl Entity for ContactNotification { type Event = Event; } @@ -47,8 +42,38 @@ impl View for ContactNotification { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { match self.event.kind { - ContactEventKind::Requested => self.render_incoming_request(cx), - ContactEventKind::Accepted => self.render_acceptance(cx), + ContactEventKind::Requested => render_user_notification( + self.event.user.clone(), + "wants to add you as a contact", + RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }, + vec![ + ( + "Decline", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }), + ), + ( + "Accept", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: true, + }), + ), + ], + cx, + ), + ContactEventKind::Accepted => render_user_notification( + self.event.user.clone(), + "accepted your contact request", + Dismiss(self.event.user.id), + vec![], + cx, + ), _ => unreachable!(), } } @@ -82,138 +107,6 @@ impl ContactNotification { Self { event, user_store } } - fn render_incoming_request(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - let user = &self.event.user; - let user_id = user.id; - - Flex::column() - .with_child(self.render_header("wants to add you as a contact.", theme, cx)) - .with_child( - Label::new( - "They won't know if you decline.".to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed(), - ) - .with_child( - Flex::row() - .with_child( - MouseEventHandler::new::( - self.event.user.id as usize, - cx, - |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Decline".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }); - }) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Accept".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }); - }) - .boxed(), - ) - .aligned() - .right() - .boxed(), - ) - .contained() - .boxed() - } - - fn render_acceptance(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - - self.render_header("accepted your contact request", theme, cx) - } - - fn render_header( - &self, - message: &'static str, - theme: &theme::ContactNotification, - cx: &mut RenderContext, - ) -> ElementBox { - let user = &self.event.user; - let user_id = user.id; - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.header_avatar) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .boxed() - })) - .with_child( - Text::new( - format!("{} {}", user.github_login, message), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .top() - .left() - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - render_icon_button( - theme.dismiss_button.style_for(state, false), - "icons/decline.svg", - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(5.)) - .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .flex_float() - .boxed(), - ) - .named("contact notification header") - } - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.user_store.update(cx, |store, cx| { store diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 03968d5aaaf740552423b4331026a38150d233f1..9ea62f0831b6097d438f4450aa756b1647cbc54f 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,5 +1,7 @@ mod contact_finder; mod contact_notification; +mod join_project_notification; +mod notifications; use client::{Contact, ContactEventKind, User, UserStore}; use contact_notification::ContactNotification; @@ -13,6 +15,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use join_project_notification::JoinProjectNotification; use serde::Deserialize; use settings::Settings; use std::sync::Arc; @@ -76,6 +79,7 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); contact_notification::init(cx); + join_project_notification::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); @@ -118,6 +122,33 @@ impl ContactsPanel { }) .detach(); + cx.defer({ + let workspace = workspace.clone(); + move |_, cx| { + if let Some(workspace_handle) = workspace.upgrade(cx) { + cx.subscribe(&workspace_handle.read(cx).project().clone(), { + let workspace = workspace.clone(); + move |_, project, event, cx| match event { + project::Event::ContactRequestedJoin(user) => { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_notification( + cx.add_view(|_| { + JoinProjectNotification::new(project, user.clone()) + }), + cx, + ) + }); + } + } + _ => {} + } + }) + .detach(); + } + } + }); + cx.subscribe(&app_state.user_store, { let user_store = app_state.user_store.downgrade(); move |_, _, event, cx| { @@ -320,7 +351,6 @@ impl ContactsPanel { .guests .iter() .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -328,7 +358,7 @@ impl ContactsPanel { .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.unshared_project_row.default; + let row = &theme.project_row.default; let tree_branch = theme.tree_branch.clone(); let line_height = row.name.text.line_height(font_cache); let cap_height = row.name.text.cap_height(font_cache); @@ -337,12 +367,7 @@ impl ContactsPanel { MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = if project.is_shared { - &theme.shared_project_row - } else { - &theme.unshared_project_row - } - .style_for(mouse_state, is_selected); + let row = theme.project_row.style_for(mouse_state, is_selected); Flex::row() .with_child( @@ -412,7 +437,7 @@ impl ContactsPanel { .with_style(row.container) .boxed() }) - .with_cursor_style(if !is_host && is_shared { + .with_cursor_style(if !is_host { CursorStyle::PointingHand } else { CursorStyle::Arrow @@ -947,7 +972,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 101, worktree_root_names: vec!["dir1".to_string()], - is_shared: true, guests: vec![2], }], }, @@ -958,7 +982,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 102, worktree_root_names: vec!["dir2".to_string()], - is_shared: true, guests: vec![2], }], }, diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..e3cd1c6c9eab2c6a30fdbedb0b268af0229787e6 --- /dev/null +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -0,0 +1,71 @@ +use client::User; +use gpui::{ + actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, +}; +use project::Project; +use std::sync::Arc; +use workspace::Notification; + +use crate::notifications::render_user_notification; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(JoinProjectNotification::decline); + cx.add_action(JoinProjectNotification::accept); +} + +pub enum Event { + Dismiss, +} + +actions!(contacts_panel, [Accept, Decline]); + +pub struct JoinProjectNotification { + project: ModelHandle, + user: Arc, +} + +impl JoinProjectNotification { + pub fn new(project: ModelHandle, user: Arc) -> Self { + Self { project, user } + } + + fn decline(&mut self, _: &Decline, cx: &mut ViewContext) { + self.project.update(cx, |project, cx| { + project.respond_to_join_request(self.user.id, false, cx) + }); + cx.emit(Event::Dismiss) + } + + fn accept(&mut self, _: &Accept, cx: &mut ViewContext) { + self.project.update(cx, |project, cx| { + project.respond_to_join_request(self.user.id, true, cx) + }); + cx.emit(Event::Dismiss) + } +} + +impl Entity for JoinProjectNotification { + type Event = Event; +} + +impl View for JoinProjectNotification { + fn ui_name() -> &'static str { + "JoinProjectNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + render_user_notification( + self.user.clone(), + "wants to join your project", + Decline, + vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))], + cx, + ) + } +} + +impl Notification for JoinProjectNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs new file mode 100644 index 0000000000000000000000000000000000000000..21eac2a3c4128a6175a70641c0d748c00ddfc2d2 --- /dev/null +++ b/crates/contacts_panel/src/notifications.rs @@ -0,0 +1,112 @@ +use crate::render_icon_button; +use client::User; +use gpui::{ + elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, + platform::CursorStyle, + Action, Element, ElementBox, RenderContext, View, +}; +use settings::Settings; +use std::sync::Arc; + +enum Dismiss {} +enum Button {} + +pub fn render_user_notification( + user: Arc, + message: &str, + dismiss_action: A, + buttons: Vec<(&'static str, Box)>, + cx: &mut RenderContext, +) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + + Flex::column() + .with_child( + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .boxed() + })) + .with_child( + Text::new( + format!("{} {}", user.github_login, message), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .top() + .left() + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |state, _| { + render_icon_button( + theme.dismiss_button.style_for(state, false), + "icons/decline.svg", + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) + .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .flex_float() + .boxed(), + ) + .named("contact notification header"), + ) + .with_child( + Label::new( + "They won't know if you decline.".to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed(), + ) + .with_children(if buttons.is_empty() { + None + } else { + Some( + Flex::row() + .with_children(buttons.into_iter().enumerate().map( + |(ix, (message, action))| { + MouseEventHandler::new::(ix, cx, |state, _| { + let button = theme.button.style_for(state, false); + Label::new(message.to_string(), button.text.clone()) + .contained() + .with_style(button.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone())) + .boxed() + }, + )) + .aligned() + .right() + .boxed(), + ) + }) + .contained() + .boxed() +} diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index a7d715fadf2fb68b957702d052c54c25aec35980..7b16ad4869c890c6a87ef9cc4ad343bc36f1766e 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -386,13 +386,17 @@ impl<'a> EventContext<'a> { } } - pub fn dispatch_action(&mut self, action: A) { + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), - action: Box::new(action), + action, }); } + pub fn dispatch_action(&mut self, action: A) { + self.dispatch_any_action(Box::new(action)); + } + pub fn notify(&mut self) { self.notify_count += 1; if let Some(view_id) = self.view_stack.last() { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d23122f45b337d5bc7152fef6cbec9abd3e4623e..cc73aadb294e3100393aa78eafb13a4841932404 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -133,6 +133,7 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), RemoteIdChanged(Option), CollaboratorLeft(PeerId), + ContactRequestedJoin(Arc), } #[derive(Serialize)] @@ -248,6 +249,7 @@ impl ProjectEntryId { impl Project { pub fn init(client: &Arc) { + client.add_model_message_handler(Self::handle_request_join_project); client.add_model_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_saved); @@ -256,7 +258,7 @@ impl Project { client.add_model_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); - client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_unregister_project); client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); @@ -362,6 +364,11 @@ impl Project { }) .await?; + let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? { + proto::join_project_response::Variant::Accept(response) => response, + proto::join_project_response::Variant::Decline(_) => Err(anyhow!("rejected"))?, + }; + let replica_id = response.replica_id as ReplicaId; let mut worktrees = Vec::new(); @@ -400,7 +407,7 @@ impl Project { // Even if we're initially connected, any future change of the status means we momentarily disconnected. if !is_connected || status.next().await.is_some() { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.project_unshared(cx)) + this.update(&mut cx, |this, cx| this.removed_from_project(cx)) } } Ok(()) @@ -814,60 +821,59 @@ impl Project { self.is_local() && self.visible_worktrees(cx).next().is_some() } - pub fn share(&self, cx: &mut ModelContext) -> Task> { - let rpc = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut this.client_state - { - *is_shared = true; - - for open_buffer in this.opened_buffers.values_mut() { - match open_buffer { - OpenBuffer::Strong(_) => {} - OpenBuffer::Weak(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - *open_buffer = OpenBuffer::Strong(buffer); - } - } - OpenBuffer::Loading(_) => unreachable!(), - } - } + pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + let project_id; + if let ProjectClientState::Local { + remote_id_rx, + is_shared, + .. + } = &mut self.client_state + { + if *is_shared { + return Task::ready(Ok(())); + } + *is_shared = true; + if let Some(id) = *remote_id_rx.borrow() { + project_id = id; + } else { + return Task::ready(Err(anyhow!("project hasn't been registered"))); + } + } else { + return Task::ready(Err(anyhow!("can't share a remote project"))); + }; - for worktree_handle in this.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(_) => {} - WorktreeHandle::Weak(worktree) => { - if let Some(worktree) = worktree.upgrade(cx) { - *worktree_handle = WorktreeHandle::Strong(worktree); - } - } - } + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); } - - remote_id_rx - .borrow() - .ok_or_else(|| anyhow!("no project id")) - } else { - Err(anyhow!("can't share a remote project")) } - })?; - - rpc.request(proto::ShareProject { project_id }).await?; + OpenBuffer::Loading(_) => unreachable!(), + } + } - let mut tasks = Vec::new(); - this.update(&mut cx, |this, cx| { - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - tasks.push(worktree.share(project_id, cx)); - }); + for worktree_handle in self.worktrees.iter_mut() { + match worktree_handle { + WorktreeHandle::Strong(_) => {} + WorktreeHandle::Weak(worktree) => { + if let Some(worktree) = worktree.upgrade(cx) { + *worktree_handle = WorktreeHandle::Strong(worktree); + } } + } + } + + let mut tasks = Vec::new(); + for worktree in self.worktrees(cx).collect::>() { + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + tasks.push(worktree.share(project_id, cx)); }); + } + + cx.spawn(|this, mut cx| async move { for task in tasks { task.await?; } @@ -877,14 +883,7 @@ impl Project { } pub fn unshare(&mut self, cx: &mut ModelContext) { - let rpc = self.client.clone(); - - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut self.client_state - { + if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { if !*is_shared { return; } @@ -913,17 +912,35 @@ impl Project { } } - if let Some(project_id) = *remote_id_rx.borrow() { - rpc.send(proto::UnshareProject { project_id }).log_err(); - } - cx.notify(); } else { log::error!("attempted to unshare a remote project"); } } - fn project_unshared(&mut self, cx: &mut ModelContext) { + pub fn respond_to_join_request( + &mut self, + requester_id: u64, + allow: bool, + cx: &mut ModelContext, + ) { + if let Some(project_id) = self.remote_id() { + let share = self.share(cx); + let client = self.client.clone(); + cx.foreground() + .spawn(async move { + share.await?; + client.send(proto::RespondToJoinProjectRequest { + requester_id, + project_id, + allow, + }) + }) + .detach_and_log_err(cx); + } + } + + fn removed_from_project(&mut self, cx: &mut ModelContext) { if let ProjectClientState::Remote { sharing_has_stopped, .. @@ -3745,13 +3762,28 @@ impl Project { // RPC message handlers - async fn handle_unshare_project( + async fn handle_request_join_project( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user_id = message.payload.requester_id; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user = user_store + .update(&mut cx, |store, cx| store.fetch_user(user_id, cx)) + .await?; + this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); + Ok(()) + } + + async fn handle_unregister_project( this: ModelHandle, - _: TypedEnvelope, + _: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.project_unshared(cx)); + this.update(&mut cx, |this, cx| this.removed_from_project(cx)); Ok(()) } @@ -3796,6 +3828,9 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + if this.collaborators.is_empty() { + this.unshare(cx); + } cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 12ff05c7575ec53b1102ab253d4a9db08790b69a..f81fabadaeaaee7b87b2559580b225b347abca86 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -14,8 +14,8 @@ message Envelope { RegisterProject register_project = 8; RegisterProjectResponse register_project_response = 9; UnregisterProject unregister_project = 10; - ShareProject share_project = 11; - UnshareProject unshare_project = 12; + RequestJoinProject request_join_project = 11; + RespondToJoinProjectRequest respond_to_join_project_request = 12; JoinProject join_project = 13; JoinProjectResponse join_project_response = 14; LeaveProject leave_project = 15; @@ -124,12 +124,15 @@ message UnregisterProject { uint64 project_id = 1; } -message ShareProject { - uint64 project_id = 1; +message RequestJoinProject { + uint64 requester_id = 1; + uint64 project_id = 2; } -message UnshareProject { - uint64 project_id = 1; +message RespondToJoinProjectRequest { + uint64 requester_id = 1; + uint64 project_id = 2; + bool allow = 3; } message JoinProject { @@ -137,10 +140,19 @@ message JoinProject { } message JoinProjectResponse { - uint32 replica_id = 1; - repeated Worktree worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; + oneof variant { + Accept accept = 1; + Decline decline = 2; + } + + message Accept { + uint32 replica_id = 1; + repeated Worktree worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; + } + + message Decline {} } message LeaveProject { @@ -882,7 +894,6 @@ message Contact { message ProjectMetadata { uint64 id = 1; - bool is_shared = 2; repeated string worktree_root_names = 3; repeated uint64 guests = 4; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0b7ba21c4a22419d1f13ba909bbaeab7e24c512b..7d1213625572cd1e461874fd36c2efeb18727276 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -135,19 +135,19 @@ messages!( (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), (RequestContact, Foreground), + (RequestJoinProject, Foreground), (RespondToContactRequest, Foreground), + (RespondToJoinProjectRequest, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), - (ShareProject, Foreground), (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), - (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), @@ -195,7 +195,6 @@ request_messages!( (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), - (ShareProject, Ack), (Test, Test), (UpdateBuffer, Ack), (UpdateWorktree, Ack), @@ -228,12 +227,13 @@ entity_messages!( PrepareRename, ReloadBuffers, RemoveProjectCollaborator, + RequestJoinProject, SaveBuffer, SearchProject, StartLanguageServer, Unfollow, + UnregisterProject, UnregisterWorktree, - UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c6ad68ace6f78fcbf3288161eef2fb36284f8f7f..8ca7ce9ae2fe9832ce6f5e7a6b4c9b24af5b45a1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -251,8 +251,7 @@ pub struct ContactsPanel { pub add_contact_button: IconButton, pub header_row: Interactive, pub contact_row: Interactive, - pub shared_project_row: Interactive, - pub unshared_project_row: Interactive, + pub project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fada690bb52a3fc80fddf0c32b83dee9cd433cea..26b355ae0a290adcbcd916e08a4f2adda478cec5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -72,7 +72,6 @@ type FollowableItemBuilders = HashMap< actions!( workspace, [ - ToggleShare, Unfollow, Save, ActivatePreviousPane, @@ -121,7 +120,6 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { join_project(action.project_id, &action.app_state, cx).detach(); }); - cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); cx.add_action( @@ -692,6 +690,7 @@ impl WorkspaceParams { pub enum Event { PaneAdded(ViewHandle), + ContactRequestedJoin(u64), } pub struct Workspace { @@ -1366,18 +1365,6 @@ impl Workspace { &self.active_pane } - fn toggle_share(&mut self, _: &ToggleShare, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - if project.is_local() { - if project.is_shared() { - project.unshare(cx); - } else if project.can_share(cx) { - project.share(cx).detach(); - } - } - }); - } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = @@ -1580,7 +1567,6 @@ impl Workspace { cx, )) .with_children(self.render_connection_status(cx)) - .with_children(self.render_share_icon(theme, cx)) .boxed(), ) .right() @@ -1701,39 +1687,6 @@ impl Workspace { } } - fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { - if self.project().read(cx).is_local() - && self.client.user_id().is_some() - && self.project().read(cx).can_share(cx) - { - Some( - MouseEventHandler::new::(0, cx, |state, cx| { - let style = &theme - .workspace - .titlebar - .share_icon - .style_for(state, self.project().read(cx).is_shared()); - Svg::new("icons/share.svg") - .with_color(style.color) - .constrained() - .with_height(14.) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(24.) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(ToggleShare)) - .boxed(), - ) - } else { - None - } - } - fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { if self.project.read(cx).is_read_only() { let theme = &cx.global::().theme; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d4938501b81c04ee329765056857a9266cecedd8..2a1fd0a7f2335eb6f98d7e6ece739c0217e64760 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -123,17 +123,18 @@ pub fn build_workspace( cx.subscribe(&cx.handle(), { let project = project.clone(); move |_, _, event, cx| { - let workspace::Event::PaneAdded(pane) = event; - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); - toolbar.add_item(buffer_search_bar, cx); - let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, cx); - }) - }); + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + } } }) .detach(); diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index a2caafadec633aeda5ecd7fc3334cecb784a2b6a..421afd1967e97395e9df11ece337df51759c20ae 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -122,7 +122,7 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - sharedProjectRow: { + projectRow: { ...projectRow, background: backgroundColor(theme, 300), name: { @@ -136,19 +136,5 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 300, "active"), } }, - unsharedProjectRow: { - ...projectRow, - background: backgroundColor(theme, 300), - name: { - ...projectRow.name, - ...text(theme, "mono", "secondary", { size: "sm" }), - }, - hover: { - background: backgroundColor(theme, 300, "hovered"), - }, - active: { - background: backgroundColor(theme, 300, "active"), - } - } } } From dd684d26a16941de0579a1913a2e2edb0d2b2b25 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 13 May 2022 17:17:20 +0200 Subject: [PATCH 02/26] Make `Project::share` and `Project::unshare` private This is still in-progress because randomized tests are failing. Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 710 +++++++++++----------------------- crates/project/src/project.rs | 77 ++-- 2 files changed, 277 insertions(+), 510 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 19d8ebc779835945258aeb723cb955a67b6b4866..869fef997fa66293f2c223730f752a6e4874eb8f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -336,6 +336,7 @@ impl Server { } async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { + println!("Signing out {:?}", connection_id); self.peer.disconnect(connection_id); let removed_connection = self.store_mut().await.remove_connection(connection_id)?; @@ -554,8 +555,13 @@ impl Server { }) }) .collect::>(); + + // Add all guests other than the requesting user's own connections as collaborators for (peer_conn_id, (peer_replica_id, peer_user_id)) in &project.guests { - if *peer_conn_id != request.sender_id { + if receipts_with_replica_ids + .iter() + .all(|(receipt, _)| receipt.sender_id != *peer_conn_id) + { collaborators.push(proto::Collaborator { peer_id: peer_conn_id.0, replica_id: *peer_replica_id as u32, @@ -563,6 +569,7 @@ impl Server { }); } } + for conn_id in project.connection_ids() { for (receipt, replica_id) in &receipts_with_replica_ids { if conn_id != receipt.sender_id { @@ -822,6 +829,7 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { + println!("{:?}: update buffer", request.sender_id); let receiver_ids = self .store() .await @@ -1640,7 +1648,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1673,20 +1681,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let client_b_peer_id = client_b.peer_id; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let replica_id_b = project_b.read_with(cx_b, |project, _| { assert_eq!( @@ -1703,7 +1701,7 @@ mod tests { project_a .condition(&cx_a, |tree, _| { tree.collaborators() - .get(&client_b.peer_id) + .get(&client_b_peer_id) .map_or(false, |collaborator| { collaborator.replica_id == replica_id_b && collaborator.user.github_login == "user_b" @@ -1749,14 +1747,21 @@ mod tests { // .await; // Dropping the client B's project removes client B from client A's collaborators. - cx_b.update(move |_| drop(project_b)); + cx_b.update(move |_| { + drop(client_b.project.take()); + drop(project_b); + }); project_a .condition(&cx_a, |project, _| project.collaborators().is_empty()) .await; } #[gpui::test(iterations = 10)] - async fn test_unshare_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_unshare_project( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); @@ -1764,7 +1769,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1796,54 +1801,27 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - // Unshare the project as client A - project_a.update(cx_a, |project, cx| project.unshare(cx)); - project_b - .condition(cx_b, |project, _| project.is_read_only()) - .await; - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + // When client B leaves the project, it gets automatically unshared. cx_b.update(|_| { + drop(client_b.project.take()); drop(project_b); }); + deterministic.run_until_parked(); + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - // Share the project again and ensure guests can still join. - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); + // When client B joins again, the project gets re-shared. + let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - - let project_b2 = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); project_b2 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await @@ -1859,7 +1837,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1891,22 +1869,11 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await @@ -1927,26 +1894,9 @@ mod tests { drop(project_b); }); - // Await reconnection - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; - - // Share the project again and ensure guests can still join. - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); + // Ensure guests can still join. + let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - - let project_b2 = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); project_b2 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await @@ -1966,8 +1916,8 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + let mut client_c = server.create_client(cx_c, "user_c").await; server .make_contacts(vec![ (&client_a, cx_a), @@ -2003,31 +1953,11 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that worktree as clients B and C. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let project_c = Project::remote( - project_id, - client_c.clone(), - client_c.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_c.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap()); @@ -2166,13 +2096,7 @@ mod tests { .await; let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); @@ -2319,7 +2243,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2351,21 +2275,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -2403,7 +2316,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2435,21 +2348,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let _worktree_b = project_b.update(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); // Open a buffer as client B @@ -2487,7 +2389,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2518,21 +2420,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client A let buffer_a = project_a @@ -2568,7 +2459,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2599,21 +2490,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // See that a guest has joined as client A. project_a @@ -2624,7 +2504,10 @@ mod tests { let buffer_b = cx_b .background() .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); - cx_b.update(|_| drop(project_b)); + cx_b.update(|_| { + drop(client_b.project.take()); + drop(project_b); + }); drop(buffer_b); // See that the guest has left. @@ -2642,7 +2525,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2674,25 +2557,9 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a - .update(cx_a, |project, _| project.next_remote_id()) - .await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); // Join that project as client B - let _project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A sees that a guest has joined. project_a @@ -2706,16 +2573,7 @@ mod tests { .await; // Rejoin the project as client B - let _project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A sees that a guest has re-joined. project_a @@ -2733,10 +2591,12 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_collaborating_with_diagnostics( + deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { - cx_a.foreground().forbid_parking(); + deterministic.forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); @@ -2755,9 +2615,14 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + let mut client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; // Share a project as client A @@ -2789,10 +2654,9 @@ mod tests { .await; let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Cause the language server to start. - let _ = cx_a + let _buffer = cx_a .background() .spawn(project_a.update(cx_a, |project, cx| { project.open_buffer( @@ -2806,6 +2670,9 @@ mod tests { .await .unwrap(); + // Join the worktree as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + // Simulate a language server reporting errors for a file. let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server @@ -2825,31 +2692,15 @@ mod tests { ); // Wait for server to see the diagnostics update. - server - .condition(|store| { - let worktree = store - .project(project_id) - .unwrap() - .worktrees - .get(&worktree_id.to_proto()) - .unwrap(); - - !worktree.diagnostic_summaries.is_empty() - }) - .await; - - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + deterministic.run_until_parked(); + { + let store = server.store.read().await; + let project = store.project(project_id).unwrap(); + let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap(); + assert!(!worktree.diagnostic_summaries.is_empty()); + } + // Ensure client B observes the new diagnostics. project_b.read_with(cx_b, |project, cx| { assert_eq!( project.diagnostic_summaries(cx).collect::>(), @@ -2867,6 +2718,25 @@ mod tests { ) }); + // Join project as client C and observe the diagnostics. + let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + project_c.read_with(cx_c, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + &[( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + ..Default::default() + }, + )] + ) + }); + // Simulate a language server reporting more errors for a file. fake_language_server.notify::( lsp::PublishDiagnosticsParams { @@ -2892,23 +2762,40 @@ mod tests { }, ); - // Client b gets the updated summaries - project_b - .condition(&cx_b, |project, cx| { - project.diagnostic_summaries(cx).collect::>() - == &[( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - DiagnosticSummary { - error_count: 1, - warning_count: 1, - ..Default::default() - }, - )] - }) - .await; + // Clients B and C get the updated summaries + deterministic.run_until_parked(); + project_b.read_with(cx_b, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + [( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + ); + }); + project_c.read_with(cx_c, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + [( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + ); + }); // Open the file with the errors on client B. They should be present. let buffer_b = cx_b @@ -2957,16 +2844,16 @@ mod tests { diagnostics: vec![], }, ); - project_a - .condition(cx_a, |project, cx| { - project.diagnostic_summaries(cx).collect::>() == &[] - }) - .await; - project_b - .condition(cx_b, |project, cx| { - project.diagnostic_summaries(cx).collect::>() == &[] - }) - .await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); + project_b.read_with(cx_b, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); + project_c.read_with(cx_c, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); } #[gpui::test(iterations = 10)] @@ -3002,7 +2889,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3034,21 +2921,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b @@ -3185,7 +3061,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3216,25 +3092,14 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b = cx_b .background() @@ -3316,7 +3181,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3347,21 +3212,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b = cx_b .background() @@ -3430,7 +3284,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3454,21 +3308,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3577,7 +3420,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3601,21 +3444,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3710,7 +3542,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3725,7 +3557,6 @@ mod tests { cx, ) }); - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let (worktree_1, _) = project_a .update(cx_a, |p, cx| { @@ -3746,20 +3577,8 @@ mod tests { .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let results = project_b .update(cx_b, |project, cx| { project.search(SearchQuery::text("world", false, false), cx) @@ -3821,7 +3640,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3845,21 +3664,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3968,7 +3776,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3992,21 +3800,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Cause the language server to start. let _buffer = cx_b @@ -4100,7 +3897,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4125,21 +3922,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b1 = cx_b .background() @@ -4200,7 +3986,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4232,26 +4018,15 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let mut params = cx_b.update(WorkspaceParams::test); params.languages = lang_registry.clone(); + params.project = project_b.clone(); params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); - params.project = project_b; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b @@ -4450,7 +4225,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4482,26 +4257,15 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let mut params = cx_b.update(WorkspaceParams::test); params.languages = lang_registry.clone(); + params.project = project_b.clone(); params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); - params.project = project_b; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b @@ -5108,29 +4872,7 @@ mod tests { }); } - let project_id = project_a - .update(cx_a, |project, _| project.next_remote_id()) - .await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); - - deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", vec![])]), - ("user_b", true, vec![]), - ("user_c", true, vec![]) - ] - ) - }); - } - - let _project_b = client_b.build_remote_project(project_id, cx_b).await; + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { @@ -5469,20 +5211,9 @@ mod tests { ) .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; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -5533,6 +5264,7 @@ mod tests { }) .await .unwrap(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { workspace .active_item(cx) @@ -5683,20 +5415,9 @@ mod tests { ) .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; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -5830,20 +5551,9 @@ mod tests { ) .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; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -6052,10 +5762,6 @@ mod tests { collab_worktree .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - host_project - .update(&mut host_cx, |project, cx| project.share(cx)) - .await - .unwrap(); // Set up fake language servers. let mut language = Language::new( @@ -6216,7 +5922,7 @@ mod tests { let (host, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { - log::error!("host error - {}", host_err); + log::error!("host error - {:?}", host_err); } host.project .as_ref() @@ -6224,7 +5930,7 @@ mod tests { .read_with(&host_cx, |project, _| assert!(!project.is_shared())); for (guest, mut guest_cx, guest_err) in clients { if let Some(guest_err) = guest_err { - log::error!("{} error - {}", guest.username, guest_err); + log::error!("{} error - {:?}", guest.username, guest_err); } // TODO // let contacts = server @@ -6299,7 +6005,7 @@ mod tests { cx.foreground().advance_clock(RECEIVE_TIMEOUT); let (guest, mut guest_cx, guest_err) = guest.await; if let Some(guest_err) = guest_err { - log::error!("{} error - {}", guest.username, guest_err); + log::error!("{} error - {:?}", guest.username, guest_err); } guest .project @@ -6353,7 +6059,7 @@ mod tests { let (host_client, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { - panic!("host error - {}", host_err); + panic!("host error - {:?}", host_err); } let host_project = host_client.project.as_ref().unwrap(); let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| { @@ -6374,7 +6080,7 @@ mod tests { for (guest_client, mut guest_cx, guest_err) in clients.into_iter() { if let Some(guest_err) = guest_err { - panic!("{} error - {}", guest_client.username, guest_err); + panic!("{} error - {:?}", guest_client.username, guest_err); } let worktree_snapshots = guest_client @@ -6757,19 +6463,37 @@ mod tests { async fn build_remote_project( &mut self, - project_id: u64, - cx: &mut TestAppContext, + host_project: &ModelHandle, + host_cx: &mut TestAppContext, + guest_cx: &mut TestAppContext, ) -> ModelHandle { - 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(); + let host_project_id = host_project + .read_with(host_cx, |project, _| project.next_remote_id()) + .await; + let guest_user_id = self.user_id().unwrap(); + let languages = + host_project.read_with(host_cx, |project, _| project.languages().clone()); + let project_b = guest_cx.spawn(|mut cx| { + let user_store = self.user_store.clone(); + let guest_client = self.client.clone(); + async move { + Project::remote( + host_project_id, + guest_client, + user_store.clone(), + languages, + FakeFs::new(cx.background()), + &mut cx, + ) + .await + .unwrap() + } + }); + host_cx.foreground().run_until_parked(); + host_project.update(host_cx, |project, cx| { + project.respond_to_join_request(guest_user_id, true, cx) + }); + let project = project_b.await; self.project = Some(project.clone()); project } @@ -6815,11 +6539,23 @@ mod tests { ) -> anyhow::Result<()> { let fs = project.read_with(cx, |project, _| project.fs().clone()); + cx.update(|cx| { + cx.subscribe(&project, move |project, event, cx| { + if let project::Event::ContactRequestedJoin(user) = event { + log::info!("Host: accepting join request from {}", user.github_login); + project.update(cx, |project, cx| { + project.respond_to_join_request(user.id, true, cx) + }); + } + }) + .detach(); + }); + while op_start_signal.next().await.is_some() { let distribution = rng.lock().gen_range::(0..100); let files = fs.as_fake().files().await; match distribution { - 0..=20 if !files.is_empty() => { + 0..=19 if !files.is_empty() => { let path = files.choose(&mut *rng.lock()).unwrap(); let mut path = path.as_path(); while let Some(parent_path) = path.parent() { @@ -6839,7 +6575,7 @@ mod tests { find_or_create_worktree.await?; } } - 10..=80 if !files.is_empty() => { + 20..=79 if !files.is_empty() => { let buffer = if client.buffers.is_empty() || rng.lock().gen() { let file = files.choose(&mut *rng.lock()).unwrap(); let (worktree, path) = project diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cc73aadb294e3100393aa78eafb13a4841932404..bf86b304b152337435cdf74928a9e3a0829a6b61 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -817,11 +817,12 @@ impl Project { } } - pub fn can_share(&self, cx: &AppContext) -> bool { - self.is_local() && self.visible_worktrees(cx).next().is_some() - } - - pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + fn share( + &mut self, + requester_user_id: UserId, + requester_peer_id: PeerId, + cx: &mut ModelContext, + ) -> Task> { let project_id; if let ProjectClientState::Local { remote_id_rx, @@ -882,7 +883,7 @@ impl Project { }) } - pub fn unshare(&mut self, cx: &mut ModelContext) { + fn unshare(&mut self, cx: &mut ModelContext) { if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { if !*is_shared { return; @@ -929,7 +930,10 @@ impl Project { let client = self.client.clone(); cx.foreground() .spawn(async move { + println!("SHARING because {} wanted to join!!!", requester_id); share.await?; + println!("DONE SHARING!!!!!"); + client.send(proto::RespondToJoinProjectRequest { requester_id, project_id, @@ -3521,25 +3525,36 @@ impl Project { }); let worktree = worktree?; - let (remote_project_id, is_shared) = - project.update(&mut cx, |project, cx| { - project.add_worktree(&worktree, cx); - (project.remote_id(), project.is_shared()) - }); + let remote_project_id = project.update(&mut cx, |project, cx| { + project.add_worktree(&worktree, cx); + project.remote_id() + }); if let Some(project_id) = remote_project_id { - if is_shared { - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().share(project_id, cx) - }) - .await?; - } else { - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().register(project_id, cx) - }) - .await?; + // Because sharing is async, we may have *unshared* the project by the time it completes, + // in which case we need to register the worktree instead. + loop { + if project.read_with(&cx, |project, _| project.is_shared()) { + if worktree + .update(&mut cx, |worktree, cx| { + worktree.as_local_mut().unwrap().share(project_id, cx) + }) + .await + .is_ok() + { + break; + } + } else { + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .register(project_id, cx) + }) + .await?; + break; + } } } @@ -3828,7 +3843,17 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + println!( + "{} observed {:?} leaving", + this.user_store + .read(cx) + .current_user() + .unwrap() + .github_login, + peer_id + ); if this.collaborators.is_empty() { + println!("UNSHARING!!!!"); this.unshare(cx); } cx.emit(Event::CollaboratorLeft(peer_id)); @@ -4087,6 +4112,7 @@ impl Project { .into_iter() .map(|op| language::proto::deserialize_operation(op)) .collect::, _>>()?; + let is_remote = this.is_remote(); match this.opened_buffers.entry(buffer_id) { hash_map::Entry::Occupied(mut e) => match e.get_mut() { OpenBuffer::Strong(buffer) => { @@ -4096,6 +4122,11 @@ impl Project { OpenBuffer::Weak(_) => {} }, hash_map::Entry::Vacant(e) => { + assert!( + is_remote, + "received buffer update from {:?}", + envelope.original_sender_id + ); e.insert(OpenBuffer::Loading(ops)); } } From 5789aeea24822e7a77b2041989d2d62d5f39cef4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 13 May 2022 17:39:13 +0200 Subject: [PATCH 03/26] Fix randomized test failure caused by unsharing while guest was joining Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 58 +++++++++++++++++++++------------- crates/collab/src/rpc/store.rs | 8 ++--- crates/project/src/project.rs | 39 +++++++++-------------- crates/rpc/proto/zed.proto | 33 +++++++++++-------- crates/rpc/src/proto.rs | 2 ++ 5 files changed, 75 insertions(+), 65 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 869fef997fa66293f2c223730f752a6e4874eb8f..c5141f16b3ae233dc29827acbba7492efaf46c91 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -336,31 +336,46 @@ impl Server { } async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { - println!("Signing out {:?}", connection_id); self.peer.disconnect(connection_id); - let removed_connection = self.store_mut().await.remove_connection(connection_id)?; - for (project_id, project) in removed_connection.hosted_projects { - broadcast(connection_id, project.guests.keys().copied(), |conn_id| { - self.peer - .send(conn_id, proto::UnregisterProject { project_id }) - }); - } + let removed_user_id = { + let mut store = self.store_mut().await; + let removed_connection = store.remove_connection(connection_id)?; - for (project_id, peer_ids) in removed_connection.guest_project_ids { - broadcast(connection_id, peer_ids, |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id, - peer_id: connection_id.0, - }, - ) - }); - } + for (project_id, project) in removed_connection.hosted_projects { + broadcast(connection_id, project.guests.keys().copied(), |conn_id| { + self.peer + .send(conn_id, proto::UnregisterProject { project_id }) + }); + } - self.update_user_contacts(removed_connection.user_id) - .await?; + for project_id in removed_connection.guest_project_ids { + if let Some(project) = store.project(project_id).trace_err() { + if project.guests.is_empty() { + self.peer + .send( + project.host_connection_id, + proto::ProjectUnshared { project_id }, + ) + .trace_err(); + } else { + broadcast(connection_id, project.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: connection_id.0, + }, + ) + }); + } + } + } + + removed_connection.user_id + }; + + self.update_user_contacts(removed_user_id).await?; Ok(()) } @@ -829,7 +844,6 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - println!("{:?}: update buffer", request.sender_id); let receiver_ids = self .store() .await diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 765162936c61d9f4f97cc5564b80baf200f1429b..4d32ead37df7d4d109b59c06d392a966a44263be 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -51,7 +51,7 @@ pub type ReplicaId = u16; pub struct RemovedConnectionState { pub user_id: UserId, pub hosted_projects: HashMap, - pub guest_project_ids: HashMap>, + pub guest_project_ids: HashSet, pub contact_ids: HashSet, } @@ -134,10 +134,8 @@ impl Store { for project_id in connection.projects.clone() { if let Ok(project) = self.unregister_project(project_id, connection_id) { result.hosted_projects.insert(project_id, project); - } else if let Ok(project) = self.leave_project(connection_id, project_id) { - result - .guest_project_ids - .insert(project_id, project.connection_ids); + } else if self.leave_project(connection_id, project_id).is_ok() { + result.guest_project_ids.insert(project_id); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bf86b304b152337435cdf74928a9e3a0829a6b61..28e3a16fd5b38d058d88a242cadb6564e049bf28 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -259,6 +259,7 @@ impl Project { client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); client.add_model_message_handler(Self::handle_unregister_project); + client.add_model_message_handler(Self::handle_project_unshared); client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); @@ -557,7 +558,7 @@ impl Project { } fn unregister(&mut self, cx: &mut ModelContext) { - self.unshare(cx); + self.unshared(cx); for worktree in &self.worktrees { if let Some(worktree) = worktree.upgrade(cx) { worktree.update(cx, |worktree, _| { @@ -817,12 +818,7 @@ impl Project { } } - fn share( - &mut self, - requester_user_id: UserId, - requester_peer_id: PeerId, - cx: &mut ModelContext, - ) -> Task> { + fn share(&mut self, cx: &mut ModelContext) -> Task> { let project_id; if let ProjectClientState::Local { remote_id_rx, @@ -883,7 +879,7 @@ impl Project { }) } - fn unshare(&mut self, cx: &mut ModelContext) { + fn unshared(&mut self, cx: &mut ModelContext) { if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { if !*is_shared { return; @@ -930,10 +926,7 @@ impl Project { let client = self.client.clone(); cx.foreground() .spawn(async move { - println!("SHARING because {} wanted to join!!!", requester_id); share.await?; - println!("DONE SHARING!!!!!"); - client.send(proto::RespondToJoinProjectRequest { requester_id, project_id, @@ -3802,6 +3795,16 @@ impl Project { Ok(()) } + async fn handle_project_unshared( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| this.unshared(cx)); + Ok(()) + } + async fn handle_add_collaborator( this: ModelHandle, mut envelope: TypedEnvelope, @@ -3843,19 +3846,7 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } - println!( - "{} observed {:?} leaving", - this.user_store - .read(cx) - .current_user() - .unwrap() - .github_login, - peer_id - ); - if this.collaborators.is_empty() { - println!("UNSHARING!!!!"); - this.unshare(cx); - } + cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f81fabadaeaaee7b87b2559580b225b347abca86..83ee63765732cf7dc4e48636ff1780808abdd0f8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -21,20 +21,21 @@ message Envelope { LeaveProject leave_project = 15; AddProjectCollaborator add_project_collaborator = 16; RemoveProjectCollaborator remove_project_collaborator = 17; - - GetDefinition get_definition = 18; - GetDefinitionResponse get_definition_response = 19; - GetReferences get_references = 20; - GetReferencesResponse get_references_response = 21; - GetDocumentHighlights get_document_highlights = 22; - GetDocumentHighlightsResponse get_document_highlights_response = 23; - GetProjectSymbols get_project_symbols = 24; - GetProjectSymbolsResponse get_project_symbols_response = 25; - OpenBufferForSymbol open_buffer_for_symbol = 26; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 27; - - RegisterWorktree register_worktree = 28; - UnregisterWorktree unregister_worktree = 29; + ProjectUnshared project_unshared = 18; + + GetDefinition get_definition = 19; + GetDefinitionResponse get_definition_response = 20; + GetReferences get_references = 21; + GetReferencesResponse get_references_response = 22; + GetDocumentHighlights get_document_highlights = 23; + GetDocumentHighlightsResponse get_document_highlights_response = 24; + GetProjectSymbols get_project_symbols = 25; + GetProjectSymbolsResponse get_project_symbols_response = 26; + OpenBufferForSymbol open_buffer_for_symbol = 27; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 28; + + RegisterWorktree register_worktree = 29; + UnregisterWorktree unregister_worktree = 30; UpdateWorktree update_worktree = 31; CreateProjectEntry create_project_entry = 32; @@ -213,6 +214,10 @@ message RemoveProjectCollaborator { uint32 peer_id = 2; } +message ProjectUnshared { + uint64 project_id = 1; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 7d1213625572cd1e461874fd36c2efeb18727276..1a87cb6a0c33f1e920e17ff8fc4bb5bc913199d4 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -128,6 +128,7 @@ messages!( (ProjectEntryResponse, Foreground), (RegisterProjectResponse, Foreground), (Ping, Foreground), + (ProjectUnshared, Foreground), (RegisterProject, Foreground), (RegisterWorktree, Foreground), (ReloadBuffers, Foreground), @@ -225,6 +226,7 @@ entity_messages!( OpenBufferForSymbol, PerformRename, PrepareRename, + ProjectUnshared, ReloadBuffers, RemoveProjectCollaborator, RequestJoinProject, From 1996b01a7464cc9d4e12d73b6eae1ab5a54fb309 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 May 2022 14:57:55 -0700 Subject: [PATCH 04/26] Tell host to unshare project when last guest leaves --- crates/collab/src/rpc.rs | 7 +++++++ crates/collab/src/rpc/store.rs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c5141f16b3ae233dc29827acbba7492efaf46c91..fe8335c5fec1b7792dbc6d74fb3153b90c0e00e6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -634,6 +634,7 @@ impl Server { { let mut state = self.store_mut().await; project = state.leave_project(sender_id, project_id)?; + let unshare = project.connection_ids.len() <= 1; broadcast(sender_id, project.connection_ids, |conn_id| { self.peer.send( conn_id, @@ -643,6 +644,12 @@ impl Server { }, ) }); + if unshare { + self.peer.send( + project.host_connection_id, + proto::ProjectUnshared { project_id }, + )?; + } } self.update_user_contacts(project.host_user_id).await?; Ok(()) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 4d32ead37df7d4d109b59c06d392a966a44263be..81e4c9633bb6ecb896e7f0c63482f15c6e4c64e9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -58,6 +58,7 @@ pub struct RemovedConnectionState { pub struct LeftProject { pub connection_ids: Vec, pub host_user_id: UserId, + pub host_connection_id: ConnectionId, } #[derive(Copy, Clone)] @@ -504,6 +505,7 @@ impl Store { Ok(LeftProject { connection_ids: project.connection_ids(), + host_connection_id: project.host_connection_id, host_user_id: project.host_user_id, }) } From f2eee6692b45f9634dce2717a257e2f558bf85d7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 May 2022 15:04:48 -0700 Subject: [PATCH 05/26] Send RemoveProjectCollaborator to host in addition to ProjectUnshared --- crates/collab/src/rpc.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fe8335c5fec1b7792dbc6d74fb3153b90c0e00e6..4e32a885cfe94cf173bbfe9b09d59ee76e03ae73 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -351,6 +351,15 @@ impl Server { for project_id in removed_connection.guest_project_ids { if let Some(project) = store.project(project_id).trace_err() { + broadcast(connection_id, project.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: connection_id.0, + }, + ) + }); if project.guests.is_empty() { self.peer .send( @@ -358,16 +367,6 @@ impl Server { proto::ProjectUnshared { project_id }, ) .trace_err(); - } else { - broadcast(connection_id, project.connection_ids(), |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id, - peer_id: connection_id.0, - }, - ) - }); } } } From 842bfae3af208006982cdfbf1226438d65aa30a0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 May 2022 17:03:48 -0700 Subject: [PATCH 06/26] WIP - update worktree's scan_id when mutating it in the foreground --- crates/project/src/worktree.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 84fedbbde7facc527500c5137a8bdf2a8bf6bbe0..37a8c26b9bc5ceb287596b09a507d09b98c9d8a4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -838,6 +838,7 @@ impl LocalWorktree { snapshot.remove_path(&old_path); } let entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; if let Some(tx) = shared_snapshots_tx { tx.send(snapshot.clone()).await.ok(); } @@ -2171,8 +2172,7 @@ impl BackgroundScanner { let root_abs_path; let next_entry_id; { - let mut snapshot = self.snapshot.lock(); - snapshot.scan_id += 1; + let snapshot = self.snapshot.lock(); root_char_bag = snapshot.root_char_bag; root_abs_path = snapshot.abs_path.clone(); next_entry_id = snapshot.next_entry_id.clone(); @@ -2197,6 +2197,7 @@ impl BackgroundScanner { let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); { let mut snapshot = self.snapshot.lock(); + snapshot.scan_id += 1; for event in &events { if let Ok(path) = event.path.strip_prefix(&root_abs_path) { snapshot.remove_path(&path); From bf638afac51fc6ec76d4592d3005158d33ce0fb3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 09:48:24 +0200 Subject: [PATCH 07/26] Initialize `UserStore` before client connects in integration tests This fixes a failure in `test_contacts` where we were receiving the initial `proto::UpdateContacts` message before `UserStore` had a chance to register a message handler for it. --- crates/collab/src/rpc.rs | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4e32a885cfe94cf173bbfe9b09d59ee76e03ae73..7462222f436836f84944d0ddf449702336c36c29 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4868,7 +4868,9 @@ mod tests { ("user_a", true, vec![]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4887,7 +4889,9 @@ mod tests { ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4903,7 +4907,9 @@ mod tests { ("user_a", true, vec![("a", vec!["user_b"])]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4922,7 +4928,9 @@ mod tests { ("user_a", true, vec![("a", vec!["user_b"])]), ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4944,7 +4952,9 @@ mod tests { ("user_a", true, vec![]), ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4960,7 +4970,9 @@ mod tests { ("user_a", true, vec![]), ("user_b", true, vec![("b", vec![])]), ("user_c", false, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -4983,7 +4995,9 @@ mod tests { ("user_a", true, vec![]), ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -6279,19 +6293,18 @@ mod tests { }) }); - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - Channel::init(&client); Project::init(&client); cx.update(|cx| { workspace::init(&client, cx); }); - let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); + let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let client = TestClient { client, From 576656ccf27ae64fa0a44c0897bd581962776829 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 09:50:36 +0200 Subject: [PATCH 08/26] Delete commented-out code --- crates/collab/src/rpc.rs | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7462222f436836f84944d0ddf449702336c36c29..e268204e034edfbcd9cd04f469cdff8b302c297b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -419,21 +419,6 @@ impl Server { Ok(()) } - // async fn share_project( - // self: Arc, - // request: TypedEnvelope, - // response: Response, - // ) -> Result<()> { - // let user_id = { - // let mut state = self.store_mut().await; - // state.share_project(request.payload.project_id, request.sender_id)?; - // state.user_id_for_connection(request.sender_id)? - // }; - // self.update_user_contacts(user_id).await?; - // response.send(proto::Ack {})?; - // Ok(()) - // } - async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; @@ -1192,29 +1177,6 @@ impl Server { Ok(()) } - // #[instrument(skip(self, state, user_ids))] - // fn update_contacts_for_users<'a>( - // self: &Arc, - // state: &Store, - // user_ids: impl IntoIterator, - // ) { - // for user_id in user_ids { - // let contacts = state.contacts_for_user(*user_id); - // for connection_id in state.connection_ids_for_user(*user_id) { - // self.peer - // .send( - // connection_id, - // proto::UpdateContacts { - // contacts: contacts.clone(), - // pending_requests_from_user_ids: Default::default(), - // pending_requests_to_user_ids: Default::default(), - // }, - // ) - // .trace_err(); - // } - // } - // } - async fn join_channel( self: Arc, request: TypedEnvelope, From b144995f27a77abba9a8bca58f7639405d54f714 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 11:46:49 +0200 Subject: [PATCH 09/26] Grab share state after retrieving metadata when refreshing entry --- crates/project/src/worktree.rs | 53 +++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 37a8c26b9bc5ceb287596b09a507d09b98c9d8a4..abaf49c9f2a9cbfdf33310d07e2aac3f55623319 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -616,8 +616,10 @@ impl LocalWorktree { let text = fs.load(&abs_path).await?; // Eagerly populate the snapshot with an updated entry for the loaded file let entry = this - .update(&mut cx, |this, _| { - this.as_local().unwrap().refresh_entry(path, abs_path, None) + .update(&mut cx, |this, cx| { + this.as_local() + .unwrap() + .refresh_entry(path, abs_path, None, cx) }) .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); @@ -753,11 +755,12 @@ impl LocalWorktree { Some(cx.spawn(|this, mut cx| async move { rename.await?; let entry = this - .update(&mut cx, |this, _| { + .update(&mut cx, |this, cx| { this.as_local_mut().unwrap().refresh_entry( new_path.clone(), abs_new_path, Some(old_path), + cx, ) }) .await?; @@ -793,10 +796,10 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let entry = this - .update(&mut cx, |this, _| { + .update(&mut cx, |this, cx| { this.as_local_mut() .unwrap() - .refresh_entry(path, abs_path, None) + .refresh_entry(path, abs_path, None, cx) }) .await?; this.update(&mut cx, |this, cx| { @@ -813,18 +816,17 @@ impl LocalWorktree { path: Arc, abs_path: PathBuf, old_path: Option>, - ) -> impl Future> { + cx: &mut ModelContext, + ) -> Task> { + let fs = self.fs.clone(); let root_char_bag; let next_entry_id; - let fs = self.fs.clone(); - let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone()); - let snapshot = self.background_snapshot.clone(); { - let snapshot = snapshot.lock(); + let snapshot = self.background_snapshot.lock(); root_char_bag = snapshot.root_char_bag; next_entry_id = snapshot.next_entry_id.clone(); } - async move { + cx.spawn_weak(|this, cx| async move { let entry = Entry::new( path, &fs.metadata(&abs_path) @@ -833,17 +835,28 @@ impl LocalWorktree { &next_entry_id, root_char_bag, ); - let mut snapshot = snapshot.lock(); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); - } - let entry = snapshot.insert_entry(entry, fs.as_ref()); - snapshot.scan_id += 1; - if let Some(tx) = shared_snapshots_tx { - tx.send(snapshot.clone()).await.ok(); + + let (entry, snapshot, snapshots_tx) = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("worktree was dropped"))? + .read_with(&cx, |this, _| { + let this = this.as_local().unwrap(); + let mut snapshot = this.background_snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + let entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; + let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); + (entry, snapshot.clone(), snapshots_tx) + }); + + if let Some(snapshots_tx) = snapshots_tx { + snapshots_tx.send(snapshot).await.ok(); } + Ok(entry) - } + }) } pub fn register( From 47ce8ae05c952bcecf091bfc3132bca15092edae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 11:50:21 +0200 Subject: [PATCH 10/26] Poll snapshot after refreshing entry --- crates/project/src/worktree.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index abaf49c9f2a9cbfdf33310d07e2aac3f55623319..1c77c3add7ca127bc3dfcde330787ac0ac38ce74 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -826,7 +826,7 @@ impl LocalWorktree { root_char_bag = snapshot.root_char_bag; next_entry_id = snapshot.next_entry_id.clone(); } - cx.spawn_weak(|this, cx| async move { + cx.spawn_weak(|this, mut cx| async move { let entry = Entry::new( path, &fs.metadata(&abs_path) @@ -836,20 +836,21 @@ impl LocalWorktree { root_char_bag, ); - let (entry, snapshot, snapshots_tx) = this + let this = this .upgrade(&cx) - .ok_or_else(|| anyhow!("worktree was dropped"))? - .read_with(&cx, |this, _| { - let this = this.as_local().unwrap(); - let mut snapshot = this.background_snapshot.lock(); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); - } - let entry = snapshot.insert_entry(entry, fs.as_ref()); - snapshot.scan_id += 1; - let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); - (entry, snapshot.clone(), snapshots_tx) - }); + .ok_or_else(|| anyhow!("worktree was dropped"))?; + let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| { + let this = this.as_local().unwrap(); + let mut snapshot = this.background_snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + let entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; + let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); + (entry, snapshot.clone(), snapshots_tx) + }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); if let Some(snapshots_tx) = snapshots_tx { snapshots_tx.send(snapshot).await.ok(); From c2973f33c2f842587596cc4db18bb7df216c6932 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 15:13:32 +0200 Subject: [PATCH 11/26] Uncomment randomized tests for contacts --- crates/collab/src/rpc.rs | 72 +++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e268204e034edfbcd9cd04f469cdff8b302c297b..b1f221464bb5320b4ad15bdb272ac8b1c90dfb09 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5928,16 +5928,23 @@ mod tests { if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } - // TODO - // let contacts = server - // .store - // .read() - // .await - // .contacts_for_user(guest.current_user_id(&guest_cx)); - // assert!(!contacts - // .iter() - // .flat_map(|contact| &contact.projects) - // .any(|project| project.id == host_project_id)); + + let contacts = server + .app_state + .db + .get_contacts(guest.current_user_id(&guest_cx)) + .await + .unwrap(); + let contacts = server + .store + .read() + .await + .build_initial_contacts_update(contacts) + .contacts; + assert!(!contacts + .iter() + .flat_map(|contact| &contact.projects) + .any(|project| project.id == host_project_id)); guest .project .as_ref() @@ -5997,9 +6004,12 @@ mod tests { let removed_guest_id = user_ids.remove(guest_ix); let guest = clients.remove(guest_ix); op_start_signals.remove(guest_ix); + server.forbid_connections(); server.disconnect_client(removed_guest_id); cx.foreground().advance_clock(RECEIVE_TIMEOUT); let (guest, mut guest_cx, guest_err) = guest.await; + server.allow_connections(); + if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } @@ -6008,23 +6018,31 @@ mod tests { .as_ref() .unwrap() .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - // TODO - // for user_id in &user_ids { - // for contact in server.store.read().await.contacts_for_user(*user_id) { - // assert_ne!( - // contact.user_id, removed_guest_id.0 as u64, - // "removed guest is still a contact of another peer" - // ); - // for project in contact.projects { - // for project_guest_id in project.guests { - // assert_ne!( - // project_guest_id, removed_guest_id.0 as u64, - // "removed guest appears as still participating on a project" - // ); - // } - // } - // } - // } + for user_id in &user_ids { + let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap(); + let contacts = server + .store + .read() + .await + .build_initial_contacts_update(contacts) + .contacts; + for contact in contacts { + if contact.online { + assert_ne!( + contact.user_id, removed_guest_id.0 as u64, + "removed guest is still a contact of another peer" + ); + } + for project in contact.projects { + for project_guest_id in project.guests { + assert_ne!( + project_guest_id, removed_guest_id.0 as u64, + "removed guest appears as still participating on a project" + ); + } + } + } + } log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); From e6576b32b2e49def177bb048c9ce1241ea9613d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 15:37:29 +0200 Subject: [PATCH 12/26] Don't show "they won't know if you decline" when request is accepted --- crates/contacts_panel/src/contact_notification.rs | 2 ++ .../contacts_panel/src/join_project_notification.rs | 1 + crates/contacts_panel/src/notifications.rs | 13 +++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs index 7df0f7cad96b0453145d895d88ef0e66ab32e031..2d408da0c2d9a4e16bdb0f3afe1f53436968f862 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/contacts_panel/src/contact_notification.rs @@ -45,6 +45,7 @@ impl View for ContactNotification { ContactEventKind::Requested => render_user_notification( self.event.user.clone(), "wants to add you as a contact", + Some("They won't know if you decline."), RespondToContactRequest { user_id: self.event.user.id, accept: false, @@ -70,6 +71,7 @@ impl View for ContactNotification { ContactEventKind::Accepted => render_user_notification( self.event.user.clone(), "accepted your contact request", + None, Dismiss(self.event.user.id), vec![], cx, diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs index e3cd1c6c9eab2c6a30fdbedb0b268af0229787e6..67ec59a7402f1bbfe35e9c2fd967eb04ccf557dc 100644 --- a/crates/contacts_panel/src/join_project_notification.rs +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -57,6 +57,7 @@ impl View for JoinProjectNotification { render_user_notification( self.user.clone(), "wants to join your project", + Some("They won't know if you decline."), Decline, vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))], cx, diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs index 21eac2a3c4128a6175a70641c0d748c00ddfc2d2..555d8962d3d8003eeaa11ad078625f51844b6dbb 100644 --- a/crates/contacts_panel/src/notifications.rs +++ b/crates/contacts_panel/src/notifications.rs @@ -13,7 +13,8 @@ enum Button {} pub fn render_user_notification( user: Arc, - message: &str, + title: &str, + body: Option<&str>, dismiss_action: A, buttons: Vec<(&'static str, Box)>, cx: &mut RenderContext, @@ -39,7 +40,7 @@ pub fn render_user_notification( })) .with_child( Text::new( - format!("{} {}", user.github_login, message), + format!("{} {}", user.github_login, title), theme.header_message.text.clone(), ) .contained() @@ -74,15 +75,15 @@ pub fn render_user_notification( ) .named("contact notification header"), ) - .with_child( + .with_children(body.map(|body| { Label::new( - "They won't know if you decline.".to_string(), + body.to_string(), theme.body_message.text.clone(), ) .contained() .with_style(theme.body_message.container) - .boxed(), - ) + .boxed() + })) .with_children(if buttons.is_empty() { None } else { From aa90c060127e4ff8eee7f9ee1f9001cdf770f6ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 17:45:50 +0200 Subject: [PATCH 13/26] Display a temporary window while remote project is loading --- assets/themes/cave-dark.json | 6 +++ assets/themes/cave-light.json | 6 +++ assets/themes/dark.json | 6 +++ assets/themes/light.json | 6 +++ assets/themes/solarized-dark.json | 6 +++ assets/themes/solarized-light.json | 6 +++ assets/themes/sulphurpool-dark.json | 6 +++ assets/themes/sulphurpool-light.json | 6 +++ crates/gpui/src/app.rs | 14 +++++++ crates/theme/src/theme.rs | 1 + crates/workspace/src/workspace.rs | 61 ++++++++++++++++++++++++---- styles/src/styleTree/workspace.ts | 4 ++ 12 files changed, 121 insertions(+), 7 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 347a8d90b9b86abde5e2c0d4ce791e685c76af0a..3942296d48e29d7bcb910e75fa072df9a4a78bb6 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#26232a", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 89acf414ab2764905ca4770760f680a88a715cd0..cdf29c27e48c75697d9727920a82746fe69b4f1f 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#e2dfe7", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#26232a", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index e91642bb25b512760e7600e7d5c6f5bb5c9c5799..80423cc5da1bbdd52a0ce14df9ab6283d58977d5 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#1c1c1c", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 99c1bab730d3b7df7eea1fbc51e5f841ebab15b4..1651d4209c7fb03b225dbfbf0bf5a27b85c63193 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#f8f8f8", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 8a75aa02dd184792b3f8dfdfaf31d74295356d4c..a60d59b85c1a2232257a92a80052e6cbe3df670e 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#073642", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#eee8d5", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 84bd0762ec07789bd8570dd78f605061857d06e3..2e4224c2b18b208f13eca3f8a29fc9ca221f1bc0 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#eee8d5", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#073642", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 716e81d6a298fff34f4cbeb39a65f4878829d884..5fd14a46cff0926bb4ba63a1ca844792cf7746ba 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#293256", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 09bb2127df7f8080c7058fd0fe5abd9f449b2d0d..65aaab6246a81396227957a8fec9f656e581e5c4 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -90,6 +90,12 @@ }, "workspace": { "background": "#dfe2f1", + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#293256", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3aba2cbffba0c71c82aae899a66178748718c33e..4f937b0448966c27cb3b46be2374b4da38974697 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1604,6 +1604,20 @@ impl MutableAppContext { }) } + pub fn replace_root_view(&mut self, window_id: usize, build_root_view: F) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + self.update(|this| { + let root_view = this.add_view(window_id, build_root_view); + let window = this.cx.windows.get_mut(&window_id).unwrap(); + window.root_view = root_view.clone().into(); + window.focused_view_id = Some(root_view.id()); + root_view + }) + } + pub fn remove_window(&mut self, window_id: usize) { self.cx.windows.remove(&window_id); self.presenters_and_platform_windows.remove(&window_id); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8ca7ce9ae2fe9832ce6f5e7a6b4c9b24af5b45a1..f663071002afdb1369e0e16fe8668efdfb110d08 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -48,6 +48,7 @@ pub struct Workspace { pub modal: ContainerStyle, pub notification: ContainerStyle, pub notifications: Notifications, + pub joining_project_message: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 26b355ae0a290adcbcd916e08a4f2adda478cec5..8f659aa1d6662abf15c13fabbd75222c027cb6b3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2272,6 +2272,34 @@ pub fn join_project( app_state: &Arc, cx: &mut MutableAppContext, ) -> Task>> { + struct JoiningNotice { + message: &'static str, + } + + impl Entity for JoiningNotice { + type Event = (); + } + + impl View for JoiningNotice { + fn ui_name() -> &'static str { + "JoiningProjectWindow" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.workspace; + Text::new( + self.message.to_string(), + theme.joining_project_message.text.clone(), + ) + .contained() + .with_style(theme.joining_project_message.container) + .aligned() + .contained() + .with_background_color(theme.background) + .boxed() + } + } + for window_id in cx.window_ids().collect::>() { if let Some(workspace) = cx.root_view::(window_id) { if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) { @@ -2282,6 +2310,10 @@ pub fn join_project( let app_state = app_state.clone(); cx.spawn(|mut cx| async move { + let (window, joining_notice) = + cx.update(|cx| cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { + message: "Loading remote project...", + })); let project = Project::remote( project_id, app_state.client.clone(), @@ -2290,13 +2322,28 @@ pub fn join_project( app_state.fs.clone(), &mut cx, ) - .await?; - Ok(cx.update(|cx| { - cx.add_window((app_state.build_window_options)(), |cx| { - (app_state.build_workspace)(project, &app_state, cx) - }) - .1 - })) + .await; + + cx.update(|cx| match project { + Ok(project) => Ok(cx.replace_root_view(window, |cx| { + let mut workspace = (app_state.build_workspace)(project, &app_state, cx); + workspace.toggle_sidebar_item( + &ToggleSidebarItem { + side: Side::Left, + item_index: 0, + }, + cx, + ); + workspace + })), + Err(error) => { + joining_notice.update(cx, |joining_notice, cx| { + joining_notice.message = "An error occurred trying to join the project. Please, close this window and retry."; + cx.notify(); + }); + Err(error) + }, + }) }) } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 65564f5cbc35299fd49e39656e97370798312084..7c17e4addd070d16b47ee7ec386aec7e3f9043e7 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -41,6 +41,10 @@ export default function workspace(theme: Theme) { return { background: backgroundColor(theme, 300), + joiningProjectMessage: { + padding: 12, + ...text(theme, "sans", "primary", { size: "lg" }) + }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, tab, From 740ec3d1922e0e9051fd2e8c5d32856e1aae9a27 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 17:46:08 +0200 Subject: [PATCH 14/26] WIP: decline pending join requests when project is unregistered --- crates/collab/src/rpc.rs | 32 +++++++++++++++++++++++++++++--- crates/collab/src/rpc/store.rs | 14 ++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b1f221464bb5320b4ad15bdb272ac8b1c90dfb09..14c3b71e1316be42b57a814172160e98f6e209ab 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -347,6 +347,19 @@ impl Server { self.peer .send(conn_id, proto::UnregisterProject { project_id }) }); + + for (_, receipts) in project.join_requests { + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline {}, + )), + }, + )?; + } + } } for project_id in removed_connection.guest_project_ids { @@ -409,11 +422,24 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let user_id = { + let (user_id, project) = { let mut state = self.store_mut().await; - state.unregister_project(request.payload.project_id, request.sender_id)?; - state.user_id_for_connection(request.sender_id)? + let project = + state.unregister_project(request.payload.project_id, request.sender_id)?; + (state.user_id_for_connection(request.sender_id)?, project) }; + for (_, receipts) in project.join_requests { + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline {}, + )), + }, + )?; + } + } self.update_user_contacts(user_id).await?; Ok(()) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 81e4c9633bb6ecb896e7f0c63482f15c6e4c64e9..9af324e5d87273390b511e974e10bd34bf65c614 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -338,6 +338,20 @@ impl Store { } } + for requester_user_id in project.join_requests.keys() { + if let Some(requester_connection_ids) = + self.connections_by_user_id.get_mut(&requester_user_id) + { + for requester_connection_id in requester_connection_ids.iter() { + if let Some(requester_connection) = + self.connections.get_mut(requester_connection_id) + { + requester_connection.requested_projects.remove(&project_id); + } + } + } + } + Ok(project) } else { Err(anyhow!("no such project"))? From ed6ed99d8f99e18dd371d7ed53a3362d2f222de5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 16 May 2022 19:56:10 +0200 Subject: [PATCH 15/26] Show the reason why a join request was declined Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/collab/src/rpc.rs | 150 ++++++++++++++++++++++++++++-- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 30 +++++- crates/rpc/proto/zed.proto | 10 +- crates/workspace/src/workspace.rs | 28 ++++-- 6 files changed, 203 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60b35ef3c624651fd7cc8e6f52d7e9d645492c19..5a640659c6781ce6faa1ba5640e1ec8bc3d1c3ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3394,6 +3394,7 @@ dependencies = [ "sum_tree", "tempdir", "text", + "thiserror", "toml", "unindent", "util", diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 14c3b71e1316be42b57a814172160e98f6e209ab..7fdaaf5d9f9c1fa7a8b539de456d60fff1c19078 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -354,7 +354,9 @@ impl Server { receipt, proto::JoinProjectResponse { variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline {}, + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::WentOffline as i32 + }, )), }, )?; @@ -434,7 +436,10 @@ impl Server { receipt, proto::JoinProjectResponse { variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline {}, + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::Closed + as i32, + }, )), }, )?; @@ -542,7 +547,10 @@ impl Server { receipt, proto::JoinProjectResponse { variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline {}, + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::Declined + as i32, + }, )), }, )?; @@ -1837,17 +1845,26 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_host_disconnect( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + ) { let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); - // Connect to a server as 2 clients. + // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; // Share a project as client A @@ -1868,6 +1885,9 @@ mod tests { cx, ) }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; let (worktree_a, _) = project_a .update(cx_a, |p, cx| { p.find_or_create_local_worktree("/a", true, cx) @@ -1887,6 +1907,24 @@ mod tests { .await .unwrap(); + // Request to join that project as client C + let project_c = cx_c.spawn(|mut cx| { + let client = client_c.client.clone(); + let user_store = client_c.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); @@ -1901,6 +1939,10 @@ mod tests { cx_b.update(|_| { drop(project_b); }); + assert!(matches!( + project_c.await.unwrap_err(), + project::JoinProjectError::HostWentOffline + )); // Ensure guests can still join. let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; @@ -1911,6 +1953,102 @@ mod tests { .unwrap(); } + #[gpui::test(iterations = 10)] + async fn test_decline_join_request( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + // Share a project as client A + fs.insert_tree("/a", json!({})).await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + // Request to join that project as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + deterministic.run_until_parked(); + project_a.update(cx_a, |project, cx| { + project.respond_to_join_request(client_b.user_id().unwrap(), false, cx) + }); + assert!(matches!( + project_b.await.unwrap_err(), + project::JoinProjectError::HostDeclined + )); + + // Request to join the project again as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + + // Close the project on the host + deterministic.run_until_parked(); + cx_a.update(|_| drop(project_a)); + deterministic.run_until_parked(); + assert!(matches!( + project_b.await.unwrap_err(), + project::JoinProjectError::HostClosedProject + )); + } + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index eaae45bcc62d31f68adde4a8492c9d4f2e9a9f7b..7a3c7c5367f1137962e3bf0262b7b5eacfac4193 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -45,6 +45,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" similar = "1.3" smol = "1.2.5" +thiserror = "1.0.29" toml = "0.5" [dev-dependencies] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 28e3a16fd5b38d058d88a242cadb6564e049bf28..d2856f876dc0d66453b9277457af54259a013fd8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -49,6 +49,7 @@ use std::{ }, time::Instant, }; +use thiserror::Error; use util::{post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; @@ -90,6 +91,18 @@ pub struct Project { nonce: u128, } +#[derive(Error, Debug)] +pub enum JoinProjectError { + #[error("host declined join request")] + HostDeclined, + #[error("host closed the project")] + HostClosedProject, + #[error("host went offline")] + HostWentOffline, + #[error("{0}")] + Other(#[from] anyhow::Error), +} + enum OpenBuffer { Strong(ModelHandle), Weak(WeakModelHandle), @@ -356,7 +369,7 @@ impl Project { languages: Arc, fs: Arc, cx: &mut AsyncAppContext, - ) -> Result> { + ) -> Result, JoinProjectError> { client.authenticate_and_connect(true, &cx).await?; let response = client @@ -367,7 +380,20 @@ impl Project { let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? { proto::join_project_response::Variant::Accept(response) => response, - proto::join_project_response::Variant::Decline(_) => Err(anyhow!("rejected"))?, + proto::join_project_response::Variant::Decline(decline) => { + match proto::join_project_response::decline::Reason::from_i32(decline.reason) { + Some(proto::join_project_response::decline::Reason::Declined) => { + Err(JoinProjectError::HostDeclined)? + } + Some(proto::join_project_response::decline::Reason::Closed) => { + Err(JoinProjectError::HostClosedProject)? + } + Some(proto::join_project_response::decline::Reason::WentOffline) => { + Err(JoinProjectError::HostWentOffline)? + } + None => Err(anyhow!("missing decline reason"))?, + } + } }; let replica_id = response.replica_id as ReplicaId; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 83ee63765732cf7dc4e48636ff1780808abdd0f8..511348c9d02625b6c61a620e4ed7b092d3dd5425 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -153,7 +153,15 @@ message JoinProjectResponse { repeated LanguageServer language_servers = 4; } - message Decline {} + message Decline { + Reason reason = 1; + + enum Reason { + Declined = 0; + Closed = 1; + WentOffline = 2; + } + } } message LeaveProject { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8f659aa1d6662abf15c13fabbd75222c027cb6b3..85e03461a9843ccad83a89ba03a3e0d344e3423a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2310,10 +2310,11 @@ pub fn join_project( let app_state = app_state.clone(); cx.spawn(|mut cx| async move { - let (window, joining_notice) = - cx.update(|cx| cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { + let (window, joining_notice) = cx.update(|cx| { + cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { message: "Loading remote project...", - })); + }) + }); let project = Project::remote( project_id, app_state.client.clone(), @@ -2336,13 +2337,24 @@ pub fn join_project( ); workspace })), - Err(error) => { - joining_notice.update(cx, |joining_notice, cx| { - joining_notice.message = "An error occurred trying to join the project. Please, close this window and retry."; + Err(error @ _) => { + let message = match error { + project::JoinProjectError::HostDeclined => { + "The host declined your request to join." + } + project::JoinProjectError::HostClosedProject => "The host closed the project.", + project::JoinProjectError::HostWentOffline => "The host went offline.", + project::JoinProjectError::Other(_) => { + "An error occurred when attempting to join the project." + } + }; + joining_notice.update(cx, |notice, cx| { + notice.message = message; cx.notify(); }); - Err(error) - }, + + Err(error)? + } }) }) } From 91257f308e952fb6c609f52f3ec4cb935e99bfae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 16 May 2022 13:15:46 -0600 Subject: [PATCH 16/26] Remove "They won't know if you decline" message --- crates/contacts_panel/src/join_project_notification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs index 67ec59a7402f1bbfe35e9c2fd967eb04ccf557dc..8e55396e3e31b2f77f329dbd48eb8cd72b8b135f 100644 --- a/crates/contacts_panel/src/join_project_notification.rs +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -57,7 +57,7 @@ impl View for JoinProjectNotification { render_user_notification( self.user.clone(), "wants to join your project", - Some("They won't know if you decline."), + None, Decline, vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))], cx, From 7c3eebf93e0c46fdf976b3ea4bf00a2db7ffacc5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 16 May 2022 16:52:31 -0600 Subject: [PATCH 17/26] Refine messages on waiting to join screen and include host avatar --- assets/themes/cave-dark.json | 4 + assets/themes/cave-light.json | 4 + assets/themes/dark.json | 4 + assets/themes/light.json | 4 + assets/themes/solarized-dark.json | 4 + assets/themes/solarized-light.json | 4 + assets/themes/sulphurpool-dark.json | 4 + assets/themes/sulphurpool-light.json | 4 + crates/contacts_panel/src/contacts_panel.rs | 17 ++-- crates/theme/src/theme.rs | 1 + crates/workspace/src/workspace.rs | 93 ++++++++++++++++----- script/watch-themes | 7 ++ styles/src/styleTree/workspace.ts | 4 + 13 files changed, 125 insertions(+), 29 deletions(-) create mode 100755 script/watch-themes diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 3942296d48e29d7bcb910e75fa072df9a4a78bb6..ab75d12b64fa20a599d46529ab8a2fa594b927e8 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#26232a", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index cdf29c27e48c75697d9727920a82746fe69b4f1f..543abffeb487bfe4bff10017494bd4244dbd5481 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#e2dfe7", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 80423cc5da1bbdd52a0ce14df9ab6283d58977d5..bdb38dbe0ef525d754156e86df4bd2e2a3a54b27 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#1c1c1c", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/light.json b/assets/themes/light.json index 1651d4209c7fb03b225dbfbf0bf5a27b85c63193..9667422894e62191185d7fd73837ba02ae1024f3 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#f8f8f8", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index a60d59b85c1a2232257a92a80052e6cbe3df670e..e4bc45ab4f0794eafcc774a824d951777b128349 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#073642", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 2e4224c2b18b208f13eca3f8a29fc9ca221f1bc0..898b7e62a11c62b74c77aacd8eac5ebdce05f5ba 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#eee8d5", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 5fd14a46cff0926bb4ba63a1ca844792cf7746ba..9a8e897d57c5996782e5d46a0fbbc169402eb0b7 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#293256", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 65aaab6246a81396227957a8fec9f656e581e5c4..d3a017ca0615eceac528f3ebb690bc7952fc0729 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -90,6 +90,10 @@ }, "workspace": { "background": "#dfe2f1", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, "joining_project_message": { "padding": 12, "family": "Zed Sans", diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 9ea62f0831b6097d438f4450aa756b1647cbc54f..a33739293fbcbb3c494e1c4d52d86af485f3cea9 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -336,14 +336,14 @@ impl ContactsPanel { fn render_contact_project( contact: Arc, current_user_id: Option, - project_ix: usize, + project_index: usize, app_state: Arc, theme: &theme::ContactsPanel, is_last_project: bool, is_selected: bool, cx: &mut LayoutContext, ) -> ElementBox { - let project = &contact.projects[project_ix]; + let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; let is_guest = !is_host @@ -445,7 +445,8 @@ impl ContactsPanel { .on_click(move |_, cx| { if !is_host && !is_guest { cx.dispatch_global_action(JoinProject { - project_id, + contact: contact.clone(), + project_index, app_state: app_state.clone(), }); } @@ -768,12 +769,12 @@ impl ContactsPanel { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } - ContactEntry::ContactProject(contact, project_ix) => { - cx.dispatch_global_action(JoinProject { - project_id: contact.projects[*project_ix].id, + ContactEntry::ContactProject(contact, project_index) => cx + .dispatch_global_action(JoinProject { + contact: contact.clone(), + project_index: *project_index, app_state: self.app_state.clone(), - }) - } + }), _ => {} } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f663071002afdb1369e0e16fe8668efdfb110d08..f4b99f7b9ef71f85bf65cbd2b5617fd6e391f950 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -48,6 +48,7 @@ pub struct Workspace { pub modal: ContainerStyle, pub notification: ContainerStyle, pub notifications: Notifications, + pub joining_project_avatar: ImageStyle, pub joining_project_message: ContainedText, } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 85e03461a9843ccad83a89ba03a3e0d344e3423a..9849e14bbad6f7043c3141da52dedbbfb57fc92d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8,7 +8,8 @@ mod toolbar; use anyhow::{anyhow, Context, Result}; use client::{ - proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, + proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User, + UserStore, }; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; @@ -97,7 +98,8 @@ pub struct ToggleFollow(pub PeerId); #[derive(Clone)] pub struct JoinProject { - pub project_id: u64, + pub contact: Arc, + pub project_index: usize, pub app_state: Arc, } @@ -117,7 +119,13 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { open_new(&action.0, cx) }); cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| { - join_project(action.project_id, &action.app_state, cx).detach(); + join_project( + action.contact.clone(), + action.project_index, + &action.app_state, + cx, + ) + .detach(); }); cx.add_async_action(Workspace::toggle_follow); @@ -2268,12 +2276,16 @@ pub fn open_paths( } pub fn join_project( - project_id: u64, + contact: Arc, + project_index: usize, app_state: &Arc, cx: &mut MutableAppContext, ) -> Task>> { + let project_id = contact.projects[project_index].id; + struct JoiningNotice { - message: &'static str, + avatar: Option>, + message: String, } impl Entity for JoiningNotice { @@ -2287,16 +2299,28 @@ pub fn join_project( fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.workspace; - Text::new( - self.message.to_string(), - theme.joining_project_message.text.clone(), - ) - .contained() - .with_style(theme.joining_project_message.container) - .aligned() - .contained() - .with_background_color(theme.background) - .boxed() + + Flex::column() + .with_children(self.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.joining_project_avatar) + .aligned() + .boxed() + })) + .with_child( + Text::new( + self.message.clone(), + theme.joining_project_message.text.clone(), + ) + .contained() + .with_style(theme.joining_project_message.container) + .aligned() + .boxed(), + ) + .aligned() + .contained() + .with_background_color(theme.background) + .boxed() } } @@ -2312,7 +2336,12 @@ pub fn join_project( cx.spawn(|mut cx| async move { let (window, joining_notice) = cx.update(|cx| { cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { - message: "Loading remote project...", + avatar: contact.user.avatar.clone(), + message: format!( + "Asking to join @{}'s copy of {}...", + contact.user.github_login, + humanize_list(&contact.projects[project_index].worktree_root_names) + ), }) }); let project = Project::remote( @@ -2338,15 +2367,22 @@ pub fn join_project( workspace })), Err(error @ _) => { + let login = &contact.user.github_login; let message = match error { project::JoinProjectError::HostDeclined => { - "The host declined your request to join." + format!("@{} declined your request.", login) + } + project::JoinProjectError::HostClosedProject => { + format!( + "@{} closed their copy of {}.", + login, + humanize_list(&contact.projects[project_index].worktree_root_names) + ) } - project::JoinProjectError::HostClosedProject => "The host closed the project.", - project::JoinProjectError::HostWentOffline => "The host went offline.", - project::JoinProjectError::Other(_) => { - "An error occurred when attempting to join the project." + project::JoinProjectError::HostWentOffline => { + format!("@{} went offline.", login) } + project::JoinProjectError::Other(_) => "An error occurred.".to_string(), }; joining_notice.update(cx, |notice, cx| { notice.message = message; @@ -2372,3 +2408,18 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { }); cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone())); } + +fn humanize_list<'a>(items: impl IntoIterator) -> String { + let mut list = String::new(); + let mut items = items.into_iter().enumerate().peekable(); + while let Some((ix, item)) = items.next() { + if ix > 0 { + list.push_str(", "); + } + if items.peek().is_none() { + list.push_str("and "); + } + list.push_str(item); + } + list +} diff --git a/script/watch-themes b/script/watch-themes new file mode 100755 index 0000000000000000000000000000000000000000..a3663962f7ca970130c6596246419cdaf91ca219 --- /dev/null +++ b/script/watch-themes @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +cd styles +npm install +npm run watch diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 7c17e4addd070d16b47ee7ec386aec7e3f9043e7..110906868885c296de2769778b50c389cd09cb95 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -41,6 +41,10 @@ export default function workspace(theme: Theme) { return { background: backgroundColor(theme, 300), + joiningProjectAvatar: { + cornerRadius: 40, + width: 80, + }, joiningProjectMessage: { padding: 12, ...text(theme, "sans", "primary", { size: "lg" }) From d821e7a4c1ef67b751585e881632b58f497079c3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 16 May 2022 19:02:23 -0600 Subject: [PATCH 18/26] Cancel join requests when the requester closes the window --- crates/client/src/user.rs | 2 + crates/collab/src/rpc.rs | 133 +++++++++++++++++++-- crates/collab/src/rpc/store.rs | 43 +++++-- crates/gpui/src/app.rs | 15 +++ crates/project/src/project.rs | 25 +++- crates/rpc/proto/zed.proto | 170 ++++++++++++++------------- crates/rpc/src/proto.rs | 2 + crates/rpc/src/rpc.rs | 2 +- crates/workspace/src/waiting_room.rs | 159 +++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 130 ++------------------ 10 files changed, 458 insertions(+), 223 deletions(-) create mode 100644 crates/workspace/src/waiting_room.rs diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 97cf225b5fa13c5945c30db38fc57af1c0016009..296c0cf9ae1df4e37e5f229da3d1c71987dd3832 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -23,6 +23,8 @@ impl PartialEq for User { } } +impl Eq for User {} + #[derive(Debug)] pub struct Contact { pub user: Arc, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7fdaaf5d9f9c1fa7a8b539de456d60fff1c19078..28c863ef0ba77bc04b3021112213a5a680a7a635 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -650,19 +650,32 @@ impl Server { let project_id = request.payload.project_id; let project; { - let mut state = self.store_mut().await; - project = state.leave_project(sender_id, project_id)?; - let unshare = project.connection_ids.len() <= 1; - broadcast(sender_id, project.connection_ids, |conn_id| { + let mut store = self.store_mut().await; + project = store.leave_project(sender_id, project_id)?; + + if project.remove_collaborator { + broadcast(sender_id, project.connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: sender_id.0, + }, + ) + }); + } + + if let Some(requester_id) = project.cancel_request { self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { + project.host_connection_id, + proto::JoinProjectRequestCancelled { project_id, - peer_id: sender_id.0, + requester_id: requester_id.to_proto(), }, - ) - }); - if unshare { + )?; + } + + if project.unshare { self.peer.send( project.host_connection_id, proto::ProjectUnshared { project_id }, @@ -1633,6 +1646,7 @@ mod tests { use settings::Settings; use sqlx::types::time::OffsetDateTime; use std::{ + cell::RefCell, env, ops::Deref, path::{Path, PathBuf}, @@ -2049,6 +2063,105 @@ mod tests { )); } + #[gpui::test(iterations = 10)] + async fn test_cancel_join_request( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + // Share a project as client A + fs.insert_tree("/a", json!({})).await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; + + let project_a_events = Rc::new(RefCell::new(Vec::new())); + let user_b = client_a + .user_store + .update(cx_a, |store, cx| { + store.fetch_user(client_b.user_id().unwrap(), cx) + }) + .await + .unwrap(); + project_a.update(cx_a, { + let project_a_events = project_a_events.clone(); + move |_, cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + project_a_events.borrow_mut().push(event.clone()); + }) + .detach(); + } + }); + + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + // Request to join that project as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + deterministic.run_until_parked(); + assert_eq!( + &*project_a_events.borrow(), + &[project::Event::ContactRequestedJoin(user_b.clone())] + ); + project_a_events.borrow_mut().clear(); + + // Cancel the join request by leaving the project + client_b + .client + .send(proto::LeaveProject { project_id }) + .unwrap(); + drop(project_b); + + deterministic.run_until_parked(); + assert_eq!( + &*project_a_events.borrow(), + &[project::Event::ContactCancelledJoinRequest(user_b.clone())] + ); + } + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9af324e5d87273390b511e974e10bd34bf65c614..80dc9f74e9f211a4f1968d48f98d750879cc947a 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,6 +1,6 @@ use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; -use collections::{BTreeMap, HashMap, HashSet}; +use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}; use rpc::{proto, ConnectionId, Receipt}; use std::{collections::hash_map, path::PathBuf}; use tracing::instrument; @@ -56,9 +56,12 @@ pub struct RemovedConnectionState { } pub struct LeftProject { - pub connection_ids: Vec, pub host_user_id: UserId, pub host_connection_id: ConnectionId, + pub connection_ids: Vec, + pub remove_collaborator: bool, + pub cancel_request: Option, + pub unshare: bool, } #[derive(Copy, Clone)] @@ -503,24 +506,48 @@ impl Store { connection_id: ConnectionId, project_id: u64, ) -> Result { + let user_id = self.user_id_for_connection(connection_id)?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - let (replica_id, _) = project - .guests - .remove(&connection_id) - .ok_or_else(|| anyhow!("cannot leave a project before joining it"))?; - project.active_replica_ids.remove(&replica_id); + + // If the connection leaving the project is a collaborator, remove it. + let remove_collaborator = + if let Some((replica_id, _)) = project.guests.remove(&connection_id) { + project.active_replica_ids.remove(&replica_id); + true + } else { + false + }; + + // If the connection leaving the project has a pending request, remove it. + // If that user has no other pending requests on other connections, indicate that the request should be cancelled. + let mut cancel_request = None; + if let Entry::Occupied(mut entry) = project.join_requests.entry(user_id) { + entry + .get_mut() + .retain(|receipt| receipt.sender_id != connection_id); + if entry.get().is_empty() { + entry.remove(); + cancel_request = Some(user_id); + } + } if let Some(connection) = self.connections.get_mut(&connection_id) { connection.projects.remove(&project_id); } + let connection_ids = project.connection_ids(); + let unshare = connection_ids.len() <= 1 && project.join_requests.is_empty(); + Ok(LeftProject { - connection_ids: project.connection_ids(), host_connection_id: project.host_connection_id, host_user_id: project.host_user_id, + connection_ids, + cancel_request, + unshare, + remove_collaborator, }) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4f937b0448966c27cb3b46be2374b4da38974697..fc19b1422ae8d8705249965bf83e166192d7e72f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3231,6 +3231,21 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } + pub fn replace_root_view(&mut self, build_root_view: F) -> ViewHandle + where + V: View, + F: FnOnce(&mut ViewContext) -> V, + { + let window_id = self.window_id; + self.update(|this| { + let root_view = this.add_view(window_id, build_root_view); + let window = this.cx.windows.get_mut(&window_id).unwrap(); + window.root_view = root_view.clone().into(); + window.focused_view_id = Some(root_view.id()); + root_view + }) + } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d2856f876dc0d66453b9277457af54259a013fd8..33e37dc900165a06fac3615ea50f19e6c7ea0ebb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -136,7 +136,7 @@ pub struct Collaborator { pub replica_id: ReplicaId, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), @@ -147,6 +147,7 @@ pub enum Event { RemoteIdChanged(Option), CollaboratorLeft(PeerId), ContactRequestedJoin(Arc), + ContactCancelledJoinRequest(Arc), } #[derive(Serialize)] @@ -269,6 +270,7 @@ impl Project { client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_join_project_request_cancelled); client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); client.add_model_message_handler(Self::handle_unregister_project); @@ -3879,6 +3881,27 @@ impl Project { }) } + async fn handle_join_project_request_cancelled( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user = this + .update(&mut cx, |this, cx| { + this.user_store.update(cx, |user_store, cx| { + user_store.fetch_user(envelope.payload.requester_id, cx) + }) + }) + .await?; + + this.update(&mut cx, |_, cx| { + cx.emit(Event::ContactCancelledJoinRequest(user)); + }); + + Ok(()) + } + async fn handle_register_worktree( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 511348c9d02625b6c61a620e4ed7b092d3dd5425..d2ab7d99ac17e4a8fe36b922e714a734cb549e7f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -16,88 +16,89 @@ message Envelope { UnregisterProject unregister_project = 10; RequestJoinProject request_join_project = 11; RespondToJoinProjectRequest respond_to_join_project_request = 12; - JoinProject join_project = 13; - JoinProjectResponse join_project_response = 14; - LeaveProject leave_project = 15; - AddProjectCollaborator add_project_collaborator = 16; - RemoveProjectCollaborator remove_project_collaborator = 17; - ProjectUnshared project_unshared = 18; - - GetDefinition get_definition = 19; - GetDefinitionResponse get_definition_response = 20; - GetReferences get_references = 21; - GetReferencesResponse get_references_response = 22; - GetDocumentHighlights get_document_highlights = 23; - GetDocumentHighlightsResponse get_document_highlights_response = 24; - GetProjectSymbols get_project_symbols = 25; - GetProjectSymbolsResponse get_project_symbols_response = 26; - OpenBufferForSymbol open_buffer_for_symbol = 27; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 28; - - RegisterWorktree register_worktree = 29; - UnregisterWorktree unregister_worktree = 30; - UpdateWorktree update_worktree = 31; - - CreateProjectEntry create_project_entry = 32; - RenameProjectEntry rename_project_entry = 33; - DeleteProjectEntry delete_project_entry = 34; - ProjectEntryResponse project_entry_response = 35; - - UpdateDiagnosticSummary update_diagnostic_summary = 36; - StartLanguageServer start_language_server = 37; - UpdateLanguageServer update_language_server = 38; - - OpenBufferById open_buffer_by_id = 39; - OpenBufferByPath open_buffer_by_path = 40; - OpenBufferResponse open_buffer_response = 41; - UpdateBuffer update_buffer = 42; - UpdateBufferFile update_buffer_file = 43; - SaveBuffer save_buffer = 44; - BufferSaved buffer_saved = 45; - BufferReloaded buffer_reloaded = 46; - ReloadBuffers reload_buffers = 47; - ReloadBuffersResponse reload_buffers_response = 48; - FormatBuffers format_buffers = 49; - FormatBuffersResponse format_buffers_response = 50; - GetCompletions get_completions = 51; - GetCompletionsResponse get_completions_response = 52; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; - GetCodeActions get_code_actions = 55; - GetCodeActionsResponse get_code_actions_response = 56; - ApplyCodeAction apply_code_action = 57; - ApplyCodeActionResponse apply_code_action_response = 58; - PrepareRename prepare_rename = 59; - PrepareRenameResponse prepare_rename_response = 60; - PerformRename perform_rename = 61; - PerformRenameResponse perform_rename_response = 62; - SearchProject search_project = 63; - SearchProjectResponse search_project_response = 64; - - GetChannels get_channels = 65; - GetChannelsResponse get_channels_response = 66; - JoinChannel join_channel = 67; - JoinChannelResponse join_channel_response = 68; - LeaveChannel leave_channel = 69; - SendChannelMessage send_channel_message = 70; - SendChannelMessageResponse send_channel_message_response = 71; - ChannelMessageSent channel_message_sent = 72; - GetChannelMessages get_channel_messages = 73; - GetChannelMessagesResponse get_channel_messages_response = 74; - - UpdateContacts update_contacts = 75; - - GetUsers get_users = 76; - FuzzySearchUsers fuzzy_search_users = 77; - UsersResponse users_response = 78; - RequestContact request_contact = 79; - RespondToContactRequest respond_to_contact_request = 80; - RemoveContact remove_contact = 81; - - Follow follow = 82; - FollowResponse follow_response = 83; - UpdateFollowers update_followers = 84; - Unfollow unfollow = 85; + JoinProjectRequestCancelled join_project_request_cancelled = 13; + JoinProject join_project = 14; + JoinProjectResponse join_project_response = 15; + LeaveProject leave_project = 16; + AddProjectCollaborator add_project_collaborator = 17; + RemoveProjectCollaborator remove_project_collaborator = 18; + ProjectUnshared project_unshared = 19; + + GetDefinition get_definition = 20; + GetDefinitionResponse get_definition_response = 21; + GetReferences get_references = 22; + GetReferencesResponse get_references_response = 23; + GetDocumentHighlights get_document_highlights = 24; + GetDocumentHighlightsResponse get_document_highlights_response = 25; + GetProjectSymbols get_project_symbols = 26; + GetProjectSymbolsResponse get_project_symbols_response = 27; + OpenBufferForSymbol open_buffer_for_symbol = 28; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; + + RegisterWorktree register_worktree = 30; + UnregisterWorktree unregister_worktree = 31; + UpdateWorktree update_worktree = 32; + + CreateProjectEntry create_project_entry = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; + ProjectEntryResponse project_entry_response = 36; + + UpdateDiagnosticSummary update_diagnostic_summary = 37; + StartLanguageServer start_language_server = 38; + UpdateLanguageServer update_language_server = 39; + + OpenBufferById open_buffer_by_id = 40; + OpenBufferByPath open_buffer_by_path = 41; + OpenBufferResponse open_buffer_response = 42; + UpdateBuffer update_buffer = 43; + UpdateBufferFile update_buffer_file = 44; + SaveBuffer save_buffer = 45; + BufferSaved buffer_saved = 46; + BufferReloaded buffer_reloaded = 47; + ReloadBuffers reload_buffers = 48; + ReloadBuffersResponse reload_buffers_response = 49; + FormatBuffers format_buffers = 50; + FormatBuffersResponse format_buffers_response = 51; + GetCompletions get_completions = 52; + GetCompletionsResponse get_completions_response = 53; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55; + GetCodeActions get_code_actions = 56; + GetCodeActionsResponse get_code_actions_response = 57; + ApplyCodeAction apply_code_action = 58; + ApplyCodeActionResponse apply_code_action_response = 59; + PrepareRename prepare_rename = 60; + PrepareRenameResponse prepare_rename_response = 61; + PerformRename perform_rename = 62; + PerformRenameResponse perform_rename_response = 63; + SearchProject search_project = 64; + SearchProjectResponse search_project_response = 65; + + GetChannels get_channels = 66; + GetChannelsResponse get_channels_response = 67; + JoinChannel join_channel = 68; + JoinChannelResponse join_channel_response = 69; + LeaveChannel leave_channel = 70; + SendChannelMessage send_channel_message = 71; + SendChannelMessageResponse send_channel_message_response = 72; + ChannelMessageSent channel_message_sent = 73; + GetChannelMessages get_channel_messages = 74; + GetChannelMessagesResponse get_channel_messages_response = 75; + + UpdateContacts update_contacts = 76; + + GetUsers get_users = 77; + FuzzySearchUsers fuzzy_search_users = 78; + UsersResponse users_response = 79; + RequestContact request_contact = 80; + RespondToContactRequest respond_to_contact_request = 81; + RemoveContact remove_contact = 82; + + Follow follow = 83; + FollowResponse follow_response = 84; + UpdateFollowers update_followers = 85; + Unfollow unfollow = 86; } } @@ -136,6 +137,11 @@ message RespondToJoinProjectRequest { bool allow = 3; } +message JoinProjectRequestCancelled { + uint64 requester_id = 1; + uint64 project_id = 2; +} + message JoinProject { uint64 project_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 1a87cb6a0c33f1e920e17ff8fc4bb5bc913199d4..a1b7425b69a724c50209c3bf0bf366987bd4fb94 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -114,6 +114,7 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (JoinProjectRequestCancelled, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -220,6 +221,7 @@ entity_messages!( GetReferences, GetProjectSymbols, JoinProject, + JoinProjectRequestCancelled, LeaveProject, OpenBufferById, OpenBufferByPath, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index f238c6792d9ed188984292fe49de18400e308c91..1d4416d77520e5a2f8036679f5d7c85e17d68226 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 17; +pub const PROTOCOL_VERSION: u32 = 18; diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs new file mode 100644 index 0000000000000000000000000000000000000000..17efebce39c71e067a062cef32d1220a62522a84 --- /dev/null +++ b/crates/workspace/src/waiting_room.rs @@ -0,0 +1,159 @@ +use crate::{ + sidebar::{Side, ToggleSidebarItem}, + AppState, +}; +use anyhow::Result; +use client::Contact; +use gpui::{elements::*, ElementBox, Entity, ImageData, RenderContext, Task, View, ViewContext}; +use project::Project; +use settings::Settings; +use std::sync::Arc; + +pub struct WaitingRoom { + avatar: Option>, + message: String, + joined: bool, + _join_task: Task>, +} + +impl Entity for WaitingRoom { + type Event = (); + + fn release(&mut self, _: &mut gpui::MutableAppContext) { + if !self.joined { + // TODO: Cancel the join request + } + } +} + +impl View for WaitingRoom { + fn ui_name() -> &'static str { + "WaitingRoom" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.workspace; + + Flex::column() + .with_children(self.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.joining_project_avatar) + .aligned() + .boxed() + })) + .with_child( + Text::new( + self.message.clone(), + theme.joining_project_message.text.clone(), + ) + .contained() + .with_style(theme.joining_project_message.container) + .aligned() + .boxed(), + ) + .aligned() + .contained() + .with_background_color(theme.background) + .boxed() + } +} + +impl WaitingRoom { + pub fn new( + contact: Arc, + project_index: usize, + app_state: Arc, + cx: &mut ViewContext, + ) -> Self { + let project_id = contact.projects[project_index].id; + + let _join_task = cx.spawn_weak({ + let contact = contact.clone(); + |this, mut cx| async move { + let project = Project::remote( + project_id, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + &mut cx, + ) + .await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| match project { + Ok(project) => { + this.joined = true; + cx.replace_root_view(|cx| { + let mut workspace = + (app_state.build_workspace)(project, &app_state, cx); + workspace.toggle_sidebar_item( + &ToggleSidebarItem { + side: Side::Left, + item_index: 0, + }, + cx, + ); + workspace + }); + } + Err(error @ _) => { + let login = &contact.user.github_login; + let message = match error { + project::JoinProjectError::HostDeclined => { + format!("@{} declined your request.", login) + } + project::JoinProjectError::HostClosedProject => { + format!( + "@{} closed their copy of {}.", + login, + humanize_list( + &contact.projects[project_index].worktree_root_names + ) + ) + } + project::JoinProjectError::HostWentOffline => { + format!("@{} went offline.", login) + } + project::JoinProjectError::Other(error) => { + log::error!("error joining project: {}", error); + "An error occurred.".to_string() + } + }; + this.message = message; + cx.notify(); + } + }) + } + + Ok(()) + } + }); + + Self { + avatar: contact.user.avatar.clone(), + message: format!( + "Asking to join @{}'s copy of {}...", + contact.user.github_login, + humanize_list(&contact.projects[project_index].worktree_root_names) + ), + joined: false, + _join_task, + } + } +} + +fn humanize_list<'a>(items: impl IntoIterator) -> String { + let mut list = String::new(); + let mut items = items.into_iter().enumerate().peekable(); + while let Some((ix, item)) = items.next() { + if ix > 0 { + list.push_str(", "); + } + if items.peek().is_none() { + list.push_str("and "); + } + list.push_str(item); + } + list +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9849e14bbad6f7043c3141da52dedbbfb57fc92d..d27cb9bfbaf9791d64160c4956db22bfccb8e843 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,7 @@ pub mod pane_group; pub mod sidebar; mod status_bar; mod toolbar; +mod waiting_room; use anyhow::{anyhow, Context, Result}; use client::{ @@ -50,6 +51,7 @@ use std::{ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; +use waiting_room::WaitingRoom; type ProjectItemBuilders = HashMap< TypeId, @@ -124,8 +126,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { action.project_index, &action.app_state, cx, - ) - .detach(); + ); }); cx.add_async_action(Workspace::toggle_follow); @@ -2280,119 +2281,21 @@ pub fn join_project( project_index: usize, app_state: &Arc, cx: &mut MutableAppContext, -) -> Task>> { +) { let project_id = contact.projects[project_index].id; - struct JoiningNotice { - avatar: Option>, - message: String, - } - - impl Entity for JoiningNotice { - type Event = (); - } - - impl View for JoiningNotice { - fn ui_name() -> &'static str { - "JoiningProjectWindow" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.workspace; - - Flex::column() - .with_children(self.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.joining_project_avatar) - .aligned() - .boxed() - })) - .with_child( - Text::new( - self.message.clone(), - theme.joining_project_message.text.clone(), - ) - .contained() - .with_style(theme.joining_project_message.container) - .aligned() - .boxed(), - ) - .aligned() - .contained() - .with_background_color(theme.background) - .boxed() - } - } - for window_id in cx.window_ids().collect::>() { if let Some(workspace) = cx.root_view::(window_id) { if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) { - return Task::ready(Ok(workspace)); + cx.activate_window(window_id); + return; } } } - let app_state = app_state.clone(); - cx.spawn(|mut cx| async move { - let (window, joining_notice) = cx.update(|cx| { - cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { - avatar: contact.user.avatar.clone(), - message: format!( - "Asking to join @{}'s copy of {}...", - contact.user.github_login, - humanize_list(&contact.projects[project_index].worktree_root_names) - ), - }) - }); - let project = Project::remote( - project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - &mut cx, - ) - .await; - - cx.update(|cx| match project { - Ok(project) => Ok(cx.replace_root_view(window, |cx| { - let mut workspace = (app_state.build_workspace)(project, &app_state, cx); - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); - workspace - })), - Err(error @ _) => { - let login = &contact.user.github_login; - let message = match error { - project::JoinProjectError::HostDeclined => { - format!("@{} declined your request.", login) - } - project::JoinProjectError::HostClosedProject => { - format!( - "@{} closed their copy of {}.", - login, - humanize_list(&contact.projects[project_index].worktree_root_names) - ) - } - project::JoinProjectError::HostWentOffline => { - format!("@{} went offline.", login) - } - project::JoinProjectError::Other(_) => "An error occurred.".to_string(), - }; - joining_notice.update(cx, |notice, cx| { - notice.message = message; - cx.notify(); - }); - - Err(error)? - } - }) - }) + cx.add_window((app_state.build_window_options)(), |cx| { + WaitingRoom::new(contact, project_index, app_state.clone(), cx) + }); } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { @@ -2408,18 +2311,3 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { }); cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone())); } - -fn humanize_list<'a>(items: impl IntoIterator) -> String { - let mut list = String::new(); - let mut items = items.into_iter().enumerate().peekable(); - while let Some((ix, item)) = items.next() { - if ix > 0 { - list.push_str(", "); - } - if items.peek().is_none() { - list.push_str("and "); - } - list.push_str(item); - } - list -} From cc598a6f71802552fca248789e9258434bb050b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 11:10:56 +0200 Subject: [PATCH 19/26] Send `LeaveProject` when waiting room is dismissed while waiting --- crates/contacts_panel/src/contacts_panel.rs | 8 +- .../src/join_project_notification.rs | 10 +- crates/workspace/src/waiting_room.rs | 118 ++++++++++-------- 3 files changed, 82 insertions(+), 54 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index a33739293fbcbb3c494e1c4d52d86af485f3cea9..91794ebf0f379fd26750ddd79a3d0e0e2431d291 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -133,8 +133,12 @@ impl ContactsPanel { if let Some(workspace) = workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace.show_notification( - cx.add_view(|_| { - JoinProjectNotification::new(project, user.clone()) + cx.add_view(|cx| { + JoinProjectNotification::new( + project, + user.clone(), + cx, + ) }), cx, ) diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs index 8e55396e3e31b2f77f329dbd48eb8cd72b8b135f..d8e8e670cff02da81a5b6c4afc6754d88661f2f2 100644 --- a/crates/contacts_panel/src/join_project_notification.rs +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -25,7 +25,15 @@ pub struct JoinProjectNotification { } impl JoinProjectNotification { - pub fn new(project: ModelHandle, user: Arc) -> Self { + pub fn new(project: ModelHandle, user: Arc, cx: &mut ViewContext) -> Self { + cx.subscribe(&project, |this, _, event, cx| { + if let project::Event::ContactCancelledJoinRequest(user) = event { + if *user == this.user { + cx.emit(Event::Dismiss); + } + } + }) + .detach(); Self { project, user } } diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index 17efebce39c71e067a062cef32d1220a62522a84..a8b717abb9f89f57639c38d5f864adfd69d7893a 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/crates/workspace/src/waiting_room.rs @@ -3,25 +3,35 @@ use crate::{ AppState, }; use anyhow::Result; -use client::Contact; -use gpui::{elements::*, ElementBox, Entity, ImageData, RenderContext, Task, View, ViewContext}; +use client::{proto, Client, Contact}; +use gpui::{ + elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View, + ViewContext, +}; use project::Project; use settings::Settings; use std::sync::Arc; +use util::ResultExt; pub struct WaitingRoom { + project_id: u64, avatar: Option>, message: String, - joined: bool, + waiting: bool, + client: Arc, _join_task: Task>, } impl Entity for WaitingRoom { type Event = (); - fn release(&mut self, _: &mut gpui::MutableAppContext) { - if !self.joined { - // TODO: Cancel the join request + fn release(&mut self, _: &mut MutableAppContext) { + if self.waiting { + self.client + .send(proto::LeaveProject { + project_id: self.project_id, + }) + .log_err(); } } } @@ -66,7 +76,7 @@ impl WaitingRoom { cx: &mut ViewContext, ) -> Self { let project_id = contact.projects[project_index].id; - + let client = app_state.client.clone(); let _join_task = cx.spawn_weak({ let contact = contact.clone(); |this, mut cx| async move { @@ -81,47 +91,50 @@ impl WaitingRoom { .await; if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| match project { - Ok(project) => { - this.joined = true; - cx.replace_root_view(|cx| { - let mut workspace = - (app_state.build_workspace)(project, &app_state, cx); - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); - workspace - }); - } - Err(error @ _) => { - let login = &contact.user.github_login; - let message = match error { - project::JoinProjectError::HostDeclined => { - format!("@{} declined your request.", login) - } - project::JoinProjectError::HostClosedProject => { - format!( - "@{} closed their copy of {}.", - login, - humanize_list( - &contact.projects[project_index].worktree_root_names + this.update(&mut cx, |this, cx| { + this.waiting = false; + match project { + Ok(project) => { + cx.replace_root_view(|cx| { + let mut workspace = + (app_state.build_workspace)(project, &app_state, cx); + workspace.toggle_sidebar_item( + &ToggleSidebarItem { + side: Side::Left, + item_index: 0, + }, + cx, + ); + workspace + }); + } + Err(error @ _) => { + let login = &contact.user.github_login; + let message = match error { + project::JoinProjectError::HostDeclined => { + format!("@{} declined your request.", login) + } + project::JoinProjectError::HostClosedProject => { + format!( + "@{} closed their copy of {}.", + login, + humanize_list( + &contact.projects[project_index] + .worktree_root_names + ) ) - ) - } - project::JoinProjectError::HostWentOffline => { - format!("@{} went offline.", login) - } - project::JoinProjectError::Other(error) => { - log::error!("error joining project: {}", error); - "An error occurred.".to_string() - } - }; - this.message = message; - cx.notify(); + } + project::JoinProjectError::HostWentOffline => { + format!("@{} went offline.", login) + } + project::JoinProjectError::Other(error) => { + log::error!("error joining project: {}", error); + "An error occurred.".to_string() + } + }; + this.message = message; + cx.notify(); + } } }) } @@ -131,13 +144,15 @@ impl WaitingRoom { }); Self { + project_id, avatar: contact.user.avatar.clone(), message: format!( "Asking to join @{}'s copy of {}...", contact.user.github_login, humanize_list(&contact.projects[project_index].worktree_root_names) ), - joined: false, + waiting: true, + client, _join_task, } } @@ -149,10 +164,11 @@ fn humanize_list<'a>(items: impl IntoIterator) -> String { while let Some((ix, item)) = items.next() { if ix > 0 { list.push_str(", "); + if items.peek().is_none() { + list.push_str("and "); + } } - if items.peek().is_none() { - list.push_str("and "); - } + list.push_str(item); } list From e0c772db3e93d2ccd13c80e69ece4354cfd1dc7b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 13:04:38 +0200 Subject: [PATCH 20/26] Hold a weak handle to `Presenter` when dispatching events This ensures that the only strong reference to the presenter is held by `App`. This is important because we want to call `flush_effects` when removing a window and implicit drops of the `Presenter` would make that hard. Before this commit, if a rendered view contained strong handles to views and models, we would only drop them on the next `flush_effects`. This was manifesting itself in `Project`s not being released when closing their containing window. --- crates/gpui/src/app.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index fc19b1422ae8d8705249965bf83e166192d7e72f..313a90c61dd7c0a7e96f9fd35ecfbbee11c6f695 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1635,20 +1635,22 @@ impl MutableAppContext { { let mut app = self.upgrade(); - let presenter = presenter.clone(); + let presenter = Rc::downgrade(&presenter); window.on_event(Box::new(move |event| { app.update(|cx| { - if let Event::KeyDown { keystroke, .. } = &event { - if cx.dispatch_keystroke( - window_id, - presenter.borrow().dispatch_path(cx.as_ref()), - keystroke, - ) { - return; + if let Some(presenter) = presenter.upgrade() { + if let Event::KeyDown { keystroke, .. } = &event { + if cx.dispatch_keystroke( + window_id, + presenter.borrow().dispatch_path(cx.as_ref()), + keystroke, + ) { + return; + } } - } - presenter.borrow_mut().dispatch_event(event, cx); + presenter.borrow_mut().dispatch_event(event, cx); + } }) })); } From 7b161b81b5230765c85d1bb3c18f1af1a471369f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 13:21:20 +0200 Subject: [PATCH 21/26] WIP: accept to join requests if user is already participating There's a panic caused by `Store::check_invariants` that we still need to figure out. --- crates/collab/src/rpc.rs | 45 +++++++++++++++++++++++++++++++---- crates/project/src/project.rs | 18 ++++++++++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 28c863ef0ba77bc04b3021112213a5a680a7a635..fadc6a36427a9cd78cbaf1e36d7122b6416530fd 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1669,7 +1669,12 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_share_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_share_project( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, + ) { let (window_b, _) = cx_b.add_window(|_| EmptyView); let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); @@ -1701,6 +1706,9 @@ mod tests { cx, ) }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; let (worktree_a, _) = project_a .update(cx_a, |p, cx| { p.find_or_create_local_worktree("/a", true, cx) @@ -1776,14 +1784,41 @@ mod tests { // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) // .await; - // Dropping the client B's project removes client B from client A's collaborators. + // Client B can join again on a different window because they are already a participant. + let client_b2 = server.create_client(cx_b2, "user_b").await; + let project_b2 = Project::remote( + project_id, + client_b2.client.clone(), + client_b2.user_store.clone(), + lang_registry.clone(), + FakeFs::new(cx_b2.background()), + &mut cx_b2.to_async(), + ) + .await + .unwrap(); + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b.read_with(cx_b, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b2.read_with(cx_b2, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + + // Dropping client B's first project removes only that from client A's collaborators. cx_b.update(move |_| { drop(client_b.project.take()); drop(project_b); }); - project_a - .condition(&cx_a, |project, _| project.collaborators().is_empty()) - .await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); + project_b2.read_with(cx_b2, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 33e37dc900165a06fac3615ea50f19e6c7ea0ebb..e6eb2dcf771fd4f8ff8924f60145834e6b2a768c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3805,11 +3805,19 @@ impl Project { mut cx: AsyncAppContext, ) -> Result<()> { let user_id = message.payload.requester_id; - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); - let user = user_store - .update(&mut cx, |store, cx| store.fetch_user(user_id, cx)) - .await?; - this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); + if this.read_with(&cx, |project, _| { + project.collaborators.values().any(|c| c.user.id == user_id) + }) { + this.update(&mut cx, |this, cx| { + this.respond_to_join_request(user_id, true, cx) + }); + } else { + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user = user_store + .update(&mut cx, |store, cx| store.fetch_user(user_id, cx)) + .await?; + this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); + } Ok(()) } From a828282771720282ca181084f7f48adf5cfc121b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 14:50:00 +0200 Subject: [PATCH 22/26] Fix `Store::remove_connection` not removing guests from projects --- crates/collab/src/rpc/store.rs | 45 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 80dc9f74e9f211a4f1968d48f98d750879cc947a..33fa1eb11396fd35fbe6a185593c1625b40a3721 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -2,7 +2,7 @@ use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}; use rpc::{proto, ConnectionId, Receipt}; -use std::{collections::hash_map, path::PathBuf}; +use std::{collections::hash_map, mem, path::PathBuf}; use tracing::instrument; #[derive(Default)] @@ -112,30 +112,25 @@ impl Store { &mut self, connection_id: ConnectionId, ) -> Result { - let connection = if let Some(connection) = self.connections.remove(&connection_id) { - connection - } else { - return Err(anyhow!("no such connection"))?; - }; + let connection = self + .connections + .get_mut(&connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; - for channel_id in &connection.channels { - if let Some(channel) = self.channels.get_mut(&channel_id) { - channel.connection_ids.remove(&connection_id); - } - } + let user_id = connection.user_id; + let connection_projects = mem::take(&mut connection.projects); + let connection_channels = mem::take(&mut connection.channels); - let user_connections = self - .connections_by_user_id - .get_mut(&connection.user_id) - .unwrap(); - user_connections.remove(&connection_id); - if user_connections.is_empty() { - self.connections_by_user_id.remove(&connection.user_id); + let mut result = RemovedConnectionState::default(); + result.user_id = user_id; + + // Leave all channels. + for channel_id in connection_channels { + self.leave_channel(connection_id, channel_id); } - let mut result = RemovedConnectionState::default(); - result.user_id = connection.user_id; - for project_id in connection.projects.clone() { + // Unregister and leave all projects. + for project_id in connection_projects { if let Ok(project) = self.unregister_project(project_id, connection_id) { result.hosted_projects.insert(project_id, project); } else if self.leave_project(connection_id, project_id).is_ok() { @@ -143,6 +138,14 @@ impl Store { } } + let user_connections = self.connections_by_user_id.get_mut(&user_id).unwrap(); + user_connections.remove(&connection_id); + if user_connections.is_empty() { + self.connections_by_user_id.remove(&user_id); + } + + self.connections.remove(&connection_id).unwrap(); + Ok(result) } From 8393bfe032698cc5d5fee4c45fd2efc5695a7eb1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 14:50:28 +0200 Subject: [PATCH 23/26] Ensure join request reaches the server before disconnecting host in test --- crates/collab/src/rpc.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fadc6a36427a9cd78cbaf1e36d7122b6416530fd..93db9f4d06d63def1ecf954fef9880e229626fe8 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1895,6 +1895,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_host_disconnect( + deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, @@ -1973,6 +1974,7 @@ mod tests { .await } }); + deterministic.run_until_parked(); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.disconnect_client(client_a.current_user_id(cx_a)); From 8f8880369511ef5d0c781766639637ed89e5dceb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 15:35:57 +0200 Subject: [PATCH 24/26] Automatically follow host when joining a project --- crates/workspace/src/waiting_room.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index a8b717abb9f89f57639c38d5f864adfd69d7893a..fef7bf2e4332ebfe94c96bafd6fefd3706cd7f13 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/crates/workspace/src/waiting_room.rs @@ -1,6 +1,6 @@ use crate::{ sidebar::{Side, ToggleSidebarItem}, - AppState, + AppState, ToggleFollow, }; use anyhow::Result; use client::{proto, Client, Contact}; @@ -96,8 +96,11 @@ impl WaitingRoom { match project { Ok(project) => { cx.replace_root_view(|cx| { - let mut workspace = - (app_state.build_workspace)(project, &app_state, cx); + let mut workspace = (app_state.build_workspace)( + project.clone(), + &app_state, + cx, + ); workspace.toggle_sidebar_item( &ToggleSidebarItem { side: Side::Left, @@ -105,6 +108,18 @@ impl WaitingRoom { }, cx, ); + if let Some((host_peer_id, _)) = project + .read(cx) + .collaborators() + .iter() + .find(|(_, collaborator)| collaborator.replica_id == 0) + { + if let Some(follow) = workspace + .toggle_follow(&ToggleFollow(*host_peer_id), cx) + { + follow.detach_and_log_err(cx); + } + } workspace }); } From 692be10b10184d9e6a3287786050691b6476a26c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 16:08:14 +0200 Subject: [PATCH 25/26] Dynamically load all themes listed under `styles/src/themes/*.ts` Co-Authored-By: Nathan Sobo --- styles/dist/dark.json | 531 ---------- styles/dist/light.json | 531 ---------- styles/dist/tokens.json | 1038 +++++++++++++++++++ styles/src/buildThemes.ts | 10 +- styles/src/buildTokens.ts | 10 +- styles/src/styleTree/app.ts | 2 +- styles/src/styleTree/chatPanel.ts | 2 +- styles/src/styleTree/commandPalette.ts | 2 +- styles/src/styleTree/components.ts | 2 +- styles/src/styleTree/contactFinder.ts | 2 +- styles/src/styleTree/contactNotification.ts | 2 +- styles/src/styleTree/contactsPanel.ts | 2 +- styles/src/styleTree/editor.ts | 2 +- styles/src/styleTree/picker.ts | 2 +- styles/src/styleTree/projectDiagnostics.ts | 2 +- styles/src/styleTree/projectPanel.ts | 2 +- styles/src/styleTree/search.ts | 2 +- styles/src/styleTree/statusBar.ts | 2 +- styles/src/styleTree/workspace.ts | 2 +- styles/src/themes.ts | 16 + styles/src/themes/cave.ts | 2 +- styles/src/themes/{ => common}/base16.ts | 4 +- styles/src/themes/{ => common}/theme.ts | 4 +- styles/src/themes/gruvbox.ts | 30 - styles/src/themes/solarized.ts | 2 +- styles/src/themes/sulphurpool.ts | 2 +- 26 files changed, 1081 insertions(+), 1127 deletions(-) delete mode 100644 styles/dist/dark.json delete mode 100644 styles/dist/light.json create mode 100644 styles/src/themes.ts rename styles/src/themes/{ => common}/base16.ts (99%) rename styles/src/themes/{ => common}/theme.ts (96%) delete mode 100644 styles/src/themes/gruvbox.ts diff --git a/styles/dist/dark.json b/styles/dist/dark.json deleted file mode 100644 index cf780140dd115f1a85553761c186365420ad2293..0000000000000000000000000000000000000000 --- a/styles/dist/dark.json +++ /dev/null @@ -1,531 +0,0 @@ -{ - "meta": { - "themeName": "cave-dark" - }, - "text": { - "primary": { - "value": "#e2dfe7", - "type": "color" - }, - "secondary": { - "value": "#8b8792", - "type": "color" - }, - "muted": { - "value": "#8b8792", - "type": "color" - }, - "placeholder": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "#e2dfe7", - "type": "color" - }, - "secondary": { - "value": "#8b8792", - "type": "color" - }, - "muted": { - "value": "#8b8792", - "type": "color" - }, - "placeholder": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "background": { - "100": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852605c", - "type": "color" - } - }, - "300": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852605c", - "type": "color" - } - }, - "500": { - "base": { - "value": "#19171c", - "type": "color" - }, - "hovered": { - "value": "#26232a3d", - "type": "color" - }, - "active": { - "value": "#26232a5c", - "type": "color" - } - }, - "on300": { - "base": { - "value": "#19171c", - "type": "color" - }, - "hovered": { - "value": "#26232a3d", - "type": "color" - }, - "active": { - "value": "#26232a7a", - "type": "color" - } - }, - "on500": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852607a", - "type": "color" - } - }, - "ok": { - "base": { - "value": "#2a929226", - "type": "color" - }, - "hovered": { - "value": "#2a929233", - "type": "color" - }, - "active": { - "value": "#2a929240", - "type": "color" - } - }, - "error": { - "base": { - "value": "#be467826", - "type": "color" - }, - "hovered": { - "value": "#be467833", - "type": "color" - }, - "active": { - "value": "#be467840", - "type": "color" - } - }, - "warning": { - "base": { - "value": "#a06e3b26", - "type": "color" - }, - "hovered": { - "value": "#a06e3b33", - "type": "color" - }, - "active": { - "value": "#a06e3b40", - "type": "color" - } - }, - "info": { - "base": { - "value": "#576ddb26", - "type": "color" - }, - "hovered": { - "value": "#576ddb33", - "type": "color" - }, - "active": { - "value": "#576ddb40", - "type": "color" - } - } - }, - "border": { - "primary": { - "value": "#19171c", - "type": "color" - }, - "secondary": { - "value": "#26232a", - "type": "color" - }, - "muted": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#655f6d", - "type": "color" - }, - "onMedia": { - "value": "#19171c1a", - "type": "color" - }, - "ok": { - "value": "#2a929226", - "type": "color" - }, - "error": { - "value": "#be467826", - "type": "color" - }, - "warning": { - "value": "#a06e3b26", - "type": "color" - }, - "info": { - "value": "#576ddb26", - "type": "color" - } - }, - "editor": { - "background": { - "value": "#19171c", - "type": "color" - }, - "indent_guide": { - "value": "#655f6d", - "type": "color" - }, - "indent_guide_active": { - "value": "#26232a", - "type": "color" - }, - "line": { - "active": { - "value": "#efecf412", - "type": "color" - }, - "highlighted": { - "value": "#efecf41f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", - "type": "color" - } - }, - "highlight": { - "selection": { - "value": "#576ddb3d", - "type": "color" - }, - "occurrence": { - "value": "#efecf41f", - "type": "color" - }, - "activeOccurrence": { - "value": "#efecf43d", - "type": "color" - }, - "matchingBracket": { - "value": "#26232a5c", - "type": "color" - }, - "match": { - "value": "#955ae77a", - "type": "color" - }, - "activeMatch": { - "value": "#955ae7b8", - "type": "color" - }, - "related": { - "value": "#26232a3d", - "type": "color" - } - }, - "gutter": { - "primary": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - } - } - }, - "syntax": { - "primary": { - "value": "#efecf4", - "type": "color" - }, - "comment": { - "value": "#8b8792", - "type": "color" - }, - "keyword": { - "value": "#576ddb", - "type": "color" - }, - "function": { - "value": "#a06e3b", - "type": "color" - }, - "type": { - "value": "#398bc6", - "type": "color" - }, - "variant": { - "value": "#576ddb", - "type": "color" - }, - "property": { - "value": "#576ddb", - "type": "color" - }, - "enum": { - "value": "#aa573c", - "type": "color" - }, - "operator": { - "value": "#aa573c", - "type": "color" - }, - "string": { - "value": "#aa573c", - "type": "color" - }, - "number": { - "value": "#2a9292", - "type": "color" - }, - "boolean": { - "value": "#2a9292", - "type": "color" - } - }, - "player": { - "1": { - "baseColor": { - "value": "#576ddb", - "type": "color" - }, - "cursorColor": { - "value": "#576ddb", - "type": "color" - }, - "selectionColor": { - "value": "#576ddb3d", - "type": "color" - }, - "borderColor": { - "value": "#576ddbcc", - "type": "color" - } - }, - "2": { - "baseColor": { - "value": "#2a9292", - "type": "color" - }, - "cursorColor": { - "value": "#2a9292", - "type": "color" - }, - "selectionColor": { - "value": "#2a92923d", - "type": "color" - }, - "borderColor": { - "value": "#2a9292cc", - "type": "color" - } - }, - "3": { - "baseColor": { - "value": "#bf40bf", - "type": "color" - }, - "cursorColor": { - "value": "#bf40bf", - "type": "color" - }, - "selectionColor": { - "value": "#bf40bf3d", - "type": "color" - }, - "borderColor": { - "value": "#bf40bfcc", - "type": "color" - } - }, - "4": { - "baseColor": { - "value": "#aa573c", - "type": "color" - }, - "cursorColor": { - "value": "#aa573c", - "type": "color" - }, - "selectionColor": { - "value": "#aa573c3d", - "type": "color" - }, - "borderColor": { - "value": "#aa573ccc", - "type": "color" - } - }, - "5": { - "baseColor": { - "value": "#955ae7", - "type": "color" - }, - "cursorColor": { - "value": "#955ae7", - "type": "color" - }, - "selectionColor": { - "value": "#955ae73d", - "type": "color" - }, - "borderColor": { - "value": "#955ae7cc", - "type": "color" - } - }, - "6": { - "baseColor": { - "value": "#398bc6", - "type": "color" - }, - "cursorColor": { - "value": "#398bc6", - "type": "color" - }, - "selectionColor": { - "value": "#398bc63d", - "type": "color" - }, - "borderColor": { - "value": "#398bc6cc", - "type": "color" - } - }, - "7": { - "baseColor": { - "value": "#be4678", - "type": "color" - }, - "cursorColor": { - "value": "#be4678", - "type": "color" - }, - "selectionColor": { - "value": "#be46783d", - "type": "color" - }, - "borderColor": { - "value": "#be4678cc", - "type": "color" - } - }, - "8": { - "baseColor": { - "value": "#a06e3b", - "type": "color" - }, - "cursorColor": { - "value": "#a06e3b", - "type": "color" - }, - "selectionColor": { - "value": "#a06e3b3d", - "type": "color" - }, - "borderColor": { - "value": "#a06e3bcc", - "type": "color" - } - } - }, - "shadowAlpha": { - "value": 0.24, - "type": "number" - } -} \ No newline at end of file diff --git a/styles/dist/light.json b/styles/dist/light.json deleted file mode 100644 index 1287dea481fc0ff6f1bc33e7e1e0ecff065badaa..0000000000000000000000000000000000000000 --- a/styles/dist/light.json +++ /dev/null @@ -1,531 +0,0 @@ -{ - "meta": { - "themeName": "cave-light" - }, - "text": { - "primary": { - "value": "#26232a", - "type": "color" - }, - "secondary": { - "value": "#585260", - "type": "color" - }, - "muted": { - "value": "#585260", - "type": "color" - }, - "placeholder": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "#26232a", - "type": "color" - }, - "secondary": { - "value": "#585260", - "type": "color" - }, - "muted": { - "value": "#585260", - "type": "color" - }, - "placeholder": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "background": { - "100": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87922e", - "type": "color" - } - }, - "300": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87922e", - "type": "color" - } - }, - "500": { - "base": { - "value": "#efecf4", - "type": "color" - }, - "hovered": { - "value": "#e2dfe71f", - "type": "color" - }, - "active": { - "value": "#e2dfe72e", - "type": "color" - } - }, - "on300": { - "base": { - "value": "#efecf4", - "type": "color" - }, - "hovered": { - "value": "#e2dfe71f", - "type": "color" - }, - "active": { - "value": "#e2dfe73d", - "type": "color" - } - }, - "on500": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87923d", - "type": "color" - } - }, - "ok": { - "base": { - "value": "#2a929226", - "type": "color" - }, - "hovered": { - "value": "#2a929233", - "type": "color" - }, - "active": { - "value": "#2a929240", - "type": "color" - } - }, - "error": { - "base": { - "value": "#be467826", - "type": "color" - }, - "hovered": { - "value": "#be467833", - "type": "color" - }, - "active": { - "value": "#be467840", - "type": "color" - } - }, - "warning": { - "base": { - "value": "#a06e3b26", - "type": "color" - }, - "hovered": { - "value": "#a06e3b33", - "type": "color" - }, - "active": { - "value": "#a06e3b40", - "type": "color" - } - }, - "info": { - "base": { - "value": "#576ddb26", - "type": "color" - }, - "hovered": { - "value": "#576ddb33", - "type": "color" - }, - "active": { - "value": "#576ddb40", - "type": "color" - } - } - }, - "border": { - "primary": { - "value": "#efecf4", - "type": "color" - }, - "secondary": { - "value": "#e2dfe7", - "type": "color" - }, - "muted": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#7e7887", - "type": "color" - }, - "onMedia": { - "value": "#efecf41a", - "type": "color" - }, - "ok": { - "value": "#2a929226", - "type": "color" - }, - "error": { - "value": "#be467826", - "type": "color" - }, - "warning": { - "value": "#a06e3b26", - "type": "color" - }, - "info": { - "value": "#576ddb26", - "type": "color" - } - }, - "editor": { - "background": { - "value": "#efecf4", - "type": "color" - }, - "indent_guide": { - "value": "#7e7887", - "type": "color" - }, - "indent_guide_active": { - "value": "#e2dfe7", - "type": "color" - }, - "line": { - "active": { - "value": "#19171c12", - "type": "color" - }, - "highlighted": { - "value": "#19171c1f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", - "type": "color" - } - }, - "highlight": { - "selection": { - "value": "#576ddb3d", - "type": "color" - }, - "occurrence": { - "value": "#19171c0f", - "type": "color" - }, - "activeOccurrence": { - "value": "#19171c1f", - "type": "color" - }, - "matchingBracket": { - "value": "#e2dfe72e", - "type": "color" - }, - "match": { - "value": "#955ae73d", - "type": "color" - }, - "activeMatch": { - "value": "#955ae75c", - "type": "color" - }, - "related": { - "value": "#e2dfe71f", - "type": "color" - } - }, - "gutter": { - "primary": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - } - } - }, - "syntax": { - "primary": { - "value": "#19171c", - "type": "color" - }, - "comment": { - "value": "#585260", - "type": "color" - }, - "keyword": { - "value": "#576ddb", - "type": "color" - }, - "function": { - "value": "#a06e3b", - "type": "color" - }, - "type": { - "value": "#398bc6", - "type": "color" - }, - "variant": { - "value": "#576ddb", - "type": "color" - }, - "property": { - "value": "#576ddb", - "type": "color" - }, - "enum": { - "value": "#aa573c", - "type": "color" - }, - "operator": { - "value": "#aa573c", - "type": "color" - }, - "string": { - "value": "#aa573c", - "type": "color" - }, - "number": { - "value": "#2a9292", - "type": "color" - }, - "boolean": { - "value": "#2a9292", - "type": "color" - } - }, - "player": { - "1": { - "baseColor": { - "value": "#576ddb", - "type": "color" - }, - "cursorColor": { - "value": "#576ddb", - "type": "color" - }, - "selectionColor": { - "value": "#576ddb3d", - "type": "color" - }, - "borderColor": { - "value": "#576ddbcc", - "type": "color" - } - }, - "2": { - "baseColor": { - "value": "#2a9292", - "type": "color" - }, - "cursorColor": { - "value": "#2a9292", - "type": "color" - }, - "selectionColor": { - "value": "#2a92923d", - "type": "color" - }, - "borderColor": { - "value": "#2a9292cc", - "type": "color" - } - }, - "3": { - "baseColor": { - "value": "#bf40bf", - "type": "color" - }, - "cursorColor": { - "value": "#bf40bf", - "type": "color" - }, - "selectionColor": { - "value": "#bf40bf3d", - "type": "color" - }, - "borderColor": { - "value": "#bf40bfcc", - "type": "color" - } - }, - "4": { - "baseColor": { - "value": "#aa573c", - "type": "color" - }, - "cursorColor": { - "value": "#aa573c", - "type": "color" - }, - "selectionColor": { - "value": "#aa573c3d", - "type": "color" - }, - "borderColor": { - "value": "#aa573ccc", - "type": "color" - } - }, - "5": { - "baseColor": { - "value": "#955ae7", - "type": "color" - }, - "cursorColor": { - "value": "#955ae7", - "type": "color" - }, - "selectionColor": { - "value": "#955ae73d", - "type": "color" - }, - "borderColor": { - "value": "#955ae7cc", - "type": "color" - } - }, - "6": { - "baseColor": { - "value": "#398bc6", - "type": "color" - }, - "cursorColor": { - "value": "#398bc6", - "type": "color" - }, - "selectionColor": { - "value": "#398bc63d", - "type": "color" - }, - "borderColor": { - "value": "#398bc6cc", - "type": "color" - } - }, - "7": { - "baseColor": { - "value": "#be4678", - "type": "color" - }, - "cursorColor": { - "value": "#be4678", - "type": "color" - }, - "selectionColor": { - "value": "#be46783d", - "type": "color" - }, - "borderColor": { - "value": "#be4678cc", - "type": "color" - } - }, - "8": { - "baseColor": { - "value": "#a06e3b", - "type": "color" - }, - "cursorColor": { - "value": "#a06e3b", - "type": "color" - }, - "selectionColor": { - "value": "#a06e3b3d", - "type": "color" - }, - "borderColor": { - "value": "#a06e3bcc", - "type": "color" - } - } - }, - "shadowAlpha": { - "value": 0.12, - "type": "number" - } -} \ No newline at end of file diff --git a/styles/dist/tokens.json b/styles/dist/tokens.json index acf9fa0c0a9a916561b3a4eae462774266b28533..5874a9a7c19f2aca2cd83a9da5468415a9648db3 100644 --- a/styles/dist/tokens.json +++ b/styles/dist/tokens.json @@ -3255,5 +3255,1043 @@ "value": 0.12, "type": "number" } + }, + "sulphurpool-dark": { + "meta": { + "themeName": "sulphurpool-dark" + }, + "text": { + "primary": { + "value": "#dfe2f1", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#979db4", + "type": "color" + }, + "placeholder": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#dfe2f1", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#979db4", + "type": "color" + }, + "placeholder": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#363f62", + "type": "color" + }, + "hovered": { + "value": "#444c6f", + "type": "color" + }, + "active": { + "value": "#51597b", + "type": "color" + } + }, + "300": { + "base": { + "value": "#293256", + "type": "color" + }, + "hovered": { + "value": "#363f62", + "type": "color" + }, + "active": { + "value": "#444c6f", + "type": "color" + } + }, + "500": { + "base": { + "value": "#202746", + "type": "color" + }, + "hovered": { + "value": "#222a4a", + "type": "color" + }, + "active": { + "value": "#252d4e", + "type": "color" + } + }, + "on300": { + "base": { + "value": "#202746", + "type": "color" + }, + "hovered": { + "value": "#222a4a", + "type": "color" + }, + "active": { + "value": "#252d4e", + "type": "color" + } + }, + "on500": { + "base": { + "value": "#363f62", + "type": "color" + }, + "hovered": { + "value": "#444c6f", + "type": "color" + }, + "active": { + "value": "#51597b", + "type": "color" + } + }, + "ok": { + "base": { + "value": "#ac973926", + "type": "color" + }, + "hovered": { + "value": "#ac973933", + "type": "color" + }, + "active": { + "value": "#ac973940", + "type": "color" + } + }, + "error": { + "base": { + "value": "#c9492226", + "type": "color" + }, + "hovered": { + "value": "#c9492233", + "type": "color" + }, + "active": { + "value": "#c9492240", + "type": "color" + } + }, + "warning": { + "base": { + "value": "#c08b3026", + "type": "color" + }, + "hovered": { + "value": "#c08b3033", + "type": "color" + }, + "active": { + "value": "#c08b3040", + "type": "color" + } + }, + "info": { + "base": { + "value": "#3d8fd126", + "type": "color" + }, + "hovered": { + "value": "#3d8fd133", + "type": "color" + }, + "active": { + "value": "#3d8fd140", + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#202746", + "type": "color" + }, + "secondary": { + "value": "#293256", + "type": "color" + }, + "muted": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#6b7394", + "type": "color" + }, + "onMedia": { + "value": "#2027461a", + "type": "color" + }, + "ok": { + "value": "#ac973926", + "type": "color" + }, + "error": { + "value": "#c9492226", + "type": "color" + }, + "warning": { + "value": "#c08b3026", + "type": "color" + }, + "info": { + "value": "#3d8fd126", + "type": "color" + } + }, + "editor": { + "background": { + "value": "#202746", + "type": "color" + }, + "indent_guide": { + "value": "#6b7394", + "type": "color" + }, + "indent_guide_active": { + "value": "#293256", + "type": "color" + }, + "line": { + "active": { + "value": "#293256", + "type": "color" + }, + "highlighted": { + "value": "#363f62", + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#3d8fd13d", + "type": "color" + }, + "occurrence": { + "value": "#5e66873d", + "type": "color" + }, + "activeOccurrence": { + "value": "#5e66877a", + "type": "color" + }, + "matchingBracket": { + "value": "#252d4e", + "type": "color" + }, + "match": { + "value": "#1a2a6d", + "type": "color" + }, + "activeMatch": { + "value": "#3d56c47a", + "type": "color" + }, + "related": { + "value": "#222a4a", + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#f5f7ff", + "type": "color" + }, + "comment": { + "value": "#979db4", + "type": "color" + }, + "keyword": { + "value": "#3d8fd1", + "type": "color" + }, + "function": { + "value": "#c08b30", + "type": "color" + }, + "type": { + "value": "#22a2c9", + "type": "color" + }, + "variant": { + "value": "#3d8fd1", + "type": "color" + }, + "property": { + "value": "#3d8fd1", + "type": "color" + }, + "enum": { + "value": "#c76b29", + "type": "color" + }, + "operator": { + "value": "#c76b29", + "type": "color" + }, + "string": { + "value": "#c76b29", + "type": "color" + }, + "number": { + "value": "#ac9739", + "type": "color" + }, + "boolean": { + "value": "#ac9739", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#3d8fd1", + "type": "color" + }, + "cursorColor": { + "value": "#3d8fd1", + "type": "color" + }, + "selectionColor": { + "value": "#3d8fd13d", + "type": "color" + }, + "borderColor": { + "value": "#3d8fd1cc", + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#ac9739", + "type": "color" + }, + "cursorColor": { + "value": "#ac9739", + "type": "color" + }, + "selectionColor": { + "value": "#ac97393d", + "type": "color" + }, + "borderColor": { + "value": "#ac9739cc", + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#9c637a", + "type": "color" + }, + "cursorColor": { + "value": "#9c637a", + "type": "color" + }, + "selectionColor": { + "value": "#9c637a3d", + "type": "color" + }, + "borderColor": { + "value": "#9c637acc", + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#c76b29", + "type": "color" + }, + "cursorColor": { + "value": "#c76b29", + "type": "color" + }, + "selectionColor": { + "value": "#c76b293d", + "type": "color" + }, + "borderColor": { + "value": "#c76b29cc", + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#6679cc", + "type": "color" + }, + "cursorColor": { + "value": "#6679cc", + "type": "color" + }, + "selectionColor": { + "value": "#6679cc3d", + "type": "color" + }, + "borderColor": { + "value": "#6679cccc", + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#22a2c9", + "type": "color" + }, + "cursorColor": { + "value": "#22a2c9", + "type": "color" + }, + "selectionColor": { + "value": "#22a2c93d", + "type": "color" + }, + "borderColor": { + "value": "#22a2c9cc", + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#c94922", + "type": "color" + }, + "cursorColor": { + "value": "#c94922", + "type": "color" + }, + "selectionColor": { + "value": "#c949223d", + "type": "color" + }, + "borderColor": { + "value": "#c94922cc", + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#c08b30", + "type": "color" + }, + "cursorColor": { + "value": "#c08b30", + "type": "color" + }, + "selectionColor": { + "value": "#c08b303d", + "type": "color" + }, + "borderColor": { + "value": "#c08b30cc", + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.24, + "type": "number" + } + }, + "sulphurpool-light": { + "meta": { + "themeName": "sulphurpool-light" + }, + "text": { + "primary": { + "value": "#293256", + "type": "color" + }, + "secondary": { + "value": "#5e6687", + "type": "color" + }, + "muted": { + "value": "#5e6687", + "type": "color" + }, + "placeholder": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#293256", + "type": "color" + }, + "secondary": { + "value": "#5e6687", + "type": "color" + }, + "muted": { + "value": "#5e6687", + "type": "color" + }, + "placeholder": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#cdd1e2", + "type": "color" + }, + "hovered": { + "value": "#bbc0d3", + "type": "color" + }, + "active": { + "value": "#a9aec3", + "type": "color" + } + }, + "300": { + "base": { + "value": "#dfe2f1", + "type": "color" + }, + "hovered": { + "value": "#cdd1e2", + "type": "color" + }, + "active": { + "value": "#bbc0d3", + "type": "color" + } + }, + "500": { + "base": { + "value": "#f5f7ff", + "type": "color" + }, + "hovered": { + "value": "#f0f2fc", + "type": "color" + }, + "active": { + "value": "#eaedf8", + "type": "color" + } + }, + "on300": { + "base": { + "value": "#f5f7ff", + "type": "color" + }, + "hovered": { + "value": "#f0f2fc", + "type": "color" + }, + "active": { + "value": "#eaedf8", + "type": "color" + } + }, + "on500": { + "base": { + "value": "#cdd1e2", + "type": "color" + }, + "hovered": { + "value": "#bbc0d3", + "type": "color" + }, + "active": { + "value": "#a9aec3", + "type": "color" + } + }, + "ok": { + "base": { + "value": "#ac973926", + "type": "color" + }, + "hovered": { + "value": "#ac973933", + "type": "color" + }, + "active": { + "value": "#ac973940", + "type": "color" + } + }, + "error": { + "base": { + "value": "#c9492226", + "type": "color" + }, + "hovered": { + "value": "#c9492233", + "type": "color" + }, + "active": { + "value": "#c9492240", + "type": "color" + } + }, + "warning": { + "base": { + "value": "#c08b3026", + "type": "color" + }, + "hovered": { + "value": "#c08b3033", + "type": "color" + }, + "active": { + "value": "#c08b3040", + "type": "color" + } + }, + "info": { + "base": { + "value": "#3d8fd126", + "type": "color" + }, + "hovered": { + "value": "#3d8fd133", + "type": "color" + }, + "active": { + "value": "#3d8fd140", + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#979db4", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#6b7394", + "type": "color" + }, + "onMedia": { + "value": "#f5f7ff1a", + "type": "color" + }, + "ok": { + "value": "#ac973926", + "type": "color" + }, + "error": { + "value": "#c9492226", + "type": "color" + }, + "warning": { + "value": "#c08b3026", + "type": "color" + }, + "info": { + "value": "#3d8fd126", + "type": "color" + } + }, + "editor": { + "background": { + "value": "#f5f7ff", + "type": "color" + }, + "indent_guide": { + "value": "#6b7394", + "type": "color" + }, + "indent_guide_active": { + "value": "#979db4", + "type": "color" + }, + "line": { + "active": { + "value": "#dfe2f1", + "type": "color" + }, + "highlighted": { + "value": "#cdd1e2", + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#3d8fd13d", + "type": "color" + }, + "occurrence": { + "value": "#979db41f", + "type": "color" + }, + "activeOccurrence": { + "value": "#979db43d", + "type": "color" + }, + "matchingBracket": { + "value": "#eaedf8", + "type": "color" + }, + "match": { + "value": "#bcc6f7", + "type": "color" + }, + "activeMatch": { + "value": "#7b8ddc3d", + "type": "color" + }, + "related": { + "value": "#f0f2fc", + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#202746", + "type": "color" + }, + "comment": { + "value": "#5e6687", + "type": "color" + }, + "keyword": { + "value": "#3d8fd1", + "type": "color" + }, + "function": { + "value": "#c08b30", + "type": "color" + }, + "type": { + "value": "#22a2c9", + "type": "color" + }, + "variant": { + "value": "#3d8fd1", + "type": "color" + }, + "property": { + "value": "#3d8fd1", + "type": "color" + }, + "enum": { + "value": "#c76b29", + "type": "color" + }, + "operator": { + "value": "#c76b29", + "type": "color" + }, + "string": { + "value": "#c76b29", + "type": "color" + }, + "number": { + "value": "#ac9739", + "type": "color" + }, + "boolean": { + "value": "#ac9739", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#3d8fd1", + "type": "color" + }, + "cursorColor": { + "value": "#3d8fd1", + "type": "color" + }, + "selectionColor": { + "value": "#3d8fd13d", + "type": "color" + }, + "borderColor": { + "value": "#3d8fd1cc", + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#ac9739", + "type": "color" + }, + "cursorColor": { + "value": "#ac9739", + "type": "color" + }, + "selectionColor": { + "value": "#ac97393d", + "type": "color" + }, + "borderColor": { + "value": "#ac9739cc", + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#9c637a", + "type": "color" + }, + "cursorColor": { + "value": "#9c637a", + "type": "color" + }, + "selectionColor": { + "value": "#9c637a3d", + "type": "color" + }, + "borderColor": { + "value": "#9c637acc", + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#c76b29", + "type": "color" + }, + "cursorColor": { + "value": "#c76b29", + "type": "color" + }, + "selectionColor": { + "value": "#c76b293d", + "type": "color" + }, + "borderColor": { + "value": "#c76b29cc", + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#6679cc", + "type": "color" + }, + "cursorColor": { + "value": "#6679cc", + "type": "color" + }, + "selectionColor": { + "value": "#6679cc3d", + "type": "color" + }, + "borderColor": { + "value": "#6679cccc", + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#22a2c9", + "type": "color" + }, + "cursorColor": { + "value": "#22a2c9", + "type": "color" + }, + "selectionColor": { + "value": "#22a2c93d", + "type": "color" + }, + "borderColor": { + "value": "#22a2c9cc", + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#c94922", + "type": "color" + }, + "cursorColor": { + "value": "#c94922", + "type": "color" + }, + "selectionColor": { + "value": "#c949223d", + "type": "color" + }, + "borderColor": { + "value": "#c94922cc", + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#c08b30", + "type": "color" + }, + "cursorColor": { + "value": "#c08b30", + "type": "color" + }, + "selectionColor": { + "value": "#c08b303d", + "type": "color" + }, + "borderColor": { + "value": "#c08b30cc", + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.12, + "type": "number" + } } } \ No newline at end of file diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts index 69bd96f2c5d5c0726f9f1b0d472981f111c753fa..520a37395d5f1775ac37c465ec4f81ac5121b0ef 100644 --- a/styles/src/buildThemes.ts +++ b/styles/src/buildThemes.ts @@ -1,17 +1,9 @@ import * as fs from "fs"; import * as path from "path"; import app from "./styleTree/app"; -import { dark as caveDark, light as caveLight } from "./themes/cave"; -import { dark as solarizedDark, light as solarizedLight } from "./themes/solarized"; -import { dark as sulphurpoolDark, light as sulphurpoolLight } from "./themes/sulphurpool"; +import themes from "./themes"; import snakeCase from "./utils/snakeCase"; -const themes = [ - caveDark, caveLight, - solarizedDark, solarizedLight, - sulphurpoolDark, sulphurpoolLight -]; - const themeDirectory = `${__dirname}/../../assets/themes/`; // Clear existing themes diff --git a/styles/src/buildTokens.ts b/styles/src/buildTokens.ts index 27ea50d814ecfa6618ab4006eb02cb65c083fc85..04b4a6b7523f50dede166f539c9d5fb83e86af67 100644 --- a/styles/src/buildTokens.ts +++ b/styles/src/buildTokens.ts @@ -1,9 +1,7 @@ import * as fs from "fs"; import * as path from "path"; -import { light as solarizedLight, dark as solarizedDark } from "./themes/solarized"; -// Use cave as "light" and "dark" themes -import { light, dark } from "./themes/cave"; -import Theme from "./themes/theme"; +import themes from "./themes"; +import Theme from "./themes/common/theme"; import { colors, fontFamilies, fontSizes, fontWeights, sizes } from "./tokens"; // Organize theme tokens @@ -50,6 +48,9 @@ const coreTokens = { const combinedTokens: any = {}; const distPath = path.resolve(`${__dirname}/../dist`); +for (const file of fs.readdirSync(distPath)) { + fs.unlinkSync(path.join(distPath, file)); +} // Add core tokens to the combined tokens and write `core.json`. // We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. @@ -60,7 +61,6 @@ combinedTokens.core = coreTokens; // Add each theme to the combined tokens and write ${theme}.json. // We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. -let themes = [dark, light, solarizedDark, solarizedLight]; themes.forEach((theme) => { const themePath = `${distPath}/${theme.name}.json` fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2)); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 9822f6766e3e2a99731dfca0b34494892ec68ea9..230bd3e57f8eb31b5b602785026c6abe927d1ffe 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; import contactFinder from "./contactFinder"; diff --git a/styles/src/styleTree/chatPanel.ts b/styles/src/styleTree/chatPanel.ts index 69b5f3baa0d74c03e4ff5e0a81ff4fc6616afdfc..778cd6758480b337a9f3dd8d87caa0ad822687cc 100644 --- a/styles/src/styleTree/chatPanel.ts +++ b/styles/src/styleTree/chatPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts index 0dd290a91c8eeb379c07a2abd0a191ad8a755de1..7b94f1cfaee2a05908e3385c66b5355170fe0f45 100644 --- a/styles/src/styleTree/commandPalette.ts +++ b/styles/src/styleTree/commandPalette.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { text, backgroundColor, border } from "./components"; export default function commandPalette(theme: Theme) { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index d0412af02e6b7e3a11ef9aae978d846c4e6da551..2a265377508e81738a105af8a44ea3baf1c096c7 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import Theme, { BackgroundColorSet } from "../themes/theme"; +import Theme, { BackgroundColorSet } from "../themes/common/theme"; import { fontFamilies, fontSizes, FontWeight } from "../tokens"; import { Color } from "../utils/color"; diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 853f87ca5e654d8824309158b0e97360e1ebc1fc..fdefa34b8ad629a7fa5de5426e6f54e23b4b8160 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import picker from "./picker"; import { backgroundColor, iconColor } from "./components"; diff --git a/styles/src/styleTree/contactNotification.ts b/styles/src/styleTree/contactNotification.ts index 09360f2f9118876070a0700392f6fc2756b9e5c4..0ed9636399e2385c4ae2d3864c0b0d2f975f9ef3 100644 --- a/styles/src/styleTree/contactNotification.ts +++ b/styles/src/styleTree/contactNotification.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, iconColor, text } from "./components"; const avatarSize = 12; diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 421afd1967e97395e9df11ece337df51759c20ae..d55487a97b5ffe8a1973686b96c7d5809b0ec3cb 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 88e630b357f87c93b064f630671be227476295e0..06f0d98d70fab9ac3096591fc5a0769fdeac5007 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts index e783945dd1366023a9bff9d2dacf266586f86628..adb53458d3457e5e1e38776016c7e58a8da5bdca 100644 --- a/styles/src/styleTree/picker.ts +++ b/styles/src/styleTree/picker.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, player, shadow, text } from "./components"; export default function picker(theme: Theme) { diff --git a/styles/src/styleTree/projectDiagnostics.ts b/styles/src/styleTree/projectDiagnostics.ts index 7331df1f98e45480d5032a81f21fa80cdd2feaea..fe2d3e5f3b24da2d7120c4fe491817853741b831 100644 --- a/styles/src/styleTree/projectDiagnostics.ts +++ b/styles/src/styleTree/projectDiagnostics.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, text, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index bacc3590e552c5900e113825229574af6c82e98d..6892b666d9b37cab5c126ecfedb91feea9c335db 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, iconColor, player, text } from "./components"; diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts index b48dccc156149c2269ae45640cf66426cd760625..7febfb98b19cee4da8ebc17d32e2974bd4dda708 100644 --- a/styles/src/styleTree/search.ts +++ b/styles/src/styleTree/search.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, player, text } from "./components"; export default function search(theme: Theme) { diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index c7b7c6a0a35b3138e77d648b042344f98c507ff2..4b3fbaafeab1f71a083d3f3d3c689d5151f56690 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, iconColor, text } from "./components"; import { workspaceBackground } from "./workspace"; diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 110906868885c296de2769778b50c389cd09cb95..560049b4947d6eddff6f54815112f4615de480a1 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, iconColor, shadow, text } from "./components"; import statusBar from "./statusBar"; diff --git a/styles/src/themes.ts b/styles/src/themes.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef02fab8219fc3aa748436461fe08e916192dd5d --- /dev/null +++ b/styles/src/themes.ts @@ -0,0 +1,16 @@ +import fs from "fs"; +import path from "path"; +import Theme from "./themes/common/theme"; + +const themes: Theme[] = []; +export default themes; + +const themesPath = path.resolve(`${__dirname}/themes`); +for (const fileName of fs.readdirSync(themesPath)) { + const filePath = path.join(themesPath, fileName); + if (fs.statSync(filePath).isFile()) { + const theme = require(filePath); + themes.push(theme.dark); + themes.push(theme.light); + } +} diff --git a/styles/src/themes/cave.ts b/styles/src/themes/cave.ts index aa443dc83734dd92356b5b7cc778022d0f5e580b..f3c8102ac9ecc5d7b267a7f201cf73e47275cbc1 100644 --- a/styles/src/themes/cave.ts +++ b/styles/src/themes/cave.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "cave"; diff --git a/styles/src/themes/base16.ts b/styles/src/themes/common/base16.ts similarity index 99% rename from styles/src/themes/base16.ts rename to styles/src/themes/common/base16.ts index 812235072725e323a03ef4352539823a45dfc46d..7fd485b0c3cb38d8322c4e37550d5b54429cac94 100644 --- a/styles/src/themes/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -1,7 +1,7 @@ import chroma from "chroma-js"; import { Scale, Color } from "chroma-js"; -import { color, ColorToken, fontWeights, NumberToken } from "../tokens"; -import { withOpacity } from "../utils/color"; +import { color, ColorToken, fontWeights, NumberToken } from "../../tokens"; +import { withOpacity } from "../../utils/color"; import Theme, { buildPlayer, Syntax } from "./theme"; export function colorRamp(color: Color): Scale { diff --git a/styles/src/themes/theme.ts b/styles/src/themes/common/theme.ts similarity index 96% rename from styles/src/themes/theme.ts rename to styles/src/themes/common/theme.ts index 21eda88268841222e3d92d88a639152ea551dca5..2c648e87ec530d639e3bd77167b01632219c38ea 100644 --- a/styles/src/themes/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -1,5 +1,5 @@ -import { ColorToken, FontWeightToken, NumberToken } from "../tokens"; -import { withOpacity } from "../utils/color"; +import { ColorToken, FontWeightToken, NumberToken } from "../../tokens"; +import { withOpacity } from "../../utils/color"; export interface SyntaxHighlightStyle { color: ColorToken; diff --git a/styles/src/themes/gruvbox.ts b/styles/src/themes/gruvbox.ts deleted file mode 100644 index f0100fa3272135a36120b6c6a494ae0b8af6416b..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox.ts +++ /dev/null @@ -1,30 +0,0 @@ -import chroma from "chroma-js"; -import { createTheme } from "./base16"; - -const name = "cave"; - -const colors = { - "red": chroma("#be4678"), - "orange": chroma("#aa573c"), - "yellow": chroma("#a06e3b"), - "green": chroma("#2a9292"), - "cyan": chroma("#398bc6"), - "blue": chroma("#576ddb"), - "violet": chroma("#955ae7"), - "magenta": chroma("#bf40bf"), -}; - -const ramps = { - neutral: chroma.scale(["#19171c", "#26232a", "#585260", "#655f6d", "#7e7887", "#8b8792", "#e2dfe7", "#efecf4"]), - red: chroma.scale([colors.red.darken(3), colors.red, colors.red.brighten(3)]), - orange: chroma.scale([colors.orange.darken(3), colors.orange, colors.orange.brighten(3)]), - yellow: chroma.scale([colors.yellow.darken(3), colors.yellow, colors.yellow.brighten(3)]), - green: chroma.scale([colors.green.darken(3), colors.green, colors.green.brighten(3)]), - cyan: chroma.scale([colors.cyan.darken(3), colors.cyan, colors.cyan.brighten(3)]), - blue: chroma.scale([colors.blue.darken(3), colors.blue, colors.blue.brighten(3)]), - violet: chroma.scale([colors.violet.darken(3), colors.violet, colors.violet.brighten(3)]), - magenta: chroma.scale([colors.magenta.darken(3), colors.magenta, colors.magenta.brighten(3)]), -} - -export const dark = createTheme(`${name}-dark`, false, ramps); -export const light = createTheme(`${name}-light`, true, ramps); \ No newline at end of file diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized.ts index 2b9db833115e9c026c6b0be35bf08916a1011613..6992dc5bf8e86f30a67a9fde5c95b5b31ee489f2 100644 --- a/styles/src/themes/solarized.ts +++ b/styles/src/themes/solarized.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "solarized"; diff --git a/styles/src/themes/sulphurpool.ts b/styles/src/themes/sulphurpool.ts index 727f1ce38ab90b4267ab7b18e5604d6b929ddde5..202d52bc68532cae2e109cedaf8c0b91c7edfcd3 100644 --- a/styles/src/themes/sulphurpool.ts +++ b/styles/src/themes/sulphurpool.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "sulphurpool"; From 2d986c79689f62adfafd2db9bb400ac6256fe91d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 May 2022 16:21:09 +0200 Subject: [PATCH 26/26] Show guest only once even if they joined on two different windows Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/client/Cargo.toml | 4 +++- crates/client/src/user.rs | 26 ++++++++++++++------- crates/contacts_panel/src/contacts_panel.rs | 7 +----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b1f2f7f1b9bd45caf75fc8d6b53cda6fc6d2425..c12a15ec574b76ba7b16c8fc18f04beaf7e0fbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,7 @@ dependencies = [ "anyhow", "async-recursion", "async-tungstenite", + "collections", "futures", "gpui", "image", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index f4f85b64d07799a85711dcb6ffee382f8beeff5d..45e8547c2c0bd40705e6694c8c4af45ec67ed5c9 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -8,9 +8,10 @@ path = "src/client.rs" doctest = false [features] -test-support = ["gpui/test-support", "rpc/test-support"] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] [dependencies] +collections = { path = "../collections" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } @@ -33,5 +34,6 @@ tiny_http = "0.8" url = "2.2" [dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 296c0cf9ae1df4e37e5f229da3d1c71987dd3832..08092f46f1951f5ab717f65e4946a80c566c12cb 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,13 +1,11 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; +use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, - sync::{Arc, Weak}, -}; +use std::sync::{Arc, Weak}; use util::TryFutureExt as _; #[derive(Debug)] @@ -17,6 +15,18 @@ pub struct User { pub avatar: Option>, } +impl PartialOrd for User { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for User { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.github_login.cmp(&other.github_login) + } +} + impl PartialEq for User { fn eq(&self, other: &Self) -> bool { self.id == other.id && self.github_login == other.github_login @@ -36,7 +46,7 @@ pub struct Contact { pub struct ProjectMetadata { pub id: u64, pub worktree_root_names: Vec, - pub guests: Vec>, + pub guests: BTreeSet>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -177,7 +187,7 @@ impl UserStore { self.client.upgrade().unwrap().id, message ); - let mut user_ids = HashSet::new(); + let mut user_ids = HashSet::default(); for contact in &message.contacts { user_ids.insert(contact.user_id); user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); @@ -554,9 +564,9 @@ impl Contact { .await?; let mut projects = Vec::new(); for project in contact.projects { - let mut guests = Vec::new(); + let mut guests = BTreeSet::new(); for participant_id in project.guests { - guests.push( + guests.insert( user_store .update(cx, |user_store, cx| { user_store.fetch_user(participant_id, cx) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 91794ebf0f379fd26750ddd79a3d0e0e2431d291..df6d85712bc0088caa974f97ceda2d2d592298d9 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -350,11 +350,6 @@ impl ContactsPanel { let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; - let is_guest = !is_host - && project - .guests - .iter() - .any(|guest| Some(guest.id) == current_user_id); let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -447,7 +442,7 @@ impl ContactsPanel { CursorStyle::Arrow }) .on_click(move |_, cx| { - if !is_host && !is_guest { + if !is_host { cx.dispatch_global_action(JoinProject { contact: contact.clone(), project_index,