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                .detach();
1014        });
1015    });
1016
1017    executor.run_until_parked();
1018
1019    // Both clients see that Client B is looking at the previous tab.
1020    assert_eq!(
1021        pane_summaries(&workspace_b, cx_b),
1022        &[
1023            PaneSummary {
1024                active: false,
1025                leader: None,
1026                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1027            },
1028            PaneSummary {
1029                active: true,
1030                leader: None,
1031                items: vec![(true, "3.txt".into()),]
1032            },
1033        ]
1034    );
1035    assert_eq!(
1036        pane_summaries(&workspace_a, cx_a),
1037        &[
1038            PaneSummary {
1039                active: false,
1040                leader: None,
1041                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1042            },
1043            PaneSummary {
1044                active: true,
1045                leader: client_b.peer_id(),
1046                items: vec![
1047                    (false, "1.txt".into()),
1048                    (false, "2.txt".into()),
1049                    (false, "4.txt".into()),
1050                    (true, "3.txt".into()),
1051                ]
1052            },
1053        ]
1054    );
1055
1056    // Client B follows client A again.
1057    workspace_b.update_in(cx_b, |workspace, window, cx| {
1058        workspace.follow(client_a.peer_id().unwrap(), window, cx)
1059    });
1060    executor.run_until_parked();
1061    // Client A cycles through some tabs.
1062    workspace_a.update_in(cx_a, |workspace, window, cx| {
1063        workspace.active_pane().update(cx, |pane, cx| {
1064            pane.activate_prev_item(true, window, cx);
1065        });
1066    });
1067    executor.run_until_parked();
1068
1069    // Client B follows client A into those tabs.
1070    assert_eq!(
1071        pane_summaries(&workspace_a, cx_a),
1072        &[
1073            PaneSummary {
1074                active: false,
1075                leader: None,
1076                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1077            },
1078            PaneSummary {
1079                active: true,
1080                leader: None,
1081                items: vec![
1082                    (false, "1.txt".into()),
1083                    (false, "2.txt".into()),
1084                    (true, "4.txt".into()),
1085                    (false, "3.txt".into()),
1086                ]
1087            },
1088        ]
1089    );
1090    assert_eq!(
1091        pane_summaries(&workspace_b, cx_b),
1092        &[
1093            PaneSummary {
1094                active: false,
1095                leader: None,
1096                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1097            },
1098            PaneSummary {
1099                active: true,
1100                leader: client_a.peer_id(),
1101                items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1102            },
1103        ]
1104    );
1105
1106    workspace_a.update_in(cx_a, |workspace, window, cx| {
1107        workspace.active_pane().update(cx, |pane, cx| {
1108            pane.activate_prev_item(true, window, cx);
1109        });
1110    });
1111    executor.run_until_parked();
1112
1113    assert_eq!(
1114        pane_summaries(&workspace_a, cx_a),
1115        &[
1116            PaneSummary {
1117                active: false,
1118                leader: None,
1119                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1120            },
1121            PaneSummary {
1122                active: true,
1123                leader: None,
1124                items: vec![
1125                    (false, "1.txt".into()),
1126                    (true, "2.txt".into()),
1127                    (false, "4.txt".into()),
1128                    (false, "3.txt".into()),
1129                ]
1130            },
1131        ]
1132    );
1133    assert_eq!(
1134        pane_summaries(&workspace_b, cx_b),
1135        &[
1136            PaneSummary {
1137                active: false,
1138                leader: None,
1139                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1140            },
1141            PaneSummary {
1142                active: true,
1143                leader: client_a.peer_id(),
1144                items: vec![
1145                    (false, "3.txt".into()),
1146                    (false, "4.txt".into()),
1147                    (true, "2.txt".into())
1148                ]
1149            },
1150        ]
1151    );
1152
1153    workspace_a.update_in(cx_a, |workspace, window, cx| {
1154        workspace.active_pane().update(cx, |pane, cx| {
1155            pane.activate_prev_item(true, window, cx);
1156        });
1157    });
1158    executor.run_until_parked();
1159
1160    assert_eq!(
1161        pane_summaries(&workspace_a, cx_a),
1162        &[
1163            PaneSummary {
1164                active: false,
1165                leader: None,
1166                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1167            },
1168            PaneSummary {
1169                active: true,
1170                leader: None,
1171                items: vec![
1172                    (true, "1.txt".into()),
1173                    (false, "2.txt".into()),
1174                    (false, "4.txt".into()),
1175                    (false, "3.txt".into()),
1176                ]
1177            },
1178        ]
1179    );
1180    assert_eq!(
1181        pane_summaries(&workspace_b, cx_b),
1182        &[
1183            PaneSummary {
1184                active: false,
1185                leader: None,
1186                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1187            },
1188            PaneSummary {
1189                active: true,
1190                leader: client_a.peer_id(),
1191                items: vec![
1192                    (false, "3.txt".into()),
1193                    (false, "4.txt".into()),
1194                    (false, "2.txt".into()),
1195                    (true, "1.txt".into()),
1196                ]
1197            },
1198        ]
1199    );
1200}
1201
1202#[gpui::test(iterations = 10)]
1203async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1204    // 2 clients connect to a server.
1205    let executor = cx_a.executor();
1206    let mut server = TestServer::start(executor.clone()).await;
1207    let client_a = server.create_client(cx_a, "user_a").await;
1208    let client_b = server.create_client(cx_b, "user_b").await;
1209    server
1210        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1211        .await;
1212    let active_call_a = cx_a.read(ActiveCall::global);
1213    let active_call_b = cx_b.read(ActiveCall::global);
1214
1215    cx_a.update(editor::init);
1216    cx_b.update(editor::init);
1217
1218    // Client A shares a project.
1219    client_a
1220        .fs()
1221        .insert_tree(
1222            path!("/a"),
1223            json!({
1224                "1.txt": "one",
1225                "2.txt": "two",
1226                "3.txt": "three",
1227            }),
1228        )
1229        .await;
1230    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1231    active_call_a
1232        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1233        .await
1234        .unwrap();
1235
1236    let project_id = active_call_a
1237        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1238        .await
1239        .unwrap();
1240    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1241    active_call_b
1242        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1243        .await
1244        .unwrap();
1245
1246    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1247    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1248
1249    let _editor_a1 = workspace_a
1250        .update_in(cx_a, |workspace, window, cx| {
1251            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1252        })
1253        .await
1254        .unwrap()
1255        .downcast::<Editor>()
1256        .unwrap();
1257
1258    // Client B starts following client A.
1259    let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
1260    let leader_id = project_b.update(cx_b, |project, _| {
1261        project.collaborators().values().next().unwrap().peer_id
1262    });
1263    workspace_b.update_in(cx_b, |workspace, window, cx| {
1264        workspace.follow(leader_id, window, cx)
1265    });
1266    executor.run_until_parked();
1267    assert_eq!(
1268        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1269        Some(leader_id.into())
1270    );
1271    let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
1272        workspace
1273            .active_item(cx)
1274            .unwrap()
1275            .downcast::<Editor>()
1276            .unwrap()
1277    });
1278
1279    // When client B moves, it automatically stops following client A.
1280    editor_b2.update_in(cx_b, |editor, window, cx| {
1281        editor.move_right(&editor::actions::MoveRight, window, cx)
1282    });
1283    assert_eq!(
1284        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1285        None
1286    );
1287
1288    workspace_b.update_in(cx_b, |workspace, window, cx| {
1289        workspace.follow(leader_id, window, cx)
1290    });
1291    executor.run_until_parked();
1292    assert_eq!(
1293        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1294        Some(leader_id.into())
1295    );
1296
1297    // When client B edits, it automatically stops following client A.
1298    editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
1299    assert_eq!(
1300        workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
1301        None
1302    );
1303
1304    workspace_b.update_in(cx_b, |workspace, window, cx| {
1305        workspace.follow(leader_id, window, cx)
1306    });
1307    executor.run_until_parked();
1308    assert_eq!(
1309        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1310        Some(leader_id.into())
1311    );
1312
1313    // When client B scrolls, it automatically stops following client A.
1314    editor_b2.update_in(cx_b, |editor, window, cx| {
1315        editor.set_scroll_position(point(0., 3.), window, cx)
1316    });
1317    assert_eq!(
1318        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1319        None
1320    );
1321
1322    workspace_b.update_in(cx_b, |workspace, window, cx| {
1323        workspace.follow(leader_id, window, cx)
1324    });
1325    executor.run_until_parked();
1326    assert_eq!(
1327        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1328        Some(leader_id.into())
1329    );
1330
1331    // When client B activates a different pane, it continues following client A in the original pane.
1332    workspace_b.update_in(cx_b, |workspace, window, cx| {
1333        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
1334    });
1335    assert_eq!(
1336        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1337        Some(leader_id.into())
1338    );
1339
1340    workspace_b.update_in(cx_b, |workspace, window, cx| {
1341        workspace.activate_next_pane(window, cx)
1342    });
1343    assert_eq!(
1344        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1345        Some(leader_id.into())
1346    );
1347
1348    // When client B activates a different item in the original pane, it automatically stops following client A.
1349    workspace_b
1350        .update_in(cx_b, |workspace, window, cx| {
1351            workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
1352        })
1353        .await
1354        .unwrap();
1355    assert_eq!(
1356        workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1357        None
1358    );
1359}
1360
1361#[gpui::test(iterations = 10)]
1362async fn test_peers_simultaneously_following_each_other(
1363    cx_a: &mut TestAppContext,
1364    cx_b: &mut TestAppContext,
1365) {
1366    let executor = cx_a.executor();
1367    let mut server = TestServer::start(executor.clone()).await;
1368    let client_a = server.create_client(cx_a, "user_a").await;
1369    let client_b = server.create_client(cx_b, "user_b").await;
1370    server
1371        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1372        .await;
1373    let active_call_a = cx_a.read(ActiveCall::global);
1374
1375    cx_a.update(editor::init);
1376    cx_b.update(editor::init);
1377
1378    client_a.fs().insert_tree("/a", json!({})).await;
1379    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1380    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1381    let project_id = active_call_a
1382        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1383        .await
1384        .unwrap();
1385
1386    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1387    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1388
1389    executor.run_until_parked();
1390    let client_a_id = project_b.update(cx_b, |project, _| {
1391        project.collaborators().values().next().unwrap().peer_id
1392    });
1393    let client_b_id = project_a.update(cx_a, |project, _| {
1394        project.collaborators().values().next().unwrap().peer_id
1395    });
1396
1397    workspace_a.update_in(cx_a, |workspace, window, cx| {
1398        workspace.follow(client_b_id, window, cx)
1399    });
1400    workspace_b.update_in(cx_b, |workspace, window, cx| {
1401        workspace.follow(client_a_id, window, cx)
1402    });
1403    executor.run_until_parked();
1404
1405    workspace_a.update(cx_a, |workspace, _| {
1406        assert_eq!(
1407            workspace.leader_for_pane(workspace.active_pane()),
1408            Some(client_b_id.into())
1409        );
1410    });
1411    workspace_b.update(cx_b, |workspace, _| {
1412        assert_eq!(
1413            workspace.leader_for_pane(workspace.active_pane()),
1414            Some(client_a_id.into())
1415        );
1416    });
1417}
1418
1419#[gpui::test(iterations = 10)]
1420async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1421    // a and b join a channel/call
1422    // a shares project 1
1423    // b shares project 2
1424    //
1425    // b follows a: causes project 2 to be joined, and b to follow a.
1426    // b opens a different file in project 2, a follows b
1427    // b opens a different file in project 1, a cannot follow b
1428    // b shares the project, a joins the project and follows b
1429    let executor = cx_a.executor();
1430    let mut server = TestServer::start(executor.clone()).await;
1431    let client_a = server.create_client(cx_a, "user_a").await;
1432    let client_b = server.create_client(cx_b, "user_b").await;
1433
1434    client_a
1435        .fs()
1436        .insert_tree(
1437            path!("/a"),
1438            json!({
1439                "w.rs": "",
1440                "x.rs": "",
1441            }),
1442        )
1443        .await;
1444
1445    client_b
1446        .fs()
1447        .insert_tree(
1448            path!("/b"),
1449            json!({
1450                "y.rs": "",
1451                "z.rs": "",
1452            }),
1453        )
1454        .await;
1455
1456    server
1457        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1458        .await;
1459    let active_call_a = cx_a.read(ActiveCall::global);
1460    let active_call_b = cx_b.read(ActiveCall::global);
1461
1462    let (project_a, worktree_id_a) = client_a.build_local_project(path!("/a"), cx_a).await;
1463    let (project_b, worktree_id_b) = client_b.build_local_project(path!("/b"), cx_b).await;
1464
1465    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1466    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1467
1468    active_call_a
1469        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1470        .await
1471        .unwrap();
1472
1473    active_call_a
1474        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1475        .await
1476        .unwrap();
1477    active_call_b
1478        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1479        .await
1480        .unwrap();
1481
1482    workspace_a
1483        .update_in(cx_a, |workspace, window, cx| {
1484            workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
1485        })
1486        .await
1487        .unwrap();
1488
1489    executor.run_until_parked();
1490    assert_eq!(visible_push_notifications(cx_b).len(), 1);
1491
1492    workspace_b.update_in(cx_b, |workspace, window, cx| {
1493        workspace.follow(client_a.peer_id().unwrap(), window, cx)
1494    });
1495
1496    executor.run_until_parked();
1497    let window_b_project_a = *cx_b
1498        .windows()
1499        .iter()
1500        .max_by_key(|window| window.window_id())
1501        .unwrap();
1502
1503    let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
1504
1505    let workspace_b_project_a = window_b_project_a
1506        .downcast::<Workspace>()
1507        .unwrap()
1508        .root(cx_b)
1509        .unwrap();
1510
1511    // assert that b is following a in project a in w.rs
1512    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1513        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1514        assert_eq!(
1515            client_a.peer_id().map(Into::into),
1516            workspace.leader_for_pane(workspace.active_pane())
1517        );
1518        let item = workspace.active_item(cx).unwrap();
1519        assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
1520    });
1521
1522    // TODO: in app code, this would be done by the collab_ui.
1523    active_call_b
1524        .update(&mut cx_b2, |call, cx| {
1525            let project = workspace_b_project_a.read(cx).project().clone();
1526            call.set_location(Some(&project), cx)
1527        })
1528        .await
1529        .unwrap();
1530
1531    // assert that there are no share notifications open
1532    assert_eq!(visible_push_notifications(cx_b).len(), 0);
1533
1534    // b moves to x.rs in a's project, and a follows
1535    workspace_b_project_a
1536        .update_in(&mut cx_b2, |workspace, window, cx| {
1537            workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
1538        })
1539        .await
1540        .unwrap();
1541
1542    executor.run_until_parked();
1543    workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1544        let item = workspace.active_item(cx).unwrap();
1545        assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
1546    });
1547
1548    workspace_a.update_in(cx_a, |workspace, window, cx| {
1549        workspace.follow(client_b.peer_id().unwrap(), window, cx)
1550    });
1551
1552    executor.run_until_parked();
1553    workspace_a.update(cx_a, |workspace, cx| {
1554        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1555        assert_eq!(
1556            client_b.peer_id().map(Into::into),
1557            workspace.leader_for_pane(workspace.active_pane())
1558        );
1559        let item = workspace.active_pane().read(cx).active_item().unwrap();
1560        assert_eq!(item.tab_content_text(0, cx), "x.rs");
1561    });
1562
1563    // b moves to y.rs in b's project, a is still following but can't yet see
1564    workspace_b
1565        .update_in(cx_b, |workspace, window, cx| {
1566            workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
1567        })
1568        .await
1569        .unwrap();
1570
1571    // TODO: in app code, this would be done by the collab_ui.
1572    active_call_b
1573        .update(cx_b, |call, cx| {
1574            let project = workspace_b.read(cx).project().clone();
1575            call.set_location(Some(&project), cx)
1576        })
1577        .await
1578        .unwrap();
1579
1580    let project_b_id = active_call_b
1581        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1582        .await
1583        .unwrap();
1584
1585    executor.run_until_parked();
1586    assert_eq!(visible_push_notifications(cx_a).len(), 1);
1587    cx_a.update(|_, cx| {
1588        workspace::join_in_room_project(
1589            project_b_id,
1590            client_b.user_id().unwrap(),
1591            client_a.app_state.clone(),
1592            cx,
1593        )
1594    })
1595    .await
1596    .unwrap();
1597
1598    executor.run_until_parked();
1599
1600    assert_eq!(visible_push_notifications(cx_a).len(), 0);
1601    let window_a_project_b = *cx_a
1602        .windows()
1603        .iter()
1604        .max_by_key(|window| window.window_id())
1605        .unwrap();
1606    let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1607    let workspace_a_project_b = window_a_project_b
1608        .downcast::<Workspace>()
1609        .unwrap()
1610        .root(cx_a)
1611        .unwrap();
1612
1613    executor.run_until_parked();
1614
1615    workspace_a_project_b.update(cx_a2, |workspace, cx| {
1616        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1617        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1618        assert_eq!(
1619            client_b.peer_id().map(Into::into),
1620            workspace.leader_for_pane(workspace.active_pane())
1621        );
1622        let item = workspace.active_item(cx).unwrap();
1623        assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1624    });
1625}
1626
1627#[gpui::test]
1628async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1629    let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1630
1631    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1632    client_a
1633        .host_workspace(&workspace_a, channel_id, cx_a)
1634        .await;
1635    let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1636
1637    cx_a.simulate_keystrokes("cmd-p");
1638    cx_a.run_until_parked();
1639    cx_a.simulate_keystrokes("2 enter");
1640
1641    let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1642        workspace.active_item_as::<Editor>(cx).unwrap()
1643    });
1644    let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1645        workspace.active_item_as::<Editor>(cx).unwrap()
1646    });
1647
1648    // b should follow a to position 1
1649    editor_a.update_in(cx_a, |editor, window, cx| {
1650        editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
1651    });
1652    cx_a.executor()
1653        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1654    cx_a.run_until_parked();
1655    editor_b.update(cx_b, |editor, cx| {
1656        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1657    });
1658
1659    // a unshares the project
1660    cx_a.update(|_, cx| {
1661        let project = workspace_a.read(cx).project().clone();
1662        ActiveCall::global(cx).update(cx, |call, cx| {
1663            call.unshare_project(project, cx).unwrap();
1664        })
1665    });
1666    cx_a.run_until_parked();
1667
1668    // b should not follow a to position 2
1669    editor_a.update_in(cx_a, |editor, window, cx| {
1670        editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
1671    });
1672    cx_a.executor()
1673        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1674    cx_a.run_until_parked();
1675    editor_b.update(cx_b, |editor, cx| {
1676        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1677    });
1678    cx_b.update(|_, cx| {
1679        let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1680        let participant = room.remote_participants().get(&client_a.id()).unwrap();
1681        assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1682    })
1683}
1684
1685#[gpui::test]
1686async fn test_following_into_excluded_file(
1687    mut cx_a: &mut TestAppContext,
1688    mut cx_b: &mut TestAppContext,
1689) {
1690    let executor = cx_a.executor();
1691    let mut server = TestServer::start(executor.clone()).await;
1692    let client_a = server.create_client(cx_a, "user_a").await;
1693    let client_b = server.create_client(cx_b, "user_b").await;
1694    for cx in [&mut cx_a, &mut cx_b] {
1695        cx.update(|cx| {
1696            cx.update_global::<SettingsStore, _>(|store, cx| {
1697                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
1698                    settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1699                });
1700            });
1701        });
1702    }
1703    server
1704        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1705        .await;
1706    let active_call_a = cx_a.read(ActiveCall::global);
1707    let active_call_b = cx_b.read(ActiveCall::global);
1708    let peer_id_a = client_a.peer_id().unwrap();
1709
1710    client_a
1711        .fs()
1712        .insert_tree(
1713            path!("/a"),
1714            json!({
1715                ".git": {
1716                    "COMMIT_EDITMSG": "write your commit message here",
1717                },
1718                "1.txt": "one\none\none",
1719                "2.txt": "two\ntwo\ntwo",
1720                "3.txt": "three\nthree\nthree",
1721            }),
1722        )
1723        .await;
1724    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1725    active_call_a
1726        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1727        .await
1728        .unwrap();
1729
1730    let project_id = active_call_a
1731        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1732        .await
1733        .unwrap();
1734    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1735    active_call_b
1736        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1737        .await
1738        .unwrap();
1739
1740    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1741    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1742
1743    // Client A opens editors for a regular file and an excluded file.
1744    let editor_for_regular = workspace_a
1745        .update_in(cx_a, |workspace, window, cx| {
1746            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1747        })
1748        .await
1749        .unwrap()
1750        .downcast::<Editor>()
1751        .unwrap();
1752    let editor_for_excluded_a = workspace_a
1753        .update_in(cx_a, |workspace, window, cx| {
1754            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
1755        })
1756        .await
1757        .unwrap()
1758        .downcast::<Editor>()
1759        .unwrap();
1760
1761    // Client A updates their selections in those editors
1762    editor_for_regular.update_in(cx_a, |editor, window, cx| {
1763        editor.handle_input("a", window, cx);
1764        editor.handle_input("b", window, cx);
1765        editor.handle_input("c", window, cx);
1766        editor.select_left(&Default::default(), window, cx);
1767        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1768    });
1769    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1770        editor.select_all(&Default::default(), window, cx);
1771        editor.handle_input("new commit message", window, cx);
1772        editor.select_left(&Default::default(), window, cx);
1773        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1774    });
1775
1776    // When client B starts following client A, currently visible file is replicated
1777    workspace_b.update_in(cx_b, |workspace, window, cx| {
1778        workspace.follow(peer_id_a, window, cx)
1779    });
1780    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1781    executor.run_until_parked();
1782
1783    let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1784        workspace
1785            .active_item(cx)
1786            .unwrap()
1787            .downcast::<Editor>()
1788            .unwrap()
1789    });
1790    assert_eq!(
1791        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1792        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1793    );
1794    assert_eq!(
1795        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1796        vec![18..17]
1797    );
1798
1799    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1800        editor.select_right(&Default::default(), window, cx);
1801    });
1802    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1803    executor.run_until_parked();
1804
1805    // Changes from B to the excluded file are replicated in A's editor
1806    editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1807        editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1808    });
1809    executor.run_until_parked();
1810    editor_for_excluded_a.update(cx_a, |editor, cx| {
1811        assert_eq!(
1812            editor.text(cx),
1813            "new commit message\nCo-Authored-By: B <b@b.b>"
1814        );
1815    });
1816}
1817
1818fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1819    let mut ret = Vec::new();
1820    for window in cx.windows() {
1821        window
1822            .update(cx, |window, _, _| {
1823                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1824                    ret.push(handle)
1825                }
1826            })
1827            .unwrap();
1828    }
1829    ret
1830}
1831
1832#[derive(Debug, PartialEq, Eq)]
1833struct PaneSummary {
1834    active: bool,
1835    leader: Option<PeerId>,
1836    items: Vec<(bool, String)>,
1837}
1838
1839fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1840    cx.read(|cx| {
1841        let active_call = ActiveCall::global(cx).read(cx);
1842        let peer_id = active_call.client().peer_id();
1843        let room = active_call.room().unwrap().read(cx);
1844        let mut result = room
1845            .remote_participants()
1846            .values()
1847            .map(|participant| participant.peer_id)
1848            .chain(peer_id)
1849            .filter_map(|peer_id| {
1850                let followers = room.followers_for(peer_id, project_id);
1851                if followers.is_empty() {
1852                    None
1853                } else {
1854                    Some((peer_id, followers.to_vec()))
1855                }
1856            })
1857            .collect::<Vec<_>>();
1858        result.sort_by_key(|e| e.0);
1859        result
1860    })
1861}
1862
1863fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1864    workspace.update(cx, |workspace, cx| {
1865        let active_pane = workspace.active_pane();
1866        workspace
1867            .panes()
1868            .iter()
1869            .map(|pane| {
1870                let leader = match workspace.leader_for_pane(pane) {
1871                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1872                    Some(CollaboratorId::Agent) => unimplemented!(),
1873                    None => None,
1874                };
1875                let active = pane == active_pane;
1876                let pane = pane.read(cx);
1877                let active_ix = pane.active_item_index();
1878                PaneSummary {
1879                    active,
1880                    leader,
1881                    items: pane
1882                        .items()
1883                        .enumerate()
1884                        .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1885                        .collect(),
1886                }
1887            })
1888            .collect()
1889    })
1890}
1891
1892#[gpui::test(iterations = 10)]
1893async fn test_following_to_channel_notes_without_a_shared_project(
1894    deterministic: BackgroundExecutor,
1895    mut cx_a: &mut TestAppContext,
1896    mut cx_b: &mut TestAppContext,
1897    mut cx_c: &mut TestAppContext,
1898) {
1899    let mut server = TestServer::start(deterministic.clone()).await;
1900    let client_a = server.create_client(cx_a, "user_a").await;
1901    let client_b = server.create_client(cx_b, "user_b").await;
1902    let client_c = server.create_client(cx_c, "user_c").await;
1903
1904    cx_a.update(editor::init);
1905    cx_b.update(editor::init);
1906    cx_c.update(editor::init);
1907    cx_a.update(collab_ui::channel_view::init);
1908    cx_b.update(collab_ui::channel_view::init);
1909    cx_c.update(collab_ui::channel_view::init);
1910
1911    let channel_1_id = server
1912        .make_channel(
1913            "channel-1",
1914            None,
1915            (&client_a, cx_a),
1916            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1917        )
1918        .await;
1919    let channel_2_id = server
1920        .make_channel(
1921            "channel-2",
1922            None,
1923            (&client_a, cx_a),
1924            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1925        )
1926        .await;
1927
1928    // Clients A, B, and C join a channel.
1929    let active_call_a = cx_a.read(ActiveCall::global);
1930    let active_call_b = cx_b.read(ActiveCall::global);
1931    let active_call_c = cx_c.read(ActiveCall::global);
1932    for (call, cx) in [
1933        (&active_call_a, &mut cx_a),
1934        (&active_call_b, &mut cx_b),
1935        (&active_call_c, &mut cx_c),
1936    ] {
1937        call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1938            .await
1939            .unwrap();
1940    }
1941    deterministic.run_until_parked();
1942
1943    // Clients A, B, and C all open their own unshared projects.
1944    client_a
1945        .fs()
1946        .insert_tree("/a", json!({ "1.txt": "" }))
1947        .await;
1948    client_b.fs().insert_tree("/b", json!({})).await;
1949    client_c.fs().insert_tree("/c", json!({})).await;
1950    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1951    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1952    let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1953    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1954    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1955    let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1956
1957    active_call_a
1958        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1959        .await
1960        .unwrap();
1961
1962    // Client A opens the notes for channel 1.
1963    let channel_notes_1_a = cx_a
1964        .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1965        .await
1966        .unwrap();
1967    channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1968        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1969        notes.editor.update(cx, |editor, cx| {
1970            editor.insert("Hello from A.", window, cx);
1971            editor.change_selections(None, window, cx, |selections| {
1972                selections.select_ranges(vec![3..4]);
1973            });
1974        });
1975    });
1976
1977    // Client B follows client A.
1978    workspace_b
1979        .update_in(cx_b, |workspace, window, cx| {
1980            workspace
1981                .start_following(client_a.peer_id().unwrap(), window, cx)
1982                .unwrap()
1983        })
1984        .await
1985        .unwrap();
1986
1987    // Client B is taken to the notes for channel 1, with the same
1988    // text selected as client A.
1989    deterministic.run_until_parked();
1990    let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1991        assert_eq!(
1992            workspace.leader_for_pane(workspace.active_pane()),
1993            Some(client_a.peer_id().unwrap().into())
1994        );
1995        workspace
1996            .active_item(cx)
1997            .expect("no active item")
1998            .downcast::<ChannelView>()
1999            .expect("active item is not a channel view")
2000    });
2001    channel_notes_1_b.update(cx_b, |notes, cx| {
2002        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2003        notes.editor.update(cx, |editor, cx| {
2004            assert_eq!(editor.text(cx), "Hello from A.");
2005            assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2006        })
2007    });
2008
2009    //  Client A opens the notes for channel 2.
2010    let channel_notes_2_a = cx_a
2011        .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2012        .await
2013        .unwrap();
2014    channel_notes_2_a.update(cx_a, |notes, cx| {
2015        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2016    });
2017
2018    // Client B is taken to the notes for channel 2.
2019    deterministic.run_until_parked();
2020    let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2021        assert_eq!(
2022            workspace.leader_for_pane(workspace.active_pane()),
2023            Some(client_a.peer_id().unwrap().into())
2024        );
2025        workspace
2026            .active_item(cx)
2027            .expect("no active item")
2028            .downcast::<ChannelView>()
2029            .expect("active item is not a channel view")
2030    });
2031    channel_notes_2_b.update(cx_b, |notes, cx| {
2032        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2033    });
2034
2035    // Client A opens a local buffer in their unshared project.
2036    let _unshared_editor_a1 = workspace_a
2037        .update_in(cx_a, |workspace, window, cx| {
2038            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
2039        })
2040        .await
2041        .unwrap()
2042        .downcast::<Editor>()
2043        .unwrap();
2044
2045    // This does not send any leader update message to client B.
2046    // If it did, an error would occur on client B, since this buffer
2047    // is not shared with them.
2048    deterministic.run_until_parked();
2049    workspace_b.update(cx_b, |workspace, cx| {
2050        assert_eq!(
2051            workspace.active_item(cx).expect("no active item").item_id(),
2052            channel_notes_2_b.entity_id()
2053        );
2054    });
2055}
2056
2057pub(crate) async fn join_channel(
2058    channel_id: ChannelId,
2059    client: &TestClient,
2060    cx: &mut TestAppContext,
2061) -> anyhow::Result<()> {
2062    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2063        .await
2064}
2065
2066async fn share_workspace(
2067    workspace: &Entity<Workspace>,
2068    cx: &mut VisualTestContext,
2069) -> anyhow::Result<u64> {
2070    let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2071    cx.read(ActiveCall::global)
2072        .update(cx, |call, cx| call.share_project(project, cx))
2073        .await
2074}
2075
2076#[gpui::test]
2077async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2078    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2079
2080    let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2081    join_channel(channel, &client_a, cx_a).await.unwrap();
2082    share_workspace(&workspace, cx_a).await.unwrap();
2083    let buffer = workspace.update(cx_a, |workspace, cx| {
2084        workspace.project().update(cx, |project, cx| {
2085            project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
2086        })
2087    });
2088    let multibuffer = cx_a.new(|cx| {
2089        let mut mb = MultiBuffer::new(Capability::ReadWrite);
2090        mb.set_excerpts_for_path(
2091            PathKey::for_buffer(&buffer, cx),
2092            buffer.clone(),
2093            [Point::row_range(1..1), Point::row_range(5..5)],
2094            1,
2095            cx,
2096        );
2097        mb
2098    });
2099    let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2100    let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2101        Editor::for_multibuffer(
2102            multibuffer.clone(),
2103            Some(workspace.read(cx).project().clone()),
2104            window,
2105            cx,
2106        )
2107    });
2108    workspace.update_in(cx_a, |workspace, window, cx| {
2109        workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2110    });
2111    editor.update_in(cx_a, |editor, window, cx| {
2112        editor.change_selections(None, window, cx, |s| {
2113            s.select_ranges([Point::row_range(4..4)]);
2114        })
2115    });
2116    let positions = editor.update(cx_a, |editor, _| {
2117        editor
2118            .selections
2119            .disjoint_anchor_ranges()
2120            .map(|range| range.start.text_anchor.to_point(&snapshot))
2121            .collect::<Vec<_>>()
2122    });
2123    multibuffer.update(cx_a, |multibuffer, cx| {
2124        multibuffer.set_excerpts_for_path(
2125            PathKey::for_buffer(&buffer, cx),
2126            buffer,
2127            [Point::row_range(1..5)],
2128            1,
2129            cx,
2130        );
2131    });
2132
2133    let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2134    cx_b.run_until_parked();
2135    let editor_b = workspace_b
2136        .update(cx_b, |workspace, cx| {
2137            workspace
2138                .active_item(cx)
2139                .and_then(|item| item.downcast::<Editor>())
2140        })
2141        .unwrap();
2142
2143    let new_positions = editor_b.update(cx_b, |editor, _| {
2144        editor
2145            .selections
2146            .disjoint_anchor_ranges()
2147            .map(|range| range.start.text_anchor.to_point(&snapshot))
2148            .collect::<Vec<_>>()
2149    });
2150    assert_eq!(positions, new_positions);
2151}
2152
2153#[gpui::test]
2154async fn test_following_to_channel_notes_other_workspace(
2155    cx_a: &mut TestAppContext,
2156    cx_b: &mut TestAppContext,
2157) {
2158    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2159
2160    let mut cx_a2 = cx_a.clone();
2161    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2162    join_channel(channel, &client_a, cx_a).await.unwrap();
2163    share_workspace(&workspace_a, cx_a).await.unwrap();
2164
2165    // a opens 1.txt
2166    cx_a.simulate_keystrokes("cmd-p");
2167    cx_a.run_until_parked();
2168    cx_a.simulate_keystrokes("1 enter");
2169    cx_a.run_until_parked();
2170    workspace_a.update(cx_a, |workspace, cx| {
2171        let editor = workspace.active_item(cx).unwrap();
2172        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2173    });
2174
2175    // b joins channel and is following a
2176    join_channel(channel, &client_b, cx_b).await.unwrap();
2177    cx_b.run_until_parked();
2178    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2179    workspace_b.update(cx_b, |workspace, cx| {
2180        let editor = workspace.active_item(cx).unwrap();
2181        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2182    });
2183
2184    // a opens a second workspace and the channel notes
2185    let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2186    cx_a2.update(|window, _| window.activate_window());
2187    cx_a2
2188        .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2189        .await
2190        .unwrap();
2191    cx_a2.run_until_parked();
2192
2193    // b should follow a to the channel notes
2194    workspace_b.update(cx_b, |workspace, cx| {
2195        let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2196        assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2197    });
2198
2199    // a returns to the shared project
2200    cx_a.update(|window, _| window.activate_window());
2201    cx_a.run_until_parked();
2202
2203    workspace_a.update(cx_a, |workspace, cx| {
2204        let editor = workspace.active_item(cx).unwrap();
2205        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2206    });
2207
2208    // b should follow a back
2209    workspace_b.update(cx_b, |workspace, cx| {
2210        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2211        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2212    });
2213}
2214
2215#[gpui::test]
2216async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2217    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2218
2219    let mut cx_a2 = cx_a.clone();
2220    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2221    join_channel(channel, &client_a, cx_a).await.unwrap();
2222    share_workspace(&workspace_a, cx_a).await.unwrap();
2223
2224    // a opens 1.txt
2225    cx_a.simulate_keystrokes("cmd-p");
2226    cx_a.run_until_parked();
2227    cx_a.simulate_keystrokes("1 enter");
2228    cx_a.run_until_parked();
2229    workspace_a.update(cx_a, |workspace, cx| {
2230        let editor = workspace.active_item(cx).unwrap();
2231        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2232    });
2233
2234    // b joins channel and is following a
2235    join_channel(channel, &client_b, cx_b).await.unwrap();
2236    cx_b.run_until_parked();
2237    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2238    workspace_b.update(cx_b, |workspace, cx| {
2239        let editor = workspace.active_item(cx).unwrap();
2240        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2241    });
2242
2243    // stop following
2244    cx_b.simulate_keystrokes("down");
2245
2246    // a opens a different file while not followed
2247    cx_a.simulate_keystrokes("cmd-p");
2248    cx_a.run_until_parked();
2249    cx_a.simulate_keystrokes("2 enter");
2250
2251    workspace_b.update(cx_b, |workspace, cx| {
2252        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2253        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2254    });
2255
2256    // a opens a file in a new window
2257    let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2258    cx_a2.update(|window, _| window.activate_window());
2259    cx_a2.simulate_keystrokes("cmd-p");
2260    cx_a2.run_until_parked();
2261    cx_a2.simulate_keystrokes("3 enter");
2262    cx_a2.run_until_parked();
2263
2264    // b starts following a again
2265    cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2266    cx_a.run_until_parked();
2267
2268    // a returns to the shared project
2269    cx_a.update(|window, _| window.activate_window());
2270    cx_a.run_until_parked();
2271
2272    workspace_a.update(cx_a, |workspace, cx| {
2273        let editor = workspace.active_item(cx).unwrap();
2274        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2275    });
2276
2277    // b should follow a back
2278    workspace_b.update(cx_b, |workspace, cx| {
2279        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2280        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2281    });
2282}