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