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