following_tests.rs

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