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    workspace_a_project_b.update(cx_a2, |workspace, cx| {
1614        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1615        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1616        assert_eq!(
1617            client_b.peer_id().map(Into::into),
1618            workspace.leader_for_pane(workspace.active_pane())
1619        );
1620        let item = workspace.active_item(cx).unwrap();
1621        assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1622    });
1623}
1624
1625#[gpui::test]
1626async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1627    let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1628
1629    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1630    client_a
1631        .host_workspace(&workspace_a, channel_id, cx_a)
1632        .await;
1633    let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1634
1635    cx_a.simulate_keystrokes("cmd-p");
1636    cx_a.run_until_parked();
1637    cx_a.simulate_keystrokes("2 enter");
1638
1639    let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1640        workspace.active_item_as::<Editor>(cx).unwrap()
1641    });
1642    let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1643        workspace.active_item_as::<Editor>(cx).unwrap()
1644    });
1645
1646    // b should follow a to position 1
1647    editor_a.update_in(cx_a, |editor, window, cx| {
1648        editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
1649    });
1650    cx_a.executor()
1651        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1652    cx_a.run_until_parked();
1653    editor_b.update(cx_b, |editor, cx| {
1654        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1655    });
1656
1657    // a unshares the project
1658    cx_a.update(|_, cx| {
1659        let project = workspace_a.read(cx).project().clone();
1660        ActiveCall::global(cx).update(cx, |call, cx| {
1661            call.unshare_project(project, cx).unwrap();
1662        })
1663    });
1664    cx_a.run_until_parked();
1665
1666    // b should not follow a to position 2
1667    editor_a.update_in(cx_a, |editor, window, cx| {
1668        editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
1669    });
1670    cx_a.executor()
1671        .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1672    cx_a.run_until_parked();
1673    editor_b.update(cx_b, |editor, cx| {
1674        assert_eq!(editor.selections.ranges(cx), vec![1..1])
1675    });
1676    cx_b.update(|_, cx| {
1677        let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1678        let participant = room.remote_participants().get(&client_a.id()).unwrap();
1679        assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1680    })
1681}
1682
1683#[gpui::test]
1684async fn test_following_into_excluded_file(
1685    mut cx_a: &mut TestAppContext,
1686    mut cx_b: &mut TestAppContext,
1687) {
1688    let executor = cx_a.executor();
1689    let mut server = TestServer::start(executor.clone()).await;
1690    let client_a = server.create_client(cx_a, "user_a").await;
1691    let client_b = server.create_client(cx_b, "user_b").await;
1692    for cx in [&mut cx_a, &mut cx_b] {
1693        cx.update(|cx| {
1694            cx.update_global::<SettingsStore, _>(|store, cx| {
1695                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
1696                    settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1697                });
1698            });
1699        });
1700    }
1701    server
1702        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1703        .await;
1704    let active_call_a = cx_a.read(ActiveCall::global);
1705    let active_call_b = cx_b.read(ActiveCall::global);
1706    let peer_id_a = client_a.peer_id().unwrap();
1707
1708    client_a
1709        .fs()
1710        .insert_tree(
1711            path!("/a"),
1712            json!({
1713                ".git": {
1714                    "COMMIT_EDITMSG": "write your commit message here",
1715                },
1716                "1.txt": "one\none\none",
1717                "2.txt": "two\ntwo\ntwo",
1718                "3.txt": "three\nthree\nthree",
1719            }),
1720        )
1721        .await;
1722    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1723    active_call_a
1724        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1725        .await
1726        .unwrap();
1727
1728    let project_id = active_call_a
1729        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1730        .await
1731        .unwrap();
1732    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1733    active_call_b
1734        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1735        .await
1736        .unwrap();
1737
1738    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1739    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1740
1741    // Client A opens editors for a regular file and an excluded file.
1742    let editor_for_regular = workspace_a
1743        .update_in(cx_a, |workspace, window, cx| {
1744            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1745        })
1746        .await
1747        .unwrap()
1748        .downcast::<Editor>()
1749        .unwrap();
1750    let editor_for_excluded_a = workspace_a
1751        .update_in(cx_a, |workspace, window, cx| {
1752            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
1753        })
1754        .await
1755        .unwrap()
1756        .downcast::<Editor>()
1757        .unwrap();
1758
1759    // Client A updates their selections in those editors
1760    editor_for_regular.update_in(cx_a, |editor, window, cx| {
1761        editor.handle_input("a", window, cx);
1762        editor.handle_input("b", window, cx);
1763        editor.handle_input("c", window, cx);
1764        editor.select_left(&Default::default(), window, cx);
1765        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1766    });
1767    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1768        editor.select_all(&Default::default(), window, cx);
1769        editor.handle_input("new commit message", window, cx);
1770        editor.select_left(&Default::default(), window, cx);
1771        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1772    });
1773
1774    // When client B starts following client A, currently visible file is replicated
1775    workspace_b.update_in(cx_b, |workspace, window, cx| {
1776        workspace.follow(peer_id_a, window, cx)
1777    });
1778    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1779    executor.run_until_parked();
1780
1781    let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1782        workspace
1783            .active_item(cx)
1784            .unwrap()
1785            .downcast::<Editor>()
1786            .unwrap()
1787    });
1788    assert_eq!(
1789        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1790        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1791    );
1792    assert_eq!(
1793        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1794        vec![18..17]
1795    );
1796
1797    editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1798        editor.select_right(&Default::default(), window, cx);
1799    });
1800    executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1801    executor.run_until_parked();
1802
1803    // Changes from B to the excluded file are replicated in A's editor
1804    editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1805        editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1806    });
1807    executor.run_until_parked();
1808    editor_for_excluded_a.update(cx_a, |editor, cx| {
1809        assert_eq!(
1810            editor.text(cx),
1811            "new commit message\nCo-Authored-By: B <b@b.b>"
1812        );
1813    });
1814}
1815
1816fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1817    let mut ret = Vec::new();
1818    for window in cx.windows() {
1819        window
1820            .update(cx, |window, _, _| {
1821                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1822                    ret.push(handle)
1823                }
1824            })
1825            .unwrap();
1826    }
1827    ret
1828}
1829
1830#[derive(Debug, PartialEq, Eq)]
1831struct PaneSummary {
1832    active: bool,
1833    leader: Option<PeerId>,
1834    items: Vec<(bool, String)>,
1835}
1836
1837fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1838    cx.read(|cx| {
1839        let active_call = ActiveCall::global(cx).read(cx);
1840        let peer_id = active_call.client().peer_id();
1841        let room = active_call.room().unwrap().read(cx);
1842        let mut result = room
1843            .remote_participants()
1844            .values()
1845            .map(|participant| participant.peer_id)
1846            .chain(peer_id)
1847            .filter_map(|peer_id| {
1848                let followers = room.followers_for(peer_id, project_id);
1849                if followers.is_empty() {
1850                    None
1851                } else {
1852                    Some((peer_id, followers.to_vec()))
1853                }
1854            })
1855            .collect::<Vec<_>>();
1856        result.sort_by_key(|e| e.0);
1857        result
1858    })
1859}
1860
1861fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1862    workspace.update(cx, |workspace, cx| {
1863        let active_pane = workspace.active_pane();
1864        workspace
1865            .panes()
1866            .iter()
1867            .map(|pane| {
1868                let leader = match workspace.leader_for_pane(pane) {
1869                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1870                    Some(CollaboratorId::Agent) => unimplemented!(),
1871                    None => None,
1872                };
1873                let active = pane == active_pane;
1874                let pane = pane.read(cx);
1875                let active_ix = pane.active_item_index();
1876                PaneSummary {
1877                    active,
1878                    leader,
1879                    items: pane
1880                        .items()
1881                        .enumerate()
1882                        .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1883                        .collect(),
1884                }
1885            })
1886            .collect()
1887    })
1888}
1889
1890#[gpui::test(iterations = 10)]
1891async fn test_following_to_channel_notes_without_a_shared_project(
1892    deterministic: BackgroundExecutor,
1893    mut cx_a: &mut TestAppContext,
1894    mut cx_b: &mut TestAppContext,
1895    mut cx_c: &mut TestAppContext,
1896) {
1897    let mut server = TestServer::start(deterministic.clone()).await;
1898    let client_a = server.create_client(cx_a, "user_a").await;
1899    let client_b = server.create_client(cx_b, "user_b").await;
1900    let client_c = server.create_client(cx_c, "user_c").await;
1901
1902    cx_a.update(editor::init);
1903    cx_b.update(editor::init);
1904    cx_c.update(editor::init);
1905    cx_a.update(collab_ui::channel_view::init);
1906    cx_b.update(collab_ui::channel_view::init);
1907    cx_c.update(collab_ui::channel_view::init);
1908
1909    let channel_1_id = server
1910        .make_channel(
1911            "channel-1",
1912            None,
1913            (&client_a, cx_a),
1914            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1915        )
1916        .await;
1917    let channel_2_id = server
1918        .make_channel(
1919            "channel-2",
1920            None,
1921            (&client_a, cx_a),
1922            &mut [(&client_b, cx_b), (&client_c, cx_c)],
1923        )
1924        .await;
1925
1926    // Clients A, B, and C join a channel.
1927    let active_call_a = cx_a.read(ActiveCall::global);
1928    let active_call_b = cx_b.read(ActiveCall::global);
1929    let active_call_c = cx_c.read(ActiveCall::global);
1930    for (call, cx) in [
1931        (&active_call_a, &mut cx_a),
1932        (&active_call_b, &mut cx_b),
1933        (&active_call_c, &mut cx_c),
1934    ] {
1935        call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1936            .await
1937            .unwrap();
1938    }
1939    deterministic.run_until_parked();
1940
1941    // Clients A, B, and C all open their own unshared projects.
1942    client_a
1943        .fs()
1944        .insert_tree("/a", json!({ "1.txt": "" }))
1945        .await;
1946    client_b.fs().insert_tree("/b", json!({})).await;
1947    client_c.fs().insert_tree("/c", json!({})).await;
1948    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1949    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1950    let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1951    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1952    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1953    let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1954
1955    active_call_a
1956        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1957        .await
1958        .unwrap();
1959
1960    // Client A opens the notes for channel 1.
1961    let channel_notes_1_a = cx_a
1962        .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1963        .await
1964        .unwrap();
1965    channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1966        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1967        notes.editor.update(cx, |editor, cx| {
1968            editor.insert("Hello from A.", window, cx);
1969            editor.change_selections(None, window, cx, |selections| {
1970                selections.select_ranges(vec![3..4]);
1971            });
1972        });
1973    });
1974
1975    // Client B follows client A.
1976    workspace_b
1977        .update_in(cx_b, |workspace, window, cx| {
1978            workspace
1979                .start_following(client_a.peer_id().unwrap(), window, cx)
1980                .unwrap()
1981        })
1982        .await
1983        .unwrap();
1984
1985    // Client B is taken to the notes for channel 1, with the same
1986    // text selected as client A.
1987    deterministic.run_until_parked();
1988    let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1989        assert_eq!(
1990            workspace.leader_for_pane(workspace.active_pane()),
1991            Some(client_a.peer_id().unwrap().into())
1992        );
1993        workspace
1994            .active_item(cx)
1995            .expect("no active item")
1996            .downcast::<ChannelView>()
1997            .expect("active item is not a channel view")
1998    });
1999    channel_notes_1_b.update(cx_b, |notes, cx| {
2000        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2001        notes.editor.update(cx, |editor, cx| {
2002            assert_eq!(editor.text(cx), "Hello from A.");
2003            assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2004        })
2005    });
2006
2007    //  Client A opens the notes for channel 2.
2008    let channel_notes_2_a = cx_a
2009        .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2010        .await
2011        .unwrap();
2012    channel_notes_2_a.update(cx_a, |notes, cx| {
2013        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2014    });
2015
2016    // Client B is taken to the notes for channel 2.
2017    deterministic.run_until_parked();
2018    let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2019        assert_eq!(
2020            workspace.leader_for_pane(workspace.active_pane()),
2021            Some(client_a.peer_id().unwrap().into())
2022        );
2023        workspace
2024            .active_item(cx)
2025            .expect("no active item")
2026            .downcast::<ChannelView>()
2027            .expect("active item is not a channel view")
2028    });
2029    channel_notes_2_b.update(cx_b, |notes, cx| {
2030        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2031    });
2032
2033    // Client A opens a local buffer in their unshared project.
2034    let _unshared_editor_a1 = workspace_a
2035        .update_in(cx_a, |workspace, window, cx| {
2036            workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
2037        })
2038        .await
2039        .unwrap()
2040        .downcast::<Editor>()
2041        .unwrap();
2042
2043    // This does not send any leader update message to client B.
2044    // If it did, an error would occur on client B, since this buffer
2045    // is not shared with them.
2046    deterministic.run_until_parked();
2047    workspace_b.update(cx_b, |workspace, cx| {
2048        assert_eq!(
2049            workspace.active_item(cx).expect("no active item").item_id(),
2050            channel_notes_2_b.entity_id()
2051        );
2052    });
2053}
2054
2055pub(crate) async fn join_channel(
2056    channel_id: ChannelId,
2057    client: &TestClient,
2058    cx: &mut TestAppContext,
2059) -> anyhow::Result<()> {
2060    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2061        .await
2062}
2063
2064async fn share_workspace(
2065    workspace: &Entity<Workspace>,
2066    cx: &mut VisualTestContext,
2067) -> anyhow::Result<u64> {
2068    let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2069    cx.read(ActiveCall::global)
2070        .update(cx, |call, cx| call.share_project(project, cx))
2071        .await
2072}
2073
2074#[gpui::test]
2075async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2076    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2077
2078    let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2079    join_channel(channel, &client_a, cx_a).await.unwrap();
2080    share_workspace(&workspace, cx_a).await.unwrap();
2081    let buffer = workspace.update(cx_a, |workspace, cx| {
2082        workspace.project().update(cx, |project, cx| {
2083            project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
2084        })
2085    });
2086    let multibuffer = cx_a.new(|cx| {
2087        let mut mb = MultiBuffer::new(Capability::ReadWrite);
2088        mb.set_excerpts_for_path(
2089            PathKey::for_buffer(&buffer, cx),
2090            buffer.clone(),
2091            [Point::row_range(1..1), Point::row_range(5..5)],
2092            1,
2093            cx,
2094        );
2095        mb
2096    });
2097    let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2098    let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2099        Editor::for_multibuffer(
2100            multibuffer.clone(),
2101            Some(workspace.read(cx).project().clone()),
2102            window,
2103            cx,
2104        )
2105    });
2106    workspace.update_in(cx_a, |workspace, window, cx| {
2107        workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2108    });
2109    editor.update_in(cx_a, |editor, window, cx| {
2110        editor.change_selections(None, window, cx, |s| {
2111            s.select_ranges([Point::row_range(4..4)]);
2112        })
2113    });
2114    let positions = editor.update(cx_a, |editor, _| {
2115        editor
2116            .selections
2117            .disjoint_anchor_ranges()
2118            .map(|range| range.start.text_anchor.to_point(&snapshot))
2119            .collect::<Vec<_>>()
2120    });
2121    multibuffer.update(cx_a, |multibuffer, cx| {
2122        multibuffer.set_excerpts_for_path(
2123            PathKey::for_buffer(&buffer, cx),
2124            buffer,
2125            [Point::row_range(1..5)],
2126            1,
2127            cx,
2128        );
2129    });
2130
2131    let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2132    cx_b.run_until_parked();
2133    let editor_b = workspace_b
2134        .update(cx_b, |workspace, cx| {
2135            workspace
2136                .active_item(cx)
2137                .and_then(|item| item.downcast::<Editor>())
2138        })
2139        .unwrap();
2140
2141    let new_positions = editor_b.update(cx_b, |editor, _| {
2142        editor
2143            .selections
2144            .disjoint_anchor_ranges()
2145            .map(|range| range.start.text_anchor.to_point(&snapshot))
2146            .collect::<Vec<_>>()
2147    });
2148    assert_eq!(positions, new_positions);
2149}
2150
2151#[gpui::test]
2152async fn test_following_to_channel_notes_other_workspace(
2153    cx_a: &mut TestAppContext,
2154    cx_b: &mut TestAppContext,
2155) {
2156    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2157
2158    let mut cx_a2 = cx_a.clone();
2159    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2160    join_channel(channel, &client_a, cx_a).await.unwrap();
2161    share_workspace(&workspace_a, cx_a).await.unwrap();
2162
2163    // a opens 1.txt
2164    cx_a.simulate_keystrokes("cmd-p");
2165    cx_a.run_until_parked();
2166    cx_a.simulate_keystrokes("1 enter");
2167    cx_a.run_until_parked();
2168    workspace_a.update(cx_a, |workspace, cx| {
2169        let editor = workspace.active_item(cx).unwrap();
2170        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2171    });
2172
2173    // b joins channel and is following a
2174    join_channel(channel, &client_b, cx_b).await.unwrap();
2175    cx_b.run_until_parked();
2176    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2177    workspace_b.update(cx_b, |workspace, cx| {
2178        let editor = workspace.active_item(cx).unwrap();
2179        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2180    });
2181
2182    // a opens a second workspace and the channel notes
2183    let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2184    cx_a2.update(|window, _| window.activate_window());
2185    cx_a2
2186        .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2187        .await
2188        .unwrap();
2189    cx_a2.run_until_parked();
2190
2191    // b should follow a to the channel notes
2192    workspace_b.update(cx_b, |workspace, cx| {
2193        let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2194        assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2195    });
2196
2197    // a returns to the shared project
2198    cx_a.update(|window, _| window.activate_window());
2199    cx_a.run_until_parked();
2200
2201    workspace_a.update(cx_a, |workspace, cx| {
2202        let editor = workspace.active_item(cx).unwrap();
2203        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2204    });
2205
2206    // b should follow a back
2207    workspace_b.update(cx_b, |workspace, cx| {
2208        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2209        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2210    });
2211}
2212
2213#[gpui::test]
2214async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2215    let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2216
2217    let mut cx_a2 = cx_a.clone();
2218    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2219    join_channel(channel, &client_a, cx_a).await.unwrap();
2220    share_workspace(&workspace_a, cx_a).await.unwrap();
2221
2222    // a opens 1.txt
2223    cx_a.simulate_keystrokes("cmd-p");
2224    cx_a.run_until_parked();
2225    cx_a.simulate_keystrokes("1 enter");
2226    cx_a.run_until_parked();
2227    workspace_a.update(cx_a, |workspace, cx| {
2228        let editor = workspace.active_item(cx).unwrap();
2229        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2230    });
2231
2232    // b joins channel and is following a
2233    join_channel(channel, &client_b, cx_b).await.unwrap();
2234    cx_b.run_until_parked();
2235    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2236    workspace_b.update(cx_b, |workspace, cx| {
2237        let editor = workspace.active_item(cx).unwrap();
2238        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2239    });
2240
2241    // stop following
2242    cx_b.simulate_keystrokes("down");
2243
2244    // a opens a different file while not followed
2245    cx_a.simulate_keystrokes("cmd-p");
2246    cx_a.run_until_parked();
2247    cx_a.simulate_keystrokes("2 enter");
2248
2249    workspace_b.update(cx_b, |workspace, cx| {
2250        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2251        assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2252    });
2253
2254    // a opens a file in a new window
2255    let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2256    cx_a2.update(|window, _| window.activate_window());
2257    cx_a2.simulate_keystrokes("cmd-p");
2258    cx_a2.run_until_parked();
2259    cx_a2.simulate_keystrokes("3 enter");
2260    cx_a2.run_until_parked();
2261
2262    // b starts following a again
2263    cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2264    cx_a.run_until_parked();
2265
2266    // a returns to the shared project
2267    cx_a.update(|window, _| window.activate_window());
2268    cx_a.run_until_parked();
2269
2270    workspace_a.update(cx_a, |workspace, cx| {
2271        let editor = workspace.active_item(cx).unwrap();
2272        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2273    });
2274
2275    // b should follow a back
2276    workspace_b.update(cx_b, |workspace, cx| {
2277        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2278        assert_eq!(editor.tab_content_text(0, cx), "2.js");
2279    });
2280}