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:
Max Brunsfeld 2023-09-26 15:19:38 -06:00
parent 7711530704
commit 545b5e0161
35 changed files with 707 additions and 639 deletions

View file

@ -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 {

View file

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

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

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

View file

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

View file

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

View file

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