Merge branch 'main' into links
This commit is contained in:
commit
d4ef764305
66 changed files with 1668 additions and 657 deletions
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.23.1"
|
||||
version = "0.23.3"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -89,7 +89,7 @@ impl Database {
|
|||
|
||||
let mut rows = channel_message::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_asc(channel_message::Column::Id)
|
||||
.order_by_desc(channel_message::Column::Id)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
@ -110,6 +110,7 @@ impl Database {
|
|||
});
|
||||
}
|
||||
drop(rows);
|
||||
messages.reverse();
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -1,10 +1,75 @@
|
|||
use crate::{
|
||||
db::{Database, NewUserParams},
|
||||
db::{Database, MessageId, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_retrieval,
|
||||
test_channel_message_retrieval_postgres,
|
||||
test_channel_message_retrieval_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
let user = db
|
||||
.create_user(
|
||||
"user@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut all_messages = Vec::new();
|
||||
for i in 0..10 {
|
||||
all_messages.push(
|
||||
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.to_proto(),
|
||||
);
|
||||
}
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(channel, user, 3, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|message| message.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(messages, &all_messages[7..10]);
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(
|
||||
channel,
|
||||
user,
|
||||
4,
|
||||
Some(MessageId::from_proto(all_messages[6])),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|message| message.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(messages, &all_messages[2..6]);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_nonces,
|
||||
test_channel_message_nonces_postgres,
|
||||
|
|
|
@ -1917,13 +1917,10 @@ async fn follow(
|
|||
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let mut response_payload = session
|
||||
let response_payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, leader_id, request)
|
||||
.await?;
|
||||
response_payload
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
if let Some(project_id) = project_id {
|
||||
|
@ -1984,14 +1981,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
|||
.await?
|
||||
};
|
||||
|
||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
let follower_connection_id = follower_peer_id.into();
|
||||
if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
|
||||
if Some(follower_peer_id) != connection_id_to_omit
|
||||
&& connection_ids.contains(&follower_connection_id)
|
||||
{
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
follower_connection_id,
|
||||
|
|
|
@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification;
|
|||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
|
@ -183,20 +184,12 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
|
@ -206,46 +199,39 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that clients B is following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C re-follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx);
|
||||
});
|
||||
workspace_c
|
||||
.update(cx_c, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client D follows client C.
|
||||
// Client D follows client B, then switches to following client C.
|
||||
workspace_d
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace.follow(peer_id_b, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_d
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace.follow(peer_id_c, cx).unwrap()
|
||||
|
@ -255,20 +241,15 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that D is following C
|
||||
cx_d.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[peer_id_d],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[
|
||||
(peer_id_a, vec![peer_id_b, peer_id_c]),
|
||||
(peer_id_c, vec![peer_id_d])
|
||||
],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C closes the project.
|
||||
|
@ -277,32 +258,12 @@ async fn test_basic_following(
|
|||
|
||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// All clients see that no-one is following C
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
("D", &active_call_d, &cx_d),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_c, project_id),
|
||||
&[],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b]),],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// When client A activates a different editor, client B does so as well.
|
||||
|
@ -724,10 +685,9 @@ async fn test_peers_following_each_other(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
// Client A opens a file.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_a1 = workspace_a
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
|
@ -736,10 +696,9 @@ async fn test_peers_following_each_other(
|
|||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client B opens an editor.
|
||||
// Client B opens a different file.
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_b1 = workspace_b
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
|
@ -754,9 +713,7 @@ async fn test_peers_following_each_other(
|
|||
});
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -765,85 +722,443 @@ async fn test_peers_following_each_other(
|
|||
});
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
// Wait for focus effects to be fully flushed
|
||||
workspace_a.update(cx_a, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
// Clients A and B return focus to the original files they had open
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see the other client's focused file in their right pane.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "1.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "2.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Clients A and B each open a new file.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Ensure leader updates don't change the active pane of followers
|
||||
workspace_a.read_with(cx_a, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
workspace_b.read_with(cx_b, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
});
|
||||
|
||||
// Ensure peers following each other doesn't cause an infinite loop.
|
||||
// Both client's see the other client open the new file, but keep their
|
||||
// focus on their own active pane.
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.project_path(cx)),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(true, "3.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client A focuses their right pane, in which they're following client B.
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client B sees that client A is now looking at the same file as them.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B focuses their right pane, in which they're following client A,
|
||||
// who is following them.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client A sees that client B is now looking at the same file as them.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B focuses a file that they previously followed A to, breaking
|
||||
// the follow.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see that client B is looking at that previous file.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
(false, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B closes tabs, some of which were originally opened by client A,
|
||||
// and some of which were originally opened by client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.close_inactive_items(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see that Client B is looking at the previous tab.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "3.txt".into()),]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B follows client A again.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A cycles through some tabs.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client B follows client A into those tabs.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(true, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "3.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "2.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(true, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "3.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "1.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
|
@ -1074,24 +1389,6 @@ async fn test_peers_simultaneously_following_each_other(
|
|||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window.read_with(cx, |window| {
|
||||
if let Some(handle) = window
|
||||
.root_view()
|
||||
.clone()
|
||||
.downcast::<ProjectSharedNotification>()
|
||||
{
|
||||
ret.push(handle)
|
||||
}
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_following_across_workspaces(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
@ -1304,3 +1601,83 @@ async fn test_following_across_workspaces(
|
|||
assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window.read_with(cx, |window| {
|
||||
if let Some(handle) = window
|
||||
.root_view()
|
||||
.clone()
|
||||
.downcast::<ProjectSharedNotification>()
|
||||
{
|
||||
ret.push(handle)
|
||||
}
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PaneSummary {
|
||||
active: bool,
|
||||
leader: Option<PeerId>,
|
||||
items: Vec<(bool, String)>,
|
||||
}
|
||||
|
||||
fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
|
||||
cx.read(|cx| {
|
||||
let active_call = ActiveCall::global(cx).read(cx);
|
||||
let peer_id = active_call.client().peer_id();
|
||||
let room = active_call.room().unwrap().read(cx);
|
||||
let mut result = room
|
||||
.remote_participants()
|
||||
.values()
|
||||
.map(|participant| participant.peer_id)
|
||||
.chain(peer_id)
|
||||
.filter_map(|peer_id| {
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
if followers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((peer_id, followers.to_vec()))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_by_key(|e| e.0);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
let active_pane = workspace.active_pane();
|
||||
workspace
|
||||
.panes()
|
||||
.iter()
|
||||
.map(|pane| {
|
||||
let leader = workspace.leader_for_pane(pane);
|
||||
let active = pane == active_pane;
|
||||
let pane = pane.read(cx);
|
||||
let active_ix = pane.active_item_index();
|
||||
PaneSummary {
|
||||
active,
|
||||
leader,
|
||||
items: pane
|
||||
.items()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
(
|
||||
ix == active_ix,
|
||||
item.tab_description(0, cx)
|
||||
.map_or(String::new(), |s| s.to_string()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ pub struct TestServer {
|
|||
pub struct TestClient {
|
||||
pub username: String,
|
||||
pub app_state: Arc<workspace::AppState>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
state: RefCell<TestClientState>,
|
||||
}
|
||||
|
||||
|
@ -206,15 +207,12 @@ impl TestServer {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(cx.background());
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
workspace_store,
|
||||
channel_store: channel_store.clone(),
|
||||
languages: Arc::new(language_registry),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
|
@ -231,7 +229,7 @@ impl TestServer {
|
|||
workspace::init(app_state.clone(), cx);
|
||||
audio::init((), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
channel::init(&client);
|
||||
channel::init(&client, user_store, cx);
|
||||
});
|
||||
|
||||
client
|
||||
|
@ -242,6 +240,7 @@ impl TestServer {
|
|||
let client = TestClient {
|
||||
app_state,
|
||||
username: name.to_string(),
|
||||
channel_store: cx.read(ChannelStore::global).clone(),
|
||||
state: Default::default(),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
|
@ -310,10 +309,9 @@ impl TestServer {
|
|||
admin: (&TestClient, &mut TestAppContext),
|
||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||
) -> u64 {
|
||||
let (admin_client, admin_cx) = admin;
|
||||
let channel_id = admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
let (_, admin_cx) = admin;
|
||||
let channel_id = admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.create_channel(channel, parent, cx)
|
||||
})
|
||||
|
@ -321,9 +319,8 @@ impl TestServer {
|
|||
.unwrap();
|
||||
|
||||
for (member_client, member_cx) in members {
|
||||
admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
|
@ -337,9 +334,8 @@ impl TestServer {
|
|||
|
||||
admin_cx.foreground().run_until_parked();
|
||||
|
||||
member_client
|
||||
.app_state
|
||||
.channel_store
|
||||
member_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(*member_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel_id, true)
|
||||
})
|
||||
|
@ -447,7 +443,7 @@ impl TestClient {
|
|||
}
|
||||
|
||||
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
|
||||
&self.app_state.channel_store
|
||||
&self.channel_store
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
|
@ -614,8 +610,8 @@ impl TestClient {
|
|||
) {
|
||||
let (other_client, other_cx) = user;
|
||||
|
||||
self.app_state
|
||||
.channel_store
|
||||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
})
|
||||
|
@ -624,11 +620,10 @@ impl TestClient {
|
|||
|
||||
cx_self.foreground().run_until_parked();
|
||||
|
||||
other_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(other_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel, true)
|
||||
other_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(other_cx, |channel_store, _| {
|
||||
channel_store.respond_to_channel_invite(channel, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue