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