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 serde_json::json;
   8use std::{borrow::Cow, sync::Arc};
   9use workspace::{
  10    dock::{test::TestPanel, DockPosition},
  11    item::{test::TestItem, ItemHandle as _},
  12    shared_screen::SharedScreen,
  13    SplitDirection, Workspace,
  14};
  15
  16#[gpui::test(iterations = 10)]
  17async fn test_basic_following(
  18    deterministic: Arc<Deterministic>,
  19    cx_a: &mut TestAppContext,
  20    cx_b: &mut TestAppContext,
  21    cx_c: &mut TestAppContext,
  22    cx_d: &mut TestAppContext,
  23) {
  24    deterministic.forbid_parking();
  25
  26    let mut server = TestServer::start(&deterministic).await;
  27    let client_a = server.create_client(cx_a, "user_a").await;
  28    let client_b = server.create_client(cx_b, "user_b").await;
  29    let client_c = server.create_client(cx_c, "user_c").await;
  30    let client_d = server.create_client(cx_d, "user_d").await;
  31    server
  32        .create_room(&mut [
  33            (&client_a, cx_a),
  34            (&client_b, cx_b),
  35            (&client_c, cx_c),
  36            (&client_d, cx_d),
  37        ])
  38        .await;
  39    let active_call_a = cx_a.read(ActiveCall::global);
  40    let active_call_b = cx_b.read(ActiveCall::global);
  41
  42    cx_a.update(editor::init);
  43    cx_b.update(editor::init);
  44
  45    client_a
  46        .fs()
  47        .insert_tree(
  48            "/a",
  49            json!({
  50                "1.txt": "one\none\none",
  51                "2.txt": "two\ntwo\ntwo",
  52                "3.txt": "three\nthree\nthree",
  53            }),
  54        )
  55        .await;
  56    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
  57    active_call_a
  58        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
  59        .await
  60        .unwrap();
  61
  62    let project_id = active_call_a
  63        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  64        .await
  65        .unwrap();
  66    let project_b = client_b.build_remote_project(project_id, cx_b).await;
  67    active_call_b
  68        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
  69        .await
  70        .unwrap();
  71
  72    let window_a = client_a.build_workspace(&project_a, cx_a);
  73    let workspace_a = window_a.root(cx_a);
  74    let window_b = client_b.build_workspace(&project_b, cx_b);
  75    let workspace_b = window_b.root(cx_b);
  76
  77    // Client A opens some editors.
  78    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
  79    let editor_a1 = workspace_a
  80        .update(cx_a, |workspace, cx| {
  81            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
  82        })
  83        .await
  84        .unwrap()
  85        .downcast::<Editor>()
  86        .unwrap();
  87    let editor_a2 = workspace_a
  88        .update(cx_a, |workspace, cx| {
  89            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
  90        })
  91        .await
  92        .unwrap()
  93        .downcast::<Editor>()
  94        .unwrap();
  95
  96    // Client B opens an editor.
  97    let editor_b1 = workspace_b
  98        .update(cx_b, |workspace, cx| {
  99            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
 100        })
 101        .await
 102        .unwrap()
 103        .downcast::<Editor>()
 104        .unwrap();
 105
 106    let peer_id_a = client_a.peer_id().unwrap();
 107    let peer_id_b = client_b.peer_id().unwrap();
 108    let peer_id_c = client_c.peer_id().unwrap();
 109    let peer_id_d = client_d.peer_id().unwrap();
 110
 111    // Client A updates their selections in those editors
 112    editor_a1.update(cx_a, |editor, cx| {
 113        editor.handle_input("a", cx);
 114        editor.handle_input("b", cx);
 115        editor.handle_input("c", cx);
 116        editor.select_left(&Default::default(), cx);
 117        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
 118    });
 119    editor_a2.update(cx_a, |editor, cx| {
 120        editor.handle_input("d", cx);
 121        editor.handle_input("e", cx);
 122        editor.select_left(&Default::default(), cx);
 123        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
 124    });
 125
 126    // When client B starts following client A, all visible view states are replicated to client B.
 127    workspace_b
 128        .update(cx_b, |workspace, cx| {
 129            workspace.follow(peer_id_a, cx).unwrap()
 130        })
 131        .await
 132        .unwrap();
 133
 134    cx_c.foreground().run_until_parked();
 135    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
 136        workspace
 137            .active_item(cx)
 138            .unwrap()
 139            .downcast::<Editor>()
 140            .unwrap()
 141    });
 142    assert_eq!(
 143        cx_b.read(|cx| editor_b2.project_path(cx)),
 144        Some((worktree_id, "2.txt").into())
 145    );
 146    assert_eq!(
 147        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
 148        vec![2..1]
 149    );
 150    assert_eq!(
 151        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
 152        vec![3..2]
 153    );
 154
 155    cx_c.foreground().run_until_parked();
 156    let active_call_c = cx_c.read(ActiveCall::global);
 157    let project_c = client_c.build_remote_project(project_id, cx_c).await;
 158    let window_c = client_c.build_workspace(&project_c, cx_c);
 159    let workspace_c = window_c.root(cx_c);
 160    active_call_c
 161        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
 162        .await
 163        .unwrap();
 164    drop(project_c);
 165
 166    // Client C also follows client A.
 167    workspace_c
 168        .update(cx_c, |workspace, cx| {
 169            workspace.follow(peer_id_a, cx).unwrap()
 170        })
 171        .await
 172        .unwrap();
 173
 174    cx_d.foreground().run_until_parked();
 175    let active_call_d = cx_d.read(ActiveCall::global);
 176    let project_d = client_d.build_remote_project(project_id, cx_d).await;
 177    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
 178    active_call_d
 179        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
 180        .await
 181        .unwrap();
 182    drop(project_d);
 183
 184    // All clients see that clients B and C are following client A.
 185    cx_c.foreground().run_until_parked();
 186    for (name, active_call, cx) in [
 187        ("A", &active_call_a, &cx_a),
 188        ("B", &active_call_b, &cx_b),
 189        ("C", &active_call_c, &cx_c),
 190        ("D", &active_call_d, &cx_d),
 191    ] {
 192        active_call.read_with(*cx, |call, cx| {
 193            let room = call.room().unwrap().read(cx);
 194            assert_eq!(
 195                room.followers_for(peer_id_a, project_id),
 196                &[peer_id_b, peer_id_c],
 197                "checking followers for A as {name}"
 198            );
 199        });
 200    }
 201
 202    // Client C unfollows client A.
 203    workspace_c.update(cx_c, |workspace, cx| {
 204        workspace.unfollow(&workspace.active_pane().clone(), cx);
 205    });
 206
 207    // All clients see that clients B is following client A.
 208    cx_c.foreground().run_until_parked();
 209    for (name, active_call, cx) in [
 210        ("A", &active_call_a, &cx_a),
 211        ("B", &active_call_b, &cx_b),
 212        ("C", &active_call_c, &cx_c),
 213        ("D", &active_call_d, &cx_d),
 214    ] {
 215        active_call.read_with(*cx, |call, cx| {
 216            let room = call.room().unwrap().read(cx);
 217            assert_eq!(
 218                room.followers_for(peer_id_a, project_id),
 219                &[peer_id_b],
 220                "checking followers for A as {name}"
 221            );
 222        });
 223    }
 224
 225    // Client C re-follows client A.
 226    workspace_c.update(cx_c, |workspace, cx| {
 227        workspace.follow(peer_id_a, cx);
 228    });
 229
 230    // All clients see that clients B and C are following client A.
 231    cx_c.foreground().run_until_parked();
 232    for (name, active_call, cx) in [
 233        ("A", &active_call_a, &cx_a),
 234        ("B", &active_call_b, &cx_b),
 235        ("C", &active_call_c, &cx_c),
 236        ("D", &active_call_d, &cx_d),
 237    ] {
 238        active_call.read_with(*cx, |call, cx| {
 239            let room = call.room().unwrap().read(cx);
 240            assert_eq!(
 241                room.followers_for(peer_id_a, project_id),
 242                &[peer_id_b, peer_id_c],
 243                "checking followers for A as {name}"
 244            );
 245        });
 246    }
 247
 248    // Client D follows client C.
 249    workspace_d
 250        .update(cx_d, |workspace, cx| {
 251            workspace.follow(peer_id_c, cx).unwrap()
 252        })
 253        .await
 254        .unwrap();
 255
 256    // All clients see that D is following C
 257    cx_d.foreground().run_until_parked();
 258    for (name, active_call, cx) in [
 259        ("A", &active_call_a, &cx_a),
 260        ("B", &active_call_b, &cx_b),
 261        ("C", &active_call_c, &cx_c),
 262        ("D", &active_call_d, &cx_d),
 263    ] {
 264        active_call.read_with(*cx, |call, cx| {
 265            let room = call.room().unwrap().read(cx);
 266            assert_eq!(
 267                room.followers_for(peer_id_c, project_id),
 268                &[peer_id_d],
 269                "checking followers for C as {name}"
 270            );
 271        });
 272    }
 273
 274    // Client C closes the project.
 275    window_c.remove(cx_c);
 276    cx_c.drop_last(workspace_c);
 277
 278    // Clients A and B see that client B is following A, and client C is not present in the followers.
 279    cx_c.foreground().run_until_parked();
 280    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
 281        active_call.read_with(*cx, |call, cx| {
 282            let room = call.room().unwrap().read(cx);
 283            assert_eq!(
 284                room.followers_for(peer_id_a, project_id),
 285                &[peer_id_b],
 286                "checking followers for A as {name}"
 287            );
 288        });
 289    }
 290
 291    // All clients see that no-one is following C
 292    for (name, active_call, cx) in [
 293        ("A", &active_call_a, &cx_a),
 294        ("B", &active_call_b, &cx_b),
 295        ("C", &active_call_c, &cx_c),
 296        ("D", &active_call_d, &cx_d),
 297    ] {
 298        active_call.read_with(*cx, |call, cx| {
 299            let room = call.room().unwrap().read(cx);
 300            assert_eq!(
 301                room.followers_for(peer_id_c, project_id),
 302                &[],
 303                "checking followers for C as {name}"
 304            );
 305        });
 306    }
 307
 308    // When client A activates a different editor, client B does so as well.
 309    workspace_a.update(cx_a, |workspace, cx| {
 310        workspace.activate_item(&editor_a1, cx)
 311    });
 312    deterministic.run_until_parked();
 313    workspace_b.read_with(cx_b, |workspace, cx| {
 314        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
 315    });
 316
 317    // When client A opens a multibuffer, client B does so as well.
 318    let multibuffer_a = cx_a.add_model(|cx| {
 319        let buffer_a1 = project_a.update(cx, |project, cx| {
 320            project
 321                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
 322                .unwrap()
 323        });
 324        let buffer_a2 = project_a.update(cx, |project, cx| {
 325            project
 326                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
 327                .unwrap()
 328        });
 329        let mut result = MultiBuffer::new(0);
 330        result.push_excerpts(
 331            buffer_a1,
 332            [ExcerptRange {
 333                context: 0..3,
 334                primary: None,
 335            }],
 336            cx,
 337        );
 338        result.push_excerpts(
 339            buffer_a2,
 340            [ExcerptRange {
 341                context: 4..7,
 342                primary: None,
 343            }],
 344            cx,
 345        );
 346        result
 347    });
 348    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
 349        let editor =
 350            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
 351        workspace.add_item(Box::new(editor.clone()), cx);
 352        editor
 353    });
 354    deterministic.run_until_parked();
 355    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
 356        workspace
 357            .active_item(cx)
 358            .unwrap()
 359            .downcast::<Editor>()
 360            .unwrap()
 361    });
 362    assert_eq!(
 363        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
 364        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
 365    );
 366
 367    // When client A navigates back and forth, client B does so as well.
 368    workspace_a
 369        .update(cx_a, |workspace, cx| {
 370            workspace.go_back(workspace.active_pane().downgrade(), cx)
 371        })
 372        .await
 373        .unwrap();
 374    deterministic.run_until_parked();
 375    workspace_b.read_with(cx_b, |workspace, cx| {
 376        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
 377    });
 378
 379    workspace_a
 380        .update(cx_a, |workspace, cx| {
 381            workspace.go_back(workspace.active_pane().downgrade(), cx)
 382        })
 383        .await
 384        .unwrap();
 385    deterministic.run_until_parked();
 386    workspace_b.read_with(cx_b, |workspace, cx| {
 387        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
 388    });
 389
 390    workspace_a
 391        .update(cx_a, |workspace, cx| {
 392            workspace.go_forward(workspace.active_pane().downgrade(), cx)
 393        })
 394        .await
 395        .unwrap();
 396    deterministic.run_until_parked();
 397    workspace_b.read_with(cx_b, |workspace, cx| {
 398        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
 399    });
 400
 401    // Changes to client A's editor are reflected on client B.
 402    editor_a1.update(cx_a, |editor, cx| {
 403        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
 404    });
 405    deterministic.run_until_parked();
 406    editor_b1.read_with(cx_b, |editor, cx| {
 407        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
 408    });
 409
 410    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
 411    deterministic.run_until_parked();
 412    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
 413
 414    editor_a1.update(cx_a, |editor, cx| {
 415        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
 416        editor.set_scroll_position(vec2f(0., 100.), cx);
 417    });
 418    deterministic.run_until_parked();
 419    editor_b1.read_with(cx_b, |editor, cx| {
 420        assert_eq!(editor.selections.ranges(cx), &[3..3]);
 421    });
 422
 423    // After unfollowing, client B stops receiving updates from client A.
 424    workspace_b.update(cx_b, |workspace, cx| {
 425        workspace.unfollow(&workspace.active_pane().clone(), cx)
 426    });
 427    workspace_a.update(cx_a, |workspace, cx| {
 428        workspace.activate_item(&editor_a2, cx)
 429    });
 430    deterministic.run_until_parked();
 431    assert_eq!(
 432        workspace_b.read_with(cx_b, |workspace, cx| workspace
 433            .active_item(cx)
 434            .unwrap()
 435            .id()),
 436        editor_b1.id()
 437    );
 438
 439    // Client A starts following client B.
 440    workspace_a
 441        .update(cx_a, |workspace, cx| {
 442            workspace.follow(peer_id_b, cx).unwrap()
 443        })
 444        .await
 445        .unwrap();
 446    assert_eq!(
 447        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 448        Some(peer_id_b)
 449    );
 450    assert_eq!(
 451        workspace_a.read_with(cx_a, |workspace, cx| workspace
 452            .active_item(cx)
 453            .unwrap()
 454            .id()),
 455        editor_a1.id()
 456    );
 457
 458    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
 459    let display = MacOSDisplay::new();
 460    active_call_b
 461        .update(cx_b, |call, cx| call.set_location(None, cx))
 462        .await
 463        .unwrap();
 464    active_call_b
 465        .update(cx_b, |call, cx| {
 466            call.room().unwrap().update(cx, |room, cx| {
 467                room.set_display_sources(vec![display.clone()]);
 468                room.share_screen(cx)
 469            })
 470        })
 471        .await
 472        .unwrap();
 473    deterministic.run_until_parked();
 474    let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
 475        workspace
 476            .active_item(cx)
 477            .expect("no active item")
 478            .downcast::<SharedScreen>()
 479            .expect("active item isn't a shared screen")
 480    });
 481
 482    // Client B activates Zed again, which causes the previous editor to become focused again.
 483    active_call_b
 484        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 485        .await
 486        .unwrap();
 487    deterministic.run_until_parked();
 488    workspace_a.read_with(cx_a, |workspace, cx| {
 489        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
 490    });
 491
 492    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
 493    workspace_b.update(cx_b, |workspace, cx| {
 494        workspace.activate_item(&multibuffer_editor_b, cx)
 495    });
 496    deterministic.run_until_parked();
 497    workspace_a.read_with(cx_a, |workspace, cx| {
 498        assert_eq!(
 499            workspace.active_item(cx).unwrap().id(),
 500            multibuffer_editor_a.id()
 501        )
 502    });
 503
 504    // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
 505    let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
 506    workspace_b.update(cx_b, |workspace, cx| {
 507        workspace.add_panel(panel, cx);
 508        workspace.toggle_panel_focus::<TestPanel>(cx);
 509    });
 510    deterministic.run_until_parked();
 511    assert_eq!(
 512        workspace_a.read_with(cx_a, |workspace, cx| workspace
 513            .active_item(cx)
 514            .unwrap()
 515            .id()),
 516        shared_screen.id()
 517    );
 518
 519    // Toggling the focus back to the pane causes client A to return to the multibuffer.
 520    workspace_b.update(cx_b, |workspace, cx| {
 521        workspace.toggle_panel_focus::<TestPanel>(cx);
 522    });
 523    deterministic.run_until_parked();
 524    workspace_a.read_with(cx_a, |workspace, cx| {
 525        assert_eq!(
 526            workspace.active_item(cx).unwrap().id(),
 527            multibuffer_editor_a.id()
 528        )
 529    });
 530
 531    // Client B activates an item that doesn't implement following,
 532    // so the previously-opened screen-sharing item gets activated.
 533    let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
 534    workspace_b.update(cx_b, |workspace, cx| {
 535        workspace.active_pane().update(cx, |pane, cx| {
 536            pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
 537        })
 538    });
 539    deterministic.run_until_parked();
 540    assert_eq!(
 541        workspace_a.read_with(cx_a, |workspace, cx| workspace
 542            .active_item(cx)
 543            .unwrap()
 544            .id()),
 545        shared_screen.id()
 546    );
 547
 548    // Following interrupts when client B disconnects.
 549    client_b.disconnect(&cx_b.to_async());
 550    deterministic.advance_clock(RECONNECT_TIMEOUT);
 551    assert_eq!(
 552        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 553        None
 554    );
 555}
 556
 557#[gpui::test]
 558async fn test_following_tab_order(
 559    deterministic: Arc<Deterministic>,
 560    cx_a: &mut TestAppContext,
 561    cx_b: &mut TestAppContext,
 562) {
 563    let mut server = TestServer::start(&deterministic).await;
 564    let client_a = server.create_client(cx_a, "user_a").await;
 565    let client_b = server.create_client(cx_b, "user_b").await;
 566    server
 567        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 568        .await;
 569    let active_call_a = cx_a.read(ActiveCall::global);
 570    let active_call_b = cx_b.read(ActiveCall::global);
 571
 572    cx_a.update(editor::init);
 573    cx_b.update(editor::init);
 574
 575    client_a
 576        .fs()
 577        .insert_tree(
 578            "/a",
 579            json!({
 580                "1.txt": "one",
 581                "2.txt": "two",
 582                "3.txt": "three",
 583            }),
 584        )
 585        .await;
 586    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 587    active_call_a
 588        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
 589        .await
 590        .unwrap();
 591
 592    let project_id = active_call_a
 593        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 594        .await
 595        .unwrap();
 596    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 597    active_call_b
 598        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 599        .await
 600        .unwrap();
 601
 602    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
 603    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
 604
 605    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 606    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
 607
 608    let client_b_id = project_a.read_with(cx_a, |project, _| {
 609        project.collaborators().values().next().unwrap().peer_id
 610    });
 611
 612    //Open 1, 3 in that order on client A
 613    workspace_a
 614        .update(cx_a, |workspace, cx| {
 615            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
 616        })
 617        .await
 618        .unwrap();
 619    workspace_a
 620        .update(cx_a, |workspace, cx| {
 621            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
 622        })
 623        .await
 624        .unwrap();
 625
 626    let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
 627        pane.update(cx, |pane, cx| {
 628            pane.items()
 629                .map(|item| {
 630                    item.project_path(cx)
 631                        .unwrap()
 632                        .path
 633                        .to_str()
 634                        .unwrap()
 635                        .to_owned()
 636                })
 637                .collect::<Vec<_>>()
 638        })
 639    };
 640
 641    //Verify that the tabs opened in the order we expect
 642    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
 643
 644    //Follow client B as client A
 645    workspace_a
 646        .update(cx_a, |workspace, cx| {
 647            workspace.follow(client_b_id, cx).unwrap()
 648        })
 649        .await
 650        .unwrap();
 651
 652    //Open just 2 on client B
 653    workspace_b
 654        .update(cx_b, |workspace, cx| {
 655            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
 656        })
 657        .await
 658        .unwrap();
 659    deterministic.run_until_parked();
 660
 661    // Verify that newly opened followed file is at the end
 662    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
 663
 664    //Open just 1 on client B
 665    workspace_b
 666        .update(cx_b, |workspace, cx| {
 667            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
 668        })
 669        .await
 670        .unwrap();
 671    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
 672    deterministic.run_until_parked();
 673
 674    // Verify that following into 1 did not reorder
 675    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
 676}
 677
 678#[gpui::test(iterations = 10)]
 679async fn test_peers_following_each_other(
 680    deterministic: Arc<Deterministic>,
 681    cx_a: &mut TestAppContext,
 682    cx_b: &mut TestAppContext,
 683) {
 684    deterministic.forbid_parking();
 685    let mut server = TestServer::start(&deterministic).await;
 686    let client_a = server.create_client(cx_a, "user_a").await;
 687    let client_b = server.create_client(cx_b, "user_b").await;
 688    server
 689        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 690        .await;
 691    let active_call_a = cx_a.read(ActiveCall::global);
 692    let active_call_b = cx_b.read(ActiveCall::global);
 693
 694    cx_a.update(editor::init);
 695    cx_b.update(editor::init);
 696
 697    // Client A shares a project.
 698    client_a
 699        .fs()
 700        .insert_tree(
 701            "/a",
 702            json!({
 703                "1.txt": "one",
 704                "2.txt": "two",
 705                "3.txt": "three",
 706                "4.txt": "four",
 707            }),
 708        )
 709        .await;
 710    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 711    active_call_a
 712        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
 713        .await
 714        .unwrap();
 715    let project_id = active_call_a
 716        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 717        .await
 718        .unwrap();
 719
 720    // Client B joins the project.
 721    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 722    active_call_b
 723        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 724        .await
 725        .unwrap();
 726
 727    // Client A opens some editors.
 728    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
 729    let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
 730    let _editor_a1 = 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 an editor.
 740    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 741    let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
 742    let _editor_b1 = workspace_b
 743        .update(cx_b, |workspace, cx| {
 744            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
 745        })
 746        .await
 747        .unwrap()
 748        .downcast::<Editor>()
 749        .unwrap();
 750
 751    // Clients A and B follow each other in split panes
 752    workspace_a.update(cx_a, |workspace, cx| {
 753        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
 754    });
 755    workspace_a
 756        .update(cx_a, |workspace, cx| {
 757            assert_ne!(*workspace.active_pane(), pane_a1);
 758            let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
 759            workspace.follow(leader_id, cx).unwrap()
 760        })
 761        .await
 762        .unwrap();
 763    workspace_b.update(cx_b, |workspace, cx| {
 764        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
 765    });
 766    workspace_b
 767        .update(cx_b, |workspace, cx| {
 768            assert_ne!(*workspace.active_pane(), pane_b1);
 769            let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
 770            workspace.follow(leader_id, cx).unwrap()
 771        })
 772        .await
 773        .unwrap();
 774
 775    workspace_a.update(cx_a, |workspace, cx| {
 776        workspace.activate_next_pane(cx);
 777    });
 778    // Wait for focus effects to be fully flushed
 779    workspace_a.update(cx_a, |workspace, _| {
 780        assert_eq!(*workspace.active_pane(), pane_a1);
 781    });
 782
 783    workspace_a
 784        .update(cx_a, |workspace, cx| {
 785            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
 786        })
 787        .await
 788        .unwrap();
 789    workspace_b.update(cx_b, |workspace, cx| {
 790        workspace.activate_next_pane(cx);
 791    });
 792
 793    workspace_b
 794        .update(cx_b, |workspace, cx| {
 795            assert_eq!(*workspace.active_pane(), pane_b1);
 796            workspace.open_path((worktree_id, "4.txt"), None, true, cx)
 797        })
 798        .await
 799        .unwrap();
 800    cx_a.foreground().run_until_parked();
 801
 802    // Ensure leader updates don't change the active pane of followers
 803    workspace_a.read_with(cx_a, |workspace, _| {
 804        assert_eq!(*workspace.active_pane(), pane_a1);
 805    });
 806    workspace_b.read_with(cx_b, |workspace, _| {
 807        assert_eq!(*workspace.active_pane(), pane_b1);
 808    });
 809
 810    // Ensure peers following each other doesn't cause an infinite loop.
 811    assert_eq!(
 812        workspace_a.read_with(cx_a, |workspace, cx| workspace
 813            .active_item(cx)
 814            .unwrap()
 815            .project_path(cx)),
 816        Some((worktree_id, "3.txt").into())
 817    );
 818    workspace_a.update(cx_a, |workspace, cx| {
 819        assert_eq!(
 820            workspace.active_item(cx).unwrap().project_path(cx),
 821            Some((worktree_id, "3.txt").into())
 822        );
 823        workspace.activate_next_pane(cx);
 824    });
 825
 826    workspace_a.update(cx_a, |workspace, cx| {
 827        assert_eq!(
 828            workspace.active_item(cx).unwrap().project_path(cx),
 829            Some((worktree_id, "4.txt").into())
 830        );
 831    });
 832
 833    workspace_b.update(cx_b, |workspace, cx| {
 834        assert_eq!(
 835            workspace.active_item(cx).unwrap().project_path(cx),
 836            Some((worktree_id, "4.txt").into())
 837        );
 838        workspace.activate_next_pane(cx);
 839    });
 840
 841    workspace_b.update(cx_b, |workspace, cx| {
 842        assert_eq!(
 843            workspace.active_item(cx).unwrap().project_path(cx),
 844            Some((worktree_id, "3.txt").into())
 845        );
 846    });
 847}
 848
 849#[gpui::test(iterations = 10)]
 850async fn test_auto_unfollowing(
 851    deterministic: Arc<Deterministic>,
 852    cx_a: &mut TestAppContext,
 853    cx_b: &mut TestAppContext,
 854) {
 855    deterministic.forbid_parking();
 856
 857    // 2 clients connect to a server.
 858    let mut server = TestServer::start(&deterministic).await;
 859    let client_a = server.create_client(cx_a, "user_a").await;
 860    let client_b = server.create_client(cx_b, "user_b").await;
 861    server
 862        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 863        .await;
 864    let active_call_a = cx_a.read(ActiveCall::global);
 865    let active_call_b = cx_b.read(ActiveCall::global);
 866
 867    cx_a.update(editor::init);
 868    cx_b.update(editor::init);
 869
 870    // Client A shares a project.
 871    client_a
 872        .fs()
 873        .insert_tree(
 874            "/a",
 875            json!({
 876                "1.txt": "one",
 877                "2.txt": "two",
 878                "3.txt": "three",
 879            }),
 880        )
 881        .await;
 882    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 883    active_call_a
 884        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
 885        .await
 886        .unwrap();
 887
 888    let project_id = active_call_a
 889        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 890        .await
 891        .unwrap();
 892    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 893    active_call_b
 894        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
 895        .await
 896        .unwrap();
 897
 898    // Client A opens some editors.
 899    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
 900    let _editor_a1 = workspace_a
 901        .update(cx_a, |workspace, cx| {
 902            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
 903        })
 904        .await
 905        .unwrap()
 906        .downcast::<Editor>()
 907        .unwrap();
 908
 909    // Client B starts following client A.
 910    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 911    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
 912    let leader_id = project_b.read_with(cx_b, |project, _| {
 913        project.collaborators().values().next().unwrap().peer_id
 914    });
 915    workspace_b
 916        .update(cx_b, |workspace, cx| {
 917            workspace.follow(leader_id, cx).unwrap()
 918        })
 919        .await
 920        .unwrap();
 921    assert_eq!(
 922        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 923        Some(leader_id)
 924    );
 925    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
 926        workspace
 927            .active_item(cx)
 928            .unwrap()
 929            .downcast::<Editor>()
 930            .unwrap()
 931    });
 932
 933    // When client B moves, it automatically stops following client A.
 934    editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
 935    assert_eq!(
 936        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 937        None
 938    );
 939
 940    workspace_b
 941        .update(cx_b, |workspace, cx| {
 942            workspace.follow(leader_id, cx).unwrap()
 943        })
 944        .await
 945        .unwrap();
 946    assert_eq!(
 947        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 948        Some(leader_id)
 949    );
 950
 951    // When client B edits, it automatically stops following client A.
 952    editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
 953    assert_eq!(
 954        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 955        None
 956    );
 957
 958    workspace_b
 959        .update(cx_b, |workspace, cx| {
 960            workspace.follow(leader_id, cx).unwrap()
 961        })
 962        .await
 963        .unwrap();
 964    assert_eq!(
 965        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 966        Some(leader_id)
 967    );
 968
 969    // When client B scrolls, it automatically stops following client A.
 970    editor_b2.update(cx_b, |editor, cx| {
 971        editor.set_scroll_position(vec2f(0., 3.), cx)
 972    });
 973    assert_eq!(
 974        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 975        None
 976    );
 977
 978    workspace_b
 979        .update(cx_b, |workspace, cx| {
 980            workspace.follow(leader_id, cx).unwrap()
 981        })
 982        .await
 983        .unwrap();
 984    assert_eq!(
 985        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 986        Some(leader_id)
 987    );
 988
 989    // When client B activates a different pane, it continues following client A in the original pane.
 990    workspace_b.update(cx_b, |workspace, cx| {
 991        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
 992    });
 993    assert_eq!(
 994        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 995        Some(leader_id)
 996    );
 997
 998    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
 999    assert_eq!(
1000        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1001        Some(leader_id)
1002    );
1003
1004    // When client B activates a different item in the original pane, it automatically stops following client A.
1005    workspace_b
1006        .update(cx_b, |workspace, cx| {
1007            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
1008        })
1009        .await
1010        .unwrap();
1011    assert_eq!(
1012        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1013        None
1014    );
1015}
1016
1017#[gpui::test(iterations = 10)]
1018async fn test_peers_simultaneously_following_each_other(
1019    deterministic: Arc<Deterministic>,
1020    cx_a: &mut TestAppContext,
1021    cx_b: &mut TestAppContext,
1022) {
1023    deterministic.forbid_parking();
1024
1025    let mut server = TestServer::start(&deterministic).await;
1026    let client_a = server.create_client(cx_a, "user_a").await;
1027    let client_b = server.create_client(cx_b, "user_b").await;
1028    server
1029        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1030        .await;
1031    let active_call_a = cx_a.read(ActiveCall::global);
1032
1033    cx_a.update(editor::init);
1034    cx_b.update(editor::init);
1035
1036    client_a.fs().insert_tree("/a", json!({})).await;
1037    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1038    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1039    let project_id = active_call_a
1040        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1041        .await
1042        .unwrap();
1043
1044    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1045    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1046
1047    deterministic.run_until_parked();
1048    let client_a_id = project_b.read_with(cx_b, |project, _| {
1049        project.collaborators().values().next().unwrap().peer_id
1050    });
1051    let client_b_id = project_a.read_with(cx_a, |project, _| {
1052        project.collaborators().values().next().unwrap().peer_id
1053    });
1054
1055    let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
1056        workspace.follow(client_b_id, cx).unwrap()
1057    });
1058    let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
1059        workspace.follow(client_a_id, cx).unwrap()
1060    });
1061
1062    futures::try_join!(a_follow_b, b_follow_a).unwrap();
1063    workspace_a.read_with(cx_a, |workspace, _| {
1064        assert_eq!(
1065            workspace.leader_for_pane(workspace.active_pane()),
1066            Some(client_b_id)
1067        );
1068    });
1069    workspace_b.read_with(cx_b, |workspace, _| {
1070        assert_eq!(
1071            workspace.leader_for_pane(workspace.active_pane()),
1072            Some(client_a_id)
1073        );
1074    });
1075}
1076
1077fn visible_push_notifications(
1078    cx: &mut TestAppContext,
1079) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
1080    let mut ret = Vec::new();
1081    for window in cx.windows() {
1082        window.read_with(cx, |window| {
1083            if let Some(handle) = window
1084                .root_view()
1085                .clone()
1086                .downcast::<ProjectSharedNotification>()
1087            {
1088                ret.push(handle)
1089            }
1090        });
1091    }
1092    ret
1093}
1094
1095#[gpui::test(iterations = 10)]
1096async fn test_following_across_workspaces(
1097    deterministic: Arc<Deterministic>,
1098    cx_a: &mut TestAppContext,
1099    cx_b: &mut TestAppContext,
1100) {
1101    // a and b join a channel/call
1102    // a shares project 1
1103    // b shares project 2
1104    //
1105    // b follows a: causes project 2 to be joined, and b to follow a.
1106    // b opens a different file in project 2, a follows b
1107    // b opens a different file in project 1, a cannot follow b
1108    // b shares the project, a joins the project and follows b
1109    deterministic.forbid_parking();
1110    let mut server = TestServer::start(&deterministic).await;
1111    let client_a = server.create_client(cx_a, "user_a").await;
1112    let client_b = server.create_client(cx_b, "user_b").await;
1113    cx_a.update(editor::init);
1114    cx_b.update(editor::init);
1115
1116    client_a
1117        .fs()
1118        .insert_tree(
1119            "/a",
1120            json!({
1121                "w.rs": "",
1122                "x.rs": "",
1123            }),
1124        )
1125        .await;
1126
1127    client_b
1128        .fs()
1129        .insert_tree(
1130            "/b",
1131            json!({
1132                "y.rs": "",
1133                "z.rs": "",
1134            }),
1135        )
1136        .await;
1137
1138    server
1139        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1140        .await;
1141    let active_call_a = cx_a.read(ActiveCall::global);
1142    let active_call_b = cx_b.read(ActiveCall::global);
1143
1144    let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
1145    let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
1146
1147    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1148    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1149
1150    cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
1151    cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
1152
1153    active_call_a
1154        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1155        .await
1156        .unwrap();
1157
1158    active_call_a
1159        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1160        .await
1161        .unwrap();
1162    active_call_b
1163        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1164        .await
1165        .unwrap();
1166
1167    workspace_a
1168        .update(cx_a, |workspace, cx| {
1169            workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
1170        })
1171        .await
1172        .unwrap();
1173
1174    deterministic.run_until_parked();
1175    assert_eq!(visible_push_notifications(cx_b).len(), 1);
1176
1177    workspace_b.update(cx_b, |workspace, cx| {
1178        workspace
1179            .follow(client_a.peer_id().unwrap(), cx)
1180            .unwrap()
1181            .detach()
1182    });
1183
1184    deterministic.run_until_parked();
1185    let workspace_b_project_a = cx_b
1186        .windows()
1187        .iter()
1188        .max_by_key(|window| window.id())
1189        .unwrap()
1190        .downcast::<Workspace>()
1191        .unwrap()
1192        .root(cx_b);
1193
1194    // assert that b is following a in project a in w.rs
1195    workspace_b_project_a.update(cx_b, |workspace, cx| {
1196        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1197        assert_eq!(
1198            client_a.peer_id(),
1199            workspace.leader_for_pane(workspace.active_pane())
1200        );
1201        let item = workspace.active_item(cx).unwrap();
1202        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
1203    });
1204
1205    // TODO: in app code, this would be done by the collab_ui.
1206    active_call_b
1207        .update(cx_b, |call, cx| {
1208            let project = workspace_b_project_a.read(cx).project().clone();
1209            call.set_location(Some(&project), cx)
1210        })
1211        .await
1212        .unwrap();
1213
1214    // assert that there are no share notifications open
1215    assert_eq!(visible_push_notifications(cx_b).len(), 0);
1216
1217    // b moves to x.rs in a's project, and a follows
1218    workspace_b_project_a
1219        .update(cx_b, |workspace, cx| {
1220            workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1221        })
1222        .await
1223        .unwrap();
1224
1225    deterministic.run_until_parked();
1226    workspace_b_project_a.update(cx_b, |workspace, cx| {
1227        let item = workspace.active_item(cx).unwrap();
1228        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1229    });
1230
1231    workspace_a.update(cx_a, |workspace, cx| {
1232        workspace
1233            .follow(client_b.peer_id().unwrap(), cx)
1234            .unwrap()
1235            .detach()
1236    });
1237
1238    deterministic.run_until_parked();
1239    workspace_a.update(cx_a, |workspace, cx| {
1240        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1241        assert_eq!(
1242            client_b.peer_id(),
1243            workspace.leader_for_pane(workspace.active_pane())
1244        );
1245        let item = workspace.active_pane().read(cx).active_item().unwrap();
1246        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1247    });
1248
1249    // b moves to y.rs in b's project, a is still following but can't yet see
1250    workspace_b
1251        .update(cx_b, |workspace, cx| {
1252            workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1253        })
1254        .await
1255        .unwrap();
1256
1257    // TODO: in app code, this would be done by the collab_ui.
1258    active_call_b
1259        .update(cx_b, |call, cx| {
1260            let project = workspace_b.read(cx).project().clone();
1261            call.set_location(Some(&project), cx)
1262        })
1263        .await
1264        .unwrap();
1265
1266    let project_b_id = active_call_b
1267        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1268        .await
1269        .unwrap();
1270
1271    deterministic.run_until_parked();
1272    assert_eq!(visible_push_notifications(cx_a).len(), 1);
1273    cx_a.update(|cx| {
1274        workspace::join_remote_project(
1275            project_b_id,
1276            client_b.user_id().unwrap(),
1277            client_a.app_state.clone(),
1278            cx,
1279        )
1280    })
1281    .await
1282    .unwrap();
1283
1284    deterministic.run_until_parked();
1285
1286    assert_eq!(visible_push_notifications(cx_a).len(), 0);
1287    let workspace_a_project_b = cx_a
1288        .windows()
1289        .iter()
1290        .max_by_key(|window| window.id())
1291        .unwrap()
1292        .downcast::<Workspace>()
1293        .unwrap()
1294        .root(cx_a);
1295
1296    workspace_a_project_b.update(cx_a, |workspace, cx| {
1297        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1298        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1299        assert_eq!(
1300            client_b.peer_id(),
1301            workspace.leader_for_pane(workspace.active_pane())
1302        );
1303        let item = workspace.active_item(cx).unwrap();
1304        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
1305    });
1306}