following_tests.rs

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