Detailed changes
@@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
"});
}
-#[gpui::test(iterations = 10)]
+#[gpui::test]
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -307,17 +307,35 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
- let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+ let mut fake_language_servers = [
+ client_a.language_registry().register_fake_lsp(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: capabilities.clone(),
+ ..FakeLspAdapter::default()
+ },
+ ),
+ client_a.language_registry().register_fake_lsp(
+ "Rust",
+ FakeLspAdapter {
+ name: "fake-analyzer",
+ capabilities: capabilities.clone(),
+ ..FakeLspAdapter::default()
+ },
+ ),
+ ];
+ client_b.language_registry().add(rust_lang());
+ client_b.language_registry().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
..FakeLspAdapter::default()
},
);
- client_b.language_registry().add(rust_lang());
client_b.language_registry().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
+ name: "fake-analyzer",
capabilities,
..FakeLspAdapter::default()
},
@@ -352,7 +370,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
});
- let fake_language_server = fake_language_servers.next().await.unwrap();
+ let fake_language_server = fake_language_servers[0].next().await.unwrap();
+ let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
cx_a.background_executor.run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
@@ -414,6 +433,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.next()
.await
.unwrap();
+ second_fake_language_server
+ .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) })
+ .next()
+ .await
+ .unwrap();
cx_a.executor().finish_waiting();
// Open the buffer on the host.
@@ -522,6 +546,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
])))
});
+ // Second language server also needs to handle the request (returns None)
+ let mut second_completion_response = second_fake_language_server
+ .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
+
// The completion now gets a new `text_edit.new_text` when resolving the completion item
let mut resolve_completion_response = fake_language_server
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
@@ -545,6 +573,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
cx_b.executor().run_until_parked();
completion_response.next().await.unwrap();
+ second_completion_response.next().await.unwrap();
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
@@ -563,6 +592,77 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
);
});
+
+ // Ensure buffer is synced before proceeding with the next test
+ cx_a.executor().run_until_parked();
+ cx_b.executor().run_until_parked();
+
+ // Test completions from the second fake language server
+ // Add another completion trigger to test the second language server
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([68..68])
+ });
+ editor.handle_input("; b", window, cx);
+ editor.handle_input(".", window, cx);
+ });
+
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(
+ buffer.text(),
+ "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
+ );
+ });
+
+ // Set up completion handlers for both language servers
+ let mut first_lsp_completion = fake_language_server
+ .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
+
+ let mut second_lsp_completion = second_fake_language_server
+ .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(1, 54),
+ );
+
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ label: "analyzer_method(…)".into(),
+ detail: Some("fn(&self) -> Result<T>".into()),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ new_text: "analyzer_method()".to_string(),
+ range: lsp::Range::new(
+ lsp::Position::new(1, 54),
+ lsp::Position::new(1, 54),
+ ),
+ })),
+ insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+ ..Default::default()
+ },
+ ])))
+ });
+
+ cx_b.executor().run_until_parked();
+
+ // Await both language server responses
+ first_lsp_completion.next().await.unwrap();
+ second_lsp_completion.next().await.unwrap();
+
+ cx_b.executor().run_until_parked();
+
+ // Confirm the completion from the second language server works
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ assert!(editor.context_menu_visible());
+ editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
+ assert_eq!(
+ editor.text(cx),
+ "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
+ );
+ });
}
#[gpui::test(iterations = 10)]
@@ -218,6 +218,7 @@ pub(crate) struct GetHover {
pub(crate) struct GetCompletions {
pub position: PointUtf16,
pub context: CompletionContext,
+ pub server_id: Option<lsp::LanguageServerId>,
}
#[derive(Clone, Debug)]
@@ -2395,6 +2396,7 @@ impl LspCommand for GetCompletions {
buffer_id: buffer.remote_id().into(),
position: Some(language::proto::serialize_anchor(&anchor)),
version: serialize_version(&buffer.version()),
+ server_id: self.server_id.map(|id| id.to_proto()),
}
}
@@ -2423,6 +2425,9 @@ impl LspCommand for GetCompletions {
trigger_kind: CompletionTriggerKind::INVOKED,
trigger_character: None,
},
+ server_id: message
+ .server_id
+ .map(|id| lsp::LanguageServerId::from_proto(id)),
})
}
@@ -3754,7 +3754,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers);
client.add_entity_request_handler(Self::handle_rename_project_entry);
client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics);
- client.add_entity_request_handler(Self::handle_lsp_command::<GetCompletions>);
+ client.add_entity_request_handler(Self::handle_lsp_get_completions);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentSymbols>);
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
@@ -4463,6 +4463,41 @@ impl LspStore {
.any(check)
}
+ fn all_capable_for_proto_request<F>(
+ &self,
+ buffer: &Entity<Buffer>,
+ mut check: F,
+ cx: &App,
+ ) -> Vec<lsp::LanguageServerId>
+ where
+ F: FnMut(&lsp::LanguageServerName, &lsp::ServerCapabilities) -> bool,
+ {
+ let Some(language) = buffer.read(cx).language().cloned() else {
+ return Vec::default();
+ };
+ let relevant_language_servers = self
+ .languages
+ .lsp_adapters(&language.name())
+ .into_iter()
+ .map(|lsp_adapter| lsp_adapter.name())
+ .collect::<HashSet<_>>();
+ self.language_server_statuses
+ .iter()
+ .filter_map(|(server_id, server_status)| {
+ relevant_language_servers
+ .contains(&server_status.name)
+ .then_some((server_id, &server_status.name))
+ })
+ .filter_map(|(server_id, server_name)| {
+ self.lsp_server_capabilities
+ .get(server_id)
+ .map(|c| (server_id, server_name, c))
+ })
+ .filter(|(_, server_name, capabilities)| check(server_name, capabilities))
+ .map(|(server_id, _, _)| *server_id)
+ .collect()
+ }
+
pub fn request_lsp<R>(
&mut self,
buffer: Entity<Buffer>,
@@ -5902,17 +5937,24 @@ impl LspStore {
let language_registry = self.languages.clone();
if let Some((upstream_client, project_id)) = self.upstream_client() {
- let request = GetCompletions { position, context };
- if !self.is_capable_for_proto_request(buffer, &request, cx) {
- return Task::ready(Ok(Vec::new()));
- }
- let task = self.send_lsp_proto_request(
- buffer.clone(),
- upstream_client,
- project_id,
- request,
+ let snapshot = buffer.read(cx).snapshot();
+ let offset = position.to_offset(&snapshot);
+ let scope = snapshot.language_scope_at(offset);
+ let capable_lsps = self.all_capable_for_proto_request(
+ buffer,
+ |server_name, capabilities| {
+ capabilities.completion_provider.is_some()
+ && scope
+ .as_ref()
+ .map(|scope| scope.language_allowed(server_name))
+ .unwrap_or(true)
+ },
cx,
);
+ if capable_lsps.is_empty() {
+ return Task::ready(Ok(Vec::new()));
+ }
+
let language = buffer.read(cx).language().cloned();
// In the future, we should provide project guests with the names of LSP adapters,
@@ -5925,19 +5967,53 @@ impl LspStore {
.cloned()
});
- cx.foreground_executor().spawn(async move {
- let completion_response = task.await?;
- let completions = populate_labels_for_completions(
- completion_response.completions,
- language,
- lsp_adapter,
- )
- .await;
- Ok(vec![CompletionResponse {
- completions,
- display_options: CompletionDisplayOptions::default(),
- is_incomplete: completion_response.is_incomplete,
- }])
+ let buffer = buffer.clone();
+
+ cx.spawn(async move |this, cx| {
+ let requests = join_all(
+ capable_lsps
+ .into_iter()
+ .map(|id| {
+ let request = GetCompletions {
+ position,
+ context: context.clone(),
+ server_id: Some(id),
+ };
+ let buffer = buffer.clone();
+ let language = language.clone();
+ let lsp_adapter = lsp_adapter.clone();
+ let upstream_client = upstream_client.clone();
+ let response = this
+ .update(cx, |this, cx| {
+ this.send_lsp_proto_request(
+ buffer,
+ upstream_client,
+ project_id,
+ request,
+ cx,
+ )
+ })
+ .log_err();
+ async move {
+ let response = response?.await.log_err()?;
+
+ let completions = populate_labels_for_completions(
+ response.completions,
+ language,
+ lsp_adapter,
+ )
+ .await;
+
+ Some(CompletionResponse {
+ completions,
+ display_options: CompletionDisplayOptions::default(),
+ is_incomplete: response.is_incomplete,
+ })
+ }
+ })
+ .collect::<Vec<_>>(),
+ );
+ Ok(requests.await.into_iter().flatten().collect::<Vec<_>>())
})
} else if let Some(local) = self.as_local() {
let snapshot = buffer.read(cx).snapshot();
@@ -5998,6 +6074,7 @@ impl LspStore {
GetCompletions {
position,
context: context.clone(),
+ server_id: Some(server_id),
},
cx,
).fuse();
@@ -8461,6 +8538,46 @@ impl LspStore {
})
}
+ async fn handle_lsp_get_completions(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::GetCompletions>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GetCompletionsResponse> {
+ let sender_id = envelope.original_sender_id().unwrap_or_default();
+
+ let buffer_id = GetCompletions::buffer_id_from_proto(&envelope.payload)?;
+ let buffer_handle = this.update(&mut cx, |this, cx| {
+ this.buffer_store.read(cx).get_existing(buffer_id)
+ })??;
+ let request = GetCompletions::from_proto(
+ envelope.payload,
+ this.clone(),
+ buffer_handle.clone(),
+ cx.clone(),
+ )
+ .await?;
+
+ let server_to_query = match request.server_id {
+ Some(server_id) => LanguageServerToQuery::Other(server_id),
+ None => LanguageServerToQuery::FirstCapable,
+ };
+
+ let response = this
+ .update(&mut cx, |this, cx| {
+ this.request_lsp(buffer_handle.clone(), server_to_query, request, cx)
+ })?
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ Ok(GetCompletions::response_to_proto(
+ response,
+ this,
+ sender_id,
+ &buffer_handle.read(cx).version(),
+ cx,
+ ))
+ })?
+ }
+
async fn handle_lsp_command<T: LspCommand>(
this: Entity<Self>,
envelope: TypedEnvelope<T::ProtoRequest>,
@@ -703,6 +703,7 @@ message GetCompletions {
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
+ optional uint64 server_id = 5;
}
message CancelLanguageServerWork {
@@ -398,12 +398,17 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
json!({
"settings.json": r#"
{
- "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
+ "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
+ },
+ "fake-analyzer": {
+ "binary": {
+ "path": "~/.cargo/bin/rust-analyzer"
+ }
}
}
}"#
@@ -431,6 +436,18 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
},
..FakeLspAdapter::default()
},
+ );
+ project.languages().register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "fake-analyzer",
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions::default()),
+ rename_provider: Some(lsp::OneOf::Left(true)),
+ ..lsp::ServerCapabilities::default()
+ },
+ ..FakeLspAdapter::default()
+ },
)
});
@@ -446,6 +463,30 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
)
});
+ let mut fake_second_lsp = server_cx.update(|cx| {
+ headless.read(cx).languages.register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "fake-analyzer",
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions::default()),
+ rename_provider: Some(lsp::OneOf::Left(true)),
+ ..lsp::ServerCapabilities::default()
+ },
+ ..FakeLspAdapter::default()
+ },
+ );
+ headless.read(cx).languages.register_fake_language_server(
+ LanguageServerName("fake-analyzer".into()),
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions::default()),
+ rename_provider: Some(lsp::OneOf::Left(true)),
+ ..lsp::ServerCapabilities::default()
+ },
+ None,
+ )
+ });
+
cx.run_until_parked();
let worktree_id = project
@@ -469,12 +510,13 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
cx.run_until_parked();
let fake_lsp = fake_lsp.next().await.unwrap();
+ let fake_second_lsp = fake_second_lsp.next().await.unwrap();
cx.read(|cx| {
let file = buffer.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
- ["rust-analyzer".to_string()]
+ ["rust-analyzer".to_string(), "fake-analyzer".to_string()]
)
});
@@ -497,7 +539,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
server_cx.read(|cx| {
let lsp_store = headless.read(cx).lsp_store.read(cx);
- assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
+ assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
});
fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
@@ -507,6 +549,13 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
}])))
});
+ fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
+ Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
+ label: "beep".to_string(),
+ ..Default::default()
+ }])))
+ });
+
let result = project
.update(cx, |project, cx| {
project.completions(
@@ -528,7 +577,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
.flat_map(|response| response.completions)
.map(|c| c.label.text)
.collect::<Vec<_>>(),
- vec!["boop".to_string()]
+ vec!["boop".to_string(), "beep".to_string()]
);
fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {