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