following_tests.rs

   1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
   2use call::ActiveCall;
   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_into_excluded_file(
1573    mut cx_a: &mut TestAppContext,
1574    mut cx_b: &mut TestAppContext,
1575) {
1576    let executor = cx_a.executor();
1577    let mut server = TestServer::start(executor.clone()).await;
1578    let client_a = server.create_client(cx_a, "user_a").await;
1579    let client_b = server.create_client(cx_b, "user_b").await;
1580    for cx in [&mut cx_a, &mut cx_b] {
1581        cx.update(|cx| {
1582            cx.update_global::<SettingsStore, _>(|store, cx| {
1583                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
1584                    project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1585                });
1586            });
1587        });
1588    }
1589    server
1590        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1591        .await;
1592    let active_call_a = cx_a.read(ActiveCall::global);
1593    let active_call_b = cx_b.read(ActiveCall::global);
1594    let peer_id_a = client_a.peer_id().unwrap();
1595
1596    cx_a.update(editor::init);
1597    cx_b.update(editor::init);
1598
1599    client_a
1600        .fs()
1601        .insert_tree(
1602            "/a",
1603            json!({
1604                ".git": {
1605                    "COMMIT_EDITMSG": "write your commit message here",
1606                },
1607                "1.txt": "one\none\none",
1608                "2.txt": "two\ntwo\ntwo",
1609                "3.txt": "three\nthree\nthree",
1610            }),
1611        )
1612        .await;
1613    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1614    active_call_a
1615        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1616        .await
1617        .unwrap();
1618
1619    let project_id = active_call_a
1620        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1621        .await
1622        .unwrap();
1623    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1624    active_call_b
1625        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1626        .await
1627        .unwrap();
1628
1629    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1630    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1631
1632    // Client A opens editors for a regular file and an excluded file.
1633    let editor_for_regular = workspace_a
1634        .update(cx_a, |workspace, cx| {
1635            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1636        })
1637        .await
1638        .unwrap()
1639        .downcast::<Editor>()
1640        .unwrap();
1641    let editor_for_excluded_a = workspace_a
1642        .update(cx_a, |workspace, cx| {
1643            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
1644        })
1645        .await
1646        .unwrap()
1647        .downcast::<Editor>()
1648        .unwrap();
1649
1650    // Client A updates their selections in those editors
1651    editor_for_regular.update(cx_a, |editor, cx| {
1652        editor.handle_input("a", cx);
1653        editor.handle_input("b", cx);
1654        editor.handle_input("c", cx);
1655        editor.select_left(&Default::default(), cx);
1656        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1657    });
1658    editor_for_excluded_a.update(cx_a, |editor, cx| {
1659        editor.select_all(&Default::default(), cx);
1660        editor.handle_input("new commit message", cx);
1661        editor.select_left(&Default::default(), cx);
1662        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1663    });
1664
1665    // When client B starts following client A, currently visible file is replicated
1666    workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
1667    executor.run_until_parked();
1668
1669    let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1670        workspace
1671            .active_item(cx)
1672            .unwrap()
1673            .downcast::<Editor>()
1674            .unwrap()
1675    });
1676    assert_eq!(
1677        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1678        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1679    );
1680    assert_eq!(
1681        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1682        vec![18..17]
1683    );
1684
1685    // Changes from B to the excluded file are replicated in A's editor
1686    editor_for_excluded_b.update(cx_b, |editor, cx| {
1687        editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
1688    });
1689    executor.run_until_parked();
1690    editor_for_excluded_a.update(cx_a, |editor, cx| {
1691        assert_eq!(
1692            editor.text(cx),
1693            "new commit messag\nCo-Authored-By: B <b@b.b>"
1694        );
1695    });
1696}
1697
1698fn visible_push_notifications(
1699    cx: &mut TestAppContext,
1700) -> Vec<gpui::View<ProjectSharedNotification>> {
1701    let mut ret = Vec::new();
1702    for window in cx.windows() {
1703        window
1704            .update(cx, |window, _| {
1705                if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1706                    ret.push(handle)
1707                }
1708            })
1709            .unwrap();
1710    }
1711    ret
1712}
1713
1714#[derive(Debug, PartialEq, Eq)]
1715struct PaneSummary {
1716    active: bool,
1717    leader: Option<PeerId>,
1718    items: Vec<(bool, String)>,
1719}
1720
1721fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1722    cx.read(|cx| {
1723        let active_call = ActiveCall::global(cx).read(cx);
1724        let peer_id = active_call.client().peer_id();
1725        let room = active_call.room().unwrap().read(cx);
1726        let mut result = room
1727            .remote_participants()
1728            .values()
1729            .map(|participant| participant.peer_id)
1730            .chain(peer_id)
1731            .filter_map(|peer_id| {
1732                let followers = room.followers_for(peer_id, project_id);
1733                if followers.is_empty() {
1734                    None
1735                } else {
1736                    Some((peer_id, followers.to_vec()))
1737                }
1738            })
1739            .collect::<Vec<_>>();
1740        result.sort_by_key(|e| e.0);
1741        result
1742    })
1743}
1744
1745fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1746    workspace.update(cx, |workspace, cx| {
1747        let active_pane = workspace.active_pane();
1748        workspace
1749            .panes()
1750            .iter()
1751            .map(|pane| {
1752                let leader = workspace.leader_for_pane(pane);
1753                let active = pane == active_pane;
1754                let pane = pane.read(cx);
1755                let active_ix = pane.active_item_index();
1756                PaneSummary {
1757                    active,
1758                    leader,
1759                    items: pane
1760                        .items()
1761                        .enumerate()
1762                        .map(|(ix, item)| {
1763                            (
1764                                ix == active_ix,
1765                                item.tab_description(0, cx)
1766                                    .map_or(String::new(), |s| s.to_string()),
1767                            )
1768                        })
1769                        .collect(),
1770                }
1771            })
1772            .collect()
1773    })
1774}