Detailed changes
@@ -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,
@@ -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)
@@ -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![],
@@ -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();
@@ -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());
@@ -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,
@@ -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![],
@@ -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)]
@@ -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)
@@ -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()),
+ ))
+}