following_tests.rs

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