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