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