diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 16f131af51..a2a4c16a2a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -368,6 +368,7 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) .add_message_handler(broadcast_project_message_from_host::) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 598d755e92..b15e2b5123 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4934,9 +4934,35 @@ async fn test_lsp_hover( .await; client_a.language_registry().add(rust_lang()); + let language_server_names = ["rust-analyzer", "CrabLang-ls"]; let mut fake_language_servers = client_a .language_registry() - .register_fake_lsp_adapter("Rust", Default::default()); + .register_specific_fake_lsp_adapter( + "Rust", + true, + FakeLspAdapter { + name: "rust-analyzer", + capabilities: lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + let _other_server = client_a + .language_registry() + .register_specific_fake_lsp_adapter( + "Rust", + false, + FakeLspAdapter { + name: "CrabLang-ls", + capabilities: lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -4949,66 +4975,133 @@ async fn test_lsp_hover( let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - // Request hover information as the guest. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params - .text_document_position_params - .text_document - .uri - .as_str(), - "file:///root-1/main.rs" - ); - assert_eq!( - params.text_document_position_params.position, - lsp::Position::new(0, 22) - ); - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Array(vec![ - lsp::MarkedString::String("Test hover content.".to_string()), - lsp::MarkedString::LanguageString(lsp::LanguageString { - language: "Rust".to_string(), - value: "let foo = 42;".to_string(), - }), - ]), - range: Some(lsp::Range::new( - lsp::Position::new(0, 22), - lsp::Position::new(0, 29), - )), - })) - }, - ); + let mut servers_with_hover_requests = HashMap::default(); + for i in 0..language_server_names.len() { + let new_server = fake_language_servers.next().await.unwrap_or_else(|| { + panic!( + "Failed to get language server #{i} with name {}", + &language_server_names[i] + ) + }); + let new_server_name = new_server.server.name(); + assert!( + !servers_with_hover_requests.contains_key(new_server_name), + "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`" + ); + let new_server_name = new_server_name.to_string(); + match new_server_name.as_str() { + "CrabLang-ls" => { + servers_with_hover_requests.insert( + new_server_name.clone(), + new_server.handle_request::( + move |params, _| { + assert_eq!( + params + .text_document_position_params + .text_document + .uri + .as_str(), + "file:///root-1/main.rs" + ); + let name = new_server_name.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Scalar( + lsp::MarkedString::String(format!("{name} hover")), + ), + range: None, + })) + } + }, + ), + ); + } + "rust-analyzer" => { + servers_with_hover_requests.insert( + new_server_name.clone(), + new_server.handle_request::( + |params, _| async move { + assert_eq!( + params + .text_document_position_params + .text_document + .uri + .as_str(), + "file:///root-1/main.rs" + ); + assert_eq!( + params.text_document_position_params.position, + lsp::Position::new(0, 22) + ); + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("Test hover content.".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "let foo = 42;".to_string(), + }), + ]), + range: Some(lsp::Range::new( + lsp::Position::new(0, 22), + lsp::Position::new(0, 29), + )), + })) + }, + ), + ); + } + unexpected => panic!("Unexpected server name: {unexpected}"), + } + } - let hovers = project_b + // Request hover information as the guest. + let mut hovers = project_b .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) .await; assert_eq!( hovers.len(), - 1, - "Expected exactly one hover but got: {hovers:?}" + 2, + "Expected two hovers from both language servers, but got: {hovers:?}" ); - let hover_info = hovers.into_iter().next().unwrap(); + let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map( + |mut hover_request| async move { + hover_request + .next() + .await + .expect("All hover requests should have been triggered") + }, + )) + .await; + + hovers.sort_by_key(|hover| hover.contents.len()); + let first_hover = hovers.first().cloned().unwrap(); + assert_eq!( + first_hover.contents, + vec![project::HoverBlock { + text: "CrabLang-ls hover".to_string(), + kind: HoverBlockKind::Markdown, + },] + ); + let second_hover = hovers.last().cloned().unwrap(); + assert_eq!( + second_hover.contents, + vec![ + project::HoverBlock { + text: "Test hover content.".to_string(), + kind: HoverBlockKind::Markdown, + }, + project::HoverBlock { + text: "let foo = 42;".to_string(), + kind: HoverBlockKind::Code { + language: "Rust".to_string() + }, + } + ] + ); buffer_b.read_with(cx_b, |buffer, _| { let snapshot = buffer.snapshot(); - assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29); - assert_eq!( - hover_info.contents, - vec![ - project::HoverBlock { - text: "Test hover content.".to_string(), - kind: HoverBlockKind::Markdown, - }, - project::HoverBlock { - text: "let foo = 42;".to_string(), - kind: HoverBlockKind::Code { - language: "Rust".to_string() - }, - } - ] - ); + assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29); }); } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ea6fdd0e65..2b88d1cfa3 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -117,6 +117,7 @@ pub(crate) struct GetDocumentHighlights { pub position: PointUtf16, } +#[derive(Clone)] pub(crate) struct GetHover { pub position: PointUtf16, } @@ -125,6 +126,7 @@ pub(crate) struct GetCompletions { pub position: PointUtf16, } +#[derive(Clone)] pub(crate) struct GetCodeActions { pub range: Range, pub kinds: Option>, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d5598621b..9e1ac82964 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,7 +26,7 @@ use futures::{ mpsc::{self, UnboundedReceiver}, oneshot, }, - future::{try_join_all, Shared}, + future::{join_all, try_join_all, Shared}, select, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, @@ -55,7 +55,7 @@ use log::error; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, - MessageActionItem, OneOf, ServerHealthStatus, ServerStatus, + MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus, ServerStatus, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -463,7 +463,7 @@ pub enum HoverBlockKind { Code { language: String }, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Hover { pub contents: Vec, pub range: Option>, @@ -601,6 +601,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_diff_base); client.add_model_request_handler(Self::handle_lsp_command::); client.add_model_request_handler(Self::handle_blame_buffer); + client.add_model_request_handler(Self::handle_multi_lsp_query); } pub fn local( @@ -5215,74 +5216,78 @@ impl Project { position: PointUtf16, cx: &mut ModelContext, ) -> Task> { - fn remove_empty_hover_blocks(mut hover: Hover) -> Option { - hover - .contents - .retain(|hover_block| !hover_block.text.trim().is_empty()); - if hover.contents.is_empty() { - None - } else { - Some(hover) - } - } - if self.is_local() { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - - let mut hover_responses = self - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| match server.capabilities().hover_provider { + let all_actions_task = self.request_multiple_lsp_locally( + &buffer, + Some(position), + |server_capabilities| match server_capabilities.hover_provider { Some(lsp::HoverProviderCapability::Simple(enabled)) => enabled, Some(lsp::HoverProviderCapability::Options(_)) => true, None => false, - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .map(|server_id| { - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetHover { position }, - cx, - ) - }) - .collect::>(); - - cx.spawn(|_, _| async move { - let mut hovers = Vec::with_capacity(hover_responses.len()); - while let Some(hover_response) = hover_responses.next().await { - if let Some(hover) = hover_response - .log_err() - .flatten() - .and_then(remove_empty_hover_blocks) - { - hovers.push(hover); - } - } - hovers - }) - } else if self.is_remote() { - let request_task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Primary, + }, GetHover { position }, cx, ); cx.spawn(|_, _| async move { - request_task + all_actions_task .await - .log_err() - .flatten() - .and_then(remove_empty_hover_blocks) - .map(|hover| vec![hover]) - .unwrap_or_default() + .into_iter() + .filter_map(|hover| remove_empty_hover_blocks(hover?)) + .collect() + }) + } else if let Some(project_id) = self.remote_id() { + let request_task = self.client().request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), + project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetHover( + GetHover { position }.to_proto(project_id, buffer.read(cx)), + )), + }); + let buffer = buffer.clone(); + cx.spawn(|weak_project, cx| async move { + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( + request_task + .await + .log_err() + .map(|response| response.responses) + .unwrap_or_default() + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetHoverResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|hover_response| { + let response = GetHover { position }.response_from_proto( + hover_response, + project.clone(), + buffer.clone(), + cx.clone(), + ); + async move { + response + .await + .log_err() + .flatten() + .and_then(remove_empty_hover_blocks) + } + }), + ) + .await + .into_iter() + .flatten() + .collect() }) } else { log::error!("cannot show hovers: project does not have a remote id"); @@ -5651,48 +5656,73 @@ impl Project { cx: &mut ModelContext, ) -> Task> { if self.is_local() { - let snapshot = buffer_handle.read(cx).snapshot(); - let offset = range.start.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - - let mut hover_responses = self - .language_servers_for_buffer(buffer_handle.read(cx), cx) - .filter(|(_, server)| GetCodeActions::supports_code_actions(server.capabilities())) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .map(|server_id| { - self.request_lsp( - buffer_handle.clone(), - LanguageServerToQuery::Other(server_id), - GetCodeActions { - range: range.clone(), - kinds: None, - }, - cx, - ) - }) - .collect::>(); - - cx.spawn(|_, _| async move { - let mut hovers = Vec::with_capacity(hover_responses.len()); - while let Some(hover_response) = hover_responses.next().await { - hovers.extend(hover_response.log_err().unwrap_or_default()); - } - hovers - }) - } else if self.is_remote() { - let request_task = self.request_lsp( - buffer_handle.clone(), - LanguageServerToQuery::Primary, - GetCodeActions { range, kinds: None }, + let all_actions_task = self.request_multiple_lsp_locally( + &buffer_handle, + Some(range.start), + GetCodeActions::supports_code_actions, + GetCodeActions { + range: range.clone(), + kinds: None, + }, cx, ); - cx.spawn(|_, _| async move { request_task.await.log_err().unwrap_or_default() }) + cx.spawn(|_, _| async move { all_actions_task.await.into_iter().flatten().collect() }) + } else if let Some(project_id) = self.remote_id() { + let request_task = self.client().request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), + project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeActions( + GetCodeActions { + range: range.clone(), + kinds: None, + } + .to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); + cx.spawn(|weak_project, cx| async move { + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( + request_task + .await + .log_err() + .map(|response| response.responses) + .unwrap_or_default() + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetCodeActionsResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|code_actions_response| { + let response = GetCodeActions { + range: range.clone(), + kinds: None, + } + .response_from_proto( + code_actions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ); + async move { response.await.log_err().unwrap_or_default() } + }), + ) + .await + .into_iter() + .flatten() + .collect() + }) } else { log::error!("cannot fetch actions: project does not have a remote id"); Task::ready(Vec::new()) @@ -6671,6 +6701,57 @@ impl Project { Task::ready(Ok(Default::default())) } + fn request_multiple_lsp_locally( + &self, + buffer: &Model, + position: Option

, + server_capabilities_check: fn(&ServerCapabilities) -> bool, + request: R, + cx: &mut ModelContext<'_, Self>, + ) -> Task> + where + P: ToOffset, + R: LspCommand + Clone, + ::Result: Send, + ::Params: Send, + { + if !self.is_local() { + debug_panic!("Should not request multiple lsp commands in non-local project"); + return Task::ready(Vec::new()); + } + let snapshot = buffer.read(cx).snapshot(); + let scope = position.and_then(|position| snapshot.language_scope_at(position)); + let mut response_results = self + .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server_capabilities_check(server.capabilities())) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .map(|server_id| { + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + request.clone(), + cx, + ) + }) + .collect::>(); + + return cx.spawn(|_, _| async move { + let mut responses = Vec::with_capacity(response_results.len()); + while let Some(response_result) = response_results.next().await { + if let Some(response) = response_result.log_err() { + responses.push(response); + } + } + responses + }); + } + fn send_lsp_proto_request( &self, buffer: Model, @@ -7614,6 +7695,118 @@ impl Project { Ok(serialize_blame_buffer_response(blame)) } + async fn handle_multi_lsp_query( + project: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + let version = deserialize_version(&envelope.payload.version); + let buffer = project.update(&mut cx, |project, _cx| { + project + .opened_buffers + .get(&buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id)) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + let buffer_version = buffer.update(&mut cx, |buffer, _| buffer.version())?; + match envelope + .payload + .strategy + .context("invalid request without the strategy")? + { + proto::multi_lsp_query::Strategy::All(_) => { + // currently, there's only one multiple language servers query strategy, + // so just ensure it's specified correctly + } + } + match envelope.payload.request { + Some(proto::multi_lsp_query::Request::GetHover(get_hover)) => { + let get_hover = + GetHover::from_proto(get_hover, project.clone(), buffer.clone(), cx.clone()) + .await?; + let all_hovers = project + .update(&mut cx, |project, cx| { + project.request_multiple_lsp_locally( + &buffer, + Some(get_hover.position), + |server_capabilities| match server_capabilities.hover_provider { + Some(lsp::HoverProviderCapability::Simple(enabled)) => enabled, + Some(lsp::HoverProviderCapability::Options(_)) => true, + None => false, + }, + get_hover, + cx, + ) + })? + .await + .into_iter() + .filter_map(|hover| remove_empty_hover_blocks(hover?)); + project.update(&mut cx, |project, cx| proto::MultiLspQueryResponse { + responses: all_hovers + .map(|hover| proto::LspResponse { + response: Some(proto::lsp_response::Response::GetHoverResponse( + GetHover::response_to_proto( + Some(hover), + project, + sender_id, + &buffer_version, + cx, + ), + )), + }) + .collect(), + }) + } + Some(proto::multi_lsp_query::Request::GetCodeActions(get_code_actions)) => { + let get_code_actions = GetCodeActions::from_proto( + get_code_actions, + project.clone(), + buffer.clone(), + cx.clone(), + ) + .await?; + + let all_actions = project + .update(&mut cx, |project, cx| { + project.request_multiple_lsp_locally( + &buffer, + Some(get_code_actions.range.start), + GetCodeActions::supports_code_actions, + get_code_actions, + cx, + ) + })? + .await + .into_iter(); + + project.update(&mut cx, |project, cx| proto::MultiLspQueryResponse { + responses: all_actions + .map(|code_actions| proto::LspResponse { + response: Some(proto::lsp_response::Response::GetCodeActionsResponse( + GetCodeActions::response_to_proto( + code_actions, + project, + sender_id, + &buffer_version, + cx, + ), + )), + }) + .collect(), + }) + } + None => anyhow::bail!("empty multi lsp query request"), + } + } + async fn handle_unshare_project( this: Model, _: TypedEnvelope, @@ -10149,3 +10342,14 @@ fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> gi messages, } } + +fn remove_empty_hover_blocks(mut hover: Hover) -> Option { + hover + .contents + .retain(|hover_block| !hover_block.text.trim().is_empty()); + if hover.contents.is_empty() { + None + } else { + Some(hover) + } +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 29ca72fb30..b473213de9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4484,10 +4484,12 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { let mut servers_with_hover_requests = HashMap::default(); for i in 0..language_server_names.len() { - let new_server = fake_tsx_language_servers - .next() - .await - .unwrap_or_else(|| panic!("Failed to get language server #{i}")); + let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| { + panic!( + "Failed to get language server #{i} with name {}", + &language_server_names[i] + ) + }); let new_server_name = new_server.server.name(); assert!( !servers_with_hover_requests.contains_key(new_server_name), @@ -4706,10 +4708,12 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { let mut servers_with_actions_requests = HashMap::default(); for i in 0..language_server_names.len() { - let new_server = fake_tsx_language_servers - .next() - .await - .unwrap_or_else(|| panic!("Failed to get language server #{i}")); + let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| { + panic!( + "Failed to get language server #{i} with name {}", + &language_server_names[i] + ) + }); let new_server_name = new_server.server.name(); assert!( !servers_with_actions_requests.contains_key(new_server_name), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 630308b459..76779c85e3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -209,8 +209,11 @@ message Envelope { BlameBuffer blame_buffer = 172; BlameBufferResponse blame_buffer_response = 173; - - UpdateNotification update_notification = 174; // current max + + UpdateNotification update_notification = 174; + + MultiLspQuery multi_lsp_query = 175; + MultiLspQueryResponse multi_lsp_query_response = 176; // current max } reserved 158 to 161; @@ -1838,3 +1841,29 @@ message BlameBufferResponse { repeated CommitMessage messages = 2; repeated CommitPermalink permalinks = 3; } + +message MultiLspQuery { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + oneof strategy { + AllLanguageServers all = 4; + } + oneof request { + GetHover get_hover = 5; + GetCodeActions get_code_actions = 6; + } +} + +message AllLanguageServers {} + +message MultiLspQueryResponse { + repeated LspResponse responses = 1; +} + +message LspResponse { + oneof response { + GetHoverResponse get_hover_response = 1; + GetCodeActionsResponse get_code_actions_response = 2; + } +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 89f44faab8..c5a8f7d32b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -299,6 +299,8 @@ messages!( (SetRoomParticipantRole, Foreground), (BlameBuffer, Foreground), (BlameBufferResponse, Foreground), + (MultiLspQuery, Background), + (MultiLspQueryResponse, Background), ); request_messages!( @@ -390,6 +392,7 @@ request_messages!( (LspExtExpandMacro, LspExtExpandMacroResponse), (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), + (MultiLspQuery, MultiLspQueryResponse), ); entity_messages!( @@ -418,6 +421,7 @@ entity_messages!( InlayHints, JoinProject, LeaveProject, + MultiLspQuery, OnTypeFormatting, OpenBufferById, OpenBufferByPath,