From 545b5e0161127e8de7b0896dbe03514af194867d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2023 15:19:38 -0600 Subject: [PATCH] Assign unique color indices to room participants, use those instead of replica_ids Co-authored-by: Conrad Co-authored-by: Antonio --- Cargo.lock | 4 + crates/call/Cargo.toml | 1 + crates/call/src/participant.rs | 2 + crates/call/src/room.rs | 11 + crates/channel/Cargo.toml | 1 + crates/channel/src/channel_buffer.rs | 105 +++---- crates/channel/src/channel_store.rs | 3 +- crates/client/Cargo.toml | 1 + crates/client/src/user.rs | 37 +++ .../20221109000000_test_schema.sql | 3 +- ...0_add_color_index_to_room_participants.sql | 1 + crates/collab/src/db.rs | 2 +- crates/collab/src/db/queries/buffers.rs | 59 ++-- crates/collab/src/db/queries/rooms.rs | 20 ++ .../collab/src/db/tables/room_participant.rs | 1 + crates/collab/src/db/tests/buffer_tests.rs | 4 +- crates/collab/src/rpc.rs | 56 ++-- .../collab/src/tests/channel_buffer_tests.rs | 279 ++++++++++-------- .../src/tests/random_channel_buffer_tests.rs | 2 +- crates/collab/src/tests/test_server.rs | 22 +- crates/collab_ui/src/channel_view.rs | 105 ++----- crates/collab_ui/src/collab_titlebar_item.rs | 18 +- crates/editor/src/editor.rs | 94 ++++-- crates/editor/src/element.rs | 94 +++--- crates/editor/src/items.rs | 12 +- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 27 +- crates/rpc/proto/zed.proto | 278 +++++++++-------- crates/rpc/src/proto.rs | 8 +- crates/theme/src/theme.rs | 14 +- crates/vim/src/vim.rs | 2 +- crates/workspace/src/item.rs | 15 +- crates/workspace/src/pane_group.rs | 34 +-- crates/workspace/src/workspace.rs | 16 +- selection-color-notes.txt | 14 + 35 files changed, 707 insertions(+), 639 deletions(-) create mode 100644 crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql create mode 100644 selection-color-notes.txt diff --git a/Cargo.lock b/Cargo.lock index 3cced78c42..46d8deb62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1079,6 +1079,7 @@ dependencies = [ "serde_derive", "serde_json", "settings", + "theme", "util", ] @@ -1219,6 +1220,7 @@ dependencies = [ "sum_tree", "tempfile", "text", + "theme", "thiserror", "time", "tiny_http", @@ -1390,6 +1392,7 @@ dependencies = [ "sum_tree", "tempfile", "text", + "theme", "thiserror", "time", "tiny_http", @@ -5510,6 +5513,7 @@ dependencies = [ "tempdir", "terminal", "text", + "theme", "thiserror", "toml 0.5.11", "unindent", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index b4e94fe56c..716bc3c27b 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -31,6 +31,7 @@ language = { path = "../language" } media = { path = "../media" } project = { path = "../project" } settings = { path = "../settings" } +theme = { path = "../theme" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index e7858869ce..b0751be919 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -6,6 +6,7 @@ pub use live_kit_client::Frame; use live_kit_client::RemoteAudioTrack; use project::Project; use std::{fmt, sync::Arc}; +use theme::ColorIndex; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { @@ -43,6 +44,7 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub projects: Vec, pub location: ParticipantLocation, + pub color_index: ColorIndex, pub muted: bool, pub speaking: bool, pub video_tracks: HashMap>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ffa941bfa1..e6759d87ca 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -21,6 +21,7 @@ use live_kit_client::{ use postage::stream::Stream; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; +use theme::ColorIndex; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -714,6 +715,7 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), + color_index: ColorIndex(participant.color_index), peer_id, projects: participant.projects, location, @@ -807,6 +809,15 @@ impl Room { let _ = this.leave(cx); } + this.user_store.update(cx, |user_store, cx| { + let color_indices_by_user_id = this + .remote_participants + .iter() + .map(|(user_id, participant)| (*user_id, participant.color_index)) + .collect(); + user_store.set_color_indices(color_indices_by_user_id, cx); + }); + this.check_invariants(); cx.notify(); }); diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 16a1d418d5..e3a74ecbe6 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -23,6 +23,7 @@ language = { path = "../language" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } anyhow.workspace = true futures.workspace = true diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 06f9093fb5..a03eb1f1b5 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,22 +1,25 @@ use crate::Channel; use anyhow::Result; -use client::Client; +use client::{Client, Collaborator, UserStore}; +use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; -use rpc::{proto, TypedEnvelope}; +use rpc::{ + proto::{self, PeerId}, + TypedEnvelope, +}; use std::sync::Arc; use util::ResultExt; pub(crate) fn init(client: &Arc) { client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); - client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); - client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); - client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators); } pub struct ChannelBuffer { pub(crate) channel: Arc, connected: bool, - collaborators: Vec, + collaborators: HashMap, + user_store: ModelHandle, buffer: ModelHandle, buffer_epoch: u64, client: Arc, @@ -46,6 +49,7 @@ impl ChannelBuffer { pub(crate) async fn new( channel: Arc, client: Arc, + user_store: ModelHandle, mut cx: AsyncAppContext, ) -> Result> { let response = client @@ -61,8 +65,6 @@ impl ChannelBuffer { .map(language::proto::deserialize_operation) .collect::, _>>()?; - let collaborators = response.collaborators; - let buffer = cx.add_model(|_| { language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) }); @@ -73,34 +75,45 @@ impl ChannelBuffer { anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - Self { + let mut this = Self { buffer, buffer_epoch: response.epoch, client, connected: true, - collaborators, + collaborators: Default::default(), channel, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), - } + user_store, + }; + this.replace_collaborators(response.collaborators, cx); + this })) } + pub fn user_store(&self) -> &ModelHandle { + &self.user_store + } + pub(crate) fn replace_collaborators( &mut self, collaborators: Vec, cx: &mut ModelContext, ) { - for old_collaborator in &self.collaborators { - if collaborators - .iter() - .any(|c| c.replica_id == old_collaborator.replica_id) - { + let mut new_collaborators = HashMap::default(); + for collaborator in collaborators { + if let Ok(collaborator) = Collaborator::from_proto(collaborator) { + new_collaborators.insert(collaborator.peer_id, collaborator); + } + } + + for (_, old_collaborator) in &self.collaborators { + if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id as u16, cx) }); } } - self.collaborators = collaborators; + self.collaborators = new_collaborators; cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); } @@ -127,64 +140,14 @@ impl ChannelBuffer { Ok(()) } - async fn handle_add_channel_buffer_collaborator( + async fn handle_update_channel_buffer_collaborators( this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let collaborator = envelope.payload.collaborator.ok_or_else(|| { - anyhow::anyhow!( - "Should have gotten a collaborator in the AddChannelBufferCollaborator message" - ) - })?; - - this.update(&mut cx, |this, cx| { - this.collaborators.push(collaborator); - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - }); - - Ok(()) - } - - async fn handle_remove_channel_buffer_collaborator( - this: ModelHandle, - message: TypedEnvelope, + message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, cx| { - this.collaborators.retain(|collaborator| { - if collaborator.peer_id == message.payload.peer_id { - this.buffer.update(cx, |buffer, cx| { - buffer.remove_peer(collaborator.replica_id as u16, cx) - }); - false - } else { - true - } - }); - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - }); - - Ok(()) - } - - async fn handle_update_channel_buffer_collaborator( - this: ModelHandle, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - for collaborator in &mut this.collaborators { - if collaborator.peer_id == message.payload.old_peer_id { - collaborator.peer_id = message.payload.new_peer_id; - break; - } - } + this.replace_collaborators(message.payload.collaborators, cx); cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -217,7 +180,7 @@ impl ChannelBuffer { self.buffer.clone() } - pub fn collaborators(&self) -> &[proto::Collaborator] { + pub fn collaborators(&self) -> &HashMap { &self.collaborators } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a5a0a92246..a8f6dd67b6 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -198,10 +198,11 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task>> { let client = self.client.clone(); + let user_store = self.user_store.clone(); self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, - |channel, cx| ChannelBuffer::new(channel, client, cx), + |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), cx, ) } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc..2bd03d789f 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -21,6 +21,7 @@ text = { path = "../text" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } anyhow.workspace = true async-recursion = "0.3" diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5f13aa40ac..0522545587 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -7,6 +7,8 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; +use text::ReplicaId; +use theme::ColorIndex; use util::http::HttpClient; use util::TryFutureExt as _; @@ -19,6 +21,13 @@ pub struct User { pub avatar: Option>, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Collaborator { + pub peer_id: proto::PeerId, + pub replica_id: ReplicaId, + pub user_id: UserId, +} + impl PartialOrd for User { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -56,6 +65,7 @@ pub enum ContactRequestStatus { pub struct UserStore { users: HashMap>, + color_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, contacts: Vec>, @@ -81,6 +91,7 @@ pub enum Event { kind: ContactEventKind, }, ShowContacts, + ColorIndicesChanged, } #[derive(Clone, Copy)] @@ -118,6 +129,7 @@ impl UserStore { current_user: current_user_rx, contacts: Default::default(), incoming_contact_requests: Default::default(), + color_indices: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, client: Arc::downgrade(&client), @@ -641,6 +653,21 @@ impl UserStore { } }) } + + pub fn set_color_indices( + &mut self, + color_indices: HashMap, + cx: &mut ModelContext, + ) { + if color_indices != self.color_indices { + self.color_indices = color_indices; + cx.emit(Event::ColorIndicesChanged); + } + } + + pub fn color_indices(&self) -> &HashMap { + &self.color_indices + } } impl User { @@ -672,6 +699,16 @@ impl Contact { } } +impl Collaborator { + pub fn from_proto(message: proto::Collaborator) -> Result { + Ok(Self { + peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, + replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, + }) + } +} + async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { let mut response = http .get(url, Default::default(), true) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index d0c4ead5ad..5f5484679f 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -158,7 +158,8 @@ CREATE TABLE "room_participants" ( "initial_project_id" INTEGER, "calling_user_id" INTEGER NOT NULL REFERENCES users (id), "calling_connection_id" INTEGER NOT NULL, - "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL + "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, + "color_index" INTEGER ); CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); diff --git a/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql b/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql new file mode 100644 index 0000000000..626268bd5c --- /dev/null +++ b/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN color_index INTEGER; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 527c4faaa5..ab2fbe3945 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -510,7 +510,7 @@ pub struct RefreshedRoom { pub struct RefreshedChannelBuffer { pub connection_ids: Vec, - pub removed_collaborators: Vec, + pub collaborators: Vec, } pub struct Project { diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 62ead11932..4b149faf2a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -2,6 +2,12 @@ use super::*; use prost::Message; use text::{EditOperation, UndoOperation}; +pub struct LeftChannelBuffer { + pub channel_id: ChannelId, + pub collaborators: Vec, + pub connections: Vec, +} + impl Database { pub async fn join_channel_buffer( &self, @@ -204,23 +210,26 @@ impl Database { server_id: ServerId, ) -> Result { self.transaction(|tx| async move { - let collaborators = channel_buffer_collaborator::Entity::find() + let db_collaborators = channel_buffer_collaborator::Entity::find() .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) .all(&*tx) .await?; let mut connection_ids = Vec::new(); - let mut removed_collaborators = Vec::new(); + let mut collaborators = Vec::new(); let mut collaborator_ids_to_remove = Vec::new(); - for collaborator in &collaborators { - if !collaborator.connection_lost && collaborator.connection_server_id == server_id { - connection_ids.push(collaborator.connection()); + for db_collaborator in &db_collaborators { + if !db_collaborator.connection_lost + && db_collaborator.connection_server_id == server_id + { + connection_ids.push(db_collaborator.connection()); + collaborators.push(proto::Collaborator { + peer_id: Some(db_collaborator.connection().into()), + replica_id: db_collaborator.replica_id.0 as u32, + user_id: db_collaborator.user_id.to_proto(), + }) } else { - removed_collaborators.push(proto::RemoveChannelBufferCollaborator { - channel_id: channel_id.to_proto(), - peer_id: Some(collaborator.connection().into()), - }); - collaborator_ids_to_remove.push(collaborator.id); + collaborator_ids_to_remove.push(db_collaborator.id); } } @@ -231,7 +240,7 @@ impl Database { Ok(RefreshedChannelBuffer { connection_ids, - removed_collaborators, + collaborators, }) }) .await @@ -241,7 +250,7 @@ impl Database { &self, channel_id: ChannelId, connection: ConnectionId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { self.leave_channel_buffer_internal(channel_id, connection, &*tx) .await @@ -275,7 +284,7 @@ impl Database { pub async fn leave_channel_buffers( &self, connection: ConnectionId, - ) -> Result)>> { + ) -> Result> { self.transaction(|tx| async move { #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] enum QueryChannelIds { @@ -294,10 +303,10 @@ impl Database { let mut result = Vec::new(); for channel_id in channel_ids { - let collaborators = self + let left_channel_buffer = self .leave_channel_buffer_internal(channel_id, connection, &*tx) .await?; - result.push((channel_id, collaborators)); + result.push(left_channel_buffer); } Ok(result) @@ -310,7 +319,7 @@ impl Database { channel_id: ChannelId, connection: ConnectionId, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result { let result = channel_buffer_collaborator::Entity::delete_many() .filter( Condition::all() @@ -327,6 +336,7 @@ impl Database { Err(anyhow!("not a collaborator on this project"))?; } + let mut collaborators = Vec::new(); let mut connections = Vec::new(); let mut rows = channel_buffer_collaborator::Entity::find() .filter( @@ -336,19 +346,26 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row = row?; - connections.push(ConnectionId { - id: row.connection_id as u32, - owner_id: row.connection_server_id.0 as u32, + let connection = row.connection(); + connections.push(connection); + collaborators.push(proto::Collaborator { + peer_id: Some(connection.into()), + replica_id: row.replica_id.0 as u32, + user_id: row.user_id.to_proto(), }); } drop(rows); - if connections.is_empty() { + if collaborators.is_empty() { self.snapshot_channel_buffer(channel_id, &tx).await?; } - Ok(connections) + Ok(LeftChannelBuffer { + channel_id, + collaborators, + connections, + }) } pub async fn get_channel_buffer_collaborators( diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 651d58c265..fca4c67690 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -152,6 +152,7 @@ impl Database { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), answering_connection_lost: ActiveValue::set(false), + color_index: ActiveValue::NotSet, calling_user_id: ActiveValue::set(calling_user_id), calling_connection_id: ActiveValue::set(calling_connection.id as i32), calling_connection_server_id: ActiveValue::set(Some(ServerId( @@ -283,6 +284,22 @@ impl Database { .await? .ok_or_else(|| anyhow!("no such room"))?; + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryColorIndices { + ColorIndex, + } + let existing_color_indices: Vec = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .select_only() + .column(room_participant::Column::ColorIndex) + .into_values::<_, QueryColorIndices>() + .all(&*tx) + .await?; + let mut color_index = 0; + while existing_color_indices.contains(&color_index) { + color_index += 1; + } + if let Some(channel_id) = channel_id { self.check_user_is_channel_member(channel_id, user_id, &*tx) .await?; @@ -300,6 +317,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), + color_index: ActiveValue::Set(color_index), ..Default::default() }]) .on_conflict( @@ -322,6 +340,7 @@ impl Database { .add(room_participant::Column::AnsweringConnectionId.is_null()), ) .set(room_participant::ActiveModel { + color_index: ActiveValue::Set(color_index), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, @@ -1071,6 +1090,7 @@ impl Database { peer_id: Some(answering_connection.into()), projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), + color_index: db_participant.color_index as u32, }, ); } else { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 57d79fa830..8072fed69c 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -18,6 +18,7 @@ pub struct Model { pub calling_user_id: UserId, pub calling_connection_id: i32, pub calling_connection_server_id: Option, + pub color_index: i32, } impl Model { diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index e71748b88b..9808a9955b 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc) { let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); assert_eq!(zed_collaborats, &[a_id, b_id]); - let collaborators = db + let left_buffer = db .leave_channel_buffer(zed_id, connection_id_b) .await .unwrap(); - assert_eq!(collaborators, &[connection_id_a],); + assert_eq!(left_buffer.connections, &[connection_id_a],); let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); let _ = db diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b3af2d4e98..56cecb2e74 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage, - EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -313,9 +313,16 @@ impl Server { .trace_err() { for connection_id in refreshed_channel_buffer.connection_ids { - for message in &refreshed_channel_buffer.removed_collaborators { - peer.send(connection_id, message.clone()).trace_err(); - } + peer.send( + connection_id, + proto::UpdateChannelBufferCollaborators { + channel_id: channel_id.to_proto(), + collaborators: refreshed_channel_buffer + .collaborators + .clone(), + }, + ) + .trace_err(); } } } @@ -2654,18 +2661,12 @@ async fn join_channel_buffer( .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; - let replica_id = open_response.replica_id; let collaborators = open_response.collaborators.clone(); - response.send(open_response)?; - let update = AddChannelBufferCollaborator { + let update = UpdateChannelBufferCollaborators { channel_id: channel_id.to_proto(), - collaborator: Some(proto::Collaborator { - user_id: session.user_id.to_proto(), - peer_id: Some(session.connection_id.into()), - replica_id, - }), + collaborators: collaborators.clone(), }; channel_buffer_updated( session.connection_id, @@ -2712,8 +2713,8 @@ async fn rejoin_channel_buffers( .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) .await?; - for buffer in &buffers { - let collaborators_to_notify = buffer + for rejoined_buffer in &buffers { + let collaborators_to_notify = rejoined_buffer .buffer .collaborators .iter() @@ -2721,10 +2722,9 @@ async fn rejoin_channel_buffers( channel_buffer_updated( session.connection_id, collaborators_to_notify, - &proto::UpdateChannelBufferCollaborator { - channel_id: buffer.buffer.channel_id, - old_peer_id: Some(buffer.old_connection_id.into()), - new_peer_id: Some(session.connection_id.into()), + &proto::UpdateChannelBufferCollaborators { + channel_id: rejoined_buffer.buffer.channel_id, + collaborators: rejoined_buffer.buffer.collaborators.clone(), }, &session.peer, ); @@ -2745,7 +2745,7 @@ async fn leave_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let collaborators_to_notify = db + let left_buffer = db .leave_channel_buffer(channel_id, session.connection_id) .await?; @@ -2753,10 +2753,10 @@ async fn leave_channel_buffer( channel_buffer_updated( session.connection_id, - collaborators_to_notify, - &proto::RemoveChannelBufferCollaborator { + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { channel_id: channel_id.to_proto(), - peer_id: Some(session.connection_id.into()), + collaborators: left_buffer.collaborators, }, &session.peer, ); @@ -3231,13 +3231,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { .leave_channel_buffers(session.connection_id) .await?; - for (channel_id, connections) in left_channel_buffers { + for left_buffer in left_channel_buffers { channel_buffer_updated( session.connection_id, - connections, - &proto::RemoveChannelBufferCollaborator { - channel_id: channel_id.to_proto(), - peer_id: Some(session.connection_id.into()), + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { + channel_id: left_buffer.channel_id.to_proto(), + collaborators: left_buffer.collaborators, }, &session.peer, ); diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index baab675a1c..e403ed6f94 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -4,14 +4,16 @@ use crate::{ }; use call::ActiveCall; use channel::Channel; -use client::UserId; +use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use rpc::{proto, RECEIVE_TIMEOUT}; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; +use theme::ColorIndex; #[gpui::test] async fn test_core_channel_buffers( @@ -120,10 +122,10 @@ async fn test_core_channel_buffers( } #[gpui::test] -async fn test_channel_buffer_replica_ids( +async fn test_channel_notes_color_indices( deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); @@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + let channel_id = server .make_channel( "the-channel", @@ -141,136 +150,158 @@ async fn test_channel_buffer_replica_ids( ) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - - // Clients A and B join a channel. - active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) - .await - .unwrap(); - - // Clients A, B, and C join a channel buffer - // C first so that the replica IDs in the project and the channel buffer are different - let channel_buffer_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - // Client B shares a project - client_b + client_a .fs() - .insert_tree("/dir", json!({ "file.txt": "contents" })) + .insert_tree("/root", json!({"file.txt": "123"})) .await; - let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; - let shared_project_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) + .await + .unwrap(); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) .await .unwrap(); - // Client A joins the project - let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + }); deterministic.run_until_parked(); - - // Client C is in a separate project. - client_c.fs().insert_tree("/dir", json!({})).await; - let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; - - // Note that each user has a different replica id in the projects vs the - // channel buffer. - channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { - assert_eq!(project_a.read(cx).replica_id(), 1); - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); }); - channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { - assert_eq!(project_b.read(cx).replica_id(), 0); - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); - }); - channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { - // C is not in the project - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + deterministic.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); }); - let channel_window_a = - cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); - let channel_window_b = - cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); - let channel_window_c = cx_c.add_window(|cx| { - ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + }); }); - let channel_view_a = channel_window_a.root(cx_a); - let channel_view_b = channel_window_b.root(cx_b); - let channel_view_c = channel_window_c.root(cx_c); + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } - // For clients A and B, the replica ids in the channel buffer are mapped - // so that they match the same users' replica ids in their shared project. - channel_view_a.read_with(cx_a, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1)].into_iter().collect::>() - ); + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(1)), 1..2), (None, 2..3)], cx); + }); }); - channel_view_b.read_with(cx_b, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1)].into_iter().collect::>(), - ) + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1), (None, 2..3)], cx); + }); }); - // Client C only sees themself, as they're not part of any shared project - channel_view_c.read_with(cx_c, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(0, 0)].into_iter().collect::>(), - ); - }); - - // Client C joins the project that clients A and B are in. - active_call_c - .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + // Client A shares a project, and client B joins. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; - deterministic.run_until_parked(); - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.replica_id(), 2); - }); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - // For clients A and B, client C's replica id in the channel buffer is - // now mapped to their replica id in the shared project. - channel_view_a.read_with(cx_a, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1), (0, 2)] - .into_iter() - .collect::>() - ); + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); }); - channel_view_b.read_with(cx_b, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1), (0, 2)] - .into_iter() - .collect::>(), - ) + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); }); + deterministic.run_until_parked(); + + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(1)), 2..3)], cx); + }); + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1)], cx); + }); +} + +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.color_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); } #[gpui::test] @@ -568,13 +599,9 @@ async fn test_channel_buffers_and_server_restarts( channel_buffer_a.read_with(cx_a, |buffer_a, _| { channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!( - buffer_a - .collaborators() - .iter() - .map(|c| c.user_id) - .collect::>(), - vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()] + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], ); assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); }); @@ -723,10 +750,10 @@ async fn test_following_to_channel_notes_without_a_shared_project( } #[track_caller] -fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { assert_eq!( collaborators - .into_iter() + .values() .map(|collaborator| collaborator.user_id) .collect::>(), ids.into_iter().map(|id| id.unwrap()).collect::>() diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 2950922e7c..ad0181602c 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest { // channel buffer. let collaborators = channel_buffer.collaborators(); let mut user_ids = - collaborators.iter().map(|c| c.user_id).collect::>(); + collaborators.values().map(|c| c.user_id).collect::>(); user_ids.sort(); assert_eq!( user_ids, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 6a15cac9e9..71537f069f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -538,15 +538,7 @@ impl TestClient { root_path: impl AsRef, cx: &mut TestAppContext, ) -> (ModelHandle, WorktreeId) { - let project = cx.update(|cx| { - Project::local( - self.client().clone(), - self.app_state.user_store.clone(), - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }); + let project = self.build_empty_local_project(cx); let (worktree, _) = project .update(cx, |p, cx| { p.find_or_create_local_worktree(root_path, true, cx) @@ -559,6 +551,18 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle { + cx.update(|cx| { + Project::local( + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), + cx, + ) + }) + } + pub async fn build_remote_project( &self, host_project_id: u64, diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index b66d1ab7c7..1d103350de 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, Result}; use call::ActiveCall; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; -use client::proto; -use clock::ReplicaId; +use client::{ + proto::{self, PeerId}, + Collaborator, +}; use collections::HashMap; -use editor::Editor; +use editor::{CollaborationHub, Editor}; use gpui::{ actions, elements::{ChildView, Label}, @@ -109,97 +111,44 @@ impl ChannelView { cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor + }); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); - cx.subscribe(&project, Self::handle_project_event).detach(); cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) .detach(); - let this = Self { + Self { editor, project, channel_buffer, remote_id: None, _editor_event_subscription, - }; - this.refresh_replica_id_map(cx); - this + } } pub fn channel(&self, cx: &AppContext) -> Arc { self.channel_buffer.read(cx).channel() } - fn handle_project_event( - &mut self, - _: ModelHandle, - event: &project::Event, - cx: &mut ViewContext, - ) { - match event { - project::Event::RemoteIdChanged(_) => {} - project::Event::DisconnectedFromHost => {} - project::Event::Closed => {} - project::Event::CollaboratorUpdated { .. } => {} - project::Event::CollaboratorLeft(_) => {} - project::Event::CollaboratorJoined(_) => {} - _ => return, - } - self.refresh_replica_id_map(cx); - } - fn handle_channel_buffer_event( &mut self, _: ModelHandle, event: &ChannelBufferEvent, cx: &mut ViewContext, ) { - match event { - ChannelBufferEvent::CollaboratorsChanged => { - self.refresh_replica_id_map(cx); - } - ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + if let ChannelBufferEvent::Disconnected = event { + self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); - }), + }) } } - - /// Build a mapping of channel buffer replica ids to the corresponding - /// replica ids in the current project. - /// - /// Using this mapping, a given user can be displayed with the same color - /// in the channel buffer as in other files in the project. Users who are - /// in the channel buffer but not the project will not have a color. - fn refresh_replica_id_map(&self, cx: &mut ViewContext) { - let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); - let project = self.project.read(cx); - let channel_buffer = self.channel_buffer.read(cx); - project_replica_ids_by_channel_buffer_replica_id - .insert(channel_buffer.replica_id(cx), project.replica_id()); - project_replica_ids_by_channel_buffer_replica_id.extend( - channel_buffer - .collaborators() - .iter() - .filter_map(|channel_buffer_collaborator| { - project - .collaborators() - .values() - .find_map(|project_collaborator| { - (project_collaborator.user_id == channel_buffer_collaborator.user_id) - .then_some(( - channel_buffer_collaborator.replica_id as ReplicaId, - project_collaborator.replica_id, - )) - }) - }), - ); - - self.editor.update(cx, |editor, cx| { - editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) - }); - } } impl Entity for ChannelView { @@ -388,13 +337,9 @@ impl FollowableItem for ChannelView { }) } - fn set_leader_replica_id( - &mut self, - leader_replica_id: Option, - cx: &mut ViewContext, - ) { + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| { - editor.set_leader_replica_id(leader_replica_id, cx) + editor.set_leader_peer_id(leader_peer_id, cx) }) } @@ -406,3 +351,15 @@ impl FollowableItem for ChannelView { false } } + +struct ChannelBufferCollaborationHub(ModelHandle); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).color_indices() + } +} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a676b40b75..5dafae0cd5 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -923,9 +923,15 @@ impl CollabTitlebarItem { .background_color .unwrap_or_default(); - if let Some(replica_id) = replica_id { + let color_index = self + .user_store + .read(cx) + .color_indices() + .get(&user_id) + .copied(); + if let Some(color_index) = color_index { if followed_by_self { - let selection = theme.editor.replica_selection_style(replica_id).selection; + let selection = theme.editor.replica_selection_style(color_index).selection; background_color = Color::blend(selection, background_color); background_color.a = 255; } @@ -990,10 +996,10 @@ impl CollabTitlebarItem { .contained() .with_style(theme.titlebar.leader_selection); - if let Some(replica_id) = replica_id { + if let Some(color_index) = color_index { if followed_by_self { let color = - theme.editor.replica_selection_style(replica_id).selection; + theme.editor.replica_selection_style(color_index).selection; container = container.with_background_color(color); } } @@ -1001,8 +1007,8 @@ impl CollabTitlebarItem { container })) .with_children((|| { - let replica_id = replica_id?; - let color = theme.editor.replica_selection_style(replica_id).cursor; + let color_index = color_index?; + let color = theme.editor.replica_selection_style(color_index).cursor; Some( AvatarRibbon::new(color) .constrained() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0827e13264..7692a54b01 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, TelemetrySettings}; +use client::{ClickhouseEvent, Collaborator, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -79,6 +79,7 @@ pub use multi_buffer::{ use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; +use rpc::proto::PeerId; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -101,7 +102,7 @@ use std::{ pub use sum_tree::Bias; use sum_tree::TreeMap; use text::Rope; -use theme::{DiagnosticStyle, Theme, ThemeSettings}; +use theme::{ColorIndex, DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; @@ -580,11 +581,11 @@ pub struct Editor { get_field_editor_theme: Option>, override_text_style: Option>, project: Option>, + collaboration_hub: Option>, focused: bool, blink_manager: ModelHandle, pub show_local_selections: bool, mode: EditorMode, - replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, @@ -608,7 +609,7 @@ pub struct Editor { keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, - leader_replica_id: Option, + leader_peer_id: Option, remote_id: Option, hover_state: HoverState, gutter_hovered: bool, @@ -630,6 +631,15 @@ pub struct EditorSnapshot { ongoing_scroll: OngoingScroll, } +pub struct RemoteSelection { + pub replica_id: ReplicaId, + pub selection: Selection, + pub cursor_shape: CursorShape, + pub peer_id: PeerId, + pub line_mode: bool, + pub color_index: Option, +} + #[derive(Clone, Debug)] struct SelectionHistoryEntry { selections: Arc<[Selection]>, @@ -1532,12 +1542,12 @@ impl Editor { active_diagnostics: None, soft_wrap_mode_override, get_field_editor_theme, + collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, focused: false, blink_manager: blink_manager.clone(), show_local_selections: true, mode, - replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1564,7 +1574,7 @@ impl Editor { keymap_context_layers: Default::default(), input_enabled: true, read_only: false, - leader_replica_id: None, + leader_peer_id: None, remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), @@ -1631,8 +1641,8 @@ impl Editor { self.buffer.read(cx).replica_id() } - pub fn leader_replica_id(&self) -> Option { - self.leader_replica_id + pub fn leader_peer_id(&self) -> Option { + self.leader_peer_id } pub fn buffer(&self) -> &ModelHandle { @@ -1696,6 +1706,14 @@ impl Editor { self.mode } + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } + + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, @@ -1772,26 +1790,13 @@ impl Editor { cx.notify(); } - pub fn replica_id_map(&self) -> Option<&HashMap> { - self.replica_id_mapping.as_ref() - } - - pub fn set_replica_id_map( - &mut self, - mapping: Option>, - cx: &mut ViewContext, - ) { - self.replica_id_mapping = mapping; - cx.notify(); - } - fn selections_did_change( &mut self, local: bool, old_cursor_position: &Anchor, cx: &mut ViewContext, ) { - if self.focused && self.leader_replica_id.is_none() { + if self.focused && self.leader_peer_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( &self.selections.disjoint_anchors(), @@ -8563,6 +8568,21 @@ impl Editor { } } +pub trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap; +} + +impl CollaborationHub for ModelHandle { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).collaborators() + } + + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).user_store().read(cx).color_indices() + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, @@ -8606,6 +8626,34 @@ fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) } impl EditorSnapshot { + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + collaboration_hub: &dyn CollaborationHub, + cx: &'a AppContext, + ) -> impl 'a + Iterator { + let color_indices = collaboration_hub.user_color_indices(cx); + let collaborators_by_peer_id = collaboration_hub.collaborators(cx); + let collaborators_by_replica_id = collaborators_by_peer_id + .iter() + .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) + .collect::>(); + self.buffer_snapshot + .remote_selections_in_range(range) + .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { + let collaborator = collaborators_by_replica_id.get(&replica_id)?; + let color_index = color_indices.get(&collaborator.user_id).copied(); + Some(RemoteSelection { + replica_id, + selection, + cursor_shape, + line_mode, + color_index, + peer_id: collaborator.peer_id, + }) + }) + } + pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) } @@ -8719,7 +8767,7 @@ impl View for Editor { self.focused = true; self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - if self.leader_replica_id.is_none() { + if self.leader_peer_id.is_none() { buffer.set_active_selections( &self.selections.disjoint_anchors(), self.selections.line_mode, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3390b70530..dad5b06626 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -17,7 +17,6 @@ use crate::{ }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; -use clock::ReplicaId; use collections::{BTreeMap, HashMap}; use git::diff::DiffHunkStatus; use gpui::{ @@ -55,6 +54,7 @@ use std::{ sync::Arc, }; use text::Point; +use theme::SelectionStyle; use workspace::item::Item; enum FoldMarkers {} @@ -868,14 +868,7 @@ impl EditorElement { let corner_radius = 0.15 * layout.position_map.line_height; let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - for (replica_id, selections) in &layout.selections { - let replica_id = *replica_id; - let selection_style = if let Some(replica_id) = replica_id { - style.replica_selection_style(replica_id) - } else { - &style.absent_selection - }; - + for (selection_style, selections) in &layout.selections { for selection in selections { self.paint_highlighted_range( selection.range.clone(), @@ -2193,7 +2186,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(Option, Vec)> = Vec::new(); + let mut selections: Vec<(SelectionStyle, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2219,35 +2212,6 @@ impl Element for EditorElement { }), ); - let mut remote_selections = HashMap::default(); - for (replica_id, line_mode, cursor_shape, selection) in snapshot - .buffer_snapshot - .remote_selections_in_range(&(start_anchor..end_anchor)) - { - let replica_id = if let Some(mapping) = &editor.replica_id_mapping { - mapping.get(&replica_id).copied() - } else { - Some(replica_id) - }; - - // The local selections match the leader's selections. - if replica_id.is_some() && replica_id == editor.leader_replica_id { - continue; - } - remote_selections - .entry(replica_id) - .or_insert(Vec::new()) - .push(SelectionLayout::new( - selection, - line_mode, - cursor_shape, - &snapshot.display_snapshot, - false, - false, - )); - } - selections.extend(remote_selections); - let mut newest_selection_head = None; if editor.show_local_selections { @@ -2282,19 +2246,45 @@ impl Element for EditorElement { layouts.push(layout); } - // Render the local selections in the leader's color when following. - let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { - leader_replica_id - } else { - let replica_id = editor.replica_id(cx); - if let Some(mapping) = &editor.replica_id_mapping { - mapping.get(&replica_id).copied().unwrap_or(replica_id) - } else { - replica_id - } - }; + selections.push((style.selection, layouts)); + } - selections.push((Some(local_replica_id), layouts)); + if let Some(collaboration_hub) = &editor.collaboration_hub { + let mut remote_selections = HashMap::default(); + for selection in snapshot.remote_selections_in_range( + &(start_anchor..end_anchor), + collaboration_hub.as_ref(), + cx, + ) { + let selection_style = if let Some(color_index) = selection.color_index { + style.replica_selection_style(color_index) + } else { + style.absent_selection + }; + + // The local selections match the leader's selections. + if Some(selection.peer_id) == editor.leader_peer_id { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = selection_style; + } + continue; + } + + remote_selections + .entry(selection.replica_id) + .or_insert((selection_style, Vec::new())) + .1 + .push(SelectionLayout::new( + selection.selection, + selection.line_mode, + selection.cursor_shape, + &snapshot.display_snapshot, + false, + false, + )); + } + + selections.extend(remote_selections.into_values()); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2686,7 +2676,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(Option, Vec)>, + selections: Vec<(SelectionStyle, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7fdbe82a9a..1fee309181 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -17,7 +17,7 @@ use language::{ SelectionGoal, }; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; -use rpc::proto::{self, update_view}; +use rpc::proto::{self, update_view, PeerId}; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -156,13 +156,9 @@ impl FollowableItem for Editor { })) } - fn set_leader_replica_id( - &mut self, - leader_replica_id: Option, - cx: &mut ViewContext, - ) { - self.leader_replica_id = leader_replica_id; - if self.leader_replica_id.is_some() { + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.leader_peer_id = leader_peer_id; + if self.leader_peer_id.is_some() { self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ffea6646e9..07f1bfa43e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -35,6 +35,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } terminal = { path = "../terminal" } +theme = { path = "../theme" } util = { path = "../util" } aho-corasick = "1.1" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e4858587ad..62df8425c4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope, UserId, UserStore}; +use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; @@ -76,6 +76,7 @@ use std::{ }; use terminals::Terminals; use text::Anchor; +use theme::ColorIndex; use util::{ debug_panic, defer, http::HttpClient, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, @@ -119,6 +120,7 @@ pub struct Project { join_project_response_message_id: u32, next_diagnostic_group_id: usize, user_store: ModelHandle, + user_color_indices: HashMap, fs: Arc, client_state: Option, collaborators: HashMap, @@ -253,13 +255,6 @@ enum ProjectClientState { }, } -#[derive(Clone, Debug)] -pub struct Collaborator { - pub peer_id: proto::PeerId, - pub replica_id: ReplicaId, - pub user_id: UserId, -} - #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), @@ -649,6 +644,7 @@ impl Project { languages, client, user_store, + user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -721,6 +717,7 @@ impl Project { _maintain_workspace_config: Self::maintain_workspace_config(cx), languages, user_store: user_store.clone(), + user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -925,6 +922,10 @@ impl Project { self.user_store.clone() } + pub fn user_color_indices(&self) -> &HashMap { + &self.user_color_indices + } + pub fn opened_buffers(&self, cx: &AppContext) -> Vec> { self.opened_buffers .values() @@ -8211,16 +8212,6 @@ impl Entity for Project { } } -impl Collaborator { - fn from_proto(message: proto::Collaborator) -> Result { - Ok(Self { - peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, - replica_id: message.replica_id as ReplicaId, - user_id: message.user_id as UserId, - }) - } -} - impl> From<(WorktreeId, P)> for ProjectPath { fn from((worktree_id, path): (WorktreeId, P)) -> Self { Self { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9c1ec4e613..da97cd35c7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -23,154 +23,152 @@ message Envelope { CreateRoomResponse create_room_response = 10; JoinRoom join_room = 11; JoinRoomResponse join_room_response = 12; - RejoinRoom rejoin_room = 108; - RejoinRoomResponse rejoin_room_response = 109; - LeaveRoom leave_room = 13; - Call call = 14; - IncomingCall incoming_call = 15; - CallCanceled call_canceled = 16; - CancelCall cancel_call = 17; - DeclineCall decline_call = 18; - UpdateParticipantLocation update_participant_location = 19; - RoomUpdated room_updated = 20; + RejoinRoom rejoin_room = 13; + RejoinRoomResponse rejoin_room_response = 14; + LeaveRoom leave_room = 15; + Call call = 16; + IncomingCall incoming_call = 17; + CallCanceled call_canceled = 18; + CancelCall cancel_call = 19; + DeclineCall decline_call = 20; + UpdateParticipantLocation update_participant_location = 21; + RoomUpdated room_updated = 22; - ShareProject share_project = 21; - ShareProjectResponse share_project_response = 22; - UnshareProject unshare_project = 23; - JoinProject join_project = 24; - JoinProjectResponse join_project_response = 25; - LeaveProject leave_project = 26; - AddProjectCollaborator add_project_collaborator = 27; - UpdateProjectCollaborator update_project_collaborator = 110; - RemoveProjectCollaborator remove_project_collaborator = 28; + ShareProject share_project = 23; + ShareProjectResponse share_project_response = 24; + UnshareProject unshare_project = 25; + JoinProject join_project = 26; + JoinProjectResponse join_project_response = 27; + LeaveProject leave_project = 28; + AddProjectCollaborator add_project_collaborator = 29; + UpdateProjectCollaborator update_project_collaborator = 30; + RemoveProjectCollaborator remove_project_collaborator = 31; - GetDefinition get_definition = 29; - GetDefinitionResponse get_definition_response = 30; - GetTypeDefinition get_type_definition = 31; - GetTypeDefinitionResponse get_type_definition_response = 32; - GetReferences get_references = 33; - GetReferencesResponse get_references_response = 34; - GetDocumentHighlights get_document_highlights = 35; - GetDocumentHighlightsResponse get_document_highlights_response = 36; - GetProjectSymbols get_project_symbols = 37; - GetProjectSymbolsResponse get_project_symbols_response = 38; - OpenBufferForSymbol open_buffer_for_symbol = 39; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40; + GetDefinition get_definition = 32; + GetDefinitionResponse get_definition_response = 33; + GetTypeDefinition get_type_definition = 34; + GetTypeDefinitionResponse get_type_definition_response = 35; + GetReferences get_references = 36; + GetReferencesResponse get_references_response = 37; + GetDocumentHighlights get_document_highlights = 38; + GetDocumentHighlightsResponse get_document_highlights_response = 39; + GetProjectSymbols get_project_symbols = 40; + GetProjectSymbolsResponse get_project_symbols_response = 41; + OpenBufferForSymbol open_buffer_for_symbol = 42; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43; - UpdateProject update_project = 41; - UpdateWorktree update_worktree = 43; + UpdateProject update_project = 44; + UpdateWorktree update_worktree = 45; - CreateProjectEntry create_project_entry = 45; - RenameProjectEntry rename_project_entry = 46; - CopyProjectEntry copy_project_entry = 47; - DeleteProjectEntry delete_project_entry = 48; - ProjectEntryResponse project_entry_response = 49; - ExpandProjectEntry expand_project_entry = 114; - ExpandProjectEntryResponse expand_project_entry_response = 115; + CreateProjectEntry create_project_entry = 46; + RenameProjectEntry rename_project_entry = 47; + CopyProjectEntry copy_project_entry = 48; + DeleteProjectEntry delete_project_entry = 49; + ProjectEntryResponse project_entry_response = 50; + ExpandProjectEntry expand_project_entry = 51; + ExpandProjectEntryResponse expand_project_entry_response = 52; - UpdateDiagnosticSummary update_diagnostic_summary = 50; - StartLanguageServer start_language_server = 51; - UpdateLanguageServer update_language_server = 52; + UpdateDiagnosticSummary update_diagnostic_summary = 53; + StartLanguageServer start_language_server = 54; + UpdateLanguageServer update_language_server = 55; - OpenBufferById open_buffer_by_id = 53; - OpenBufferByPath open_buffer_by_path = 54; - OpenBufferResponse open_buffer_response = 55; - CreateBufferForPeer create_buffer_for_peer = 56; - UpdateBuffer update_buffer = 57; - UpdateBufferFile update_buffer_file = 58; - SaveBuffer save_buffer = 59; - BufferSaved buffer_saved = 60; - BufferReloaded buffer_reloaded = 61; - ReloadBuffers reload_buffers = 62; - ReloadBuffersResponse reload_buffers_response = 63; - SynchronizeBuffers synchronize_buffers = 200; - SynchronizeBuffersResponse synchronize_buffers_response = 201; - FormatBuffers format_buffers = 64; - FormatBuffersResponse format_buffers_response = 65; - GetCompletions get_completions = 66; - GetCompletionsResponse get_completions_response = 67; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69; - GetCodeActions get_code_actions = 70; - GetCodeActionsResponse get_code_actions_response = 71; - GetHover get_hover = 72; - GetHoverResponse get_hover_response = 73; - ApplyCodeAction apply_code_action = 74; - ApplyCodeActionResponse apply_code_action_response = 75; - PrepareRename prepare_rename = 76; - PrepareRenameResponse prepare_rename_response = 77; - PerformRename perform_rename = 78; - PerformRenameResponse perform_rename_response = 79; - SearchProject search_project = 80; - SearchProjectResponse search_project_response = 81; + OpenBufferById open_buffer_by_id = 56; + OpenBufferByPath open_buffer_by_path = 57; + OpenBufferResponse open_buffer_response = 58; + CreateBufferForPeer create_buffer_for_peer = 59; + UpdateBuffer update_buffer = 60; + UpdateBufferFile update_buffer_file = 61; + SaveBuffer save_buffer = 62; + BufferSaved buffer_saved = 63; + BufferReloaded buffer_reloaded = 64; + ReloadBuffers reload_buffers = 65; + ReloadBuffersResponse reload_buffers_response = 66; + SynchronizeBuffers synchronize_buffers = 67; + SynchronizeBuffersResponse synchronize_buffers_response = 68; + FormatBuffers format_buffers = 69; + FormatBuffersResponse format_buffers_response = 70; + GetCompletions get_completions = 71; + GetCompletionsResponse get_completions_response = 72; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; + GetCodeActions get_code_actions = 75; + GetCodeActionsResponse get_code_actions_response = 76; + GetHover get_hover = 77; + GetHoverResponse get_hover_response = 78; + ApplyCodeAction apply_code_action = 79; + ApplyCodeActionResponse apply_code_action_response = 80; + PrepareRename prepare_rename = 81; + PrepareRenameResponse prepare_rename_response = 82; + PerformRename perform_rename = 83; + PerformRenameResponse perform_rename_response = 84; + SearchProject search_project = 85; + SearchProjectResponse search_project_response = 86; - UpdateContacts update_contacts = 92; - UpdateInviteInfo update_invite_info = 93; - ShowContacts show_contacts = 94; + UpdateContacts update_contacts = 87; + UpdateInviteInfo update_invite_info = 88; + ShowContacts show_contacts = 89; - GetUsers get_users = 95; - FuzzySearchUsers fuzzy_search_users = 96; - UsersResponse users_response = 97; - RequestContact request_contact = 98; - RespondToContactRequest respond_to_contact_request = 99; - RemoveContact remove_contact = 100; + GetUsers get_users = 90; + FuzzySearchUsers fuzzy_search_users = 91; + UsersResponse users_response = 92; + RequestContact request_contact = 93; + RespondToContactRequest respond_to_contact_request = 94; + RemoveContact remove_contact = 95; - Follow follow = 101; - FollowResponse follow_response = 102; - UpdateFollowers update_followers = 103; - Unfollow unfollow = 104; - GetPrivateUserInfo get_private_user_info = 105; - GetPrivateUserInfoResponse get_private_user_info_response = 106; - UpdateDiffBase update_diff_base = 107; + Follow follow = 96; + FollowResponse follow_response = 97; + UpdateFollowers update_followers = 98; + Unfollow unfollow = 99; + GetPrivateUserInfo get_private_user_info = 100; + GetPrivateUserInfoResponse get_private_user_info_response = 101; + UpdateDiffBase update_diff_base = 102; - OnTypeFormatting on_type_formatting = 111; - OnTypeFormattingResponse on_type_formatting_response = 112; + OnTypeFormatting on_type_formatting = 103; + OnTypeFormattingResponse on_type_formatting_response = 104; - UpdateWorktreeSettings update_worktree_settings = 113; + UpdateWorktreeSettings update_worktree_settings = 105; - InlayHints inlay_hints = 116; - InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 137; - ResolveInlayHintResponse resolve_inlay_hint_response = 138; - RefreshInlayHints refresh_inlay_hints = 118; + InlayHints inlay_hints = 106; + InlayHintsResponse inlay_hints_response = 107; + ResolveInlayHint resolve_inlay_hint = 108; + ResolveInlayHintResponse resolve_inlay_hint_response = 109; + RefreshInlayHints refresh_inlay_hints = 110; - CreateChannel create_channel = 119; - CreateChannelResponse create_channel_response = 120; - InviteChannelMember invite_channel_member = 121; - RemoveChannelMember remove_channel_member = 122; - RespondToChannelInvite respond_to_channel_invite = 123; - UpdateChannels update_channels = 124; - JoinChannel join_channel = 125; - DeleteChannel delete_channel = 126; - GetChannelMembers get_channel_members = 127; - GetChannelMembersResponse get_channel_members_response = 128; - SetChannelMemberAdmin set_channel_member_admin = 129; - RenameChannel rename_channel = 130; - RenameChannelResponse rename_channel_response = 154; + CreateChannel create_channel = 111; + CreateChannelResponse create_channel_response = 112; + InviteChannelMember invite_channel_member = 113; + RemoveChannelMember remove_channel_member = 114; + RespondToChannelInvite respond_to_channel_invite = 115; + UpdateChannels update_channels = 116; + JoinChannel join_channel = 117; + DeleteChannel delete_channel = 118; + GetChannelMembers get_channel_members = 119; + GetChannelMembersResponse get_channel_members_response = 120; + SetChannelMemberAdmin set_channel_member_admin = 121; + RenameChannel rename_channel = 122; + RenameChannelResponse rename_channel_response = 123; - JoinChannelBuffer join_channel_buffer = 131; - JoinChannelBufferResponse join_channel_buffer_response = 132; - UpdateChannelBuffer update_channel_buffer = 133; - LeaveChannelBuffer leave_channel_buffer = 134; - AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; - RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; - UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139; - RejoinChannelBuffers rejoin_channel_buffers = 140; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; + JoinChannelBuffer join_channel_buffer = 124; + JoinChannelBufferResponse join_channel_buffer_response = 125; + UpdateChannelBuffer update_channel_buffer = 126; + LeaveChannelBuffer leave_channel_buffer = 127; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; + RejoinChannelBuffers rejoin_channel_buffers = 129; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - JoinChannelChat join_channel_chat = 142; - JoinChannelChatResponse join_channel_chat_response = 143; - LeaveChannelChat leave_channel_chat = 144; - SendChannelMessage send_channel_message = 145; - SendChannelMessageResponse send_channel_message_response = 146; - ChannelMessageSent channel_message_sent = 147; - GetChannelMessages get_channel_messages = 148; - GetChannelMessagesResponse get_channel_messages_response = 149; - RemoveChannelMessage remove_channel_message = 150; + JoinChannelChat join_channel_chat = 131; + JoinChannelChatResponse join_channel_chat_response = 132; + LeaveChannelChat leave_channel_chat = 133; + SendChannelMessage send_channel_message = 134; + SendChannelMessageResponse send_channel_message_response = 135; + ChannelMessageSent channel_message_sent = 136; + GetChannelMessages get_channel_messages = 137; + GetChannelMessagesResponse get_channel_messages_response = 138; + RemoveChannelMessage remove_channel_message = 139; - LinkChannel link_channel = 151; - UnlinkChannel unlink_channel = 152; - MoveChannel move_channel = 153; // Current max: 154 + LinkChannel link_channel = 140; + UnlinkChannel unlink_channel = 141; + MoveChannel move_channel = 142; } } @@ -258,6 +256,7 @@ message Participant { PeerId peer_id = 2; repeated ParticipantProject projects = 3; ParticipantLocation location = 4; + uint32 color_index = 5; } message PendingParticipant { @@ -440,20 +439,9 @@ message RemoveProjectCollaborator { PeerId peer_id = 2; } -message AddChannelBufferCollaborator { +message UpdateChannelBufferCollaborators { uint64 channel_id = 1; - Collaborator collaborator = 2; -} - -message RemoveChannelBufferCollaborator { - uint64 channel_id = 1; - PeerId peer_id = 2; -} - -message UpdateChannelBufferCollaborator { - uint64 channel_id = 1; - PeerId old_peer_id = 2; - PeerId new_peer_id = 3; + repeated Collaborator collaborators = 2; } message GetDefinition { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 48e9eef710..6d0a0f85d1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -270,9 +270,7 @@ messages!( (JoinChannelBufferResponse, Foreground), (LeaveChannelBuffer, Background), (UpdateChannelBuffer, Foreground), - (RemoveChannelBufferCollaborator, Foreground), - (AddChannelBufferCollaborator, Foreground), - (UpdateChannelBufferCollaborator, Foreground), + (UpdateChannelBufferCollaborators, Foreground), ); request_messages!( @@ -407,10 +405,8 @@ entity_messages!( channel_id, ChannelMessageSent, UpdateChannelBuffer, - RemoveChannelBufferCollaborator, RemoveChannelMessage, - AddChannelBufferCollaborator, - UpdateChannelBufferCollaborator + UpdateChannelBufferCollaborators ); const KIB: usize = 1024; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5ea5ce8778..a96a3d9c7c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1064,14 +1064,16 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ColorIndex(pub u32); + impl Editor { - 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] + pub fn replica_selection_style(&self, color_index: ColorIndex) -> SelectionStyle { + if self.guest_selections.is_empty() { + return SelectionStyle::default(); } + let style_ix = color_index.0 as usize % self.guest_selections.len(); + self.guest_selections[style_ix] } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e2fa6e989a..c223726422 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -168,7 +168,7 @@ impl Vim { self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { Event::SelectionsChanged { local: true } => { let editor = editor.read(cx); - if editor.leader_replica_id().is_none() { + if editor.leader_peer_id().is_none() { let newest = editor.selections.newest::(cx); local_selections_changed(newest, cx); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index de19e82c8b..a489dfd9a4 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -4,7 +4,10 @@ use crate::{ }; use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; use anyhow::Result; -use client::{proto, Client}; +use client::{ + proto::{self, PeerId}, + Client, +}; use gpui::geometry::vector::Vector2F; use gpui::AnyWindowHandle; use gpui::{ @@ -698,13 +701,13 @@ pub trait FollowableItem: Item { ) -> Task>; fn is_project_item(&self, cx: &AppContext) -> bool; - fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; - fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut WindowContext); + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext); fn to_state_proto(&self, cx: &AppContext) -> Option; fn add_event_to_update_proto( &self, @@ -732,10 +735,8 @@ impl FollowableItemHandle for ViewHandle { }) } - fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut WindowContext) { - self.update(cx, |this, cx| { - this.set_leader_replica_id(leader_replica_id, cx) - }) + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) } fn to_state_proto(&self, cx: &AppContext) -> Option { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 29068ce923..425fd00b5a 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -183,25 +183,23 @@ impl Member { }) .and_then(|leader_id| { let room = active_call?.read(cx).room()?.read(cx); - let collaborator = project.read(cx).collaborators().get(leader_id)?; - let participant = room.remote_participant_for_peer_id(*leader_id)?; - Some((collaborator.replica_id, participant)) + room.remote_participant_for_peer_id(*leader_id) }); - let border = if let Some((replica_id, _)) = leader.as_ref() { - let leader_color = theme.editor.replica_selection_style(*replica_id).cursor; - let mut border = Border::all(theme.workspace.leader_border_width, leader_color); - border + let mut leader_border = Border::default(); + let mut leader_status_box = None; + if let Some(leader) = &leader { + let leader_color = theme + .editor + .replica_selection_style(leader.color_index) + .cursor; + leader_border = Border::all(theme.workspace.leader_border_width, leader_color); + leader_border .color .fade_out(1. - theme.workspace.leader_border_opacity); - border.overlay = true; - border - } else { - Border::default() - }; + leader_border.overlay = true; - let leader_status_box = if let Some((_, leader)) = leader { - match leader.location { + leader_status_box = match leader.location { ParticipantLocation::SharedProject { project_id: leader_project_id, } => { @@ -279,13 +277,11 @@ impl Member { .right() .into_any(), ), - } - } else { - None - }; + }; + } Stack::new() - .with_child(pane_element.contained().with_border(border)) + .with_child(pane_element.contained().with_border(leader_border)) .with_children(leader_status_box) .into_any() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f874f5ee16..9d0d276e49 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2423,7 +2423,7 @@ impl Workspace { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { for item in state.items_by_leader_view_id.into_values() { - item.set_leader_replica_id(None, cx); + item.set_leader_peer_id(None, cx); } } } @@ -2527,7 +2527,7 @@ impl Workspace { let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(pane) { for (_, item) in state.items_by_leader_view_id { - item.set_leader_replica_id(None, cx); + item.set_leader_peer_id(None, cx); } if states_by_pane.is_empty() { @@ -2828,16 +2828,6 @@ impl Workspace { let this = this .upgrade(cx) .ok_or_else(|| anyhow!("workspace dropped"))?; - let project = this - .read_with(cx, |this, _| this.project.clone()) - .ok_or_else(|| anyhow!("window dropped"))?; - - let replica_id = project.read_with(cx, |project, _| { - project - .collaborators() - .get(&leader_id) - .map(|c| c.replica_id) - }); let item_builders = cx.update(|cx| { cx.default_global::() @@ -2882,7 +2872,7 @@ impl Workspace { .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_leader_replica_id(replica_id, cx); + item.set_leader_peer_id(Some(leader_id), cx); state.items_by_leader_view_id.insert(id, item); } diff --git a/selection-color-notes.txt b/selection-color-notes.txt new file mode 100644 index 0000000000..6186adcac1 --- /dev/null +++ b/selection-color-notes.txt @@ -0,0 +1,14 @@ +Assign selection colors to users. goals: + * current user is always main color + * every other user has the same color in every context + * users don't need to be in a shared project to have a color. they can either be in the call, or in a channel notes. + +Places colors are used: + * editor element, driven by the buffer's `remote_selections` + * pane border (access to more state) + * collab titlebar (access to more state) + +Currently, editor holds an optional "replica id map". + +Most challenging part is in the editor, because the editor should be fairly self-contained, not depend on e.g. the user store. +