following_tests.rs

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