following_tests.rs

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