following_tests.rs

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