Assign unique color indices to room participants, use those instead of replica_ids

Max Brunsfeld , Conrad , and Antonio created

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>

Change summary

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 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                   |   3 
crates/collab/migrations/20230926102500_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 
crates/collab/src/db/tables/room_participant.rs                                  |   1 
crates/collab/src/db/tests/buffer_tests.rs                                       |   4 
crates/collab/src/rpc.rs                                                         |  56 
crates/collab/src/tests/channel_buffer_tests.rs                                  | 269 
crates/collab/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                                                     |  90 
crates/editor/src/items.rs                                                       |  12 
crates/project/Cargo.toml                                                        |   1 
crates/project/src/project.rs                                                    |  27 
crates/rpc/proto/zed.proto                                                       | 310 
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, 716 insertions(+), 648 deletions(-)

Detailed changes

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",

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

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<proto::ParticipantProject>,
     pub location: ParticipantLocation,
+    pub color_index: ColorIndex,
     pub muted: bool,
     pub speaking: bool,
     pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

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();
             });

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

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>) {
     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<Channel>,
     connected: bool,
-    collaborators: Vec<proto::Collaborator>,
+    collaborators: HashMap<PeerId, Collaborator>,
+    user_store: ModelHandle<UserStore>,
     buffer: ModelHandle<language::Buffer>,
     buffer_epoch: u64,
     client: Arc<Client>,
@@ -46,6 +49,7 @@ impl ChannelBuffer {
     pub(crate) async fn new(
         channel: Arc<Channel>,
         client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
         mut cx: AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let response = client
@@ -61,8 +65,6 @@ impl ChannelBuffer {
             .map(language::proto::deserialize_operation)
             .collect::<Result<Vec<_>, _>>()?;
 
-        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<UserStore> {
+        &self.user_store
+    }
+
     pub(crate) fn replace_collaborators(
         &mut self,
         collaborators: Vec<proto::Collaborator>,
         cx: &mut ModelContext<Self>,
     ) {
-        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(
-        this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
-        _: Arc<Client>,
-        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(
+    async fn handle_update_channel_buffer_collaborators(
         this: ModelHandle<Self>,
-        message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+        message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
         _: Arc<Client>,
         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<Self>,
-        message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
-        _: Arc<Client>,
-        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<PeerId, Collaborator> {
         &self.collaborators
     }
 

crates/channel/src/channel_store.rs 🔗

@@ -198,10 +198,11 @@ impl ChannelStore {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
         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,
         )
     }

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"

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<Arc<ImageData>>,
 }
 
+#[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<std::cmp::Ordering> {
         Some(self.cmp(other))
@@ -56,6 +65,7 @@ pub enum ContactRequestStatus {
 
 pub struct UserStore {
     users: HashMap<u64, Arc<User>>,
+    color_indices: HashMap<u64, ColorIndex>,
     update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
     current_user: watch::Receiver<Option<Arc<User>>>,
     contacts: Vec<Arc<Contact>>,
@@ -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<u64, ColorIndex>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if color_indices != self.color_indices {
+            self.color_indices = color_indices;
+            cx.emit(Event::ColorIndicesChanged);
+        }
+    }
+
+    pub fn color_indices(&self) -> &HashMap<u64, ColorIndex> {
+        &self.color_indices
+    }
 }
 
 impl User {
@@ -672,6 +699,16 @@ impl Contact {
     }
 }
 
+impl Collaborator {
+    pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
+        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<Arc<ImageData>> {
     let mut response = http
         .get(url, Default::default(), true)

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");

crates/collab/src/db.rs 🔗

@@ -510,7 +510,7 @@ pub struct RefreshedRoom {
 
 pub struct RefreshedChannelBuffer {
     pub connection_ids: Vec<ConnectionId>,
-    pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
+    pub collaborators: Vec<proto::Collaborator>,
 }
 
 pub struct Project {

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<proto::Collaborator>,
+    pub connections: Vec<ConnectionId>,
+}
+
 impl Database {
     pub async fn join_channel_buffer(
         &self,
@@ -204,23 +210,26 @@ impl Database {
         server_id: ServerId,
     ) -> Result<RefreshedChannelBuffer> {
         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<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         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<Vec<(ChannelId, Vec<ConnectionId>)>> {
+    ) -> Result<Vec<LeftChannelBuffer>> {
         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<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         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(

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<i32> = 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 {

crates/collab/src/db/tests/buffer_tests.rs 🔗

@@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
     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

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,
         );

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<Deterministic>,
-    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,138 +150,160 @@ 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);
+    client_a
+        .fs()
+        .insert_tree("/root", json!({"file.txt": "123"}))
+        .await;
+    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 and B join a channel.
-    active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
+    // 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();
-    active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
+    let channel_view_b = cx_b
+        .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), 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))
+    let channel_view_c = cx_c
+        .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), 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
-        .fs()
-        .insert_tree("/dir", json!({ "file.txt": "contents" }))
-        .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))
-        .await
-        .unwrap();
-
-    // Client A joins the project
-    let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
-    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);
+    // 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]);
+            });
+        });
     });
-    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);
+    deterministic.run_until_parked();
+    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_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::<HashMap<_, _>>()
-        );
+    // 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::<HashMap<u16, u16>>(),
-        )
+    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::<HashMap<u16, u16>>(),
-        );
-    });
+    // 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_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);
 
-    // 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))
+    // 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::<Editor>()
         .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 editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    editor_a.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![0..1]);
+        });
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![2..3]);
+        });
     });
+    deterministic.run_until_parked();
 
-    // 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::<HashMap<_, _>>()
-        );
+    // 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);
     });
-    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::<HashMap<_, _>>(),
-        )
+    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<ColorIndex>, Range<usize>)],
+    cx: &mut ViewContext<Editor>,
+) {
+    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::<Vec<_>>();
+    assert_eq!(
+        remote_selections, expected_selections,
+        "incorrect remote selections"
+    );
+}
+
 #[gpui::test]
 async fn test_multiple_handles_to_channel_buffer(
     deterministic: Arc<Deterministic>,
@@ -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<_>>(),
-                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<UserId>]) {
+fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
     assert_eq!(
         collaborators
-            .into_iter()
+            .values()
             .map(|collaborator| collaborator.user_id)
             .collect::<Vec<_>>(),
         ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()

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::<Vec<_>>();
+                            collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
                         user_ids.sort();
                         assert_eq!(
                             user_ids,

crates/collab/src/tests/test_server.rs 🔗

@@ -538,15 +538,7 @@ impl TestClient {
         root_path: impl AsRef<Path>,
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, 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<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,
+            )
+        })
+    }
+
     pub async fn build_remote_project(
         &self,
         host_project_id: u64,

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>,
     ) -> 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<Channel> {
         self.channel_buffer.read(cx).channel()
     }
 
-    fn handle_project_event(
-        &mut self,
-        _: ModelHandle<Project>,
-        event: &project::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        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<ChannelBuffer>,
         event: &ChannelBufferEvent,
         cx: &mut ViewContext<Self>,
     ) {
-        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<Self>) {
-        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<u16>,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
         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<ChannelBuffer>);
+
+impl CollaborationHub for ChannelBufferCollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.0.read(cx).collaborators()
+    }
+
+    fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap<u64, theme::ColorIndex> {
+        self.0.read(cx).user_store().read(cx).color_indices()
+    }
+}

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()

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<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
+    collaboration_hub: Option<Box<dyn CollaborationHub>>,
     focused: bool,
     blink_manager: ModelHandle<BlinkManager>,
     pub show_local_selections: bool,
     mode: EditorMode,
-    replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
@@ -608,7 +609,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
     read_only: bool,
-    leader_replica_id: Option<u16>,
+    leader_peer_id: Option<PeerId>,
     remote_id: Option<ViewId>,
     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<Anchor>,
+    pub cursor_shape: CursorShape,
+    pub peer_id: PeerId,
+    pub line_mode: bool,
+    pub color_index: Option<ColorIndex>,
+}
+
 #[derive(Clone, Debug)]
 struct SelectionHistoryEntry {
     selections: Arc<[Selection<Anchor>]>,
@@ -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<ReplicaId> {
-        self.leader_replica_id
+    pub fn leader_peer_id(&self) -> Option<PeerId> {
+        self.leader_peer_id
     }
 
     pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
@@ -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<dyn CollaborationHub>) {
+        self.collaboration_hub = Some(hub);
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -1772,26 +1790,13 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
-        self.replica_id_mapping.as_ref()
-    }
-
-    pub fn set_replica_id_map(
-        &mut self,
-        mapping: Option<HashMap<ReplicaId, ReplicaId>>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.replica_id_mapping = mapping;
-        cx.notify();
-    }
-
     fn selections_did_change(
         &mut self,
         local: bool,
         old_cursor_position: &Anchor,
         cx: &mut ViewContext<Self>,
     ) {
-        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<PeerId, Collaborator>;
+    fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap<u64, ColorIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.read(cx).collaborators()
+    }
+
+    fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap<u64, ColorIndex> {
+        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<Point>, display_map: &DisplaySnapshot)
 }
 
 impl EditorSnapshot {
+    pub fn remote_selections_in_range<'a>(
+        &'a self,
+        range: &'a Range<Anchor>,
+        collaboration_hub: &dyn CollaborationHub,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = RemoteSelection> {
+        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::<HashMap<_, _>>();
+        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<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
         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,

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<DisplayPoint>; 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<Editor> for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
+        let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = 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<Editor> 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<Editor> 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)
+            selections.push((style.selection, 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 {
-                    replica_id
+                    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;
                 }
-            };
 
-            selections.push((Some(local_replica_id), layouts));
+                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::<EditorSettings>(cx).scrollbar;
@@ -2686,7 +2676,7 @@ pub struct LayoutState {
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
-    selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
+    selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
     is_singleton: bool,

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<u16>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        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<PeerId>, cx: &mut ViewContext<Self>) {
+        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);
             });

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"

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<UserStore>,
+    user_color_indices: HashMap<ReplicaId, ColorIndex>,
     fs: Arc<dyn Fs>,
     client_state: Option<ProjectClientState>,
     collaborators: HashMap<proto::PeerId, Collaborator>,
@@ -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<ReplicaId, ColorIndex> {
+        &self.user_color_indices
+    }
+
     pub fn opened_buffers(&self, cx: &AppContext) -> Vec<ModelHandle<Buffer>> {
         self.opened_buffers
             .values()
@@ -8211,16 +8212,6 @@ impl Entity for Project {
     }
 }
 
-impl Collaborator {
-    fn from_proto(message: proto::Collaborator) -> Result<Self> {
-        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<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     fn from((worktree_id, path): (WorktreeId, P)) -> Self {
         Self {

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;
-
-        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;
-
-        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;
-
-        UpdateProject update_project = 41;
-        UpdateWorktree update_worktree = 43;
-
-        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;
-
-        UpdateDiagnosticSummary update_diagnostic_summary = 50;
-        StartLanguageServer start_language_server = 51;
-        UpdateLanguageServer update_language_server = 52;
-
-        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;
-
-        UpdateContacts update_contacts = 92;
-        UpdateInviteInfo update_invite_info = 93;
-        ShowContacts show_contacts = 94;
-
-        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;
-
-        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;
-
-        OnTypeFormatting on_type_formatting = 111;
-        OnTypeFormattingResponse on_type_formatting_response = 112;
-
-        UpdateWorktreeSettings update_worktree_settings = 113;
-
-        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;
-
-        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;
-
-        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;
-
-        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;
-
-        LinkChannel link_channel = 151;
-        UnlinkChannel unlink_channel = 152;
-        MoveChannel move_channel = 153; // Current max: 154
+        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 = 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 = 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 = 44;
+        UpdateWorktree update_worktree = 45;
+
+        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 = 53;
+        StartLanguageServer start_language_server = 54;
+        UpdateLanguageServer update_language_server = 55;
+
+        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 = 87;
+        UpdateInviteInfo update_invite_info = 88;
+        ShowContacts show_contacts = 89;
+
+        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 = 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 = 103;
+        OnTypeFormattingResponse on_type_formatting_response = 104;
+
+        UpdateWorktreeSettings update_worktree_settings = 105;
+
+        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 = 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 = 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 = 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 = 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 {

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;

crates/theme/src/theme.rs 🔗

@@ -1064,14 +1064,16 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
     }
 }
 
+#[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]
     }
 }
 

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::<usize>(cx);
                     local_selections_changed(newest, cx);
                 }

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<Result<()>>;
     fn is_project_item(&self, cx: &AppContext) -> bool;
 
-    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
 }
 
 pub trait FollowableItemHandle: ItemHandle {
     fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext);
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
         &self,
@@ -732,10 +735,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
         })
     }
 
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, 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<PeerId>, 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<proto::view::Variant> {

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()
             }

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::<FollowableItemBuilders>()
@@ -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);
                 }
 

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.
+