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