languages: Fix rust completions not having proper detail labels (#35772)

rust-analyzer changed the format here a bit some months ago which
partially broke our nice detailed highlighted completion labels. The
brings that back while also cleaning up the code a bit.

Also fixes a bug where disabling rust-analyzers snippet callable
completions would fully break them.

Release Notes:

- N/A
This commit is contained in:
Lukas Wirth 2025-08-07 12:38:58 +02:00 committed by GitHub
parent 5b1b3c51d4
commit f5f837d39a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -305,66 +305,63 @@ impl LspAdapter for RustLspAdapter {
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
language: &Arc<Language>, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let detail = completion // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered
// this usually contains signatures of the thing to be completed
let detail_right = completion
.label_details .label_details
.as_ref() .as_ref()
.and_then(|detail| detail.detail.as_ref()) .and_then(|detail| detail.description.as_ref())
.or(completion.detail.as_ref()) .or(completion.detail.as_ref())
.map(|detail| detail.trim()); .map(|detail| detail.trim());
let function_signature = completion // this tends to contain alias and import information
let detail_left = completion
.label_details .label_details
.as_ref() .as_ref()
.and_then(|detail| detail.description.as_deref()) .and_then(|detail| detail.detail.as_deref());
.or(completion.detail.as_deref()); let mk_label = |text: String, runs| {
match (detail, completion.kind) { let filter_range = completion
(Some(detail), Some(lsp::CompletionItemKind::FIELD)) => { .filter_text
.as_deref()
.and_then(|filter| {
completion
.label
.find(filter)
.map(|ix| ix..ix + filter.len())
})
.unwrap_or(0..completion.label.len());
CodeLabel {
text,
runs,
filter_range,
}
};
let mut label = match (detail_right, completion.kind) {
(Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
let name = &completion.label; let name = &completion.label;
let text = format!("{name}: {detail}"); let text = format!("{name}: {signature}");
let prefix = "struct S { "; let prefix = "struct S { ";
let source = Rope::from(format!("{prefix}{text} }}")); let source = Rope::from(format!("{prefix}{text} }}"));
let runs = let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
let filter_range = completion mk_label(text, runs)
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..name.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
} }
( (
Some(detail), Some(signature),
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE), Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE),
) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => { ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => {
let name = &completion.label; let name = &completion.label;
let text = format!( let text = format!("{name}: {signature}",);
"{}: {}",
name,
completion.detail.as_deref().unwrap_or(detail)
);
let prefix = "let "; let prefix = "let ";
let source = Rope::from(format!("{prefix}{text} = ();")); let source = Rope::from(format!("{prefix}{text} = ();"));
let runs = let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
let filter_range = completion mk_label(text, runs)
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..name.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
} }
( (
Some(detail), function_signature,
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD), Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD),
) => { ) => {
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
const FUNCTION_PREFIXES: [&str; 6] = [ const FUNCTION_PREFIXES: [&str; 6] = [
"async fn", "async fn",
"async unsafe fn", "async unsafe fn",
@ -373,34 +370,27 @@ impl LspAdapter for RustLspAdapter {
"unsafe fn", "unsafe fn",
"fn", "fn",
]; ];
// Is it function `async`? let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| {
let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| { function_signature?
function_signature.as_ref().and_then(|signature| { .strip_prefix(prefix)
signature .map(|suffix| (prefix, suffix))
.strip_prefix(*prefix)
.map(|suffix| (*prefix, suffix))
})
}); });
// fn keyword should be followed by opening parenthesis. // fn keyword should be followed by opening parenthesis.
if let Some((prefix, suffix)) = fn_keyword { if let Some((prefix, suffix)) = fn_prefixed {
let mut text = REGEX.replace(&completion.label, suffix).to_string(); let label = if let Some(label) = completion
.label
.strip_suffix("(…)")
.or_else(|| completion.label.strip_suffix("()"))
{
label
} else {
&completion.label
};
let text = format!("{label}{suffix}");
let source = Rope::from(format!("{prefix} {text} {{}}")); let source = Rope::from(format!("{prefix} {text} {{}}"));
let run_start = prefix.len() + 1; let run_start = prefix.len() + 1;
let runs = language.highlight_text(&source, run_start..run_start + text.len()); let runs = language.highlight_text(&source, run_start..run_start + text.len());
if detail.starts_with("(") { mk_label(text, runs)
text.push(' ');
text.push_str(&detail);
}
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..completion.label.find('(').unwrap_or(text.len()));
return Some(CodeLabel {
filter_range,
text,
runs,
});
} else if completion } else if completion
.detail .detail
.as_ref() .as_ref()
@ -410,20 +400,13 @@ impl LspAdapter for RustLspAdapter {
let len = text.len(); let len = text.len();
let source = Rope::from(text.as_str()); let source = Rope::from(text.as_str());
let runs = language.highlight_text(&source, 0..len); let runs = language.highlight_text(&source, 0..len);
let filter_range = completion mk_label(text, runs)
.filter_text } else {
.as_deref() mk_label(completion.label.clone(), vec![])
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
return Some(CodeLabel {
filter_range,
text,
runs,
});
} }
} }
(_, Some(kind)) => { (_, kind) => {
let highlight_name = match kind { let highlight_name = kind.and_then(|kind| match kind {
lsp::CompletionItemKind::STRUCT lsp::CompletionItemKind::STRUCT
| lsp::CompletionItemKind::INTERFACE | lsp::CompletionItemKind::INTERFACE
| lsp::CompletionItemKind::ENUM => Some("type"), | lsp::CompletionItemKind::ENUM => Some("type"),
@ -433,27 +416,32 @@ impl LspAdapter for RustLspAdapter {
Some("constant") Some("constant")
} }
_ => None, _ => None,
}; });
let mut label = completion.label.clone(); let label = completion.label.clone();
if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) { let mut runs = vec![];
label.push(' ');
label.push_str(detail);
}
let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
if let Some(highlight_name) = highlight_name { if let Some(highlight_name) = highlight_name {
let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?; let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
label.runs.push(( runs.push((
0..label.text.rfind('(').unwrap_or(completion.label.len()), 0..label.rfind('(').unwrap_or(completion.label.len()),
highlight_id, highlight_id,
)); ));
} }
mk_label(label, runs)
return Some(label); }
};
if let Some(detail_left) = detail_left {
label.text.push(' ');
if !detail_left.starts_with('(') {
label.text.push('(');
}
label.text.push_str(detail_left);
if !detail_left.ends_with(')') {
label.text.push(')');
} }
_ => {}
} }
None Some(label)
} }
async fn label_for_symbol( async fn label_for_symbol(
@ -1169,7 +1157,7 @@ mod tests {
.await, .await,
Some(CodeLabel { Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(), text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5, filter_range: 0..10,
runs: vec![ runs: vec![
(0..5, highlight_function), (0..5, highlight_function),
(7..10, highlight_keyword), (7..10, highlight_keyword),
@ -1187,7 +1175,7 @@ mod tests {
kind: Some(lsp::CompletionItemKind::FUNCTION), kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(), label: "hello(…)".to_string(),
label_details: Some(CompletionItemLabelDetails { label_details: Some(CompletionItemLabelDetails {
detail: Some(" (use crate::foo)".into()), detail: Some("(use crate::foo)".into()),
description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()), description: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
}), }),
..Default::default() ..Default::default()
@ -1197,7 +1185,7 @@ mod tests {
.await, .await,
Some(CodeLabel { Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(), text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5, filter_range: 0..10,
runs: vec![ runs: vec![
(0..5, highlight_function), (0..5, highlight_function),
(7..10, highlight_keyword), (7..10, highlight_keyword),
@ -1234,7 +1222,7 @@ mod tests {
kind: Some(lsp::CompletionItemKind::FUNCTION), kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(), label: "hello(…)".to_string(),
label_details: Some(CompletionItemLabelDetails { label_details: Some(CompletionItemLabelDetails {
detail: Some(" (use crate::foo)".to_string()), detail: Some("(use crate::foo)".to_string()),
description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()), description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
}), }),
@ -1243,6 +1231,35 @@ mod tests {
&language &language
) )
.await, .await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..10,
runs: vec![
(0..5, highlight_function),
(7..10, highlight_keyword),
(11..17, highlight_type),
(18..19, highlight_type),
(25..28, highlight_type),
(29..30, highlight_type),
],
})
);
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello".to_string(),
label_details: Some(CompletionItemLabelDetails {
detail: Some("(use crate::foo)".to_string()),
description: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
}),
..Default::default()
},
&language
)
.await,
Some(CodeLabel { Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(), text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5, filter_range: 0..5,
@ -1274,9 +1291,14 @@ mod tests {
) )
.await, .await,
Some(CodeLabel { Some(CodeLabel {
text: "await.as_deref_mut()".to_string(), text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
filter_range: 6..18, filter_range: 6..18,
runs: vec![], runs: vec![
(6..18, HighlightId(2)),
(20..23, HighlightId(1)),
(33..40, HighlightId(0)),
(45..46, HighlightId(0))
],
}) })
); );