Use more LSP data when falling back to regular completions label (#23909)

Closes https://github.com/zed-industries/zed/issues/23590
Closes https://x.com/steeve/status/1865129235536568555

Before:

<img width="773" alt="before"
src="https://github.com/user-attachments/assets/129a8d12-9298-4bf5-8f2d-b3292c2562bf"
/>


After:

<img width="768" alt="after"
src="https://github.com/user-attachments/assets/e0516fb3-b02a-48be-8923-63bba05fdb69"
/>


The list obviously needs some solution for the cut-off part of the
completion label, but this is the reality for all extensions'
completions too, so one step at a time.


Release Notes:

- Improved default completion label fallback
This commit is contained in:
Kirill Bulatov 2025-01-30 17:05:34 +02:00 committed by GitHub
parent 48dba9a9d9
commit 9e4555797d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 12 deletions

View file

@ -11282,7 +11282,7 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
let item1 = lsp::CompletionItem { let item1 = lsp::CompletionItem {
label: "id".to_string(), label: "method id()".to_string(),
filter_text: Some("id".to_string()), filter_text: Some("id".to_string()),
detail: None, detail: None,
documentation: None, documentation: None,
@ -11332,7 +11332,7 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
.iter() .iter()
.map(|completion| &completion.label.text) .map(|completion| &completion.label.text)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec!["id", "other"] vec!["method id()", "other"]
) )
} }
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"), CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
@ -11387,7 +11387,7 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
.iter() .iter()
.map(|completion| &completion.label.text) .map(|completion| &completion.label.text)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec!["method id()", "other"], vec!["method id() Now resolved!", "other"],
"Should update first completion label, but not second as the filter text did not match." "Should update first completion label, but not second as the filter text did not match."
); );
} }

View file

@ -1750,6 +1750,58 @@ impl Grammar {
} }
impl CodeLabel { impl CodeLabel {
pub fn fallback_for_completion(
item: &lsp::CompletionItem,
language: Option<&Language>,
) -> Self {
let highlight_id = item.kind.and_then(|kind| {
let grammar = language?.grammar()?;
use lsp::CompletionItemKind as Kind;
match kind {
Kind::CLASS => grammar.highlight_id_for_name("type"),
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("constructor"),
Kind::ENUM => grammar
.highlight_id_for_name("enum")
.or_else(|| grammar.highlight_id_for_name("type")),
Kind::FIELD => grammar.highlight_id_for_name("property"),
Kind::FUNCTION => grammar.highlight_id_for_name("function"),
Kind::INTERFACE => grammar.highlight_id_for_name("type"),
Kind::METHOD => grammar
.highlight_id_for_name("function.method")
.or_else(|| grammar.highlight_id_for_name("function")),
Kind::OPERATOR => grammar.highlight_id_for_name("operator"),
Kind::PROPERTY => grammar.highlight_id_for_name("property"),
Kind::STRUCT => grammar.highlight_id_for_name("type"),
Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
_ => None,
}
});
let label = &item.label;
let label_length = label.len();
let runs = highlight_id
.map(|highlight_id| vec![(0..label_length, highlight_id)])
.unwrap_or_default();
let text = if let Some(detail) = &item.detail {
format!("{label} {detail}")
} else if let Some(description) = item
.label_details
.as_ref()
.and_then(|label_details| label_details.description.as_ref())
{
format!("{label} {description}")
} else {
label.clone()
};
Self {
text,
runs,
filter_range: 0..label_length,
}
}
pub fn plain(text: String, filter_text: Option<&str>) -> Self { pub fn plain(text: String, filter_text: Option<&str>) -> Self {
let mut result = Self { let mut result = Self {
runs: Vec::new(), runs: Vec::new(),

View file

@ -4380,7 +4380,8 @@ impl LspStore {
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
// So we have to update the label here anyway... // So we have to update the label here anyway...
let mut new_label = match snapshot.language() { let language = snapshot.language();
let mut new_label = match language {
Some(language) => { Some(language) => {
adapter adapter
.labels_for_completions(&[completion_item.clone()], language) .labels_for_completions(&[completion_item.clone()], language)
@ -4391,9 +4392,9 @@ impl LspStore {
.pop() .pop()
.flatten() .flatten()
.unwrap_or_else(|| { .unwrap_or_else(|| {
CodeLabel::plain( CodeLabel::fallback_for_completion(
completion_item.label, &completion_item,
completion_item.filter_text.as_deref(), language.map(|language| language.as_ref()),
) )
}); });
ensure_uniform_list_compatible_label(&mut new_label); ensure_uniform_list_compatible_label(&mut new_label);
@ -8079,10 +8080,7 @@ async fn populate_labels_for_completions(
}; };
let mut label = label.unwrap_or_else(|| { let mut label = label.unwrap_or_else(|| {
CodeLabel::plain( CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref())
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)
}); });
ensure_uniform_list_compatible_label(&mut label); ensure_uniform_list_compatible_label(&mut label);
@ -8883,7 +8881,7 @@ fn include_text(server: &lsp::LanguageServer) -> Option<bool> {
/// Completion items are displayed in a `UniformList`. /// Completion items are displayed in a `UniformList`.
/// Usually, those items are single-line strings, but in LSP responses, /// Usually, those items are single-line strings, but in LSP responses,
/// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces. /// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces.
/// Many language plugins construct these items by joining these parts together, and we may fall back to `CodeLabel::plain` that uses `label`. /// Many language plugins construct these items by joining these parts together, and we may use `CodeLabel::fallback_for_completion` that uses `label` at least.
/// All that may lead to a newline being inserted into resulting `CodeLabel.text`, which will force `UniformList` to bloat each entry to occupy more space, /// All that may lead to a newline being inserted into resulting `CodeLabel.text`, which will force `UniformList` to bloat each entry to occupy more space,
/// breaking the completions menu presentation. /// breaking the completions menu presentation.
/// ///