From 0ca9f286c6da11e718e23a54972e8fe1ef3363a4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 21:43:44 -0700 Subject: [PATCH] Show cursors for remote participants --- crates/client/src/user.rs | 46 +++++++++++++++- crates/collab_ui/src/channel_view.rs | 9 ++++ crates/editor/src/editor.rs | 13 +++++ crates/editor/src/element.rs | 55 +++++++++++++++++++- crates/gpui/src/executor.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 1 + 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 4453bb40ea..75f0acd810 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -3,7 +3,10 @@ use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task}; +use gpui::{ + AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUrl, Task, + WeakModel, +}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; @@ -77,6 +80,7 @@ pub struct UserStore { client: Weak, _maintain_contacts: Task<()>, _maintain_current_user: Task>, + weak_self: WeakModel, } #[derive(Clone)] @@ -194,6 +198,7 @@ impl UserStore { Ok(()) }), pending_contact_requests: Default::default(), + weak_self: cx.weak_model(), } } @@ -579,6 +584,19 @@ impl UserStore { self.users.get(&user_id).cloned() } + pub fn get_user_optimistic( + &mut self, + user_id: u64, + cx: &mut ModelContext, + ) -> Option> { + if let Some(user) = self.users.get(&user_id).cloned() { + return Some(user); + } + + self.get_user(user_id, cx).detach_and_log_err(cx); + None + } + pub fn get_user( &mut self, user_id: u64, @@ -617,6 +635,7 @@ impl UserStore { cx.spawn(|this, mut cx| async move { if let Some(rpc) = client.upgrade() { let response = rpc.request(request).await.context("error loading users")?; + dbg!(&response.users); let users = response .users .into_iter() @@ -651,6 +670,31 @@ impl UserStore { pub fn participant_indices(&self) -> &HashMap { &self.participant_indices } + + pub fn participant_names( + &self, + user_ids: impl Iterator, + cx: &AppContext, + ) -> HashMap { + let mut ret = HashMap::default(); + let mut missing_user_ids = Vec::new(); + for id in user_ids { + if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) { + ret.insert(id, github_login.into()); + } else { + missing_user_ids.push(id) + } + } + if !missing_user_ids.is_empty() { + let this = self.weak_self.clone(); + cx.spawn(|mut cx| async move { + this.update(&mut cx, |this, cx| this.get_users(missing_user_ids, cx))? + .await + }) + .detach_and_log_err(cx); + } + ret + } } impl User { diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 033889f771..b2c243dc89 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -442,4 +442,13 @@ impl CollaborationHub for ChannelBufferCollaborationHub { ) -> &'a HashMap { self.0.read(cx).user_store().read(cx).participant_indices() } + + fn user_names(&self, cx: &AppContext) -> HashMap { + let user_ids = self.collaborators(cx).values().map(|c| c.user_id); + self.0 + .read(cx) + .user_store() + .read(cx) + .participant_names(user_ids, cx) + } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30b0a73d37..d8918d3f29 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -627,6 +627,7 @@ pub struct RemoteSelection { pub peer_id: PeerId, pub line_mode: bool, pub participant_index: Option, + pub user_name: Option, } #[derive(Clone, Debug)] @@ -9246,6 +9247,7 @@ pub trait CollaborationHub { &self, cx: &'a AppContext, ) -> &'a HashMap; + fn user_names(&self, cx: &AppContext) -> HashMap; } impl CollaborationHub for Model { @@ -9259,6 +9261,14 @@ impl CollaborationHub for Model { ) -> &'a HashMap { self.read(cx).user_store().read(cx).participant_indices() } + + fn user_names(&self, cx: &AppContext) -> HashMap { + let this = self.read(cx); + let user_ids = this.collaborators().values().map(|c| c.user_id); + this.user_store().read_with(cx, |user_store, cx| { + user_store.participant_names(user_ids, cx) + }) + } } fn inlay_hint_settings( @@ -9310,6 +9320,7 @@ impl EditorSnapshot { collaboration_hub: &dyn CollaborationHub, cx: &'a AppContext, ) -> impl 'a + Iterator { + let participant_names = collaboration_hub.user_names(cx); let participant_indices = collaboration_hub.user_participant_indices(cx); let collaborators_by_peer_id = collaboration_hub.collaborators(cx); let collaborators_by_replica_id = collaborators_by_peer_id @@ -9321,6 +9332,7 @@ impl EditorSnapshot { .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { let collaborator = collaborators_by_replica_id.get(&replica_id)?; let participant_index = participant_indices.get(&collaborator.user_id).copied(); + let user_name = participant_names.get(&collaborator.user_id).cloned(); Some(RemoteSelection { replica_id, selection, @@ -9328,6 +9340,7 @@ impl EditorSnapshot { line_mode, participant_index, peer_id: collaborator.peer_id, + user_name, }) }) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b82bd55bcf..4e7a3bc243 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -64,6 +64,7 @@ struct SelectionLayout { is_local: bool, range: Range, active_rows: Range, + user_name: Option, } impl SelectionLayout { @@ -74,6 +75,7 @@ impl SelectionLayout { map: &DisplaySnapshot, is_newest: bool, is_local: bool, + user_name: Option, ) -> Self { let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let display_selection = point_selection.map(|p| p.to_display_point(map)); @@ -113,6 +115,7 @@ impl SelectionLayout { is_local, range, active_rows, + user_name, } } } @@ -980,8 +983,10 @@ impl EditorElement { let corner_radius = 0.15 * layout.position_map.line_height; let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - for (selection_style, selections) in &layout.selections { - for selection in selections { + for (participant_ix, (selection_style, selections)) in + layout.selections.iter().enumerate() + { + for selection in selections.into_iter() { self.paint_highlighted_range( selection.range.clone(), selection_style.selection, @@ -1062,6 +1067,7 @@ impl EditorElement { )) }); } + cursors.push(Cursor { color: selection_style.cursor, block_width, @@ -1069,6 +1075,14 @@ impl EditorElement { line_height: layout.position_map.line_height, shape: selection.cursor_shape, block_text, + cursor_name: selection.user_name.clone().map(|name| { + CursorName { + string: name, + color: self.style.background, + is_top_row: cursor_position.row() == 0, + z_index: (participant_ix % 256).try_into().unwrap(), + } + }), }); } } @@ -1887,6 +1901,7 @@ impl EditorElement { &snapshot.display_snapshot, is_newest, true, + None, ); if is_newest { newest_selection_head = Some(layout.head); @@ -1959,6 +1974,7 @@ impl EditorElement { &snapshot.display_snapshot, false, false, + selection.user_name, )); } @@ -1990,6 +2006,7 @@ impl EditorElement { &snapshot.display_snapshot, true, true, + None, ) .head }); @@ -3096,6 +3113,15 @@ pub struct Cursor { color: Hsla, shape: CursorShape, block_text: Option, + cursor_name: Option, +} + +#[derive(Debug)] +pub struct CursorName { + string: SharedString, + color: Hsla, + is_top_row: bool, + z_index: u8, } impl Cursor { @@ -3106,6 +3132,7 @@ impl Cursor { color: Hsla, shape: CursorShape, block_text: Option, + cursor_name: Option, ) -> Cursor { Cursor { origin, @@ -3114,6 +3141,7 @@ impl Cursor { color, shape, block_text, + cursor_name, } } @@ -3156,6 +3184,29 @@ impl Cursor { .paint(self.origin + origin, self.line_height, cx) .log_err(); } + + if let Some(name) = &self.cursor_name { + let name_origin = if name.is_top_row { + point(bounds.right() - px(1.), bounds.top()) + } else { + point(bounds.left(), bounds.top() - self.line_height / 4. - px(1.)) + }; + cx.with_z_index(name.z_index, |cx| { + div() + .bg(self.color) + .text_size(self.line_height / 2.) + .px_0p5() + .line_height(self.line_height / 2. + px(1.)) + .text_color(name.color) + .child(name.string.clone()) + .into_any_element() + .draw( + name_origin, + size(AvailableSpace::MinContent, AvailableSpace::MinContent), + cx, + ) + }) + } } pub fn shape(&self) -> CursorShape { diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index fc60cb1ec6..1fe05b2557 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -68,7 +68,7 @@ where /// Run the task to completion in the background and log any /// errors that occur. #[track_caller] - pub fn detach_and_log_err(self, cx: &mut AppContext) { + pub fn detach_and_log_err(self, cx: &AppContext) { let location = core::panic::Location::caller(); cx.foreground_executor() .spawn(self.log_tracked_err(*location)) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1fec041de9..65e013d0e0 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -550,6 +550,7 @@ impl TerminalElement { theme.players().local().cursor, shape, text, + None, ) }, )