From f453928b4495adb745b26ca56ce350967d4e01dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 10:21:56 -0700 Subject: [PATCH 01/13] Associate the project with an active worktree This is similar to the active entry, but it can remain assigned even if there is no active entry and we can potentially manipulate it in other scenarios such as interaction with the project browser. This prepares the ground to show the collaborators for the active worktree. --- crates/project/src/lib.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 3e129c8fb8..d1d48f49ca 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -19,6 +19,7 @@ pub use worktree::*; pub struct Project { worktrees: Vec>, + active_worktree: Option, active_entry: Option, languages: Arc, client: Arc, @@ -46,6 +47,7 @@ impl Project { pub fn new(languages: Arc, rpc: Arc, fs: Arc) -> Self { Self { worktrees: Default::default(), + active_worktree: None, active_entry: None, languages, client: rpc, @@ -109,10 +111,25 @@ impl Project { fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); + if self.active_worktree.is_none() { + self.set_active_worktree(Some(worktree.id()), cx); + } self.worktrees.push(worktree); cx.notify(); } + fn set_active_worktree(&mut self, worktree_id: Option, cx: &mut ModelContext) { + if self.active_worktree != worktree_id { + self.active_worktree = worktree_id; + cx.notify(); + } + } + + pub fn active_worktree(&self) -> Option> { + self.active_worktree + .and_then(|worktree_id| self.worktree_for_id(worktree_id)) + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id)?; @@ -123,6 +140,9 @@ impl Project { }) }); if new_active_entry != self.active_entry { + if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) { + self.set_active_worktree(Some(worktree_id), cx); + } self.active_entry = new_active_entry; cx.emit(Event::ActiveEntryChanged(new_active_entry)); } @@ -184,6 +204,7 @@ impl Project { } pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext) { + let mut reset_active = None; self.worktrees.retain(|worktree| { let keep = worktree.update(cx, |worktree, cx| { if let Some(worktree) = worktree.as_remote_mut() { @@ -196,9 +217,15 @@ impl Project { }); if !keep { cx.emit(Event::WorktreeRemoved(worktree.id())); + reset_active = Some(worktree.id()); } keep }); + + if self.active_worktree == reset_active { + self.active_worktree = self.worktrees.first().map(|w| w.id()); + cx.notify(); + } } pub fn match_paths<'a>( From 9f29eeda0316d980530c42662dd320323f9bcff5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 10:22:34 -0700 Subject: [PATCH 02/13] Associate Peers with a user_id This will help us render the collaborator avatars for the active worktree when we know its peers. --- crates/rpc/proto/zed.proto | 1 + crates/server/src/rpc.rs | 12 +++++--- crates/server/src/rpc/store.rs | 50 ++++++++++++++-------------------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 950178be34..741f81c8cd 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -199,6 +199,7 @@ message UpdateCollaborators { message Peer { uint32 peer_id = 1; uint32 replica_id = 2; + uint64 user_id = 3; } message User { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f35bbb00e6..9349b9538b 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -173,7 +173,7 @@ impl Server { if let Some(share) = worktree.share { broadcast( connection_id, - share.guest_connection_ids.keys().copied().collect(), + share.guests.keys().copied().collect(), |conn_id| { self.peer .send(conn_id, proto::UnshareWorktree { worktree_id }) @@ -234,6 +234,7 @@ impl Server { let collaborator_user_ids = collaborator_user_ids.into_iter().collect::>(); let worktree_id = self.state_mut().add_worktree(Worktree { host_connection_id: request.sender_id, + host_user_id, collaborator_user_ids: collaborator_user_ids.clone(), root_name: request.payload.root_name, share: None, @@ -260,7 +261,7 @@ impl Server { if let Some(share) = worktree.share { broadcast( request.sender_id, - share.guest_connection_ids.keys().copied().collect(), + share.guests.keys().copied().collect(), |conn_id| { self.peer .send(conn_id, proto::UnshareWorktree { worktree_id }) @@ -341,17 +342,19 @@ impl Server { .join_worktree(request.sender_id, user_id, worktree_id) .and_then(|joined| { let share = joined.worktree.share()?; - let peer_count = share.guest_connection_ids.len(); + let peer_count = share.guests.len(); let mut peers = Vec::with_capacity(peer_count); peers.push(proto::Peer { peer_id: joined.worktree.host_connection_id.0, replica_id: 0, + user_id: joined.worktree.host_user_id.to_proto(), }); - for (peer_conn_id, peer_replica_id) in &share.guest_connection_ids { + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { if *peer_conn_id != request.sender_id { peers.push(proto::Peer { peer_id: peer_conn_id.0, replica_id: *peer_replica_id as u32, + user_id: peer_user_id.to_proto(), }); } } @@ -379,6 +382,7 @@ impl Server { peer: Some(proto::Peer { peer_id: request.sender_id.0, replica_id: response.replica_id, + user_id: user_id.to_proto(), }), }, ) diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index ac9c069146..424c1fc24c 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -1,7 +1,7 @@ use crate::db::{ChannelId, UserId}; use anyhow::anyhow; -use std::collections::{hash_map, HashMap, HashSet}; use rpc::{proto, ConnectionId}; +use std::collections::{hash_map, HashMap, HashSet}; #[derive(Default)] pub struct Store { @@ -21,13 +21,14 @@ struct ConnectionState { pub struct Worktree { pub host_connection_id: ConnectionId, + pub host_user_id: UserId, pub collaborator_user_ids: Vec, pub root_name: String, pub share: Option, } pub struct WorktreeShare { - pub guest_connection_ids: HashMap, + pub guests: HashMap, pub active_replica_ids: HashSet, pub entries: HashMap, } @@ -181,7 +182,7 @@ impl Store { let mut guests = HashSet::new(); if let Ok(share) = worktree.share() { - for guest_connection_id in share.guest_connection_ids.keys() { + for guest_connection_id in share.guests.keys() { if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { guests.insert(user_id.to_proto()); } @@ -247,7 +248,7 @@ impl Store { } if let Some(share) = &worktree.share { - for connection_id in share.guest_connection_ids.keys() { + for connection_id in share.guests.keys() { if let Some(connection) = self.connections.get_mut(connection_id) { connection.worktrees.remove(&worktree_id); } @@ -278,7 +279,7 @@ impl Store { if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { if worktree.host_connection_id == connection_id { worktree.share = Some(WorktreeShare { - guest_connection_ids: Default::default(), + guests: Default::default(), active_replica_ids: Default::default(), entries, }); @@ -306,7 +307,7 @@ impl Store { let connection_ids = worktree.connection_ids(); let collaborator_ids = worktree.collaborator_user_ids.clone(); if let Some(share) = worktree.share.take() { - for connection_id in share.guest_connection_ids.into_keys() { + for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { connection.worktrees.remove(&worktree_id); } @@ -354,7 +355,7 @@ impl Store { replica_id += 1; } share.active_replica_ids.insert(replica_id); - share.guest_connection_ids.insert(connection_id, replica_id); + share.guests.insert(connection_id, (replica_id, user_id)); #[cfg(test)] self.check_invariants(); @@ -372,7 +373,7 @@ impl Store { ) -> Option { let worktree = self.worktrees.get_mut(&worktree_id)?; let share = worktree.share.as_mut()?; - let replica_id = share.guest_connection_ids.remove(&connection_id)?; + let (replica_id, _) = share.guests.remove(&connection_id)?; share.active_replica_ids.remove(&replica_id); if let Some(connection) = self.connections.get_mut(&connection_id) { @@ -427,7 +428,7 @@ impl Store { Ok(self .read_worktree(worktree_id, connection_id)? .share()? - .guest_connection_ids + .guests .keys() .copied() .collect()) @@ -458,10 +459,7 @@ impl Store { .ok_or_else(|| anyhow!("worktree not found"))?; if worktree.host_connection_id == connection_id - || worktree - .share()? - .guest_connection_ids - .contains_key(&connection_id) + || worktree.share()?.guests.contains_key(&connection_id) { Ok(worktree) } else { @@ -484,9 +482,10 @@ impl Store { .ok_or_else(|| anyhow!("worktree not found"))?; if worktree.host_connection_id == connection_id - || worktree.share.as_ref().map_or(false, |share| { - share.guest_connection_ids.contains_key(&connection_id) - }) + || worktree + .share + .as_ref() + .map_or(false, |share| share.guests.contains_key(&connection_id)) { Ok(worktree) } else { @@ -504,11 +503,7 @@ impl Store { for worktree_id in &connection.worktrees { let worktree = &self.worktrees.get(&worktree_id).unwrap(); if worktree.host_connection_id != *connection_id { - assert!(worktree - .share() - .unwrap() - .guest_connection_ids - .contains_key(connection_id)); + assert!(worktree.share().unwrap().guests.contains_key(connection_id)); } } for channel_id in &connection.channels { @@ -544,20 +539,17 @@ impl Store { } if let Some(share) = &worktree.share { - for guest_connection_id in share.guest_connection_ids.keys() { + for guest_connection_id in share.guests.keys() { let guest_connection = self.connections.get(guest_connection_id).unwrap(); assert!(guest_connection.worktrees.contains(worktree_id)); } - assert_eq!( - share.active_replica_ids.len(), - share.guest_connection_ids.len(), - ); + assert_eq!(share.active_replica_ids.len(), share.guests.len(),); assert_eq!( share.active_replica_ids, share - .guest_connection_ids + .guests .values() - .copied() + .map(|(replica_id, _)| *replica_id) .collect::>(), ); } @@ -583,7 +575,7 @@ impl Worktree { pub fn connection_ids(&self) -> Vec { if let Some(share) = &self.share { share - .guest_connection_ids + .guests .keys() .copied() .chain(Some(self.host_connection_id)) From cd2c3c36066a7a66a685deb5d204ef369be17321 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 10:59:41 -0700 Subject: [PATCH 03/13] Rename Collaborators to Contacts This will allow us to use the word "collaborator" to describe users that are actively collaborating on a worktree. Co-Authored-By: Antonio Scandurra --- crates/client/src/user.rs | 64 ++++++++++++--------------- crates/people_panel/src/lib.rs | 26 +++++------ crates/rpc/proto/zed.proto | 8 ++-- crates/rpc/src/proto.rs | 2 +- crates/server/src/rpc.rs | 81 ++++++++++++++++------------------ crates/server/src/rpc/store.rs | 57 ++++++++++++------------ 6 files changed, 113 insertions(+), 125 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 47ea9ae55c..7ba4478834 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -20,7 +20,7 @@ pub struct User { } #[derive(Debug)] -pub struct Collaborator { +pub struct Contact { pub user: Arc, pub worktrees: Vec, } @@ -36,10 +36,10 @@ pub struct WorktreeMetadata { pub struct UserStore { users: HashMap>, current_user: watch::Receiver>>, - collaborators: Arc<[Collaborator]>, + contacts: Arc<[Contact]>, rpc: Arc, http: Arc, - _maintain_collaborators: Task<()>, + _maintain_contacts: Task<()>, _maintain_current_user: Task<()>, } @@ -52,26 +52,26 @@ impl Entity for UserStore { impl UserStore { pub fn new(rpc: Arc, http: Arc, cx: &mut ModelContext) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); - let (mut update_collaborators_tx, mut update_collaborators_rx) = - watch::channel::>(); - let update_collaborators_subscription = rpc.subscribe( + let (mut update_contacts_tx, mut update_contacts_rx) = + watch::channel::>(); + let update_contacts_subscription = rpc.subscribe( cx, - move |_: &mut Self, msg: TypedEnvelope, _, _| { - let _ = update_collaborators_tx.blocking_send(Some(msg.payload)); + move |_: &mut Self, msg: TypedEnvelope, _, _| { + let _ = update_contacts_tx.blocking_send(Some(msg.payload)); Ok(()) }, ); Self { users: Default::default(), current_user: current_user_rx, - collaborators: Arc::from([]), + contacts: Arc::from([]), rpc: rpc.clone(), http, - _maintain_collaborators: cx.spawn_weak(|this, mut cx| async move { - let _subscription = update_collaborators_subscription; - while let Some(message) = update_collaborators_rx.recv().await { + _maintain_contacts: cx.spawn_weak(|this, mut cx| async move { + let _subscription = update_contacts_subscription; + while let Some(message) = update_contacts_rx.recv().await { if let Some((message, this)) = message.zip(this.upgrade(&cx)) { - this.update(&mut cx, |this, cx| this.update_collaborators(message, cx)) + this.update(&mut cx, |this, cx| this.update_contacts(message, cx)) .log_err() .await; } @@ -100,35 +100,29 @@ impl UserStore { } } - fn update_collaborators( + fn update_contacts( &mut self, - message: proto::UpdateCollaborators, + message: proto::UpdateContacts, cx: &mut ModelContext, ) -> Task> { let mut user_ids = HashSet::new(); - for collaborator in &message.collaborators { - user_ids.insert(collaborator.user_id); - user_ids.extend( - collaborator - .worktrees - .iter() - .flat_map(|w| &w.guests) - .copied(), - ); + for contact in &message.contacts { + user_ids.insert(contact.user_id); + user_ids.extend(contact.worktrees.iter().flat_map(|w| &w.guests).copied()); } let load_users = self.load_users(user_ids.into_iter().collect(), cx); cx.spawn(|this, mut cx| async move { load_users.await?; - let mut collaborators = Vec::new(); - for collaborator in message.collaborators { - collaborators.push(Collaborator::from_proto(collaborator, &this, &mut cx).await?); + let mut contacts = Vec::new(); + for contact in message.contacts { + contacts.push(Contact::from_proto(contact, &this, &mut cx).await?); } this.update(&mut cx, |this, cx| { - collaborators.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); - this.collaborators = collaborators.into(); + contacts.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); + this.contacts = contacts.into(); cx.notify(); }); @@ -136,8 +130,8 @@ impl UserStore { }) } - pub fn collaborators(&self) -> &Arc<[Collaborator]> { - &self.collaborators + pub fn contacts(&self) -> &Arc<[Contact]> { + &self.contacts } pub fn load_users( @@ -212,19 +206,19 @@ impl User { } } -impl Collaborator { +impl Contact { async fn from_proto( - collaborator: proto::Collaborator, + contact: proto::Contact, user_store: &ModelHandle, cx: &mut AsyncAppContext, ) -> Result { let user = user_store .update(cx, |user_store, cx| { - user_store.fetch_user(collaborator.user_id, cx) + user_store.fetch_user(contact.user_id, cx) }) .await?; let mut worktrees = Vec::new(); - for worktree in collaborator.worktrees { + for worktree in contact.worktrees { let mut guests = Vec::new(); for participant_id in worktree.guests { guests.push( diff --git a/crates/people_panel/src/lib.rs b/crates/people_panel/src/lib.rs index 4a9d292737..00e87dc7e6 100644 --- a/crates/people_panel/src/lib.rs +++ b/crates/people_panel/src/lib.rs @@ -1,4 +1,4 @@ -use client::{Collaborator, UserStore}; +use client::{Contact, UserStore}; use gpui::{ action, elements::*, @@ -24,10 +24,10 @@ pub fn init(cx: &mut MutableAppContext) { } pub struct PeoplePanel { - collaborators: ListState, + contacts: ListState, user_store: ModelHandle, settings: watch::Receiver, - _maintain_collaborators: Subscription, + _maintain_contacts: Subscription, } impl PeoplePanel { @@ -37,8 +37,8 @@ impl PeoplePanel { cx: &mut ViewContext, ) -> Self { Self { - collaborators: ListState::new( - user_store.read(cx).collaborators().len(), + contacts: ListState::new( + user_store.read(cx).contacts().len(), Orientation::Top, 1000., { @@ -46,10 +46,10 @@ impl PeoplePanel { let settings = settings.clone(); move |ix, cx| { let user_store = user_store.read(cx); - let collaborators = user_store.collaborators().clone(); + let contacts = user_store.contacts().clone(); let current_user_id = user_store.current_user().map(|user| user.id); Self::render_collaborator( - &collaborators[ix], + &contacts[ix], current_user_id, &settings.borrow().theme, cx, @@ -57,7 +57,7 @@ impl PeoplePanel { } }, ), - _maintain_collaborators: cx.observe(&user_store, Self::update_collaborators), + _maintain_contacts: cx.observe(&user_store, Self::update_contacts), user_store, settings, } @@ -103,14 +103,14 @@ impl PeoplePanel { .update(cx, |p, cx| p.close_remote_worktree(action.0, cx)); } - fn update_collaborators(&mut self, _: ModelHandle, cx: &mut ViewContext) { - self.collaborators - .reset(self.user_store.read(cx).collaborators().len()); + fn update_contacts(&mut self, _: ModelHandle, cx: &mut ViewContext) { + self.contacts + .reset(self.user_store.read(cx).contacts().len()); cx.notify(); } fn render_collaborator( - collaborator: &Collaborator, + collaborator: &Contact, current_user_id: Option, theme: &Theme, cx: &mut LayoutContext, @@ -305,7 +305,7 @@ impl View for PeoplePanel { fn render(&mut self, _: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme.people_panel; - Container::new(List::new(self.collaborators.clone()).boxed()) + Container::new(List::new(self.contacts.clone()).boxed()) .with_style(theme.container) .boxed() } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 741f81c8cd..ed979b9342 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -38,7 +38,7 @@ message Envelope { OpenWorktree open_worktree = 33; OpenWorktreeResponse open_worktree_response = 34; UnshareWorktree unshare_worktree = 35; - UpdateCollaborators update_collaborators = 36; + UpdateContacts update_contacts = 36; LeaveWorktree leave_worktree = 37; } } @@ -190,8 +190,8 @@ message GetChannelMessagesResponse { bool done = 2; } -message UpdateCollaborators { - repeated Collaborator collaborators = 1; +message UpdateContacts { + repeated Contact contacts = 1; } // Entities @@ -358,7 +358,7 @@ message ChannelMessage { Nonce nonce = 5; } -message Collaborator { +message Contact { uint64 user_id = 1; repeated WorktreeMetadata worktrees = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index e9de319a1c..8299952c79 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -131,7 +131,7 @@ messages!( GetChannelMessagesResponse, GetChannels, GetChannelsResponse, - UpdateCollaborators, + UpdateContacts, GetUsers, GetUsersResponse, JoinChannel, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9349b9538b..88e58a629b 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -118,8 +118,8 @@ impl Server { let (connection_id, handle_io, mut incoming_rx) = this.peer.add_connection(connection).await; this.state_mut().add_connection(connection_id, user_id); - if let Err(err) = this.update_collaborators_for_users(&[user_id]).await { - log::error!("error updating collaborators for {:?}: {}", user_id, err); + if let Err(err) = this.update_contacts_for_users(&[user_id]).await { + log::error!("error updating contacts for {:?}: {}", user_id, err); } let handle_io = handle_io.fuse(); @@ -196,7 +196,7 @@ impl Server { .await?; } - self.update_collaborators_for_users(removed_connection.collaborator_ids.iter()) + self.update_contacts_for_users(removed_connection.contact_ids.iter()) .await?; Ok(()) @@ -214,12 +214,12 @@ impl Server { let receipt = request.receipt(); let host_user_id = self.state().user_id_for_connection(request.sender_id)?; - let mut collaborator_user_ids = HashSet::new(); - collaborator_user_ids.insert(host_user_id); + let mut contact_user_ids = HashSet::new(); + contact_user_ids.insert(host_user_id); for github_login in request.payload.collaborator_logins { match self.app_state.db.create_user(&github_login, false).await { - Ok(collaborator_user_id) => { - collaborator_user_ids.insert(collaborator_user_id); + Ok(contact_user_id) => { + contact_user_ids.insert(contact_user_id); } Err(err) => { let message = err.to_string(); @@ -231,11 +231,11 @@ impl Server { } } - let collaborator_user_ids = collaborator_user_ids.into_iter().collect::>(); + let contact_user_ids = contact_user_ids.into_iter().collect::>(); let worktree_id = self.state_mut().add_worktree(Worktree { host_connection_id: request.sender_id, host_user_id, - collaborator_user_ids: collaborator_user_ids.clone(), + contact_user_ids: contact_user_ids.clone(), root_name: request.payload.root_name, share: None, }); @@ -243,8 +243,7 @@ impl Server { self.peer .respond(receipt, proto::OpenWorktreeResponse { worktree_id }) .await?; - self.update_collaborators_for_users(&collaborator_user_ids) - .await?; + self.update_contacts_for_users(&contact_user_ids).await?; Ok(()) } @@ -269,7 +268,7 @@ impl Server { ) .await?; } - self.update_collaborators_for_users(&worktree.collaborator_user_ids) + self.update_contacts_for_users(&worktree.contact_user_ids) .await?; Ok(()) } @@ -288,15 +287,14 @@ impl Server { .map(|entry| (entry.id, entry)) .collect(); - let collaborator_user_ids = + let contact_user_ids = self.state_mut() .share_worktree(worktree.id, request.sender_id, entries); - if let Some(collaborator_user_ids) = collaborator_user_ids { + if let Some(contact_user_ids) = contact_user_ids { self.peer .respond(request.receipt(), proto::ShareWorktreeResponse {}) .await?; - self.update_collaborators_for_users(&collaborator_user_ids) - .await?; + self.update_contacts_for_users(&contact_user_ids).await?; } else { self.peer .respond_with_error( @@ -324,7 +322,7 @@ impl Server { .send(conn_id, proto::UnshareWorktree { worktree_id }) }) .await?; - self.update_collaborators_for_users(&worktree.collaborator_ids) + self.update_contacts_for_users(&worktree.contact_ids) .await?; Ok(()) @@ -368,12 +366,12 @@ impl Server { peers, }; let connection_ids = joined.worktree.connection_ids(); - let collaborator_user_ids = joined.worktree.collaborator_user_ids.clone(); - Ok((response, connection_ids, collaborator_user_ids)) + let contact_user_ids = joined.worktree.contact_user_ids.clone(); + Ok((response, connection_ids, contact_user_ids)) }); match response_data { - Ok((response, connection_ids, collaborator_user_ids)) => { + Ok((response, connection_ids, contact_user_ids)) => { broadcast(request.sender_id, connection_ids, |conn_id| { self.peer.send( conn_id, @@ -389,8 +387,7 @@ impl Server { }) .await?; self.peer.respond(request.receipt(), response).await?; - self.update_collaborators_for_users(&collaborator_user_ids) - .await?; + self.update_contacts_for_users(&contact_user_ids).await?; } Err(error) => { self.peer @@ -425,7 +422,7 @@ impl Server { ) }) .await?; - self.update_collaborators_for_users(&worktree.collaborator_ids) + self.update_contacts_for_users(&worktree.contact_ids) .await?; } Ok(()) @@ -595,7 +592,7 @@ impl Server { Ok(()) } - async fn update_collaborators_for_users<'a>( + async fn update_contacts_for_users<'a>( self: &Arc, user_ids: impl IntoIterator, ) -> tide::Result<()> { @@ -604,12 +601,12 @@ impl Server { { let state = self.state(); for user_id in user_ids { - let collaborators = state.collaborators_for_user(*user_id); + let contacts = state.contacts_for_user(*user_id); for connection_id in state.connection_ids_for_user(*user_id) { send_futures.push(self.peer.send( connection_id, - proto::UpdateCollaborators { - collaborators: collaborators.clone(), + proto::UpdateContacts { + contacts: contacts.clone(), }, )); } @@ -2108,7 +2105,7 @@ mod tests { } #[gpui::test] - async fn test_collaborators( + async fn test_contacts( mut cx_a: TestAppContext, mut cx_b: TestAppContext, mut cx_c: TestAppContext, @@ -2145,17 +2142,17 @@ mod tests { user_store_a .condition(&cx_a, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) .await; user_store_b .condition(&cx_b, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) .await; user_store_c .condition(&cx_c, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) .await; @@ -2175,37 +2172,37 @@ mod tests { user_store_a .condition(&cx_a, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; user_store_b .condition(&cx_b, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; user_store_c .condition(&cx_c, |user_store, _| { - collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; cx_a.update(move |_| drop(worktree_a)); user_store_a - .condition(&cx_a, |user_store, _| collaborators(user_store) == vec![]) + .condition(&cx_a, |user_store, _| contacts(user_store) == vec![]) .await; user_store_b - .condition(&cx_b, |user_store, _| collaborators(user_store) == vec![]) + .condition(&cx_b, |user_store, _| contacts(user_store) == vec![]) .await; user_store_c - .condition(&cx_c, |user_store, _| collaborators(user_store) == vec![]) + .condition(&cx_c, |user_store, _| contacts(user_store) == vec![]) .await; - fn collaborators(user_store: &UserStore) -> Vec<(&str, Vec<(&str, Vec<&str>)>)> { + fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, Vec<&str>)>)> { user_store - .collaborators() + .contacts() .iter() - .map(|collaborator| { - let worktrees = collaborator + .map(|contact| { + let worktrees = contact .worktrees .iter() .map(|w| { @@ -2215,7 +2212,7 @@ mod tests { ) }) .collect(); - (collaborator.user.github_login.as_str(), worktrees) + (contact.user.github_login.as_str(), worktrees) }) .collect() } diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index 424c1fc24c..2039ac21d2 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -22,7 +22,7 @@ struct ConnectionState { pub struct Worktree { pub host_connection_id: ConnectionId, pub host_user_id: UserId, - pub collaborator_user_ids: Vec, + pub contact_user_ids: Vec, pub root_name: String, pub share: Option, } @@ -44,7 +44,7 @@ pub type ReplicaId = u16; pub struct RemovedConnectionState { pub hosted_worktrees: HashMap, pub guest_worktree_ids: HashMap>, - pub collaborator_ids: HashSet, + pub contact_ids: HashSet, } pub struct JoinedWorktree<'a> { @@ -54,12 +54,12 @@ pub struct JoinedWorktree<'a> { pub struct UnsharedWorktree { pub connection_ids: Vec, - pub collaborator_ids: Vec, + pub contact_ids: Vec, } pub struct LeftWorktree { pub connection_ids: Vec, - pub collaborator_ids: Vec, + pub contact_ids: Vec, } impl Store { @@ -107,14 +107,14 @@ impl Store { for worktree_id in connection.worktrees.clone() { if let Ok(worktree) = self.remove_worktree(worktree_id, connection_id) { result - .collaborator_ids - .extend(worktree.collaborator_user_ids.iter().copied()); + .contact_ids + .extend(worktree.contact_user_ids.iter().copied()); result.hosted_worktrees.insert(worktree_id, worktree); } else if let Some(worktree) = self.leave_worktree(connection_id, worktree_id) { result .guest_worktree_ids .insert(worktree_id, worktree.connection_ids); - result.collaborator_ids.extend(worktree.collaborator_ids); + result.contact_ids.extend(worktree.contact_ids); } } @@ -171,8 +171,8 @@ impl Store { .copied() } - pub fn collaborators_for_user(&self, user_id: UserId) -> Vec { - let mut collaborators = HashMap::new(); + pub fn contacts_for_user(&self, user_id: UserId) -> Vec { + let mut contacts = HashMap::new(); for worktree_id in self .visible_worktrees_by_user_id .get(&user_id) @@ -190,9 +190,9 @@ impl Store { } if let Ok(host_user_id) = self.user_id_for_connection(worktree.host_connection_id) { - collaborators + contacts .entry(host_user_id) - .or_insert_with(|| proto::Collaborator { + .or_insert_with(|| proto::Contact { user_id: host_user_id.to_proto(), worktrees: Vec::new(), }) @@ -206,14 +206,14 @@ impl Store { } } - collaborators.into_values().collect() + contacts.into_values().collect() } pub fn add_worktree(&mut self, worktree: Worktree) -> u64 { let worktree_id = self.next_worktree_id; - for collaborator_user_id in &worktree.collaborator_user_ids { + for contact_user_id in &worktree.contact_user_ids { self.visible_worktrees_by_user_id - .entry(*collaborator_user_id) + .entry(*contact_user_id) .or_default() .insert(worktree_id); } @@ -255,10 +255,9 @@ impl Store { } } - for collaborator_user_id in &worktree.collaborator_user_ids { - if let Some(visible_worktrees) = self - .visible_worktrees_by_user_id - .get_mut(&collaborator_user_id) + for contact_user_id in &worktree.contact_user_ids { + if let Some(visible_worktrees) = + self.visible_worktrees_by_user_id.get_mut(&contact_user_id) { visible_worktrees.remove(&worktree_id); } @@ -283,7 +282,7 @@ impl Store { active_replica_ids: Default::default(), entries, }); - return Some(worktree.collaborator_user_ids.clone()); + return Some(worktree.contact_user_ids.clone()); } } None @@ -305,7 +304,7 @@ impl Store { } let connection_ids = worktree.connection_ids(); - let collaborator_ids = worktree.collaborator_user_ids.clone(); + let contact_ids = worktree.contact_user_ids.clone(); if let Some(share) = worktree.share.take() { for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { @@ -318,7 +317,7 @@ impl Store { Ok(UnsharedWorktree { connection_ids, - collaborator_ids, + contact_ids, }) } else { Err(anyhow!("worktree is not shared"))? @@ -339,7 +338,7 @@ impl Store { .worktrees .get_mut(&worktree_id) .and_then(|worktree| { - if worktree.collaborator_user_ids.contains(&user_id) { + if worktree.contact_user_ids.contains(&user_id) { Some(worktree) } else { None @@ -381,14 +380,14 @@ impl Store { } let connection_ids = worktree.connection_ids(); - let collaborator_ids = worktree.collaborator_user_ids.clone(); + let contact_ids = worktree.contact_user_ids.clone(); #[cfg(test)] self.check_invariants(); Some(LeftWorktree { connection_ids, - collaborator_ids, + contact_ids, }) } @@ -530,11 +529,9 @@ impl Store { let host_connection = self.connections.get(&worktree.host_connection_id).unwrap(); assert!(host_connection.worktrees.contains(worktree_id)); - for collaborator_id in &worktree.collaborator_user_ids { - let visible_worktree_ids = self - .visible_worktrees_by_user_id - .get(collaborator_id) - .unwrap(); + for contact_id in &worktree.contact_user_ids { + let visible_worktree_ids = + self.visible_worktrees_by_user_id.get(contact_id).unwrap(); assert!(visible_worktree_ids.contains(worktree_id)); } @@ -558,7 +555,7 @@ impl Store { for (user_id, visible_worktree_ids) in &self.visible_worktrees_by_user_id { for worktree_id in visible_worktree_ids { let worktree = self.worktrees.get(worktree_id).unwrap(); - assert!(worktree.collaborator_user_ids.contains(user_id)); + assert!(worktree.contact_user_ids.contains(user_id)); } } From c8ad5b68e088e242102a510f6e04f931fc640475 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 11:08:34 -0700 Subject: [PATCH 04/13] Rename collaborator_logins to authorized_logins Again, this is about reserving the concept of a "collaborator" for actual collaborators on a worktree. Co-Authored-By: Antonio Scandurra --- crates/project/src/worktree.rs | 6 ++--- crates/rpc/proto/zed.proto | 2 +- crates/server/src/rpc.rs | 12 +++++----- crates/server/src/rpc/store.rs | 43 ++++++++++++++++++---------------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e6b444b548..79c235fc75 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -848,13 +848,13 @@ impl LocalWorktree { while let Some(status) = status.recv().await { if let Some(this) = this.upgrade(&cx) { let remote_id = if let client::Status::Connected { .. } = status { - let collaborator_logins = this.read_with(&cx, |this, _| { + let authorized_logins = this.read_with(&cx, |this, _| { this.as_local().unwrap().config.collaborators.clone() }); let response = rpc .request(proto::OpenWorktree { root_name: root_name.clone(), - collaborator_logins, + authorized_logins, }) .await?; @@ -3316,7 +3316,7 @@ mod tests { open_worktree.payload, proto::OpenWorktree { root_name: "the-dir".to_string(), - collaborator_logins: vec!["friend-1".to_string(), "friend-2".to_string()], + authorized_logins: vec!["friend-1".to_string(), "friend-2".to_string()], } ); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ed979b9342..5b899ae916 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -55,7 +55,7 @@ message Error { message OpenWorktree { string root_name = 1; - repeated string collaborator_logins = 2; + repeated string authorized_logins = 2; } message OpenWorktreeResponse { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 88e58a629b..a66e1cf386 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -216,7 +216,7 @@ impl Server { let mut contact_user_ids = HashSet::new(); contact_user_ids.insert(host_user_id); - for github_login in request.payload.collaborator_logins { + for github_login in request.payload.authorized_logins { match self.app_state.db.create_user(&github_login, false).await { Ok(contact_user_id) => { contact_user_ids.insert(contact_user_id); @@ -235,7 +235,7 @@ impl Server { let worktree_id = self.state_mut().add_worktree(Worktree { host_connection_id: request.sender_id, host_user_id, - contact_user_ids: contact_user_ids.clone(), + authorized_user_ids: contact_user_ids.clone(), root_name: request.payload.root_name, share: None, }); @@ -268,7 +268,7 @@ impl Server { ) .await?; } - self.update_contacts_for_users(&worktree.contact_user_ids) + self.update_contacts_for_users(&worktree.authorized_user_ids) .await?; Ok(()) } @@ -322,7 +322,7 @@ impl Server { .send(conn_id, proto::UnshareWorktree { worktree_id }) }) .await?; - self.update_contacts_for_users(&worktree.contact_ids) + self.update_contacts_for_users(&worktree.authorized_user_ids) .await?; Ok(()) @@ -366,7 +366,7 @@ impl Server { peers, }; let connection_ids = joined.worktree.connection_ids(); - let contact_user_ids = joined.worktree.contact_user_ids.clone(); + let contact_user_ids = joined.worktree.authorized_user_ids.clone(); Ok((response, connection_ids, contact_user_ids)) }); @@ -422,7 +422,7 @@ impl Server { ) }) .await?; - self.update_contacts_for_users(&worktree.contact_ids) + self.update_contacts_for_users(&worktree.authorized_user_ids) .await?; } Ok(()) diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index 2039ac21d2..77bb01702f 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -22,7 +22,7 @@ struct ConnectionState { pub struct Worktree { pub host_connection_id: ConnectionId, pub host_user_id: UserId, - pub contact_user_ids: Vec, + pub authorized_user_ids: Vec, pub root_name: String, pub share: Option, } @@ -54,12 +54,12 @@ pub struct JoinedWorktree<'a> { pub struct UnsharedWorktree { pub connection_ids: Vec, - pub contact_ids: Vec, + pub authorized_user_ids: Vec, } pub struct LeftWorktree { pub connection_ids: Vec, - pub contact_ids: Vec, + pub authorized_user_ids: Vec, } impl Store { @@ -108,13 +108,13 @@ impl Store { if let Ok(worktree) = self.remove_worktree(worktree_id, connection_id) { result .contact_ids - .extend(worktree.contact_user_ids.iter().copied()); + .extend(worktree.authorized_user_ids.iter().copied()); result.hosted_worktrees.insert(worktree_id, worktree); } else if let Some(worktree) = self.leave_worktree(connection_id, worktree_id) { result .guest_worktree_ids .insert(worktree_id, worktree.connection_ids); - result.contact_ids.extend(worktree.contact_ids); + result.contact_ids.extend(worktree.authorized_user_ids); } } @@ -211,9 +211,9 @@ impl Store { pub fn add_worktree(&mut self, worktree: Worktree) -> u64 { let worktree_id = self.next_worktree_id; - for contact_user_id in &worktree.contact_user_ids { + for authorized_user_id in &worktree.authorized_user_ids { self.visible_worktrees_by_user_id - .entry(*contact_user_id) + .entry(*authorized_user_id) .or_default() .insert(worktree_id); } @@ -255,9 +255,10 @@ impl Store { } } - for contact_user_id in &worktree.contact_user_ids { - if let Some(visible_worktrees) = - self.visible_worktrees_by_user_id.get_mut(&contact_user_id) + for authorized_user_id in &worktree.authorized_user_ids { + if let Some(visible_worktrees) = self + .visible_worktrees_by_user_id + .get_mut(&authorized_user_id) { visible_worktrees.remove(&worktree_id); } @@ -282,7 +283,7 @@ impl Store { active_replica_ids: Default::default(), entries, }); - return Some(worktree.contact_user_ids.clone()); + return Some(worktree.authorized_user_ids.clone()); } } None @@ -304,7 +305,7 @@ impl Store { } let connection_ids = worktree.connection_ids(); - let contact_ids = worktree.contact_user_ids.clone(); + let authorized_user_ids = worktree.authorized_user_ids.clone(); if let Some(share) = worktree.share.take() { for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { @@ -317,7 +318,7 @@ impl Store { Ok(UnsharedWorktree { connection_ids, - contact_ids, + authorized_user_ids, }) } else { Err(anyhow!("worktree is not shared"))? @@ -338,7 +339,7 @@ impl Store { .worktrees .get_mut(&worktree_id) .and_then(|worktree| { - if worktree.contact_user_ids.contains(&user_id) { + if worktree.authorized_user_ids.contains(&user_id) { Some(worktree) } else { None @@ -380,14 +381,14 @@ impl Store { } let connection_ids = worktree.connection_ids(); - let contact_ids = worktree.contact_user_ids.clone(); + let authorized_user_ids = worktree.authorized_user_ids.clone(); #[cfg(test)] self.check_invariants(); Some(LeftWorktree { connection_ids, - contact_ids, + authorized_user_ids, }) } @@ -529,9 +530,11 @@ impl Store { let host_connection = self.connections.get(&worktree.host_connection_id).unwrap(); assert!(host_connection.worktrees.contains(worktree_id)); - for contact_id in &worktree.contact_user_ids { - let visible_worktree_ids = - self.visible_worktrees_by_user_id.get(contact_id).unwrap(); + for authorized_user_ids in &worktree.authorized_user_ids { + let visible_worktree_ids = self + .visible_worktrees_by_user_id + .get(authorized_user_ids) + .unwrap(); assert!(visible_worktree_ids.contains(worktree_id)); } @@ -555,7 +558,7 @@ impl Store { for (user_id, visible_worktree_ids) in &self.visible_worktrees_by_user_id { for worktree_id in visible_worktree_ids { let worktree = self.worktrees.get(worktree_id).unwrap(); - assert!(worktree.contact_user_ids.contains(user_id)); + assert!(worktree.authorized_user_ids.contains(user_id)); } } From d78d5712be50a0dca8ee6a82888ef707f237aaf7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 11:13:05 -0700 Subject: [PATCH 05/13] Rename PeoplePanel to ContactsPanel Yeah, it's true they're people, but this is a more specific way in which they're people. Co-Authored-By: Antonio Scandurra --- Cargo.lock | 24 +++++++++---------- .../Cargo.toml | 2 +- .../src/lib.rs | 24 +++++++++---------- crates/server/src/rpc.rs | 4 ++-- crates/theme/src/lib.rs | 4 ++-- crates/zed/Cargo.toml | 2 +- crates/zed/assets/themes/_base.toml | 22 ++++++++--------- crates/zed/src/lib.rs | 10 ++++---- crates/zed/src/main.rs | 2 +- 9 files changed, 48 insertions(+), 46 deletions(-) rename crates/{people_panel => contacts_panel}/Cargo.toml (91%) rename crates/{people_panel => contacts_panel}/src/lib.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 5b25f51ff2..851d49181a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1109,6 +1109,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "contacts_panel" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "postage", + "theme", + "workspace", +] + [[package]] name = "cookie" version = "0.14.4" @@ -3197,17 +3208,6 @@ dependencies = [ "regex", ] -[[package]] -name = "people_panel" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "postage", - "theme", - "workspace", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -5673,6 +5673,7 @@ dependencies = [ "chat_panel", "client", "clock", + "contacts_panel", "crossbeam-channel", "ctor", "dirs", @@ -5697,7 +5698,6 @@ dependencies = [ "lsp", "num_cpus", "parking_lot", - "people_panel", "postage", "project", "project_panel", diff --git a/crates/people_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml similarity index 91% rename from crates/people_panel/Cargo.toml rename to crates/contacts_panel/Cargo.toml index dd8e2114be..ffeb8e6a3d 100644 --- a/crates/people_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "people_panel" +name = "contacts_panel" version = "0.1.0" edition = "2018" diff --git a/crates/people_panel/src/lib.rs b/crates/contacts_panel/src/lib.rs similarity index 96% rename from crates/people_panel/src/lib.rs rename to crates/contacts_panel/src/lib.rs index 00e87dc7e6..8cf8b9191b 100644 --- a/crates/people_panel/src/lib.rs +++ b/crates/contacts_panel/src/lib.rs @@ -17,20 +17,20 @@ action!(ShareWorktree, u64); action!(UnshareWorktree, u64); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(PeoplePanel::share_worktree); - cx.add_action(PeoplePanel::unshare_worktree); - cx.add_action(PeoplePanel::join_worktree); - cx.add_action(PeoplePanel::leave_worktree); + cx.add_action(ContactsPanel::share_worktree); + cx.add_action(ContactsPanel::unshare_worktree); + cx.add_action(ContactsPanel::join_worktree); + cx.add_action(ContactsPanel::leave_worktree); } -pub struct PeoplePanel { +pub struct ContactsPanel { contacts: ListState, user_store: ModelHandle, settings: watch::Receiver, _maintain_contacts: Subscription, } -impl PeoplePanel { +impl ContactsPanel { pub fn new( user_store: ModelHandle, settings: watch::Receiver, @@ -115,7 +115,7 @@ impl PeoplePanel { theme: &Theme, cx: &mut LayoutContext, ) -> ElementBox { - let theme = &theme.people_panel; + let theme = &theme.contacts_panel; let worktree_count = collaborator.worktrees.len(); let font_cache = cx.font_cache(); let line_height = theme.unshared_worktree.name.text.line_height(font_cache); @@ -216,7 +216,7 @@ impl PeoplePanel { .any(|guest| Some(guest.id) == current_user_id); let is_shared = worktree.is_shared; - MouseEventHandler::new::( + MouseEventHandler::new::( worktree_id as usize, cx, |mouse_state, _| { @@ -294,17 +294,17 @@ impl PeoplePanel { pub enum Event {} -impl Entity for PeoplePanel { +impl Entity for ContactsPanel { type Event = Event; } -impl View for PeoplePanel { +impl View for ContactsPanel { fn ui_name() -> &'static str { - "PeoplePanel" + "ContactsPanel" } fn render(&mut self, _: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme.people_panel; + let theme = &self.settings.borrow().theme.contacts_panel; Container::new(List::new(self.contacts.clone()).boxed()) .with_style(theme.container) .boxed() diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index a66e1cf386..4034789308 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -940,6 +940,7 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, }, + contacts_panel::JoinWorktree, editor::{Editor, EditorSettings, Input}, fs::{FakeFs, Fs as _}, language::{ @@ -947,7 +948,6 @@ mod tests { LanguageServerConfig, Point, }, lsp, - people_panel::JoinWorktree, project::{ProjectPath, Worktree}, test::test_app_state, workspace::Workspace, @@ -1060,7 +1060,7 @@ mod tests { #[gpui::test] async fn test_unshare_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - cx_b.update(zed::people_panel::init); + cx_b.update(zed::contacts_panel::init); let mut app_state_a = cx_a.update(test_app_state); let mut app_state_b = cx_b.update(test_app_state); diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index e280b224cc..1a484c6db7 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -20,7 +20,7 @@ pub struct Theme { pub name: String, pub workspace: Workspace, pub chat_panel: ChatPanel, - pub people_panel: PeoplePanel, + pub contacts_panel: ContactsPanel, pub project_panel: ProjectPanel, pub selector: Selector, pub editor: EditorStyle, @@ -137,7 +137,7 @@ pub struct ProjectPanelEntry { } #[derive(Deserialize, Default)] -pub struct PeoplePanel { +pub struct ContactsPanel { #[serde(flatten)] pub container: ContainerStyle, pub host_row_height: f32, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8e4441b594..df27401711 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -40,7 +40,7 @@ go_to_line = { path = "../go_to_line" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } -people_panel = { path = "../people_panel" } +contacts_panel = { path = "../contacts_panel" } project = { path = "../project" } project_panel = { path = "../project_panel" } rpc = { path = "../rpc" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 94d4415421..dca2e06028 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -146,7 +146,7 @@ underline = true extends = "$chat_panel.sign_in_prompt" color = "$text.1.color" -[people_panel] +[contacts_panel] extends = "$panel" host_row_height = 28 host_avatar = { corner_radius = 10, width = 20 } @@ -154,30 +154,30 @@ host_username = { extends = "$text.0", padding.left = 8 } tree_branch_width = 1 tree_branch_color = "$surface.2" -[people_panel.worktree] +[contacts_panel.worktree] height = 24 padding = { left = 8 } guest_avatar = { corner_radius = 8, width = 16 } guest_avatar_spacing = 4 -[people_panel.worktree.name] +[contacts_panel.worktree.name] extends = "$text.1" margin = { right = 6 } -[people_panel.unshared_worktree] -extends = "$people_panel.worktree" +[contacts_panel.unshared_worktree] +extends = "$contacts_panel.worktree" -[people_panel.hovered_unshared_worktree] -extends = "$people_panel.unshared_worktree" +[contacts_panel.hovered_unshared_worktree] +extends = "$contacts_panel.unshared_worktree" background = "$state.hover" corner_radius = 6 -[people_panel.shared_worktree] -extends = "$people_panel.worktree" +[contacts_panel.shared_worktree] +extends = "$contacts_panel.worktree" name.color = "$text.0.color" -[people_panel.hovered_shared_worktree] -extends = "$people_panel.shared_worktree" +[contacts_panel.hovered_shared_worktree] +extends = "$contacts_panel.shared_worktree" background = "$state.hover" corner_radius = 6 diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index cace44a816..49e5c30120 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -7,6 +7,8 @@ pub mod test; use self::language::LanguageRegistry; use chat_panel::ChatPanel; pub use client; +pub use contacts_panel; +use contacts_panel::ContactsPanel; pub use editor; use gpui::{ action, @@ -17,8 +19,6 @@ use gpui::{ }; pub use lsp; use parking_lot::Mutex; -pub use people_panel; -use people_panel::PeoplePanel; use postage::watch; pub use project::{self, fs}; use project_panel::ProjectPanel; @@ -144,8 +144,10 @@ fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext) -> ); workspace.right_sidebar_mut().add_item( "icons/user-16.svg", - cx.add_view(|cx| PeoplePanel::new(params.user_store.clone(), params.settings.clone(), cx)) - .into(), + cx.add_view(|cx| { + ContactsPanel::new(params.user_store.clone(), params.settings.clone(), cx) + }) + .into(), ); workspace.right_sidebar_mut().add_item( "icons/comment-16.svg", diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b28066717c..13a3ccf643 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -40,7 +40,7 @@ fn main() { editor::init(cx, &mut entry_openers); go_to_line::init(cx); file_finder::init(cx); - people_panel::init(cx); + contacts_panel::init(cx); chat_panel::init(cx); project_panel::init(cx); From 21aba54dc3a6c05b3ff9d77fcee1cb04b9661c4e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 11:28:30 -0700 Subject: [PATCH 06/13] Introduce a worktree::Collaborator struct that holds the user_id We can use this to render avatars. Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- crates/project/src/worktree.rs | 64 +++++++++++++++++++++++++--------- crates/server/src/rpc.rs | 14 ++++---- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 79c235fc75..08310f2fa2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -63,6 +63,12 @@ pub enum Event { Closed, } +pub struct Collaborator { + pub user_id: u64, + pub peer_id: PeerId, + pub replica_id: ReplicaId, +} + impl Entity for Worktree { type Event = Event; @@ -259,9 +265,19 @@ impl Worktree { updates_tx, client: rpc.clone(), open_buffers: Default::default(), - peers: peers + collaborators: peers .into_iter() - .map(|p| (PeerId(p.peer_id), p.replica_id as ReplicaId)) + .map(|p| { + let peer_id = PeerId(p.peer_id); + ( + peer_id, + Collaborator { + peer_id, + user_id: p.user_id, + replica_id: p.replica_id as ReplicaId, + }, + ) + }) .collect(), queued_operations: Default::default(), languages, @@ -390,10 +406,10 @@ impl Worktree { .close_remote_buffer(envelope, cx) } - pub fn peers(&self) -> &HashMap { + pub fn collaborators(&self) -> &HashMap { match self { - Worktree::Local(worktree) => &worktree.peers, - Worktree::Remote(worktree) => &worktree.peers, + Worktree::Local(worktree) => &worktree.collaborators, + Worktree::Remote(worktree) => &worktree.collaborators, } } @@ -772,7 +788,7 @@ pub struct LocalWorktree { open_buffers: HashMap>, shared_buffers: HashMap>>, diagnostics: HashMap>, - peers: HashMap, + collaborators: HashMap, queued_operations: Vec<(u64, Operation)>, languages: Arc, rpc: Arc, @@ -887,7 +903,7 @@ impl LocalWorktree { shared_buffers: Default::default(), diagnostics: Default::default(), queued_operations: Default::default(), - peers: Default::default(), + collaborators: Default::default(), languages, rpc, fs, @@ -1085,8 +1101,15 @@ impl LocalWorktree { .peer .as_ref() .ok_or_else(|| anyhow!("empty peer"))?; - self.peers - .insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId); + let peer_id = PeerId(peer.peer_id); + self.collaborators.insert( + peer_id, + Collaborator { + peer_id, + user_id: peer.user_id, + replica_id: peer.replica_id as ReplicaId, + }, + ); cx.notify(); Ok(()) @@ -1099,9 +1122,10 @@ impl LocalWorktree { ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); let replica_id = self - .peers + .collaborators .remove(&peer_id) - .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?; + .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? + .replica_id; self.shared_buffers.remove(&peer_id); for (_, buffer) in &self.open_buffers { if let Some(buffer) = buffer.upgrade(cx) { @@ -1373,7 +1397,7 @@ pub struct RemoteWorktree { updates_tx: postage::mpsc::Sender, replica_id: ReplicaId, open_buffers: HashMap, - peers: HashMap, + collaborators: HashMap, languages: Arc, queued_operations: Vec<(u64, Operation)>, _subscriptions: Vec, @@ -1501,8 +1525,15 @@ impl RemoteWorktree { .peer .as_ref() .ok_or_else(|| anyhow!("empty peer"))?; - self.peers - .insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId); + let peer_id = PeerId(peer.peer_id); + self.collaborators.insert( + peer_id, + Collaborator { + peer_id, + user_id: peer.user_id, + replica_id: peer.replica_id as ReplicaId, + }, + ); cx.notify(); Ok(()) } @@ -1514,9 +1545,10 @@ impl RemoteWorktree { ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); let replica_id = self - .peers + .collaborators .remove(&peer_id) - .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?; + .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? + .replica_id; for (_, buffer) in &self.open_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 4034789308..9aa65bd8a1 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1005,9 +1005,9 @@ mod tests { let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); worktree_a .condition(&cx_a, |tree, _| { - tree.peers() + tree.collaborators() .values() - .any(|replica_id| *replica_id == replica_id_b) + .any(|collaborator| collaborator.replica_id == replica_id_b) }) .await; @@ -1054,7 +1054,7 @@ mod tests { // Dropping the worktree removes client B from client A's peers. cx_b.update(move |_| drop(worktree_b)); worktree_a - .condition(&cx_a, |tree, _| tree.peers().is_empty()) + .condition(&cx_a, |tree, _| tree.collaborators().is_empty()) .await; } @@ -1492,7 +1492,7 @@ mod tests { .await .unwrap(); worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .condition(&cx_a, |tree, _| tree.collaborators().len() == 1) .await; let buffer_b = cx_b @@ -1501,7 +1501,7 @@ mod tests { cx_b.update(|_| drop(worktree_b)); drop(buffer_b); worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .condition(&cx_a, |tree, _| tree.collaborators().len() == 0) .await; } @@ -1553,13 +1553,13 @@ mod tests { .await .unwrap(); worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .condition(&cx_a, |tree, _| tree.collaborators().len() == 1) .await; // Drop client B's connection and ensure client A observes client B leaving the worktree. client_b.disconnect(&cx_b.to_async()).await.unwrap(); worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .condition(&cx_a, |tree, _| tree.collaborators().len() == 0) .await; } From 9930e924121d51c5d704943d1713f19ce46e3e60 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 19:12:12 -0700 Subject: [PATCH 07/13] WIP: Give worktrees a reference to the UserStore This will allow them to fetch user data when peers are added or removed. Still work to do though. --- crates/client/src/test.rs | 4 + crates/client/src/user.rs | 18 ++-- crates/project/src/lib.rs | 30 ++++-- crates/project/src/worktree.rs | 160 ++++++++++++++++++++++---------- crates/project_panel/src/lib.rs | 1 + crates/server/src/rpc.rs | 25 +++-- crates/workspace/src/lib.rs | 1 + 7 files changed, 166 insertions(+), 73 deletions(-) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index e471398b53..b91a943002 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -172,6 +172,10 @@ impl FakeHttpClient { handler: Box::new(move |req| Box::pin(handler(req))), }) } + + pub fn with_404_response() -> Arc { + Self::new(|_| async move { Ok(ServerResponse::new(404)) }) + } } impl fmt::Debug for FakeHttpClient { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 7ba4478834..0a387487e1 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -37,7 +37,7 @@ pub struct UserStore { users: HashMap>, current_user: watch::Receiver>>, contacts: Arc<[Contact]>, - rpc: Arc, + client: Arc, http: Arc, _maintain_contacts: Task<()>, _maintain_current_user: Task<()>, @@ -50,11 +50,15 @@ impl Entity for UserStore { } impl UserStore { - pub fn new(rpc: Arc, http: Arc, cx: &mut ModelContext) -> Self { + pub fn new( + client: Arc, + http: Arc, + cx: &mut ModelContext, + ) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); let (mut update_contacts_tx, mut update_contacts_rx) = watch::channel::>(); - let update_contacts_subscription = rpc.subscribe( + let update_contacts_subscription = client.subscribe( cx, move |_: &mut Self, msg: TypedEnvelope, _, _| { let _ = update_contacts_tx.blocking_send(Some(msg.payload)); @@ -65,7 +69,7 @@ impl UserStore { users: Default::default(), current_user: current_user_rx, contacts: Arc::from([]), - rpc: rpc.clone(), + client: client.clone(), http, _maintain_contacts: cx.spawn_weak(|this, mut cx| async move { let _subscription = update_contacts_subscription; @@ -78,11 +82,11 @@ impl UserStore { } }), _maintain_current_user: cx.spawn_weak(|this, mut cx| async move { - let mut status = rpc.status(); + let mut status = client.status(); while let Some(status) = status.recv().await { match status { Status::Connected { .. } => { - if let Some((this, user_id)) = this.upgrade(&cx).zip(rpc.user_id()) { + if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { let user = this .update(&mut cx, |this, cx| this.fetch_user(user_id, cx)) .log_err() @@ -139,7 +143,7 @@ impl UserStore { mut user_ids: Vec, cx: &mut ModelContext, ) -> Task> { - let rpc = self.rpc.clone(); + let rpc = self.client.clone(); let http = self.http.clone(); user_ids.retain(|id| !self.users.contains_key(id)); cx.spawn_weak(|this, mut cx| async move { diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index d1d48f49ca..b200db63dd 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -3,7 +3,7 @@ mod ignore; mod worktree; use anyhow::Result; -use client::Client; +use client::{Client, UserStore}; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; @@ -23,6 +23,7 @@ pub struct Project { active_entry: Option, languages: Arc, client: Arc, + user_store: ModelHandle, fs: Arc, } @@ -44,13 +45,19 @@ pub struct ProjectEntry { } impl Project { - pub fn new(languages: Arc, rpc: Arc, fs: Arc) -> Self { + pub fn new( + languages: Arc, + client: Arc, + user_store: ModelHandle, + fs: Arc, + ) -> Self { Self { worktrees: Default::default(), active_worktree: None, active_entry: None, languages, - client: rpc, + client, + user_store, fs, } } @@ -72,11 +79,13 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let fs = self.fs.clone(); - let rpc = self.client.clone(); + let client = self.client.clone(); + let user_store = self.user_store.clone(); let languages = self.languages.clone(); let path = Arc::from(abs_path); cx.spawn(|this, mut cx| async move { - let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?; + let worktree = + Worktree::open_local(client, user_store, path, fs, languages, &mut cx).await?; this.update(&mut cx, |this, cx| { this.add_worktree(worktree.clone(), cx); }); @@ -91,10 +100,12 @@ impl Project { ) -> Task>> { let rpc = self.client.clone(); let languages = self.languages.clone(); + let user_store = self.user_store.clone(); cx.spawn(|this, mut cx| async move { rpc.authenticate_and_connect(&cx).await?; let worktree = - Worktree::open_remote(rpc.clone(), remote_id, languages, &mut cx).await?; + Worktree::open_remote(rpc.clone(), remote_id, languages, user_store, &mut cx) + .await?; this.update(&mut cx, |this, cx| { cx.subscribe(&worktree, move |this, _, event, cx| match event { worktree::Event::Closed => { @@ -329,6 +340,7 @@ impl Entity for Project { #[cfg(test)] mod tests { use super::*; + use client::{http::ServerResponse, test::FakeHttpClient}; use fs::RealFs; use gpui::TestAppContext; use language::LanguageRegistry; @@ -434,7 +446,9 @@ mod tests { fn build_project(cx: &mut TestAppContext) -> ModelHandle { let languages = Arc::new(LanguageRegistry::new()); let fs = Arc::new(RealFs); - let rpc = client::Client::new(); - cx.add_model(|_| Project::new(languages, rpc, fs)) + let client = client::Client::new(); + let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + cx.add_model(|_| Project::new(languages, client, user_store, fs)) } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 08310f2fa2..f21abf06de 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4,7 +4,7 @@ use super::{ }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, PeerId, TypedEnvelope}; +use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use futures::{Stream, StreamExt}; use fuzzy::CharBag; @@ -64,7 +64,7 @@ pub enum Event { } pub struct Collaborator { - pub user_id: u64, + pub user: Arc, pub peer_id: PeerId, pub replica_id: ReplicaId, } @@ -76,7 +76,7 @@ impl Entity for Worktree { match self { Self::Local(tree) => { if let Some(worktree_id) = *tree.remote_id.borrow() { - let rpc = tree.rpc.clone(); + let rpc = tree.client.clone(); cx.spawn(|_| async move { if let Err(err) = rpc.send(proto::CloseWorktree { worktree_id }).await { log::error!("error closing worktree: {}", err); @@ -124,14 +124,15 @@ impl Entity for Worktree { impl Worktree { pub async fn open_local( - rpc: Arc, + client: Arc, + user_store: ModelHandle, path: impl Into>, fs: Arc, languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { let (tree, scan_states_tx) = - LocalWorktree::new(rpc, path, fs.clone(), languages, cx).await?; + LocalWorktree::new(client, user_store, path, fs.clone(), languages, cx).await?; tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); let abs_path = tree.snapshot.abs_path.clone(); @@ -148,18 +149,22 @@ impl Worktree { } pub async fn open_remote( - rpc: Arc, + client: Arc, id: u64, languages: Arc, + user_store: ModelHandle, cx: &mut AsyncAppContext, ) -> Result> { - let response = rpc.request(proto::JoinWorktree { worktree_id: id }).await?; - Worktree::remote(response, rpc, languages, cx).await + let response = client + .request(proto::JoinWorktree { worktree_id: id }) + .await?; + Worktree::remote(response, client, user_store, languages, cx).await } async fn remote( join_response: proto::JoinWorktreeResponse, - rpc: Arc, + client: Arc, + user_store: ModelHandle, languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { @@ -204,6 +209,26 @@ impl Worktree { }) .await; + let user_ids = peers.iter().map(|peer| peer.user_id).collect(); + user_store + .update(cx, |user_store, cx| user_store.load_users(user_ids, cx)) + .await?; + let mut collaborators = HashMap::with_capacity(peers.len()); + for peer in &peers { + let peer_id = PeerId(peer.peer_id); + let user = user_store + .update(cx, |user_store, cx| user_store.fetch_user(peer.user_id, cx)) + .await?; + collaborators.insert( + peer_id, + Collaborator { + peer_id, + user, + replica_id: peer.replica_id as ReplicaId, + }, + ); + } + let worktree = cx.update(|cx| { cx.add_model(|cx: &mut ModelContext| { let snapshot = Snapshot { @@ -249,12 +274,12 @@ impl Worktree { } let _subscriptions = vec![ - rpc.subscribe_to_entity(remote_id, cx, Self::handle_add_peer), - rpc.subscribe_to_entity(remote_id, cx, Self::handle_remove_peer), - rpc.subscribe_to_entity(remote_id, cx, Self::handle_update), - rpc.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), - rpc.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), - rpc.subscribe_to_entity(remote_id, cx, Self::handle_unshare), + client.subscribe_to_entity(remote_id, cx, Self::handle_add_peer), + client.subscribe_to_entity(remote_id, cx, Self::handle_remove_peer), + client.subscribe_to_entity(remote_id, cx, Self::handle_update), + client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), + client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), + client.subscribe_to_entity(remote_id, cx, Self::handle_unshare), ]; Worktree::Remote(RemoteWorktree { @@ -263,24 +288,12 @@ impl Worktree { snapshot, snapshot_rx, updates_tx, - client: rpc.clone(), + client: client.clone(), open_buffers: Default::default(), - collaborators: peers - .into_iter() - .map(|p| { - let peer_id = PeerId(p.peer_id); - ( - peer_id, - Collaborator { - peer_id, - user_id: p.user_id, - replica_id: p.replica_id as ReplicaId, - }, - ) - }) - .collect(), + collaborators, queued_operations: Default::default(), languages, + user_store, _subscriptions, }) }) @@ -737,7 +750,7 @@ impl Worktree { Worktree::Local(worktree) => worktree .remote_id .borrow() - .map(|id| (worktree.rpc.clone(), id)), + .map(|id| (worktree.client.clone(), id)), Worktree::Remote(worktree) => Some((worktree.client.clone(), worktree.remote_id)), } { cx.spawn(|worktree, mut cx| async move { @@ -791,7 +804,8 @@ pub struct LocalWorktree { collaborators: HashMap, queued_operations: Vec<(u64, Operation)>, languages: Arc, - rpc: Arc, + client: Arc, + user_store: ModelHandle, fs: Arc, language_servers: HashMap>, } @@ -803,7 +817,8 @@ struct WorktreeConfig { impl LocalWorktree { async fn new( - rpc: Arc, + client: Arc, + user_store: ModelHandle, path: impl Into>, fs: Arc, languages: Arc, @@ -857,7 +872,7 @@ impl LocalWorktree { let (mut remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ - let rpc = rpc.clone(); + let rpc = client.clone(); move |this, cx| { async move { let mut status = rpc.status(); @@ -905,7 +920,8 @@ impl LocalWorktree { queued_operations: Default::default(), collaborators: Default::default(), languages, - rpc, + client, + user_store, fs, language_servers: Default::default(), }; @@ -1106,7 +1122,7 @@ impl LocalWorktree { peer_id, Collaborator { peer_id, - user_id: peer.user_id, + user: todo!(), replica_id: peer.replica_id as ReplicaId, }, ); @@ -1267,7 +1283,7 @@ impl LocalWorktree { pub fn share(&mut self, cx: &mut ModelContext) -> Task> { let snapshot = self.snapshot(); let share_request = self.share_request(cx); - let rpc = self.rpc.clone(); + let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { let share_request = if let Some(request) = share_request.await { request @@ -1321,7 +1337,7 @@ impl LocalWorktree { pub fn unshare(&mut self, cx: &mut ModelContext) { self.share.take(); - let rpc = self.rpc.clone(); + let rpc = self.client.clone(); let remote_id = self.remote_id(); cx.foreground() .spawn( @@ -1399,6 +1415,7 @@ pub struct RemoteWorktree { open_buffers: HashMap, collaborators: HashMap, languages: Arc, + user_store: ModelHandle, queued_operations: Vec<(u64, Operation)>, _subscriptions: Vec, } @@ -1530,7 +1547,7 @@ impl RemoteWorktree { peer_id, Collaborator { peer_id, - user_id: peer.user_id, + user: todo!(), replica_id: peer.replica_id as ReplicaId, }, ); @@ -2009,7 +2026,7 @@ impl language::File for File { ) -> Task> { self.worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { - let rpc = worktree.rpc.clone(); + let rpc = worktree.client.clone(); let worktree_id = *worktree.remote_id.borrow(); let save = worktree.save(self.path.clone(), text, cx); cx.background().spawn(async move { @@ -2976,7 +2993,7 @@ mod tests { use crate::fs::FakeFs; use anyhow::Result; use buffer::Point; - use client::test::FakeServer; + use client::test::{FakeHttpClient, FakeServer}; use fs::RealFs; use language::{tree_sitter_rust, LanguageServerConfig}; use language::{Diagnostic, LanguageConfig}; @@ -2992,7 +3009,7 @@ mod tests { use util::test::temp_tree; #[gpui::test] - async fn test_traversal(cx: gpui::TestAppContext) { + async fn test_traversal(mut cx: gpui::TestAppContext) { let fs = FakeFs::new(); fs.insert_tree( "/root", @@ -3006,8 +3023,13 @@ mod tests { ) .await; + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, Arc::from(Path::new("/root")), Arc::new(fs), Default::default(), @@ -3038,8 +3060,14 @@ mod tests { let dir = temp_tree(json!({ "file1": "the old contents", })); + + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, dir.path(), Arc::new(RealFs), Default::default(), @@ -3068,8 +3096,13 @@ mod tests { })); let file_path = dir.path().join("file1"); + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, file_path.clone(), Arc::new(RealFs), Default::default(), @@ -3113,9 +3146,12 @@ mod tests { let user_id = 5; let mut client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let server = FakeServer::for_client(user_id, &mut client, &cx).await; let tree = Worktree::open_local( client, + user_store.clone(), dir.path(), Arc::new(RealFs), Default::default(), @@ -3170,6 +3206,7 @@ mod tests { peers: Vec::new(), }, Client::new(), + user_store, Default::default(), &mut cx.to_async(), ) @@ -3262,7 +3299,7 @@ mod tests { } #[gpui::test] - async fn test_rescan_with_gitignore(cx: gpui::TestAppContext) { + async fn test_rescan_with_gitignore(mut cx: gpui::TestAppContext) { let dir = temp_tree(json!({ ".git": {}, ".gitignore": "ignored-dir\n", @@ -3274,8 +3311,13 @@ mod tests { } })); + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, dir.path(), Arc::new(RealFs), Default::default(), @@ -3313,6 +3355,8 @@ mod tests { let user_id = 100; let mut client = Client::new(); let server = FakeServer::for_client(user_id, &mut client, &cx).await; + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let fs = Arc::new(FakeFs::new()); fs.insert_tree( @@ -3330,6 +3374,7 @@ mod tests { let worktree = Worktree::open_local( client.clone(), + user_store, "/path/to/the-dir".as_ref(), fs, Default::default(), @@ -3376,8 +3421,13 @@ mod tests { "file2": "def", "file3": "ghi", })); + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, dir.path(), Arc::new(RealFs), Default::default(), @@ -3509,8 +3559,13 @@ mod tests { let initial_contents = "aaa\nbbbbb\nc\n"; let dir = temp_tree(json!({ "the-file": initial_contents })); + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, dir.path(), Arc::new(RealFs), Default::default(), @@ -3619,8 +3674,13 @@ mod tests { "b.rs": "const y: i32 = 1", })); + let client = Client::new(); + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let tree = Worktree::open_local( - Client::new(), + client, + user_store, dir.path(), Arc::new(RealFs), Arc::new(languages), diff --git a/crates/project_panel/src/lib.rs b/crates/project_panel/src/lib.rs index 385b7dbca2..4d05ed317a 100644 --- a/crates/project_panel/src/lib.rs +++ b/crates/project_panel/src/lib.rs @@ -621,6 +621,7 @@ mod tests { Project::new( params.languages.clone(), params.client.clone(), + params.user_store.clone(), params.fs.clone(), ) }); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9aa65bd8a1..9f4c724770 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -961,7 +961,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; cx_a.foreground().forbid_parking(); @@ -998,6 +998,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1163,8 +1164,8 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; - let (client_c, _) = server.create_client(&mut cx_c, "user_c").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let (client_c, user_store_c) = server.create_client(&mut cx_c, "user_c").await; let fs = Arc::new(FakeFs::new()); @@ -1201,6 +1202,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1209,6 +1211,7 @@ mod tests { client_c.clone(), worktree_id, lang_registry.clone(), + user_store_c, &mut cx_c.to_async(), ) .await @@ -1302,7 +1305,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1337,6 +1340,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1387,7 +1391,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1421,6 +1425,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1453,7 +1458,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1487,6 +1492,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1513,7 +1519,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_a, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_a, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1548,6 +1554,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -1587,7 +1594,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, _) = server.create_client(&mut cx_a, "user_b").await; + let (client_b, user_store_b) = server.create_client(&mut cx_a, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1656,6 +1663,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b, &mut cx_b.to_async(), ) .await @@ -2165,6 +2173,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), + user_store_b.clone(), &mut cx_b.to_async(), ) .await diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 5901d694d3..ea75277b9c 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -348,6 +348,7 @@ impl Workspace { Project::new( params.languages.clone(), params.client.clone(), + params.user_store.clone(), params.fs.clone(), ) }); From b307a7e91d56326af64b22106b73572c3d2874e8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 Nov 2021 20:35:50 -0700 Subject: [PATCH 08/13] Populate the user data of worktree collaborators This will make it possible for us to render their avatars. Previously we only had the user ids. During rendering, everything needs to be available synchronously. So now, whenever collaborators are added, we perform the async I/O to fetch their user data prior to adding them to the worktree. --- crates/client/src/test.rs | 20 +++- crates/project/src/worktree.rs | 170 +++++++++++++++++---------------- crates/rpc/proto/zed.proto | 14 +-- crates/rpc/src/proto.rs | 8 +- crates/server/src/rpc.rs | 47 +++++---- 5 files changed, 145 insertions(+), 114 deletions(-) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index b91a943002..a40d1ee3a7 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -2,7 +2,7 @@ use super::Client; use super::*; use crate::http::{HttpClient, Request, Response, ServerResponse}; use futures::{future::BoxFuture, Future}; -use gpui::TestAppContext; +use gpui::{ModelHandle, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, prelude::Stream}; use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope}; @@ -155,6 +155,24 @@ impl FakeServer { fn connection_id(&self) -> ConnectionId { self.connection_id.lock().expect("not connected") } + + pub async fn build_user_store( + &self, + client: Arc, + cx: &mut TestAppContext, + ) -> ModelHandle { + let http_client = FakeHttpClient::with_404_response(); + let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx)); + assert_eq!( + self.receive::() + .await + .unwrap() + .payload + .user_ids, + &[self.user_id] + ); + user_store + } } pub struct FakeHttpClient { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index f21abf06de..0fec2d4209 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -69,6 +69,26 @@ pub struct Collaborator { pub replica_id: ReplicaId, } +impl Collaborator { + fn from_proto( + message: proto::Collaborator, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> impl Future> { + let user = user_store.update(cx, |user_store, cx| { + user_store.fetch_user(message.user_id, cx) + }); + + async move { + Ok(Self { + peer_id: PeerId(message.peer_id), + user: user.await?, + replica_id: message.replica_id as ReplicaId, + }) + } + } +} + impl Entity for Worktree { type Event = Event; @@ -174,7 +194,6 @@ impl Worktree { let remote_id = worktree.id; let replica_id = join_response.replica_id as ReplicaId; - let peers = join_response.peers; let root_char_bag: CharBag = worktree .root_name .chars() @@ -209,24 +228,18 @@ impl Worktree { }) .await; - let user_ids = peers.iter().map(|peer| peer.user_id).collect(); + let user_ids = join_response + .collaborators + .iter() + .map(|peer| peer.user_id) + .collect(); user_store .update(cx, |user_store, cx| user_store.load_users(user_ids, cx)) .await?; - let mut collaborators = HashMap::with_capacity(peers.len()); - for peer in &peers { - let peer_id = PeerId(peer.peer_id); - let user = user_store - .update(cx, |user_store, cx| user_store.fetch_user(peer.user_id, cx)) - .await?; - collaborators.insert( - peer_id, - Collaborator { - peer_id, - user, - replica_id: peer.replica_id as ReplicaId, - }, - ); + let mut collaborators = HashMap::with_capacity(join_response.collaborators.len()); + for message in join_response.collaborators { + let collaborator = Collaborator::from_proto(message, &user_store, cx).await?; + collaborators.insert(collaborator.peer_id, collaborator); } let worktree = cx.update(|cx| { @@ -274,8 +287,8 @@ impl Worktree { } let _subscriptions = vec![ - client.subscribe_to_entity(remote_id, cx, Self::handle_add_peer), - client.subscribe_to_entity(remote_id, cx, Self::handle_remove_peer), + client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator), + client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator), client.subscribe_to_entity(remote_id, cx, Self::handle_update), client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), @@ -347,27 +360,52 @@ impl Worktree { } } - pub fn handle_add_peer( - &mut self, - envelope: TypedEnvelope, - _: Arc, - cx: &mut ModelContext, - ) -> Result<()> { + pub fn user_store(&self) -> &ModelHandle { match self { - Worktree::Local(worktree) => worktree.add_peer(envelope, cx), - Worktree::Remote(worktree) => worktree.add_peer(envelope, cx), + Worktree::Local(worktree) => &worktree.user_store, + Worktree::Remote(worktree) => &worktree.user_store, } } - pub fn handle_remove_peer( + pub fn handle_add_collaborator( &mut self, - envelope: TypedEnvelope, + mut envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let user_store = self.user_store().clone(); + let collaborator = envelope + .payload + .collaborator + .take() + .ok_or_else(|| anyhow!("empty collaborator"))?; + + cx.spawn(|this, mut cx| { + async move { + let collaborator = + Collaborator::from_proto(collaborator, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| match this { + Worktree::Local(worktree) => worktree.add_collaborator(collaborator, cx), + Worktree::Remote(worktree) => worktree.add_collaborator(collaborator, cx), + }); + Ok(()) + } + .log_err() + }) + .detach(); + + Ok(()) + } + + pub fn handle_remove_collaborator( + &mut self, + envelope: TypedEnvelope, _: Arc, cx: &mut ModelContext, ) -> Result<()> { match self { - Worktree::Local(worktree) => worktree.remove_peer(envelope, cx), - Worktree::Remote(worktree) => worktree.remove_peer(envelope, cx), + Worktree::Local(worktree) => worktree.remove_collaborator(envelope, cx), + Worktree::Remote(worktree) => worktree.remove_collaborator(envelope, cx), } } @@ -1107,33 +1145,19 @@ impl LocalWorktree { Ok(()) } - pub fn add_peer( + pub fn add_collaborator( &mut self, - envelope: TypedEnvelope, + collaborator: Collaborator, cx: &mut ModelContext, - ) -> Result<()> { - let peer = envelope - .payload - .peer - .as_ref() - .ok_or_else(|| anyhow!("empty peer"))?; - let peer_id = PeerId(peer.peer_id); - self.collaborators.insert( - peer_id, - Collaborator { - peer_id, - user: todo!(), - replica_id: peer.replica_id as ReplicaId, - }, - ); + ) { + self.collaborators + .insert(collaborator.peer_id, collaborator); cx.notify(); - - Ok(()) } - pub fn remove_peer( + pub fn remove_collaborator( &mut self, - envelope: TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -1316,8 +1340,8 @@ impl LocalWorktree { this.update(&mut cx, |worktree, cx| { let _subscriptions = vec![ - rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_add_peer), - rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_remove_peer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_add_collaborator), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_remove_collaborator), rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_open_buffer), rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_close_buffer), rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_update_buffer), @@ -1532,32 +1556,19 @@ impl RemoteWorktree { Ok(()) } - pub fn add_peer( + pub fn add_collaborator( &mut self, - envelope: TypedEnvelope, + collaborator: Collaborator, cx: &mut ModelContext, - ) -> Result<()> { - let peer = envelope - .payload - .peer - .as_ref() - .ok_or_else(|| anyhow!("empty peer"))?; - let peer_id = PeerId(peer.peer_id); - self.collaborators.insert( - peer_id, - Collaborator { - peer_id, - user: todo!(), - replica_id: peer.replica_id as ReplicaId, - }, - ); + ) { + self.collaborators + .insert(collaborator.peer_id, collaborator); cx.notify(); - Ok(()) } - pub fn remove_peer( + pub fn remove_collaborator( &mut self, - envelope: TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -3146,9 +3157,8 @@ mod tests { let user_id = 5; let mut client = Client::new(); - let http_client = FakeHttpClient::with_404_response(); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let server = FakeServer::for_client(user_id, &mut client, &cx).await; + let user_store = server.build_user_store(client.clone(), &mut cx).await; let tree = Worktree::open_local( client, user_store.clone(), @@ -3203,7 +3213,7 @@ mod tests { proto::JoinWorktreeResponse { worktree: share_request.await.unwrap().worktree, replica_id: 1, - peers: Vec::new(), + collaborators: Vec::new(), }, Client::new(), user_store, @@ -3355,8 +3365,7 @@ mod tests { let user_id = 100; let mut client = Client::new(); let server = FakeServer::for_client(user_id, &mut client, &cx).await; - let http_client = FakeHttpClient::with_404_response(); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let user_store = server.build_user_store(client.clone(), &mut cx).await; let fs = Arc::new(FakeFs::new()); fs.insert_tree( @@ -3383,11 +3392,6 @@ mod tests { .await .unwrap(); - { - let cx = cx.to_async(); - client.authenticate_and_connect(&cx).await.unwrap(); - } - let open_worktree = server.receive::().await.unwrap(); assert_eq!( open_worktree.payload, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5b899ae916..775f94d595 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -21,8 +21,8 @@ message Envelope { UpdateBuffer update_buffer = 16; SaveBuffer save_buffer = 17; BufferSaved buffer_saved = 18; - AddPeer add_peer = 19; - RemovePeer remove_peer = 20; + AddCollaborator add_collaborator = 19; + RemoveCollaborator remove_collaborator = 20; GetChannels get_channels = 21; GetChannelsResponse get_channels_response = 22; GetUsers get_users = 23; @@ -83,7 +83,7 @@ message LeaveWorktree { message JoinWorktreeResponse { Worktree worktree = 2; uint32 replica_id = 3; - repeated Peer peers = 4; + repeated Collaborator collaborators = 4; } message UpdateWorktree { @@ -96,12 +96,12 @@ message CloseWorktree { uint64 worktree_id = 1; } -message AddPeer { +message AddCollaborator { uint64 worktree_id = 1; - Peer peer = 2; + Collaborator collaborator = 2; } -message RemovePeer { +message RemoveCollaborator { uint64 worktree_id = 1; uint32 peer_id = 2; } @@ -196,7 +196,7 @@ message UpdateContacts { // Entities -message Peer { +message Collaborator { uint32 peer_id = 1; uint32 replica_id = 2; uint64 user_id = 3; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8299952c79..bfdce85b77 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -121,7 +121,7 @@ macro_rules! entity_messages { messages!( Ack, - AddPeer, + AddCollaborator, BufferSaved, ChannelMessageSent, CloseBuffer, @@ -145,7 +145,7 @@ messages!( OpenWorktree, OpenWorktreeResponse, Ping, - RemovePeer, + RemoveCollaborator, SaveBuffer, SendChannelMessage, SendChannelMessageResponse, @@ -174,13 +174,13 @@ request_messages!( entity_messages!( worktree_id, - AddPeer, + AddCollaborator, BufferSaved, CloseBuffer, CloseWorktree, OpenBuffer, JoinWorktree, - RemovePeer, + RemoveCollaborator, SaveBuffer, UnshareWorktree, UpdateBuffer, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9f4c724770..a4c21b4ea4 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -187,7 +187,7 @@ impl Server { broadcast(connection_id, peer_ids, |conn_id| { self.peer.send( conn_id, - proto::RemovePeer { + proto::RemoveCollaborator { worktree_id, peer_id: connection_id.0, }, @@ -341,15 +341,15 @@ impl Server { .and_then(|joined| { let share = joined.worktree.share()?; let peer_count = share.guests.len(); - let mut peers = Vec::with_capacity(peer_count); - peers.push(proto::Peer { + let mut collaborators = Vec::with_capacity(peer_count); + collaborators.push(proto::Collaborator { peer_id: joined.worktree.host_connection_id.0, replica_id: 0, user_id: joined.worktree.host_user_id.to_proto(), }); for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { if *peer_conn_id != request.sender_id { - peers.push(proto::Peer { + collaborators.push(proto::Collaborator { peer_id: peer_conn_id.0, replica_id: *peer_replica_id as u32, user_id: peer_user_id.to_proto(), @@ -363,7 +363,7 @@ impl Server { entries: share.entries.values().cloned().collect(), }), replica_id: joined.replica_id as u32, - peers, + collaborators, }; let connection_ids = joined.worktree.connection_ids(); let contact_user_ids = joined.worktree.authorized_user_ids.clone(); @@ -375,9 +375,9 @@ impl Server { broadcast(request.sender_id, connection_ids, |conn_id| { self.peer.send( conn_id, - proto::AddPeer { + proto::AddCollaborator { worktree_id, - peer: Some(proto::Peer { + collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, replica_id: response.replica_id, user_id: user_id.to_proto(), @@ -415,7 +415,7 @@ impl Server { broadcast(sender_id, worktree.connection_ids, |conn_id| { self.peer.send( conn_id, - proto::RemovePeer { + proto::RemoveCollaborator { worktree_id, peer_id: sender_id.0, }, @@ -960,7 +960,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; cx_a.foreground().forbid_parking(); @@ -978,6 +978,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/a".as_ref(), fs, lang_registry.clone(), @@ -1052,7 +1053,7 @@ mod tests { .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) .await; - // Dropping the worktree removes client B from client A's peers. + // Dropping the worktree removes client B from client A's collaborators. cx_b.update(move |_| drop(worktree_b)); worktree_a .condition(&cx_a, |tree, _| tree.collaborators().is_empty()) @@ -1089,6 +1090,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( app_state_a.client.clone(), + app_state_a.user_store.clone(), "/a".as_ref(), fs, app_state_a.languages.clone(), @@ -1163,7 +1165,7 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; let (client_c, user_store_c) = server.create_client(&mut cx_c, "user_c").await; @@ -1182,6 +1184,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/a".as_ref(), fs.clone(), lang_registry.clone(), @@ -1304,7 +1307,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A @@ -1320,6 +1323,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/dir".as_ref(), fs, lang_registry.clone(), @@ -1390,7 +1394,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A @@ -1405,6 +1409,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/dir".as_ref(), fs, lang_registry.clone(), @@ -1457,7 +1462,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A @@ -1472,6 +1477,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/dir".as_ref(), fs, lang_registry.clone(), @@ -1512,14 +1518,14 @@ mod tests { } #[gpui::test] - async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { + async fn test_peer_disconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_a, "user_b").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1534,6 +1540,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/a".as_ref(), fs, lang_registry.clone(), @@ -1593,8 +1600,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_a, "user_b").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1609,6 +1616,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a, "/a".as_ref(), fs, lang_registry.clone(), @@ -2140,6 +2148,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), + user_store_a.clone(), "/a".as_ref(), fs.clone(), lang_registry.clone(), From 4bd43e67ef8da054b605d7ad93ba8af65ad0b958 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 27 Nov 2021 12:33:25 -0700 Subject: [PATCH 09/13] Introduce a TestClient and associate it with a PeerId This makes it easier to integration test peer interactions because now we know their PeerIds. --- crates/project/src/worktree.rs | 1 + crates/rpc/src/peer.rs | 14 +- crates/server/src/rpc.rs | 237 +++++++++++++++++++++------------ 3 files changed, 156 insertions(+), 96 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0fec2d4209..50f5b2d044 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -63,6 +63,7 @@ pub enum Event { Closed, } +#[derive(Debug)] pub struct Collaborator { pub user: Arc, pub peer_id: PeerId, diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 7752fc231a..454881fece 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -8,6 +8,7 @@ use postage::{ prelude::{Sink as _, Stream as _}, }; use smol_timeout::TimeoutExt as _; +use std::sync::atomic::Ordering::SeqCst; use std::{ collections::HashMap, fmt, @@ -81,12 +82,12 @@ impl TypedEnvelope { } pub struct Peer { - connections: RwLock>, + pub connections: RwLock>, next_connection_id: AtomicU32, } #[derive(Clone)] -struct ConnectionState { +pub struct ConnectionState { outgoing_tx: mpsc::Sender, next_message_id: Arc, response_channels: Arc>>>>, @@ -110,10 +111,7 @@ impl Peer { impl Future> + Send, mpsc::Receiver>, ) { - let connection_id = ConnectionId( - self.next_connection_id - .fetch_add(1, atomic::Ordering::SeqCst), - ); + let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst)); let (mut incoming_tx, incoming_rx) = mpsc::channel(64); let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64); let connection_state = ConnectionState { @@ -219,9 +217,7 @@ impl Peer { let (tx, mut rx) = mpsc::channel(1); async move { let mut connection = this.connection_state(receiver_id).await?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); + let message_id = connection.next_message_id.fetch_add(1, SeqCst); connection .response_channels .lock() diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index a4c21b4ea4..0e317028c7 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -112,11 +112,17 @@ impl Server { connection: Connection, addr: String, user_id: UserId, + mut send_connection_id: Option>, ) -> impl Future { let mut this = self.clone(); async move { let (connection_id, handle_io, mut incoming_rx) = this.peer.add_connection(connection).await; + + if let Some(send_connection_id) = send_connection_id.as_mut() { + let _ = send_connection_id.send(connection_id).await; + } + this.state_mut().add_connection(connection_id, user_id); if let Err(err) = this.update_contacts_for_users(&[user_id]).await { log::error!("error updating contacts for {:?}: {}", user_id, err); @@ -887,6 +893,7 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { ), addr, user_id, + None, ) .await; } @@ -925,9 +932,11 @@ mod tests { use gpui::{ModelHandle, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, watch}; + use rpc::PeerId; use serde_json::json; use sqlx::types::time::OffsetDateTime; use std::{ + ops::Deref, path::Path, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, @@ -960,8 +969,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; cx_a.foreground().forbid_parking(); @@ -978,7 +987,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/a".as_ref(), fs, lang_registry.clone(), @@ -999,17 +1008,31 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await .unwrap(); - let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); + + let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| { + assert_eq!( + tree.collaborators() + .get(&client_a.peer_id) + .unwrap() + .user + .github_login, + "user_a" + ); + tree.replica_id() + }); worktree_a .condition(&cx_a, |tree, _| { tree.collaborators() - .values() - .any(|collaborator| collaborator.replica_id == replica_id_b) + .get(&client_b.peer_id) + .map_or(false, |collaborator| { + collaborator.replica_id == replica_id_b + && collaborator.user.github_login == "user_b" + }) }) .await; @@ -1068,12 +1091,12 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; - Arc::get_mut(&mut app_state_a).unwrap().client = client_a; - Arc::get_mut(&mut app_state_a).unwrap().user_store = user_store_a; - Arc::get_mut(&mut app_state_b).unwrap().client = client_b; - Arc::get_mut(&mut app_state_b).unwrap().user_store = user_store_b; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + Arc::get_mut(&mut app_state_a).unwrap().client = client_a.clone(); + Arc::get_mut(&mut app_state_a).unwrap().user_store = client_a.user_store.clone(); + Arc::get_mut(&mut app_state_b).unwrap().client = client_b.clone(); + Arc::get_mut(&mut app_state_b).unwrap().user_store = client_b.user_store.clone(); cx_a.foreground().forbid_parking(); @@ -1165,9 +1188,9 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; - let (client_c, user_store_c) = server.create_client(&mut cx_c, "user_c").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + let client_c = server.create_client(&mut cx_c, "user_c").await; let fs = Arc::new(FakeFs::new()); @@ -1184,7 +1207,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/a".as_ref(), fs.clone(), lang_registry.clone(), @@ -1205,7 +1228,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1214,7 +1237,7 @@ mod tests { client_c.clone(), worktree_id, lang_registry.clone(), - user_store_c, + client_c.user_store.clone(), &mut cx_c.to_async(), ) .await @@ -1307,8 +1330,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1323,7 +1346,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/dir".as_ref(), fs, lang_registry.clone(), @@ -1344,7 +1367,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1394,8 +1417,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1409,7 +1432,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/dir".as_ref(), fs, lang_registry.clone(), @@ -1430,7 +1453,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1462,8 +1485,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1477,7 +1500,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/dir".as_ref(), fs, lang_registry.clone(), @@ -1498,7 +1521,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1524,8 +1547,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1540,7 +1563,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/a".as_ref(), fs, lang_registry.clone(), @@ -1561,7 +1584,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1600,8 +1623,8 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); @@ -1616,7 +1639,7 @@ mod tests { .await; let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a, + client_a.user_store.clone(), "/a".as_ref(), fs, lang_registry.clone(), @@ -1671,7 +1694,7 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b, + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await @@ -1719,30 +1742,30 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; // Create an org that includes these 2 users. let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) + db.add_org_member(org_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_b, &cx_b), false) + db.add_org_member(org_id, client_b.current_user_id(&cx_b), false) .await .unwrap(); // Create a channel that includes all the users. let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) + db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_b, &cx_b), false) + db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false) .await .unwrap(); db.create_channel_message( channel_id, - current_user_id(&user_store_b, &cx_b), + client_b.current_user_id(&cx_b), "hello A, it's B.", OffsetDateTime::now_utc(), 1, @@ -1750,7 +1773,8 @@ mod tests { .await .unwrap(); - let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + let channels_a = cx_a + .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx)); channels_a .condition(&mut cx_a, |list, _| list.available_channels().is_some()) .await; @@ -1774,7 +1798,8 @@ mod tests { }) .await; - let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx)); + let channels_b = cx_b + .add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx)); channels_b .condition(&mut cx_b, |list, _| list.available_channels().is_some()) .await; @@ -1856,19 +1881,20 @@ mod tests { cx_a.foreground().forbid_parking(); let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) + db.add_org_member(org_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) + db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + let channels_a = cx_a + .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx)); channels_a .condition(&mut cx_a, |list, _| list.available_channels().is_some()) .await; @@ -1916,31 +1942,31 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; let mut status_b = client_b.status(); // Create an org that includes these 2 users. let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) + db.add_org_member(org_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_b, &cx_b), false) + db.add_org_member(org_id, client_b.current_user_id(&cx_b), false) .await .unwrap(); // Create a channel that includes all the users. let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) + db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_b, &cx_b), false) + db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false) .await .unwrap(); db.create_channel_message( channel_id, - current_user_id(&user_store_b, &cx_b), + client_b.current_user_id(&cx_b), "hello A, it's B.", OffsetDateTime::now_utc(), 2, @@ -1948,7 +1974,8 @@ mod tests { .await .unwrap(); - let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + let channels_a = cx_a + .add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx)); channels_a .condition(&mut cx_a, |list, _| list.available_channels().is_some()) .await; @@ -1973,7 +2000,8 @@ mod tests { }) .await; - let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b.clone(), client_b, cx)); + let channels_b = cx_b + .add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx)); channels_b .condition(&mut cx_b, |list, _| list.available_channels().is_some()) .await; @@ -2000,7 +2028,7 @@ mod tests { // Disconnect client B, ensuring we can still access its cached channel data. server.forbid_connections(); - server.disconnect_client(current_user_id(&user_store_b, &cx_b)); + server.disconnect_client(client_b.current_user_id(&cx_b)); while !matches!( status_b.recv().await, Some(client::Status::ReconnectionError { .. }) @@ -2131,9 +2159,9 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start().await; - let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; - let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; - let (_client_c, user_store_c) = server.create_client(&mut cx_c, "user_c").await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + let client_c = server.create_client(&mut cx_c, "user_c").await; let fs = Arc::new(FakeFs::new()); @@ -2148,7 +2176,7 @@ mod tests { let worktree_a = Worktree::open_local( client_a.clone(), - user_store_a.clone(), + client_a.user_store.clone(), "/a".as_ref(), fs.clone(), lang_registry.clone(), @@ -2157,17 +2185,20 @@ mod tests { .await .unwrap(); - user_store_a + client_a + .user_store .condition(&cx_a, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) .await; - user_store_b + client_b + .user_store .condition(&cx_b, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) .await; - user_store_c + client_c + .user_store .condition(&cx_c, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec![])])] }) @@ -2182,36 +2213,42 @@ mod tests { client_b.clone(), worktree_id, lang_registry.clone(), - user_store_b.clone(), + client_b.user_store.clone(), &mut cx_b.to_async(), ) .await .unwrap(); - user_store_a + client_a + .user_store .condition(&cx_a, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; - user_store_b + client_b + .user_store .condition(&cx_b, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; - user_store_c + client_c + .user_store .condition(&cx_c, |user_store, _| { contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] }) .await; cx_a.update(move |_| drop(worktree_a)); - user_store_a + client_a + .user_store .condition(&cx_a, |user_store, _| contacts(user_store) == vec![]) .await; - user_store_b + client_b + .user_store .condition(&cx_b, |user_store, _| contacts(user_store) == vec![]) .await; - user_store_c + client_c + .user_store .condition(&cx_c, |user_store, _| contacts(user_store) == vec![]) .await; @@ -2264,17 +2301,15 @@ mod tests { } } - async fn create_client( - &mut self, - cx: &mut TestAppContext, - name: &str, - ) -> (Arc, ModelHandle) { + async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let client_name = name.to_string(); let mut client = Client::new(); let server = self.server.clone(); let connection_killers = self.connection_killers.clone(); let forbid_connections = self.forbid_connections.clone(); + let (connection_id_tx, mut connection_id_rx) = postage::mpsc::channel(16); + Arc::get_mut(&mut client) .unwrap() .override_authenticate(move |cx| { @@ -2294,6 +2329,7 @@ mod tests { let connection_killers = connection_killers.clone(); let forbid_connections = forbid_connections.clone(); let client_name = client_name.clone(); + let connection_id_tx = connection_id_tx.clone(); cx.spawn(move |cx| async move { if forbid_connections.load(SeqCst) { Err(EstablishConnectionError::other(anyhow!( @@ -2303,7 +2339,12 @@ mod tests { let (client_conn, server_conn, kill_conn) = Connection::in_memory(); connection_killers.lock().insert(user_id, kill_conn); cx.background() - .spawn(server.handle_connection(server_conn, client_name, user_id)) + .spawn(server.handle_connection( + server_conn, + client_name, + user_id, + Some(connection_id_tx), + )) .detach(); Ok(client_conn) } @@ -2316,12 +2357,17 @@ mod tests { .await .unwrap(); + let peer_id = PeerId(connection_id_rx.recv().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let mut authed_user = user_store.read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.recv().await.unwrap().is_none() {} - (client, user_store) + TestClient { + client, + peer_id, + user_store, + } } fn disconnect_client(&self, user_id: UserId) { @@ -2377,10 +2423,27 @@ mod tests { } } - fn current_user_id(user_store: &ModelHandle, cx: &TestAppContext) -> UserId { - UserId::from_proto( - user_store.read_with(cx, |user_store, _| user_store.current_user().unwrap().id), - ) + struct TestClient { + client: Arc, + pub peer_id: PeerId, + pub user_store: ModelHandle, + } + + impl Deref for TestClient { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.client + } + } + + impl TestClient { + pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { + UserId::from_proto( + self.user_store + .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), + ) + } } fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> { From 6ce76ca13e5d1c873c52a9465a663f3d1ca792ad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 28 Nov 2021 12:28:10 -0700 Subject: [PATCH 10/13] Render active worktree collaborator avatars in the titlebar --- crates/workspace/src/lib.rs | 102 +++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index ea75277b9c..a9bcd82619 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -5,7 +5,7 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{Authenticate, ChannelList, Client, UserStore}; +use client::{Authenticate, ChannelList, Client, User, UserStore}; use gpui::{ action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext, @@ -27,6 +27,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use theme::Theme; action!(OpenNew, WorkspaceParams); action!(Save); @@ -952,14 +953,64 @@ impl Workspace { } } - fn render_avatar(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme; - if let Some(avatar) = self - .user_store - .read(cx) - .current_user() - .and_then(|user| user.avatar.clone()) - { + fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { + ConstrainedBox::new( + Container::new( + Stack::new() + .with_child( + Align::new( + Label::new("zed".into(), theme.workspace.titlebar.title.clone()) + .boxed(), + ) + .boxed(), + ) + .with_child( + Align::new( + Flex::row() + .with_children(self.render_collaborators(theme, cx)) + .with_child(self.render_avatar( + self.user_store.read(cx).current_user().as_ref(), + theme, + cx, + )) + .with_children(self.render_connection_status()) + .boxed(), + ) + .right() + .boxed(), + ) + .boxed(), + ) + .with_style(theme.workspace.titlebar.container) + .boxed(), + ) + .with_height(32.) + .named("titlebar") + } + + fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext) -> Vec { + let mut elements = Vec::new(); + if let Some(active_worktree) = self.project.read(cx).active_worktree() { + let users = active_worktree + .read(cx) + .collaborators() + .values() + .map(|c| c.user.clone()) + .collect::>(); + for user in users { + elements.push(self.render_avatar(Some(&user), theme, cx)); + } + } + elements + } + + fn render_avatar( + &self, + user: Option<&Arc>, + theme: &Theme, + cx: &mut RenderContext, + ) -> ElementBox { + if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { ConstrainedBox::new( Align::new( ConstrainedBox::new( @@ -1008,38 +1059,7 @@ impl View for Workspace { let theme = &settings.theme; Container::new( Flex::column() - .with_child( - ConstrainedBox::new( - Container::new( - Stack::new() - .with_child( - Align::new( - Label::new( - "zed".into(), - theme.workspace.titlebar.title.clone(), - ) - .boxed(), - ) - .boxed(), - ) - .with_child( - Align::new( - Flex::row() - .with_children(self.render_connection_status()) - .with_child(self.render_avatar(cx)) - .boxed(), - ) - .right() - .boxed(), - ) - .boxed(), - ) - .with_style(theme.workspace.titlebar.container) - .boxed(), - ) - .with_height(32.) - .named("titlebar"), - ) + .with_child(self.render_titlebar(&theme, cx)) .with_child( Expanded::new( 1.0, From a5039cad650403dbad34fdd629ba68bc0a77310c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 28 Nov 2021 12:41:46 -0700 Subject: [PATCH 11/13] Tweak avatar sizes in titlebar --- crates/theme/src/lib.rs | 1 + crates/workspace/src/lib.rs | 2 +- crates/zed/assets/themes/_base.toml | 9 +++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 1a484c6db7..431937cc7e 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -42,6 +42,7 @@ pub struct Workspace { pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, + pub height: f32, pub title: TextStyle, pub avatar_width: f32, pub offline_icon: OfflineIcon, diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index a9bcd82619..0f636c23eb 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -984,7 +984,7 @@ impl Workspace { .with_style(theme.workspace.titlebar.container) .boxed(), ) - .with_height(32.) + .with_height(theme.workspace.titlebar.height) .named("titlebar") } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index dca2e06028..40868262fb 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -6,9 +6,10 @@ background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } [workspace.titlebar] +height = 32 border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" -avatar_width = 20 +avatar_width = 18 icon_color = "$text.2.color" avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } } outdated_warning = { extends = "$text.2", size = 13 } @@ -46,7 +47,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -width = 32 +width = 30 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] @@ -149,7 +150,7 @@ color = "$text.1.color" [contacts_panel] extends = "$panel" host_row_height = 28 -host_avatar = { corner_radius = 10, width = 20 } +host_avatar = { corner_radius = 10, width = 18 } host_username = { extends = "$text.0", padding.left = 8 } tree_branch_width = 1 tree_branch_color = "$surface.2" @@ -157,7 +158,7 @@ tree_branch_color = "$surface.2" [contacts_panel.worktree] height = 24 padding = { left = 8 } -guest_avatar = { corner_radius = 8, width = 16 } +guest_avatar = { corner_radius = 8, width = 14 } guest_avatar_spacing = 4 [contacts_panel.worktree.name] From fbc307cd5ebddddb2d51cd27d532accd0fa8b117 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 28 Nov 2021 13:25:05 -0700 Subject: [PATCH 12/13] Associate collaborator avatars with "ribbons" corresponding to their cursor color --- crates/editor/src/element.rs | 7 +--- crates/gpui/src/elements/align.rs | 5 +++ crates/project/src/worktree.rs | 2 +- crates/theme/src/lib.rs | 18 +++++++++ crates/workspace/src/lib.rs | 59 +++++++++++++++++++++-------- crates/zed/assets/themes/_base.toml | 3 +- 6 files changed, 70 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 03b04c8c6b..2f4923985f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -371,12 +371,7 @@ impl EditorElement { let content_origin = bounds.origin() + layout.text_offset; for (replica_id, selections) in &layout.selections { - let style_ix = *replica_id as usize % (style.guest_selections.len() + 1); - let style = if style_ix == 0 { - &style.selection - } else { - &style.guest_selections[style_ix - 1] - }; + let style = style.replica_selection_style(*replica_id); for selection in selections { if selection.start != selection.end { diff --git a/crates/gpui/src/elements/align.rs b/crates/gpui/src/elements/align.rs index 7f065e2b53..ce99437f3b 100644 --- a/crates/gpui/src/elements/align.rs +++ b/crates/gpui/src/elements/align.rs @@ -25,6 +25,11 @@ impl Align { self } + pub fn bottom(mut self) -> Self { + self.alignment.set_y(1.0); + self + } + pub fn left(mut self) -> Self { self.alignment.set_x(-1.0); self diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 50f5b2d044..7187310be7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -63,7 +63,7 @@ pub enum Event { Closed, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Collaborator { pub user: Arc, pub peer_id: PeerId, diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 431937cc7e..716ce4a46a 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -45,6 +45,7 @@ pub struct Titlebar { pub height: f32, pub title: TextStyle, pub avatar_width: f32, + pub avatar_ribbon: AvatarRibbon, pub offline_icon: OfflineIcon, pub icon_color: Color, pub avatar: ImageStyle, @@ -53,6 +54,14 @@ pub struct Titlebar { pub outdated_warning: ContainedText, } +#[derive(Clone, Deserialize, Default)] +pub struct AvatarRibbon { + #[serde(flatten)] + pub container: ContainerStyle, + pub width: f32, + pub height: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct OfflineIcon { #[serde(flatten)] @@ -276,6 +285,15 @@ impl EditorStyle { pub fn placeholder_text(&self) -> &TextStyle { self.placeholder_text.as_ref().unwrap_or(&self.text) } + + pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle { + let style_ix = replica_id as usize % (self.guest_selections.len() + 1); + if style_ix == 0 { + &self.selection + } else { + &self.guest_selections[style_ix - 1] + } + } } impl InputEditorStyle { diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 0f636c23eb..9c6d0d43c2 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -968,11 +968,17 @@ impl Workspace { Align::new( Flex::row() .with_children(self.render_collaborators(theme, cx)) - .with_child(self.render_avatar( - self.user_store.read(cx).current_user().as_ref(), - theme, - cx, - )) + .with_child( + self.render_avatar( + self.user_store.read(cx).current_user().as_ref(), + self.project + .read(cx) + .active_worktree() + .map(|worktree| worktree.read(cx).replica_id()), + theme, + cx, + ), + ) .with_children(self.render_connection_status()) .boxed(), ) @@ -991,14 +997,19 @@ impl Workspace { fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext) -> Vec { let mut elements = Vec::new(); if let Some(active_worktree) = self.project.read(cx).active_worktree() { - let users = active_worktree + let collaborators = active_worktree .read(cx) .collaborators() .values() - .map(|c| c.user.clone()) + .cloned() .collect::>(); - for user in users { - elements.push(self.render_avatar(Some(&user), theme, cx)); + for collaborator in collaborators { + elements.push(self.render_avatar( + Some(&collaborator.user), + Some(collaborator.replica_id), + theme, + cx, + )); } } elements @@ -1007,21 +1018,37 @@ impl Workspace { fn render_avatar( &self, user: Option<&Arc>, + replica_id: Option, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { ConstrainedBox::new( - Align::new( - ConstrainedBox::new( - Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) + Stack::new() + .with_child( + ConstrainedBox::new( + Image::new(avatar) + .with_style(theme.workspace.titlebar.avatar) + .boxed(), + ) + .with_width(theme.workspace.titlebar.avatar_width) + .aligned() + .boxed(), + ) + .with_child( + Container::new(Empty::new().boxed()) + .with_style(theme.workspace.titlebar.avatar_ribbon.container) + .with_background_color(replica_id.map_or(Default::default(), |id| { + theme.editor.replica_selection_style(id).cursor + })) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() .boxed(), ) - .with_width(theme.workspace.titlebar.avatar_width) .boxed(), - ) - .boxed(), ) .with_width(theme.workspace.right_sidebar.width) .boxed() diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 40868262fb..7d25ee87fc 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -10,8 +10,9 @@ height = 32 border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" avatar_width = 18 -icon_color = "$text.2.color" avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } } +avatar_ribbon = { background = "#ff0000", height = 3, width = 12 } +icon_color = "$text.2.color" outdated_warning = { extends = "$text.2", size = 13 } [workspace.titlebar.sign_in_prompt] From 815cc7ee91f363a3b1d61d3156f3248a51fef735 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 28 Nov 2021 14:04:31 -0700 Subject: [PATCH 13/13] Give avatar ribbons a rounded top --- crates/workspace/src/lib.rs | 96 ++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 9c6d0d43c2..1f11f11693 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -7,7 +7,13 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use gpui::{ - action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle, + action, + color::Color, + elements::*, + geometry::{vector::vec2f, PathBuilder}, + json::{self, to_string_pretty, ToJson}, + keymap::Binding, + platform::CursorStyle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; @@ -1036,17 +1042,15 @@ impl Workspace { .boxed(), ) .with_child( - Container::new(Empty::new().boxed()) - .with_style(theme.workspace.titlebar.avatar_ribbon.container) - .with_background_color(replica_id.map_or(Default::default(), |id| { - theme.editor.replica_selection_style(id).cursor - })) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed(), + AvatarRibbon::new(replica_id.map_or(Default::default(), |id| { + theme.editor.replica_selection_style(id).cursor + })) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), ) .boxed(), ) @@ -1154,3 +1158,71 @@ impl WorkspaceHandle for ViewHandle { .collect::>() } } + +pub struct AvatarRibbon { + color: Color, +} + +impl AvatarRibbon { + pub fn new(color: Color) -> AvatarRibbon { + AvatarRibbon { color } + } +} + +impl Element for AvatarRibbon { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + _: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + _: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + let mut path = PathBuilder::new(); + path.reset(bounds.lower_left()); + path.curve_to( + bounds.origin() + vec2f(bounds.height(), 0.), + bounds.origin(), + ); + path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.)); + path.curve_to(bounds.lower_right(), bounds.upper_right()); + path.line_to(bounds.lower_left()); + cx.scene.push_path(path.build(self.color, None)); + } + + fn dispatch_event( + &mut self, + _: &gpui::Event, + _: gpui::geometry::rect::RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + _: &mut gpui::EventContext, + ) -> bool { + false + } + + fn debug( + &self, + bounds: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::DebugContext, + ) -> gpui::json::Value { + json::json!({ + "type": "AvatarRibbon", + "bounds": bounds.to_json(), + "color": self.color.to_json(), + }) + } +}