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