following_tests.rs

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