editor_tests.rs

   1use crate::{
   2    rpc::RECONNECT_TIMEOUT,
   3    tests::{rust_lang, TestServer},
   4};
   5use call::ActiveCall;
   6use editor::{
   7    actions::{
   8        ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
   9        ToggleCodeActions, Undo,
  10    },
  11    test::editor_test_context::{AssertionContextManager, EditorTestContext},
  12    Editor, RowInfo,
  13};
  14use fs::Fs;
  15use futures::StreamExt;
  16use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
  17use indoc::indoc;
  18use language::{
  19    language_settings::{AllLanguageSettings, InlayHintSettings},
  20    FakeLspAdapter,
  21};
  22use project::{
  23    project_settings::{InlineBlameSettings, ProjectSettings},
  24    ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
  25};
  26use recent_projects::disconnected_overlay::DisconnectedOverlay;
  27use rpc::RECEIVE_TIMEOUT;
  28use serde_json::json;
  29use settings::SettingsStore;
  30use std::{
  31    ops::Range,
  32    path::{Path, PathBuf},
  33    sync::{
  34        atomic::{self, AtomicBool, AtomicUsize},
  35        Arc,
  36    },
  37};
  38use text::Point;
  39use workspace::{CloseIntent, Workspace};
  40
  41#[gpui::test(iterations = 10)]
  42async fn test_host_disconnect(
  43    cx_a: &mut TestAppContext,
  44    cx_b: &mut TestAppContext,
  45    cx_c: &mut TestAppContext,
  46) {
  47    let mut server = TestServer::start(cx_a.executor()).await;
  48    let client_a = server.create_client(cx_a, "user_a").await;
  49    let client_b = server.create_client(cx_b, "user_b").await;
  50    let client_c = server.create_client(cx_c, "user_c").await;
  51    server
  52        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
  53        .await;
  54
  55    cx_b.update(editor::init);
  56    cx_b.update(recent_projects::init);
  57
  58    client_a
  59        .fs()
  60        .insert_tree(
  61            "/a",
  62            json!({
  63                "a.txt": "a-contents",
  64                "b.txt": "b-contents",
  65            }),
  66        )
  67        .await;
  68
  69    let active_call_a = cx_a.read(ActiveCall::global);
  70    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
  71
  72    let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
  73    let project_id = active_call_a
  74        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  75        .await
  76        .unwrap();
  77
  78    let project_b = client_b.join_remote_project(project_id, cx_b).await;
  79    cx_a.background_executor.run_until_parked();
  80
  81    assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
  82
  83    let workspace_b = cx_b.add_window(|window, cx| {
  84        Workspace::new(
  85            None,
  86            project_b.clone(),
  87            client_b.app_state.clone(),
  88            window,
  89            cx,
  90        )
  91    });
  92    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
  93    let workspace_b_view = workspace_b.root(cx_b).unwrap();
  94
  95    let editor_b = workspace_b
  96        .update(cx_b, |workspace, window, cx| {
  97            workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
  98        })
  99        .unwrap()
 100        .await
 101        .unwrap()
 102        .downcast::<Editor>()
 103        .unwrap();
 104
 105    //TODO: focus
 106    assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
 107    editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
 108
 109    cx_b.update(|_, cx| {
 110        assert!(workspace_b_view.read(cx).is_edited());
 111    });
 112
 113    // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
 114    server.forbid_connections();
 115    server.disconnect_client(client_a.peer_id().unwrap());
 116    cx_a.background_executor
 117        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 118
 119    project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
 120
 121    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 122
 123    project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
 124
 125    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
 126
 127    // Ensure client B's edited state is reset and that the whole window is blurred.
 128    workspace_b
 129        .update(cx_b, |workspace, _, cx| {
 130            assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
 131            assert!(!workspace.is_edited());
 132        })
 133        .unwrap();
 134
 135    // Ensure client B is not prompted to save edits when closing window after disconnecting.
 136    let can_close = workspace_b
 137        .update(cx_b, |workspace, window, cx| {
 138            workspace.prepare_to_close(CloseIntent::Quit, window, cx)
 139        })
 140        .unwrap()
 141        .await
 142        .unwrap();
 143    assert!(can_close);
 144
 145    // Allow client A to reconnect to the server.
 146    server.allow_connections();
 147    cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
 148
 149    // Client B calls client A again after they reconnected.
 150    let active_call_b = cx_b.read(ActiveCall::global);
 151    active_call_b
 152        .update(cx_b, |call, cx| {
 153            call.invite(client_a.user_id().unwrap(), None, cx)
 154        })
 155        .await
 156        .unwrap();
 157    cx_a.background_executor.run_until_parked();
 158    active_call_a
 159        .update(cx_a, |call, cx| call.accept_incoming(cx))
 160        .await
 161        .unwrap();
 162
 163    active_call_a
 164        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 165        .await
 166        .unwrap();
 167
 168    // Drop client A's connection again. We should still unshare it successfully.
 169    server.forbid_connections();
 170    server.disconnect_client(client_a.peer_id().unwrap());
 171    cx_a.background_executor
 172        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 173
 174    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 175}
 176
 177#[gpui::test]
 178async fn test_newline_above_or_below_does_not_move_guest_cursor(
 179    cx_a: &mut TestAppContext,
 180    cx_b: &mut TestAppContext,
 181) {
 182    let mut server = TestServer::start(cx_a.executor()).await;
 183    let client_a = server.create_client(cx_a, "user_a").await;
 184    let client_b = server.create_client(cx_b, "user_b").await;
 185    let executor = cx_a.executor();
 186    server
 187        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 188        .await;
 189    let active_call_a = cx_a.read(ActiveCall::global);
 190
 191    client_a
 192        .fs()
 193        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
 194        .await;
 195    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 196    let project_id = active_call_a
 197        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 198        .await
 199        .unwrap();
 200
 201    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 202
 203    // Open a buffer as client A
 204    let buffer_a = project_a
 205        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 206        .await
 207        .unwrap();
 208    let cx_a = cx_a.add_empty_window();
 209    let editor_a = cx_a
 210        .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
 211
 212    let mut editor_cx_a = EditorTestContext {
 213        cx: cx_a.clone(),
 214        window: cx_a.window_handle(),
 215        editor: editor_a,
 216        assertion_cx: AssertionContextManager::new(),
 217    };
 218
 219    let cx_b = cx_b.add_empty_window();
 220    // Open a buffer as client B
 221    let buffer_b = project_b
 222        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 223        .await
 224        .unwrap();
 225    let editor_b = cx_b
 226        .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
 227
 228    let mut editor_cx_b = EditorTestContext {
 229        cx: cx_b.clone(),
 230        window: cx_b.window_handle(),
 231        editor: editor_b,
 232        assertion_cx: AssertionContextManager::new(),
 233    };
 234
 235    // Test newline above
 236    editor_cx_a.set_selections_state(indoc! {"
 237        Some textˇ
 238    "});
 239    editor_cx_b.set_selections_state(indoc! {"
 240        Some textˇ
 241    "});
 242    editor_cx_a.update_editor(|editor, window, cx| {
 243        editor.newline_above(&editor::actions::NewlineAbove, window, cx)
 244    });
 245    executor.run_until_parked();
 246    editor_cx_a.assert_editor_state(indoc! {"
 247        ˇ
 248        Some text
 249    "});
 250    editor_cx_b.assert_editor_state(indoc! {"
 251
 252        Some textˇ
 253    "});
 254
 255    // Test newline below
 256    editor_cx_a.set_selections_state(indoc! {"
 257
 258        Some textˇ
 259    "});
 260    editor_cx_b.set_selections_state(indoc! {"
 261
 262        Some textˇ
 263    "});
 264    editor_cx_a.update_editor(|editor, window, cx| {
 265        editor.newline_below(&editor::actions::NewlineBelow, window, cx)
 266    });
 267    executor.run_until_parked();
 268    editor_cx_a.assert_editor_state(indoc! {"
 269
 270        Some text
 271        ˇ
 272    "});
 273    editor_cx_b.assert_editor_state(indoc! {"
 274
 275        Some textˇ
 276
 277    "});
 278}
 279
 280#[gpui::test(iterations = 10)]
 281async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 282    let mut server = TestServer::start(cx_a.executor()).await;
 283    let client_a = server.create_client(cx_a, "user_a").await;
 284    let client_b = server.create_client(cx_b, "user_b").await;
 285    server
 286        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 287        .await;
 288    let active_call_a = cx_a.read(ActiveCall::global);
 289
 290    client_a.language_registry().add(rust_lang());
 291    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
 292        "Rust",
 293        FakeLspAdapter {
 294            capabilities: lsp::ServerCapabilities {
 295                completion_provider: Some(lsp::CompletionOptions {
 296                    trigger_characters: Some(vec![".".to_string()]),
 297                    resolve_provider: Some(true),
 298                    ..Default::default()
 299                }),
 300                ..Default::default()
 301            },
 302            ..Default::default()
 303        },
 304    );
 305
 306    client_a
 307        .fs()
 308        .insert_tree(
 309            "/a",
 310            json!({
 311                "main.rs": "fn main() { a }",
 312                "other.rs": "",
 313            }),
 314        )
 315        .await;
 316    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 317    let project_id = active_call_a
 318        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 319        .await
 320        .unwrap();
 321    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 322
 323    // Open a file in an editor as the guest.
 324    let buffer_b = project_b
 325        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 326        .await
 327        .unwrap();
 328    let cx_b = cx_b.add_empty_window();
 329    let editor_b = cx_b.new_window_entity(|window, cx| {
 330        Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
 331    });
 332
 333    let fake_language_server = fake_language_servers.next().await.unwrap();
 334    cx_a.background_executor.run_until_parked();
 335
 336    buffer_b.read_with(cx_b, |buffer, _| {
 337        assert!(!buffer.completion_triggers().is_empty())
 338    });
 339
 340    // Type a completion trigger character as the guest.
 341    editor_b.update_in(cx_b, |editor, window, cx| {
 342        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
 343        editor.handle_input(".", window, cx);
 344    });
 345    cx_b.focus(&editor_b);
 346
 347    // Receive a completion request as the host's language server.
 348    // Return some completions from the host's language server.
 349    cx_a.executor().start_waiting();
 350    fake_language_server
 351        .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
 352            assert_eq!(
 353                params.text_document_position.text_document.uri,
 354                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 355            );
 356            assert_eq!(
 357                params.text_document_position.position,
 358                lsp::Position::new(0, 14),
 359            );
 360
 361            Ok(Some(lsp::CompletionResponse::Array(vec![
 362                lsp::CompletionItem {
 363                    label: "first_method(…)".into(),
 364                    detail: Some("fn(&mut self, B) -> C".into()),
 365                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 366                        new_text: "first_method($1)".to_string(),
 367                        range: lsp::Range::new(
 368                            lsp::Position::new(0, 14),
 369                            lsp::Position::new(0, 14),
 370                        ),
 371                    })),
 372                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 373                    ..Default::default()
 374                },
 375                lsp::CompletionItem {
 376                    label: "second_method(…)".into(),
 377                    detail: Some("fn(&mut self, C) -> D<E>".into()),
 378                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 379                        new_text: "second_method()".to_string(),
 380                        range: lsp::Range::new(
 381                            lsp::Position::new(0, 14),
 382                            lsp::Position::new(0, 14),
 383                        ),
 384                    })),
 385                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 386                    ..Default::default()
 387                },
 388            ])))
 389        })
 390        .next()
 391        .await
 392        .unwrap();
 393    cx_a.executor().finish_waiting();
 394
 395    // Open the buffer on the host.
 396    let buffer_a = project_a
 397        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 398        .await
 399        .unwrap();
 400    cx_a.executor().run_until_parked();
 401
 402    buffer_a.read_with(cx_a, |buffer, _| {
 403        assert_eq!(buffer.text(), "fn main() { a. }")
 404    });
 405
 406    // Confirm a completion on the guest.
 407    editor_b.update_in(cx_b, |editor, window, cx| {
 408        assert!(editor.context_menu_visible());
 409        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
 410        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
 411    });
 412
 413    // Return a resolved completion from the host's language server.
 414    // The resolved completion has an additional text edit.
 415    fake_language_server.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
 416        |params, _| async move {
 417            assert_eq!(params.label, "first_method(…)");
 418            Ok(lsp::CompletionItem {
 419                label: "first_method(…)".into(),
 420                detail: Some("fn(&mut self, B) -> C".into()),
 421                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 422                    new_text: "first_method($1)".to_string(),
 423                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
 424                })),
 425                additional_text_edits: Some(vec![lsp::TextEdit {
 426                    new_text: "use d::SomeTrait;\n".to_string(),
 427                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 428                }]),
 429                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 430                ..Default::default()
 431            })
 432        },
 433    );
 434
 435    // The additional edit is applied.
 436    cx_a.executor().run_until_parked();
 437
 438    buffer_a.read_with(cx_a, |buffer, _| {
 439        assert_eq!(
 440            buffer.text(),
 441            "use d::SomeTrait;\nfn main() { a.first_method() }"
 442        );
 443    });
 444
 445    buffer_b.read_with(cx_b, |buffer, _| {
 446        assert_eq!(
 447            buffer.text(),
 448            "use d::SomeTrait;\nfn main() { a.first_method() }"
 449        );
 450    });
 451
 452    // Now we do a second completion, this time to ensure that documentation/snippets are
 453    // resolved
 454    editor_b.update_in(cx_b, |editor, window, cx| {
 455        editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
 456        editor.handle_input("; a", window, cx);
 457        editor.handle_input(".", window, cx);
 458    });
 459
 460    buffer_b.read_with(cx_b, |buffer, _| {
 461        assert_eq!(
 462            buffer.text(),
 463            "use d::SomeTrait;\nfn main() { a.first_method(); a. }"
 464        );
 465    });
 466
 467    let mut completion_response = fake_language_server
 468        .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
 469            assert_eq!(
 470                params.text_document_position.text_document.uri,
 471                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 472            );
 473            assert_eq!(
 474                params.text_document_position.position,
 475                lsp::Position::new(1, 32),
 476            );
 477
 478            Ok(Some(lsp::CompletionResponse::Array(vec![
 479                lsp::CompletionItem {
 480                    label: "third_method(…)".into(),
 481                    detail: Some("fn(&mut self, B, C, D) -> E".into()),
 482                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 483                        // no snippet placehodlers
 484                        new_text: "third_method".to_string(),
 485                        range: lsp::Range::new(
 486                            lsp::Position::new(1, 32),
 487                            lsp::Position::new(1, 32),
 488                        ),
 489                    })),
 490                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 491                    documentation: None,
 492                    ..Default::default()
 493                },
 494            ])))
 495        });
 496
 497    // The completion now gets a new `text_edit.new_text` when resolving the completion item
 498    let mut resolve_completion_response = fake_language_server
 499        .set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
 500            assert_eq!(params.label, "third_method(…)");
 501            Ok(lsp::CompletionItem {
 502                label: "third_method(…)".into(),
 503                detail: Some("fn(&mut self, B, C, D) -> E".into()),
 504                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 505                    // Now it's a snippet
 506                    new_text: "third_method($1, $2, $3)".to_string(),
 507                    range: lsp::Range::new(lsp::Position::new(1, 32), lsp::Position::new(1, 32)),
 508                })),
 509                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 510                documentation: Some(lsp::Documentation::String(
 511                    "this is the documentation".into(),
 512                )),
 513                ..Default::default()
 514            })
 515        });
 516
 517    cx_b.executor().run_until_parked();
 518
 519    completion_response.next().await.unwrap();
 520
 521    editor_b.update_in(cx_b, |editor, window, cx| {
 522        assert!(editor.context_menu_visible());
 523        editor.context_menu_first(&ContextMenuFirst {}, window, cx);
 524    });
 525
 526    resolve_completion_response.next().await.unwrap();
 527    cx_b.executor().run_until_parked();
 528
 529    // When accepting the completion, the snippet is insert.
 530    editor_b.update_in(cx_b, |editor, window, cx| {
 531        assert!(editor.context_menu_visible());
 532        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
 533        assert_eq!(
 534            editor.text(cx),
 535            "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
 536        );
 537    });
 538}
 539
 540#[gpui::test(iterations = 10)]
 541async fn test_collaborating_with_code_actions(
 542    cx_a: &mut TestAppContext,
 543    cx_b: &mut TestAppContext,
 544) {
 545    let mut server = TestServer::start(cx_a.executor()).await;
 546    let client_a = server.create_client(cx_a, "user_a").await;
 547    //
 548    let client_b = server.create_client(cx_b, "user_b").await;
 549    server
 550        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 551        .await;
 552    let active_call_a = cx_a.read(ActiveCall::global);
 553
 554    cx_b.update(editor::init);
 555
 556    // Set up a fake language server.
 557    client_a.language_registry().add(rust_lang());
 558    let mut fake_language_servers = client_a
 559        .language_registry()
 560        .register_fake_lsp("Rust", FakeLspAdapter::default());
 561
 562    client_a
 563        .fs()
 564        .insert_tree(
 565            "/a",
 566            json!({
 567                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
 568                "other.rs": "pub fn foo() -> usize { 4 }",
 569            }),
 570        )
 571        .await;
 572    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 573    let project_id = active_call_a
 574        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 575        .await
 576        .unwrap();
 577
 578    // Join the project as client B.
 579    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 580    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 581    let editor_b = workspace_b
 582        .update_in(cx_b, |workspace, window, cx| {
 583            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
 584        })
 585        .await
 586        .unwrap()
 587        .downcast::<Editor>()
 588        .unwrap();
 589
 590    let mut fake_language_server = fake_language_servers.next().await.unwrap();
 591    let mut requests = fake_language_server
 592        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 593            assert_eq!(
 594                params.text_document.uri,
 595                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 596            );
 597            assert_eq!(params.range.start, lsp::Position::new(0, 0));
 598            assert_eq!(params.range.end, lsp::Position::new(0, 0));
 599            Ok(None)
 600        });
 601    cx_a.background_executor
 602        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 603    requests.next().await;
 604
 605    // Move cursor to a location that contains code actions.
 606    editor_b.update_in(cx_b, |editor, window, cx| {
 607        editor.change_selections(None, window, cx, |s| {
 608            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
 609        });
 610    });
 611    cx_b.focus(&editor_b);
 612
 613    let mut requests = fake_language_server
 614        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 615            assert_eq!(
 616                params.text_document.uri,
 617                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 618            );
 619            assert_eq!(params.range.start, lsp::Position::new(1, 31));
 620            assert_eq!(params.range.end, lsp::Position::new(1, 31));
 621
 622            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 623                lsp::CodeAction {
 624                    title: "Inline into all callers".to_string(),
 625                    edit: Some(lsp::WorkspaceEdit {
 626                        changes: Some(
 627                            [
 628                                (
 629                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
 630                                    vec![lsp::TextEdit::new(
 631                                        lsp::Range::new(
 632                                            lsp::Position::new(1, 22),
 633                                            lsp::Position::new(1, 34),
 634                                        ),
 635                                        "4".to_string(),
 636                                    )],
 637                                ),
 638                                (
 639                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
 640                                    vec![lsp::TextEdit::new(
 641                                        lsp::Range::new(
 642                                            lsp::Position::new(0, 0),
 643                                            lsp::Position::new(0, 27),
 644                                        ),
 645                                        "".to_string(),
 646                                    )],
 647                                ),
 648                            ]
 649                            .into_iter()
 650                            .collect(),
 651                        ),
 652                        ..Default::default()
 653                    }),
 654                    data: Some(json!({
 655                        "codeActionParams": {
 656                            "range": {
 657                                "start": {"line": 1, "column": 31},
 658                                "end": {"line": 1, "column": 31},
 659                            }
 660                        }
 661                    })),
 662                    ..Default::default()
 663                },
 664            )]))
 665        });
 666    cx_a.background_executor
 667        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 668    requests.next().await;
 669
 670    // Toggle code actions and wait for them to display.
 671    editor_b.update_in(cx_b, |editor, window, cx| {
 672        editor.toggle_code_actions(
 673            &ToggleCodeActions {
 674                deployed_from_indicator: None,
 675            },
 676            window,
 677            cx,
 678        );
 679    });
 680    cx_a.background_executor.run_until_parked();
 681
 682    editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 683
 684    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 685
 686    // Confirming the code action will trigger a resolve request.
 687    let confirm_action = editor_b
 688        .update_in(cx_b, |editor, window, cx| {
 689            Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
 690        })
 691        .unwrap();
 692    fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
 693        |_, _| async move {
 694            Ok(lsp::CodeAction {
 695                title: "Inline into all callers".to_string(),
 696                edit: Some(lsp::WorkspaceEdit {
 697                    changes: Some(
 698                        [
 699                            (
 700                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 701                                vec![lsp::TextEdit::new(
 702                                    lsp::Range::new(
 703                                        lsp::Position::new(1, 22),
 704                                        lsp::Position::new(1, 34),
 705                                    ),
 706                                    "4".to_string(),
 707                                )],
 708                            ),
 709                            (
 710                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
 711                                vec![lsp::TextEdit::new(
 712                                    lsp::Range::new(
 713                                        lsp::Position::new(0, 0),
 714                                        lsp::Position::new(0, 27),
 715                                    ),
 716                                    "".to_string(),
 717                                )],
 718                            ),
 719                        ]
 720                        .into_iter()
 721                        .collect(),
 722                    ),
 723                    ..Default::default()
 724                }),
 725                ..Default::default()
 726            })
 727        },
 728    );
 729
 730    // After the action is confirmed, an editor containing both modified files is opened.
 731    confirm_action.await.unwrap();
 732
 733    let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
 734        workspace
 735            .active_item(cx)
 736            .unwrap()
 737            .downcast::<Editor>()
 738            .unwrap()
 739    });
 740    code_action_editor.update_in(cx_b, |editor, window, cx| {
 741        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 742        editor.undo(&Undo, window, cx);
 743        assert_eq!(
 744            editor.text(cx),
 745            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
 746        );
 747        editor.redo(&Redo, window, cx);
 748        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 749    });
 750}
 751
 752#[gpui::test(iterations = 10)]
 753async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 754    let mut server = TestServer::start(cx_a.executor()).await;
 755    let client_a = server.create_client(cx_a, "user_a").await;
 756    let client_b = server.create_client(cx_b, "user_b").await;
 757    server
 758        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 759        .await;
 760    let active_call_a = cx_a.read(ActiveCall::global);
 761
 762    cx_b.update(editor::init);
 763
 764    // Set up a fake language server.
 765    client_a.language_registry().add(rust_lang());
 766    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
 767        "Rust",
 768        FakeLspAdapter {
 769            capabilities: lsp::ServerCapabilities {
 770                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 771                    prepare_provider: Some(true),
 772                    work_done_progress_options: Default::default(),
 773                })),
 774                ..Default::default()
 775            },
 776            ..Default::default()
 777        },
 778    );
 779
 780    client_a
 781        .fs()
 782        .insert_tree(
 783            "/dir",
 784            json!({
 785                "one.rs": "const ONE: usize = 1;",
 786                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 787            }),
 788        )
 789        .await;
 790    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 791    let project_id = active_call_a
 792        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 793        .await
 794        .unwrap();
 795    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 796
 797    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 798    let editor_b = workspace_b
 799        .update_in(cx_b, |workspace, window, cx| {
 800            workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
 801        })
 802        .await
 803        .unwrap()
 804        .downcast::<Editor>()
 805        .unwrap();
 806    let fake_language_server = fake_language_servers.next().await.unwrap();
 807
 808    // Move cursor to a location that can be renamed.
 809    let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
 810        editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
 811        editor.rename(&Rename, window, cx).unwrap()
 812    });
 813
 814    fake_language_server
 815        .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 816            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 817            assert_eq!(params.position, lsp::Position::new(0, 7));
 818            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 819                lsp::Position::new(0, 6),
 820                lsp::Position::new(0, 9),
 821            ))))
 822        })
 823        .next()
 824        .await
 825        .unwrap();
 826    prepare_rename.await.unwrap();
 827    editor_b.update(cx_b, |editor, cx| {
 828        use editor::ToOffset;
 829        let rename = editor.pending_rename().unwrap();
 830        let buffer = editor.buffer().read(cx).snapshot(cx);
 831        assert_eq!(
 832            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
 833            6..9
 834        );
 835        rename.editor.update(cx, |rename_editor, cx| {
 836            let rename_selection = rename_editor.selections.newest::<usize>(cx);
 837            assert_eq!(
 838                rename_selection.range(),
 839                0..3,
 840                "Rename that was triggered from zero selection caret, should propose the whole word."
 841            );
 842            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 843                rename_buffer.edit([(0..3, "THREE")], None, cx);
 844            });
 845        });
 846    });
 847
 848    // Cancel the rename, and repeat the same, but use selections instead of cursor movement
 849    editor_b.update_in(cx_b, |editor, window, cx| {
 850        editor.cancel(&editor::actions::Cancel, window, cx);
 851    });
 852    let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
 853        editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
 854        editor.rename(&Rename, window, cx).unwrap()
 855    });
 856
 857    fake_language_server
 858        .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 859            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 860            assert_eq!(params.position, lsp::Position::new(0, 8));
 861            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 862                lsp::Position::new(0, 6),
 863                lsp::Position::new(0, 9),
 864            ))))
 865        })
 866        .next()
 867        .await
 868        .unwrap();
 869    prepare_rename.await.unwrap();
 870    editor_b.update(cx_b, |editor, cx| {
 871        use editor::ToOffset;
 872        let rename = editor.pending_rename().unwrap();
 873        let buffer = editor.buffer().read(cx).snapshot(cx);
 874        let lsp_rename_start = rename.range.start.to_offset(&buffer);
 875        let lsp_rename_end = rename.range.end.to_offset(&buffer);
 876        assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
 877        rename.editor.update(cx, |rename_editor, cx| {
 878            let rename_selection = rename_editor.selections.newest::<usize>(cx);
 879            assert_eq!(
 880                rename_selection.range(),
 881                1..2,
 882                "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
 883            );
 884            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 885                rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
 886            });
 887        });
 888    });
 889
 890    let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
 891        Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
 892    });
 893    fake_language_server
 894        .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
 895            assert_eq!(
 896                params.text_document_position.text_document.uri.as_str(),
 897                "file:///dir/one.rs"
 898            );
 899            assert_eq!(
 900                params.text_document_position.position,
 901                lsp::Position::new(0, 6)
 902            );
 903            assert_eq!(params.new_name, "THREE");
 904            Ok(Some(lsp::WorkspaceEdit {
 905                changes: Some(
 906                    [
 907                        (
 908                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 909                            vec![lsp::TextEdit::new(
 910                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 911                                "THREE".to_string(),
 912                            )],
 913                        ),
 914                        (
 915                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 916                            vec![
 917                                lsp::TextEdit::new(
 918                                    lsp::Range::new(
 919                                        lsp::Position::new(0, 24),
 920                                        lsp::Position::new(0, 27),
 921                                    ),
 922                                    "THREE".to_string(),
 923                                ),
 924                                lsp::TextEdit::new(
 925                                    lsp::Range::new(
 926                                        lsp::Position::new(0, 35),
 927                                        lsp::Position::new(0, 38),
 928                                    ),
 929                                    "THREE".to_string(),
 930                                ),
 931                            ],
 932                        ),
 933                    ]
 934                    .into_iter()
 935                    .collect(),
 936                ),
 937                ..Default::default()
 938            }))
 939        })
 940        .next()
 941        .await
 942        .unwrap();
 943    confirm_rename.await.unwrap();
 944
 945    let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
 946        workspace.active_item_as::<Editor>(cx).unwrap()
 947    });
 948
 949    rename_editor.update_in(cx_b, |editor, window, cx| {
 950        assert_eq!(
 951            editor.text(cx),
 952            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 953        );
 954        editor.undo(&Undo, window, cx);
 955        assert_eq!(
 956            editor.text(cx),
 957            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 958        );
 959        editor.redo(&Redo, window, cx);
 960        assert_eq!(
 961            editor.text(cx),
 962            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 963        );
 964    });
 965
 966    // Ensure temporary rename edits cannot be undone/redone.
 967    editor_b.update_in(cx_b, |editor, window, cx| {
 968        editor.undo(&Undo, window, cx);
 969        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 970        editor.undo(&Undo, window, cx);
 971        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 972        editor.redo(&Redo, window, cx);
 973        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 974    })
 975}
 976
 977#[gpui::test(iterations = 10)]
 978async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 979    let mut server = TestServer::start(cx_a.executor()).await;
 980    let executor = cx_a.executor();
 981    let client_a = server.create_client(cx_a, "user_a").await;
 982    let client_b = server.create_client(cx_b, "user_b").await;
 983    server
 984        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 985        .await;
 986    let active_call_a = cx_a.read(ActiveCall::global);
 987
 988    cx_b.update(editor::init);
 989
 990    client_a.language_registry().add(rust_lang());
 991    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
 992        "Rust",
 993        FakeLspAdapter {
 994            name: "the-language-server",
 995            ..Default::default()
 996        },
 997    );
 998
 999    client_a
1000        .fs()
1001        .insert_tree(
1002            "/dir",
1003            json!({
1004                "main.rs": "const ONE: usize = 1;",
1005            }),
1006        )
1007        .await;
1008    let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
1009
1010    let _buffer_a = project_a
1011        .update(cx_a, |p, cx| {
1012            p.open_local_buffer_with_lsp("/dir/main.rs", cx)
1013        })
1014        .await
1015        .unwrap();
1016
1017    let fake_language_server = fake_language_servers.next().await.unwrap();
1018    fake_language_server.start_progress("the-token").await;
1019
1020    executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1021    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1022        token: lsp::NumberOrString::String("the-token".to_string()),
1023        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1024            lsp::WorkDoneProgressReport {
1025                message: Some("the-message".to_string()),
1026                ..Default::default()
1027            },
1028        )),
1029    });
1030    executor.run_until_parked();
1031
1032    project_a.read_with(cx_a, |project, cx| {
1033        let status = project.language_server_statuses(cx).next().unwrap().1;
1034        assert_eq!(status.name, "the-language-server");
1035        assert_eq!(status.pending_work.len(), 1);
1036        assert_eq!(
1037            status.pending_work["the-token"].message.as_ref().unwrap(),
1038            "the-message"
1039        );
1040    });
1041
1042    let project_id = active_call_a
1043        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1044        .await
1045        .unwrap();
1046    executor.run_until_parked();
1047    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1048
1049    project_b.read_with(cx_b, |project, cx| {
1050        let status = project.language_server_statuses(cx).next().unwrap().1;
1051        assert_eq!(status.name, "the-language-server");
1052    });
1053
1054    executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1055    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1056        token: lsp::NumberOrString::String("the-token".to_string()),
1057        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1058            lsp::WorkDoneProgressReport {
1059                message: Some("the-message-2".to_string()),
1060                ..Default::default()
1061            },
1062        )),
1063    });
1064    executor.run_until_parked();
1065
1066    project_a.read_with(cx_a, |project, cx| {
1067        let status = project.language_server_statuses(cx).next().unwrap().1;
1068        assert_eq!(status.name, "the-language-server");
1069        assert_eq!(status.pending_work.len(), 1);
1070        assert_eq!(
1071            status.pending_work["the-token"].message.as_ref().unwrap(),
1072            "the-message-2"
1073        );
1074    });
1075
1076    project_b.read_with(cx_b, |project, cx| {
1077        let status = project.language_server_statuses(cx).next().unwrap().1;
1078        assert_eq!(status.name, "the-language-server");
1079        assert_eq!(status.pending_work.len(), 1);
1080        assert_eq!(
1081            status.pending_work["the-token"].message.as_ref().unwrap(),
1082            "the-message-2"
1083        );
1084    });
1085}
1086
1087#[gpui::test(iterations = 10)]
1088async fn test_share_project(
1089    cx_a: &mut TestAppContext,
1090    cx_b: &mut TestAppContext,
1091    cx_c: &mut TestAppContext,
1092) {
1093    let executor = cx_a.executor();
1094    let cx_b = cx_b.add_empty_window();
1095    let mut server = TestServer::start(executor.clone()).await;
1096    let client_a = server.create_client(cx_a, "user_a").await;
1097    let client_b = server.create_client(cx_b, "user_b").await;
1098    let client_c = server.create_client(cx_c, "user_c").await;
1099    server
1100        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1101        .await;
1102    let active_call_a = cx_a.read(ActiveCall::global);
1103    let active_call_b = cx_b.read(ActiveCall::global);
1104    let active_call_c = cx_c.read(ActiveCall::global);
1105
1106    client_a
1107        .fs()
1108        .insert_tree(
1109            "/a",
1110            json!({
1111                ".gitignore": "ignored-dir",
1112                "a.txt": "a-contents",
1113                "b.txt": "b-contents",
1114                "ignored-dir": {
1115                    "c.txt": "",
1116                    "d.txt": "",
1117                }
1118            }),
1119        )
1120        .await;
1121
1122    // Invite client B to collaborate on a project
1123    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1124    active_call_a
1125        .update(cx_a, |call, cx| {
1126            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1127        })
1128        .await
1129        .unwrap();
1130
1131    // Join that project as client B
1132
1133    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1134    executor.run_until_parked();
1135    let call = incoming_call_b.borrow().clone().unwrap();
1136    assert_eq!(call.calling_user.github_login, "user_a");
1137    let initial_project = call.initial_project.unwrap();
1138    active_call_b
1139        .update(cx_b, |call, cx| call.accept_incoming(cx))
1140        .await
1141        .unwrap();
1142    let client_b_peer_id = client_b.peer_id().unwrap();
1143    let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1144
1145    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1146
1147    executor.run_until_parked();
1148
1149    project_a.read_with(cx_a, |project, _| {
1150        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1151        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1152    });
1153
1154    project_b.read_with(cx_b, |project, cx| {
1155        let worktree = project.worktrees(cx).next().unwrap().read(cx);
1156        assert_eq!(
1157            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1158            [
1159                Path::new(".gitignore"),
1160                Path::new("a.txt"),
1161                Path::new("b.txt"),
1162                Path::new("ignored-dir"),
1163            ]
1164        );
1165    });
1166
1167    project_b
1168        .update(cx_b, |project, cx| {
1169            let worktree = project.worktrees(cx).next().unwrap();
1170            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1171            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1172        })
1173        .await
1174        .unwrap();
1175
1176    project_b.read_with(cx_b, |project, cx| {
1177        let worktree = project.worktrees(cx).next().unwrap().read(cx);
1178        assert_eq!(
1179            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1180            [
1181                Path::new(".gitignore"),
1182                Path::new("a.txt"),
1183                Path::new("b.txt"),
1184                Path::new("ignored-dir"),
1185                Path::new("ignored-dir/c.txt"),
1186                Path::new("ignored-dir/d.txt"),
1187            ]
1188        );
1189    });
1190
1191    // Open the same file as client B and client A.
1192    let buffer_b = project_b
1193        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1194        .await
1195        .unwrap();
1196
1197    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1198
1199    project_a.read_with(cx_a, |project, cx| {
1200        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1201    });
1202    let buffer_a = project_a
1203        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1204        .await
1205        .unwrap();
1206
1207    let editor_b =
1208        cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
1209
1210    // Client A sees client B's selection
1211    executor.run_until_parked();
1212
1213    buffer_a.read_with(cx_a, |buffer, _| {
1214        buffer
1215            .snapshot()
1216            .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1217            .count()
1218            == 1
1219    });
1220
1221    // Edit the buffer as client B and see that edit as client A.
1222    editor_b.update_in(cx_b, |editor, window, cx| {
1223        editor.handle_input("ok, ", window, cx)
1224    });
1225    executor.run_until_parked();
1226
1227    buffer_a.read_with(cx_a, |buffer, _| {
1228        assert_eq!(buffer.text(), "ok, b-contents")
1229    });
1230
1231    // Client B can invite client C on a project shared by client A.
1232    active_call_b
1233        .update(cx_b, |call, cx| {
1234            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1235        })
1236        .await
1237        .unwrap();
1238
1239    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1240    executor.run_until_parked();
1241    let call = incoming_call_c.borrow().clone().unwrap();
1242    assert_eq!(call.calling_user.github_login, "user_b");
1243    let initial_project = call.initial_project.unwrap();
1244    active_call_c
1245        .update(cx_c, |call, cx| call.accept_incoming(cx))
1246        .await
1247        .unwrap();
1248    let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1249
1250    // Client B closes the editor, and client A sees client B's selections removed.
1251    cx_b.update(move |_, _| drop(editor_b));
1252    executor.run_until_parked();
1253
1254    buffer_a.read_with(cx_a, |buffer, _| {
1255        buffer
1256            .snapshot()
1257            .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1258            .count()
1259            == 0
1260    });
1261}
1262
1263#[gpui::test(iterations = 10)]
1264async fn test_on_input_format_from_host_to_guest(
1265    cx_a: &mut TestAppContext,
1266    cx_b: &mut TestAppContext,
1267) {
1268    let mut server = TestServer::start(cx_a.executor()).await;
1269    let executor = cx_a.executor();
1270    let client_a = server.create_client(cx_a, "user_a").await;
1271    let client_b = server.create_client(cx_b, "user_b").await;
1272    server
1273        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1274        .await;
1275    let active_call_a = cx_a.read(ActiveCall::global);
1276
1277    client_a.language_registry().add(rust_lang());
1278    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1279        "Rust",
1280        FakeLspAdapter {
1281            capabilities: lsp::ServerCapabilities {
1282                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1283                    first_trigger_character: ":".to_string(),
1284                    more_trigger_character: Some(vec![">".to_string()]),
1285                }),
1286                ..Default::default()
1287            },
1288            ..Default::default()
1289        },
1290    );
1291
1292    client_a
1293        .fs()
1294        .insert_tree(
1295            "/a",
1296            json!({
1297                "main.rs": "fn main() { a }",
1298                "other.rs": "// Test file",
1299            }),
1300        )
1301        .await;
1302    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1303    let project_id = active_call_a
1304        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1305        .await
1306        .unwrap();
1307    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1308
1309    // Open a file in an editor as the host.
1310    let buffer_a = project_a
1311        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1312        .await
1313        .unwrap();
1314    let cx_a = cx_a.add_empty_window();
1315    let editor_a = cx_a.new_window_entity(|window, cx| {
1316        Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1317    });
1318
1319    let fake_language_server = fake_language_servers.next().await.unwrap();
1320    executor.run_until_parked();
1321
1322    // Receive an OnTypeFormatting request as the host's language server.
1323    // Return some formatting from the host's language server.
1324    fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1325        |params, _| async move {
1326            assert_eq!(
1327                params.text_document_position.text_document.uri,
1328                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1329            );
1330            assert_eq!(
1331                params.text_document_position.position,
1332                lsp::Position::new(0, 14),
1333            );
1334
1335            Ok(Some(vec![lsp::TextEdit {
1336                new_text: "~<".to_string(),
1337                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1338            }]))
1339        },
1340    );
1341
1342    // Open the buffer on the guest and see that the formatting worked
1343    let buffer_b = project_b
1344        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1345        .await
1346        .unwrap();
1347
1348    // Type a on type formatting trigger character as the guest.
1349    cx_a.focus(&editor_a);
1350    editor_a.update_in(cx_a, |editor, window, cx| {
1351        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1352        editor.handle_input(">", window, cx);
1353    });
1354
1355    executor.run_until_parked();
1356
1357    buffer_b.read_with(cx_b, |buffer, _| {
1358        assert_eq!(buffer.text(), "fn main() { a>~< }")
1359    });
1360
1361    // Undo should remove LSP edits first
1362    editor_a.update_in(cx_a, |editor, window, cx| {
1363        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1364        editor.undo(&Undo, window, cx);
1365        assert_eq!(editor.text(cx), "fn main() { a> }");
1366    });
1367    executor.run_until_parked();
1368
1369    buffer_b.read_with(cx_b, |buffer, _| {
1370        assert_eq!(buffer.text(), "fn main() { a> }")
1371    });
1372
1373    editor_a.update_in(cx_a, |editor, window, cx| {
1374        assert_eq!(editor.text(cx), "fn main() { a> }");
1375        editor.undo(&Undo, window, cx);
1376        assert_eq!(editor.text(cx), "fn main() { a }");
1377    });
1378    executor.run_until_parked();
1379
1380    buffer_b.read_with(cx_b, |buffer, _| {
1381        assert_eq!(buffer.text(), "fn main() { a }")
1382    });
1383}
1384
1385#[gpui::test(iterations = 10)]
1386async fn test_on_input_format_from_guest_to_host(
1387    cx_a: &mut TestAppContext,
1388    cx_b: &mut TestAppContext,
1389) {
1390    let mut server = TestServer::start(cx_a.executor()).await;
1391    let executor = cx_a.executor();
1392    let client_a = server.create_client(cx_a, "user_a").await;
1393    let client_b = server.create_client(cx_b, "user_b").await;
1394    server
1395        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1396        .await;
1397    let active_call_a = cx_a.read(ActiveCall::global);
1398
1399    client_a.language_registry().add(rust_lang());
1400    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1401        "Rust",
1402        FakeLspAdapter {
1403            capabilities: lsp::ServerCapabilities {
1404                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1405                    first_trigger_character: ":".to_string(),
1406                    more_trigger_character: Some(vec![">".to_string()]),
1407                }),
1408                ..Default::default()
1409            },
1410            ..Default::default()
1411        },
1412    );
1413
1414    client_a
1415        .fs()
1416        .insert_tree(
1417            "/a",
1418            json!({
1419                "main.rs": "fn main() { a }",
1420                "other.rs": "// Test file",
1421            }),
1422        )
1423        .await;
1424    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1425    let project_id = active_call_a
1426        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1427        .await
1428        .unwrap();
1429    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1430
1431    // Open a file in an editor as the guest.
1432    let buffer_b = project_b
1433        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1434        .await
1435        .unwrap();
1436    let cx_b = cx_b.add_empty_window();
1437    let editor_b = cx_b.new_window_entity(|window, cx| {
1438        Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1439    });
1440
1441    let fake_language_server = fake_language_servers.next().await.unwrap();
1442    executor.run_until_parked();
1443
1444    // Type a on type formatting trigger character as the guest.
1445    cx_b.focus(&editor_b);
1446    editor_b.update_in(cx_b, |editor, window, cx| {
1447        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1448        editor.handle_input(":", window, cx);
1449    });
1450
1451    // Receive an OnTypeFormatting request as the host's language server.
1452    // Return some formatting from the host's language server.
1453    executor.start_waiting();
1454    fake_language_server
1455        .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1456            assert_eq!(
1457                params.text_document_position.text_document.uri,
1458                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1459            );
1460            assert_eq!(
1461                params.text_document_position.position,
1462                lsp::Position::new(0, 14),
1463            );
1464
1465            Ok(Some(vec![lsp::TextEdit {
1466                new_text: "~:".to_string(),
1467                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1468            }]))
1469        })
1470        .next()
1471        .await
1472        .unwrap();
1473    executor.finish_waiting();
1474
1475    // Open the buffer on the host and see that the formatting worked
1476    let buffer_a = project_a
1477        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1478        .await
1479        .unwrap();
1480    executor.run_until_parked();
1481
1482    buffer_a.read_with(cx_a, |buffer, _| {
1483        assert_eq!(buffer.text(), "fn main() { a:~: }")
1484    });
1485
1486    // Undo should remove LSP edits first
1487    editor_b.update_in(cx_b, |editor, window, cx| {
1488        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1489        editor.undo(&Undo, window, cx);
1490        assert_eq!(editor.text(cx), "fn main() { a: }");
1491    });
1492    executor.run_until_parked();
1493
1494    buffer_a.read_with(cx_a, |buffer, _| {
1495        assert_eq!(buffer.text(), "fn main() { a: }")
1496    });
1497
1498    editor_b.update_in(cx_b, |editor, window, cx| {
1499        assert_eq!(editor.text(cx), "fn main() { a: }");
1500        editor.undo(&Undo, window, cx);
1501        assert_eq!(editor.text(cx), "fn main() { a }");
1502    });
1503    executor.run_until_parked();
1504
1505    buffer_a.read_with(cx_a, |buffer, _| {
1506        assert_eq!(buffer.text(), "fn main() { a }")
1507    });
1508}
1509
1510#[gpui::test(iterations = 10)]
1511async fn test_mutual_editor_inlay_hint_cache_update(
1512    cx_a: &mut TestAppContext,
1513    cx_b: &mut TestAppContext,
1514) {
1515    let mut server = TestServer::start(cx_a.executor()).await;
1516    let executor = cx_a.executor();
1517    let client_a = server.create_client(cx_a, "user_a").await;
1518    let client_b = server.create_client(cx_b, "user_b").await;
1519    server
1520        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1521        .await;
1522    let active_call_a = cx_a.read(ActiveCall::global);
1523    let active_call_b = cx_b.read(ActiveCall::global);
1524
1525    cx_a.update(editor::init);
1526    cx_b.update(editor::init);
1527
1528    cx_a.update(|cx| {
1529        SettingsStore::update_global(cx, |store, cx| {
1530            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1531                settings.defaults.inlay_hints = Some(InlayHintSettings {
1532                    enabled: true,
1533                    edit_debounce_ms: 0,
1534                    scroll_debounce_ms: 0,
1535                    show_type_hints: true,
1536                    show_parameter_hints: false,
1537                    show_other_hints: true,
1538                    show_background: false,
1539                    toggle_on_modifiers_press: None,
1540                })
1541            });
1542        });
1543    });
1544    cx_b.update(|cx| {
1545        SettingsStore::update_global(cx, |store, cx| {
1546            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1547                settings.defaults.inlay_hints = Some(InlayHintSettings {
1548                    enabled: true,
1549                    edit_debounce_ms: 0,
1550                    scroll_debounce_ms: 0,
1551                    show_type_hints: true,
1552                    show_parameter_hints: false,
1553                    show_other_hints: true,
1554                    show_background: false,
1555                    toggle_on_modifiers_press: None,
1556                })
1557            });
1558        });
1559    });
1560
1561    client_a.language_registry().add(rust_lang());
1562    client_b.language_registry().add(rust_lang());
1563    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1564        "Rust",
1565        FakeLspAdapter {
1566            capabilities: lsp::ServerCapabilities {
1567                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1568                ..Default::default()
1569            },
1570            ..Default::default()
1571        },
1572    );
1573
1574    // Client A opens a project.
1575    client_a
1576        .fs()
1577        .insert_tree(
1578            "/a",
1579            json!({
1580                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1581                "other.rs": "// Test file",
1582            }),
1583        )
1584        .await;
1585    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1586    active_call_a
1587        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1588        .await
1589        .unwrap();
1590    let project_id = active_call_a
1591        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1592        .await
1593        .unwrap();
1594
1595    // Client B joins the project
1596    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1597    active_call_b
1598        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1599        .await
1600        .unwrap();
1601
1602    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1603    executor.start_waiting();
1604
1605    // The host opens a rust file.
1606    let _buffer_a = project_a
1607        .update(cx_a, |project, cx| {
1608            project.open_local_buffer("/a/main.rs", cx)
1609        })
1610        .await
1611        .unwrap();
1612    let editor_a = workspace_a
1613        .update_in(cx_a, |workspace, window, cx| {
1614            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1615        })
1616        .await
1617        .unwrap()
1618        .downcast::<Editor>()
1619        .unwrap();
1620
1621    let fake_language_server = fake_language_servers.next().await.unwrap();
1622
1623    // Set up the language server to return an additional inlay hint on each request.
1624    let edits_made = Arc::new(AtomicUsize::new(0));
1625    let closure_edits_made = Arc::clone(&edits_made);
1626    fake_language_server
1627        .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1628            let task_edits_made = Arc::clone(&closure_edits_made);
1629            async move {
1630                assert_eq!(
1631                    params.text_document.uri,
1632                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1633                );
1634                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1635                Ok(Some(vec![lsp::InlayHint {
1636                    position: lsp::Position::new(0, edits_made as u32),
1637                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1638                    kind: None,
1639                    text_edits: None,
1640                    tooltip: None,
1641                    padding_left: None,
1642                    padding_right: None,
1643                    data: None,
1644                }]))
1645            }
1646        })
1647        .next()
1648        .await
1649        .unwrap();
1650
1651    executor.run_until_parked();
1652
1653    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1654    editor_a.update(cx_a, |editor, _| {
1655        assert_eq!(
1656            vec![initial_edit.to_string()],
1657            extract_hint_labels(editor),
1658            "Host should get its first hints when opens an editor"
1659        );
1660    });
1661    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1662    let editor_b = workspace_b
1663        .update_in(cx_b, |workspace, window, cx| {
1664            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1665        })
1666        .await
1667        .unwrap()
1668        .downcast::<Editor>()
1669        .unwrap();
1670
1671    executor.run_until_parked();
1672    editor_b.update(cx_b, |editor, _| {
1673        assert_eq!(
1674            vec![initial_edit.to_string()],
1675            extract_hint_labels(editor),
1676            "Client should get its first hints when opens an editor"
1677        );
1678    });
1679
1680    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1681    editor_b.update_in(cx_b, |editor, window, cx| {
1682        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
1683        editor.handle_input(":", window, cx);
1684    });
1685    cx_b.focus(&editor_b);
1686
1687    executor.run_until_parked();
1688    editor_a.update(cx_a, |editor, _| {
1689        assert_eq!(
1690            vec![after_client_edit.to_string()],
1691            extract_hint_labels(editor),
1692        );
1693    });
1694    editor_b.update(cx_b, |editor, _| {
1695        assert_eq!(
1696            vec![after_client_edit.to_string()],
1697            extract_hint_labels(editor),
1698        );
1699    });
1700
1701    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1702    editor_a.update_in(cx_a, |editor, window, cx| {
1703        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1704        editor.handle_input("a change to increment both buffers' versions", window, cx);
1705    });
1706    cx_a.focus(&editor_a);
1707
1708    executor.run_until_parked();
1709    editor_a.update(cx_a, |editor, _| {
1710        assert_eq!(
1711            vec![after_host_edit.to_string()],
1712            extract_hint_labels(editor),
1713        );
1714    });
1715    editor_b.update(cx_b, |editor, _| {
1716        assert_eq!(
1717            vec![after_host_edit.to_string()],
1718            extract_hint_labels(editor),
1719        );
1720    });
1721
1722    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1723    fake_language_server
1724        .request::<lsp::request::InlayHintRefreshRequest>(())
1725        .await
1726        .expect("inlay refresh request failed");
1727
1728    executor.run_until_parked();
1729    editor_a.update(cx_a, |editor, _| {
1730        assert_eq!(
1731            vec![after_special_edit_for_refresh.to_string()],
1732            extract_hint_labels(editor),
1733            "Host should react to /refresh LSP request"
1734        );
1735    });
1736    editor_b.update(cx_b, |editor, _| {
1737        assert_eq!(
1738            vec![after_special_edit_for_refresh.to_string()],
1739            extract_hint_labels(editor),
1740            "Guest should get a /refresh LSP request propagated by host"
1741        );
1742    });
1743}
1744
1745#[gpui::test(iterations = 10)]
1746async fn test_inlay_hint_refresh_is_forwarded(
1747    cx_a: &mut TestAppContext,
1748    cx_b: &mut TestAppContext,
1749) {
1750    let mut server = TestServer::start(cx_a.executor()).await;
1751    let executor = cx_a.executor();
1752    let client_a = server.create_client(cx_a, "user_a").await;
1753    let client_b = server.create_client(cx_b, "user_b").await;
1754    server
1755        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1756        .await;
1757    let active_call_a = cx_a.read(ActiveCall::global);
1758    let active_call_b = cx_b.read(ActiveCall::global);
1759
1760    cx_a.update(editor::init);
1761    cx_b.update(editor::init);
1762
1763    cx_a.update(|cx| {
1764        SettingsStore::update_global(cx, |store, cx| {
1765            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1766                settings.defaults.inlay_hints = Some(InlayHintSettings {
1767                    enabled: false,
1768                    edit_debounce_ms: 0,
1769                    scroll_debounce_ms: 0,
1770                    show_type_hints: false,
1771                    show_parameter_hints: false,
1772                    show_other_hints: false,
1773                    show_background: false,
1774                    toggle_on_modifiers_press: None,
1775                })
1776            });
1777        });
1778    });
1779    cx_b.update(|cx| {
1780        SettingsStore::update_global(cx, |store, cx| {
1781            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1782                settings.defaults.inlay_hints = Some(InlayHintSettings {
1783                    enabled: true,
1784                    edit_debounce_ms: 0,
1785                    scroll_debounce_ms: 0,
1786                    show_type_hints: true,
1787                    show_parameter_hints: true,
1788                    show_other_hints: true,
1789                    show_background: false,
1790                    toggle_on_modifiers_press: None,
1791                })
1792            });
1793        });
1794    });
1795
1796    client_a.language_registry().add(rust_lang());
1797    client_b.language_registry().add(rust_lang());
1798    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1799        "Rust",
1800        FakeLspAdapter {
1801            capabilities: lsp::ServerCapabilities {
1802                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1803                ..Default::default()
1804            },
1805            ..Default::default()
1806        },
1807    );
1808
1809    client_a
1810        .fs()
1811        .insert_tree(
1812            "/a",
1813            json!({
1814                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1815                "other.rs": "// Test file",
1816            }),
1817        )
1818        .await;
1819    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1820    active_call_a
1821        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1822        .await
1823        .unwrap();
1824    let project_id = active_call_a
1825        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1826        .await
1827        .unwrap();
1828
1829    let project_b = client_b.join_remote_project(project_id, cx_b).await;
1830    active_call_b
1831        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1832        .await
1833        .unwrap();
1834
1835    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1836    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1837
1838    cx_a.background_executor.start_waiting();
1839
1840    let editor_a = workspace_a
1841        .update_in(cx_a, |workspace, window, cx| {
1842            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1843        })
1844        .await
1845        .unwrap()
1846        .downcast::<Editor>()
1847        .unwrap();
1848
1849    let editor_b = workspace_b
1850        .update_in(cx_b, |workspace, window, cx| {
1851            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1852        })
1853        .await
1854        .unwrap()
1855        .downcast::<Editor>()
1856        .unwrap();
1857
1858    let other_hints = Arc::new(AtomicBool::new(false));
1859    let fake_language_server = fake_language_servers.next().await.unwrap();
1860    let closure_other_hints = Arc::clone(&other_hints);
1861    fake_language_server
1862        .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1863            let task_other_hints = Arc::clone(&closure_other_hints);
1864            async move {
1865                assert_eq!(
1866                    params.text_document.uri,
1867                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1868                );
1869                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1870                let character = if other_hints { 0 } else { 2 };
1871                let label = if other_hints {
1872                    "other hint"
1873                } else {
1874                    "initial hint"
1875                };
1876                Ok(Some(vec![lsp::InlayHint {
1877                    position: lsp::Position::new(0, character),
1878                    label: lsp::InlayHintLabel::String(label.to_string()),
1879                    kind: None,
1880                    text_edits: None,
1881                    tooltip: None,
1882                    padding_left: None,
1883                    padding_right: None,
1884                    data: None,
1885                }]))
1886            }
1887        })
1888        .next()
1889        .await
1890        .unwrap();
1891    executor.finish_waiting();
1892
1893    executor.run_until_parked();
1894    editor_a.update(cx_a, |editor, _| {
1895        assert!(
1896            extract_hint_labels(editor).is_empty(),
1897            "Host should get no hints due to them turned off"
1898        );
1899    });
1900
1901    executor.run_until_parked();
1902    editor_b.update(cx_b, |editor, _| {
1903        assert_eq!(
1904            vec!["initial hint".to_string()],
1905            extract_hint_labels(editor),
1906            "Client should get its first hints when opens an editor"
1907        );
1908    });
1909
1910    other_hints.fetch_or(true, atomic::Ordering::Release);
1911    fake_language_server
1912        .request::<lsp::request::InlayHintRefreshRequest>(())
1913        .await
1914        .expect("inlay refresh request failed");
1915    executor.run_until_parked();
1916    editor_a.update(cx_a, |editor, _| {
1917        assert!(
1918            extract_hint_labels(editor).is_empty(),
1919            "Host should get no hints due to them turned off, even after the /refresh"
1920        );
1921    });
1922
1923    executor.run_until_parked();
1924    editor_b.update(cx_b, |editor, _| {
1925        assert_eq!(
1926            vec!["other hint".to_string()],
1927            extract_hint_labels(editor),
1928            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1929        );
1930    });
1931}
1932
1933#[gpui::test(iterations = 10)]
1934async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1935    let mut server = TestServer::start(cx_a.executor()).await;
1936    let client_a = server.create_client(cx_a, "user_a").await;
1937    let client_b = server.create_client(cx_b, "user_b").await;
1938    server
1939        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1940        .await;
1941    let active_call_a = cx_a.read(ActiveCall::global);
1942
1943    cx_a.update(editor::init);
1944    cx_b.update(editor::init);
1945    // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1946    let inline_blame_off_settings = Some(InlineBlameSettings {
1947        enabled: false,
1948        delay_ms: None,
1949        min_column: None,
1950        show_commit_summary: false,
1951    });
1952    cx_a.update(|cx| {
1953        SettingsStore::update_global(cx, |store, cx| {
1954            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1955                settings.git.inline_blame = inline_blame_off_settings;
1956            });
1957        });
1958    });
1959    cx_b.update(|cx| {
1960        SettingsStore::update_global(cx, |store, cx| {
1961            store.update_user_settings::<ProjectSettings>(cx, |settings| {
1962                settings.git.inline_blame = inline_blame_off_settings;
1963            });
1964        });
1965    });
1966
1967    client_a
1968        .fs()
1969        .insert_tree(
1970            "/my-repo",
1971            json!({
1972                ".git": {},
1973                "file.txt": "line1\nline2\nline3\nline\n",
1974            }),
1975        )
1976        .await;
1977
1978    let blame = git::blame::Blame {
1979        entries: vec![
1980            blame_entry("1b1b1b", 0..1),
1981            blame_entry("0d0d0d", 1..2),
1982            blame_entry("3a3a3a", 2..3),
1983            blame_entry("4c4c4c", 3..4),
1984        ],
1985        messages: [
1986            ("1b1b1b", "message for idx-0"),
1987            ("0d0d0d", "message for idx-1"),
1988            ("3a3a3a", "message for idx-2"),
1989            ("4c4c4c", "message for idx-3"),
1990        ]
1991        .into_iter()
1992        .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
1993        .collect(),
1994        remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
1995    };
1996    client_a
1997        .fs()
1998        .set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
1999
2000    let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2001    let project_id = active_call_a
2002        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2003        .await
2004        .unwrap();
2005
2006    // Create editor_a
2007    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2008    let editor_a = workspace_a
2009        .update_in(cx_a, |workspace, window, cx| {
2010            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2011        })
2012        .await
2013        .unwrap()
2014        .downcast::<Editor>()
2015        .unwrap();
2016
2017    // Join the project as client B.
2018    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2019    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2020    let editor_b = workspace_b
2021        .update_in(cx_b, |workspace, window, cx| {
2022            workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2023        })
2024        .await
2025        .unwrap()
2026        .downcast::<Editor>()
2027        .unwrap();
2028    let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
2029        editor_b
2030            .buffer()
2031            .read(cx)
2032            .as_singleton()
2033            .unwrap()
2034            .read(cx)
2035            .remote_id()
2036    });
2037
2038    // client_b now requests git blame for the open buffer
2039    editor_b.update_in(cx_b, |editor_b, window, cx| {
2040        assert!(editor_b.blame().is_none());
2041        editor_b.toggle_git_blame(&git::Blame {}, window, cx);
2042    });
2043
2044    cx_a.executor().run_until_parked();
2045    cx_b.executor().run_until_parked();
2046
2047    editor_b.update(cx_b, |editor_b, cx| {
2048        let blame = editor_b.blame().expect("editor_b should have blame now");
2049        let entries = blame.update(cx, |blame, cx| {
2050            blame
2051                .blame_for_rows(
2052                    &(0..4)
2053                        .map(|row| RowInfo {
2054                            buffer_row: Some(row),
2055                            buffer_id: Some(buffer_id_b),
2056                            ..Default::default()
2057                        })
2058                        .collect::<Vec<_>>(),
2059                    cx,
2060                )
2061                .collect::<Vec<_>>()
2062        });
2063
2064        assert_eq!(
2065            entries,
2066            vec![
2067                Some(blame_entry("1b1b1b", 0..1)),
2068                Some(blame_entry("0d0d0d", 1..2)),
2069                Some(blame_entry("3a3a3a", 2..3)),
2070                Some(blame_entry("4c4c4c", 3..4)),
2071            ]
2072        );
2073
2074        blame.update(cx, |blame, _| {
2075            for (idx, entry) in entries.iter().flatten().enumerate() {
2076                let details = blame.details_for_entry(entry).unwrap();
2077                assert_eq!(details.message, format!("message for idx-{}", idx));
2078                assert_eq!(
2079                    details.permalink.unwrap().to_string(),
2080                    format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2081                );
2082            }
2083        });
2084    });
2085
2086    // editor_b updates the file, which gets sent to client_a, which updates git blame,
2087    // which gets back to client_b.
2088    editor_b.update_in(cx_b, |editor_b, _, cx| {
2089        editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2090    });
2091
2092    cx_a.executor().run_until_parked();
2093    cx_b.executor().run_until_parked();
2094
2095    editor_b.update(cx_b, |editor_b, cx| {
2096        let blame = editor_b.blame().expect("editor_b should have blame now");
2097        let entries = blame.update(cx, |blame, cx| {
2098            blame
2099                .blame_for_rows(
2100                    &(0..4)
2101                        .map(|row| RowInfo {
2102                            buffer_row: Some(row),
2103                            buffer_id: Some(buffer_id_b),
2104                            ..Default::default()
2105                        })
2106                        .collect::<Vec<_>>(),
2107                    cx,
2108                )
2109                .collect::<Vec<_>>()
2110        });
2111
2112        assert_eq!(
2113            entries,
2114            vec![
2115                None,
2116                Some(blame_entry("0d0d0d", 1..2)),
2117                Some(blame_entry("3a3a3a", 2..3)),
2118                Some(blame_entry("4c4c4c", 3..4)),
2119            ]
2120        );
2121    });
2122
2123    // Now editor_a also updates the file
2124    editor_a.update_in(cx_a, |editor_a, _, cx| {
2125        editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2126    });
2127
2128    cx_a.executor().run_until_parked();
2129    cx_b.executor().run_until_parked();
2130
2131    editor_b.update(cx_b, |editor_b, cx| {
2132        let blame = editor_b.blame().expect("editor_b should have blame now");
2133        let entries = blame.update(cx, |blame, cx| {
2134            blame
2135                .blame_for_rows(
2136                    &(0..4)
2137                        .map(|row| RowInfo {
2138                            buffer_row: Some(row),
2139                            buffer_id: Some(buffer_id_b),
2140                            ..Default::default()
2141                        })
2142                        .collect::<Vec<_>>(),
2143                    cx,
2144                )
2145                .collect::<Vec<_>>()
2146        });
2147
2148        assert_eq!(
2149            entries,
2150            vec![
2151                None,
2152                None,
2153                Some(blame_entry("3a3a3a", 2..3)),
2154                Some(blame_entry("4c4c4c", 3..4)),
2155            ]
2156        );
2157    });
2158}
2159
2160#[gpui::test(iterations = 30)]
2161async fn test_collaborating_with_editorconfig(
2162    cx_a: &mut TestAppContext,
2163    cx_b: &mut TestAppContext,
2164) {
2165    let mut server = TestServer::start(cx_a.executor()).await;
2166    let client_a = server.create_client(cx_a, "user_a").await;
2167    let client_b = server.create_client(cx_b, "user_b").await;
2168    server
2169        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2170        .await;
2171    let active_call_a = cx_a.read(ActiveCall::global);
2172
2173    cx_b.update(editor::init);
2174
2175    // Set up a fake language server.
2176    client_a.language_registry().add(rust_lang());
2177    client_a
2178        .fs()
2179        .insert_tree(
2180            "/a",
2181            json!({
2182                "src": {
2183                    "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2184                    "other_mod": {
2185                        "other.rs": "pub fn foo() -> usize {\n    4\n}",
2186                        ".editorconfig": "",
2187                    },
2188                },
2189                ".editorconfig": "[*]\ntab_width = 2\n",
2190            }),
2191        )
2192        .await;
2193    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2194    let project_id = active_call_a
2195        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2196        .await
2197        .unwrap();
2198    let main_buffer_a = project_a
2199        .update(cx_a, |p, cx| {
2200            p.open_buffer((worktree_id, "src/main.rs"), cx)
2201        })
2202        .await
2203        .unwrap();
2204    let other_buffer_a = project_a
2205        .update(cx_a, |p, cx| {
2206            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2207        })
2208        .await
2209        .unwrap();
2210    let cx_a = cx_a.add_empty_window();
2211    let main_editor_a = cx_a.new_window_entity(|window, cx| {
2212        Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
2213    });
2214    let other_editor_a = cx_a.new_window_entity(|window, cx| {
2215        Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
2216    });
2217    let mut main_editor_cx_a = EditorTestContext {
2218        cx: cx_a.clone(),
2219        window: cx_a.window_handle(),
2220        editor: main_editor_a,
2221        assertion_cx: AssertionContextManager::new(),
2222    };
2223    let mut other_editor_cx_a = EditorTestContext {
2224        cx: cx_a.clone(),
2225        window: cx_a.window_handle(),
2226        editor: other_editor_a,
2227        assertion_cx: AssertionContextManager::new(),
2228    };
2229
2230    // Join the project as client B.
2231    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2232    let main_buffer_b = project_b
2233        .update(cx_b, |p, cx| {
2234            p.open_buffer((worktree_id, "src/main.rs"), cx)
2235        })
2236        .await
2237        .unwrap();
2238    let other_buffer_b = project_b
2239        .update(cx_b, |p, cx| {
2240            p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2241        })
2242        .await
2243        .unwrap();
2244    let cx_b = cx_b.add_empty_window();
2245    let main_editor_b = cx_b.new_window_entity(|window, cx| {
2246        Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
2247    });
2248    let other_editor_b = cx_b.new_window_entity(|window, cx| {
2249        Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
2250    });
2251    let mut main_editor_cx_b = EditorTestContext {
2252        cx: cx_b.clone(),
2253        window: cx_b.window_handle(),
2254        editor: main_editor_b,
2255        assertion_cx: AssertionContextManager::new(),
2256    };
2257    let mut other_editor_cx_b = EditorTestContext {
2258        cx: cx_b.clone(),
2259        window: cx_b.window_handle(),
2260        editor: other_editor_b,
2261        assertion_cx: AssertionContextManager::new(),
2262    };
2263
2264    let initial_main = indoc! {"
2265ˇmod other;
2266fn main() { let foo = other::foo(); }"};
2267    let initial_other = indoc! {"
2268ˇpub fn foo() -> usize {
2269    4
2270}"};
2271
2272    let first_tabbed_main = indoc! {"
2273  ˇmod other;
2274fn main() { let foo = other::foo(); }"};
2275    tab_undo_assert(
2276        &mut main_editor_cx_a,
2277        &mut main_editor_cx_b,
2278        initial_main,
2279        first_tabbed_main,
2280        true,
2281    );
2282    tab_undo_assert(
2283        &mut main_editor_cx_a,
2284        &mut main_editor_cx_b,
2285        initial_main,
2286        first_tabbed_main,
2287        false,
2288    );
2289
2290    let first_tabbed_other = indoc! {"
2291  ˇpub fn foo() -> usize {
2292    4
2293}"};
2294    tab_undo_assert(
2295        &mut other_editor_cx_a,
2296        &mut other_editor_cx_b,
2297        initial_other,
2298        first_tabbed_other,
2299        true,
2300    );
2301    tab_undo_assert(
2302        &mut other_editor_cx_a,
2303        &mut other_editor_cx_b,
2304        initial_other,
2305        first_tabbed_other,
2306        false,
2307    );
2308
2309    client_a
2310        .fs()
2311        .atomic_write(
2312            PathBuf::from("/a/src/.editorconfig"),
2313            "[*]\ntab_width = 3\n".to_owned(),
2314        )
2315        .await
2316        .unwrap();
2317    cx_a.run_until_parked();
2318    cx_b.run_until_parked();
2319
2320    let second_tabbed_main = indoc! {"
2321   ˇmod other;
2322fn main() { let foo = other::foo(); }"};
2323    tab_undo_assert(
2324        &mut main_editor_cx_a,
2325        &mut main_editor_cx_b,
2326        initial_main,
2327        second_tabbed_main,
2328        true,
2329    );
2330    tab_undo_assert(
2331        &mut main_editor_cx_a,
2332        &mut main_editor_cx_b,
2333        initial_main,
2334        second_tabbed_main,
2335        false,
2336    );
2337
2338    let second_tabbed_other = indoc! {"
2339   ˇpub fn foo() -> usize {
2340    4
2341}"};
2342    tab_undo_assert(
2343        &mut other_editor_cx_a,
2344        &mut other_editor_cx_b,
2345        initial_other,
2346        second_tabbed_other,
2347        true,
2348    );
2349    tab_undo_assert(
2350        &mut other_editor_cx_a,
2351        &mut other_editor_cx_b,
2352        initial_other,
2353        second_tabbed_other,
2354        false,
2355    );
2356
2357    let editorconfig_buffer_b = project_b
2358        .update(cx_b, |p, cx| {
2359            p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2360        })
2361        .await
2362        .unwrap();
2363    editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2364        buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2365    });
2366    project_b
2367        .update(cx_b, |project, cx| {
2368            project.save_buffer(editorconfig_buffer_b.clone(), cx)
2369        })
2370        .await
2371        .unwrap();
2372    cx_a.run_until_parked();
2373    cx_b.run_until_parked();
2374
2375    tab_undo_assert(
2376        &mut main_editor_cx_a,
2377        &mut main_editor_cx_b,
2378        initial_main,
2379        second_tabbed_main,
2380        true,
2381    );
2382    tab_undo_assert(
2383        &mut main_editor_cx_a,
2384        &mut main_editor_cx_b,
2385        initial_main,
2386        second_tabbed_main,
2387        false,
2388    );
2389
2390    let third_tabbed_other = indoc! {"
2391      ˇpub fn foo() -> usize {
2392    4
2393}"};
2394    tab_undo_assert(
2395        &mut other_editor_cx_a,
2396        &mut other_editor_cx_b,
2397        initial_other,
2398        third_tabbed_other,
2399        true,
2400    );
2401
2402    tab_undo_assert(
2403        &mut other_editor_cx_a,
2404        &mut other_editor_cx_b,
2405        initial_other,
2406        third_tabbed_other,
2407        false,
2408    );
2409}
2410
2411#[gpui::test]
2412async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2413    let executor = cx_a.executor();
2414    let mut server = TestServer::start(executor.clone()).await;
2415    let client_a = server.create_client(cx_a, "user_a").await;
2416    let client_b = server.create_client(cx_b, "user_b").await;
2417    server
2418        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2419        .await;
2420    let active_call_a = cx_a.read(ActiveCall::global);
2421    let active_call_b = cx_b.read(ActiveCall::global);
2422    cx_a.update(editor::init);
2423    cx_b.update(editor::init);
2424    client_a
2425        .fs()
2426        .insert_tree(
2427            "/a",
2428            json!({
2429                "test.txt": "one\ntwo\nthree\nfour\nfive",
2430            }),
2431        )
2432        .await;
2433    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2434    let project_path = ProjectPath {
2435        worktree_id,
2436        path: Arc::from(Path::new(&"test.txt")),
2437    };
2438    let abs_path = project_a.read_with(cx_a, |project, cx| {
2439        project
2440            .absolute_path(&project_path, cx)
2441            .map(|path_buf| Arc::from(path_buf.to_owned()))
2442            .unwrap()
2443    });
2444
2445    active_call_a
2446        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2447        .await
2448        .unwrap();
2449    let project_id = active_call_a
2450        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2451        .await
2452        .unwrap();
2453    let project_b = client_b.join_remote_project(project_id, cx_b).await;
2454    active_call_b
2455        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2456        .await
2457        .unwrap();
2458    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2459    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2460
2461    // Client A opens an editor.
2462    let editor_a = workspace_a
2463        .update_in(cx_a, |workspace, window, cx| {
2464            workspace.open_path(project_path.clone(), None, true, window, cx)
2465        })
2466        .await
2467        .unwrap()
2468        .downcast::<Editor>()
2469        .unwrap();
2470
2471    // Client B opens same editor as A.
2472    let editor_b = workspace_b
2473        .update_in(cx_b, |workspace, window, cx| {
2474            workspace.open_path(project_path.clone(), None, true, window, cx)
2475        })
2476        .await
2477        .unwrap()
2478        .downcast::<Editor>()
2479        .unwrap();
2480
2481    cx_a.run_until_parked();
2482    cx_b.run_until_parked();
2483
2484    // Client A adds breakpoint on line (1)
2485    editor_a.update_in(cx_a, |editor, window, cx| {
2486        editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2487    });
2488
2489    cx_a.run_until_parked();
2490    cx_b.run_until_parked();
2491
2492    let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2493        editor
2494            .breakpoint_store()
2495            .clone()
2496            .unwrap()
2497            .read(cx)
2498            .all_breakpoints(cx)
2499            .clone()
2500    });
2501    let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2502        editor
2503            .breakpoint_store()
2504            .clone()
2505            .unwrap()
2506            .read(cx)
2507            .all_breakpoints(cx)
2508            .clone()
2509    });
2510
2511    assert_eq!(1, breakpoints_a.len());
2512    assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2513    assert_eq!(breakpoints_a, breakpoints_b);
2514
2515    // Client B adds breakpoint on line(2)
2516    editor_b.update_in(cx_b, |editor, window, cx| {
2517        editor.move_down(&editor::actions::MoveDown, window, cx);
2518        editor.move_down(&editor::actions::MoveDown, window, cx);
2519        editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2520    });
2521
2522    cx_a.run_until_parked();
2523    cx_b.run_until_parked();
2524
2525    let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2526        editor
2527            .breakpoint_store()
2528            .clone()
2529            .unwrap()
2530            .read(cx)
2531            .all_breakpoints(cx)
2532            .clone()
2533    });
2534    let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2535        editor
2536            .breakpoint_store()
2537            .clone()
2538            .unwrap()
2539            .read(cx)
2540            .all_breakpoints(cx)
2541            .clone()
2542    });
2543
2544    assert_eq!(1, breakpoints_a.len());
2545    assert_eq!(breakpoints_a, breakpoints_b);
2546    assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
2547
2548    // Client A removes last added breakpoint from client B
2549    editor_a.update_in(cx_a, |editor, window, cx| {
2550        editor.move_down(&editor::actions::MoveDown, window, cx);
2551        editor.move_down(&editor::actions::MoveDown, window, cx);
2552        editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2553    });
2554
2555    cx_a.run_until_parked();
2556    cx_b.run_until_parked();
2557
2558    let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2559        editor
2560            .breakpoint_store()
2561            .clone()
2562            .unwrap()
2563            .read(cx)
2564            .all_breakpoints(cx)
2565            .clone()
2566    });
2567    let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2568        editor
2569            .breakpoint_store()
2570            .clone()
2571            .unwrap()
2572            .read(cx)
2573            .all_breakpoints(cx)
2574            .clone()
2575    });
2576
2577    assert_eq!(1, breakpoints_a.len());
2578    assert_eq!(breakpoints_a, breakpoints_b);
2579    assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2580
2581    // Client B removes first added breakpoint by client A
2582    editor_b.update_in(cx_b, |editor, window, cx| {
2583        editor.move_up(&editor::actions::MoveUp, window, cx);
2584        editor.move_up(&editor::actions::MoveUp, window, cx);
2585        editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2586    });
2587
2588    cx_a.run_until_parked();
2589    cx_b.run_until_parked();
2590
2591    let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2592        editor
2593            .breakpoint_store()
2594            .clone()
2595            .unwrap()
2596            .read(cx)
2597            .all_breakpoints(cx)
2598            .clone()
2599    });
2600    let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2601        editor
2602            .breakpoint_store()
2603            .clone()
2604            .unwrap()
2605            .read(cx)
2606            .all_breakpoints(cx)
2607            .clone()
2608    });
2609
2610    assert_eq!(0, breakpoints_a.len());
2611    assert_eq!(breakpoints_a, breakpoints_b);
2612}
2613
2614#[track_caller]
2615fn tab_undo_assert(
2616    cx_a: &mut EditorTestContext,
2617    cx_b: &mut EditorTestContext,
2618    expected_initial: &str,
2619    expected_tabbed: &str,
2620    a_tabs: bool,
2621) {
2622    cx_a.assert_editor_state(expected_initial);
2623    cx_b.assert_editor_state(expected_initial);
2624
2625    if a_tabs {
2626        cx_a.update_editor(|editor, window, cx| {
2627            editor.tab(&editor::actions::Tab, window, cx);
2628        });
2629    } else {
2630        cx_b.update_editor(|editor, window, cx| {
2631            editor.tab(&editor::actions::Tab, window, cx);
2632        });
2633    }
2634
2635    cx_a.run_until_parked();
2636    cx_b.run_until_parked();
2637
2638    cx_a.assert_editor_state(expected_tabbed);
2639    cx_b.assert_editor_state(expected_tabbed);
2640
2641    if a_tabs {
2642        cx_a.update_editor(|editor, window, cx| {
2643            editor.undo(&editor::actions::Undo, window, cx);
2644        });
2645    } else {
2646        cx_b.update_editor(|editor, window, cx| {
2647            editor.undo(&editor::actions::Undo, window, cx);
2648        });
2649    }
2650    cx_a.run_until_parked();
2651    cx_b.run_until_parked();
2652    cx_a.assert_editor_state(expected_initial);
2653    cx_b.assert_editor_state(expected_initial);
2654}
2655
2656fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2657    let mut labels = Vec::new();
2658    for hint in editor.inlay_hint_cache().hints() {
2659        match hint.label {
2660            project::InlayHintLabel::String(s) => labels.push(s),
2661            _ => unreachable!(),
2662        }
2663    }
2664    labels
2665}
2666
2667fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2668    git::blame::BlameEntry {
2669        sha: sha.parse().unwrap(),
2670        range,
2671        ..Default::default()
2672    }
2673}