Detailed changes
@@ -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 {
@@ -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<Client>,
_maintain_contacts: Task<()>,
_maintain_current_user: Task<Result<()>>,
+ weak_self: WeakModel<Self>,
}
#[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<Self>,
+ ) -> Option<Arc<User>> {
+ 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,
@@ -651,6 +669,31 @@ impl UserStore {
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
&self.participant_indices
}
+
+ pub fn participant_names(
+ &self,
+ user_ids: impl Iterator<Item = u64>,
+ cx: &AppContext,
+ ) -> HashMap<u64, SharedString> {
+ 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 {
@@ -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))
@@ -442,4 +442,13 @@ impl CollaborationHub for ChannelBufferCollaborationHub {
) -> &'a HashMap<u64, ParticipantIndex> {
self.0.read(cx).user_store().read(cx).participant_indices()
}
+
+ fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString> {
+ 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)
+ }
}
@@ -367,6 +367,8 @@ pub struct Editor {
project: Option<Model<Project>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
+ recently_focused: bool,
+ hovered_cursor: Option<HoveredCursor>,
pub show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
@@ -418,6 +420,7 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
+#[derive(Debug)]
pub struct RemoteSelection {
pub replica_id: ReplicaId,
pub selection: Selection<Anchor>,
@@ -425,6 +428,7 @@ pub struct RemoteSelection {
pub peer_id: PeerId,
pub line_mode: bool,
pub participant_index: Option<ParticipantIndex>,
+ pub user_name: Option<SharedString>,
}
#[derive(Clone, Debug)]
@@ -441,6 +445,11 @@ enum SelectionHistoryMode {
Redoing,
}
+struct HoveredCursor {
+ replica_id: u16,
+ selection_id: usize,
+}
+
impl Default for SelectionHistoryMode {
fn default() -> Self {
Self::Normal
@@ -1604,6 +1613,8 @@ impl Editor {
pixel_position_of_newest_cursor: None,
gutter_width: Default::default(),
style: None,
+ recently_focused: false,
+ hovered_cursor: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
_subscriptions: vec![
@@ -8992,6 +9003,17 @@ impl Editor {
cx.focus(&rename_editor_focus_handle);
} 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(2)).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() {
@@ -9043,6 +9065,7 @@ pub trait CollaborationHub {
&self,
cx: &'a AppContext,
) -> &'a HashMap<u64, ParticipantIndex>;
+ fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString>;
}
impl CollaborationHub for Model<Project> {
@@ -9056,6 +9079,14 @@ impl CollaborationHub for Model<Project> {
) -> &'a HashMap<u64, ParticipantIndex> {
self.read(cx).user_store().read(cx).participant_indices()
}
+
+ fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString> {
+ 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(
@@ -9107,6 +9138,7 @@ impl EditorSnapshot {
collaboration_hub: &dyn CollaborationHub,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = RemoteSelection> {
+ 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
@@ -9118,6 +9150,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,
@@ -9125,6 +9158,7 @@ impl EditorSnapshot {
line_mode,
participant_index,
peer_id: collaborator.peer_id,
+ user_name,
})
})
}
@@ -64,6 +64,7 @@ struct SelectionLayout {
is_local: bool,
range: Range<DisplayPoint>,
active_rows: Range<u32>,
+ user_name: Option<SharedString>,
}
impl SelectionLayout {
@@ -74,6 +75,7 @@ impl SelectionLayout {
map: &DisplaySnapshot,
is_newest: bool,
is_local: bool,
+ user_name: Option<SharedString>,
) -> 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,
}
}
}
@@ -564,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(
@@ -585,6 +589,39 @@ impl EditorElement {
}
}
+ fn update_visible_cursor(
+ editor: &mut Editor,
+ point: DisplayPoint,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let snapshot = editor.snapshot(cx);
+ 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).min(snapshot.line_len(point.row())),
+ );
+
+ 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);
+
+ 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(
&self,
gutter_bounds: Bounds<Pixels>,
@@ -982,8 +1019,10 @@ impl EditorElement {
let corner_radius = 0.15 * layout.position_map.line_height;
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 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,
@@ -1064,6 +1103,7 @@ impl EditorElement {
))
});
}
+
cursors.push(Cursor {
color: selection_style.cursor,
block_width,
@@ -1071,6 +1111,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(),
+ }
+ }),
});
}
}
@@ -1889,6 +1937,7 @@ impl EditorElement {
&snapshot.display_snapshot,
is_newest,
true,
+ None,
);
if is_newest {
newest_selection_head = Some(layout.head);
@@ -1949,6 +1998,7 @@ impl EditorElement {
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
+ 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)
@@ -1961,6 +2011,11 @@ impl EditorElement {
&snapshot.display_snapshot,
false,
false,
+ if is_shown {
+ selection.user_name
+ } else {
+ None
+ },
));
}
@@ -1992,6 +2047,7 @@ impl EditorElement {
&snapshot.display_snapshot,
true,
true,
+ None,
)
.head
});
@@ -3097,6 +3153,15 @@ pub struct Cursor {
color: Hsla,
shape: CursorShape,
block_text: Option<ShapedLine>,
+ cursor_name: Option<CursorName>,
+}
+
+#[derive(Debug)]
+pub struct CursorName {
+ string: SharedString,
+ color: Hsla,
+ is_top_row: bool,
+ z_index: u8,
}
impl Cursor {
@@ -3107,6 +3172,7 @@ impl Cursor {
color: Hsla,
shape: CursorShape,
block_text: Option<ShapedLine>,
+ cursor_name: Option<CursorName>,
) -> Cursor {
Cursor {
origin,
@@ -3115,6 +3181,7 @@ impl Cursor {
color,
shape,
block_text,
+ cursor_name,
}
}
@@ -3150,6 +3217,31 @@ impl Cursor {
fill(bounds, self.color)
};
+ 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() - text_size / 2. - px(1.))
+ };
+ cx.with_z_index(name.z_index, |cx| {
+ div()
+ .bg(self.color)
+ .text_size(text_size)
+ .px_0p5()
+ .line_height(text_size + px(2.))
+ .text_color(name.color)
+ .child(name.string.clone())
+ .into_any_element()
+ .draw(
+ name_origin,
+ size(AvailableSpace::MinContent, AvailableSpace::MinContent),
+ cx,
+ )
+ })
+ }
+
cx.paint_quad(cursor);
if let Some(block_text) = &self.block_text {
@@ -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))
@@ -550,6 +550,7 @@ impl TerminalElement {
theme.players().local().cursor,
shape,
text,
+ None,
)
},
)