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