following_tests.rs

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