From 614ee4eac7b71a50524175278b9227cba4e4389e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 20 Dec 2021 11:36:59 -0800 Subject: [PATCH] Send worktree info only when sharing worktree Co-Authored-By: Antonio Scandurra Co-Authored-By: Nathan Sobo --- crates/contacts_panel/src/contacts_panel.rs | 107 ++++---------- crates/diagnostics/src/diagnostics.rs | 2 +- crates/gpui/src/executor.rs | 8 ++ crates/project/src/project.rs | 88 +++++++----- crates/project/src/worktree.rs | 146 +++++++++++--------- crates/project_panel/src/project_panel.rs | 5 +- crates/rpc/proto/zed.proto | 7 +- crates/rpc/src/proto.rs | 3 +- crates/theme/src/theme.rs | 8 +- crates/workspace/src/workspace.rs | 34 ++--- 10 files changed, 200 insertions(+), 208 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 8cf8b9191b..e3db1931af 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -11,16 +11,10 @@ use postage::watch; use theme::Theme; use workspace::{Settings, Workspace}; -action!(JoinWorktree, u64); -action!(LeaveWorktree, u64); -action!(ShareWorktree, u64); -action!(UnshareWorktree, u64); +action!(JoinProject, u64); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContactsPanel::share_worktree); - cx.add_action(ContactsPanel::unshare_worktree); - cx.add_action(ContactsPanel::join_worktree); - cx.add_action(ContactsPanel::leave_worktree); + cx.add_action(ContactsPanel::join_project); } pub struct ContactsPanel { @@ -63,44 +57,8 @@ impl ContactsPanel { } } - fn share_worktree( - workspace: &mut Workspace, - action: &ShareWorktree, - cx: &mut ViewContext, - ) { - workspace - .project() - .update(cx, |p, cx| p.share_worktree(action.0, cx)); - } - - fn unshare_worktree( - workspace: &mut Workspace, - action: &UnshareWorktree, - cx: &mut ViewContext, - ) { - workspace - .project() - .update(cx, |p, cx| p.unshare_worktree(action.0, cx)); - } - - fn join_worktree( - workspace: &mut Workspace, - action: &JoinWorktree, - cx: &mut ViewContext, - ) { - workspace - .project() - .update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach()); - } - - fn leave_worktree( - workspace: &mut Workspace, - action: &LeaveWorktree, - cx: &mut ViewContext, - ) { - workspace - .project() - .update(cx, |p, cx| p.close_remote_worktree(action.0, cx)); + fn join_project(_: &mut Workspace, _: &JoinProject, _: &mut ViewContext) { + todo!(); } fn update_contacts(&mut self, _: ModelHandle, cx: &mut ViewContext) { @@ -116,16 +74,12 @@ impl ContactsPanel { cx: &mut LayoutContext, ) -> ElementBox { let theme = &theme.contacts_panel; - let worktree_count = collaborator.worktrees.len(); + let project_count = collaborator.projects.len(); let font_cache = cx.font_cache(); - let line_height = theme.unshared_worktree.name.text.line_height(font_cache); - let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache); - let baseline_offset = theme - .unshared_worktree - .name - .text - .baseline_offset(font_cache) - + (theme.unshared_worktree.height - line_height) / 2.; + let line_height = theme.unshared_project.name.text.line_height(font_cache); + let cap_height = theme.unshared_project.name.text.cap_height(font_cache); + let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache) + + (theme.unshared_project.height - line_height) / 2.; let tree_branch_width = theme.tree_branch_width; let tree_branch_color = theme.tree_branch_color; let host_avatar_height = theme @@ -161,11 +115,11 @@ impl ContactsPanel { ) .with_children( collaborator - .worktrees + .projects .iter() .enumerate() - .map(|(ix, worktree)| { - let worktree_id = worktree.id; + .map(|(ix, project)| { + let project_id = project.id; Flex::row() .with_child( @@ -182,7 +136,7 @@ impl ContactsPanel { vec2f(start_x, start_y), vec2f( start_x + tree_branch_width, - if ix + 1 == worktree_count { + if ix + 1 == project_count { end_y } else { bounds.max_y() @@ -210,28 +164,27 @@ impl ContactsPanel { .with_child({ let is_host = Some(collaborator.user.id) == current_user_id; let is_guest = !is_host - && worktree + && project .guests .iter() .any(|guest| Some(guest.id) == current_user_id); - let is_shared = worktree.is_shared; + let is_shared = project.is_shared; MouseEventHandler::new::( - worktree_id as usize, + project_id as usize, cx, |mouse_state, _| { - let style = match (worktree.is_shared, mouse_state.hovered) - { - (false, false) => &theme.unshared_worktree, - (false, true) => &theme.hovered_unshared_worktree, - (true, false) => &theme.shared_worktree, - (true, true) => &theme.hovered_shared_worktree, + let style = match (project.is_shared, mouse_state.hovered) { + (false, false) => &theme.unshared_project, + (false, true) => &theme.hovered_unshared_project, + (true, false) => &theme.shared_project, + (true, true) => &theme.hovered_shared_project, }; Flex::row() .with_child( Label::new( - worktree.root_name.clone(), + project.worktree_root_names.join(", "), style.name.text.clone(), ) .aligned() @@ -240,7 +193,7 @@ impl ContactsPanel { .with_style(style.name.container) .boxed(), ) - .with_children(worktree.guests.iter().filter_map( + .with_children(project.guests.iter().filter_map( |participant| { participant.avatar.clone().map(|avatar| { Image::new(avatar) @@ -268,23 +221,15 @@ impl ContactsPanel { CursorStyle::Arrow }) .on_click(move |cx| { - if is_shared { - if is_host { - cx.dispatch_action(UnshareWorktree(worktree_id)); - } else if is_guest { - cx.dispatch_action(LeaveWorktree(worktree_id)); - } else { - cx.dispatch_action(JoinWorktree(worktree_id)) - } - } else if is_host { - cx.dispatch_action(ShareWorktree(worktree_id)); + if !is_host && !is_guest { + cx.dispatch_action(JoinProject(project_id)) } }) .expanded(1.0) .boxed() }) .constrained() - .with_height(theme.unshared_worktree.height) + .with_height(theme.unshared_project.height) .boxed() }), ) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f39803f148..def194b724 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -213,7 +213,7 @@ impl workspace::Item for ProjectDiagnostics { }) .detach(); - ProjectDiagnosticsEditor::new(project.read(cx).replica_id(cx), settings, cx) + ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx) } fn project_path(&self) -> Option { diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index c5f976e6f5..23b870c11f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -54,6 +54,7 @@ type AnyLocalTask = async_task::Task>; #[must_use] pub enum Task { + Ready(Option), Local { any_task: AnyLocalTask, result_type: PhantomData, @@ -594,6 +595,10 @@ pub fn deterministic(seed: u64) -> (Rc, Arc) { } impl Task { + pub fn ready(value: T) -> Self { + Self::Ready(Some(value)) + } + fn local(any_task: AnyLocalTask) -> Self { Self::Local { any_task, @@ -603,6 +608,7 @@ impl Task { pub fn detach(self) { match self { + Task::Ready(_) => {} Task::Local { any_task, .. } => any_task.detach(), Task::Send { any_task, .. } => any_task.detach(), } @@ -621,6 +627,7 @@ impl Task { impl fmt::Debug for Task { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Task::Ready(value) => value.fmt(f), Task::Local { any_task, .. } => any_task.fmt(f), Task::Send { any_task, .. } => any_task.fmt(f), } @@ -632,6 +639,7 @@ impl Future for Task { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match unsafe { self.get_unchecked_mut() } { + Task::Ready(value) => Poll::Ready(value.take().unwrap()), Task::Local { any_task, .. } => { any_task.poll(cx).map(|value| *value.downcast().unwrap()) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2ee742af7e..0fd26f4797 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -213,7 +213,8 @@ impl Project { subscriptions: vec![ 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_register_worktree), + client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree), + client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree), client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree), client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), @@ -231,14 +232,6 @@ impl Project { *remote_id_tx.borrow_mut() = remote_id; } - for worktree in &self.worktrees { - worktree.update(cx, |worktree, _| { - if let Some(worktree) = worktree.as_local_mut() { - worktree.set_project_remote_id(remote_id); - } - }); - } - self.subscriptions.clear(); if let Some(remote_id) = remote_id { self.subscriptions.extend([ @@ -307,7 +300,11 @@ impl Project { this.update(&mut cx, |this, cx| { for worktree in &this.worktrees { worktree.update(cx, |worktree, cx| { - worktree.as_local_mut().unwrap().share(cx).detach(); + worktree + .as_local_mut() + .unwrap() + .share(remote_id, cx) + .detach(); }); } }); @@ -327,6 +324,13 @@ impl Project { } } + fn is_shared(&self) -> bool { + match &self.client_state { + ProjectClientState::Local { is_shared, .. } => *is_shared, + ProjectClientState::Remote { .. } => false, + } + } + pub fn add_local_worktree( &mut self, abs_path: &Path, @@ -337,32 +341,35 @@ impl Project { 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 { + cx.spawn(|project, mut cx| async move { let worktree = Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx) .await?; - this.update(&mut cx, |this, cx| { - if let Some(project_id) = this.remote_id() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.set_project_remote_id(Some(project_id)); - let serialized_worktree = worktree.to_proto(cx); - let authorized_logins = worktree.authorized_logins(); - cx.foreground() - .spawn(async move { - client - .request(proto::RegisterWorktree { - project_id, - worktree: Some(serialized_worktree), - authorized_logins, - }) - .log_err(); - }) - .detach(); - }); - } - this.add_worktree(worktree.clone(), cx); + + let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| { + project.add_worktree(worktree.clone(), cx); + (project.remote_id(), project.is_shared()) }); + + if let Some(project_id) = remote_project_id { + let register_message = worktree.update(&mut cx, |worktree, _| { + let worktree = worktree.as_local_mut().unwrap(); + proto::RegisterWorktree { + project_id, + root_name: worktree.root_name().to_string(), + authorized_logins: worktree.authorized_logins(), + } + }); + client.request(register_message).await?; + if is_shared { + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_local_mut().unwrap().share(project_id, cx) + }) + .await?; + } + } + Ok(worktree) }) } @@ -476,9 +483,9 @@ impl Project { Ok(()) } - fn handle_register_worktree( + fn handle_share_worktree( &mut self, - envelope: TypedEnvelope, + envelope: TypedEnvelope, client: Arc, cx: &mut ModelContext, ) -> Result<()> { @@ -505,6 +512,19 @@ impl Project { Ok(()) } + fn handle_unregister_worktree( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + self.worktrees.retain(|worktree| { + worktree.read(cx).as_remote().unwrap().remote_id() != envelope.payload.worktree_id + }); + cx.notify(); + Ok(()) + } + fn handle_update_worktree( &mut self, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 44259f9aed..0ec03af08d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -208,7 +208,7 @@ impl Worktree { } Worktree::Remote(RemoteWorktree { - project_remote_id, + project_id: project_remote_id, remote_id, replica_id, snapshot, @@ -236,6 +236,14 @@ impl Worktree { } } + pub fn as_remote(&self) -> Option<&RemoteWorktree> { + if let Worktree::Remote(worktree) = self { + Some(worktree) + } else { + None + } + } + pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> { if let Worktree::Local(worktree) = self { Some(worktree) @@ -483,8 +491,10 @@ impl Worktree { let sender_id = envelope.original_sender_id()?; let this = self.as_local().unwrap(); let project_id = this - .project_remote_id - .ok_or_else(|| anyhow!("can't save buffer while disconnected"))?; + .share + .as_ref() + .ok_or_else(|| anyhow!("can't save buffer while disconnected"))? + .project_id; let buffer = this .shared_buffers @@ -756,13 +766,12 @@ impl Worktree { operation: Operation, cx: &mut ModelContext, ) { - if let Some((rpc, project_id)) = match self { + if let Some((project_id, rpc)) = match self { Worktree::Local(worktree) => worktree - .project_remote_id - .map(|id| (worktree.client.clone(), id)), - Worktree::Remote(worktree) => { - Some((worktree.client.clone(), worktree.project_remote_id)) - } + .share + .as_ref() + .map(|share| (share.project_id, worktree.client.clone())), + Worktree::Remote(worktree) => Some((worktree.project_id, worktree.client.clone())), } { cx.spawn(|worktree, mut cx| async move { if let Err(error) = rpc @@ -809,7 +818,6 @@ pub struct LocalWorktree { background_snapshot: Arc>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, - project_remote_id: Option, poll_task: Option>, share: Option, loading_buffers: LoadingBuffers, @@ -826,11 +834,12 @@ pub struct LocalWorktree { } struct ShareState { + project_id: u64, snapshots_tx: Sender, } pub struct RemoteWorktree { - project_remote_id: u64, + project_id: u64, remote_id: u64, snapshot: Snapshot, snapshot_rx: watch::Receiver, @@ -913,7 +922,6 @@ impl LocalWorktree { let tree = Self { snapshot: snapshot.clone(), config, - project_remote_id: None, background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, @@ -965,10 +973,6 @@ impl LocalWorktree { Ok((tree, scan_states_tx)) } - pub fn set_project_remote_id(&mut self, id: Option) { - self.project_remote_id = id; - } - pub fn authorized_logins(&self) -> Vec { self.config.collaborators.clone() } @@ -1244,63 +1248,54 @@ impl LocalWorktree { }) } - pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + pub fn share( + &mut self, + project_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if self.share.is_some() { + return Task::ready(Ok(())); + } + let snapshot = self.snapshot(); let rpc = self.client.clone(); - let project_id = self.project_remote_id; let worktree_id = cx.model_id() as u64; - cx.spawn(|this, mut cx| async move { - let project_id = project_id.ok_or_else(|| anyhow!("no project id"))?; + let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::(); + self.share = Some(ShareState { + project_id, + snapshots_tx: snapshots_to_send_tx, + }); - let (snapshots_to_send_tx, snapshots_to_send_rx) = - smol::channel::unbounded::(); - cx.background() - .spawn({ - let rpc = rpc.clone(); - async move { - let mut prev_snapshot = snapshot; - while let Ok(snapshot) = snapshots_to_send_rx.recv().await { - let message = snapshot.build_update( - &prev_snapshot, - project_id, - worktree_id, - false, - ); - match rpc.send(message).await { - Ok(()) => prev_snapshot = snapshot, - Err(err) => log::error!("error sending snapshot diff {}", err), - } + cx.background() + .spawn({ + let rpc = rpc.clone(); + let snapshot = snapshot.clone(); + async move { + let mut prev_snapshot = snapshot; + while let Ok(snapshot) = snapshots_to_send_rx.recv().await { + let message = + snapshot.build_update(&prev_snapshot, project_id, worktree_id, false); + match rpc.send(message).await { + Ok(()) => prev_snapshot = snapshot, + Err(err) => log::error!("error sending snapshot diff {}", err), } } - }) - .detach(); + } + }) + .detach(); - this.update(&mut cx, |worktree, _| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.share = Some(ShareState { - snapshots_tx: snapshots_to_send_tx, - }); - }); + let share_message = cx.background().spawn(async move { + proto::ShareWorktree { + project_id, + worktree: Some(snapshot.to_proto()), + } + }); + cx.foreground().spawn(async move { + rpc.request(share_message.await).await?; Ok(()) }) } - - pub fn to_proto(&self, cx: &mut ModelContext) -> proto::Worktree { - let id = cx.model_id() as u64; - let snapshot = self.snapshot(); - let root_name = self.root_name.clone(); - proto::Worktree { - id, - root_name, - entries: snapshot - .entries_by_path - .cursor::<()>() - .filter(|e| !e.is_ignored) - .map(Into::into) - .collect(), - } - } } fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { @@ -1339,6 +1334,10 @@ impl fmt::Debug for LocalWorktree { } impl RemoteWorktree { + pub fn remote_id(&self) -> u64 { + self.remote_id + } + fn get_open_buffer( &mut self, path: &Path, @@ -1368,7 +1367,7 @@ impl RemoteWorktree { ) -> Task>> { let rpc = self.client.clone(); let replica_id = self.replica_id; - let project_id = self.project_remote_id; + let project_id = self.project_id; let remote_worktree_id = self.remote_id; let root_path = self.snapshot.abs_path.clone(); let path: Arc = Arc::from(path); @@ -1481,6 +1480,20 @@ impl Snapshot { self.id } + pub fn to_proto(&self) -> proto::Worktree { + let root_name = self.root_name.clone(); + proto::Worktree { + id: self.id as u64, + root_name, + entries: self + .entries_by_path + .cursor::<()>() + .filter(|e| !e.is_ignored) + .map(Into::into) + .collect(), + } + } + pub fn build_update( &self, other: &Self, @@ -1540,6 +1553,7 @@ impl Snapshot { proto::UpdateWorktree { project_id, worktree_id, + root_name: self.root_name().to_string(), updated_entries, removed_entries, } @@ -1902,7 +1916,7 @@ impl language::File for File { self.worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { let rpc = worktree.client.clone(); - let project_id = worktree.project_remote_id; + let project_id = worktree.share.as_ref().map(|share| share.project_id); let save = worktree.save(self.path.clone(), text, cx); cx.background().spawn(async move { let entry = save.await?; @@ -1921,7 +1935,7 @@ impl language::File for File { } Worktree::Remote(worktree) => { let rpc = worktree.client.clone(); - let project_id = worktree.project_remote_id; + let project_id = worktree.project_id; cx.foreground().spawn(async move { let response = rpc .request(proto::SaveBuffer { @@ -1961,7 +1975,7 @@ impl language::File for File { let worktree_id = self.worktree.id() as u64; self.worktree.update(cx, |worktree, cx| { if let Worktree::Remote(worktree) = worktree { - let project_id = worktree.project_remote_id; + let project_id = worktree.project_id; let rpc = worktree.client.clone(); cx.background() .spawn(async move { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d8ce1df3c7..c96e478c26 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -617,12 +617,13 @@ mod tests { ) .await; - let project = cx.add_model(|_| { - Project::new( + let project = cx.add_model(|cx| { + Project::local( params.languages.clone(), params.client.clone(), params.user_store.clone(), params.fs.clone(), + cx, ) }); let root1 = project diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 4a0081ce7f..b86a4c1e30 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -96,7 +96,7 @@ message LeaveProject { message RegisterWorktree { uint64 project_id = 1; - Worktree worktree = 2; + string root_name = 2; repeated string authorized_logins = 3; } @@ -113,8 +113,9 @@ message ShareWorktree { message UpdateWorktree { uint64 project_id = 1; uint64 worktree_id = 2; - repeated Entry updated_entries = 3; - repeated uint64 removed_entries = 4; + string root_name = 3; + repeated Entry updated_entries = 4; + repeated uint64 removed_entries = 5; } message AddProjectCollaborator { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8eb62d8ce0..de338fe43f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -152,6 +152,7 @@ messages!( ShareWorktree, UnregisterProject, UnregisterWorktree, + UnshareProject, UpdateBuffer, UpdateContacts, UpdateWorktree, @@ -183,7 +184,7 @@ entity_messages!( OpenBuffer, CloseBuffer, SaveBuffer, - RegisterWorktree, + ShareWorktree, UnregisterWorktree, UpdateBuffer, UpdateWorktree, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6f685ce70d..6fca696689 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -155,10 +155,10 @@ pub struct ContactsPanel { pub host_username: ContainedText, pub tree_branch_width: f32, pub tree_branch_color: Color, - pub shared_worktree: WorktreeRow, - pub hovered_shared_worktree: WorktreeRow, - pub unshared_worktree: WorktreeRow, - pub hovered_unshared_worktree: WorktreeRow, + pub shared_project: WorktreeRow, + pub hovered_shared_project: WorktreeRow, + pub unshared_project: WorktreeRow, + pub hovered_unshared_project: WorktreeRow, } #[derive(Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6f0aa75032..090aae5567 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -358,12 +358,13 @@ pub struct Workspace { impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { - let project = cx.add_model(|_| { - Project::new( + let project = cx.add_model(|cx| { + Project::local( params.languages.clone(), params.client.clone(), params.user_store.clone(), params.fs.clone(), + cx, ) }); cx.observe(&project, |_, _, cx| cx.notify()).detach(); @@ -988,24 +989,25 @@ 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 collaborators = active_worktree - .read(cx) - .collaborators() - .values() - .cloned() - .collect::>(); - for collaborator in collaborators { - elements.push(self.render_avatar( + let mut collaborators = self + .project + .read(cx) + .collaborators() + .values() + .cloned() + .collect::>(); + collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id); + collaborators + .into_iter() + .map(|collaborator| { + self.render_avatar( Some(&collaborator.user), Some(collaborator.replica_id), theme, cx, - )); - } - } - elements + ) + }) + .collect() } fn render_avatar(