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