Query code actions and hovers from all related local language servers (#9943)

<img width="1122" alt="Screenshot 2024-03-28 at 21 51 18"
src="https://github.com/zed-industries/zed/assets/2690773/37ef7202-f10f-462f-a2fa-044b2d806191">


Part of https://github.com/zed-industries/zed/issues/7947 and
https://github.com/zed-industries/zed/issues/9912 that adds makes Zed
query all related language servers instead of the primary one.

Collab clients are still querying the primary one only, but this is
quite hard to solve, https://github.com/zed-industries/zed/pull/8634
drafts a part of it.
The local part is useful per se, as many people use Zed & Tailwind but
do not use collab features.

Unfortunately, eslint still returns empty actions list when queried, but
querying actions for all related language servers looks reasonable and
rare enough to be dangerous.

Release Notes:

- Added Tailwind CSS hover popovers for Zed in single player mode
([7947](https://github.com/zed-industries/zed/issues/7947))
This commit is contained in:
Kirill Bulatov 2024-03-29 11:18:38 +01:00 committed by GitHub
parent c4bc172850
commit c7f04691d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 493 additions and 56 deletions

View File

@ -4980,8 +4980,7 @@ async fn test_lsp_hover(
let hovers = project_b
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
.await
.unwrap();
.await;
assert_eq!(
hovers.len(),
1,

View File

@ -832,7 +832,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.boxed(),
LspRequestKind::CodeAction => project
.code_actions(&buffer, offset..offset, cx)
.map_ok(|_| ())
.map(|_| Ok(()))
.boxed(),
LspRequestKind::Definition => project
.definition(&buffer, offset, cx)

View File

@ -376,6 +376,7 @@ impl Copilot {
use node_runtime::FakeNodeRuntime;
let (server, fake_server) = FakeLanguageServer::new(
LanguageServerId(0),
LanguageServerBinary {
path: "path/to/copilot".into(),
arguments: vec![],

View File

@ -3758,19 +3758,17 @@ impl Editor {
let actions = if let Ok(code_actions) = project.update(&mut cx, |project, cx| {
project.code_actions(&start_buffer, start..end, cx)
}) {
code_actions.await.log_err()
code_actions.await
} else {
None
Vec::new()
};
this.update(&mut cx, |this, cx| {
this.available_code_actions = actions.and_then(|actions| {
if actions.is_empty() {
None
} else {
Some((start_buffer, actions.into()))
}
});
this.available_code_actions = if actions.is_empty() {
None
} else {
Some((start_buffer, actions.into()))
};
cx.notify();
})
.log_err();

View File

@ -295,7 +295,7 @@ fn show_hover(
});
})?;
let hovers_response = hover_request.await.ok().unwrap_or_default();
let hovers_response = hover_request.await;
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let mut hover_highlights = Vec::with_capacity(hovers_response.len());

View File

@ -234,13 +234,23 @@ impl LanguageRegistry {
&self,
language_name: &str,
adapter: crate::FakeLspAdapter,
) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
self.register_specific_fake_lsp_adapter(language_name, true, adapter)
}
#[cfg(any(feature = "test-support", test))]
pub fn register_specific_fake_lsp_adapter(
&self,
language_name: &str,
primary: bool,
adapter: crate::FakeLspAdapter,
) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
self.state
.write()
.lsp_adapters
.entry(language_name.into())
.or_default()
.push(CachedLspAdapter::new(Arc::new(adapter), true));
.push(CachedLspAdapter::new(Arc::new(adapter), primary));
self.fake_language_servers(language_name)
}
@ -739,6 +749,7 @@ impl LanguageRegistry {
.unwrap_or_default();
let (server, mut fake_server) = lsp::FakeLanguageServer::new(
server_id,
binary,
adapter.name.0.to_string(),
capabilities,

View File

@ -1108,6 +1108,7 @@ pub struct FakeLanguageServer {
impl FakeLanguageServer {
/// Construct a fake language server.
pub fn new(
server_id: LanguageServerId,
binary: LanguageServerBinary,
name: String,
capabilities: ServerCapabilities,
@ -1117,8 +1118,8 @@ impl FakeLanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let (notifications_tx, notifications_rx) = channel::unbounded();
let server = LanguageServer::new_internal(
LanguageServerId(0),
let mut server = LanguageServer::new_internal(
server_id,
stdin_writer,
stdout_reader,
None::<async_pipe::PipeReader>,
@ -1129,30 +1130,35 @@ impl FakeLanguageServer {
cx.clone(),
|_| {},
);
server.name = name.as_str().into();
let fake = FakeLanguageServer {
binary,
server: Arc::new(LanguageServer::new_internal(
LanguageServerId(0),
stdout_writer,
stdin_reader,
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
Path::new("/"),
None,
cx,
move |msg| {
notifications_tx
.try_send((
msg.method.to_string(),
msg.params
.map(|raw_value| raw_value.get())
.unwrap_or("null")
.to_string(),
))
.ok();
},
)),
server: Arc::new({
let mut server = LanguageServer::new_internal(
server_id,
stdout_writer,
stdin_reader,
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
Path::new("/"),
None,
cx,
move |msg| {
notifications_tx
.try_send((
msg.method.to_string(),
msg.params
.map(|raw_value| raw_value.get())
.unwrap_or("null")
.to_string(),
))
.ok();
},
);
server.name = name.as_str().into();
server
}),
notifications_rx,
};
fake.handle_request::<request::Initialize, _, _>({
@ -1350,6 +1356,7 @@ mod tests {
release_channel::init("0.0.0", cx);
});
let (server, mut fake) = FakeLanguageServer::new(
LanguageServerId(0),
LanguageServerBinary {
path: "path/to/language-server".into(),
arguments: vec![],

View File

@ -1855,6 +1855,17 @@ impl GetCodeActions {
})
.unwrap_or(false)
}
pub fn supports_code_actions(capabilities: &ServerCapabilities) -> bool {
capabilities
.code_action_provider
.as_ref()
.map(|options| match options {
lsp::CodeActionProviderCapability::Simple(is_supported) => *is_supported,
lsp::CodeActionProviderCapability::Options(_) => true,
})
.unwrap_or(false)
}
}
#[async_trait(?Send)]

View File

@ -5192,14 +5192,64 @@ impl Project {
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Hover>>> {
let request_task = self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetHover { position },
cx,
);
cx.spawn(|_, _| async move { request_task.await.map(|hover| hover.into_iter().collect()) })
) -> Task<Vec<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 {
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() {
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
.await
.log_err()
.flatten()
.map(|hover| vec![hover])
.unwrap_or_default()
})
} else {
log::error!("cannot show hovers: project does not have a remote id");
Task::ready(Vec::new())
}
}
pub fn hover<T: ToPointUtf16>(
@ -5207,7 +5257,7 @@ impl Project {
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Hover>>> {
) -> Task<Vec<Hover>> {
let position = position.to_point_utf16(buffer.read(cx));
self.hover_impl(buffer, position, cx)
}
@ -5561,13 +5611,54 @@ impl Project {
buffer_handle: &Model<Buffer>,
range: Range<Anchor>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<CodeAction>>> {
self.request_lsp(
buffer_handle.clone(),
LanguageServerToQuery::Primary,
GetCodeActions { range, kinds: None },
cx,
)
) -> 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 },
cx,
);
cx.spawn(|_, _| async move { request_task.await.log_err().unwrap_or_default() })
} else {
log::error!("cannot fetch actions: project does not have a remote id");
Task::ready(Vec::new())
}
}
pub fn code_actions<T: Clone + ToOffset>(
@ -5575,7 +5666,7 @@ impl Project {
buffer_handle: &Model<Buffer>,
range: Range<T>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<CodeAction>>> {
) -> Task<Vec<CodeAction>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
self.code_actions_impl(buffer_handle, range, cx)

View File

@ -2522,7 +2522,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
.next()
.await;
let action = actions.await.unwrap()[0].clone();
let action = actions.await[0].clone();
let apply = project.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), action, true, cx)
});
@ -4404,6 +4404,311 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
assert!(result.is_err())
}
#[gpui::test]
async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
"a.tsx": "a",
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(tsx_lang());
let language_server_names = [
"TypeScriptServer",
"TailwindServer",
"ESLintServer",
"NoHoverCapabilitiesServer",
];
let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
"tsx",
true,
FakeLspAdapter {
name: &language_server_names[0],
capabilities: lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _a = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[1],
capabilities: lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _b = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[2],
capabilities: lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _c = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[3],
capabilities: lsp::ServerCapabilities {
hover_provider: None,
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
.await
.unwrap();
cx.executor().run_until_parked();
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_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() {
"TailwindServer" | "TypeScriptServer" => {
servers_with_hover_requests.insert(
new_server_name.clone(),
new_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| {
let name = new_server_name.clone();
async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Scalar(lsp::MarkedString::String(
format!("{name} hover"),
)),
range: None,
}))
}
}),
);
}
"ESLintServer" => {
servers_with_hover_requests.insert(
new_server_name,
new_server.handle_request::<lsp::request::HoverRequest, _, _>(
|_, _| async move { Ok(None) },
),
);
}
"NoHoverCapabilitiesServer" => {
let _never_handled = new_server.handle_request::<lsp::request::HoverRequest, _, _>(
|_, _| async move {
panic!(
"Should not call for hovers server with no corresponding capabilities"
)
},
);
}
unexpected => panic!("Unexpected server name: {unexpected}"),
}
}
let hover_task = project.update(cx, |project, cx| {
project.hover(&buffer, Point::new(0, 0), cx)
});
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;
assert_eq!(
vec!["TailwindServer hover", "TypeScriptServer hover"],
hover_task
.await
.into_iter()
.map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
.sorted()
.collect::<Vec<_>>(),
"Should receive hover responses from all related servers with hover capabilities"
);
}
#[gpui::test]
async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
"a.tsx": "a",
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(tsx_lang());
let language_server_names = [
"TypeScriptServer",
"TailwindServer",
"ESLintServer",
"NoActionsCapabilitiesServer",
];
let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
"tsx",
true,
FakeLspAdapter {
name: &language_server_names[0],
capabilities: lsp::ServerCapabilities {
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _a = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[1],
capabilities: lsp::ServerCapabilities {
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _b = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[2],
capabilities: lsp::ServerCapabilities {
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let _c = language_registry.register_specific_fake_lsp_adapter(
"tsx",
false,
FakeLspAdapter {
name: &language_server_names[3],
capabilities: lsp::ServerCapabilities {
code_action_provider: None,
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
.await
.unwrap();
cx.executor().run_until_parked();
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_name = new_server.server.name();
assert!(
!servers_with_actions_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() {
"TailwindServer" | "TypeScriptServer" => {
servers_with_actions_requests.insert(
new_server_name.clone(),
new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
move |_, _| {
let name = new_server_name.clone();
async move {
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
lsp::CodeAction {
title: format!("{name} code action"),
..lsp::CodeAction::default()
},
)]))
}
},
),
);
}
"ESLintServer" => {
servers_with_actions_requests.insert(
new_server_name,
new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
|_, _| async move { Ok(None) },
),
);
}
"NoActionsCapabilitiesServer" => {
let _never_handled = new_server
.handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
panic!(
"Should not call for code actions server with no corresponding capabilities"
)
});
}
unexpected => panic!("Unexpected server name: {unexpected}"),
}
}
let code_actions_task = project.update(cx, |project, cx| {
project.code_actions(&buffer, 0..buffer.read(cx).len(), cx)
});
let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
|mut code_actions_request| async move {
code_actions_request
.next()
.await
.expect("All code actions requests should have been triggered")
},
))
.await;
assert_eq!(
vec!["TailwindServer code action", "TypeScriptServer code action"],
code_actions_task
.await
.into_iter()
.map(|code_action| code_action.lsp_action.title)
.sorted()
.collect::<Vec<_>>(),
"Should receive code actions responses from all related servers with hover capabilities"
);
}
async fn search(
project: &Model<Project>,
query: SearchQuery,
@ -4508,3 +4813,17 @@ fn typescript_lang() -> Arc<Language> {
Some(tree_sitter_typescript::language_typescript()),
))
}
fn tsx_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "tsx".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["tsx".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_typescript::language_tsx()),
))
}