Query code actions and hovers from all related local language servers (from remote clients) (#10111)

Supersedes https://github.com/zed-industries/zed/pull/8634
Fixes https://github.com/zed-industries/zed/issues/7947 by continuing
https://github.com/zed-industries/zed/pull/9943 with the remote part.

Now, clients are able to issue collab requests, that query all related
language servers, not only the primary one.
Such mode is enabled for GetHover and GetCodeActions LSP requests only.

Release Notes:

- Added Tailwind CSS hover popovers for Zed in multi player mode
([7947](https://github.com/zed-industries/zed/issues/7947))
This commit is contained in:
Kirill Bulatov 2024-04-03 12:34:56 +02:00 committed by GitHub
parent 3a0d3cee87
commit 9aad30a559
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 501 additions and 164 deletions

View file

@ -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)
}
}