editor_tests.rs

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