following_tests.rs

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