editor: Utilize `filter_text` from language server for `filter_range` (#33155)

Smit Barmase created

Closes #33106

Instead of directly using `filter_text` in `StringMatchCandidate`, which
yields better results for fuzzy matching but messes up with highlighting
letters in bold, as `positions` list generated by fuzzy crate are now of
`filter_text` and not `label` shown to the user.

This PR fixes it by keeping use of `filter_range` in
`StringMatchCandidate`, which is range w.r.t to `label` shown to user.
And actually generating this `filter_range` at source by using
`filter_range` if exists.

- [x] Tests

Release Notes:

- Fixed issue where incorrect letters are marked as bold in completions.

Change summary

crates/editor/src/code_completion_tests.rs |  8 -
crates/editor/src/code_context_menus.rs    |  4 
crates/language/src/language.rs            | 20 +++--
crates/languages/src/c.rs                  | 52 +++++++++++---
crates/languages/src/go.rs                 | 60 +++++++++++++++-
crates/languages/src/python.rs             | 14 +++
crates/languages/src/rust.rs               | 83 +++++++++++++++++++++--
crates/languages/src/typescript.rs         |  8 +
crates/languages/src/vtsls.rs              |  8 +
crates/project/src/project.rs              | 10 --
10 files changed, 206 insertions(+), 61 deletions(-)

Detailed changes

crates/editor/src/code_completion_tests.rs 🔗

@@ -264,11 +264,7 @@ impl CompletionBuilder {
         Completion {
             replace_range: Anchor::MIN..Anchor::MAX,
             new_text: label.to_string(),
-            label: CodeLabel {
-                text: label.to_string(),
-                runs: Default::default(),
-                filter_range: 0..label.len(),
-            },
+            label: CodeLabel::plain(label.to_string(), filter_text),
             documentation: None,
             source: CompletionSource::Lsp {
                 insert_range: None,
@@ -299,7 +295,7 @@ async fn filter_and_sort_matches(
     let candidates: Arc<[StringMatchCandidate]> = completions
         .iter()
         .enumerate()
-        .map(|(id, completion)| StringMatchCandidate::new(id, &completion.filter_text()))
+        .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
         .collect();
     let cancel_flag = Arc::new(AtomicBool::new(false));
     let background_executor = cx.executor();

crates/editor/src/code_context_menus.rs 🔗

@@ -260,7 +260,7 @@ impl CompletionsMenu {
         let match_candidates = completions
             .iter()
             .enumerate()
-            .map(|(id, completion)| StringMatchCandidate::new(id, completion.filter_text()))
+            .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
             .collect();
 
         let completions_menu = Self {
@@ -1115,7 +1115,7 @@ impl CompletionsMenu {
                     SnippetSortOrder::Inline => Reverse(0),
                 };
                 let sort_positions = string_match.positions.clone();
-                let sort_exact = Reverse(if Some(completion.filter_text()) == query {
+                let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
                     1
                 } else {
                     0

crates/language/src/language.rs 🔗

@@ -1982,25 +1982,27 @@ impl CodeLabel {
         } else {
             label.clone()
         };
+        let filter_range = item
+            .filter_text
+            .as_deref()
+            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..label_length);
         Self {
             text,
             runs,
-            filter_range: 0..label_length,
+            filter_range,
         }
     }
 
     pub fn plain(text: String, filter_text: Option<&str>) -> Self {
-        let mut result = Self {
+        let filter_range = filter_text
+            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..text.len());
+        Self {
             runs: Vec::new(),
-            filter_range: 0..text.len(),
+            filter_range,
             text,
-        };
-        if let Some(filter_text) = filter_text {
-            if let Some(ix) = result.text.find(filter_text) {
-                result.filter_range = ix..ix + filter_text.len();
-            }
         }
-        result
     }
 
     pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {

crates/languages/src/c.rs 🔗

@@ -131,8 +131,16 @@ impl super::LspAdapter for CLspAdapter {
                 let text = format!("{} {}", detail, label);
                 let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
                 let runs = language.highlight_text(&source, 11..11 + text.len());
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(detail.len() + 1..text.len());
                 return Some(CodeLabel {
-                    filter_range: detail.len() + 1..text.len(),
+                    filter_range,
                     text,
                     runs,
                 });
@@ -143,8 +151,16 @@ impl super::LspAdapter for CLspAdapter {
                 let detail = completion.detail.as_ref().unwrap();
                 let text = format!("{} {}", detail, label);
                 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(detail.len() + 1..text.len());
                 return Some(CodeLabel {
-                    filter_range: detail.len() + 1..text.len(),
+                    filter_range,
                     text,
                     runs,
                 });
@@ -155,16 +171,24 @@ impl super::LspAdapter for CLspAdapter {
                 let detail = completion.detail.as_ref().unwrap();
                 let text = format!("{} {}", detail, label);
                 let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
-                let filter_start = detail.len() + 1;
-                let filter_end =
-                    if let Some(end) = text.rfind('(').filter(|end| *end > filter_start) {
-                        end
-                    } else {
-                        text.len()
-                    };
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or_else(|| {
+                        let filter_start = detail.len() + 1;
+                        let filter_end = text
+                            .rfind('(')
+                            .filter(|end| *end > filter_start)
+                            .unwrap_or(text.len());
+                        filter_start..filter_end
+                    });
 
                 return Some(CodeLabel {
-                    filter_range: filter_start..filter_end,
+                    filter_range,
                     text,
                     runs,
                 });
@@ -186,7 +210,8 @@ impl super::LspAdapter for CLspAdapter {
                     .grammar()
                     .and_then(|g| g.highlight_id_for_name(highlight_name?))
                 {
-                    let mut label = CodeLabel::plain(label.to_string(), None);
+                    let mut label =
+                        CodeLabel::plain(label.to_string(), completion.filter_text.as_deref());
                     label.runs.push((
                         0..label.text.rfind('(').unwrap_or(label.text.len()),
                         highlight_id,
@@ -196,7 +221,10 @@ impl super::LspAdapter for CLspAdapter {
             }
             _ => {}
         }
-        Some(CodeLabel::plain(label.to_string(), None))
+        Some(CodeLabel::plain(
+            label.to_string(),
+            completion.filter_text.as_deref(),
+        ))
     }
 
     async fn label_for_symbol(

crates/languages/src/go.rs 🔗

@@ -233,10 +233,18 @@ impl super::LspAdapter for GoLspAdapter {
                 let text = format!("{label} {detail}");
                 let source = Rope::from(format!("import {text}").as_str());
                 let runs = language.highlight_text(&source, 7..7 + text.len());
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(0..label.len());
                 return Some(CodeLabel {
                     text,
                     runs,
-                    filter_range: 0..label.len(),
+                    filter_range,
                 });
             }
             Some((
@@ -250,10 +258,18 @@ impl super::LspAdapter for GoLspAdapter {
                     name_offset,
                     language.highlight_text(&source, 4..4 + text.len()),
                 );
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(0..label.len());
                 return Some(CodeLabel {
                     text,
                     runs,
-                    filter_range: 0..label.len(),
+                    filter_range,
                 });
             }
             Some((lsp::CompletionItemKind::STRUCT, _)) => {
@@ -263,10 +279,18 @@ impl super::LspAdapter for GoLspAdapter {
                     name_offset,
                     language.highlight_text(&source, 5..5 + text.len()),
                 );
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(0..label.len());
                 return Some(CodeLabel {
                     text,
                     runs,
-                    filter_range: 0..label.len(),
+                    filter_range,
                 });
             }
             Some((lsp::CompletionItemKind::INTERFACE, _)) => {
@@ -276,10 +300,18 @@ impl super::LspAdapter for GoLspAdapter {
                     name_offset,
                     language.highlight_text(&source, 5..5 + text.len()),
                 );
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(0..label.len());
                 return Some(CodeLabel {
                     text,
                     runs,
-                    filter_range: 0..label.len(),
+                    filter_range,
                 });
             }
             Some((lsp::CompletionItemKind::FIELD, detail)) => {
@@ -290,10 +322,18 @@ impl super::LspAdapter for GoLspAdapter {
                     name_offset,
                     language.highlight_text(&source, 16..16 + text.len()),
                 );
+                let filter_range = completion
+                    .filter_text
+                    .as_deref()
+                    .and_then(|filter_text| {
+                        text.find(filter_text)
+                            .map(|start| start..start + filter_text.len())
+                    })
+                    .unwrap_or(0..label.len());
                 return Some(CodeLabel {
                     text,
                     runs,
-                    filter_range: 0..label.len(),
+                    filter_range,
                 });
             }
             Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
@@ -304,8 +344,16 @@ impl super::LspAdapter for GoLspAdapter {
                         name_offset,
                         language.highlight_text(&source, 5..5 + text.len()),
                     );
+                    let filter_range = completion
+                        .filter_text
+                        .as_deref()
+                        .and_then(|filter_text| {
+                            text.find(filter_text)
+                                .map(|start| start..start + filter_text.len())
+                        })
+                        .unwrap_or(0..label.len());
                     return Some(CodeLabel {
-                        filter_range: 0..label.len(),
+                        filter_range,
                         text,
                         runs,
                     });

crates/languages/src/python.rs 🔗

@@ -268,10 +268,15 @@ impl LspAdapter for PythonLspAdapter {
             lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
             _ => return None,
         };
+        let filter_range = item
+            .filter_text
+            .as_deref()
+            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..label.len());
         Some(language::CodeLabel {
             text: label.clone(),
             runs: vec![(0..label.len(), highlight_id)],
-            filter_range: 0..label.len(),
+            filter_range,
         })
     }
 
@@ -1152,10 +1157,15 @@ impl LspAdapter for PyLspAdapter {
             lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
             _ => return None,
         };
+        let filter_range = item
+            .filter_text
+            .as_deref()
+            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..label.len());
         Some(language::CodeLabel {
             text: label.clone(),
             runs: vec![(0..label.len(), highlight_id)],
-            filter_range: 0..label.len(),
+            filter_range,
         })
     }
 

crates/languages/src/rust.rs 🔗

@@ -310,10 +310,15 @@ impl LspAdapter for RustLspAdapter {
                 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: 0..name.len(),
+                    filter_range,
                 });
             }
             (
@@ -330,10 +335,15 @@ impl LspAdapter for RustLspAdapter {
                 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: 0..name.len(),
+                    filter_range,
                 });
             }
             (
@@ -367,9 +377,13 @@ impl LspAdapter for RustLspAdapter {
                         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: 0..completion.label.find('(').unwrap_or(text.len()),
+                        filter_range,
                         text,
                         runs,
                     });
@@ -378,12 +392,18 @@ impl LspAdapter for RustLspAdapter {
                     .as_ref()
                     .map_or(false, |detail| detail.starts_with("macro_rules! "))
                 {
-                    let source = Rope::from(completion.label.as_str());
-                    let runs = language.highlight_text(&source, 0..completion.label.len());
-
+                    let text = completion.label.clone();
+                    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: 0..completion.label.len(),
-                        text: completion.label.clone(),
+                        filter_range,
+                        text,
                         runs,
                     });
                 }
@@ -406,7 +426,7 @@ impl LspAdapter for RustLspAdapter {
                     label.push(' ');
                     label.push_str(detail);
                 }
-                let mut label = CodeLabel::plain(label, None);
+                let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
                 if let Some(highlight_name) = highlight_name {
                     let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
                     label.runs.push((
@@ -1181,6 +1201,49 @@ mod tests {
                 ],
             })
         );
+
+        assert_eq!(
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::METHOD),
+                        label: "await.as_deref_mut()".to_string(),
+                        filter_text: Some("as_deref_mut".to_string()),
+                        label_details: Some(CompletionItemLabelDetails {
+                            detail: None,
+                            description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
+                        }),
+                        ..Default::default()
+                    },
+                    &language
+                )
+                .await,
+            Some(CodeLabel {
+                text: "await.as_deref_mut()".to_string(),
+                filter_range: 6..18,
+                runs: vec![],
+            })
+        );
+
+        assert_eq!(
+            adapter
+                .label_for_completion(
+                    &lsp::CompletionItem {
+                        kind: Some(lsp::CompletionItemKind::FIELD),
+                        label: "inner_value".to_string(),
+                        filter_text: Some("value".to_string()),
+                        detail: Some("String".to_string()),
+                        ..Default::default()
+                    },
+                    &language,
+                )
+                .await,
+            Some(CodeLabel {
+                text: "inner_value: String".to_string(),
+                filter_range: 6..11,
+                runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
+            })
+        );
     }
 
     #[gpui::test]

crates/languages/src/typescript.rs 🔗

@@ -668,11 +668,15 @@ impl LspAdapter for TypeScriptLspAdapter {
         } else {
             item.label.clone()
         };
-
+        let filter_range = item
+            .filter_text
+            .as_deref()
+            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..len);
         Some(language::CodeLabel {
             text,
             runs: vec![(0..len, highlight_id)],
-            filter_range: 0..len,
+            filter_range,
         })
     }
 

crates/languages/src/vtsls.rs 🔗

@@ -195,11 +195,15 @@ impl LspAdapter for VtslsLspAdapter {
         } else {
             item.label.clone()
         };
-
+        let filter_range = item
+            .filter_text
+            .as_deref()
+            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..len);
         Some(language::CodeLabel {
             text,
             runs: vec![(0..len, highlight_id)],
-            filter_range: 0..len,
+            filter_range,
         })
     }
 

crates/project/src/project.rs 🔗

@@ -5306,16 +5306,6 @@ impl ProjectItem for Buffer {
 }
 
 impl Completion {
-    pub fn filter_text(&self) -> &str {
-        match &self.source {
-            CompletionSource::Lsp { lsp_completion, .. } => lsp_completion
-                .filter_text
-                .as_deref()
-                .unwrap_or_else(|| self.label.filter_text()),
-            _ => self.label.filter_text(),
-        }
-    }
-
     pub fn kind(&self) -> Option<CompletionItemKind> {
         self.source
             // `lsp::CompletionListItemDefaults` has no `kind` field