following_tests.rs

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