From 0ca9f286c6da11e718e23a54972e8fe1ef3363a4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 21:43:44 -0700 Subject: [PATCH 1/4] 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, ) }, ) From ef0432da0db27d3250c1c76b72aacb4c32594316 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 10:14:20 -0700 Subject: [PATCH 2/4] Hide cursors by default, but show some --- crates/editor/src/editor.rs | 14 ++++++++++++++ crates/editor/src/element.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d8918d3f29..6b449f4e73 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -569,6 +569,8 @@ pub struct Editor { project: Option>, collaboration_hub: Option>, blink_manager: Model, + recently_focused: bool, + hovered_selections: HashSet<(ReplicaId, usize)>, pub show_local_selections: bool, mode: EditorMode, show_gutter: bool, @@ -1808,6 +1810,8 @@ impl Editor { pixel_position_of_newest_cursor: None, gutter_width: Default::default(), style: None, + recently_focused: false, + hovered_selections: Default::default(), editor_actions: Default::default(), show_copilot_suggestions: mode == EditorMode::Full, _subscriptions: vec![ @@ -9196,6 +9200,16 @@ impl Editor { cx.focus(&rename_editor_focus_handle); } else { self.blink_manager.update(cx, BlinkManager::enable); + self.recently_focused = true; + cx.spawn(|this, mut cx| async move { + cx.background_executor().timer(Duration::from_secs(5)).await; + this.update(&mut cx, |this, cx| { + this.recently_focused = false; + cx.notify() + }) + .ok() + }) + .detach(); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); if self.leader_peer_id.is_none() { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4e7a3bc243..cf3b1d2f16 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -586,6 +586,32 @@ impl EditorElement { } } + fn update_visible_cursor( + editor: &mut Editor, + point: DisplayPoint, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx); + if let Some(hub) = editor.collaboration_hub() { + let range = if point.column() > 0 { + DisplayPoint::new(point.row(), point.column() - 1)..point + } else { + point..DisplayPoint::new(point.row(), point.column() + 1) + }; + let range = snapshot + .buffer_snapshot + .anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left) + ..snapshot + .buffer_snapshot + .anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right); + for selection in snapshot.remote_selections_in_range(&range, hub, cx) { + let key = (selection.replica_id, selection.selection.id); + editor.hovered_selections.insert(key); + } + } + editor.hovered_selections.clear(); + } + fn paint_background( &self, gutter_bounds: Bounds, @@ -1962,6 +1988,7 @@ impl EditorElement { if Some(selection.peer_id) == editor.leader_peer_id { continue; } + let id = (selection.replica_id, selection.selection.id); remote_selections .entry(selection.replica_id) @@ -1974,7 +2001,11 @@ impl EditorElement { &snapshot.display_snapshot, false, false, - selection.user_name, + if editor.recently_focused || editor.hovered_selections.contains(&id) { + selection.user_name + } else { + None + }, )); } From e513020fbbca97e9e0d45f9a4be7399759123fc8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 15:59:13 -0700 Subject: [PATCH 3/4] Fine tune --- crates/channel/src/channel_buffer.rs | 12 ++++- crates/client/src/user.rs | 1 - crates/collab/src/db/queries/buffers.rs | 17 ++++++- crates/editor/src/editor.rs | 13 +++-- crates/editor/src/element.rs | 67 ++++++++++++++----------- 5 files changed, 73 insertions(+), 37 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 1aca05ec86..b5f4a06b97 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,6 +1,6 @@ use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; -use client::{Client, Collaborator, UserStore}; +use client::{Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; use language::proto::serialize_version; @@ -181,6 +181,16 @@ impl ChannelBuffer { ) { match event { language::Event::Operation(operation) => { + if *ZED_ALWAYS_ACTIVE { + match operation { + language::Operation::UpdateSelections { selections, .. } => { + if selections.is_empty() { + return; + } + } + _ => {} + } + } let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 75f0acd810..dcab0e5394 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -635,7 +635,6 @@ 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() diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index bdcaaab6ef..c19cd530a0 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -450,8 +450,21 @@ impl Database { )> { self.transaction(move |tx| async move { let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_member(&channel, user, &*tx) - .await?; + + let mut requires_write_permission = false; + for op in operations.iter() { + match op.variant { + None | Some(proto::operation::Variant::UpdateSelections(_)) => {} + Some(_) => requires_write_permission = true, + } + } + if requires_write_permission { + self.check_user_is_channel_member(&channel, user, &*tx) + .await?; + } else { + self.check_user_is_channel_participant(&channel, user, &*tx) + .await?; + } let buffer = buffer::Entity::find() .filter(buffer::Column::ChannelId.eq(channel_id)) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 56709fc79f..8876feecef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -368,7 +368,7 @@ pub struct Editor { collaboration_hub: Option>, blink_manager: Model, recently_focused: bool, - hovered_selections: HashSet<(ReplicaId, usize)>, + hovered_cursor: Option, pub show_local_selections: bool, mode: EditorMode, show_gutter: bool, @@ -420,6 +420,7 @@ pub struct EditorSnapshot { ongoing_scroll: OngoingScroll, } +#[derive(Debug)] pub struct RemoteSelection { pub replica_id: ReplicaId, pub selection: Selection, @@ -444,6 +445,11 @@ enum SelectionHistoryMode { Redoing, } +struct HoveredCursor { + replica_id: u16, + selection_id: usize, +} + impl Default for SelectionHistoryMode { fn default() -> Self { Self::Normal @@ -1608,7 +1614,7 @@ impl Editor { gutter_width: Default::default(), style: None, recently_focused: false, - hovered_selections: Default::default(), + hovered_cursor: Default::default(), editor_actions: Default::default(), show_copilot_suggestions: mode == EditorMode::Full, _subscriptions: vec![ @@ -8998,8 +9004,9 @@ impl Editor { } else { self.blink_manager.update(cx, BlinkManager::enable); self.recently_focused = true; + cx.notify(); cx.spawn(|this, mut cx| async move { - cx.background_executor().timer(Duration::from_secs(5)).await; + cx.background_executor().timer(Duration::from_secs(2)).await; this.update(&mut cx, |this, cx| { this.recently_focused = false; cx.notify() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 942ca9fa64..309f6c7789 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -567,6 +567,7 @@ impl EditorElement { cx, ); hover_at(editor, Some(point), cx); + Self::update_visible_cursor(editor, point, cx); } None => { update_inlay_link_and_hover_points( @@ -594,24 +595,28 @@ impl EditorElement { cx: &mut ViewContext, ) { let snapshot = editor.snapshot(cx); - if let Some(hub) = editor.collaboration_hub() { - let range = if point.column() > 0 { - DisplayPoint::new(point.row(), point.column() - 1)..point - } else { - point..DisplayPoint::new(point.row(), point.column() + 1) - }; - let range = snapshot + let Some(hub) = editor.collaboration_hub() else { + return; + }; + let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1)) + ..DisplayPoint::new(point.row(), point.column() + 1); + + let range = snapshot + .buffer_snapshot + .anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left) + ..snapshot .buffer_snapshot - .anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left) - ..snapshot - .buffer_snapshot - .anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right); - for selection in snapshot.remote_selections_in_range(&range, hub, cx) { - let key = (selection.replica_id, selection.selection.id); - editor.hovered_selections.insert(key); - } - } - editor.hovered_selections.clear(); + .anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right); + + let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else { + editor.hovered_cursor.take(); + return; + }; + editor.hovered_cursor.replace(crate::HoveredCursor { + replica_id: selection.replica_id, + selection_id: selection.selection.id, + }); + cx.notify() } fn paint_background( @@ -1990,7 +1995,7 @@ impl EditorElement { if Some(selection.peer_id) == editor.leader_peer_id { continue; } - let id = (selection.replica_id, selection.selection.id); + let is_shown = editor.recently_focused || editor.hovered_cursor.as_ref().is_some_and(|c| c.replica_id == selection.replica_id && c.selection_id == selection.selection.id); remote_selections .entry(selection.replica_id) @@ -2003,7 +2008,7 @@ impl EditorElement { &snapshot.display_snapshot, false, false, - if editor.recently_focused || editor.hovered_selections.contains(&id) { + if is_shown { selection.user_name } else { None @@ -3209,26 +3214,20 @@ impl Cursor { fill(bounds, self.color) }; - cx.paint_quad(cursor); - - if let Some(block_text) = &self.block_text { - block_text - .paint(self.origin + origin, self.line_height, cx) - .log_err(); - } - if let Some(name) = &self.cursor_name { + let text_size = self.line_height / 1.5; + 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.)) + point(bounds.left(), bounds.top() - text_size / 2. - px(1.)) }; cx.with_z_index(name.z_index, |cx| { div() .bg(self.color) - .text_size(self.line_height / 2.) + .text_size(text_size) .px_0p5() - .line_height(self.line_height / 2. + px(1.)) + .line_height(text_size + px(2.)) .text_color(name.color) .child(name.string.clone()) .into_any_element() @@ -3239,6 +3238,14 @@ impl Cursor { ) }) } + + cx.paint_quad(cursor); + + if let Some(block_text) = &self.block_text { + block_text + .paint(self.origin + origin, self.line_height, cx) + .log_err(); + } } pub fn shape(&self) -> CursorShape { From 276032398bd22952a29052bb827119e00ac161c9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 16:53:05 -0700 Subject: [PATCH 4/4] Fix overflow in hover handler --- crates/editor/src/element.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 309f6c7789..6c6bfba190 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -599,7 +599,10 @@ impl EditorElement { return; }; let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1)) - ..DisplayPoint::new(point.row(), point.column() + 1); + ..DisplayPoint::new( + point.row(), + (point.column() + 1).min(snapshot.line_len(point.row())), + ); let range = snapshot .buffer_snapshot