mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-28 01:24:17 +00:00
Fix remote server completions not being queried from all LSP servers (#42723)
Closes #41294 Release Notes: - Fixed remote LSPs not being queried
This commit is contained in:
parent
faa1136651
commit
fd1494c31a
5 changed files with 303 additions and 31 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue