following tests (#3902)

Max Brunsfeld created

Starting work on the following tests.

* The first failure was caused by not properly dropping the references,
which is now fixed.
* The next failure is caused by a `cx.after_window_update` being changed
to `cx.on_next_frame`. This doesn't seem to work (at least in tests),
but might also explain the flaky following behavior Max and I observed
if there's a timing bug in production too.
* * When you are following someone and they move their cursor you should
receive two proto messages: UpdateBuffer to move the cursor, and
UpdateFollowers to update where you're following. It seems like this
could be made a bit less chatty/fragile, but probably out of scope for
this.
* With that worked around, there's one more failure, but I haven't
looked into it yet. (possibly caused by an incorrect fix for the cx
one).

Change summary

crates/collab/src/db/queries/rooms.rs                     |    8 
crates/collab/src/tests/following_tests.rs                | 3667 ++++----
crates/editor/src/editor_tests.rs                         |    8 
crates/editor/src/test/editor_lsp_test_context.rs         |   24 
crates/editor/src/test/editor_test_context.rs             |   12 
crates/file_finder/src/file_finder.rs                     |    2 
crates/gpui/src/app/test_context.rs                       |   33 
crates/search/src/buffer_search.rs                        |    7 
crates/terminal_view/src/terminal_panel.rs                |    1 
crates/vim/src/test/neovim_backed_binding_test_context.rs |   21 
crates/vim/src/test/neovim_backed_test_context.rs         |   21 
crates/vim/src/test/vim_test_context.rs                   |   18 
crates/workspace/src/dock.rs                              |   29 
crates/workspace/src/item.rs                              |    2 
crates/workspace/src/workspace.rs                         |   10 
15 files changed, 1,889 insertions(+), 1,974 deletions(-)

Detailed changes

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(

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::<Editor>()
-//         .unwrap();
-//     let editor_a2 = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .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::<Editor>()
-//         .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::<Editor>()
-//             .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::<Editor>()
-//             .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::<SharedScreen>()
-//             .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::<TestPanel>(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::<TestPanel>(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<workspace::Pane>, cx: &mut TestAppContext| {
-//         pane.update(cx, |pane, cx| {
-//             pane.items()
-//                 .map(|item| {
-//                     item.project_path(cx)
-//                         .unwrap()
-//                         .path
-//                         .to_str()
-//                         .unwrap()
-//                         .to_owned()
-//                 })
-//                 .collect::<Vec<_>>()
-//         })
-//     };
-
-//     //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::<Editor>()
-//         .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::<Editor>()
-//         .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::<Editor>()
-//         .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::<Editor>()
-//             .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::<Workspace>()
-//         .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::<Workspace>()
-//         .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::<SettingsStore, _>(|store, cx| {
-//                 store.update_user_settings::<ProjectSettings>(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::<Editor>()
-//         .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::<Editor>()
-//         .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::<Editor>()
-//             .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 <b@b.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 <b@b.b>"
-//         );
-//     });
-// }
-
-// fn visible_push_notifications(
-//     cx: &mut TestAppContext,
-// ) -> Vec<gpui::View<ProjectSharedNotification>> {
-//     let mut ret = Vec::new();
-//     for window in cx.windows() {
-//         window.update(cx, |window| {
-//             if let Some(handle) = window
-//                 .root_view()
-//                 .clone()
-//                 .downcast::<ProjectSharedNotification>()
-//             {
-//                 ret.push(handle)
-//             }
-//         });
-//     }
-//     ret
-// }
-
-// #[derive(Debug, PartialEq, Eq)]
-// struct PaneSummary {
-//     active: bool,
-//     leader: Option<PeerId>,
-//     items: Vec<(bool, String)>,
-// }
-
-// fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
-//     cx.read(|cx| {
-//         let active_call = ActiveCall::global(cx).read(cx);
-//         let peer_id = active_call.client().peer_id();
-//         let room = active_call.room().unwrap().read(cx);
-//         let mut result = room
-//             .remote_participants()
-//             .values()
-//             .map(|participant| participant.peer_id)
-//             .chain(peer_id)
-//             .filter_map(|peer_id| {
-//                 let followers = room.followers_for(peer_id, project_id);
-//                 if followers.is_empty() {
-//                     None
-//                 } else {
-//                     Some((peer_id, followers.to_vec()))
-//                 }
-//             })
-//             .collect::<Vec<_>>();
-//         result.sort_by_key(|e| e.0);
-//         result
-//     })
-// }
-
-// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
-//     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::<Editor>()
+        .unwrap();
+    let editor_a2 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .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::<Editor>()
+        .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::<Editor>()
+            .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::<Editor>()
+            .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::<SharedScreen>()
+            .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::<TestPanel>(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::<TestPanel>(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<workspace::Pane>, cx: &mut VisualTestContext| {
+        pane.update(cx, |pane, cx| {
+            pane.items()
+                .map(|item| {
+                    item.project_path(cx)
+                        .unwrap()
+                        .path
+                        .to_str()
+                        .unwrap()
+                        .to_owned()
+                })
+                .collect::<Vec<_>>()
+        })
+    };
+
+    //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::<Editor>()
+        .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::<Editor>()
+        .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::<Editor>()
+        .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::<Editor>()
+            .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::<Workspace>()
+        .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::<Workspace>()
+        .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::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<ProjectSettings>(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::<Editor>()
+        .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::<Editor>()
+        .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::<Editor>()
+            .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 <b@b.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 <b@b.b>"
+        );
+    });
+}
+
+fn visible_push_notifications(
+    cx: &mut TestAppContext,
+) -> Vec<gpui::View<ProjectSharedNotification>> {
+    let mut ret = Vec::new();
+    for window in cx.windows() {
+        window
+            .update(cx, |window, _| {
+                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
+                    ret.push(handle)
+                }
+            })
+            .unwrap();
+    }
+    ret
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+    active: bool,
+    leader: Option<PeerId>,
+    items: Vec<(bool, String)>,
+}
+
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+    cx.read(|cx| {
+        let active_call = ActiveCall::global(cx).read(cx);
+        let peer_id = active_call.client().peer_id();
+        let room = active_call.room().unwrap().read(cx);
+        let mut result = room
+            .remote_participants()
+            .values()
+            .map(|participant| participant.peer_id)
+            .chain(peer_id)
+            .filter_map(|peer_id| {
+                let followers = room.followers_for(peer_id, project_id);
+                if followers.is_empty() {
+                    None
+                } else {
+                    Some((peer_id, followers.to_vec()))
+                }
+            })
+            .collect::<Vec<_>>();
+        result.sort_by_key(|e| e.0);
+        result
+    })
+}
+
+fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
+    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()
+    })
+}

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<Output = ()> {
@@ -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<Vec<(&'static str, &'static str)>>,
 ) -> impl Future<Output = ()> {
     let edits = edits.map(|edits| {

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<Workspace>,
     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<char> = 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
     }

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<Editor>,
     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
     }

crates/file_finder/src/file_finder.rs 🔗

@@ -1843,7 +1843,7 @@ mod tests {
         expected_matches: usize,
         expected_editor_title: &str,
         workspace: &View<Workspace>,
-        cx: &mut gpui::VisualTestContext<'_>,
+        cx: &mut gpui::VisualTestContext,
     ) -> Vec<FoundPath> {
         let picker = open_file_picker(&workspace, cx);
         cx.simulate_input(input);

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<AnyWindowHandle> {
+        self.app.borrow().windows().clone()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -479,21 +483,24 @@ impl<V> View<V> {
 }
 
 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<R>(&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<T> = <TestAppContext as Context>::Result<T>;
 
     fn new_model<T: 'static>(
@@ -578,7 +585,7 @@ impl<'a> Context for VisualTestContext<'a> {
     }
 }
 
-impl<'a> VisualContext for VisualTestContext<'a> {
+impl VisualContext for VisualTestContext {
     fn new_view<V>(
         &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<R> {
         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<V: crate::FocusableView>(&mut self, view: &View<V>) -> 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()

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<Editor>,
-        View<BufferSearchBar>,
-        &mut VisualTestContext<'_>,
-    ) {
+    ) -> (View<Editor>, View<BufferSearchBar>, &mut VisualTestContext) {
         init_globals(cx);
         let buffer = cx.new_model(|cx| {
             Buffer::new(

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

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<const COUNT: usize> {
+    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<const COUNT: usize> NeovimBackedBindingTestContext<COUNT> {
+    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<const NEW_COUNT: usize>(
         self,
         keystrokes: [&'static str; NEW_COUNT],
-    ) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
+    ) -> NeovimBackedBindingTestContext<NEW_COUNT> {
         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<const COUNT: usize> Deref for NeovimBackedBindingTestContext<COUNT> {
+    type Target = NeovimBackedTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx
     }
 }
 
-impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
+impl<const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<COUNT> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.cx
     }

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<String, Option<HashSet<String>>>,
@@ -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<const COUNT: usize>(
         self,
         keystrokes: [&'static str; COUNT],
-    ) -> NeovimBackedBindingTestContext<'a, COUNT> {
+    ) -> NeovimBackedBindingTestContext<COUNT> {
         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) {

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::<Vim>() {
             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::<VimModeSetting>(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
     }

crates/workspace/src/dock.rs 🔗

@@ -19,7 +19,6 @@ pub enum PanelEvent {
     ZoomOut,
     Activate,
     Close,
-    Focus,
 }
 
 pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
@@ -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<Self>) -> impl IntoElement {
-            div()
+            div().id("test").track_focus(&self.focus_handle)
         }
     }
 

crates/workspace/src/item.rs 🔗

@@ -442,7 +442,7 @@ impl<T: Item> ItemHandle for View<T> {
                         ) && !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| {

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<Self>) {
         let active_entry = self.active_project_path(cx);