Port 1.00 following tests

Conrad Irwin and Max created

Co-Authored-By: Max <max@zed.dev>

Change summary

crates/collab/src/db/queries/rooms.rs      |    8 
crates/collab/src/tests/following_tests.rs | 1064 +++++++++++------------
crates/terminal_view/src/terminal_panel.rs |    1 
crates/workspace/src/dock.rs               |   29 
crates/workspace/src/item.rs               |    2 
5 files changed, 552 insertions(+), 552 deletions(-)

Detailed changes

crates/collab/src/db/queries/rooms.rs 🔗

@@ -855,6 +855,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,549 +1,521 @@
-//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
-//     );
-// }
+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, TestAppContext, View, VisualContext, 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(
+    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);
+        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(

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -214,7 +214,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/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)
         }
     }
 }
@@ -720,7 +741,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| {