following_tests.rs

   1#![allow(clippy::reversed_empty_ranges)]
   2use crate::tests::TestServer;
   3use call::{ActiveCall, ParticipantLocation};
   4use client::ChannelId;
   5use collab_ui::{
   6    channel_view::ChannelView,
   7    notifications::project_shared_notification::ProjectSharedNotification,
   8};
   9use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
  10use gpui::{
  11    AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
  12    VisualContext, VisualTestContext, point,
  13};
  14use language::Capability;
  15use project::WorktreeSettings;
  16use rpc::proto::PeerId;
  17use serde_json::json;
  18use settings::SettingsStore;
  19use text::{Point, ToPoint};
  20use util::{path, test::sample_text};
  21use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
  22
  23use super::TestClient;
  24
  25#[gpui::test(iterations = 10)]
  26async fn test_basic_following(
  27    cx_a: &mut TestAppContext,
  28    cx_b: &mut TestAppContext,
  29    cx_c: &mut TestAppContext,
  30    cx_d: &mut TestAppContext,
  31) {
  32    let executor = cx_a.executor();
  33    let mut server = TestServer::start(executor.clone()).await;
  34    let client_a = server.create_client(cx_a, "user_a").await;
  35    let client_b = server.create_client(cx_b, "user_b").await;
  36    let client_c = server.create_client(cx_c, "user_c").await;
  37    let client_d = server.create_client(cx_d, "user_d").await;
  38    server
  39        .create_room(&mut [
  40            (&client_a, cx_a),
  41            (&client_b, cx_b),
  42            (&client_c, cx_c),
  43            (&client_d, cx_d),
  44        ])
  45        .await;
  46    let active_call_a = cx_a.read(ActiveCall::global);
  47    let active_call_b = cx_b.read(ActiveCall::global);
  48
  49    cx_a.update(editor::init);
  50    cx_b.update(editor::init);
  51
  52    client_a
  53        .fs()
  54        .insert_tree(
  55            path!("/a"),
  56            json!({
  57                "1.txt": "one\none\none",
  58                "2.txt": "two\ntwo\ntwo",
  59                "3.txt": "three\nthree\nthree",
  60            }),
  61        )
  62        .await;
  63    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
  64    active_call_a
  65        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
  66        .await
  67        .unwrap();
  68
  69    let project_id = active_call_a
  70        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  71        .await
  72        .unwrap();
  73    let project_b = client_b.join_remote_project(project_id, cx_b).await;
  74    active_call_b
  75        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
  76        .await
  77        .unwrap();
  78
  79    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
  80    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
  81
  82    cx_b.update(|window, _| {
  83        assert!(window.is_window_active());
  84    });
  85
  86    // Client A opens some editors.
  87    let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
  88    let editor_a1 = workspace_a
  89        .update_in(cx_a, |workspace, window, cx| {
  90            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
  91        })
  92        .await
  93        .unwrap()
  94        .downcast::<Editor>()
  95        .unwrap();
  96    let editor_a2 = workspace_a
  97        .update_in(cx_a, |workspace, window, cx| {
  98            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
  99        })
 100        .await
 101        .unwrap()
 102        .downcast::<Editor>()
 103        .unwrap();
 104
 105    // Client B opens an editor.
 106    let editor_b1 = workspace_b
 107        .update_in(cx_b, |workspace, window, cx| {
 108            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
 109        })
 110        .await
 111        .unwrap()
 112        .downcast::<Editor>()
 113        .unwrap();
 114
 115    let peer_id_a = client_a.peer_id().unwrap();
 116    let peer_id_b = client_b.peer_id().unwrap();
 117    let peer_id_c = client_c.peer_id().unwrap();
 118    let peer_id_d = client_d.peer_id().unwrap();
 119
 120    // Client A updates their selections in those editors
 121    editor_a1.update_in(cx_a, |editor, window, cx| {
 122        editor.handle_input("a", window, cx);
 123        editor.handle_input("b", window, cx);
 124        editor.handle_input("c", window, cx);
 125        editor.select_left(&Default::default(), window, cx);
 126        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
 127    });
 128    editor_a2.update_in(cx_a, |editor, window, cx| {
 129        editor.handle_input("d", window, cx);
 130        editor.handle_input("e", window, cx);
 131        editor.select_left(&Default::default(), window, cx);
 132        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
 133    });
 134
 135    // When client B starts following client A, only the active view state is replicated to client B.
 136    workspace_b.update_in(cx_b, |workspace, window, cx| {
 137        workspace.follow(peer_id_a, window, cx)
 138    });
 139
 140    cx_c.executor().run_until_parked();
 141    let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
 142        workspace
 143            .active_item(cx)
 144            .unwrap()
 145            .downcast::<Editor>()
 146            .unwrap()
 147    });
 148    assert_eq!(
 149        cx_b.read(|cx| editor_b2.project_path(cx)),
 150        Some((worktree_id, "2.txt").into())
 151    );
 152    assert_eq!(
 153        editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
 154        vec![2..1]
 155    );
 156    assert_eq!(
 157        editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
 158        vec![3..3]
 159    );
 160
 161    executor.run_until_parked();
 162    let active_call_c = cx_c.read(ActiveCall::global);
 163    let project_c = client_c.join_remote_project(project_id, cx_c).await;
 164    let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
 165    active_call_c
 166        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
 167        .await
 168        .unwrap();
 169    drop(project_c);
 170
 171    // Client C also follows client A.
 172    workspace_c.update_in(cx_c, |workspace, window, cx| {
 173        workspace.follow(peer_id_a, window, cx)
 174    });
 175
 176    cx_d.executor().run_until_parked();
 177    let active_call_d = cx_d.read(ActiveCall::global);
 178    let project_d = client_d.join_remote_project(project_id, cx_d).await;
 179    let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
 180    active_call_d
 181        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
 182        .await
 183        .unwrap();
 184    drop(project_d);
 185
 186    // All clients see that clients B and C are following client A.
 187    cx_c.executor().run_until_parked();
 188    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 189        assert_eq!(
 190            followers_by_leader(project_id, cx),
 191            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
 192            "followers seen by {name}"
 193        );
 194    }
 195
 196    // Client C unfollows client A.
 197    workspace_c.update_in(cx_c, |workspace, window, cx| {
 198        workspace.unfollow(peer_id_a, window, cx).unwrap();
 199    });
 200
 201    // All clients see that clients B is following client A.
 202    cx_c.executor().run_until_parked();
 203    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 204        assert_eq!(
 205            followers_by_leader(project_id, cx),
 206            &[(peer_id_a, vec![peer_id_b])],
 207            "followers seen by {name}"
 208        );
 209    }
 210
 211    // Client C re-follows client A.
 212    workspace_c.update_in(cx_c, |workspace, window, cx| {
 213        workspace.follow(peer_id_a, window, cx)
 214    });
 215
 216    // All clients see that clients B and C are following client A.
 217    cx_c.executor().run_until_parked();
 218    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 219        assert_eq!(
 220            followers_by_leader(project_id, cx),
 221            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
 222            "followers seen by {name}"
 223        );
 224    }
 225
 226    // Client D follows client B, then switches to following client C.
 227    workspace_d.update_in(cx_d, |workspace, window, cx| {
 228        workspace.follow(peer_id_b, window, cx)
 229    });
 230    cx_a.executor().run_until_parked();
 231    workspace_d.update_in(cx_d, |workspace, window, cx| {
 232        workspace.follow(peer_id_c, window, cx)
 233    });
 234
 235    // All clients see that D is following C
 236    cx_a.executor().run_until_parked();
 237    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 238        assert_eq!(
 239            followers_by_leader(project_id, cx),
 240            &[
 241                (peer_id_a, vec![peer_id_b, peer_id_c]),
 242                (peer_id_c, vec![peer_id_d])
 243            ],
 244            "followers seen by {name}"
 245        );
 246    }
 247
 248    // Client C closes the project.
 249    let weak_workspace_c = workspace_c.downgrade();
 250    workspace_c.update_in(cx_c, |workspace, window, cx| {
 251        workspace.close_window(&Default::default(), window, cx);
 252    });
 253    executor.run_until_parked();
 254    // are you sure you want to leave the call?
 255    cx_c.simulate_prompt_answer("Close window and hang up");
 256    cx_c.cx.update(|_| {
 257        drop(workspace_c);
 258    });
 259    executor.run_until_parked();
 260    cx_c.cx.update(|_| {});
 261
 262    weak_workspace_c.assert_released();
 263
 264    // Clients A and B see that client B is following A, and client C is not present in the followers.
 265    executor.run_until_parked();
 266    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
 267        assert_eq!(
 268            followers_by_leader(project_id, cx),
 269            &[(peer_id_a, vec![peer_id_b]),],
 270            "followers seen by {name}"
 271        );
 272    }
 273
 274    // When client A activates a different editor, client B does so as well.
 275    workspace_a.update_in(cx_a, |workspace, window, cx| {
 276        workspace.activate_item(&editor_a1, true, true, window, cx)
 277    });
 278    executor.run_until_parked();
 279    workspace_b.update(cx_b, |workspace, cx| {
 280        assert_eq!(
 281            workspace.active_item(cx).unwrap().item_id(),
 282            editor_b1.item_id()
 283        );
 284    });
 285
 286    // When client A opens a multibuffer, client B does so as well.
 287    let multibuffer_a = cx_a.new(|cx| {
 288        let buffer_a1 = project_a.update(cx, |project, cx| {
 289            project
 290                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
 291                .unwrap()
 292        });
 293        let buffer_a2 = project_a.update(cx, |project, cx| {
 294            project
 295                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
 296                .unwrap()
 297        });
 298        let mut result = MultiBuffer::new(Capability::ReadWrite);
 299        result.set_excerpts_for_path(
 300            PathKey::for_buffer(&buffer_a1, cx),
 301            buffer_a1,
 302            [Point::row_range(1..2)],
 303            1,
 304            cx,
 305        );
 306        result.set_excerpts_for_path(
 307            PathKey::for_buffer(&buffer_a2, cx),
 308            buffer_a2,
 309            [Point::row_range(5..6)],
 310            1,
 311            cx,
 312        );
 313        result
 314    });
 315    let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
 316        let editor = cx
 317            .new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
 318        workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
 319        editor
 320    });
 321    executor.run_until_parked();
 322    let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
 323        workspace
 324            .active_item(cx)
 325            .unwrap()
 326            .downcast::<Editor>()
 327            .unwrap()
 328    });
 329    assert_eq!(
 330        multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
 331        multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
 332    );
 333
 334    // When client A navigates back and forth, client B does so as well.
 335    workspace_a
 336        .update_in(cx_a, |workspace, window, cx| {
 337            workspace.go_back(workspace.active_pane().downgrade(), window, cx)
 338        })
 339        .await
 340        .unwrap();
 341    executor.run_until_parked();
 342    workspace_b.update(cx_b, |workspace, cx| {
 343        assert_eq!(
 344            workspace.active_item(cx).unwrap().item_id(),
 345            editor_b1.item_id()
 346        );
 347    });
 348
 349    workspace_a
 350        .update_in(cx_a, |workspace, window, cx| {
 351            workspace.go_back(workspace.active_pane().downgrade(), window, cx)
 352        })
 353        .await
 354        .unwrap();
 355    executor.run_until_parked();
 356    workspace_b.update(cx_b, |workspace, cx| {
 357        assert_eq!(
 358            workspace.active_item(cx).unwrap().item_id(),
 359            editor_b2.item_id()
 360        );
 361    });
 362
 363    workspace_a
 364        .update_in(cx_a, |workspace, window, cx| {
 365            workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
 366        })
 367        .await
 368        .unwrap();
 369    executor.run_until_parked();
 370    workspace_b.update(cx_b, |workspace, cx| {
 371        assert_eq!(
 372            workspace.active_item(cx).unwrap().item_id(),
 373            editor_b1.item_id()
 374        );
 375    });
 376
 377    // Changes to client A's editor are reflected on client B.
 378    editor_a1.update_in(cx_a, |editor, window, cx| {
 379        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 380            s.select_ranges([1..1, 2..2])
 381        });
 382    });
 383    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
 384    executor.run_until_parked();
 385    cx_b.background_executor.run_until_parked();
 386
 387    editor_b1.update(cx_b, |editor, cx| {
 388        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
 389    });
 390
 391    editor_a1.update_in(cx_a, |editor, window, cx| {
 392        editor.set_text("TWO", window, cx)
 393    });
 394    executor.run_until_parked();
 395    editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
 396
 397    editor_a1.update_in(cx_a, |editor, window, cx| {
 398        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 399            s.select_ranges([3..3])
 400        });
 401        editor.set_scroll_position(point(0., 100.), window, cx);
 402    });
 403    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
 404    executor.run_until_parked();
 405    editor_b1.update(cx_b, |editor, cx| {
 406        assert_eq!(editor.selections.ranges(cx), &[3..3]);
 407    });
 408
 409    // After unfollowing, client B stops receiving updates from client A.
 410    workspace_b.update_in(cx_b, |workspace, window, cx| {
 411        workspace.unfollow(peer_id_a, window, cx).unwrap()
 412    });
 413    workspace_a.update_in(cx_a, |workspace, window, cx| {
 414        workspace.activate_item(&editor_a2, true, true, window, cx)
 415    });
 416    executor.run_until_parked();
 417    assert_eq!(
 418        workspace_b.update(cx_b, |workspace, cx| workspace
 419            .active_item(cx)
 420            .unwrap()
 421            .item_id()),
 422        editor_b1.item_id()
 423    );
 424
 425    // Client A starts following client B.
 426    workspace_a.update_in(cx_a, |workspace, window, cx| {
 427        workspace.follow(peer_id_b, window, cx)
 428    });
 429    executor.run_until_parked();
 430    assert_eq!(
 431        workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 432        Some(peer_id_b.into())
 433    );
 434    assert_eq!(
 435        workspace_a.update_in(cx_a, |workspace, _, cx| workspace
 436            .active_item(cx)
 437            .unwrap()
 438            .item_id()),
 439        editor_a1.item_id()
 440    );
 441
 442    // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
 443    {
 444        use crate::rpc::RECONNECT_TIMEOUT;
 445        use gpui::TestScreenCaptureSource;
 446        use workspace::{
 447            dock::{DockPosition, test::TestPanel},
 448            item::test::TestItem,
 449            shared_screen::SharedScreen,
 450        };
 451
 452        // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
 453        let display = TestScreenCaptureSource::new();
 454        active_call_b
 455            .update(cx_b, |call, cx| call.set_location(None, cx))
 456            .await
 457            .unwrap();
 458        cx_b.set_screen_capture_sources(vec![display]);
 459        let source = cx_b
 460            .read(|cx| cx.screen_capture_sources())
 461            .await
 462            .unwrap()
 463            .unwrap()
 464            .into_iter()
 465            .next()
 466            .unwrap();
 467        active_call_b
 468            .update(cx_b, |call, cx| {
 469                call.room()
 470                    .unwrap()
 471                    .update(cx, |room, cx| room.share_screen(source, cx))
 472            })
 473            .await
 474            .unwrap();
 475        executor.run_until_parked();
 476
 477        let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
 478            workspace
 479                .active_item(cx)
 480                .expect("no active item")
 481                .downcast::<SharedScreen>()
 482                .expect("active item isn't a shared screen")
 483        });
 484
 485        // Client B activates Zed again, which causes the previous editor to become focused again.
 486        active_call_b
 487            .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 488            .await
 489            .unwrap();
 490        executor.run_until_parked();
 491        workspace_a.update(cx_a, |workspace, cx| {
 492            assert_eq!(
 493                workspace.active_item(cx).unwrap().item_id(),
 494                editor_a1.item_id()
 495            )
 496        });
 497
 498        // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
 499        workspace_b.update_in(cx_b, |workspace, window, cx| {
 500            workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
 501        });
 502        executor.run_until_parked();
 503        workspace_a.update(cx_a, |workspace, cx| {
 504            assert_eq!(
 505                workspace.active_item(cx).unwrap().item_id(),
 506                multibuffer_editor_a.item_id()
 507            )
 508        });
 509
 510        // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
 511        let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
 512        workspace_b.update_in(cx_b, |workspace, window, cx| {
 513            workspace.add_panel(panel, window, cx);
 514            workspace.toggle_panel_focus::<TestPanel>(window, cx);
 515        });
 516        executor.run_until_parked();
 517        assert_eq!(
 518            workspace_a.update(cx_a, |workspace, cx| workspace
 519                .active_item(cx)
 520                .unwrap()
 521                .item_id()),
 522            shared_screen.item_id()
 523        );
 524
 525        // Toggling the focus back to the pane causes client A to return to the multibuffer.
 526        workspace_b.update_in(cx_b, |workspace, window, cx| {
 527            workspace.toggle_panel_focus::<TestPanel>(window, cx);
 528        });
 529        executor.run_until_parked();
 530        workspace_a.update(cx_a, |workspace, cx| {
 531            assert_eq!(
 532                workspace.active_item(cx).unwrap().item_id(),
 533                multibuffer_editor_a.item_id()
 534            )
 535        });
 536
 537        // Client B activates an item that doesn't implement following,
 538        // so the previously-opened screen-sharing item gets activated.
 539        let unfollowable_item = cx_b.new(TestItem::new);
 540        workspace_b.update_in(cx_b, |workspace, window, cx| {
 541            workspace.active_pane().update(cx, |pane, cx| {
 542                pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
 543            })
 544        });
 545        executor.run_until_parked();
 546        assert_eq!(
 547            workspace_a.update(cx_a, |workspace, cx| workspace
 548                .active_item(cx)
 549                .unwrap()
 550                .item_id()),
 551            shared_screen.item_id()
 552        );
 553
 554        // Following interrupts when client B disconnects.
 555        client_b.disconnect(&cx_b.to_async());
 556        executor.advance_clock(RECONNECT_TIMEOUT);
 557        assert_eq!(
 558            workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 559            None
 560        );
 561    }
 562}
 563
 564#[gpui::test]
 565async fn test_following_tab_order(
 566    executor: BackgroundExecutor,
 567    cx_a: &mut TestAppContext,
 568    cx_b: &mut TestAppContext,
 569) {
 570    let mut server = TestServer::start(executor.clone()).await;
 571    let client_a = server.create_client(cx_a, "user_a").await;
 572    let client_b = server.create_client(cx_b, "user_b").await;
 573    server
 574        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 575        .await;
 576    let active_call_a = cx_a.read(ActiveCall::global);
 577    let active_call_b = cx_b.read(ActiveCall::global);
 578
 579    cx_a.update(editor::init);
 580    cx_b.update(editor::init);
 581
 582    client_a
 583        .fs()
 584        .insert_tree(
 585            path!("/a"),
 586            json!({
 587                "1.txt": "one",
 588                "2.txt": "two",
 589                "3.txt": "three",
 590            }),
 591        )
 592        .await;
 593    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
 594    active_call_a
 595        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
 596        .await
 597        .unwrap();
 598
 599    let project_id = active_call_a
 600        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 601        .await
 602        .unwrap();
 603    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 604    active_call_b
 605        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 606        .await
 607        .unwrap();
 608
 609    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
 610    let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
 611
 612    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 613    let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
 614
 615    let client_b_id = project_a.update(cx_a, |project, _| {
 616        project.collaborators().values().next().unwrap().peer_id
 617    });
 618
 619    //Open 1, 3 in that order on client A
 620    workspace_a
 621        .update_in(cx_a, |workspace, window, cx| {
 622            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
 623        })
 624        .await
 625        .unwrap();
 626    workspace_a
 627        .update_in(cx_a, |workspace, window, cx| {
 628            workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
 629        })
 630        .await
 631        .unwrap();
 632
 633    let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
 634        pane.update(cx, |pane, cx| {
 635            pane.items()
 636                .map(|item| {
 637                    item.project_path(cx)
 638                        .unwrap()
 639                        .path
 640                        .to_str()
 641                        .unwrap()
 642                        .to_owned()
 643                })
 644                .collect::<Vec<_>>()
 645        })
 646    };
 647
 648    //Verify that the tabs opened in the order we expect
 649    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
 650
 651    //Follow client B as client A
 652    workspace_a.update_in(cx_a, |workspace, window, cx| {
 653        workspace.follow(client_b_id, window, cx)
 654    });
 655    executor.run_until_parked();
 656
 657    //Open just 2 on client B
 658    workspace_b
 659        .update_in(cx_b, |workspace, window, cx| {
 660            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
 661        })
 662        .await
 663        .unwrap();
 664    executor.run_until_parked();
 665
 666    // Verify that newly opened followed file is at the end
 667    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
 668
 669    //Open just 1 on client B
 670    workspace_b
 671        .update_in(cx_b, |workspace, window, cx| {
 672            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
 673        })
 674        .await
 675        .unwrap();
 676    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
 677    executor.run_until_parked();
 678
 679    // Verify that following into 1 did not reorder
 680    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
 681}
 682
 683#[gpui::test(iterations = 10)]
 684async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 685    let executor = cx_a.executor();
 686    let mut server = TestServer::start(executor.clone()).await;
 687    let client_a = server.create_client(cx_a, "user_a").await;
 688    let client_b = server.create_client(cx_b, "user_b").await;
 689    server
 690        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 691        .await;
 692    let active_call_a = cx_a.read(ActiveCall::global);
 693    let active_call_b = cx_b.read(ActiveCall::global);
 694
 695    cx_a.update(editor::init);
 696    cx_b.update(editor::init);
 697
 698    // Client A shares a project.
 699    client_a
 700        .fs()
 701        .insert_tree(
 702            path!("/a"),
 703            json!({
 704                "1.txt": "one",
 705                "2.txt": "two",
 706                "3.txt": "three",
 707                "4.txt": "four",
 708            }),
 709        )
 710        .await;
 711    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
 712    active_call_a
 713        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
 714        .await
 715        .unwrap();
 716    let project_id = active_call_a
 717        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 718        .await
 719        .unwrap();
 720
 721    // Client B joins the project.
 722    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 723    active_call_b
 724        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 725        .await
 726        .unwrap();
 727
 728    // Client A opens a file.
 729    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
 730    workspace_a
 731        .update_in(cx_a, |workspace, window, cx| {
 732            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
 733        })
 734        .await
 735        .unwrap()
 736        .downcast::<Editor>()
 737        .unwrap();
 738
 739    // Client B opens a different file.
 740    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 741    workspace_b
 742        .update_in(cx_b, |workspace, window, cx| {
 743            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
 744        })
 745        .await
 746        .unwrap()
 747        .downcast::<Editor>()
 748        .unwrap();
 749
 750    // Clients A and B follow each other in split panes
 751    workspace_a.update_in(cx_a, |workspace, window, cx| {
 752        workspace.split_and_clone(
 753            workspace.active_pane().clone(),
 754            SplitDirection::Right,
 755            window,
 756            cx,
 757        );
 758    });
 759    workspace_a.update_in(cx_a, |workspace, window, cx| {
 760        workspace.follow(client_b.peer_id().unwrap(), window, cx)
 761    });
 762    executor.run_until_parked();
 763    workspace_b.update_in(cx_b, |workspace, window, cx| {
 764        workspace.split_and_clone(
 765            workspace.active_pane().clone(),
 766            SplitDirection::Right,
 767            window,
 768            cx,
 769        );
 770    });
 771    workspace_b.update_in(cx_b, |workspace, window, cx| {
 772        workspace.follow(client_a.peer_id().unwrap(), window, cx)
 773    });
 774    executor.run_until_parked();
 775
 776    // Clients A and B return focus to the original files they had open
 777    workspace_a.update_in(cx_a, |workspace, window, cx| {
 778        workspace.activate_next_pane(window, cx)
 779    });
 780    workspace_b.update_in(cx_b, |workspace, window, cx| {
 781        workspace.activate_next_pane(window, cx)
 782    });
 783    executor.run_until_parked();
 784
 785    // Both clients see the other client's focused file in their right pane.
 786    assert_eq!(
 787        pane_summaries(&workspace_a, cx_a),
 788        &[
 789            PaneSummary {
 790                active: true,
 791                leader: None,
 792                items: vec![(true, "1.txt".into())]
 793            },
 794            PaneSummary {
 795                active: false,
 796                leader: client_b.peer_id(),
 797                items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
 798            },
 799        ]
 800    );
 801    assert_eq!(
 802        pane_summaries(&workspace_b, cx_b),
 803        &[
 804            PaneSummary {
 805                active: true,
 806                leader: None,
 807                items: vec![(true, "2.txt".into())]
 808            },
 809            PaneSummary {
 810                active: false,
 811                leader: client_a.peer_id(),
 812                items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
 813            },
 814        ]
 815    );
 816
 817    // Clients A and B each open a new file.
 818    workspace_a
 819        .update_in(cx_a, |workspace, window, cx| {
 820            workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
 821        })
 822        .await
 823        .unwrap();
 824
 825    workspace_b
 826        .update_in(cx_b, |workspace, window, cx| {
 827            workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
 828        })
 829        .await
 830        .unwrap();
 831    executor.run_until_parked();
 832
 833    // Both client's see the other client open the new file, but keep their
 834    // focus on their own active pane.
 835    assert_eq!(
 836        pane_summaries(&workspace_a, cx_a),
 837        &[
 838            PaneSummary {
 839                active: true,
 840                leader: None,
 841                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
 842            },
 843            PaneSummary {
 844                active: false,
 845                leader: client_b.peer_id(),
 846                items: vec![
 847                    (false, "1.txt".into()),
 848                    (false, "2.txt".into()),
 849                    (true, "4.txt".into())
 850                ]
 851            },
 852        ]
 853    );
 854    assert_eq!(
 855        pane_summaries(&workspace_b, cx_b),
 856        &[
 857            PaneSummary {
 858                active: true,
 859                leader: None,
 860                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
 861            },
 862            PaneSummary {
 863                active: false,
 864                leader: client_a.peer_id(),
 865                items: vec![
 866                    (false, "2.txt".into()),
 867                    (false, "1.txt".into()),
 868                    (true, "3.txt".into())
 869                ]
 870            },
 871        ]
 872    );
 873
 874    // Client A focuses their right pane, in which they're following client B.
 875    workspace_a.update_in(cx_a, |workspace, window, cx| {
 876        workspace.activate_next_pane(window, cx)
 877    });
 878    executor.run_until_parked();
 879
 880    // Client B sees that client A is now looking at the same file as them.
 881    assert_eq!(
 882        pane_summaries(&workspace_a, cx_a),
 883        &[
 884            PaneSummary {
 885                active: false,
 886                leader: None,
 887                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
 888            },
 889            PaneSummary {
 890                active: true,
 891                leader: client_b.peer_id(),
 892                items: vec![
 893                    (false, "1.txt".into()),
 894                    (false, "2.txt".into()),
 895                    (true, "4.txt".into())
 896                ]
 897            },
 898        ]
 899    );
 900    assert_eq!(
 901        pane_summaries(&workspace_b, cx_b),
 902        &[
 903            PaneSummary {
 904                active: true,
 905                leader: None,
 906                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
 907            },
 908            PaneSummary {
 909                active: false,
 910                leader: client_a.peer_id(),
 911                items: vec![
 912                    (false, "2.txt".into()),
 913                    (false, "1.txt".into()),
 914                    (false, "3.txt".into()),
 915                    (true, "4.txt".into())
 916                ]
 917            },
 918        ]
 919    );
 920
 921    // Client B focuses their right pane, in which they're following client A,
 922    // who is following them.
 923    workspace_b.update_in(cx_b, |workspace, window, cx| {
 924        workspace.activate_next_pane(window, cx)
 925    });
 926    executor.run_until_parked();
 927
 928    // Client A sees that client B is now looking at the same file as them.
 929    assert_eq!(
 930        pane_summaries(&workspace_b, cx_b),
 931        &[
 932            PaneSummary {
 933                active: false,
 934                leader: None,
 935                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
 936            },
 937            PaneSummary {
 938                active: true,
 939                leader: client_a.peer_id(),
 940                items: vec![
 941                    (false, "2.txt".into()),
 942                    (false, "1.txt".into()),
 943                    (false, "3.txt".into()),
 944                    (true, "4.txt".into())
 945                ]
 946            },
 947        ]
 948    );
 949    assert_eq!(
 950        pane_summaries(&workspace_a, cx_a),
 951        &[
 952            PaneSummary {
 953                active: false,
 954                leader: None,
 955                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
 956            },
 957            PaneSummary {
 958                active: true,
 959                leader: client_b.peer_id(),
 960                items: vec![
 961                    (false, "1.txt".into()),
 962                    (false, "2.txt".into()),
 963                    (true, "4.txt".into())
 964                ]
 965            },
 966        ]
 967    );
 968
 969    // Client B focuses a file that they previously followed A to, breaking
 970    // the follow.
 971    workspace_b.update_in(cx_b, |workspace, window, cx| {
 972        workspace.active_pane().update(cx, |pane, cx| {
 973            pane.activate_previous_item(&Default::default(), window, cx);
 974        });
 975    });
 976    executor.run_until_parked();
 977
 978    // Both clients see that client B is looking at that previous file.
 979    assert_eq!(
 980        pane_summaries(&workspace_b, cx_b),
 981        &[
 982            PaneSummary {
 983                active: false,
 984                leader: None,
 985                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
 986            },
 987            PaneSummary {
 988                active: true,
 989                leader: None,
 990                items: vec![
 991                    (false, "2.txt".into()),
 992                    (false, "1.txt".into()),
 993                    (true, "3.txt".into()),
 994                    (false, "4.txt".into())
 995                ]
 996            },
 997        ]
 998    );
 999    assert_eq!(
1000        pane_summaries(&workspace_a, cx_a),
1001        &[
1002            PaneSummary {
1003                active: false,
1004                leader: None,
1005                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1006            },
1007            PaneSummary {
1008                active: true,
1009                leader: client_b.peer_id(),
1010                items: vec![
1011                    (false, "1.txt".into()),
1012                    (false, "2.txt".into()),
1013                    (false, "4.txt".into()),
1014                    (true, "3.txt".into()),
1015                ]
1016            },
1017        ]
1018    );
1019
1020    // Client B closes tabs, some of which were originally opened by client A,
1021    // and some of which were originally opened by client B.
1022    workspace_b.update_in(cx_b, |workspace, window, cx| {
1023        workspace.active_pane().update(cx, |pane, cx| {
1024            pane.close_other_items(&Default::default(), None, window, cx)
1025                .detach();
1026        });
1027    });
1028
1029    executor.run_until_parked();
1030
1031    // Both clients see that Client B is looking at the previous tab.
1032    assert_eq!(
1033        pane_summaries(&workspace_b, cx_b),
1034        &[
1035            PaneSummary {
1036                active: false,
1037                leader: None,
1038                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1039            },
1040            PaneSummary {
1041                active: true,
1042                leader: None,
1043                items: vec![(true, "3.txt".into()),]
1044            },
1045        ]
1046    );
1047    assert_eq!(
1048        pane_summaries(&workspace_a, cx_a),
1049        &[
1050            PaneSummary {
1051                active: false,
1052                leader: None,
1053                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1054            },
1055            PaneSummary {
1056                active: true,
1057                leader: client_b.peer_id(),
1058                items: vec![
1059                    (false, "1.txt".into()),
1060                    (false, "2.txt".into()),
1061                    (false, "4.txt".into()),
1062                    (true, "3.txt".into()),
1063                ]
1064            },
1065        ]
1066    );
1067
1068    // Client B follows client A again.
1069    workspace_b.update_in(cx_b, |workspace, window, cx| {
1070        workspace.follow(client_a.peer_id().unwrap(), window, cx)
1071    });
1072    executor.run_until_parked();
1073    // Client A cycles through some tabs.
1074    workspace_a.update_in(cx_a, |workspace, window, cx| {
1075        workspace.active_pane().update(cx, |pane, cx| {
1076            pane.activate_previous_item(&Default::default(), window, cx);
1077        });
1078    });
1079    executor.run_until_parked();
1080
1081    // Client B follows client A into those tabs.
1082    assert_eq!(
1083        pane_summaries(&workspace_a, cx_a),
1084        &[
1085            PaneSummary {
1086                active: false,
1087                leader: None,
1088                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1089            },
1090            PaneSummary {
1091                active: true,
1092                leader: None,
1093                items: vec![
1094                    (false, "1.txt".into()),
1095                    (false, "2.txt".into()),
1096                    (true, "4.txt".into()),
1097                    (false, "3.txt".into()),
1098                ]
1099            },
1100        ]
1101    );
1102    assert_eq!(
1103        pane_summaries(&workspace_b, cx_b),
1104        &[
1105            PaneSummary {
1106                active: false,
1107                leader: None,
1108                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1109            },
1110            PaneSummary {
1111                active: true,
1112                leader: client_a.peer_id(),
1113                items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1114            },
1115        ]
1116    );
1117
1118    workspace_a.update_in(cx_a, |workspace, window, cx| {
1119        workspace.active_pane().update(cx, |pane, cx| {
1120            pane.activate_previous_item(&Default::default(), window, cx);
1121        });
1122    });
1123    executor.run_until_parked();
1124
1125    assert_eq!(
1126        pane_summaries(&workspace_a, cx_a),
1127        &[
1128            PaneSummary {
1129                active: false,
1130                leader: None,
1131                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1132            },
1133            PaneSummary {
1134                active: true,
1135                leader: None,
1136                items: vec![
1137                    (false, "1.txt".into()),
1138                    (true, "2.txt".into()),
1139                    (false, "4.txt".into()),
1140                    (false, "3.txt".into()),
1141                ]
1142            },
1143        ]
1144    );
1145    assert_eq!(
1146        pane_summaries(&workspace_b, cx_b),
1147        &[
1148            PaneSummary {
1149                active: false,
1150                leader: None,
1151                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1152            },
1153            PaneSummary {
1154                active: true,
1155                leader: client_a.peer_id(),
1156                items: vec![
1157                    (false, "3.txt".into()),
1158                    (false, "4.txt".into()),
1159                    (true, "2.txt".into())
1160                ]
1161            },
1162        ]
1163    );
1164
1165    workspace_a.update_in(cx_a, |workspace, window, cx| {
1166        workspace.active_pane().update(cx, |pane, cx| {
1167            pane.activate_previous_item(&Default::default(), window, cx);
1168        });
1169    });
1170    executor.run_until_parked();
1171
1172    assert_eq!(
1173        pane_summaries(&workspace_a, cx_a),
1174        &[
1175            PaneSummary {
1176                active: false,
1177                leader: None,
1178                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1179            },
1180            PaneSummary {
1181                active: true,
1182                leader: None,
1183                items: vec![
1184                    (true, "1.txt".into()),
1185                    (false, "2.txt".into()),
1186                    (false, "4.txt".into()),
1187                    (false, "3.txt".into()),
1188                ]
1189            },
1190        ]
1191    );
1192    assert_eq!(
1193        pane_summaries(&workspace_b, cx_b),
1194        &[
1195            PaneSummary {
1196                active: false,
1197                leader: None,
1198                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1199            },
1200            PaneSummary {
1201                active: true,
1202                leader: client_a.peer_id(),
1203                items: vec![
1204                    (false, "3.txt".into()),
1205                    (false, "4.txt".into()),
1206                    (false, "2.txt".into()),
1207                    (true, "1.txt".into()),
1208                ]
1209            },
1210        ]
1211    );
1212}
1213
1214#[gpui::test(iterations = 10)]
1215async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1216    // 2 clients connect to a server.
1217    let executor = cx_a.executor();
1218    let mut server = TestServer::start(executor.clone()).await;
1219    let client_a = server.create_client(cx_a, "user_a").await;
1220    let client_b = server.create_client(cx_b, "user_b").await;
1221    server
1222        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1223        .await;
1224    let active_call_a = cx_a.read(ActiveCall::global);
1225    let active_call_b = cx_b.read(ActiveCall::global);
1226
1227    cx_a.update(editor::init);
1228    cx_b.update(editor::init);
1229
1230    // Client A shares a project.
1231    client_a
1232        .fs()
1233        .insert_tree(
1234            path!("/a"),
1235            json!({
1236                "1.txt": "one",
1237                "2.txt": "two",
1238                "3.txt": "three",
1239            }),
1240        )
1241        .await;
1242    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1243    active_call_a
1244        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1245        .await
1246        .unwrap();
1247
1248    let project_id = active_call_a
1249        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1250        .await
1251        .unwrap();
1252    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1253    active_call_b
1254        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1255        .await
1256        .unwrap();
1257
1258    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1259    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1260
1261    let _editor_a1 = workspace_a
1262        .update_in(cx_a, |workspace, window, cx| {
1263            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1264        })
1265        .await
1266        .unwrap()
1267        .downcast::<Editor>()
1268        .unwrap();
1269
1270    // Client B starts following client A.
1271    let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
1272    let leader_id = project_b.update(cx_b, |project, _| {
1273        project.collaborators().values().next().unwrap().peer_id
1274    });
1275    workspace_b.update_in(cx_b, |workspace, window, cx| {
1276        workspace.follow(leader_id, window, cx)
1277    });
1278    executor.run_until_parked();
1279    assert_eq!(
1280        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1281        Some(leader_id.into())
1282    );
1283    let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
1284        workspace
1285            .active_item(cx)
1286            .unwrap()
1287            .downcast::<Editor>()
1288            .unwrap()
1289    });
1290
1291    // When client B moves, it automatically stops following client A.
1292    editor_b2.update_in(cx_b, |editor, window, cx| {
1293        editor.move_right(&editor::actions::MoveRight, window, cx)
1294    });
1295    assert_eq!(
1296        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1297        None
1298    );
1299
1300    workspace_b.update_in(cx_b, |workspace, window, cx| {
1301        workspace.follow(leader_id, window, cx)
1302    });
1303    executor.run_until_parked();
1304    assert_eq!(
1305        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1306        Some(leader_id.into())
1307    );
1308
1309    // When client B edits, it automatically stops following client A.
1310    editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
1311    assert_eq!(
1312        workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
1313        None
1314    );
1315
1316    workspace_b.update_in(cx_b, |workspace, window, cx| {
1317        workspace.follow(leader_id, window, cx)
1318    });
1319    executor.run_until_parked();
1320    assert_eq!(
1321        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1322        Some(leader_id.into())
1323    );
1324
1325    // When client B scrolls, it automatically stops following client A.
1326    editor_b2.update_in(cx_b, |editor, window, cx| {
1327        editor.set_scroll_position(point(0., 3.), window, cx)
1328    });
1329    assert_eq!(
1330        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1331        None
1332    );
1333
1334    workspace_b.update_in(cx_b, |workspace, window, cx| {
1335        workspace.follow(leader_id, window, cx)
1336    });
1337    executor.run_until_parked();
1338    assert_eq!(
1339        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1340        Some(leader_id.into())
1341    );
1342
1343    // When client B activates a different pane, it continues following client A in the original pane.
1344    workspace_b.update_in(cx_b, |workspace, window, cx| {
1345        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
1346    });
1347    assert_eq!(
1348        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1349        Some(leader_id.into())
1350    );
1351
1352    workspace_b.update_in(cx_b, |workspace, window, cx| {
1353        workspace.activate_next_pane(window, cx)
1354    });
1355    assert_eq!(
1356        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1357        Some(leader_id.into())
1358    );
1359
1360    // When client B activates a different item in the original pane, it automatically stops following client A.
1361    workspace_b
1362        .update_in(cx_b, |workspace, window, cx| {
1363            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
1364        })
1365        .await
1366        .unwrap();
1367    assert_eq!(
1368        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1369        None
1370    );
1371}
1372
1373#[gpui::test(iterations = 10)]
1374async fn test_peers_simultaneously_following_each_other(
1375    cx_a: &mut TestAppContext,
1376    cx_b: &mut TestAppContext,
1377) {
1378    let executor = cx_a.executor();
1379    let mut server = TestServer::start(executor.clone()).await;
1380    let client_a = server.create_client(cx_a, "user_a").await;
1381    let client_b = server.create_client(cx_b, "user_b").await;
1382    server
1383        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1384        .await;
1385    let active_call_a = cx_a.read(ActiveCall::global);
1386
1387    cx_a.update(editor::init);
1388    cx_b.update(editor::init);
1389
1390    client_a.fs().insert_tree("/a", json!({})).await;
1391    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1392    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1393    let project_id = active_call_a
1394        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1395        .await
1396        .unwrap();
1397
1398    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1399    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1400
1401    executor.run_until_parked();
1402    let client_a_id = project_b.update(cx_b, |project, _| {
1403        project.collaborators().values().next().unwrap().peer_id
1404    });
1405    let client_b_id = project_a.update(cx_a, |project, _| {
1406        project.collaborators().values().next().unwrap().peer_id
1407    });
1408
1409    workspace_a.update_in(cx_a, |workspace, window, cx| {
1410        workspace.follow(client_b_id, window, cx)
1411    });
1412    workspace_b.update_in(cx_b, |workspace, window, cx| {
1413        workspace.follow(client_a_id, window, cx)
1414    });
1415    executor.run_until_parked();
1416
1417    workspace_a.update(cx_a, |workspace, _| {
1418        assert_eq!(
1419            workspace.leader_for_pane(workspace.active_pane()),
1420            Some(client_b_id.into())
1421        );
1422    });
1423    workspace_b.update(cx_b, |workspace, _| {
1424        assert_eq!(
1425            workspace.leader_for_pane(workspace.active_pane()),
1426            Some(client_a_id.into())
1427        );
1428    });
1429}
1430
1431#[gpui::test(iterations = 10)]
1432async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1433    // a and b join a channel/call
1434    // a shares project 1
1435    // b shares project 2
1436    //
1437    // b follows a: causes project 2 to be joined, and b to follow a.
1438    // b opens a different file in project 2, a follows b
1439    // b opens a different file in project 1, a cannot follow b
1440    // b shares the project, a joins the project and follows b
1441    let executor = cx_a.executor();
1442    let mut server = TestServer::start(executor.clone()).await;
1443    let client_a = server.create_client(cx_a, "user_a").await;
1444    let client_b = server.create_client(cx_b, "user_b").await;
1445
1446    client_a
1447        .fs()
1448        .insert_tree(
1449            path!("/a"),
1450            json!({
1451                "w.rs": "",
1452                "x.rs": "",
1453            }),
1454        )
1455        .await;
1456
1457    client_b
1458        .fs()
1459        .insert_tree(
1460            path!("/b"),
1461            json!({
1462                "y.rs": "",
1463                "z.rs": "",
1464            }),
1465        )
1466        .await;
1467
1468    server
1469        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1470        .await;
1471    let active_call_a = cx_a.read(ActiveCall::global);
1472    let active_call_b = cx_b.read(ActiveCall::global);
1473
1474    let (project_a, worktree_id_a) = client_a.build_local_project(path!("/a"), cx_a).await;
1475    let (project_b, worktree_id_b) = client_b.build_local_project(path!("/b"), cx_b).await;
1476
1477    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1478    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1479
1480    active_call_a
1481        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1482        .await
1483        .unwrap();
1484
1485    active_call_a
1486        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1487        .await
1488        .unwrap();
1489    active_call_b
1490        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1491        .await
1492        .unwrap();
1493
1494    workspace_a
1495        .update_in(cx_a, |workspace, window, cx| {
1496            workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
1497        })
1498        .await
1499        .unwrap();
1500
1501    executor.run_until_parked();
1502    assert_eq!(visible_push_notifications(cx_b).len(), 1);
1503
1504    workspace_b.update_in(cx_b, |workspace, window, cx| {
1505        workspace.follow(client_a.peer_id().unwrap(), window, cx)
1506    });
1507
1508    executor.run_until_parked();
1509    let window_b_project_a = *cx_b
1510        .windows()
1511        .iter()
1512        .max_by_key(|window| window.window_id())
1513        .unwrap();
1514
1515    let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
1516
1517    let workspace_b_project_a = window_b_project_a
1518        .downcast::<Workspace>()
1519        .unwrap()
1520        .root(cx_b)
1521        .unwrap();
1522
1523    // assert that b is following a in project a in w.rs
1524    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1525        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1526        assert_eq!(
1527            client_a.peer_id().map(Into::into),
1528            workspace.leader_for_pane(workspace.active_pane())
1529        );
1530        let item = workspace.active_item(cx).unwrap();
1531        assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
1532    });
1533
1534    // TODO: in app code, this would be done by the collab_ui.
1535    active_call_b
1536        .update(&mut cx_b2, |call, cx| {
1537            let project = workspace_b_project_a.read(cx).project().clone();
1538            call.set_location(Some(&project), cx)
1539        })
1540        .await
1541        .unwrap();
1542
1543    // assert that there are no share notifications open
1544    assert_eq!(visible_push_notifications(cx_b).len(), 0);
1545
1546    // b moves to x.rs in a's project, and a follows
1547    workspace_b_project_a
1548        .update_in(&mut cx_b2, |workspace, window, cx| {
1549            workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
1550        })
1551        .await
1552        .unwrap();
1553
1554    executor.run_until_parked();
1555    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1556        let item = workspace.active_item(cx).unwrap();
1557        assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
1558    });
1559
1560    workspace_a.update_in(cx_a, |workspace, window, cx| {
1561        workspace.follow(client_b.peer_id().unwrap(), window, cx)
1562    });
1563
1564    executor.run_until_parked();
1565    workspace_a.update(cx_a, |workspace, cx| {
1566        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1567        assert_eq!(
1568            client_b.peer_id().map(Into::into),
1569            workspace.leader_for_pane(workspace.active_pane())
1570        );
1571        let item = workspace.active_pane().read(cx).active_item().unwrap();
1572        assert_eq!(item.tab_content_text(0, cx), "x.rs");
1573    });
1574
1575    // b moves to y.rs in b's project, a is still following but can't yet see
1576    workspace_b
1577        .update_in(cx_b, |workspace, window, cx| {
1578            workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
1579        })
1580        .await
1581        .unwrap();
1582
1583    // TODO: in app code, this would be done by the collab_ui.
1584    active_call_b
1585        .update(cx_b, |call, cx| {
1586            let project = workspace_b.read(cx).project().clone();
1587            call.set_location(Some(&project), cx)
1588        })
1589        .await
1590        .unwrap();
1591
1592    let project_b_id = active_call_b
1593        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1594        .await
1595        .unwrap();
1596
1597    executor.run_until_parked();
1598    assert_eq!(visible_push_notifications(cx_a).len(), 1);
1599    cx_a.update(|_, cx| {
1600        workspace::join_in_room_project(
1601            project_b_id,
1602            client_b.user_id().unwrap(),
1603            client_a.app_state.clone(),
1604            cx,
1605        )
1606    })
1607    .await
1608    .unwrap();
1609
1610    executor.run_until_parked();
1611
1612    assert_eq!(visible_push_notifications(cx_a).len(), 0);
1613    let window_a_project_b = *cx_a
1614        .windows()
1615        .iter()
1616        .max_by_key(|window| window.window_id())
1617        .unwrap();
1618    let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1619    let workspace_a_project_b = window_a_project_b
1620        .downcast::<Workspace>()
1621        .unwrap()
1622        .root(cx_a)
1623        .unwrap();
1624
1625    executor.run_until_parked();
1626
1627    workspace_a_project_b.update(cx_a2, |workspace, cx| {
1628        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1629        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1630        assert_eq!(
1631            client_b.peer_id().map(Into::into),
1632            workspace.leader_for_pane(workspace.active_pane())
1633        );
1634        let item = workspace.active_item(cx).unwrap();
1635        assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1636    });
1637}
1638
1639#[gpui::test]
1640async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1641    let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1642
1643    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1644    client_a
1645        .host_workspace(&workspace_a, channel_id, cx_a)
1646        .await;
1647    let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1648
1649    cx_a.simulate_keystrokes("cmd-p");
1650    cx_a.run_until_parked();
1651    cx_a.simulate_keystrokes("2 enter");
1652
1653    let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1654        workspace.active_item_as::<Editor>(cx).unwrap()
1655    });
1656    let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1657        workspace.active_item_as::<Editor>(cx).unwrap()
1658    });
1659
1660    // b should follow a to position 1
1661    editor_a.update_in(cx_a, |editor, window, cx| {
1662        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1663            s.select_ranges([1..1])
1664        })
1665    });
1666    cx_a.executor()
1667        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1668    cx_a.run_until_parked();
1669    editor_b.update(cx_b, |editor, cx| {
1670        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1671    });
1672
1673    // a unshares the project
1674    cx_a.update(|_, cx| {
1675        let project = workspace_a.read(cx).project().clone();
1676        ActiveCall::global(cx).update(cx, |call, cx| {
1677            call.unshare_project(project, cx).unwrap();
1678        })
1679    });
1680    cx_a.run_until_parked();
1681
1682    // b should not follow a to position 2
1683    editor_a.update_in(cx_a, |editor, window, cx| {
1684        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1685            s.select_ranges([2..2])
1686        })
1687    });
1688    cx_a.executor()
1689        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1690    cx_a.run_until_parked();
1691    editor_b.update(cx_b, |editor, cx| {
1692        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1693    });
1694    cx_b.update(|_, cx| {
1695        let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1696        let participant = room.remote_participants().get(&client_a.id()).unwrap();
1697        assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1698    })
1699}
1700
1701#[gpui::test]
1702async fn test_following_into_excluded_file(
1703    mut cx_a: &mut TestAppContext,
1704    mut cx_b: &mut TestAppContext,
1705) {
1706    let executor = cx_a.executor();
1707    let mut server = TestServer::start(executor.clone()).await;
1708    let client_a = server.create_client(cx_a, "user_a").await;
1709    let client_b = server.create_client(cx_b, "user_b").await;
1710    for cx in [&mut cx_a, &mut cx_b] {
1711        cx.update(|cx| {
1712            cx.update_global::<SettingsStore, _>(|store, cx| {
1713                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
1714                    settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1715                });
1716            });
1717        });
1718    }
1719    server
1720        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1721        .await;
1722    let active_call_a = cx_a.read(ActiveCall::global);
1723    let active_call_b = cx_b.read(ActiveCall::global);
1724    let peer_id_a = client_a.peer_id().unwrap();
1725
1726    client_a
1727        .fs()
1728        .insert_tree(
1729            path!("/a"),
1730            json!({
1731                ".git": {
1732                    "COMMIT_EDITMSG": "write your commit message here",
1733                },
1734                "1.txt": "one\none\none",
1735                "2.txt": "two\ntwo\ntwo",
1736                "3.txt": "three\nthree\nthree",
1737            }),
1738        )
1739        .await;
1740    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1741    active_call_a
1742        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1743        .await
1744        .unwrap();
1745
1746    let project_id = active_call_a
1747        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1748        .await
1749        .unwrap();
1750    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1751    active_call_b
1752        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1753        .await
1754        .unwrap();
1755
1756    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1757    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1758
1759    // Client A opens editors for a regular file and an excluded file.
1760    let editor_for_regular = workspace_a
1761        .update_in(cx_a, |workspace, window, cx| {
1762            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1763        })
1764        .await
1765        .unwrap()
1766        .downcast::<Editor>()
1767        .unwrap();
1768    let editor_for_excluded_a = workspace_a
1769        .update_in(cx_a, |workspace, window, cx| {
1770            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
1771        })
1772        .await
1773        .unwrap()
1774        .downcast::<Editor>()
1775        .unwrap();
1776
1777    // Client A updates their selections in those editors
1778    editor_for_regular.update_in(cx_a, |editor, window, cx| {
1779        editor.handle_input("a", window, cx);
1780        editor.handle_input("b", window, cx);
1781        editor.handle_input("c", window, cx);
1782        editor.select_left(&Default::default(), window, cx);
1783        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1784    });
1785    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1786        editor.select_all(&Default::default(), window, cx);
1787        editor.handle_input("new commit message", window, cx);
1788        editor.select_left(&Default::default(), window, cx);
1789        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1790    });
1791
1792    // When client B starts following client A, currently visible file is replicated
1793    workspace_b.update_in(cx_b, |workspace, window, cx| {
1794        workspace.follow(peer_id_a, window, cx)
1795    });
1796    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1797    executor.run_until_parked();
1798
1799    let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1800        workspace
1801            .active_item(cx)
1802            .unwrap()
1803            .downcast::<Editor>()
1804            .unwrap()
1805    });
1806    assert_eq!(
1807        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1808        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1809    );
1810    assert_eq!(
1811        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1812        vec![18..17]
1813    );
1814
1815    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1816        editor.select_right(&Default::default(), window, cx);
1817    });
1818    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1819    executor.run_until_parked();
1820
1821    // Changes from B to the excluded file are replicated in A's editor
1822    editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1823        editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1824    });
1825    executor.run_until_parked();
1826    editor_for_excluded_a.update(cx_a, |editor, cx| {
1827        assert_eq!(
1828            editor.text(cx),
1829            "new commit message\nCo-Authored-By: B <b@b.b>"
1830        );
1831    });
1832}
1833
1834fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1835    let mut ret = Vec::new();
1836    for window in cx.windows() {
1837        window
1838            .update(cx, |window, _, _| {
1839                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1840                    ret.push(handle)
1841                }
1842            })
1843            .unwrap();
1844    }
1845    ret
1846}
1847
1848#[derive(Debug, PartialEq, Eq)]
1849struct PaneSummary {
1850    active: bool,
1851    leader: Option<PeerId>,
1852    items: Vec<(bool, String)>,
1853}
1854
1855fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1856    cx.read(|cx| {
1857        let active_call = ActiveCall::global(cx).read(cx);
1858        let peer_id = active_call.client().peer_id();
1859        let room = active_call.room().unwrap().read(cx);
1860        let mut result = room
1861            .remote_participants()
1862            .values()
1863            .map(|participant| participant.peer_id)
1864            .chain(peer_id)
1865            .filter_map(|peer_id| {
1866                let followers = room.followers_for(peer_id, project_id);
1867                if followers.is_empty() {
1868                    None
1869                } else {
1870                    Some((peer_id, followers.to_vec()))
1871                }
1872            })
1873            .collect::<Vec<_>>();
1874        result.sort_by_key(|e| e.0);
1875        result
1876    })
1877}
1878
1879fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1880    workspace.update(cx, |workspace, cx| {
1881        let active_pane = workspace.active_pane();
1882        workspace
1883            .panes()
1884            .iter()
1885            .map(|pane| {
1886                let leader = match workspace.leader_for_pane(pane) {
1887                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1888                    Some(CollaboratorId::Agent) => unimplemented!(),
1889                    None => None,
1890                };
1891                let active = pane == active_pane;
1892                let pane = pane.read(cx);
1893                let active_ix = pane.active_item_index();
1894                PaneSummary {
1895                    active,
1896                    leader,
1897                    items: pane
1898                        .items()
1899                        .enumerate()
1900                        .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1901                        .collect(),
1902                }
1903            })
1904            .collect()
1905    })
1906}
1907
1908#[gpui::test(iterations = 10)]
1909async fn test_following_to_channel_notes_without_a_shared_project(
1910    deterministic: BackgroundExecutor,
1911    mut cx_a: &mut TestAppContext,
1912    mut cx_b: &mut TestAppContext,
1913    mut cx_c: &mut TestAppContext,
1914) {
1915    let mut server = TestServer::start(deterministic.clone()).await;
1916    let client_a = server.create_client(cx_a, "user_a").await;
1917    let client_b = server.create_client(cx_b, "user_b").await;
1918    let client_c = server.create_client(cx_c, "user_c").await;
1919
1920    cx_a.update(editor::init);
1921    cx_b.update(editor::init);
1922    cx_c.update(editor::init);
1923    cx_a.update(collab_ui::channel_view::init);
1924    cx_b.update(collab_ui::channel_view::init);
1925    cx_c.update(collab_ui::channel_view::init);
1926
1927    let channel_1_id = server
1928        .make_channel(
1929            "channel-1",
1930            None,
1931            (&client_a, cx_a),
1932            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1933        )
1934        .await;
1935    let channel_2_id = server
1936        .make_channel(
1937            "channel-2",
1938            None,
1939            (&client_a, cx_a),
1940            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1941        )
1942        .await;
1943
1944    // Clients A, B, and C join a channel.
1945    let active_call_a = cx_a.read(ActiveCall::global);
1946    let active_call_b = cx_b.read(ActiveCall::global);
1947    let active_call_c = cx_c.read(ActiveCall::global);
1948    for (call, cx) in [
1949        (&active_call_a, &mut cx_a),
1950        (&active_call_b, &mut cx_b),
1951        (&active_call_c, &mut cx_c),
1952    ] {
1953        call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1954            .await
1955            .unwrap();
1956    }
1957    deterministic.run_until_parked();
1958
1959    // Clients A, B, and C all open their own unshared projects.
1960    client_a
1961        .fs()
1962        .insert_tree("/a", json!({ "1.txt": "" }))
1963        .await;
1964    client_b.fs().insert_tree("/b", json!({})).await;
1965    client_c.fs().insert_tree("/c", json!({})).await;
1966    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1967    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1968    let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1969    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1970    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1971    let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1972
1973    active_call_a
1974        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1975        .await
1976        .unwrap();
1977
1978    // Client A opens the notes for channel 1.
1979    let channel_notes_1_a = cx_a
1980        .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1981        .await
1982        .unwrap();
1983    channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1984        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1985        notes.editor.update(cx, |editor, cx| {
1986            editor.insert("Hello from A.", window, cx);
1987            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1988                selections.select_ranges(vec![3..4]);
1989            });
1990        });
1991    });
1992
1993    // Client B follows client A.
1994    workspace_b
1995        .update_in(cx_b, |workspace, window, cx| {
1996            workspace
1997                .start_following(client_a.peer_id().unwrap(), window, cx)
1998                .unwrap()
1999        })
2000        .await
2001        .unwrap();
2002
2003    // Client B is taken to the notes for channel 1, with the same
2004    // text selected as client A.
2005    deterministic.run_until_parked();
2006    let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
2007        assert_eq!(
2008            workspace.leader_for_pane(workspace.active_pane()),
2009            Some(client_a.peer_id().unwrap().into())
2010        );
2011        workspace
2012            .active_item(cx)
2013            .expect("no active item")
2014            .downcast::<ChannelView>()
2015            .expect("active item is not a channel view")
2016    });
2017    channel_notes_1_b.update(cx_b, |notes, cx| {
2018        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2019        notes.editor.update(cx, |editor, cx| {
2020            assert_eq!(editor.text(cx), "Hello from A.");
2021            assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2022        })
2023    });
2024
2025    //  Client A opens the notes for channel 2.
2026    let channel_notes_2_a = cx_a
2027        .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2028        .await
2029        .unwrap();
2030    channel_notes_2_a.update(cx_a, |notes, cx| {
2031        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2032    });
2033
2034    // Client B is taken to the notes for channel 2.
2035    deterministic.run_until_parked();
2036    let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2037        assert_eq!(
2038            workspace.leader_for_pane(workspace.active_pane()),
2039            Some(client_a.peer_id().unwrap().into())
2040        );
2041        workspace
2042            .active_item(cx)
2043            .expect("no active item")
2044            .downcast::<ChannelView>()
2045            .expect("active item is not a channel view")
2046    });
2047    channel_notes_2_b.update(cx_b, |notes, cx| {
2048        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2049    });
2050
2051    // Client A opens a local buffer in their unshared project.
2052    let _unshared_editor_a1 = workspace_a
2053        .update_in(cx_a, |workspace, window, cx| {
2054            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
2055        })
2056        .await
2057        .unwrap()
2058        .downcast::<Editor>()
2059        .unwrap();
2060
2061    // This does not send any leader update message to client B.
2062    // If it did, an error would occur on client B, since this buffer
2063    // is not shared with them.
2064    deterministic.run_until_parked();
2065    workspace_b.update(cx_b, |workspace, cx| {
2066        assert_eq!(
2067            workspace.active_item(cx).expect("no active item").item_id(),
2068            channel_notes_2_b.entity_id()
2069        );
2070    });
2071}
2072
2073pub(crate) async fn join_channel(
2074    channel_id: ChannelId,
2075    client: &TestClient,
2076    cx: &mut TestAppContext,
2077) -> anyhow::Result<()> {
2078    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2079        .await
2080}
2081
2082async fn share_workspace(
2083    workspace: &Entity<Workspace>,
2084    cx: &mut VisualTestContext,
2085) -> anyhow::Result<u64> {
2086    let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2087    cx.read(ActiveCall::global)
2088        .update(cx, |call, cx| call.share_project(project, cx))
2089        .await
2090}
2091
2092#[gpui::test]
2093async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2094    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2095
2096    let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2097    join_channel(channel, &client_a, cx_a).await.unwrap();
2098    share_workspace(&workspace, cx_a).await.unwrap();
2099    let buffer = workspace.update(cx_a, |workspace, cx| {
2100        workspace.project().update(cx, |project, cx| {
2101            project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
2102        })
2103    });
2104    let multibuffer = cx_a.new(|cx| {
2105        let mut mb = MultiBuffer::new(Capability::ReadWrite);
2106        mb.set_excerpts_for_path(
2107            PathKey::for_buffer(&buffer, cx),
2108            buffer.clone(),
2109            [Point::row_range(1..1), Point::row_range(5..5)],
2110            1,
2111            cx,
2112        );
2113        mb
2114    });
2115    let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2116    let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2117        Editor::for_multibuffer(
2118            multibuffer.clone(),
2119            Some(workspace.read(cx).project().clone()),
2120            window,
2121            cx,
2122        )
2123    });
2124    workspace.update_in(cx_a, |workspace, window, cx| {
2125        workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2126    });
2127    editor.update_in(cx_a, |editor, window, cx| {
2128        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2129            s.select_ranges([Point::row_range(4..4)]);
2130        })
2131    });
2132    let positions = editor.update(cx_a, |editor, _| {
2133        editor
2134            .selections
2135            .disjoint_anchor_ranges()
2136            .map(|range| range.start.text_anchor.to_point(&snapshot))
2137            .collect::<Vec<_>>()
2138    });
2139    multibuffer.update(cx_a, |multibuffer, cx| {
2140        multibuffer.set_excerpts_for_path(
2141            PathKey::for_buffer(&buffer, cx),
2142            buffer,
2143            [Point::row_range(1..5)],
2144            1,
2145            cx,
2146        );
2147    });
2148
2149    let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2150    cx_b.run_until_parked();
2151    let editor_b = workspace_b
2152        .update(cx_b, |workspace, cx| {
2153            workspace
2154                .active_item(cx)
2155                .and_then(|item| item.downcast::<Editor>())
2156        })
2157        .unwrap();
2158
2159    let new_positions = editor_b.update(cx_b, |editor, _| {
2160        editor
2161            .selections
2162            .disjoint_anchor_ranges()
2163            .map(|range| range.start.text_anchor.to_point(&snapshot))
2164            .collect::<Vec<_>>()
2165    });
2166    assert_eq!(positions, new_positions);
2167}
2168
2169#[gpui::test]
2170async fn test_following_to_channel_notes_other_workspace(
2171    cx_a: &mut TestAppContext,
2172    cx_b: &mut TestAppContext,
2173) {
2174    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2175
2176    let mut cx_a2 = cx_a.clone();
2177    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2178    join_channel(channel, &client_a, cx_a).await.unwrap();
2179    share_workspace(&workspace_a, cx_a).await.unwrap();
2180
2181    // a opens 1.txt
2182    cx_a.simulate_keystrokes("cmd-p");
2183    cx_a.run_until_parked();
2184    cx_a.simulate_keystrokes("1 enter");
2185    cx_a.run_until_parked();
2186    workspace_a.update(cx_a, |workspace, cx| {
2187        let editor = workspace.active_item(cx).unwrap();
2188        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2189    });
2190
2191    // b joins channel and is following a
2192    join_channel(channel, &client_b, cx_b).await.unwrap();
2193    cx_b.run_until_parked();
2194    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2195    workspace_b.update(cx_b, |workspace, cx| {
2196        let editor = workspace.active_item(cx).unwrap();
2197        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2198    });
2199
2200    // a opens a second workspace and the channel notes
2201    let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2202    cx_a2.update(|window, _| window.activate_window());
2203    cx_a2
2204        .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2205        .await
2206        .unwrap();
2207    cx_a2.run_until_parked();
2208
2209    // b should follow a to the channel notes
2210    workspace_b.update(cx_b, |workspace, cx| {
2211        let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2212        assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2213    });
2214
2215    // a returns to the shared project
2216    cx_a.update(|window, _| window.activate_window());
2217    cx_a.run_until_parked();
2218
2219    workspace_a.update(cx_a, |workspace, cx| {
2220        let editor = workspace.active_item(cx).unwrap();
2221        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2222    });
2223
2224    // b should follow a back
2225    workspace_b.update(cx_b, |workspace, cx| {
2226        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2227        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2228    });
2229}
2230
2231#[gpui::test]
2232async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2233    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2234
2235    let mut cx_a2 = cx_a.clone();
2236    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2237    join_channel(channel, &client_a, cx_a).await.unwrap();
2238    share_workspace(&workspace_a, cx_a).await.unwrap();
2239
2240    // a opens 1.txt
2241    cx_a.simulate_keystrokes("cmd-p");
2242    cx_a.run_until_parked();
2243    cx_a.simulate_keystrokes("1 enter");
2244    cx_a.run_until_parked();
2245    workspace_a.update(cx_a, |workspace, cx| {
2246        let editor = workspace.active_item(cx).unwrap();
2247        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2248    });
2249
2250    // b joins channel and is following a
2251    join_channel(channel, &client_b, cx_b).await.unwrap();
2252    cx_b.run_until_parked();
2253    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2254    workspace_b.update(cx_b, |workspace, cx| {
2255        let editor = workspace.active_item(cx).unwrap();
2256        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2257    });
2258
2259    // stop following
2260    cx_b.simulate_keystrokes("down");
2261
2262    // a opens a different file while not followed
2263    cx_a.simulate_keystrokes("cmd-p");
2264    cx_a.run_until_parked();
2265    cx_a.simulate_keystrokes("2 enter");
2266
2267    workspace_b.update(cx_b, |workspace, cx| {
2268        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2269        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2270    });
2271
2272    // a opens a file in a new window
2273    let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2274    cx_a2.update(|window, _| window.activate_window());
2275    cx_a2.simulate_keystrokes("cmd-p");
2276    cx_a2.run_until_parked();
2277    cx_a2.simulate_keystrokes("3 enter");
2278    cx_a2.run_until_parked();
2279
2280    // b starts following a again
2281    cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2282    cx_a.run_until_parked();
2283
2284    // a returns to the shared project
2285    cx_a.update(|window, _| window.activate_window());
2286    cx_a.run_until_parked();
2287
2288    workspace_a.update(cx_a, |workspace, cx| {
2289        let editor = workspace.active_item(cx).unwrap();
2290        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2291    });
2292
2293    // b should follow a back
2294    workspace_b.update(cx_b, |workspace, cx| {
2295        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2296        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2297    });
2298}