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