From 76e3136369484841cba934eb8669570e24d597fb Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 21 Jun 2025 19:47:16 +0530 Subject: [PATCH] editor: Utilize `filter_text` from language server for `filter_range` (#33155) 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. --- 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(-) diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 83e8ed0927..4f9822b597 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/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(); diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 85b8aa005e..e9642657f8 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/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 diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 549eedc05a..8b8c411366 100644 --- a/crates/language/src/language.rs +++ b/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) { diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 446519c3dc..c06c35ee69 100644 --- a/crates/languages/src/c.rs +++ b/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( diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 9844df80d6..25aa5a67b9 100644 --- a/crates/languages/src/go.rs +++ b/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, }); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 38151c3b9b..03b1b749c1 100644 --- a/crates/languages/src/python.rs +++ b/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, }) } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 1397d5dbf1..c4edec1f29 100644 --- a/crates/languages/src/rust.rs +++ b/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] diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 4c59bd0704..0a0326f4f7 100644 --- a/crates/languages/src/typescript.rs +++ b/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, }) } diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 10c64ce536..ca07673d5f 100644 --- a/crates/languages/src/vtsls.rs +++ b/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, }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 130e4c7a40..2fc7fbbe76 100644 --- a/crates/project/src/project.rs +++ b/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 { self.source // `lsp::CompletionListItemDefaults` has no `kind` field