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