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