following_tests.rs

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