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