editor_tests.rs

   1use std::{
   2    path::Path,
   3    sync::{
   4        atomic::{self, AtomicBool, AtomicUsize},
   5        Arc,
   6    },
   7};
   8
   9use call::ActiveCall;
  10use editor::{
  11    actions::{
  12        ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
  13    },
  14    test::editor_test_context::{AssertionContextManager, EditorTestContext},
  15    Editor,
  16};
  17use futures::StreamExt;
  18use gpui::{TestAppContext, VisualContext, VisualTestContext};
  19use indoc::indoc;
  20use language::{
  21    language_settings::{AllLanguageSettings, InlayHintSettings},
  22    tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
  23};
  24use rpc::RECEIVE_TIMEOUT;
  25use serde_json::json;
  26use settings::SettingsStore;
  27use text::Point;
  28use workspace::Workspace;
  29
  30use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
  31
  32#[gpui::test(iterations = 10)]
  33async fn test_host_disconnect(
  34    cx_a: &mut TestAppContext,
  35    cx_b: &mut TestAppContext,
  36    cx_c: &mut TestAppContext,
  37) {
  38    let mut server = TestServer::start(cx_a.executor()).await;
  39    let client_a = server.create_client(cx_a, "user_a").await;
  40    let client_b = server.create_client(cx_b, "user_b").await;
  41    let client_c = server.create_client(cx_c, "user_c").await;
  42    server
  43        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
  44        .await;
  45
  46    cx_b.update(editor::init);
  47
  48    client_a
  49        .fs()
  50        .insert_tree(
  51            "/a",
  52            serde_json::json!({
  53                "a.txt": "a-contents",
  54                "b.txt": "b-contents",
  55            }),
  56        )
  57        .await;
  58
  59    let active_call_a = cx_a.read(ActiveCall::global);
  60    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
  61
  62    let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
  63    let project_id = active_call_a
  64        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
  65        .await
  66        .unwrap();
  67
  68    let project_b = client_b.build_remote_project(project_id, cx_b).await;
  69    cx_a.background_executor.run_until_parked();
  70
  71    assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
  72
  73    let workspace_b =
  74        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
  75    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
  76    let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
  77
  78    let editor_b = workspace_b
  79        .update(cx_b, |workspace, cx| {
  80            workspace.open_path((worktree_id, "b.txt"), None, true, cx)
  81        })
  82        .unwrap()
  83        .await
  84        .unwrap()
  85        .downcast::<Editor>()
  86        .unwrap();
  87
  88    //TODO: focus
  89    assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
  90    editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
  91
  92    cx_b.update(|cx| {
  93        assert!(workspace_b_view.read(cx).is_edited());
  94    });
  95
  96    // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
  97    server.forbid_connections();
  98    server.disconnect_client(client_a.peer_id().unwrap());
  99    cx_a.background_executor
 100        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 101
 102    project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
 103
 104    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 105
 106    project_b.read_with(cx_b, |project, _| project.is_read_only());
 107
 108    assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
 109
 110    // Ensure client B's edited state is reset and that the whole window is blurred.
 111
 112    workspace_b
 113        .update(cx_b, |workspace, cx| {
 114            assert_eq!(cx.focused(), None);
 115            assert!(!workspace.is_edited())
 116        })
 117        .unwrap();
 118
 119    // Ensure client B is not prompted to save edits when closing window after disconnecting.
 120    let can_close = workspace_b
 121        .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
 122        .unwrap()
 123        .await
 124        .unwrap();
 125    assert!(can_close);
 126
 127    // Allow client A to reconnect to the server.
 128    server.allow_connections();
 129    cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
 130
 131    // Client B calls client A again after they reconnected.
 132    let active_call_b = cx_b.read(ActiveCall::global);
 133    active_call_b
 134        .update(cx_b, |call, cx| {
 135            call.invite(client_a.user_id().unwrap(), None, cx)
 136        })
 137        .await
 138        .unwrap();
 139    cx_a.background_executor.run_until_parked();
 140    active_call_a
 141        .update(cx_a, |call, cx| call.accept_incoming(cx))
 142        .await
 143        .unwrap();
 144
 145    active_call_a
 146        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 147        .await
 148        .unwrap();
 149
 150    // Drop client A's connection again. We should still unshare it successfully.
 151    server.forbid_connections();
 152    server.disconnect_client(client_a.peer_id().unwrap());
 153    cx_a.background_executor
 154        .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 155
 156    project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 157}
 158
 159#[gpui::test]
 160async fn test_newline_above_or_below_does_not_move_guest_cursor(
 161    cx_a: &mut TestAppContext,
 162    cx_b: &mut TestAppContext,
 163) {
 164    let mut server = TestServer::start(cx_a.executor()).await;
 165    let client_a = server.create_client(cx_a, "user_a").await;
 166    let client_b = server.create_client(cx_b, "user_b").await;
 167    let executor = cx_a.executor();
 168    server
 169        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 170        .await;
 171    let active_call_a = cx_a.read(ActiveCall::global);
 172
 173    client_a
 174        .fs()
 175        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
 176        .await;
 177    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 178    let project_id = active_call_a
 179        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 180        .await
 181        .unwrap();
 182
 183    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 184
 185    // Open a buffer as client A
 186    let buffer_a = project_a
 187        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 188        .await
 189        .unwrap();
 190    let cx_a = cx_a.add_empty_window();
 191    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 192
 193    let mut editor_cx_a = EditorTestContext {
 194        cx: cx_a.clone(),
 195        window: cx_a.handle(),
 196        editor: editor_a,
 197        assertion_cx: AssertionContextManager::new(),
 198    };
 199
 200    let cx_b = cx_b.add_empty_window();
 201    // Open a buffer as client B
 202    let buffer_b = project_b
 203        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
 204        .await
 205        .unwrap();
 206    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
 207
 208    let mut editor_cx_b = EditorTestContext {
 209        cx: cx_b.clone(),
 210        window: cx_b.handle(),
 211        editor: editor_b,
 212        assertion_cx: AssertionContextManager::new(),
 213    };
 214
 215    // Test newline above
 216    editor_cx_a.set_selections_state(indoc! {"
 217        Some textˇ
 218    "});
 219    editor_cx_b.set_selections_state(indoc! {"
 220        Some textˇ
 221    "});
 222    editor_cx_a
 223        .update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
 224    executor.run_until_parked();
 225    editor_cx_a.assert_editor_state(indoc! {"
 226        ˇ
 227        Some text
 228    "});
 229    editor_cx_b.assert_editor_state(indoc! {"
 230
 231        Some textˇ
 232    "});
 233
 234    // Test newline below
 235    editor_cx_a.set_selections_state(indoc! {"
 236
 237        Some textˇ
 238    "});
 239    editor_cx_b.set_selections_state(indoc! {"
 240
 241        Some textˇ
 242    "});
 243    editor_cx_a
 244        .update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
 245    executor.run_until_parked();
 246    editor_cx_a.assert_editor_state(indoc! {"
 247
 248        Some text
 249        ˇ
 250    "});
 251    editor_cx_b.assert_editor_state(indoc! {"
 252
 253        Some textˇ
 254
 255    "});
 256}
 257
 258#[gpui::test(iterations = 10)]
 259async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 260    let mut server = TestServer::start(cx_a.executor()).await;
 261    let client_a = server.create_client(cx_a, "user_a").await;
 262    let client_b = server.create_client(cx_b, "user_b").await;
 263    server
 264        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 265        .await;
 266    let active_call_a = cx_a.read(ActiveCall::global);
 267
 268    // Set up a fake language server.
 269    let mut language = Language::new(
 270        LanguageConfig {
 271            name: "Rust".into(),
 272            matcher: LanguageMatcher {
 273                path_suffixes: vec!["rs".to_string()],
 274                ..Default::default()
 275            },
 276            ..Default::default()
 277        },
 278        Some(tree_sitter_rust::language()),
 279    );
 280    let mut fake_language_servers = language
 281        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 282            capabilities: lsp::ServerCapabilities {
 283                completion_provider: Some(lsp::CompletionOptions {
 284                    trigger_characters: Some(vec![".".to_string()]),
 285                    resolve_provider: Some(true),
 286                    ..Default::default()
 287                }),
 288                ..Default::default()
 289            },
 290            ..Default::default()
 291        }))
 292        .await;
 293    client_a.language_registry().add(Arc::new(language));
 294
 295    client_a
 296        .fs()
 297        .insert_tree(
 298            "/a",
 299            json!({
 300                "main.rs": "fn main() { a }",
 301                "other.rs": "",
 302            }),
 303        )
 304        .await;
 305    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 306    let project_id = active_call_a
 307        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 308        .await
 309        .unwrap();
 310    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 311
 312    // Open a file in an editor as the guest.
 313    let buffer_b = project_b
 314        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 315        .await
 316        .unwrap();
 317    let cx_b = cx_b.add_empty_window();
 318    let editor_b =
 319        cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
 320
 321    let fake_language_server = fake_language_servers.next().await.unwrap();
 322    cx_a.background_executor.run_until_parked();
 323
 324    buffer_b.read_with(cx_b, |buffer, _| {
 325        assert!(!buffer.completion_triggers().is_empty())
 326    });
 327
 328    // Type a completion trigger character as the guest.
 329    editor_b.update(cx_b, |editor, cx| {
 330        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
 331        editor.handle_input(".", cx);
 332    });
 333    cx_b.focus_view(&editor_b);
 334
 335    // Receive a completion request as the host's language server.
 336    // Return some completions from the host's language server.
 337    cx_a.executor().start_waiting();
 338    fake_language_server
 339        .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
 340            assert_eq!(
 341                params.text_document_position.text_document.uri,
 342                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 343            );
 344            assert_eq!(
 345                params.text_document_position.position,
 346                lsp::Position::new(0, 14),
 347            );
 348
 349            Ok(Some(lsp::CompletionResponse::Array(vec![
 350                lsp::CompletionItem {
 351                    label: "first_method(…)".into(),
 352                    detail: Some("fn(&mut self, B) -> C".into()),
 353                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 354                        new_text: "first_method($1)".to_string(),
 355                        range: lsp::Range::new(
 356                            lsp::Position::new(0, 14),
 357                            lsp::Position::new(0, 14),
 358                        ),
 359                    })),
 360                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 361                    ..Default::default()
 362                },
 363                lsp::CompletionItem {
 364                    label: "second_method(…)".into(),
 365                    detail: Some("fn(&mut self, C) -> D<E>".into()),
 366                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 367                        new_text: "second_method()".to_string(),
 368                        range: lsp::Range::new(
 369                            lsp::Position::new(0, 14),
 370                            lsp::Position::new(0, 14),
 371                        ),
 372                    })),
 373                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 374                    ..Default::default()
 375                },
 376            ])))
 377        })
 378        .next()
 379        .await
 380        .unwrap();
 381    cx_a.executor().finish_waiting();
 382
 383    // Open the buffer on the host.
 384    let buffer_a = project_a
 385        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 386        .await
 387        .unwrap();
 388    cx_a.executor().run_until_parked();
 389
 390    buffer_a.read_with(cx_a, |buffer, _| {
 391        assert_eq!(buffer.text(), "fn main() { a. }")
 392    });
 393
 394    // Confirm a completion on the guest.
 395    editor_b.update(cx_b, |editor, cx| {
 396        assert!(editor.context_menu_visible());
 397        editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
 398        assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
 399    });
 400
 401    // Return a resolved completion from the host's language server.
 402    // The resolved completion has an additional text edit.
 403    fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
 404        |params, _| async move {
 405            assert_eq!(params.label, "first_method(…)");
 406            Ok(lsp::CompletionItem {
 407                label: "first_method(…)".into(),
 408                detail: Some("fn(&mut self, B) -> C".into()),
 409                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 410                    new_text: "first_method($1)".to_string(),
 411                    range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
 412                })),
 413                additional_text_edits: Some(vec![lsp::TextEdit {
 414                    new_text: "use d::SomeTrait;\n".to_string(),
 415                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 416                }]),
 417                insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
 418                ..Default::default()
 419            })
 420        },
 421    );
 422
 423    // The additional edit is applied.
 424    cx_a.executor().run_until_parked();
 425
 426    buffer_a.read_with(cx_a, |buffer, _| {
 427        assert_eq!(
 428            buffer.text(),
 429            "use d::SomeTrait;\nfn main() { a.first_method() }"
 430        );
 431    });
 432
 433    buffer_b.read_with(cx_b, |buffer, _| {
 434        assert_eq!(
 435            buffer.text(),
 436            "use d::SomeTrait;\nfn main() { a.first_method() }"
 437        );
 438    });
 439}
 440
 441#[gpui::test(iterations = 10)]
 442async fn test_collaborating_with_code_actions(
 443    cx_a: &mut TestAppContext,
 444    cx_b: &mut TestAppContext,
 445) {
 446    let mut server = TestServer::start(cx_a.executor()).await;
 447    let client_a = server.create_client(cx_a, "user_a").await;
 448    //
 449    let client_b = server.create_client(cx_b, "user_b").await;
 450    server
 451        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 452        .await;
 453    let active_call_a = cx_a.read(ActiveCall::global);
 454
 455    cx_b.update(editor::init);
 456
 457    // Set up a fake language server.
 458    let mut language = Language::new(
 459        LanguageConfig {
 460            name: "Rust".into(),
 461            matcher: LanguageMatcher {
 462                path_suffixes: vec!["rs".to_string()],
 463                ..Default::default()
 464            },
 465            ..Default::default()
 466        },
 467        Some(tree_sitter_rust::language()),
 468    );
 469    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
 470    client_a.language_registry().add(Arc::new(language));
 471
 472    client_a
 473        .fs()
 474        .insert_tree(
 475            "/a",
 476            json!({
 477                "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
 478                "other.rs": "pub fn foo() -> usize { 4 }",
 479            }),
 480        )
 481        .await;
 482    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 483    let project_id = active_call_a
 484        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 485        .await
 486        .unwrap();
 487
 488    // Join the project as client B.
 489    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 490    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 491    let editor_b = workspace_b
 492        .update(cx_b, |workspace, cx| {
 493            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
 494        })
 495        .await
 496        .unwrap()
 497        .downcast::<Editor>()
 498        .unwrap();
 499
 500    let mut fake_language_server = fake_language_servers.next().await.unwrap();
 501    let mut requests = fake_language_server
 502        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 503            assert_eq!(
 504                params.text_document.uri,
 505                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 506            );
 507            assert_eq!(params.range.start, lsp::Position::new(0, 0));
 508            assert_eq!(params.range.end, lsp::Position::new(0, 0));
 509            Ok(None)
 510        });
 511    cx_a.background_executor
 512        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 513    requests.next().await;
 514
 515    // Move cursor to a location that contains code actions.
 516    editor_b.update(cx_b, |editor, cx| {
 517        editor.change_selections(None, cx, |s| {
 518            s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
 519        });
 520    });
 521    cx_b.focus_view(&editor_b);
 522
 523    let mut requests = fake_language_server
 524        .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
 525            assert_eq!(
 526                params.text_document.uri,
 527                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 528            );
 529            assert_eq!(params.range.start, lsp::Position::new(1, 31));
 530            assert_eq!(params.range.end, lsp::Position::new(1, 31));
 531
 532            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 533                lsp::CodeAction {
 534                    title: "Inline into all callers".to_string(),
 535                    edit: Some(lsp::WorkspaceEdit {
 536                        changes: Some(
 537                            [
 538                                (
 539                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
 540                                    vec![lsp::TextEdit::new(
 541                                        lsp::Range::new(
 542                                            lsp::Position::new(1, 22),
 543                                            lsp::Position::new(1, 34),
 544                                        ),
 545                                        "4".to_string(),
 546                                    )],
 547                                ),
 548                                (
 549                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
 550                                    vec![lsp::TextEdit::new(
 551                                        lsp::Range::new(
 552                                            lsp::Position::new(0, 0),
 553                                            lsp::Position::new(0, 27),
 554                                        ),
 555                                        "".to_string(),
 556                                    )],
 557                                ),
 558                            ]
 559                            .into_iter()
 560                            .collect(),
 561                        ),
 562                        ..Default::default()
 563                    }),
 564                    data: Some(json!({
 565                        "codeActionParams": {
 566                            "range": {
 567                                "start": {"line": 1, "column": 31},
 568                                "end": {"line": 1, "column": 31},
 569                            }
 570                        }
 571                    })),
 572                    ..Default::default()
 573                },
 574            )]))
 575        });
 576    cx_a.background_executor
 577        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
 578    requests.next().await;
 579
 580    // Toggle code actions and wait for them to display.
 581    editor_b.update(cx_b, |editor, cx| {
 582        editor.toggle_code_actions(
 583            &ToggleCodeActions {
 584                deployed_from_indicator: false,
 585            },
 586            cx,
 587        );
 588    });
 589    cx_a.background_executor.run_until_parked();
 590
 591    editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 592
 593    fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 594
 595    // Confirming the code action will trigger a resolve request.
 596    let confirm_action = editor_b
 597        .update(cx_b, |editor, cx| {
 598            Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
 599        })
 600        .unwrap();
 601    fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
 602        |_, _| async move {
 603            Ok(lsp::CodeAction {
 604                title: "Inline into all callers".to_string(),
 605                edit: Some(lsp::WorkspaceEdit {
 606                    changes: Some(
 607                        [
 608                            (
 609                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
 610                                vec![lsp::TextEdit::new(
 611                                    lsp::Range::new(
 612                                        lsp::Position::new(1, 22),
 613                                        lsp::Position::new(1, 34),
 614                                    ),
 615                                    "4".to_string(),
 616                                )],
 617                            ),
 618                            (
 619                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
 620                                vec![lsp::TextEdit::new(
 621                                    lsp::Range::new(
 622                                        lsp::Position::new(0, 0),
 623                                        lsp::Position::new(0, 27),
 624                                    ),
 625                                    "".to_string(),
 626                                )],
 627                            ),
 628                        ]
 629                        .into_iter()
 630                        .collect(),
 631                    ),
 632                    ..Default::default()
 633                }),
 634                ..Default::default()
 635            })
 636        },
 637    );
 638
 639    // After the action is confirmed, an editor containing both modified files is opened.
 640    confirm_action.await.unwrap();
 641
 642    let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
 643        workspace
 644            .active_item(cx)
 645            .unwrap()
 646            .downcast::<Editor>()
 647            .unwrap()
 648    });
 649    code_action_editor.update(cx_b, |editor, cx| {
 650        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 651        editor.undo(&Undo, cx);
 652        assert_eq!(
 653            editor.text(cx),
 654            "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
 655        );
 656        editor.redo(&Redo, cx);
 657        assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
 658    });
 659}
 660
 661#[gpui::test(iterations = 10)]
 662async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 663    let mut server = TestServer::start(cx_a.executor()).await;
 664    let client_a = server.create_client(cx_a, "user_a").await;
 665    let client_b = server.create_client(cx_b, "user_b").await;
 666    server
 667        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 668        .await;
 669    let active_call_a = cx_a.read(ActiveCall::global);
 670
 671    cx_b.update(editor::init);
 672
 673    // Set up a fake language server.
 674    let mut language = Language::new(
 675        LanguageConfig {
 676            name: "Rust".into(),
 677            matcher: LanguageMatcher {
 678                path_suffixes: vec!["rs".to_string()],
 679                ..Default::default()
 680            },
 681            ..Default::default()
 682        },
 683        Some(tree_sitter_rust::language()),
 684    );
 685    let mut fake_language_servers = language
 686        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 687            capabilities: lsp::ServerCapabilities {
 688                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 689                    prepare_provider: Some(true),
 690                    work_done_progress_options: Default::default(),
 691                })),
 692                ..Default::default()
 693            },
 694            ..Default::default()
 695        }))
 696        .await;
 697    client_a.language_registry().add(Arc::new(language));
 698
 699    client_a
 700        .fs()
 701        .insert_tree(
 702            "/dir",
 703            json!({
 704                "one.rs": "const ONE: usize = 1;",
 705                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 706            }),
 707        )
 708        .await;
 709    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 710    let project_id = active_call_a
 711        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 712        .await
 713        .unwrap();
 714    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 715
 716    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
 717    let editor_b = workspace_b
 718        .update(cx_b, |workspace, cx| {
 719            workspace.open_path((worktree_id, "one.rs"), None, true, cx)
 720        })
 721        .await
 722        .unwrap()
 723        .downcast::<Editor>()
 724        .unwrap();
 725    let fake_language_server = fake_language_servers.next().await.unwrap();
 726
 727    // Move cursor to a location that can be renamed.
 728    let prepare_rename = editor_b.update(cx_b, |editor, cx| {
 729        editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
 730        editor.rename(&Rename, cx).unwrap()
 731    });
 732
 733    fake_language_server
 734        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 735            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
 736            assert_eq!(params.position, lsp::Position::new(0, 7));
 737            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 738                lsp::Position::new(0, 6),
 739                lsp::Position::new(0, 9),
 740            ))))
 741        })
 742        .next()
 743        .await
 744        .unwrap();
 745    prepare_rename.await.unwrap();
 746    editor_b.update(cx_b, |editor, cx| {
 747        use editor::ToOffset;
 748        let rename = editor.pending_rename().unwrap();
 749        let buffer = editor.buffer().read(cx).snapshot(cx);
 750        assert_eq!(
 751            rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
 752            6..9
 753        );
 754        rename.editor.update(cx, |rename_editor, cx| {
 755            rename_editor.buffer().update(cx, |rename_buffer, cx| {
 756                rename_buffer.edit([(0..3, "THREE")], None, cx);
 757            });
 758        });
 759    });
 760
 761    let confirm_rename = editor_b.update(cx_b, |editor, cx| {
 762        Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
 763    });
 764    fake_language_server
 765        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
 766            assert_eq!(
 767                params.text_document_position.text_document.uri.as_str(),
 768                "file:///dir/one.rs"
 769            );
 770            assert_eq!(
 771                params.text_document_position.position,
 772                lsp::Position::new(0, 6)
 773            );
 774            assert_eq!(params.new_name, "THREE");
 775            Ok(Some(lsp::WorkspaceEdit {
 776                changes: Some(
 777                    [
 778                        (
 779                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
 780                            vec![lsp::TextEdit::new(
 781                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 782                                "THREE".to_string(),
 783                            )],
 784                        ),
 785                        (
 786                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
 787                            vec![
 788                                lsp::TextEdit::new(
 789                                    lsp::Range::new(
 790                                        lsp::Position::new(0, 24),
 791                                        lsp::Position::new(0, 27),
 792                                    ),
 793                                    "THREE".to_string(),
 794                                ),
 795                                lsp::TextEdit::new(
 796                                    lsp::Range::new(
 797                                        lsp::Position::new(0, 35),
 798                                        lsp::Position::new(0, 38),
 799                                    ),
 800                                    "THREE".to_string(),
 801                                ),
 802                            ],
 803                        ),
 804                    ]
 805                    .into_iter()
 806                    .collect(),
 807                ),
 808                ..Default::default()
 809            }))
 810        })
 811        .next()
 812        .await
 813        .unwrap();
 814    confirm_rename.await.unwrap();
 815
 816    let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
 817        workspace.active_item_as::<Editor>(cx).unwrap()
 818    });
 819
 820    rename_editor.update(cx_b, |editor, cx| {
 821        assert_eq!(
 822            editor.text(cx),
 823            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 824        );
 825        editor.undo(&Undo, cx);
 826        assert_eq!(
 827            editor.text(cx),
 828            "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
 829        );
 830        editor.redo(&Redo, cx);
 831        assert_eq!(
 832            editor.text(cx),
 833            "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
 834        );
 835    });
 836
 837    // Ensure temporary rename edits cannot be undone/redone.
 838    editor_b.update(cx_b, |editor, cx| {
 839        editor.undo(&Undo, cx);
 840        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 841        editor.undo(&Undo, cx);
 842        assert_eq!(editor.text(cx), "const ONE: usize = 1;");
 843        editor.redo(&Redo, cx);
 844        assert_eq!(editor.text(cx), "const THREE: usize = 1;");
 845    })
 846}
 847
 848#[gpui::test(iterations = 10)]
 849async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 850    let mut server = TestServer::start(cx_a.executor()).await;
 851    let executor = cx_a.executor();
 852    let client_a = server.create_client(cx_a, "user_a").await;
 853    let client_b = server.create_client(cx_b, "user_b").await;
 854    server
 855        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 856        .await;
 857    let active_call_a = cx_a.read(ActiveCall::global);
 858
 859    cx_b.update(editor::init);
 860
 861    // Set up a fake language server.
 862    let mut language = Language::new(
 863        LanguageConfig {
 864            name: "Rust".into(),
 865            matcher: LanguageMatcher {
 866                path_suffixes: vec!["rs".to_string()],
 867                ..Default::default()
 868            },
 869            ..Default::default()
 870        },
 871        Some(tree_sitter_rust::language()),
 872    );
 873    let mut fake_language_servers = language
 874        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 875            name: "the-language-server",
 876            ..Default::default()
 877        }))
 878        .await;
 879    client_a.language_registry().add(Arc::new(language));
 880
 881    client_a
 882        .fs()
 883        .insert_tree(
 884            "/dir",
 885            json!({
 886                "main.rs": "const ONE: usize = 1;",
 887            }),
 888        )
 889        .await;
 890    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
 891
 892    let _buffer_a = project_a
 893        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
 894        .await
 895        .unwrap();
 896
 897    let fake_language_server = fake_language_servers.next().await.unwrap();
 898    fake_language_server.start_progress("the-token").await;
 899    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 900        token: lsp::NumberOrString::String("the-token".to_string()),
 901        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 902            lsp::WorkDoneProgressReport {
 903                message: Some("the-message".to_string()),
 904                ..Default::default()
 905            },
 906        )),
 907    });
 908    executor.run_until_parked();
 909
 910    project_a.read_with(cx_a, |project, _| {
 911        let status = project.language_server_statuses().next().unwrap();
 912        assert_eq!(status.name, "the-language-server");
 913        assert_eq!(status.pending_work.len(), 1);
 914        assert_eq!(
 915            status.pending_work["the-token"].message.as_ref().unwrap(),
 916            "the-message"
 917        );
 918    });
 919
 920    let project_id = active_call_a
 921        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 922        .await
 923        .unwrap();
 924    executor.run_until_parked();
 925    let project_b = client_b.build_remote_project(project_id, cx_b).await;
 926
 927    project_b.read_with(cx_b, |project, _| {
 928        let status = project.language_server_statuses().next().unwrap();
 929        assert_eq!(status.name, "the-language-server");
 930    });
 931
 932    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
 933        token: lsp::NumberOrString::String("the-token".to_string()),
 934        value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
 935            lsp::WorkDoneProgressReport {
 936                message: Some("the-message-2".to_string()),
 937                ..Default::default()
 938            },
 939        )),
 940    });
 941    executor.run_until_parked();
 942
 943    project_a.read_with(cx_a, |project, _| {
 944        let status = project.language_server_statuses().next().unwrap();
 945        assert_eq!(status.name, "the-language-server");
 946        assert_eq!(status.pending_work.len(), 1);
 947        assert_eq!(
 948            status.pending_work["the-token"].message.as_ref().unwrap(),
 949            "the-message-2"
 950        );
 951    });
 952
 953    project_b.read_with(cx_b, |project, _| {
 954        let status = project.language_server_statuses().next().unwrap();
 955        assert_eq!(status.name, "the-language-server");
 956        assert_eq!(status.pending_work.len(), 1);
 957        assert_eq!(
 958            status.pending_work["the-token"].message.as_ref().unwrap(),
 959            "the-message-2"
 960        );
 961    });
 962}
 963
 964#[gpui::test(iterations = 10)]
 965async fn test_share_project(
 966    cx_a: &mut TestAppContext,
 967    cx_b: &mut TestAppContext,
 968    cx_c: &mut TestAppContext,
 969) {
 970    let executor = cx_a.executor();
 971    let cx_b = cx_b.add_empty_window();
 972    let mut server = TestServer::start(executor.clone()).await;
 973    let client_a = server.create_client(cx_a, "user_a").await;
 974    let client_b = server.create_client(cx_b, "user_b").await;
 975    let client_c = server.create_client(cx_c, "user_c").await;
 976    server
 977        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
 978        .await;
 979    let active_call_a = cx_a.read(ActiveCall::global);
 980    let active_call_b = cx_b.read(ActiveCall::global);
 981    let active_call_c = cx_c.read(ActiveCall::global);
 982
 983    client_a
 984        .fs()
 985        .insert_tree(
 986            "/a",
 987            json!({
 988                ".gitignore": "ignored-dir",
 989                "a.txt": "a-contents",
 990                "b.txt": "b-contents",
 991                "ignored-dir": {
 992                    "c.txt": "",
 993                    "d.txt": "",
 994                }
 995            }),
 996        )
 997        .await;
 998
 999    // Invite client B to collaborate on a project
1000    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1001    active_call_a
1002        .update(cx_a, |call, cx| {
1003            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1004        })
1005        .await
1006        .unwrap();
1007
1008    // Join that project as client B
1009
1010    let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1011    executor.run_until_parked();
1012    let call = incoming_call_b.borrow().clone().unwrap();
1013    assert_eq!(call.calling_user.github_login, "user_a");
1014    let initial_project = call.initial_project.unwrap();
1015    active_call_b
1016        .update(cx_b, |call, cx| call.accept_incoming(cx))
1017        .await
1018        .unwrap();
1019    let client_b_peer_id = client_b.peer_id().unwrap();
1020    let project_b = client_b
1021        .build_remote_project(initial_project.id, cx_b)
1022        .await;
1023
1024    let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1025
1026    executor.run_until_parked();
1027
1028    project_a.read_with(cx_a, |project, _| {
1029        let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1030        assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1031    });
1032
1033    project_b.read_with(cx_b, |project, cx| {
1034        let worktree = project.worktrees().next().unwrap().read(cx);
1035        assert_eq!(
1036            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1037            [
1038                Path::new(".gitignore"),
1039                Path::new("a.txt"),
1040                Path::new("b.txt"),
1041                Path::new("ignored-dir"),
1042            ]
1043        );
1044    });
1045
1046    project_b
1047        .update(cx_b, |project, cx| {
1048            let worktree = project.worktrees().next().unwrap();
1049            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1050            project.expand_entry(worktree_id, entry.id, cx).unwrap()
1051        })
1052        .await
1053        .unwrap();
1054
1055    project_b.read_with(cx_b, |project, cx| {
1056        let worktree = project.worktrees().next().unwrap().read(cx);
1057        assert_eq!(
1058            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1059            [
1060                Path::new(".gitignore"),
1061                Path::new("a.txt"),
1062                Path::new("b.txt"),
1063                Path::new("ignored-dir"),
1064                Path::new("ignored-dir/c.txt"),
1065                Path::new("ignored-dir/d.txt"),
1066            ]
1067        );
1068    });
1069
1070    // Open the same file as client B and client A.
1071    let buffer_b = project_b
1072        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1073        .await
1074        .unwrap();
1075
1076    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1077
1078    project_a.read_with(cx_a, |project, cx| {
1079        assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1080    });
1081    let buffer_a = project_a
1082        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1083        .await
1084        .unwrap();
1085
1086    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1087
1088    // Client A sees client B's selection
1089    executor.run_until_parked();
1090
1091    buffer_a.read_with(cx_a, |buffer, _| {
1092        buffer
1093            .snapshot()
1094            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1095            .count()
1096            == 1
1097    });
1098
1099    // Edit the buffer as client B and see that edit as client A.
1100    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1101    executor.run_until_parked();
1102
1103    buffer_a.read_with(cx_a, |buffer, _| {
1104        assert_eq!(buffer.text(), "ok, b-contents")
1105    });
1106
1107    // Client B can invite client C on a project shared by client A.
1108    active_call_b
1109        .update(cx_b, |call, cx| {
1110            call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1111        })
1112        .await
1113        .unwrap();
1114
1115    let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1116    executor.run_until_parked();
1117    let call = incoming_call_c.borrow().clone().unwrap();
1118    assert_eq!(call.calling_user.github_login, "user_b");
1119    let initial_project = call.initial_project.unwrap();
1120    active_call_c
1121        .update(cx_c, |call, cx| call.accept_incoming(cx))
1122        .await
1123        .unwrap();
1124    let _project_c = client_c
1125        .build_remote_project(initial_project.id, cx_c)
1126        .await;
1127
1128    // Client B closes the editor, and client A sees client B's selections removed.
1129    cx_b.update(move |_| drop(editor_b));
1130    executor.run_until_parked();
1131
1132    buffer_a.read_with(cx_a, |buffer, _| {
1133        buffer
1134            .snapshot()
1135            .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1136            .count()
1137            == 0
1138    });
1139}
1140
1141#[gpui::test(iterations = 10)]
1142async fn test_on_input_format_from_host_to_guest(
1143    cx_a: &mut TestAppContext,
1144    cx_b: &mut TestAppContext,
1145) {
1146    let mut server = TestServer::start(cx_a.executor()).await;
1147    let executor = cx_a.executor();
1148    let client_a = server.create_client(cx_a, "user_a").await;
1149    let client_b = server.create_client(cx_b, "user_b").await;
1150    server
1151        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1152        .await;
1153    let active_call_a = cx_a.read(ActiveCall::global);
1154
1155    // Set up a fake language server.
1156    let mut language = Language::new(
1157        LanguageConfig {
1158            name: "Rust".into(),
1159            matcher: LanguageMatcher {
1160                path_suffixes: vec!["rs".to_string()],
1161                ..Default::default()
1162            },
1163            ..Default::default()
1164        },
1165        Some(tree_sitter_rust::language()),
1166    );
1167    let mut fake_language_servers = language
1168        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1169            capabilities: lsp::ServerCapabilities {
1170                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1171                    first_trigger_character: ":".to_string(),
1172                    more_trigger_character: Some(vec![">".to_string()]),
1173                }),
1174                ..Default::default()
1175            },
1176            ..Default::default()
1177        }))
1178        .await;
1179    client_a.language_registry().add(Arc::new(language));
1180
1181    client_a
1182        .fs()
1183        .insert_tree(
1184            "/a",
1185            json!({
1186                "main.rs": "fn main() { a }",
1187                "other.rs": "// Test file",
1188            }),
1189        )
1190        .await;
1191    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1192    let project_id = active_call_a
1193        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1194        .await
1195        .unwrap();
1196    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1197
1198    // Open a file in an editor as the host.
1199    let buffer_a = project_a
1200        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1201        .await
1202        .unwrap();
1203    let cx_a = cx_a.add_empty_window();
1204    let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1205
1206    let fake_language_server = fake_language_servers.next().await.unwrap();
1207    executor.run_until_parked();
1208
1209    // Receive an OnTypeFormatting request as the host's language server.
1210    // Return some formatting from the host's language server.
1211    fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1212        |params, _| async move {
1213            assert_eq!(
1214                params.text_document_position.text_document.uri,
1215                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1216            );
1217            assert_eq!(
1218                params.text_document_position.position,
1219                lsp::Position::new(0, 14),
1220            );
1221
1222            Ok(Some(vec![lsp::TextEdit {
1223                new_text: "~<".to_string(),
1224                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1225            }]))
1226        },
1227    );
1228
1229    // Open the buffer on the guest and see that the formatting worked
1230    let buffer_b = project_b
1231        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1232        .await
1233        .unwrap();
1234
1235    // Type a on type formatting trigger character as the guest.
1236    cx_a.focus_view(&editor_a);
1237    editor_a.update(cx_a, |editor, cx| {
1238        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1239        editor.handle_input(">", cx);
1240    });
1241
1242    executor.run_until_parked();
1243
1244    buffer_b.read_with(cx_b, |buffer, _| {
1245        assert_eq!(buffer.text(), "fn main() { a>~< }")
1246    });
1247
1248    // Undo should remove LSP edits first
1249    editor_a.update(cx_a, |editor, cx| {
1250        assert_eq!(editor.text(cx), "fn main() { a>~< }");
1251        editor.undo(&Undo, cx);
1252        assert_eq!(editor.text(cx), "fn main() { a> }");
1253    });
1254    executor.run_until_parked();
1255
1256    buffer_b.read_with(cx_b, |buffer, _| {
1257        assert_eq!(buffer.text(), "fn main() { a> }")
1258    });
1259
1260    editor_a.update(cx_a, |editor, cx| {
1261        assert_eq!(editor.text(cx), "fn main() { a> }");
1262        editor.undo(&Undo, cx);
1263        assert_eq!(editor.text(cx), "fn main() { a }");
1264    });
1265    executor.run_until_parked();
1266
1267    buffer_b.read_with(cx_b, |buffer, _| {
1268        assert_eq!(buffer.text(), "fn main() { a }")
1269    });
1270}
1271
1272#[gpui::test(iterations = 10)]
1273async fn test_on_input_format_from_guest_to_host(
1274    cx_a: &mut TestAppContext,
1275    cx_b: &mut TestAppContext,
1276) {
1277    let mut server = TestServer::start(cx_a.executor()).await;
1278    let executor = cx_a.executor();
1279    let client_a = server.create_client(cx_a, "user_a").await;
1280    let client_b = server.create_client(cx_b, "user_b").await;
1281    server
1282        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1283        .await;
1284    let active_call_a = cx_a.read(ActiveCall::global);
1285
1286    // Set up a fake language server.
1287    let mut language = Language::new(
1288        LanguageConfig {
1289            name: "Rust".into(),
1290            matcher: LanguageMatcher {
1291                path_suffixes: vec!["rs".to_string()],
1292                ..Default::default()
1293            },
1294            ..Default::default()
1295        },
1296        Some(tree_sitter_rust::language()),
1297    );
1298    let mut fake_language_servers = language
1299        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1300            capabilities: lsp::ServerCapabilities {
1301                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1302                    first_trigger_character: ":".to_string(),
1303                    more_trigger_character: Some(vec![">".to_string()]),
1304                }),
1305                ..Default::default()
1306            },
1307            ..Default::default()
1308        }))
1309        .await;
1310    client_a.language_registry().add(Arc::new(language));
1311
1312    client_a
1313        .fs()
1314        .insert_tree(
1315            "/a",
1316            json!({
1317                "main.rs": "fn main() { a }",
1318                "other.rs": "// Test file",
1319            }),
1320        )
1321        .await;
1322    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1323    let project_id = active_call_a
1324        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1325        .await
1326        .unwrap();
1327    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1328
1329    // Open a file in an editor as the guest.
1330    let buffer_b = project_b
1331        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1332        .await
1333        .unwrap();
1334    let cx_b = cx_b.add_empty_window();
1335    let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1336
1337    let fake_language_server = fake_language_servers.next().await.unwrap();
1338    executor.run_until_parked();
1339
1340    // Type a on type formatting trigger character as the guest.
1341    cx_b.focus_view(&editor_b);
1342    editor_b.update(cx_b, |editor, cx| {
1343        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1344        editor.handle_input(":", cx);
1345    });
1346
1347    // Receive an OnTypeFormatting request as the host's language server.
1348    // Return some formatting from the host's language server.
1349    executor.start_waiting();
1350    fake_language_server
1351        .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1352            assert_eq!(
1353                params.text_document_position.text_document.uri,
1354                lsp::Url::from_file_path("/a/main.rs").unwrap(),
1355            );
1356            assert_eq!(
1357                params.text_document_position.position,
1358                lsp::Position::new(0, 14),
1359            );
1360
1361            Ok(Some(vec![lsp::TextEdit {
1362                new_text: "~:".to_string(),
1363                range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1364            }]))
1365        })
1366        .next()
1367        .await
1368        .unwrap();
1369    executor.finish_waiting();
1370
1371    // Open the buffer on the host and see that the formatting worked
1372    let buffer_a = project_a
1373        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1374        .await
1375        .unwrap();
1376    executor.run_until_parked();
1377
1378    buffer_a.read_with(cx_a, |buffer, _| {
1379        assert_eq!(buffer.text(), "fn main() { a:~: }")
1380    });
1381
1382    // Undo should remove LSP edits first
1383    editor_b.update(cx_b, |editor, cx| {
1384        assert_eq!(editor.text(cx), "fn main() { a:~: }");
1385        editor.undo(&Undo, cx);
1386        assert_eq!(editor.text(cx), "fn main() { a: }");
1387    });
1388    executor.run_until_parked();
1389
1390    buffer_a.read_with(cx_a, |buffer, _| {
1391        assert_eq!(buffer.text(), "fn main() { a: }")
1392    });
1393
1394    editor_b.update(cx_b, |editor, cx| {
1395        assert_eq!(editor.text(cx), "fn main() { a: }");
1396        editor.undo(&Undo, cx);
1397        assert_eq!(editor.text(cx), "fn main() { a }");
1398    });
1399    executor.run_until_parked();
1400
1401    buffer_a.read_with(cx_a, |buffer, _| {
1402        assert_eq!(buffer.text(), "fn main() { a }")
1403    });
1404}
1405
1406#[gpui::test(iterations = 10)]
1407async fn test_mutual_editor_inlay_hint_cache_update(
1408    cx_a: &mut TestAppContext,
1409    cx_b: &mut TestAppContext,
1410) {
1411    let mut server = TestServer::start(cx_a.executor()).await;
1412    let executor = cx_a.executor();
1413    let client_a = server.create_client(cx_a, "user_a").await;
1414    let client_b = server.create_client(cx_b, "user_b").await;
1415    server
1416        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1417        .await;
1418    let active_call_a = cx_a.read(ActiveCall::global);
1419    let active_call_b = cx_b.read(ActiveCall::global);
1420
1421    cx_a.update(editor::init);
1422    cx_b.update(editor::init);
1423
1424    cx_a.update(|cx| {
1425        cx.update_global(|store: &mut SettingsStore, cx| {
1426            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1427                settings.defaults.inlay_hints = Some(InlayHintSettings {
1428                    enabled: true,
1429                    show_type_hints: true,
1430                    show_parameter_hints: false,
1431                    show_other_hints: true,
1432                })
1433            });
1434        });
1435    });
1436    cx_b.update(|cx| {
1437        cx.update_global(|store: &mut SettingsStore, cx| {
1438            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1439                settings.defaults.inlay_hints = Some(InlayHintSettings {
1440                    enabled: true,
1441                    show_type_hints: true,
1442                    show_parameter_hints: false,
1443                    show_other_hints: true,
1444                })
1445            });
1446        });
1447    });
1448
1449    let mut language = Language::new(
1450        LanguageConfig {
1451            name: "Rust".into(),
1452            matcher: LanguageMatcher {
1453                path_suffixes: vec!["rs".to_string()],
1454                ..Default::default()
1455            },
1456            ..Default::default()
1457        },
1458        Some(tree_sitter_rust::language()),
1459    );
1460    let mut fake_language_servers = language
1461        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1462            capabilities: lsp::ServerCapabilities {
1463                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1464                ..Default::default()
1465            },
1466            ..Default::default()
1467        }))
1468        .await;
1469    let language = Arc::new(language);
1470    client_a.language_registry().add(Arc::clone(&language));
1471    client_b.language_registry().add(language);
1472
1473    // Client A opens a project.
1474    client_a
1475        .fs()
1476        .insert_tree(
1477            "/a",
1478            json!({
1479                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1480                "other.rs": "// Test file",
1481            }),
1482        )
1483        .await;
1484    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1485    active_call_a
1486        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1487        .await
1488        .unwrap();
1489    let project_id = active_call_a
1490        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1491        .await
1492        .unwrap();
1493
1494    // Client B joins the project
1495    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1496    active_call_b
1497        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1498        .await
1499        .unwrap();
1500
1501    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1502    executor.start_waiting();
1503
1504    // The host opens a rust file.
1505    let _buffer_a = project_a
1506        .update(cx_a, |project, cx| {
1507            project.open_local_buffer("/a/main.rs", cx)
1508        })
1509        .await
1510        .unwrap();
1511    let fake_language_server = fake_language_servers.next().await.unwrap();
1512    let editor_a = workspace_a
1513        .update(cx_a, |workspace, cx| {
1514            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1515        })
1516        .await
1517        .unwrap()
1518        .downcast::<Editor>()
1519        .unwrap();
1520
1521    // Set up the language server to return an additional inlay hint on each request.
1522    let edits_made = Arc::new(AtomicUsize::new(0));
1523    let closure_edits_made = Arc::clone(&edits_made);
1524    fake_language_server
1525        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1526            let task_edits_made = Arc::clone(&closure_edits_made);
1527            async move {
1528                assert_eq!(
1529                    params.text_document.uri,
1530                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1531                );
1532                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1533                Ok(Some(vec![lsp::InlayHint {
1534                    position: lsp::Position::new(0, edits_made as u32),
1535                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
1536                    kind: None,
1537                    text_edits: None,
1538                    tooltip: None,
1539                    padding_left: None,
1540                    padding_right: None,
1541                    data: None,
1542                }]))
1543            }
1544        })
1545        .next()
1546        .await
1547        .unwrap();
1548
1549    executor.run_until_parked();
1550
1551    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1552    editor_a.update(cx_a, |editor, _| {
1553        assert_eq!(
1554            vec![initial_edit.to_string()],
1555            extract_hint_labels(editor),
1556            "Host should get its first hints when opens an editor"
1557        );
1558        let inlay_cache = editor.inlay_hint_cache();
1559        assert_eq!(
1560            inlay_cache.version(),
1561            1,
1562            "Host editor update the cache version after every cache/view change",
1563        );
1564    });
1565    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1566    let editor_b = workspace_b
1567        .update(cx_b, |workspace, cx| {
1568            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1569        })
1570        .await
1571        .unwrap()
1572        .downcast::<Editor>()
1573        .unwrap();
1574
1575    executor.run_until_parked();
1576    editor_b.update(cx_b, |editor, _| {
1577        assert_eq!(
1578            vec![initial_edit.to_string()],
1579            extract_hint_labels(editor),
1580            "Client should get its first hints when opens an editor"
1581        );
1582        let inlay_cache = editor.inlay_hint_cache();
1583        assert_eq!(
1584            inlay_cache.version(),
1585            1,
1586            "Guest editor update the cache version after every cache/view change"
1587        );
1588    });
1589
1590    let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1591    editor_b.update(cx_b, |editor, cx| {
1592        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1593        editor.handle_input(":", cx);
1594    });
1595    cx_b.focus_view(&editor_b);
1596
1597    executor.run_until_parked();
1598    editor_a.update(cx_a, |editor, _| {
1599        assert_eq!(
1600            vec![after_client_edit.to_string()],
1601            extract_hint_labels(editor),
1602        );
1603        let inlay_cache = editor.inlay_hint_cache();
1604        assert_eq!(inlay_cache.version(), 2);
1605    });
1606    editor_b.update(cx_b, |editor, _| {
1607        assert_eq!(
1608            vec![after_client_edit.to_string()],
1609            extract_hint_labels(editor),
1610        );
1611        let inlay_cache = editor.inlay_hint_cache();
1612        assert_eq!(inlay_cache.version(), 2);
1613    });
1614
1615    let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1616    editor_a.update(cx_a, |editor, cx| {
1617        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1618        editor.handle_input("a change to increment both buffers' versions", cx);
1619    });
1620    cx_a.focus_view(&editor_a);
1621
1622    executor.run_until_parked();
1623    editor_a.update(cx_a, |editor, _| {
1624        assert_eq!(
1625            vec![after_host_edit.to_string()],
1626            extract_hint_labels(editor),
1627        );
1628        let inlay_cache = editor.inlay_hint_cache();
1629        assert_eq!(inlay_cache.version(), 3);
1630    });
1631    editor_b.update(cx_b, |editor, _| {
1632        assert_eq!(
1633            vec![after_host_edit.to_string()],
1634            extract_hint_labels(editor),
1635        );
1636        let inlay_cache = editor.inlay_hint_cache();
1637        assert_eq!(inlay_cache.version(), 3);
1638    });
1639
1640    let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1641    fake_language_server
1642        .request::<lsp::request::InlayHintRefreshRequest>(())
1643        .await
1644        .expect("inlay refresh request failed");
1645
1646    executor.run_until_parked();
1647    editor_a.update(cx_a, |editor, _| {
1648        assert_eq!(
1649            vec![after_special_edit_for_refresh.to_string()],
1650            extract_hint_labels(editor),
1651            "Host should react to /refresh LSP request"
1652        );
1653        let inlay_cache = editor.inlay_hint_cache();
1654        assert_eq!(
1655            inlay_cache.version(),
1656            4,
1657            "Host should accepted all edits and bump its cache version every time"
1658        );
1659    });
1660    editor_b.update(cx_b, |editor, _| {
1661        assert_eq!(
1662            vec![after_special_edit_for_refresh.to_string()],
1663            extract_hint_labels(editor),
1664            "Guest should get a /refresh LSP request propagated by host"
1665        );
1666        let inlay_cache = editor.inlay_hint_cache();
1667        assert_eq!(
1668            inlay_cache.version(),
1669            4,
1670            "Guest should accepted all edits and bump its cache version every time"
1671        );
1672    });
1673}
1674
1675#[gpui::test(iterations = 10)]
1676async fn test_inlay_hint_refresh_is_forwarded(
1677    cx_a: &mut TestAppContext,
1678    cx_b: &mut TestAppContext,
1679) {
1680    let mut server = TestServer::start(cx_a.executor()).await;
1681    let executor = cx_a.executor();
1682    let client_a = server.create_client(cx_a, "user_a").await;
1683    let client_b = server.create_client(cx_b, "user_b").await;
1684    server
1685        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1686        .await;
1687    let active_call_a = cx_a.read(ActiveCall::global);
1688    let active_call_b = cx_b.read(ActiveCall::global);
1689
1690    cx_a.update(editor::init);
1691    cx_b.update(editor::init);
1692
1693    cx_a.update(|cx| {
1694        cx.update_global(|store: &mut SettingsStore, cx| {
1695            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1696                settings.defaults.inlay_hints = Some(InlayHintSettings {
1697                    enabled: false,
1698                    show_type_hints: false,
1699                    show_parameter_hints: false,
1700                    show_other_hints: false,
1701                })
1702            });
1703        });
1704    });
1705    cx_b.update(|cx| {
1706        cx.update_global(|store: &mut SettingsStore, cx| {
1707            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1708                settings.defaults.inlay_hints = Some(InlayHintSettings {
1709                    enabled: true,
1710                    show_type_hints: true,
1711                    show_parameter_hints: true,
1712                    show_other_hints: true,
1713                })
1714            });
1715        });
1716    });
1717
1718    let mut language = Language::new(
1719        LanguageConfig {
1720            name: "Rust".into(),
1721            matcher: LanguageMatcher {
1722                path_suffixes: vec!["rs".to_string()],
1723                ..Default::default()
1724            },
1725            ..Default::default()
1726        },
1727        Some(tree_sitter_rust::language()),
1728    );
1729    let mut fake_language_servers = language
1730        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1731            capabilities: lsp::ServerCapabilities {
1732                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1733                ..Default::default()
1734            },
1735            ..Default::default()
1736        }))
1737        .await;
1738    let language = Arc::new(language);
1739    client_a.language_registry().add(Arc::clone(&language));
1740    client_b.language_registry().add(language);
1741
1742    client_a
1743        .fs()
1744        .insert_tree(
1745            "/a",
1746            json!({
1747                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1748                "other.rs": "// Test file",
1749            }),
1750        )
1751        .await;
1752    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1753    active_call_a
1754        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1755        .await
1756        .unwrap();
1757    let project_id = active_call_a
1758        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1759        .await
1760        .unwrap();
1761
1762    let project_b = client_b.build_remote_project(project_id, cx_b).await;
1763    active_call_b
1764        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1765        .await
1766        .unwrap();
1767
1768    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1769    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1770
1771    cx_a.background_executor.start_waiting();
1772
1773    let editor_a = workspace_a
1774        .update(cx_a, |workspace, cx| {
1775            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1776        })
1777        .await
1778        .unwrap()
1779        .downcast::<Editor>()
1780        .unwrap();
1781
1782    let editor_b = workspace_b
1783        .update(cx_b, |workspace, cx| {
1784            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1785        })
1786        .await
1787        .unwrap()
1788        .downcast::<Editor>()
1789        .unwrap();
1790
1791    let other_hints = Arc::new(AtomicBool::new(false));
1792    let fake_language_server = fake_language_servers.next().await.unwrap();
1793    let closure_other_hints = Arc::clone(&other_hints);
1794    fake_language_server
1795        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1796            let task_other_hints = Arc::clone(&closure_other_hints);
1797            async move {
1798                assert_eq!(
1799                    params.text_document.uri,
1800                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
1801                );
1802                let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1803                let character = if other_hints { 0 } else { 2 };
1804                let label = if other_hints {
1805                    "other hint"
1806                } else {
1807                    "initial hint"
1808                };
1809                Ok(Some(vec![lsp::InlayHint {
1810                    position: lsp::Position::new(0, character),
1811                    label: lsp::InlayHintLabel::String(label.to_string()),
1812                    kind: None,
1813                    text_edits: None,
1814                    tooltip: None,
1815                    padding_left: None,
1816                    padding_right: None,
1817                    data: None,
1818                }]))
1819            }
1820        })
1821        .next()
1822        .await
1823        .unwrap();
1824    executor.finish_waiting();
1825
1826    executor.run_until_parked();
1827    editor_a.update(cx_a, |editor, _| {
1828        assert!(
1829            extract_hint_labels(editor).is_empty(),
1830            "Host should get no hints due to them turned off"
1831        );
1832        let inlay_cache = editor.inlay_hint_cache();
1833        assert_eq!(
1834            inlay_cache.version(),
1835            0,
1836            "Turned off hints should not generate version updates"
1837        );
1838    });
1839
1840    executor.run_until_parked();
1841    editor_b.update(cx_b, |editor, _| {
1842        assert_eq!(
1843            vec!["initial hint".to_string()],
1844            extract_hint_labels(editor),
1845            "Client should get its first hints when opens an editor"
1846        );
1847        let inlay_cache = editor.inlay_hint_cache();
1848        assert_eq!(
1849            inlay_cache.version(),
1850            1,
1851            "Should update cache version after first hints"
1852        );
1853    });
1854
1855    other_hints.fetch_or(true, atomic::Ordering::Release);
1856    fake_language_server
1857        .request::<lsp::request::InlayHintRefreshRequest>(())
1858        .await
1859        .expect("inlay refresh request failed");
1860    executor.run_until_parked();
1861    editor_a.update(cx_a, |editor, _| {
1862        assert!(
1863            extract_hint_labels(editor).is_empty(),
1864            "Host should get nop hints due to them turned off, even after the /refresh"
1865        );
1866        let inlay_cache = editor.inlay_hint_cache();
1867        assert_eq!(
1868            inlay_cache.version(),
1869            0,
1870            "Turned off hints should not generate version updates, again"
1871        );
1872    });
1873
1874    executor.run_until_parked();
1875    editor_b.update(cx_b, |editor, _| {
1876        assert_eq!(
1877            vec!["other hint".to_string()],
1878            extract_hint_labels(editor),
1879            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1880        );
1881        let inlay_cache = editor.inlay_hint_cache();
1882        assert_eq!(
1883            inlay_cache.version(),
1884            2,
1885            "Guest should accepted all edits and bump its cache version every time"
1886        );
1887    });
1888}
1889
1890fn extract_hint_labels(editor: &Editor) -> Vec<String> {
1891    let mut labels = Vec::new();
1892    for hint in editor.inlay_hint_cache().hints() {
1893        match hint.label {
1894            project::InlayHintLabel::String(s) => labels.push(s),
1895            _ => unreachable!(),
1896        }
1897    }
1898    labels
1899}