diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 878f0d67cfaf6ec03cf51880c2dff67566454ec0..9a87f91b8182b69b32539810e9fc46f7afdadb14 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -888,6 +888,14 @@ impl Database { .exec(&*tx) .await?; + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::FollowerConnectionId.eq(connection.id as i32)), + ) + .exec(&*tx) + .await?; + // Unshare projects. project::Entity::delete_many() .filter( diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 5178df408f95b8809495836576c3f1c74159cf85..0486e294619fd4fd34604c6cc4113fb03f1057ad 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,1890 +1,1777 @@ -//todo!(workspace) - -// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -// use call::ActiveCall; -// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; -// use editor::{Editor, ExcerptRange, MultiBuffer}; -// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext}; -// use live_kit_client::MacOSDisplay; -// use project::project_settings::ProjectSettings; -// use rpc::proto::PeerId; -// use serde_json::json; -// use settings::SettingsStore; -// use std::borrow::Cow; -// use workspace::{ -// dock::{test::TestPanel, DockPosition}, -// item::{test::TestItem, ItemHandle as _}, -// shared_screen::SharedScreen, -// SplitDirection, Workspace, -// }; - -// #[gpui::test(iterations = 10)] -// async fn test_basic_following( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// cx_d: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; -// let client_d = server.create_client(cx_d, "user_d").await; -// server -// .create_room(&mut [ -// (&client_a, cx_a), -// (&client_b, cx_b), -// (&client_c, cx_c), -// (&client_d, cx_d), -// ]) -// .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); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one\none\none", -// "2.txt": "two\ntwo\ntwo", -// "3.txt": "three\nthree\nthree", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a).unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; -// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c); -// let cx_c = &mut cx_c; -// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d); -// let cx_d = &mut cx_d; - -// // Client A opens some editors. -// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); -// let editor_a1 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_a2 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client B opens an editor. -// let editor_b1 = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let peer_id_a = client_a.peer_id().unwrap(); -// let peer_id_b = client_b.peer_id().unwrap(); -// let peer_id_c = client_c.peer_id().unwrap(); -// let peer_id_d = client_d.peer_id().unwrap(); - -// // Client A updates their selections in those editors -// editor_a1.update(cx_a, |editor, cx| { -// editor.handle_input("a", cx); -// editor.handle_input("b", cx); -// editor.handle_input("c", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![3..2]); -// }); -// editor_a2.update(cx_a, |editor, cx| { -// editor.handle_input("d", cx); -// editor.handle_input("e", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![2..1]); -// }); - -// // When client B starts following client A, all visible view states are replicated to client B. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// cx_c.executor().run_until_parked(); -// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// cx_b.read(|cx| editor_b2.project_path(cx)), -// Some((worktree_id, "2.txt").into()) -// ); -// assert_eq!( -// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![2..1] -// ); -// assert_eq!( -// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![3..2] -// ); - -// cx_c.executor().run_until_parked(); -// let active_call_c = cx_c.read(ActiveCall::global); -// let project_c = client_c.build_remote_project(project_id, cx_c).await; -// let window_c = client_c.build_workspace(&project_c, cx_c); -// let workspace_c = window_c.root(cx_c).unwrap(); -// active_call_c -// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) -// .await -// .unwrap(); -// drop(project_c); - -// // Client C also follows client A. -// workspace_c -// .update(cx_c, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// cx_d.executor().run_until_parked(); -// let active_call_d = cx_d.read(ActiveCall::global); -// let project_d = client_d.build_remote_project(project_id, cx_d).await; -// let workspace_d = client_d -// .build_workspace(&project_d, cx_d) -// .root(cx_d) -// .unwrap(); -// active_call_d -// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) -// .await -// .unwrap(); -// drop(project_d); - -// // All clients see that clients B and C are following client A. -// cx_c.executor().run_until_parked(); -// 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. -// workspace_c.update(cx_c, |workspace, cx| { -// workspace.unfollow(&workspace.active_pane().clone(), cx); -// }); - -// // All clients see that clients B is following client A. -// cx_c.executor().run_until_parked(); -// 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).unwrap() -// }) -// .await -// .unwrap(); - -// // All clients see that clients B and C are following client A. -// cx_c.executor().run_until_parked(); -// 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 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() -// }) -// .await -// .unwrap(); - -// // All clients see that D is following C -// cx_d.executor().run_until_parked(); -// 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. -// window_c.remove(cx_c); -// cx_c.drop_last(workspace_c); - -// // Clients A and B see that client B is following A, and client C is not present in the followers. -// cx_c.executor().run_until_parked(); -// 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. -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.activate_item(&editor_a1, cx) -// }); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// // When client A opens a multibuffer, client B does so as well. -// let multibuffer_a = cx_a.build_model(|cx| { -// let buffer_a1 = project_a.update(cx, |project, cx| { -// project -// .get_open_buffer(&(worktree_id, "1.txt").into(), cx) -// .unwrap() -// }); -// let buffer_a2 = project_a.update(cx, |project, cx| { -// project -// .get_open_buffer(&(worktree_id, "2.txt").into(), cx) -// .unwrap() -// }); -// let mut result = MultiBuffer::new(0); -// result.push_excerpts( -// buffer_a1, -// [ExcerptRange { -// context: 0..3, -// primary: None, -// }], -// cx, -// ); -// result.push_excerpts( -// buffer_a2, -// [ExcerptRange { -// context: 4..7, -// primary: None, -// }], -// cx, -// ); -// result -// }); -// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { -// let editor = -// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); -// workspace.add_item(Box::new(editor.clone()), cx); -// editor -// }); -// executor.run_until_parked(); -// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), -// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), -// ); - -// // When client A navigates back and forth, client B does so as well. -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b2.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_forward(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// // Changes to client A's editor are reflected on client B. -// editor_a1.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); -// }); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| { -// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); -// }); - -// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - -// editor_a1.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.set_scroll_position(point(0., 100.), cx); -// }); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| { -// assert_eq!(editor.selections.ranges(cx), &[3..3]); -// }); - -// // After unfollowing, client B stops receiving updates from client A. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.unfollow(&workspace.active_pane().clone(), cx) -// }); -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.activate_item(&editor_a2, cx) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_b1.item_id() -// ); - -// // Client A starts following client B. -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(peer_id_b, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// Some(peer_id_b) -// ); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_a1.item_id() -// ); - -// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. -// let display = MacOSDisplay::new(); -// active_call_b -// .update(cx_b, |call, cx| call.set_location(None, cx)) -// .await -// .unwrap(); -// active_call_b -// .update(cx_b, |call, cx| { -// call.room().unwrap().update(cx, |room, cx| { -// room.set_display_sources(vec![display.clone()]); -// room.share_screen(cx) -// }) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// let shared_screen = workspace_a.update(cx_a, |workspace, cx| { -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item isn't a shared screen") -// }); - -// // Client B activates Zed again, which causes the previous editor to become focused again. -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_a1.item_id() -// ) -// }); - -// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.activate_item(&multibuffer_editor_b, cx) -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_id() -// ) -// }); - -// // Client B activates a panel, and the previously-opened screen-sharing item gets activated. -// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left)); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.add_panel(panel, cx); -// workspace.toggle_panel_focus::(cx); -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_id() -// ); - -// // Toggling the focus back to the pane causes client A to return to the multibuffer. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_id() -// ) -// }); - -// // Client B activates an item that doesn't implement following, -// // so the previously-opened screen-sharing item gets activated. -// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new()); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.add_item(Box::new(unfollowable_item), true, true, None, cx) -// }) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_id() -// ); - -// // Following interrupts when client B disconnects. -// client_b.disconnect(&cx_b.to_async()); -// executor.advance_clock(RECONNECT_TIMEOUT); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// None -// ); -// } - -// #[gpui::test] -// async fn test_following_tab_order( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .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); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); - -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); - -// let client_b_id = project_a.update(cx_a, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); - -// //Open 1, 3 in that order on client A -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "3.txt"), None, true, cx) -// }) -// .await -// .unwrap(); - -// let pane_paths = |pane: &View, cx: &mut TestAppContext| { -// pane.update(cx, |pane, cx| { -// pane.items() -// .map(|item| { -// item.project_path(cx) -// .unwrap() -// .path -// .to_str() -// .unwrap() -// .to_owned() -// }) -// .collect::>() -// }) -// }; - -// //Verify that the tabs opened in the order we expect -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); - -// //Follow client B as client A -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(client_b_id, cx).unwrap() -// }) -// .await -// .unwrap(); - -// //Open just 2 on client B -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// // Verify that newly opened followed file is at the end -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); - -// //Open just 1 on client B -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); -// executor.run_until_parked(); - -// // Verify that following into 1 did not reorder -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_peers_following_each_other( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .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); - -// // Client A shares a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// "4.txt": "four", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Client B joins the project. -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// // Client A opens a file. -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client B opens a different file. -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Clients A and B follow each other in split panes -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); -// }); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(client_b.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); -// }); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); - -// // 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)); -// executor.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.open_path((worktree_id, "4.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// // Both client's see the other client open the new file, but keep their -// // focus on their own active pane. -// assert_eq!( -// 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()) -// ] -// }, -// ] -// ); -// 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)); -// executor.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)); -// executor.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); -// }); -// }); -// executor.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(); -// }); -// }); - -// executor.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); -// }); -// }); -// executor.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| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.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![ -// (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_a.update(cx_a, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.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)] -// async fn test_auto_unfollowing( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// // 2 clients connect to a server. -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .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); - -// // Client A shares a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// // Client A opens some editors. -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let _editor_a1 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client B starts following client A. -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let leader_id = project_b.update(cx_b, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); -// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// // When client B moves, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); - -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); - -// // When client B edits, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); - -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); - -// // When client B scrolls, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| { -// editor.set_scroll_position(point(0., 3.), cx) -// }); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); - -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); - -// // When client B activates a different pane, it continues following client A in the original pane. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) -// }); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); - -// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); - -// // When client B activates a different item in the original pane, it automatically stops following client A. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_peers_simultaneously_following_each_other( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// client_a.fs().insert_tree("/a", json!({})).await; -// let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); - -// executor.run_until_parked(); -// let client_a_id = project_b.update(cx_b, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); -// let client_b_id = project_a.update(cx_a, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); - -// let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { -// workspace.follow(client_b_id, cx).unwrap() -// }); -// let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { -// workspace.follow(client_a_id, cx).unwrap() -// }); - -// futures::try_join!(a_follow_b, b_follow_a).unwrap(); -// workspace_a.update(cx_a, |workspace, _| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_b_id) -// ); -// }); -// workspace_b.update(cx_b, |workspace, _| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a_id) -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_following_across_workspaces( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// // a and b join a channel/call -// // a shares project 1 -// // b shares project 2 -// // -// // b follows a: causes project 2 to be joined, and b to follow a. -// // b opens a different file in project 2, a follows b -// // b opens a different file in project 1, a cannot follow b -// // b shares the project, a joins the project and follows b -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "w.rs": "", -// "x.rs": "", -// }), -// ) -// .await; - -// client_b -// .fs() -// .insert_tree( -// "/b", -// json!({ -// "y.rs": "", -// "z.rs": "", -// }), -// ) -// .await; - -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; -// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; - -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); - -// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); -// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); - -// active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) -// }) -// .await -// .unwrap(); - -// executor.run_until_parked(); -// assert_eq!(visible_push_notifications(cx_b).len(), 1); - -// workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .follow(client_a.peer_id().unwrap(), cx) -// .unwrap() -// .detach() -// }); - -// executor.run_until_parked(); -// let workspace_b_project_a = cx_b -// .windows() -// .iter() -// .max_by_key(|window| window.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_b) -// .unwrap(); - -// // assert that b is following a in project a in w.rs -// workspace_b_project_a.update(cx_b, |workspace, cx| { -// assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); -// assert_eq!( -// client_a.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs")); -// }); - -// // TODO: in app code, this would be done by the collab_ui. -// active_call_b -// .update(cx_b, |call, cx| { -// let project = workspace_b_project_a.read(cx).project().clone(); -// call.set_location(Some(&project), cx) -// }) -// .await -// .unwrap(); - -// // assert that there are no share notifications open -// assert_eq!(visible_push_notifications(cx_b).len(), 0); - -// // b moves to x.rs in a's project, and a follows -// workspace_b_project_a -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) -// }) -// .await -// .unwrap(); - -// executor.run_until_parked(); -// workspace_b_project_a.update(cx_b, |workspace, cx| { -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); -// }); - -// workspace_a.update(cx_a, |workspace, cx| { -// workspace -// .follow(client_b.peer_id().unwrap(), cx) -// .unwrap() -// .detach() -// }); - -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); -// assert_eq!( -// client_b.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_pane().read(cx).active_item().unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into()); -// }); - -// // b moves to y.rs in b's project, a is still following but can't yet see -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) -// }) -// .await -// .unwrap(); - -// // TODO: in app code, this would be done by the collab_ui. -// active_call_b -// .update(cx_b, |call, cx| { -// let project = workspace_b.read(cx).project().clone(); -// call.set_location(Some(&project), cx) -// }) -// .await -// .unwrap(); - -// let project_b_id = active_call_b -// .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) -// .await -// .unwrap(); - -// executor.run_until_parked(); -// assert_eq!(visible_push_notifications(cx_a).len(), 1); -// cx_a.update(|cx| { -// workspace::join_remote_project( -// project_b_id, -// client_b.user_id().unwrap(), -// client_a.app_state.clone(), -// cx, -// ) -// }) -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// assert_eq!(visible_push_notifications(cx_a).len(), 0); -// let workspace_a_project_b = cx_a -// .windows() -// .iter() -// .max_by_key(|window| window.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_a) -// .unwrap(); - -// workspace_a_project_b.update(cx_a, |workspace, cx| { -// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); -// assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); -// assert_eq!( -// client_b.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); -// }); -// } - -// #[gpui::test] -// async fn test_following_into_excluded_file( -// executor: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// for cx in [&mut cx_a, &mut cx_b] { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |project_settings| { -// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); -// }); -// }); -// }); -// } -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .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); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// ".git": { -// "COMMIT_EDITMSG": "write your commit message here", -// }, -// "1.txt": "one\none\none", -// "2.txt": "two\ntwo\ntwo", -// "3.txt": "three\nthree\nthree", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a).unwrap(); -// let peer_id_a = client_a.peer_id().unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// // Client A opens editors for a regular file and an excluded file. -// let editor_for_regular = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_for_excluded_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client A updates their selections in those editors -// editor_for_regular.update(cx_a, |editor, cx| { -// editor.handle_input("a", cx); -// editor.handle_input("b", cx); -// editor.handle_input("c", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![3..2]); -// }); -// editor_for_excluded_a.update(cx_a, |editor, cx| { -// editor.select_all(&Default::default(), cx); -// editor.handle_input("new commit message", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![18..17]); -// }); - -// // When client B starts following client A, currently visible file is replicated -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), -// Some((worktree_id, ".git/COMMIT_EDITMSG").into()) -// ); -// assert_eq!( -// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![18..17] -// ); - -// // Changes from B to the excluded file are replicated in A's editor -// editor_for_excluded_b.update(cx_b, |editor, cx| { -// editor.handle_input("\nCo-Authored-By: B ", cx); -// }); -// executor.run_until_parked(); -// editor_for_excluded_a.update(cx_a, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "new commit messag\nCo-Authored-By: B " -// ); -// }); -// } - -// fn visible_push_notifications( -// cx: &mut TestAppContext, -// ) -> Vec> { -// let mut ret = Vec::new(); -// for window in cx.windows() { -// window.update(cx, |window| { -// if let Some(handle) = window -// .root_view() -// .clone() -// .downcast::() -// { -// ret.push(handle) -// } -// }); -// } -// ret -// } - -// #[derive(Debug, PartialEq, Eq)] -// struct PaneSummary { -// active: bool, -// leader: Option, -// items: Vec<(bool, String)>, -// } - -// fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { -// 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::>(); -// result.sort_by_key(|e| e.0); -// result -// }) -// } - -// fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { -// workspace.update(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() -// }) -// } +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use gpui::{ + point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext, + VisualTestContext, +}; +use language::Capability; +use live_kit_client::MacOSDisplay; +use project::project_settings::ProjectSettings; +use rpc::proto::PeerId; +use serde_json::json; +use settings::SettingsStore; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; + +#[gpui::test(iterations = 10)] +async fn test_basic_following( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + let client_d = server.create_client(cx_d, "user_d").await; + server + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .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); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens some editors. + let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); + + // Client A updates their selections in those editors + editor_a1.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_a2.update(cx_a, |editor, cx| { + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); + }); + + // When client B starts following client A, all visible view states are replicated to client B. + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + + cx_c.executor().run_until_parked(); + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + + executor.run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + let weak_project_c = project_c.downgrade(); + drop(project_c); + + // Client C also follows client A. + workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); + + cx_d.executor().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + + // All clients see that clients B and C are following client A. + cx_c.executor().run_until_parked(); + 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. + workspace_c.update(cx_c, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx); + }); + + // All clients see that clients B is following client A. + cx_c.executor().run_until_parked(); + 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)); + + // All clients see that clients B and C are following client A. + cx_c.executor().run_until_parked(); + 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 B, then switches to following client C. + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx)); + cx_a.executor().run_until_parked(); + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx)); + + // All clients see that D is following C + cx_a.executor().run_until_parked(); + 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. + let weak_workspace_c = workspace_c.downgrade(); + workspace_c.update(cx_c, |workspace, cx| { + workspace.close_window(&Default::default(), cx); + }); + cx_c.update(|_| { + drop(workspace_c); + }); + cx_b.executor().run_until_parked(); + // are you sure you want to leave the call? + cx_c.simulate_prompt_answer(0); + cx_b.executor().run_until_parked(); + executor.run_until_parked(); + + weak_workspace_c.assert_dropped(); + weak_project_c.assert_dropped(); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + executor.run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("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. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a1, cx) + }); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.new_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0, Capability::ReadWrite); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + executor.run_until_parked(); + let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), + ); + + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b2.item_id() + ); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + // Changes to client A's editor are reflected on client B. + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + }); + executor.run_until_parked(); + cx_b.background_executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); + + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); + + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(point(0., 100.), cx); + }); + executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx) + }); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + editor_b1.item_id() + ); + + // Client A starts following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(peer_id_b) + ); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + editor_a1.item_id() + ); + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = MacOSDisplay::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_display_sources(vec![display.clone()]); + room.share_screen(cx) + }) + }) + .await + .unwrap(); + executor.run_until_parked(); + let shared_screen = workspace_a.update(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); + + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_a1.item_id() + ) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = cx_b.new_view(|cx| TestItem::new(cx)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + executor.advance_clock(RECONNECT_TIMEOUT); + assert_eq!( + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); +} + +#[gpui::test] +async fn test_following_tab_order( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .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); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); + + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); + + let client_b_id = project_a.update(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + //Open 1, 3 in that order on client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + + let pane_paths = |pane: &View, cx: &mut VisualTestContext| { + pane.update(cx, |pane, cx| { + pane.items() + .map(|item| { + item.project_path(cx) + .unwrap() + .path + .to_str() + .unwrap() + .to_owned() + }) + .collect::>() + }) + }; + + //Verify that the tabs opened in the order we expect + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); + + //Follow client B as client A + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + executor.run_until_parked(); + + //Open just 2 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + + // Verify that newly opened followed file is at the end + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); + + //Open just 1 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); + executor.run_until_parked(); + + // Verify that following into 1 did not reorder + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .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); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens a file. + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens a different file. + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a.update(cx_a, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); + + // 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)); + executor.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.open_path((worktree_id, "4.txt"), None, true, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + + // Both client's see the other client open the new file, but keep their + // focus on their own active pane. + assert_eq!( + 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()) + ] + }, + ] + ); + 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)); + executor.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)); + executor.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); + }); + }); + executor.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(); + }); + }); + + executor.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) + }); + executor.run_until_parked(); + // 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); + }); + }); + executor.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| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.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![ + (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_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.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)] +async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + // 2 clients connect to a server. + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .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); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.update(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(point(0., 3.), cx) + }); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_simultaneously_following_each_other( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a.fs().insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + executor.run_until_parked(); + let client_a_id = project_b.update(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.update(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx)); + executor.run_until_parked(); + + workspace_a.update(cx_a, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_b_id) + ); + }); + workspace_b.update(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a_id) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + // a and b join a channel/call + // a shares project 1 + // b shares project 2 + // + // b follows a: causes project 2 to be joined, and b to follow a. + // b opens a different file in project 2, a follows b + // b opens a different file in project 1, a cannot follow b + // b shares the project, a joins the project and follows b + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "w.rs": "", + "x.rs": "", + }), + ) + .await; + + client_b + .fs() + .insert_tree( + "/b", + json!({ + "y.rs": "", + "z.rs": "", + }), + ) + .await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; + let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); + cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); + + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + }) + .await + .unwrap(); + + executor.run_until_parked(); + assert_eq!(visible_push_notifications(cx_b).len(), 1); + + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + + executor.run_until_parked(); + let window_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.window_id()) + .unwrap() + .clone(); + + let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b); + + let workspace_b_project_a = window_b_project_a + .downcast::() + .unwrap() + .root(cx_b) + .unwrap(); + + // assert that b is following a in project a in w.rs + workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { + assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); + assert_eq!( + client_a.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("w.rs") + ); + }); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(&mut cx_b2, |call, cx| { + let project = workspace_b_project_a.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + // assert that there are no share notifications open + assert_eq!(visible_push_notifications(cx_b).len(), 0); + + // b moves to x.rs in a's project, and a follows + workspace_b_project_a + .update(&mut cx_b2, |workspace, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + }) + .await + .unwrap(); + + executor.run_until_parked(); + workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("x.rs") + ); + }); + + workspace_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx) + }); + + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_pane().read(cx).active_item().unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs"); + }); + + // b moves to y.rs in b's project, a is still following but can't yet see + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + }) + .await + .unwrap(); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + executor.run_until_parked(); + assert_eq!(visible_push_notifications(cx_a).len(), 1); + cx_a.update(|cx| { + workspace::join_remote_project( + project_b_id, + client_b.user_id().unwrap(), + client_a.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); + + executor.run_until_parked(); + + assert_eq!(visible_push_notifications(cx_a).len(), 0); + let window_a_project_b = cx_a + .windows() + .iter() + .max_by_key(|window| window.window_id()) + .unwrap() + .clone(); + let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a); + let workspace_a_project_b = window_a_project_b + .downcast::() + .unwrap() + .root(cx_a) + .unwrap(); + + workspace_a_project_b.update(cx_a2, |workspace, cx| { + assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("y.rs") + ); + }); +} + +#[gpui::test] +async fn test_following_into_excluded_file( + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + for cx in [&mut cx_a, &mut cx_b] { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); + }); + }); + }); + } + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let peer_id_a = client_a.peer_id().unwrap(); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".git": { + "COMMIT_EDITMSG": "write your commit message here", + }, + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens editors for a regular file and an excluded file. + let editor_for_regular = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_for_excluded_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client A updates their selections in those editors + editor_for_regular.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_for_excluded_a.update(cx_a, |editor, cx| { + editor.select_all(&Default::default(), cx); + editor.handle_input("new commit message", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![18..17]); + }); + + // When client B starts following client A, currently visible file is replicated + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + executor.run_until_parked(); + + let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), + Some((worktree_id, ".git/COMMIT_EDITMSG").into()) + ); + assert_eq!( + editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![18..17] + ); + + // Changes from B to the excluded file are replicated in A's editor + editor_for_excluded_b.update(cx_b, |editor, cx| { + editor.handle_input("\nCo-Authored-By: B ", cx); + }); + executor.run_until_parked(); + editor_for_excluded_a.update(cx_a, |editor, cx| { + assert_eq!( + editor.text(cx), + "new commit messag\nCo-Authored-By: B " + ); + }); +} + +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window + .update(cx, |window, _| { + if let Ok(handle) = window.downcast::() { + ret.push(handle) + } + }) + .unwrap(); + } + ret +} + +#[derive(Debug, PartialEq, Eq)] +struct PaneSummary { + active: bool, + leader: Option, + items: Vec<(bool, String)>, +} + +fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { + 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::>(); + result.sort_by_key(|e| e.0); + result + }) +} + +fn pane_summaries(workspace: &View, cx: &mut VisualTestContext) -> Vec { + workspace.update(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() + }) +} diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a84b866e1f8139a8ca558ccd54ac57c1d6e08bdd..b64e3cccc3e6330429eefb54a410024cbc5e1ce6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8131,8 +8131,8 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo /// Handle completion request passing a marked string specifying where the completion /// should be triggered from using '|' character, what range should be replaced, and what completions /// should be returned using '<' and '>' to delimit the range -pub fn handle_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, +pub fn handle_completion_request( + cx: &mut EditorLspTestContext, marked_string: &str, completions: Vec<&'static str>, ) -> impl Future { @@ -8177,8 +8177,8 @@ pub fn handle_completion_request<'a>( } } -fn handle_resolve_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, +fn handle_resolve_completion_request( + cx: &mut EditorLspTestContext, edits: Option>, ) -> impl Future { let edits = edits.map(|edits| { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 7ee55cddba1edba9356be2c6773c3f097f57c1c8..70c1699b83d090ef24c74f393cd8530502b7ce02 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -21,19 +21,19 @@ use workspace::{AppState, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; -pub struct EditorLspTestContext<'a> { - pub cx: EditorTestContext<'a>, +pub struct EditorLspTestContext { + pub cx: EditorTestContext, pub lsp: lsp::FakeLanguageServer, pub workspace: View, pub buffer_lsp_url: lsp::Url, } -impl<'a> EditorLspTestContext<'a> { +impl EditorLspTestContext { pub async fn new( mut language: Language, capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let app_state = cx.update(AppState::test); cx.update(|cx| { @@ -110,8 +110,8 @@ impl<'a> EditorLspTestContext<'a> { pub async fn new_rust( capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let language = Language::new( LanguageConfig { name: "Rust".into(), @@ -152,8 +152,8 @@ impl<'a> EditorLspTestContext<'a> { pub async fn new_typescript( capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let mut word_characters: HashSet = Default::default(); word_characters.insert('$'); word_characters.insert('#'); @@ -283,15 +283,15 @@ impl<'a> EditorLspTestContext<'a> { } } -impl<'a> Deref for EditorLspTestContext<'a> { - type Target = EditorTestContext<'a>; +impl Deref for EditorLspTestContext { + type Target = EditorTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for EditorLspTestContext<'a> { +impl DerefMut for EditorLspTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index bd5acb99459c131d316a5376688f3d4bcb81da93..18916f844cec8dc379de5f56259940e183aad447 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -26,15 +26,15 @@ use util::{ use super::build_editor_with_project; -pub struct EditorTestContext<'a> { - pub cx: gpui::VisualTestContext<'a>, +pub struct EditorTestContext { + pub cx: gpui::VisualTestContext, pub window: AnyWindowHandle, pub editor: View, pub assertion_cx: AssertionContextManager, } -impl<'a> EditorTestContext<'a> { - pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { +impl EditorTestContext { + pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext { let fs = FakeFs::new(cx.executor()); // fs.insert_file("/file", "".to_owned()).await; fs.insert_tree( @@ -342,7 +342,7 @@ impl<'a> EditorTestContext<'a> { } } -impl<'a> Deref for EditorTestContext<'a> { +impl Deref for EditorTestContext { type Target = gpui::TestAppContext; fn deref(&self) -> &Self::Target { @@ -350,7 +350,7 @@ impl<'a> Deref for EditorTestContext<'a> { } } -impl<'a> DerefMut for EditorTestContext<'a> { +impl DerefMut for EditorTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 589f634d01b7db8c5889f134e9707eabad4943bc..323d4555671f1020b970eff091d28dd694977293 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1843,7 +1843,7 @@ mod tests { expected_matches: usize, expected_editor_title: &str, workspace: &View, - cx: &mut gpui::VisualTestContext<'_>, + cx: &mut gpui::VisualTestContext, ) -> Vec { let picker = open_file_picker(&workspace, cx); cx.simulate_input(input); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index a5f66be09b2f2ab15ad4e7a465876071d0b01414..470315f887c6fd5e4c6d3523498dbe496ff041a8 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -187,6 +187,10 @@ impl TestAppContext { self.test_window(window_handle).simulate_resize(size); } + pub fn windows(&self) -> Vec { + self.app.borrow().windows().clone() + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -479,21 +483,24 @@ impl View { } use derive_more::{Deref, DerefMut}; -#[derive(Deref, DerefMut)] -pub struct VisualTestContext<'a> { +#[derive(Deref, DerefMut, Clone)] +pub struct VisualTestContext { #[deref] #[deref_mut] - cx: &'a mut TestAppContext, + cx: TestAppContext, window: AnyWindowHandle, } -impl<'a> VisualTestContext<'a> { +impl<'a> VisualTestContext { pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() } - pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { - Self { cx, window } + pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { + Self { + cx: cx.clone(), + window, + } } pub fn run_until_parked(&self) { @@ -527,7 +534,7 @@ impl<'a> VisualTestContext<'a> { } } -impl<'a> Context for VisualTestContext<'a> { +impl Context for VisualTestContext { type Result = ::Result; fn new_model( @@ -578,7 +585,7 @@ impl<'a> Context for VisualTestContext<'a> { } } -impl<'a> VisualContext for VisualTestContext<'a> { +impl VisualContext for VisualTestContext { fn new_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -587,7 +594,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: 'static + Render, { self.window - .update(self.cx, |_, cx| cx.new_view(build_view)) + .update(&mut self.cx, |_, cx| cx.new_view(build_view)) .unwrap() } @@ -597,7 +604,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Self::Result { self.window - .update(self.cx, |_, cx| cx.update_view(view, update)) + .update(&mut self.cx, |_, cx| cx.update_view(view, update)) .unwrap() } @@ -609,13 +616,13 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: 'static + Render, { self.window - .update(self.cx, |_, cx| cx.replace_root_view(build_view)) + .update(&mut self.cx, |_, cx| cx.replace_root_view(build_view)) .unwrap() } fn focus_view(&mut self, view: &View) -> Self::Result<()> { self.window - .update(self.cx, |_, cx| { + .update(&mut self.cx, |_, cx| { view.read(cx).focus_handle(cx).clone().focus(cx) }) .unwrap() @@ -626,7 +633,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: crate::ManagedView, { self.window - .update(self.cx, |_, cx| { + .update(&mut self.cx, |_, cx| { view.update(cx, |_, cx| cx.emit(crate::DismissEvent)) }) .unwrap() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 3f1cbce1bded6c7b7517597606f176062c34e06b..c889f0a4a4c11d3f104e130e34e5b87c092565d6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1091,13 +1091,10 @@ mod tests { theme::init(theme::LoadThemes::JustBase, cx); }); } + fn init_test( cx: &mut TestAppContext, - ) -> ( - View, - View, - &mut VisualTestContext<'_>, - ) { + ) -> (View, View, &mut VisualTestContext) { init_globals(cx); let buffer = cx.new_model(|cx| { Buffer::new( diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 32bad620ba03b0b42b59257c9af9f3686e104b38..5e3e4c3c23d27ab3f59f31b759573c9d1c2203d3 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -217,7 +217,6 @@ impl TerminalPanel { pane::Event::Remove => cx.emit(PanelEvent::Close), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::Focus => cx.emit(PanelEvent::Focus), pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { diff --git a/crates/vim/src/test/neovim_backed_binding_test_context.rs b/crates/vim/src/test/neovim_backed_binding_test_context.rs index 15fce99aad3f4ea0e03129342a4bca48fba4166f..0f64a6c849a32b6764cd5d605b5568c64759d89d 100644 --- a/crates/vim/src/test/neovim_backed_binding_test_context.rs +++ b/crates/vim/src/test/neovim_backed_binding_test_context.rs @@ -4,30 +4,27 @@ use crate::state::Mode; use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES}; -pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> { - cx: NeovimBackedTestContext<'a>, +pub struct NeovimBackedBindingTestContext { + cx: NeovimBackedTestContext, keystrokes_under_test: [&'static str; COUNT], } -impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> { - pub fn new( - keystrokes_under_test: [&'static str; COUNT], - cx: NeovimBackedTestContext<'a>, - ) -> Self { +impl NeovimBackedBindingTestContext { + pub fn new(keystrokes_under_test: [&'static str; COUNT], cx: NeovimBackedTestContext) -> Self { Self { cx, keystrokes_under_test, } } - pub fn consume(self) -> NeovimBackedTestContext<'a> { + pub fn consume(self) -> NeovimBackedTestContext { self.cx } pub fn binding( self, keystrokes: [&'static str; NEW_COUNT], - ) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> { + ) -> NeovimBackedBindingTestContext { self.consume().binding(keystrokes) } @@ -80,15 +77,15 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> { } } -impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> { - type Target = NeovimBackedTestContext<'a>; +impl Deref for NeovimBackedBindingTestContext { + type Target = NeovimBackedTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> { +impl DerefMut for NeovimBackedBindingTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7380537655b1e2765003aeaec37d7918c2607bfb..fe5c5db62f831a3725e753ef4df3d448c28c4e68 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -47,8 +47,8 @@ impl ExemptionFeatures { } } -pub struct NeovimBackedTestContext<'a> { - cx: VimTestContext<'a>, +pub struct NeovimBackedTestContext { + cx: VimTestContext, // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which // bindings are exempted. If None, all bindings are ignored for that insertion text. exemptions: HashMap>>, @@ -60,8 +60,8 @@ pub struct NeovimBackedTestContext<'a> { is_dirty: bool, } -impl<'a> NeovimBackedTestContext<'a> { - pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> { +impl NeovimBackedTestContext { + pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { // rust stores the name of the test on the current thread. // We use this to automatically name a file that will store // the neovim connection's requests/responses so that we can @@ -393,20 +393,20 @@ impl<'a> NeovimBackedTestContext<'a> { pub fn binding( self, keystrokes: [&'static str; COUNT], - ) -> NeovimBackedBindingTestContext<'a, COUNT> { + ) -> NeovimBackedBindingTestContext { NeovimBackedBindingTestContext::new(keystrokes, self) } } -impl<'a> Deref for NeovimBackedTestContext<'a> { - type Target = VimTestContext<'a>; +impl Deref for NeovimBackedTestContext { + type Target = VimTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for NeovimBackedTestContext<'a> { +impl DerefMut for NeovimBackedTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } @@ -415,7 +415,7 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> { // a common mistake in tests is to call set_shared_state when // you mean asswert_shared_state. This notices that and lets // you know. -impl<'a> Drop for NeovimBackedTestContext<'a> { +impl Drop for NeovimBackedTestContext { fn drop(&mut self) { if self.is_dirty { panic!("Test context was dropped after set_shared_state before assert_shared_state") @@ -425,9 +425,8 @@ impl<'a> Drop for NeovimBackedTestContext<'a> { #[cfg(test)] mod test { - use gpui::TestAppContext; - use crate::test::NeovimBackedTestContext; + use gpui::TestAppContext; #[gpui::test] async fn neovim_backed_test_context_works(cx: &mut TestAppContext) { diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 21b041b2451f5dce4d930ebbc9c66705ed5a22a2..5ed5296bff44d3e76c32f2a4b768afd760d1d121 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -10,11 +10,11 @@ use search::BufferSearchBar; use crate::{state::Operator, *}; -pub struct VimTestContext<'a> { - cx: EditorLspTestContext<'a>, +pub struct VimTestContext { + cx: EditorLspTestContext, } -impl<'a> VimTestContext<'a> { +impl VimTestContext { pub fn init(cx: &mut gpui::TestAppContext) { if cx.has_global::() { dbg!("OOPS"); @@ -29,13 +29,13 @@ impl<'a> VimTestContext<'a> { }); } - pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { + pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext { Self::init(cx); let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await; Self::new_with_lsp(lsp, enabled) } - pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> { + pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { Self::init(cx); Self::new_with_lsp( EditorLspTestContext::new_typescript(Default::default(), cx).await, @@ -43,7 +43,7 @@ impl<'a> VimTestContext<'a> { ) } - pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> { + pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext { cx.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); @@ -162,15 +162,15 @@ impl<'a> VimTestContext<'a> { } } -impl<'a> Deref for VimTestContext<'a> { - type Target = EditorTestContext<'a>; +impl Deref for VimTestContext { + type Target = EditorTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for VimTestContext<'a> { +impl DerefMut for VimTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 236d96ef9fa547dd6ff35b8e2a44ee341ad3b0ba..c13a00b11c897b46ef5e2ac69ae10848c573ebf2 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -19,7 +19,6 @@ pub enum PanelEvent { ZoomOut, Activate, Close, - Focus, } pub trait Panel: FocusableView + EventEmitter { @@ -216,6 +215,28 @@ impl Dock { } }); + cx.on_focus_in(&focus_handle, { + let dock = dock.downgrade(); + move |workspace, cx| { + let Some(dock) = dock.upgrade() else { + return; + }; + let Some(panel) = dock.read(cx).active_panel() else { + return; + }; + if panel.is_zoomed(cx) { + workspace.zoomed = Some(panel.to_any().downgrade().into()); + workspace.zoomed_position = Some(position); + } else { + workspace.zoomed = None; + workspace.zoomed_position = None; + } + workspace.dismiss_zoomed_items_to_reveal(Some(position), cx); + workspace.update_active_view_for_followers(cx) + } + }) + .detach(); + cx.observe(&dock, move |workspace, dock, cx| { if dock.read(cx).is_open() { if let Some(panel) = dock.read(cx).active_panel() { @@ -394,7 +415,6 @@ impl Dock { this.set_open(false, cx); } } - PanelEvent::Focus => {} }), ]; @@ -561,6 +581,7 @@ impl Render for Dock { } div() + .track_focus(&self.focus_handle) .flex() .bg(cx.theme().colors().panel_background) .border_color(cx.theme().colors().border) @@ -584,7 +605,7 @@ impl Render for Dock { ) .child(handle) } else { - div() + div().track_focus(&self.focus_handle) } } } @@ -724,7 +745,7 @@ pub mod test { impl Render for TestPanel { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div() + div().id("test").track_focus(&self.focus_handle) } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 38b76630307faee9f015c61e04f75d9fd703f153..45f6141df2f172ccad176c48bc1f83eab6174c3e 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -442,7 +442,7 @@ impl ItemHandle for View { ) && !pending_update_scheduled.load(Ordering::SeqCst) { pending_update_scheduled.store(true, Ordering::SeqCst); - cx.on_next_frame({ + cx.defer({ let pending_update = pending_update.clone(); let pending_update_scheduled = pending_update_scheduled.clone(); move |this, cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 474747c907d3ca1404bffcfb687aa0fcff257332..6b29496f2cfd919a60e4947adb2f989fbb425486 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2457,11 +2457,11 @@ impl Workspace { Some(leader_id) } - // pub fn is_being_followed(&self, peer_id: PeerId) -> bool { - // self.follower_states - // .values() - // .any(|state| state.leader_id == peer_id) - // } + pub fn is_being_followed(&self, peer_id: PeerId) -> bool { + self.follower_states + .values() + .any(|state| state.leader_id == peer_id) + } fn active_item_path_changed(&mut self, cx: &mut ViewContext) { let active_entry = self.active_project_path(cx);