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