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