mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
Assign unique color indices to room participants, use those instead of replica_ids
Co-authored-by: Conrad <conrad@zed.dev> Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
7711530704
commit
545b5e0161
35 changed files with 707 additions and 639 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
async fn handle_update_channel_buffer_collaborators(
|
||||
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(
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE room_participants ADD COLUMN color_index INTEGER;
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -18,6 +18,7 @@ pub struct Model {
|
|||
pub calling_user_id: UserId,
|
||||
pub calling_connection_id: i32,
|
||||
pub calling_connection_server_id: Option<ServerId>,
|
||||
pub color_index: i32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,136 +150,158 @@ async fn test_channel_buffer_replica_ids(
|
|||
)
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
|
||||
// Clients A and B join a channel.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Clients A, B, and C join a channel buffer
|
||||
// C first so that the replica IDs in the project and the channel buffer are different
|
||||
let channel_buffer_c = client_c
|
||||
.channel_store()
|
||||
.update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_buffer_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_buffer_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B shares a project
|
||||
client_b
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree("/dir", json!({ "file.txt": "contents" }))
|
||||
.insert_tree("/root", json!({"file.txt": "123"}))
|
||||
.await;
|
||||
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
|
||||
let shared_project_id = active_call_b
|
||||
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
|
||||
let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let project_c = client_c.build_empty_local_project(cx_c);
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
|
||||
|
||||
// Clients A, B, and C open the channel notes
|
||||
let channel_view_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_c = cx_c
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A joins the project
|
||||
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
|
||||
// Clients A, B, and C all insert and select some text
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("a", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client C is in a separate project.
|
||||
client_c.fs().insert_tree("/dir", json!({})).await;
|
||||
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
|
||||
|
||||
// Note that each user has a different replica id in the projects vs the
|
||||
// channel buffer.
|
||||
channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
|
||||
assert_eq!(project_a.read(cx).replica_id(), 1);
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("b", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![1..2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
|
||||
assert_eq!(project_b.read(cx).replica_id(), 0);
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
|
||||
});
|
||||
channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
|
||||
// C is not in the project
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
|
||||
deterministic.run_until_parked();
|
||||
channel_view_c.update(cx_c, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("c", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let channel_window_a =
|
||||
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
|
||||
let channel_window_b =
|
||||
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
|
||||
let channel_window_c = cx_c.add_window(|cx| {
|
||||
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
|
||||
// Client A sees clients B and C without assigned colors, because they aren't
|
||||
// in a call together.
|
||||
deterministic.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
|
||||
});
|
||||
});
|
||||
|
||||
let channel_view_a = channel_window_a.root(cx_a);
|
||||
let channel_view_b = channel_window_b.root(cx_b);
|
||||
let channel_view_c = channel_window_c.root(cx_c);
|
||||
// Clients A and B join the same call.
|
||||
for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
|
||||
call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// For clients A and B, the replica ids in the channel buffer are mapped
|
||||
// so that they match the same users' replica ids in their shared project.
|
||||
channel_view_a.read_with(cx_a, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1)].into_iter().collect::<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 C joins the project that clients A and B are in.
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.join_channel(channel_id, cx))
|
||||
// Client A shares a project, and client B joins.
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
|
||||
deterministic.run_until_parked();
|
||||
project_c.read_with(cx_c, |project, _| {
|
||||
assert_eq!(project.replica_id(), 2);
|
||||
});
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
|
||||
// For clients A and B, client C's replica id in the channel buffer is
|
||||
// now mapped to their replica id in the shared project.
|
||||
channel_view_a.read_with(cx_a, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1), (0, 2)]
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>()
|
||||
);
|
||||
// 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 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]);
|
||||
});
|
||||
});
|
||||
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| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Clients A and B see each other with the same colors as in the channel notes.
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ColorIndex(1)), 2..3)], cx);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1)], cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_remote_selections(
|
||||
editor: &mut Editor,
|
||||
expected_selections: &[(Option<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]
|
||||
|
@ -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<_>>()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
} else {
|
||||
replica_id
|
||||
}
|
||||
};
|
||||
selections.push((style.selection, layouts));
|
||||
}
|
||||
|
||||
selections.push((Some(local_replica_id), layouts));
|
||||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||||
let mut remote_selections = HashMap::default();
|
||||
for selection in snapshot.remote_selections_in_range(
|
||||
&(start_anchor..end_anchor),
|
||||
collaboration_hub.as_ref(),
|
||||
cx,
|
||||
) {
|
||||
let selection_style = if let Some(color_index) = selection.color_index {
|
||||
style.replica_selection_style(color_index)
|
||||
} else {
|
||||
style.absent_selection
|
||||
};
|
||||
|
||||
// The local selections match the leader's selections.
|
||||
if Some(selection.peer_id) == editor.leader_peer_id {
|
||||
if let Some((local_selection_style, _)) = selections.first_mut() {
|
||||
*local_selection_style = selection_style;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
remote_selections
|
||||
.entry(selection.replica_id)
|
||||
.or_insert((selection_style, Vec::new()))
|
||||
.1
|
||||
.push(SelectionLayout::new(
|
||||
selection.selection,
|
||||
selection.line_mode,
|
||||
selection.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
selections.extend(remote_selections.into_values());
|
||||
}
|
||||
|
||||
let scrollbar_settings = &settings::get::<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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -23,154 +23,152 @@ message Envelope {
|
|||
CreateRoomResponse create_room_response = 10;
|
||||
JoinRoom join_room = 11;
|
||||
JoinRoomResponse join_room_response = 12;
|
||||
RejoinRoom rejoin_room = 108;
|
||||
RejoinRoomResponse rejoin_room_response = 109;
|
||||
LeaveRoom leave_room = 13;
|
||||
Call call = 14;
|
||||
IncomingCall incoming_call = 15;
|
||||
CallCanceled call_canceled = 16;
|
||||
CancelCall cancel_call = 17;
|
||||
DeclineCall decline_call = 18;
|
||||
UpdateParticipantLocation update_participant_location = 19;
|
||||
RoomUpdated room_updated = 20;
|
||||
RejoinRoom rejoin_room = 13;
|
||||
RejoinRoomResponse rejoin_room_response = 14;
|
||||
LeaveRoom leave_room = 15;
|
||||
Call call = 16;
|
||||
IncomingCall incoming_call = 17;
|
||||
CallCanceled call_canceled = 18;
|
||||
CancelCall cancel_call = 19;
|
||||
DeclineCall decline_call = 20;
|
||||
UpdateParticipantLocation update_participant_location = 21;
|
||||
RoomUpdated room_updated = 22;
|
||||
|
||||
ShareProject share_project = 21;
|
||||
ShareProjectResponse share_project_response = 22;
|
||||
UnshareProject unshare_project = 23;
|
||||
JoinProject join_project = 24;
|
||||
JoinProjectResponse join_project_response = 25;
|
||||
LeaveProject leave_project = 26;
|
||||
AddProjectCollaborator add_project_collaborator = 27;
|
||||
UpdateProjectCollaborator update_project_collaborator = 110;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 28;
|
||||
ShareProject share_project = 23;
|
||||
ShareProjectResponse share_project_response = 24;
|
||||
UnshareProject unshare_project = 25;
|
||||
JoinProject join_project = 26;
|
||||
JoinProjectResponse join_project_response = 27;
|
||||
LeaveProject leave_project = 28;
|
||||
AddProjectCollaborator add_project_collaborator = 29;
|
||||
UpdateProjectCollaborator update_project_collaborator = 30;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 31;
|
||||
|
||||
GetDefinition get_definition = 29;
|
||||
GetDefinitionResponse get_definition_response = 30;
|
||||
GetTypeDefinition get_type_definition = 31;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 32;
|
||||
GetReferences get_references = 33;
|
||||
GetReferencesResponse get_references_response = 34;
|
||||
GetDocumentHighlights get_document_highlights = 35;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 36;
|
||||
GetProjectSymbols get_project_symbols = 37;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 38;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 39;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
|
||||
GetDefinition get_definition = 32;
|
||||
GetDefinitionResponse get_definition_response = 33;
|
||||
GetTypeDefinition get_type_definition = 34;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 35;
|
||||
GetReferences get_references = 36;
|
||||
GetReferencesResponse get_references_response = 37;
|
||||
GetDocumentHighlights get_document_highlights = 38;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 39;
|
||||
GetProjectSymbols get_project_symbols = 40;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 41;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 42;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
|
||||
|
||||
UpdateProject update_project = 41;
|
||||
UpdateWorktree update_worktree = 43;
|
||||
UpdateProject update_project = 44;
|
||||
UpdateWorktree update_worktree = 45;
|
||||
|
||||
CreateProjectEntry create_project_entry = 45;
|
||||
RenameProjectEntry rename_project_entry = 46;
|
||||
CopyProjectEntry copy_project_entry = 47;
|
||||
DeleteProjectEntry delete_project_entry = 48;
|
||||
ProjectEntryResponse project_entry_response = 49;
|
||||
ExpandProjectEntry expand_project_entry = 114;
|
||||
ExpandProjectEntryResponse expand_project_entry_response = 115;
|
||||
CreateProjectEntry create_project_entry = 46;
|
||||
RenameProjectEntry rename_project_entry = 47;
|
||||
CopyProjectEntry copy_project_entry = 48;
|
||||
DeleteProjectEntry delete_project_entry = 49;
|
||||
ProjectEntryResponse project_entry_response = 50;
|
||||
ExpandProjectEntry expand_project_entry = 51;
|
||||
ExpandProjectEntryResponse expand_project_entry_response = 52;
|
||||
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 50;
|
||||
StartLanguageServer start_language_server = 51;
|
||||
UpdateLanguageServer update_language_server = 52;
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 53;
|
||||
StartLanguageServer start_language_server = 54;
|
||||
UpdateLanguageServer update_language_server = 55;
|
||||
|
||||
OpenBufferById open_buffer_by_id = 53;
|
||||
OpenBufferByPath open_buffer_by_path = 54;
|
||||
OpenBufferResponse open_buffer_response = 55;
|
||||
CreateBufferForPeer create_buffer_for_peer = 56;
|
||||
UpdateBuffer update_buffer = 57;
|
||||
UpdateBufferFile update_buffer_file = 58;
|
||||
SaveBuffer save_buffer = 59;
|
||||
BufferSaved buffer_saved = 60;
|
||||
BufferReloaded buffer_reloaded = 61;
|
||||
ReloadBuffers reload_buffers = 62;
|
||||
ReloadBuffersResponse reload_buffers_response = 63;
|
||||
SynchronizeBuffers synchronize_buffers = 200;
|
||||
SynchronizeBuffersResponse synchronize_buffers_response = 201;
|
||||
FormatBuffers format_buffers = 64;
|
||||
FormatBuffersResponse format_buffers_response = 65;
|
||||
GetCompletions get_completions = 66;
|
||||
GetCompletionsResponse get_completions_response = 67;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69;
|
||||
GetCodeActions get_code_actions = 70;
|
||||
GetCodeActionsResponse get_code_actions_response = 71;
|
||||
GetHover get_hover = 72;
|
||||
GetHoverResponse get_hover_response = 73;
|
||||
ApplyCodeAction apply_code_action = 74;
|
||||
ApplyCodeActionResponse apply_code_action_response = 75;
|
||||
PrepareRename prepare_rename = 76;
|
||||
PrepareRenameResponse prepare_rename_response = 77;
|
||||
PerformRename perform_rename = 78;
|
||||
PerformRenameResponse perform_rename_response = 79;
|
||||
SearchProject search_project = 80;
|
||||
SearchProjectResponse search_project_response = 81;
|
||||
OpenBufferById open_buffer_by_id = 56;
|
||||
OpenBufferByPath open_buffer_by_path = 57;
|
||||
OpenBufferResponse open_buffer_response = 58;
|
||||
CreateBufferForPeer create_buffer_for_peer = 59;
|
||||
UpdateBuffer update_buffer = 60;
|
||||
UpdateBufferFile update_buffer_file = 61;
|
||||
SaveBuffer save_buffer = 62;
|
||||
BufferSaved buffer_saved = 63;
|
||||
BufferReloaded buffer_reloaded = 64;
|
||||
ReloadBuffers reload_buffers = 65;
|
||||
ReloadBuffersResponse reload_buffers_response = 66;
|
||||
SynchronizeBuffers synchronize_buffers = 67;
|
||||
SynchronizeBuffersResponse synchronize_buffers_response = 68;
|
||||
FormatBuffers format_buffers = 69;
|
||||
FormatBuffersResponse format_buffers_response = 70;
|
||||
GetCompletions get_completions = 71;
|
||||
GetCompletionsResponse get_completions_response = 72;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
|
||||
GetCodeActions get_code_actions = 75;
|
||||
GetCodeActionsResponse get_code_actions_response = 76;
|
||||
GetHover get_hover = 77;
|
||||
GetHoverResponse get_hover_response = 78;
|
||||
ApplyCodeAction apply_code_action = 79;
|
||||
ApplyCodeActionResponse apply_code_action_response = 80;
|
||||
PrepareRename prepare_rename = 81;
|
||||
PrepareRenameResponse prepare_rename_response = 82;
|
||||
PerformRename perform_rename = 83;
|
||||
PerformRenameResponse perform_rename_response = 84;
|
||||
SearchProject search_project = 85;
|
||||
SearchProjectResponse search_project_response = 86;
|
||||
|
||||
UpdateContacts update_contacts = 92;
|
||||
UpdateInviteInfo update_invite_info = 93;
|
||||
ShowContacts show_contacts = 94;
|
||||
UpdateContacts update_contacts = 87;
|
||||
UpdateInviteInfo update_invite_info = 88;
|
||||
ShowContacts show_contacts = 89;
|
||||
|
||||
GetUsers get_users = 95;
|
||||
FuzzySearchUsers fuzzy_search_users = 96;
|
||||
UsersResponse users_response = 97;
|
||||
RequestContact request_contact = 98;
|
||||
RespondToContactRequest respond_to_contact_request = 99;
|
||||
RemoveContact remove_contact = 100;
|
||||
GetUsers get_users = 90;
|
||||
FuzzySearchUsers fuzzy_search_users = 91;
|
||||
UsersResponse users_response = 92;
|
||||
RequestContact request_contact = 93;
|
||||
RespondToContactRequest respond_to_contact_request = 94;
|
||||
RemoveContact remove_contact = 95;
|
||||
|
||||
Follow follow = 101;
|
||||
FollowResponse follow_response = 102;
|
||||
UpdateFollowers update_followers = 103;
|
||||
Unfollow unfollow = 104;
|
||||
GetPrivateUserInfo get_private_user_info = 105;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 106;
|
||||
UpdateDiffBase update_diff_base = 107;
|
||||
Follow follow = 96;
|
||||
FollowResponse follow_response = 97;
|
||||
UpdateFollowers update_followers = 98;
|
||||
Unfollow unfollow = 99;
|
||||
GetPrivateUserInfo get_private_user_info = 100;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 101;
|
||||
UpdateDiffBase update_diff_base = 102;
|
||||
|
||||
OnTypeFormatting on_type_formatting = 111;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
||||
OnTypeFormatting on_type_formatting = 103;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 104;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 113;
|
||||
UpdateWorktreeSettings update_worktree_settings = 105;
|
||||
|
||||
InlayHints inlay_hints = 116;
|
||||
InlayHintsResponse inlay_hints_response = 117;
|
||||
ResolveInlayHint resolve_inlay_hint = 137;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 138;
|
||||
RefreshInlayHints refresh_inlay_hints = 118;
|
||||
InlayHints inlay_hints = 106;
|
||||
InlayHintsResponse inlay_hints_response = 107;
|
||||
ResolveInlayHint resolve_inlay_hint = 108;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 109;
|
||||
RefreshInlayHints refresh_inlay_hints = 110;
|
||||
|
||||
CreateChannel create_channel = 119;
|
||||
CreateChannelResponse create_channel_response = 120;
|
||||
InviteChannelMember invite_channel_member = 121;
|
||||
RemoveChannelMember remove_channel_member = 122;
|
||||
RespondToChannelInvite respond_to_channel_invite = 123;
|
||||
UpdateChannels update_channels = 124;
|
||||
JoinChannel join_channel = 125;
|
||||
DeleteChannel delete_channel = 126;
|
||||
GetChannelMembers get_channel_members = 127;
|
||||
GetChannelMembersResponse get_channel_members_response = 128;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 129;
|
||||
RenameChannel rename_channel = 130;
|
||||
RenameChannelResponse rename_channel_response = 154;
|
||||
CreateChannel create_channel = 111;
|
||||
CreateChannelResponse create_channel_response = 112;
|
||||
InviteChannelMember invite_channel_member = 113;
|
||||
RemoveChannelMember remove_channel_member = 114;
|
||||
RespondToChannelInvite respond_to_channel_invite = 115;
|
||||
UpdateChannels update_channels = 116;
|
||||
JoinChannel join_channel = 117;
|
||||
DeleteChannel delete_channel = 118;
|
||||
GetChannelMembers get_channel_members = 119;
|
||||
GetChannelMembersResponse get_channel_members_response = 120;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 121;
|
||||
RenameChannel rename_channel = 122;
|
||||
RenameChannelResponse rename_channel_response = 123;
|
||||
|
||||
JoinChannelBuffer join_channel_buffer = 131;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 132;
|
||||
UpdateChannelBuffer update_channel_buffer = 133;
|
||||
LeaveChannelBuffer leave_channel_buffer = 134;
|
||||
AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
|
||||
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
|
||||
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 140;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
|
||||
JoinChannelBuffer join_channel_buffer = 124;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 125;
|
||||
UpdateChannelBuffer update_channel_buffer = 126;
|
||||
LeaveChannelBuffer leave_channel_buffer = 127;
|
||||
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 129;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
|
||||
|
||||
JoinChannelChat join_channel_chat = 142;
|
||||
JoinChannelChatResponse join_channel_chat_response = 143;
|
||||
LeaveChannelChat leave_channel_chat = 144;
|
||||
SendChannelMessage send_channel_message = 145;
|
||||
SendChannelMessageResponse send_channel_message_response = 146;
|
||||
ChannelMessageSent channel_message_sent = 147;
|
||||
GetChannelMessages get_channel_messages = 148;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 149;
|
||||
RemoveChannelMessage remove_channel_message = 150;
|
||||
JoinChannelChat join_channel_chat = 131;
|
||||
JoinChannelChatResponse join_channel_chat_response = 132;
|
||||
LeaveChannelChat leave_channel_chat = 133;
|
||||
SendChannelMessage send_channel_message = 134;
|
||||
SendChannelMessageResponse send_channel_message_response = 135;
|
||||
ChannelMessageSent channel_message_sent = 136;
|
||||
GetChannelMessages get_channel_messages = 137;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 138;
|
||||
RemoveChannelMessage remove_channel_message = 139;
|
||||
|
||||
LinkChannel link_channel = 151;
|
||||
UnlinkChannel unlink_channel = 152;
|
||||
MoveChannel move_channel = 153; // Current max: 154
|
||||
LinkChannel link_channel = 140;
|
||||
UnlinkChannel unlink_channel = 141;
|
||||
MoveChannel move_channel = 142;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,6 +256,7 @@ message Participant {
|
|||
PeerId peer_id = 2;
|
||||
repeated ParticipantProject projects = 3;
|
||||
ParticipantLocation location = 4;
|
||||
uint32 color_index = 5;
|
||||
}
|
||||
|
||||
message PendingParticipant {
|
||||
|
@ -440,20 +439,9 @@ message RemoveProjectCollaborator {
|
|||
PeerId peer_id = 2;
|
||||
}
|
||||
|
||||
message AddChannelBufferCollaborator {
|
||||
message UpdateChannelBufferCollaborators {
|
||||
uint64 channel_id = 1;
|
||||
Collaborator collaborator = 2;
|
||||
}
|
||||
|
||||
message RemoveChannelBufferCollaborator {
|
||||
uint64 channel_id = 1;
|
||||
PeerId peer_id = 2;
|
||||
}
|
||||
|
||||
message UpdateChannelBufferCollaborator {
|
||||
uint64 channel_id = 1;
|
||||
PeerId old_peer_id = 2;
|
||||
PeerId new_peer_id = 3;
|
||||
repeated Collaborator collaborators = 2;
|
||||
}
|
||||
|
||||
message GetDefinition {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
14
selection-color-notes.txt
Normal file
14
selection-color-notes.txt
Normal file
|
@ -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.
|
||||
|
Loading…
Reference in a new issue