following_tests.rs

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