@@ -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::<lsp::request::HoverRequest, _, _>(
- |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::<lsp::request::HoverRequest, _, _>(
+ 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::<lsp::request::HoverRequest, _, _>(
+ |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);
});
}
@@ -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<HoverBlock>,
pub range: Option<Range<language::Anchor>>,
@@ -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::<lsp_ext_command::ExpandMacro>);
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<Self>,
) -> Task<Vec<Hover>> {
- fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
- 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::<FuturesUnordered<_>>();
-
- 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<Self>,
) -> Task<Vec<CodeAction>> {
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::<FuturesUnordered<_>>();
-
- 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<P, R>(
+ &self,
+ buffer: &Model<Buffer>,
+ position: Option<P>,
+ server_capabilities_check: fn(&ServerCapabilities) -> bool,
+ request: R,
+ cx: &mut ModelContext<'_, Self>,
+ ) -> Task<Vec<R::Response>>
+ where
+ P: ToOffset,
+ R: LspCommand + Clone,
+ <R::LspRequest as lsp::request::Request>::Result: Send,
+ <R::LspRequest as lsp::request::Request>::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::<FuturesUnordered<_>>();
+
+ 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<R: LspCommand>(
&self,
buffer: Model<Buffer>,
@@ -7614,6 +7695,118 @@ impl Project {
Ok(serialize_blame_buffer_response(blame))
}
+ async fn handle_multi_lsp_query(
+ project: Model<Self>,
+ envelope: TypedEnvelope<proto::MultiLspQuery>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::MultiLspQueryResponse> {
+ 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<Self>,
_: TypedEnvelope<proto::UnshareProject>,
@@ -10149,3 +10342,14 @@ fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> gi
messages,
}
}
+
+fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
+ hover
+ .contents
+ .retain(|hover_block| !hover_block.text.trim().is_empty());
+ if hover.contents.is_empty() {
+ None
+ } else {
+ Some(hover)
+ }
+}