Auto merge of #15956 - ahlinc:label-detail, r=lnicola

Improve completion label details display

This PR improves completion label details display by separating trait and alias info from the `label` LSP field to an additional existing `label_detail` field. Changes look like the following:

### Before

![Screenshot from 2023-11-24 12-03-08](https://github.com/rust-lang/rust-analyzer/assets/14666676/74066f0d-f1ac-4e99-8be5-c5141d513d23)

### After

![Screenshot from 2023-11-24 12-21-57](https://github.com/rust-lang/rust-analyzer/assets/14666676/45fca112-4612-40a3-81b9-07ff12de0962)

_All existing tests are passed without any changes in test themselves logic._
This commit is contained in:
bors 2023-11-24 12:54:33 +00:00
commit 8733728323
4 changed files with 39 additions and 9 deletions

View file

@ -26,6 +26,10 @@ use crate::{
pub struct CompletionItem { pub struct CompletionItem {
/// Label in the completion pop up which identifies completion. /// Label in the completion pop up which identifies completion.
pub label: SmolStr, pub label: SmolStr,
/// Addition label details in the completion pop up that are
/// displayed and aligned on the right side after the label.
pub label_detail: Option<SmolStr>,
/// Range of identifier that is being completed. /// Range of identifier that is being completed.
/// ///
/// It should be used primarily for UI, but we also use this to convert /// It should be used primarily for UI, but we also use this to convert
@ -425,13 +429,14 @@ impl Builder {
pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem { pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem {
let _p = profile::span("item::Builder::build"); let _p = profile::span("item::Builder::build");
let mut label = self.label; let label = self.label;
let mut label_detail = None;
let mut lookup = self.lookup.unwrap_or_else(|| label.clone()); let mut lookup = self.lookup.unwrap_or_else(|| label.clone());
let insert_text = self.insert_text.unwrap_or_else(|| label.to_string()); let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
if !self.doc_aliases.is_empty() { if !self.doc_aliases.is_empty() {
let doc_aliases = self.doc_aliases.iter().join(", "); let doc_aliases = self.doc_aliases.iter().join(", ");
label = SmolStr::from(format!("{label} (alias {doc_aliases})")); label_detail.replace(SmolStr::from(format!(" (alias {doc_aliases})")));
let lookup_doc_aliases = self let lookup_doc_aliases = self
.doc_aliases .doc_aliases
.iter() .iter()
@ -454,10 +459,17 @@ impl Builder {
if let [import_edit] = &*self.imports_to_add { if let [import_edit] = &*self.imports_to_add {
// snippets can have multiple imports, but normal completions only have up to one // snippets can have multiple imports, but normal completions only have up to one
if let Some(original_path) = import_edit.original_path.as_ref() { if let Some(original_path) = import_edit.original_path.as_ref() {
label = SmolStr::from(format!("{label} (use {})", original_path.display(db))); label_detail.replace(SmolStr::from(format!(
"{} (use {})",
label_detail.as_deref().unwrap_or_default(),
original_path.display(db)
)));
} }
} else if let Some(trait_name) = self.trait_name { } else if let Some(trait_name) = self.trait_name {
label = SmolStr::from(format!("{label} (as {trait_name})")); label_detail.replace(SmolStr::from(format!(
"{} (as {trait_name})",
label_detail.as_deref().unwrap_or_default(),
)));
} }
let text_edit = match self.text_edit { let text_edit = match self.text_edit {
@ -479,6 +491,7 @@ impl Builder {
CompletionItem { CompletionItem {
source_range: self.source_range, source_range: self.source_range,
label, label,
label_detail,
text_edit, text_edit,
is_snippet: self.is_snippet, is_snippet: self.is_snippet,
detail: self.detail, detail: self.detail,

View file

@ -557,7 +557,11 @@ mod tests {
let tag = it.kind.tag(); let tag = it.kind.tag();
let relevance = display_relevance(it.relevance); let relevance = display_relevance(it.relevance);
items.push(format!("{tag} {} {relevance}\n", it.label)); items.push(format!(
"{tag} {}{} {relevance}\n",
it.label,
it.label_detail.clone().unwrap_or_default(),
));
if let Some((label, _indel, relevance)) = it.ref_match() { if let Some((label, _indel, relevance)) = it.ref_match() {
let relevance = display_relevance(relevance); let relevance = display_relevance(relevance);

View file

@ -150,16 +150,29 @@ fn render_completion_list(completions: Vec<CompletionItem>) -> String {
fn monospace_width(s: &str) -> usize { fn monospace_width(s: &str) -> usize {
s.chars().count() s.chars().count()
} }
let label_width = let label_width = completions
completions.iter().map(|it| monospace_width(&it.label)).max().unwrap_or_default().min(22); .iter()
.map(|it| {
monospace_width(&it.label)
+ monospace_width(it.label_detail.as_deref().unwrap_or_default())
})
.max()
.unwrap_or_default()
.min(22);
completions completions
.into_iter() .into_iter()
.map(|it| { .map(|it| {
let tag = it.kind.tag(); let tag = it.kind.tag();
let var_name = format!("{tag} {}", it.label); let var_name = format!("{tag} {}", it.label);
let mut buf = var_name; let mut buf = var_name;
if let Some(ref label_detail) = it.label_detail {
format_to!(buf, "{label_detail}");
}
if let Some(detail) = it.detail { if let Some(detail) = it.detail {
let width = label_width.saturating_sub(monospace_width(&it.label)); let width = label_width.saturating_sub(
monospace_width(&it.label)
+ monospace_width(&it.label_detail.unwrap_or_default()),
);
format_to!(buf, "{:width$} {}", "", detail, width = width); format_to!(buf, "{:width$} {}", "", detail, width = width);
} }
if it.deprecated { if it.deprecated {

View file

@ -301,7 +301,7 @@ fn completion_item(
if config.completion_label_details_support() { if config.completion_label_details_support() {
lsp_item.label_details = Some(lsp_types::CompletionItemLabelDetails { lsp_item.label_details = Some(lsp_types::CompletionItemLabelDetails {
detail: None, detail: item.label_detail.as_ref().map(ToString::to_string),
description: lsp_item.detail.clone(), description: lsp_item.detail.clone(),
}); });
} }