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

Lukas Wirth created

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

Change summary

crates/languages/src/rust.rs | 200 +++++++++++++++++++++----------------
1 file changed, 111 insertions(+), 89 deletions(-)

Detailed changes

crates/languages/src/rust.rs 🔗

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