diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 5880d998925743d4cdd822574b647b53194e2116..e015550df9482c5850396b8bcf10e9cee24d5b76 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( "}); } -#[gpui::test(iterations = 10)] +#[gpui::test] 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; @@ -307,17 +307,35 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); - let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + let mut fake_language_servers = [ + client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ), + client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + name: "fake-analyzer", + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ), + ]; + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), ..FakeLspAdapter::default() }, ); - client_b.language_registry().add(rust_lang()); client_b.language_registry().register_fake_lsp_adapter( "Rust", FakeLspAdapter { + name: "fake-analyzer", capabilities, ..FakeLspAdapter::default() }, @@ -352,7 +370,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx) }); - let fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers[0].next().await.unwrap(); + let second_fake_language_server = fake_language_servers[1].next().await.unwrap(); cx_a.background_executor.run_until_parked(); buffer_b.read_with(cx_b, |buffer, _| { @@ -414,6 +433,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .next() .await .unwrap(); + second_fake_language_server + .set_request_handler::(|_, _| async move { Ok(None) }) + .next() + .await + .unwrap(); cx_a.executor().finish_waiting(); // Open the buffer on the host. @@ -522,6 +546,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ]))) }); + // Second language server also needs to handle the request (returns None) + let mut second_completion_response = second_fake_language_server + .set_request_handler::(|_, _| async move { Ok(None) }); + // The completion now gets a new `text_edit.new_text` when resolving the completion item let mut resolve_completion_response = fake_language_server .set_request_handler::(|params, _| async move { @@ -545,6 +573,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu cx_b.executor().run_until_parked(); completion_response.next().await.unwrap(); + second_completion_response.next().await.unwrap(); editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); @@ -563,6 +592,77 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }" ); }); + + // Ensure buffer is synced before proceeding with the next test + cx_a.executor().run_until_parked(); + cx_b.executor().run_until_parked(); + + // Test completions from the second fake language server + // Add another completion trigger to test the second language server + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([68..68]) + }); + editor.handle_input("; b", 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.third_method(, , ); b. }" + ); + }); + + // Set up completion handlers for both language servers + let mut first_lsp_completion = fake_language_server + .set_request_handler::(|_, _| async move { Ok(None) }); + + let mut second_lsp_completion = second_fake_language_server + .set_request_handler::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(1, 54), + ); + + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "analyzer_method(…)".into(), + detail: Some("fn(&self) -> Result".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "analyzer_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(1, 54), + lsp::Position::new(1, 54), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ]))) + }); + + cx_b.executor().run_until_parked(); + + // Await both language server responses + first_lsp_completion.next().await.unwrap(); + second_lsp_completion.next().await.unwrap(); + + cx_b.executor().run_until_parked(); + + // Confirm the completion from the second language server works + 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), + "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }" + ); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 89b3315272b137e507a65df19f98ac28aa194d6a..adea507f00eda72e715fe535da7016af44a4f723 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -218,6 +218,7 @@ pub(crate) struct GetHover { pub(crate) struct GetCompletions { pub position: PointUtf16, pub context: CompletionContext, + pub server_id: Option, } #[derive(Clone, Debug)] @@ -2395,6 +2396,7 @@ impl LspCommand for GetCompletions { buffer_id: buffer.remote_id().into(), position: Some(language::proto::serialize_anchor(&anchor)), version: serialize_version(&buffer.version()), + server_id: self.server_id.map(|id| id.to_proto()), } } @@ -2423,6 +2425,9 @@ impl LspCommand for GetCompletions { trigger_kind: CompletionTriggerKind::INVOKED, trigger_character: None, }, + server_id: message + .server_id + .map(|id| lsp::LanguageServerId::from_proto(id)), }) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 069c12c75c44e790028f27abdc12ffd6d2b613ab..53c1db7ad4aa2a0a98f5aba740133fbde823cf17 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3754,7 +3754,7 @@ impl LspStore { client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); client.add_entity_request_handler(Self::handle_rename_project_entry); client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics); - client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_get_completions); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); @@ -4463,6 +4463,41 @@ impl LspStore { .any(check) } + fn all_capable_for_proto_request( + &self, + buffer: &Entity, + mut check: F, + cx: &App, + ) -> Vec + where + F: FnMut(&lsp::LanguageServerName, &lsp::ServerCapabilities) -> bool, + { + let Some(language) = buffer.read(cx).language().cloned() else { + return Vec::default(); + }; + let relevant_language_servers = self + .languages + .lsp_adapters(&language.name()) + .into_iter() + .map(|lsp_adapter| lsp_adapter.name()) + .collect::>(); + self.language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + relevant_language_servers + .contains(&server_status.name) + .then_some((server_id, &server_status.name)) + }) + .filter_map(|(server_id, server_name)| { + self.lsp_server_capabilities + .get(server_id) + .map(|c| (server_id, server_name, c)) + }) + .filter(|(_, server_name, capabilities)| check(server_name, capabilities)) + .map(|(server_id, _, _)| *server_id) + .collect() + } + pub fn request_lsp( &mut self, buffer: Entity, @@ -5902,17 +5937,24 @@ impl LspStore { let language_registry = self.languages.clone(); if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCompletions { position, context }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); - } - let task = self.send_lsp_proto_request( - buffer.clone(), - upstream_client, - project_id, - request, + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(offset); + let capable_lsps = self.all_capable_for_proto_request( + buffer, + |server_name, capabilities| { + capabilities.completion_provider.is_some() + && scope + .as_ref() + .map(|scope| scope.language_allowed(server_name)) + .unwrap_or(true) + }, cx, ); + if capable_lsps.is_empty() { + return Task::ready(Ok(Vec::new())); + } + let language = buffer.read(cx).language().cloned(); // In the future, we should provide project guests with the names of LSP adapters, @@ -5925,19 +5967,53 @@ impl LspStore { .cloned() }); - cx.foreground_executor().spawn(async move { - let completion_response = task.await?; - let completions = populate_labels_for_completions( - completion_response.completions, - language, - lsp_adapter, - ) - .await; - Ok(vec![CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete: completion_response.is_incomplete, - }]) + let buffer = buffer.clone(); + + cx.spawn(async move |this, cx| { + let requests = join_all( + capable_lsps + .into_iter() + .map(|id| { + let request = GetCompletions { + position, + context: context.clone(), + server_id: Some(id), + }; + let buffer = buffer.clone(); + let language = language.clone(); + let lsp_adapter = lsp_adapter.clone(); + let upstream_client = upstream_client.clone(); + let response = this + .update(cx, |this, cx| { + this.send_lsp_proto_request( + buffer, + upstream_client, + project_id, + request, + cx, + ) + }) + .log_err(); + async move { + let response = response?.await.log_err()?; + + let completions = populate_labels_for_completions( + response.completions, + language, + lsp_adapter, + ) + .await; + + Some(CompletionResponse { + completions, + display_options: CompletionDisplayOptions::default(), + is_incomplete: response.is_incomplete, + }) + } + }) + .collect::>(), + ); + Ok(requests.await.into_iter().flatten().collect::>()) }) } else if let Some(local) = self.as_local() { let snapshot = buffer.read(cx).snapshot(); @@ -5998,6 +6074,7 @@ impl LspStore { GetCompletions { position, context: context.clone(), + server_id: Some(server_id), }, cx, ).fuse(); @@ -8461,6 +8538,46 @@ impl LspStore { }) } + async fn handle_lsp_get_completions( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let sender_id = envelope.original_sender_id().unwrap_or_default(); + + let buffer_id = GetCompletions::buffer_id_from_proto(&envelope.payload)?; + let buffer_handle = this.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + let request = GetCompletions::from_proto( + envelope.payload, + this.clone(), + buffer_handle.clone(), + cx.clone(), + ) + .await?; + + let server_to_query = match request.server_id { + Some(server_id) => LanguageServerToQuery::Other(server_id), + None => LanguageServerToQuery::FirstCapable, + }; + + let response = this + .update(&mut cx, |this, cx| { + this.request_lsp(buffer_handle.clone(), server_to_query, request, cx) + })? + .await?; + this.update(&mut cx, |this, cx| { + Ok(GetCompletions::response_to_proto( + response, + this, + sender_id, + &buffer_handle.read(cx).version(), + cx, + )) + })? + } + async fn handle_lsp_command( this: Entity, envelope: TypedEnvelope, diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 644e492ef6a5d639a99f75b18465ca93b0c0ef92..3bdd46c4572acbc570c198288ba5c79b93aa4286 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -703,6 +703,7 @@ message GetCompletions { uint64 buffer_id = 2; Anchor position = 3; repeated VectorClockEntry version = 4; + optional uint64 server_id = 5; } message CancelLanguageServerWork { diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 4ceaf2048c5967b7fe1fceeb47c68efc6cc15678..4ff0c57d5571c5fb9e16df18078514e16ea12867 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -398,12 +398,17 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext json!({ "settings.json": r#" { - "languages": {"Rust":{"language_servers":["rust-analyzer"]}}, + "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}}, "lsp": { "rust-analyzer": { "binary": { "path": "~/.cargo/bin/rust-analyzer" } + }, + "fake-analyzer": { + "binary": { + "path": "~/.cargo/bin/rust-analyzer" + } } } }"# @@ -431,6 +436,18 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext }, ..FakeLspAdapter::default() }, + ); + project.languages().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "fake-analyzer", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions::default()), + rename_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, ) }); @@ -446,6 +463,30 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext ) }); + let mut fake_second_lsp = server_cx.update(|cx| { + headless.read(cx).languages.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "fake-analyzer", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions::default()), + rename_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + headless.read(cx).languages.register_fake_language_server( + LanguageServerName("fake-analyzer".into()), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions::default()), + rename_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + None, + ) + }); + cx.run_until_parked(); let worktree_id = project @@ -469,12 +510,13 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext cx.run_until_parked(); let fake_lsp = fake_lsp.next().await.unwrap(); + let fake_second_lsp = fake_second_lsp.next().await.unwrap(); cx.read(|cx| { let file = buffer.read(cx).file(); assert_eq!( language_settings(Some("Rust".into()), file, cx).language_servers, - ["rust-analyzer".to_string()] + ["rust-analyzer".to_string(), "fake-analyzer".to_string()] ) }); @@ -497,7 +539,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext server_cx.read(|cx| { let lsp_store = headless.read(cx).lsp_store.read(cx); - assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1); + assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2); }); fake_lsp.set_request_handler::(|_, _| async move { @@ -507,6 +549,13 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext }]))) }); + fake_second_lsp.set_request_handler::(|_, _| async move { + Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem { + label: "beep".to_string(), + ..Default::default() + }]))) + }); + let result = project .update(cx, |project, cx| { project.completions( @@ -528,7 +577,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext .flat_map(|response| response.completions) .map(|c| c.label.text) .collect::>(), - vec!["boop".to_string()] + vec!["boop".to_string(), "beep".to_string()] ); fake_lsp.set_request_handler::(|_, _| async move {