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