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