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