use crate::{ rpc::RECONNECT_TIMEOUT, tests::{rust_lang, TestServer},};use call::ActiveCall;use collections::HashMap;use editor::{ actions::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename, ToggleCodeActions, Undo, }, test::editor_test_context::{AssertionContextManager, EditorTestContext}, Editor, RowInfo,};use fs::Fs;use futures::StreamExt;use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};use indoc::indoc;use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, FakeLspAdapter,};use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, SERVER_PROGRESS_THROTTLE_TIMEOUT,};use recent_projects::disconnected_overlay::DisconnectedOverlay;use rpc::RECEIVE_TIMEOUT;use serde_json::json;use settings::SettingsStore;use std::{ ops::Range, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicBool, AtomicUsize}, Arc, },};use text::Point;use workspace::{CloseIntent, Workspace}; #[gpui::test(iterations = 10)]async fn test_host_disconnect( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext,) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; cx_b.update(editor::init); cx_b.update(recent_projects::init); client_a .fs() .insert_tree( "/a", json!({ "a.txt": "a-contents", "b.txt": "b-contents", }), ) .await; let active_call_a = cx_a.read(ActiveCall::global); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; cx_a.background_executor.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); let workspace_b = cx_b.add_window(|window, cx| { Workspace::new( None, project_b.clone(), client_b.app_state.clone(), window, cx, ) }); let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); let workspace_b_view = workspace_b.root(cx_b).unwrap(); let editor_b = workspace_b .update(cx_b, |workspace, window, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, window, cx) }) .unwrap() .await .unwrap() .downcast::<Editor>() .unwrap(); //TODO: focus assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window))); editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); cx_b.update(|_, cx| { assert!(workspace_b_view.read(cx).is_edited()); }); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); cx_a.background_executor .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer())); // Ensure client B's edited state is reset and that the whole window is blurred. workspace_b .update(cx_b, |workspace, _, cx| { assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some()); assert!(!workspace.is_edited()); }) .unwrap(); // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b .update(cx_b, |workspace, window, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) .unwrap() .await .unwrap(); assert!(can_close); // Allow client A to reconnect to the server. server.allow_connections(); cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT); // Client B calls client A again after they reconnected. let active_call_b = cx_b.read(ActiveCall::global); active_call_b .update(cx_b, |call, cx| { call.invite(client_a.user_id().unwrap(), None, cx) }) .await .unwrap(); cx_a.background_executor.run_until_parked(); active_call_a .update(cx_a, |call, cx| call.accept_incoming(cx)) .await .unwrap(); active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); // Drop client A's connection again. We should still unshare it successfully. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); cx_a.background_executor .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));} #[gpui::test]async fn test_newline_above_or_below_does_not_move_guest_cursor( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext,) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let executor = cx_a.executor(); server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); client_a .fs() .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client A let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); let cx_a = cx_a.add_empty_window(); let editor_a = cx_a .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a.clone(), window: cx_a.window_handle(), editor: editor_a, assertion_cx: AssertionContextManager::new(), }; let cx_b = cx_b.add_empty_window(); // Open a buffer as client B let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); let editor_b = cx_b .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b.clone(), window: cx_b.window_handle(), editor: editor_b, assertion_cx: AssertionContextManager::new(), }; // Test newline above editor_cx_a.set_selections_state(indoc! {" Some textˇ "}); editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); editor_cx_a.update_editor(|editor, window, cx| { editor.newline_above(&editor::actions::NewlineAbove, window, cx) }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" ˇ Some text "}); editor_cx_b.assert_editor_state(indoc! {" Some textˇ "}); // Test newline below editor_cx_a.set_selections_state(indoc! {" Some textˇ "}); editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); editor_cx_a.update_editor(|editor, window, cx| { editor.newline_below(&editor::actions::NewlineBelow, window, cx) }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" Some text ˇ "}); editor_cx_b.assert_editor_state(indoc! {" Some textˇ "});} #[gpui::test(iterations = 10)]async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), resolve_provider: Some(true), ..Default::default() }), ..Default::default() }, ..Default::default() }, ); client_a .fs() .insert_tree( "/a", json!({ "main.rs": "fn main() { a }", "other.rs": "", }), ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); let cx_b = cx_b.add_empty_window(); let editor_b = cx_b.new_window_entity(|window, cx| { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx) }); let fake_language_server = fake_language_servers.next().await.unwrap(); cx_a.background_executor.run_until_parked(); buffer_b.read_with(cx_b, |buffer, _| { assert!(!buffer.completion_triggers().is_empty()) }); // Type a completion trigger character as the guest. editor_b.update_in(cx_b, |editor, window, cx| { editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); editor.handle_input(".", window, cx); }); cx_b.focus(&editor_b); // Receive a completion request as the host's language server. // Return some completions from the host's language server. cx_a.executor().start_waiting(); fake_language_server .handle_request::<lsp::request::Completion, _, _>(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); assert_eq!( params.text_document_position.position, lsp::Position::new(0, 14), ); Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "first_method($1)".to_string(), range: lsp::Range::new( lsp::Position::new(0, 14), lsp::Position::new(0, 14), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, lsp::CompletionItem { label: "second_method(…)".into(), detail: Some("fn(&mut self, C) -> D<E>".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "second_method()".to_string(), range: lsp::Range::new( lsp::Position::new(0, 14), lsp::Position::new(0, 14), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, ]))) }) .next() .await .unwrap(); cx_a.executor().finish_waiting(); // Open the buffer on the host. let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); cx_a.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.text(), "fn main() { a. }") }); // Confirm a completion on the guest. editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx); assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); }); // Return a resolved completion from the host's language server. // The resolved completion has an additional text edit. fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>( |params, _| async move { assert_eq!(params.label, "first_method(…)"); Ok(lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "first_method($1)".to_string(), range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), })), additional_text_edits: Some(vec![lsp::TextEdit { new_text: "use d::SomeTrait;\n".to_string(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), }]), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }) }, ); // The additional edit is applied. cx_a.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method() }" ); }); buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method() }" ); }); // Now we do a second completion, this time to ensure that documentation/snippets are // resolved editor_b.update_in(cx_b, |editor, window, cx| { editor.change_selections(None, window, cx, |s| s.select_ranges([46..46])); editor.handle_input("; a", window, cx); editor.handle_input(".", window, cx); }); buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method(); a. }" ); }); let mut completion_response = fake_language_server .handle_request::<lsp::request::Completion, _, _>(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); assert_eq!( params.text_document_position.position, lsp::Position::new(1, 32), ); Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "third_method(…)".into(), detail: Some("fn(&mut self, B, C, D) -> E".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { // no snippet placehodlers new_text: "third_method".to_string(), range: lsp::Range::new( lsp::Position::new(1, 32), lsp::Position::new(1, 32), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), documentation: None, ..Default::default() }, ]))) }); // The completion now gets a new `text_edit.new_text` when resolving the completion item let mut resolve_completion_response = fake_language_server .handle_request::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {Lines 1–500 of 2472. Load more
use crate::{ rpc::RECONNECT_TIMEOUT, tests::{rust_lang, TestServer}, }; use call::ActiveCall; use collections::HashMap; use editor::{ actions::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename, ToggleCodeActions, Undo, }, test::editor_test_context::{AssertionContextManager, EditorTestContext}, Editor, RowInfo, }; use fs::Fs; use futures::StreamExt; use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, FakeLspAdapter, }; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; use serde_json::json; use settings::SettingsStore; use std::{ ops::Range, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicBool, AtomicUsize}, Arc, }, }; use text::Point; use workspace::{CloseIntent, Workspace}; #[gpui::test(iterations = 10)] async fn test_host_disconnect( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; cx_b.update(editor::init); cx_b.update(recent_projects::init); client_a .fs() .insert_tree( "/a", json!({ "a.txt": "a-contents", "b.txt": "b-contents", }), ) .await; let active_call_a = cx_a.read(ActiveCall::global); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; cx_a.background_executor.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); let workspace_b = cx_b.add_window(|window, cx| { Workspace::new( None, project_b.clone(), client_b.app_state.clone(), window, cx, ) }); let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); let workspace_b_view = workspace_b.root(cx_b).unwrap(); let editor_b = workspace_b .update(cx_b, |workspace, window, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, window, cx) }) .unwrap() .await .unwrap() .downcast::<Editor>() .unwrap(); //TODO: focus assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window))); editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); cx_b.update(|_, cx| { assert!(workspace_b_view.read(cx).is_edited()); }); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); cx_a.background_executor .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer())); // Ensure client B's edited state is reset and that the whole window is blurred. workspace_b .update(cx_b, |workspace, _, cx| { assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some()); assert!(!workspace.is_edited()); }) .unwrap(); // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b .update(cx_b, |workspace, window, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) .unwrap() .await .unwrap(); assert!(can_close); // Allow client A to reconnect to the server. server.allow_connections(); cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT); // Client B calls client A again after they reconnected. let active_call_b = cx_b.read(ActiveCall::global); active_call_b .update(cx_b, |call, cx| { call.invite(client_a.user_id().unwrap(), None, cx) }) .await .unwrap(); cx_a.background_executor.run_until_parked(); active_call_a .update(cx_a, |call, cx| call.accept_incoming(cx)) .await .unwrap(); active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); // Drop client A's connection again. We should still unshare it successfully. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); cx_a.background_executor .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); } #[gpui::test] async fn test_newline_above_or_below_does_not_move_guest_cursor( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let executor = cx_a.executor(); server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); client_a .fs() .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client A let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); let cx_a = cx_a.add_empty_window(); let editor_a = cx_a .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a.clone(), window: cx_a.window_handle(), editor: editor_a, assertion_cx: AssertionContextManager::new(), }; let cx_b = cx_b.add_empty_window(); // Open a buffer as client B let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); let editor_b = cx_b .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b.clone(), window: cx_b.window_handle(), editor: editor_b, assertion_cx: AssertionContextManager::new(), }; // Test newline above editor_cx_a.set_selections_state(indoc! {" Some textˇ "}); editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); editor_cx_a.update_editor(|editor, window, cx| { editor.newline_above(&editor::actions::NewlineAbove, window, cx) }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" ˇ Some text "}); editor_cx_b.assert_editor_state(indoc! {" Some textˇ "}); // Test newline below editor_cx_a.set_selections_state(indoc! {" Some textˇ "}); editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); editor_cx_a.update_editor(|editor, window, cx| { editor.newline_below(&editor::actions::NewlineBelow, window, cx) }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" Some text ˇ "}); editor_cx_b.assert_editor_state(indoc! {" Some textˇ "}); } #[gpui::test(iterations = 10)] async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), resolve_provider: Some(true), ..Default::default() }), ..Default::default() }, ..Default::default() }, ); client_a .fs() .insert_tree( "/a", json!({ "main.rs": "fn main() { a }", "other.rs": "", }), ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); let cx_b = cx_b.add_empty_window(); let editor_b = cx_b.new_window_entity(|window, cx| { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx) }); let fake_language_server = fake_language_servers.next().await.unwrap(); cx_a.background_executor.run_until_parked(); buffer_b.read_with(cx_b, |buffer, _| { assert!(!buffer.completion_triggers().is_empty()) }); // Type a completion trigger character as the guest. editor_b.update_in(cx_b, |editor, window, cx| { editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); editor.handle_input(".", window, cx); }); cx_b.focus(&editor_b); // Receive a completion request as the host's language server. // Return some completions from the host's language server. cx_a.executor().start_waiting(); fake_language_server .handle_request::<lsp::request::Completion, _, _>(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); assert_eq!( params.text_document_position.position, lsp::Position::new(0, 14), ); Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "first_method($1)".to_string(), range: lsp::Range::new( lsp::Position::new(0, 14), lsp::Position::new(0, 14), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, lsp::CompletionItem { label: "second_method(…)".into(), detail: Some("fn(&mut self, C) -> D<E>".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "second_method()".to_string(), range: lsp::Range::new( lsp::Position::new(0, 14), lsp::Position::new(0, 14), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, ]))) }) .next() .await .unwrap(); cx_a.executor().finish_waiting(); // Open the buffer on the host. let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); cx_a.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.text(), "fn main() { a. }") }); // Confirm a completion on the guest. editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx); assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); }); // Return a resolved completion from the host's language server. // The resolved completion has an additional text edit. fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>( |params, _| async move { assert_eq!(params.label, "first_method(…)"); Ok(lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text: "first_method($1)".to_string(), range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), })), additional_text_edits: Some(vec![lsp::TextEdit { new_text: "use d::SomeTrait;\n".to_string(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), }]), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }) }, ); // The additional edit is applied. cx_a.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method() }" ); }); buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method() }" ); }); // Now we do a second completion, this time to ensure that documentation/snippets are // resolved editor_b.update_in(cx_b, |editor, window, cx| { editor.change_selections(None, window, cx, |s| s.select_ranges([46..46])); editor.handle_input("; a", window, cx); editor.handle_input(".", window, cx); }); buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method(); a. }" ); }); let mut completion_response = fake_language_server .handle_request::<lsp::request::Completion, _, _>(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); assert_eq!( params.text_document_position.position, lsp::Position::new(1, 32), ); Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "third_method(…)".into(), detail: Some("fn(&mut self, B, C, D) -> E".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { // no snippet placehodlers new_text: "third_method".to_string(), range: lsp::Range::new( lsp::Position::new(1, 32), lsp::Position::new(1, 32), ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), documentation: None, ..Default::default() }, ]))) }); // The completion now gets a new `text_edit.new_text` when resolving the completion item let mut resolve_completion_response = fake_language_server .handle_request::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {