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