wip

HactarCE and Cole Miller created

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/agent_ui/src/acp/completion_provider.rs            |  16 
crates/agent_ui/src/context_picker/completion_provider.rs |  14 
crates/agent_ui/src/slash_command.rs                      |   4 
crates/debugger_ui/src/session/running/console.rs         |   4 
crates/editor/src/code_completion_tests.rs                |   2 
crates/editor/src/code_context_menus.rs                   | 167 +++-----
crates/editor/src/editor.rs                               |  45 +-
crates/editor/src/editor_tests.rs                         |  74 +-
crates/editor/src/test/editor_test_context.rs             |   2 
crates/inspector_ui/src/div_inspector.rs                  |   2 
crates/keymap_editor/src/keymap_editor.rs                 |   2 
crates/project/src/lsp_store.rs                           |   6 
crates/project/src/project.rs                             |  18 
crates/snippet_provider/src/lib.rs                        |   1 
14 files changed, 177 insertions(+), 180 deletions(-)

Detailed changes

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -109,7 +109,7 @@ impl ContextPickerCompletionProvider {
                 icon_path: Some(mode.icon().path().into()),
                 documentation: None,
                 source: project::CompletionSource::Custom,
-                buffer_match: None,
+                match_start: None,
                 insert_text_mode: None,
                 // This ensures that when a user accepts this completion, the
                 // completion menu will still be shown after "@category " is
@@ -147,7 +147,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
-            buffer_match: None,
+            match_start: None,
             icon_path: Some(icon_for_completion),
             confirm: Some(confirm_completion_callback(
                 thread_entry.title().clone(),
@@ -179,7 +179,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
-            buffer_match: None,
+            match_start: None,
             icon_path: Some(icon_path),
             confirm: Some(confirm_completion_callback(
                 rule.title,
@@ -236,7 +236,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(completion_icon_path),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 file_name,
@@ -283,7 +283,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_path),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 symbol.name.into(),
@@ -316,7 +316,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_path),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 url_to_fetch.to_string().into(),
@@ -385,7 +385,7 @@ impl ContextPickerCompletionProvider {
             icon_path: Some(action.icon().path().into()),
             documentation: None,
             source: project::CompletionSource::Custom,
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             // This ensures that when a user accepts this completion, the
             // completion menu will still be shown after "@category " is
@@ -759,7 +759,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 )),
                                 source: project::CompletionSource::Custom,
                                 icon_path: None,
-                                buffer_match: None,
+                                match_start: None,
                                 insert_text_mode: None,
                                 confirm: Some(Arc::new({
                                     let editor = editor.clone();

crates/agent_ui/src/context_picker/completion_provider.rs 🔗

@@ -278,7 +278,7 @@ impl ContextPickerCompletionProvider {
                 icon_path: Some(mode.icon().path().into()),
                 documentation: None,
                 source: project::CompletionSource::Custom,
-                buffer_match: None,
+                match_start: None,
                 insert_text_mode: None,
                 // This ensures that when a user accepts this completion, the
                 // completion menu will still be shown after "@category " is
@@ -387,7 +387,7 @@ impl ContextPickerCompletionProvider {
                     icon_path: Some(action.icon().path().into()),
                     documentation: None,
                     source: project::CompletionSource::Custom,
-                    buffer_match: None,
+                    match_start: None,
                     insert_text_mode: None,
                     // This ensures that when a user accepts this completion, the
                     // completion menu will still be shown after "@category " is
@@ -419,7 +419,7 @@ impl ContextPickerCompletionProvider {
             replace_range: source_range.clone(),
             new_text,
             label: CodeLabel::plain(thread_entry.title().to_string(), None),
-            buffer_match: None,
+            match_start: None,
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
@@ -487,7 +487,7 @@ impl ContextPickerCompletionProvider {
             replace_range: source_range.clone(),
             new_text,
             label: CodeLabel::plain(rules.title.to_string(), None),
-            buffer_match: None,
+            match_start: None,
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
@@ -528,7 +528,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::ToolWeb.path().into()),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::ToolWeb.path().into(),
@@ -617,7 +617,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(completion_icon_path),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 crease_icon_path,
@@ -696,7 +696,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::Code.path().into()),
-            buffer_match: None,
+            match_start: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::Code.path().into(),

crates/agent_ui/src/slash_command.rs 🔗

@@ -127,7 +127,7 @@ impl SlashCommandCompletionProvider {
                             new_text,
                             label: command.label(cx),
                             icon_path: None,
-                            buffer_match: None,
+                            match_start: None,
                             insert_text_mode: None,
                             confirm,
                             source: CompletionSource::Custom,
@@ -233,7 +233,7 @@ impl SlashCommandCompletionProvider {
                             icon_path: None,
                             new_text,
                             documentation: None,
-                            buffer_match: None,
+                            match_start: None,
                             confirm,
                             insert_text_mode: None,
                             source: CompletionSource::Custom,

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -670,7 +670,7 @@ impl ConsoleQueryBarCompletionProvider {
                         ),
                         new_text: string_match.string.clone(),
                         label: CodeLabel::plain(string_match.string.clone(), None),
-                        buffer_match: None,
+                        match_start: None,
                         icon_path: None,
                         documentation: Some(CompletionDocumentation::MultiLineMarkdown(
                             variable_value.into(),
@@ -784,7 +784,7 @@ impl ConsoleQueryBarCompletionProvider {
                         documentation: completion.detail.map(|detail| {
                             CompletionDocumentation::MultiLineMarkdown(detail.into())
                         }),
-                        buffer_match: None,
+                        match_start: None,
                         confirm: None,
                         source: project::CompletionSource::Dap { sort_text },
                         insert_text_mode: None,

crates/editor/src/code_context_menus.rs 🔗

@@ -217,10 +217,8 @@ pub struct CompletionsMenu {
     pub is_incomplete: bool,
     pub buffer: Entity<Buffer>,
     pub completions: Rc<RefCell<Box<[Completion]>>>,
-    /// Match candidates for completions that have `buffer_match = None`
-    match_candidates: Arc<[StringMatchCandidate]>,
-    /// Precomputed `buffer_match` for candidates that have it
-    precomputed_entries: Arc<[StringMatch]>,
+    /// String match candidate for each completion, grouped by `match_start`.
+    match_candidates: Arc<[(Option<text::Anchor>, Vec<StringMatchCandidate>)]>,
     pub entries: Rc<RefCell<Box<[StringMatch]>>>,
     pub selected_item: usize,
     filter_task: Task<()>,
@@ -284,17 +282,10 @@ impl CompletionsMenu {
         let match_candidates = completions
             .iter()
             .enumerate()
-            .filter(|(_id, completion)| completion.buffer_match.is_none())
             .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
-            .collect();
-        let precomputed_entries = completions
-            .iter()
-            .enumerate()
-            .filter_map(|(id, completion)| {
-                let mut m = completion.buffer_match.clone()?;
-                m.candidate_id = id;
-                Some(m)
-            })
+            .into_group_map_by(|candidate| completions[candidate.id].match_start)
+            .into_iter()
+            .map(|(k, v)| (k, v))
             .collect();
 
         let completions_menu = Self {
@@ -308,7 +299,6 @@ impl CompletionsMenu {
             show_completion_documentation,
             completions: RefCell::new(completions).into(),
             match_candidates,
-            precomputed_entries,
             entries: Rc::new(RefCell::new(Box::new([]))),
             selected_item: 0,
             filter_task: Task::ready(()),
@@ -343,7 +333,7 @@ impl CompletionsMenu {
                 replace_range: selection.start.text_anchor..selection.end.text_anchor,
                 new_text: choice.to_string(),
                 label: CodeLabel::plain(choice.to_string(), None),
-                buffer_match: None,
+                match_start: None,
                 icon_path: None,
                 documentation: None,
                 confirm: None,
@@ -352,11 +342,14 @@ impl CompletionsMenu {
             })
             .collect();
 
-        let match_candidates = choices
-            .iter()
-            .enumerate()
-            .map(|(id, completion)| StringMatchCandidate::new(id, completion))
-            .collect();
+        let match_candidates = Arc::new([(
+            None,
+            choices
+                .iter()
+                .enumerate()
+                .map(|(id, completion)| StringMatchCandidate::new(id, completion))
+                .collect(),
+        )]);
         let entries = choices
             .iter()
             .enumerate()
@@ -376,7 +369,6 @@ impl CompletionsMenu {
             is_incomplete: false,
             buffer,
             completions: RefCell::new(completions).into(),
-            precomputed_entries: Arc::new([]),
             match_candidates,
             entries: RefCell::new(entries).into(),
             selected_item: 0,
@@ -1006,60 +998,74 @@ impl CompletionsMenu {
 
     pub fn filter(
         &mut self,
-        query: Option<Arc<String>>,
+        query: Arc<String>,
+        query_end: text::Anchor,
+        buffer: &Entity<Buffer>,
         provider: Option<Rc<dyn CompletionProvider>>,
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
         self.cancel_filter.store(true, Ordering::Relaxed);
-        if let Some(query) = query {
-            self.cancel_filter = Arc::new(AtomicBool::new(false));
-            let matches = self.do_async_filtering(query, cx);
-            let id = self.id;
-            self.filter_task = cx.spawn_in(window, async move |editor, cx| {
-                let matches = matches.await;
-                editor
-                    .update_in(cx, |editor, window, cx| {
-                        editor.with_completions_menu_matching_id(id, |this| {
-                            if let Some(this) = this {
-                                this.set_filter_results(matches, provider, window, cx);
-                            }
-                        });
-                    })
-                    .ok();
-            });
-        } else {
-            self.filter_task = Task::ready(());
-            let matches = self.unfiltered_matches();
-            self.set_filter_results(matches, provider, window, cx);
-        }
+        self.cancel_filter = Arc::new(AtomicBool::new(false));
+        let matches = self.do_async_filtering(query, query_end, buffer, cx);
+        let id = self.id;
+        self.filter_task = cx.spawn_in(window, async move |editor, cx| {
+            let matches = matches.await;
+            editor
+                .update_in(cx, |editor, window, cx| {
+                    editor.with_completions_menu_matching_id(id, |this| {
+                        if let Some(this) = this {
+                            this.set_filter_results(matches, provider, window, cx);
+                        }
+                    });
+                })
+                .ok();
+        });
     }
 
     pub fn do_async_filtering(
         &self,
         query: Arc<String>,
+        query_end: text::Anchor,
+        buffer: &Entity<Buffer>,
         cx: &Context<Editor>,
     ) -> Task<Vec<StringMatch>> {
-        let matches_task = cx.background_spawn({
-            let query = query.clone();
-            let match_candidates = self.match_candidates.clone();
-            let precomputed_entries = self.precomputed_entries.clone();
-            let cancel_filter = self.cancel_filter.clone();
-            let background_executor = cx.background_executor().clone();
-            async move {
-                let mut matches = fuzzy::match_strings(
-                    &match_candidates,
-                    &query,
-                    query.chars().any(|c| c.is_uppercase()),
-                    false,
-                    1000,
-                    &cancel_filter,
-                    background_executor,
-                )
-                .await;
-                matches.extend(precomputed_entries.iter().cloned());
-                matches
+        let buffer_snapshot = buffer.read(cx).snapshot();
+        let background_executor = cx.background_executor().clone();
+        let match_candidates = self.match_candidates.clone();
+        let cancel_filter = self.cancel_filter.clone();
+        let default_query = query.clone();
+
+        let matches_task = cx.background_spawn(async move {
+            let queries_and_candidates = match_candidates
+                .iter()
+                .map(|(query_start, candidates)| {
+                    let query_for_batch = match query_start {
+                        Some(start) => {
+                            Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
+                        }
+                        None => default_query.clone(),
+                    };
+                    (query_for_batch, candidates)
+                })
+                .collect_vec();
+
+            let mut results = vec![];
+            for (query, match_candidates) in queries_and_candidates {
+                results.extend(
+                    fuzzy::match_strings(
+                        &match_candidates,
+                        &query,
+                        query.chars().any(|c| c.is_uppercase()),
+                        false,
+                        1000,
+                        &cancel_filter,
+                        background_executor.clone(),
+                    )
+                    .await,
+                );
             }
+            results
         });
 
         let completions = self.completions.clone();
@@ -1071,7 +1077,7 @@ impl CompletionsMenu {
             if sort_completions {
                 matches = Self::sort_string_matches(
                     matches,
-                    Some(&query),
+                    Some(&query), // used for non-snippets only
                     snippet_sort_order,
                     completions.borrow().as_ref(),
                 );
@@ -1081,33 +1087,6 @@ impl CompletionsMenu {
         })
     }
 
-    /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
-    pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
-        let mut matches = self
-            .match_candidates
-            .iter()
-            .enumerate()
-            .map(|(candidate_id, candidate)| StringMatch {
-                candidate_id,
-                score: Default::default(),
-                positions: Default::default(),
-                string: candidate.string.clone(),
-            })
-            .chain(self.precomputed_entries.iter().cloned())
-            .collect();
-
-        if self.sort_completions {
-            matches = Self::sort_string_matches(
-                matches,
-                None,
-                self.snippet_sort_order,
-                self.completions.borrow().as_ref(),
-            );
-        }
-
-        matches
-    }
-
     pub fn set_filter_results(
         &mut self,
         matches: Vec<StringMatch>,
@@ -1150,7 +1129,8 @@ impl CompletionsMenu {
             .and_then(|c| c.to_lowercase().next());
 
         if snippet_sort_order == SnippetSortOrder::None {
-            matches.retain(|string_match| !completions[string_match.candidate_id].is_snippet());
+            matches
+                .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
         }
 
         matches.sort_unstable_by_key(|string_match| {
@@ -1168,7 +1148,7 @@ impl CompletionsMenu {
             let sort_score = Reverse(OrderedFloat(score));
 
             // Snippets do their own first-letter matching logic elsewhere.
-            let is_snippet = completion.is_snippet();
+            let is_snippet = completion.is_snippet_kind();
             let query_start_doesnt_match_split_words = !is_snippet
                 && query_start_lower
                     .map(|query_char| {
@@ -1189,6 +1169,7 @@ impl CompletionsMenu {
                     SnippetSortOrder::None => Reverse(0),
                 };
                 let sort_positions = string_match.positions.clone();
+                // This exact matching won't work for multi-word snippets, but it's fine
                 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
                     1
                 } else {

crates/editor/src/editor.rs 🔗

@@ -4937,7 +4937,6 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        dbg!(&text);
         let completions_source = self
             .context_menu
             .borrow()
@@ -4977,9 +4976,6 @@ impl Editor {
                     cx,
                 );
             }
-            _ => {
-                self.hide_context_menu(window, cx);
-            }
         }
     }
 
@@ -5379,7 +5375,14 @@ impl Editor {
 
         if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
             if filter_completions {
-                menu.filter(query.clone(), provider.clone(), window, cx);
+                menu.filter(
+                    query.clone().unwrap_or_default(),
+                    buffer_position.text_anchor,
+                    &buffer,
+                    provider.clone(),
+                    window,
+                    cx,
+                );
             }
             // When `is_incomplete` is false, no need to re-query completions when the current query
             // is a suffix of the initial query.
@@ -5572,7 +5575,7 @@ impl Editor {
                 replace_range: word_replace_range.clone(),
                 new_text: word.clone(),
                 label: CodeLabel::plain(word, None),
-                buffer_match: None,
+                match_start: None,
                 icon_path: None,
                 documentation: None,
                 source: CompletionSource::BufferWord {
@@ -5610,11 +5613,12 @@ impl Editor {
                     );
 
                     let query = if filter_completions { query } else { None };
-                    let matches_task = if let Some(query) = query {
-                        menu.do_async_filtering(query, cx)
-                    } else {
-                        Task::ready(menu.unfiltered_matches())
-                    };
+                    let matches_task = menu.do_async_filtering(
+                        query.unwrap_or_default(),
+                        buffer_position,
+                        &buffer,
+                        cx,
+                    );
                     (menu, matches_task)
                 }) else {
                     return;
@@ -22871,7 +22875,7 @@ fn snippet_completions(
             let buffer_windows = snippet_candidate_suffixes(&max_buffer_window)
                 .take(
                     sorted_snippet_candidates
-                        .last()
+                        .first()
                         .map(|(_, _, word_count)| *word_count)
                         .unwrap_or_default(),
                 )
@@ -22901,7 +22905,7 @@ fn snippet_completions(
 
                 let candidates = snippet_candidates_at_word_len
                     .iter()
-                    .map(|(snippet_ix, prefix, snippet_word_count)| prefix)
+                    .map(|(_snippet_ix, prefix, _snippet_word_count)| prefix)
                     .enumerate() // index in `sorted_snippet_candidates`
                     // First char must match
                     .filter(|(_ix, prefix)| {
@@ -22960,7 +22964,7 @@ fn snippet_completions(
                 matches
                     .iter()
                     .filter_map(|(string_match, buffer_window_len)| {
-                        let (snippet_index, matching_prefix) =
+                        let (snippet_index, matching_prefix, _snippet_word_count) =
                             sorted_snippet_candidates[string_match.candidate_id];
                         let snippet = &snippets[snippet_index];
                         let start = buffer_offset - buffer_window_len;
@@ -23018,7 +23022,7 @@ fn snippet_completions(
                             ),
                             insert_text_mode: None,
                             confirm: None,
-                            buffer_match: Some(string_match.clone()),
+                            match_start: Some(start),
                         })
                     }),
             );
@@ -24281,19 +24285,22 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
 /// strings a snippet could match to. More precisely: returns an iterator over
 /// suffixes of `text` created by splitting at word boundaries (for a particular
 /// definition of "word").
+///
+/// Shorter suffixes are returned first.
 pub(crate) fn snippet_candidate_suffixes(text: &str) -> impl std::iter::Iterator<Item = &str> {
-    let mut prev_index = 0;
-    let mut prev_codepoint: Option<char> = None;
+    let mut prev_index = text.len();
+    let mut prev_codepoint = None;
     let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
     text.char_indices()
-        .chain([(text.len(), '\0')])
+        .rev()
+        .chain([(0, '\0')])
         .filter_map(move |(index, codepoint)| {
+            let prev_index = std::mem::replace(&mut prev_index, index);
             let prev_codepoint = prev_codepoint.replace(codepoint)?;
             if is_word_char(prev_codepoint) && is_word_char(codepoint) {
                 None
             } else {
                 let chunk = &text[prev_index..]; // go to end of string
-                prev_index = index;
                 Some(chunk)
             }
         })

crates/editor/src/editor_tests.rs 🔗

@@ -11196,7 +11196,7 @@ async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
     let mut cx = EditorTestContext::new(cx).await;
     cx.update_editor(|editor, _, cx| {
         editor.project().unwrap().update(cx, |project, cx| {
-            project.snippets().update(cx, |snippets, cx| {
+            project.snippets().update(cx, |snippets, _cx| {
                 let snippet = project::snippet_provider::Snippet {
                     prefix: vec!["multi word".to_string()],
                     body: "this is many words".to_string(),
@@ -11207,34 +11207,33 @@ async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
                     None,
                     PathBuf::from("test_snippets.json"),
                     vec![Arc::new(snippet)],
-                    cx,
                 );
             });
         })
     });
 
-    cx.set_state("ˇ");
-    // cx.simulate_input("m");
-    // cx.simulate_input("m ");
-    // cx.simulate_input("m w");
-    // cx.simulate_input("aa m w");
-    cx.simulate_input("aa m g"); // fails correctly
-
-    cx.update_editor(|editor, _, _| {
-        let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
-        else {
-            panic!("expected completion menu");
-        };
-        assert!(context_menu.visible());
-        let completions = context_menu.completions.borrow();
+    for (input_to_simulate, should_match_snippet) in [
+        ("m", true),
+        ("m ", true),
+        ("m w", true),
+        ("aa m w", true),
+        ("aa m g", false),
+    ] {
+        cx.set_state("ˇ");
+        cx.simulate_input(input_to_simulate); // fails correctly
+
+        cx.update_editor(|editor, _, _| {
+            let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
+            else {
+                assert!(!should_match_snippet); // no completions! don't even show the menu
+                return;
+            };
+            assert!(context_menu.visible());
+            let completions = context_menu.completions.borrow();
 
-        assert!(
-            completions
-                .iter()
-                .any(|c| c.new_text == "this is many words"),
-            "Expected to find 'multi word' snippet in completions"
-        );
-    });
+            assert_eq!(!completions.is_empty(), should_match_snippet);
+        });
+    }
 }
 
 #[gpui::test]
@@ -17255,23 +17254,24 @@ fn test_split_words_for_snippet_prefix() {
     assert_eq!(
         split("this@is!@#$^many   . symbols"),
         &[
-            "this@is!@#$^many   . symbols",
-            "@is!@#$^many   . symbols",
-            "is!@#$^many   . symbols",
-            "!@#$^many   . symbols",
-            "@#$^many   . symbols",
-            "#$^many   . symbols",
-            "$^many   . symbols",
-            "^many   . symbols",
-            "many   . symbols",
-            "   . symbols",
-            "  . symbols",
-            " . symbols",
-            ". symbols",
+            "symbols",
             " symbols",
-            "symbols"
+            ". symbols",
+            " . symbols",
+            "  . symbols",
+            "   . symbols",
+            "many   . symbols",
+            "^many   . symbols",
+            "$^many   . symbols",
+            "#$^many   . symbols",
+            "@#$^many   . symbols",
+            "!@#$^many   . symbols",
+            "is!@#$^many   . symbols",
+            "@is!@#$^many   . symbols",
+            "this@is!@#$^many   . symbols",
         ],
     );
+    assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
 }
 
 #[gpui::test]

crates/editor/src/test/editor_test_context.rs 🔗

@@ -60,7 +60,7 @@ impl EditorTestContext {
             .unwrap();
 
         let language = project
-            .read_with(cx, |project, cx| {
+            .read_with(cx, |project, _cx| {
                 project.languages().language_for_name("Plain Text")
             })
             .await

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -665,7 +665,7 @@ impl CompletionProvider for RustStyleCompletionProvider {
                     replace_range: replace_range.clone(),
                     new_text: format!(".{}()", method.name),
                     label: CodeLabel::plain(method.name.to_string(), None),
-                    buffer_match: None,
+                    match_start: None,
                     icon_path: None,
                     documentation: method.documentation.map(|documentation| {
                         CompletionDocumentation::MultiLineMarkdown(documentation.into())

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -2938,7 +2938,7 @@ impl CompletionProvider for KeyContextCompletionProvider {
                     documentation: None,
                     source: project::CompletionSource::Custom,
                     icon_path: None,
-                    buffer_match: None,
+                    match_start: None,
                     insert_text_mode: None,
                     confirm: None,
                 })

crates/project/src/lsp_store.rs 🔗

@@ -9970,7 +9970,7 @@ impl LspStore {
                     source: completion.source,
                     documentation: None,
                     label: CodeLabel::default(),
-                    buffer_match: None,
+                    match_start: None,
                     insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
@@ -12554,7 +12554,7 @@ async fn populate_labels_for_completions(
                     source: completion.source,
                     icon_path: None,
                     confirm: None,
-                    buffer_match: None,
+                    match_start: None,
                 });
             }
             None => {
@@ -12569,7 +12569,7 @@ async fn populate_labels_for_completions(
                     insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
-                    buffer_match: None,
+                    match_start: None,
                 });
             }
         }

crates/project/src/project.rs 🔗

@@ -26,8 +26,7 @@ mod project_tests;
 mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
-pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
-use fuzzy::StringMatch;
+pub use environment::ProjectEnvironmentEvent;
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
 pub mod search_history;
@@ -477,8 +476,10 @@ pub struct Completion {
     pub source: CompletionSource,
     /// A path to an icon for this completion that is shown in the menu.
     pub icon_path: Option<SharedString>,
-    /// String match against part of the buffer contents (typically the last word).
-    pub buffer_match: Option<StringMatch>,
+    /// Text starting here and ending at the cursor will be used as the query for filtering this completion.
+    ///
+    /// If None, the start of the surrounding word is used.
+    pub match_start: Option<text::Anchor>,
     /// Whether to adjust indentation (the default) or not.
     pub insert_text_mode: Option<InsertTextMode>,
     /// An optional callback to invoke when this completion is confirmed.
@@ -5638,6 +5639,15 @@ impl Completion {
     }
 
     /// Whether this completion is a snippet.
+    pub fn is_snippet_kind(&self) -> bool {
+        matches!(
+            &self.source,
+            CompletionSource::Lsp { lsp_completion, .. }
+            if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
+        )
+    }
+
+    /// Whether this completion is a snippet or snippet-style LSP completion.
     pub fn is_snippet(&self) -> bool {
         self.source
             // `lsp::CompletionListItemDefaults` has `insert_text_format` field

crates/snippet_provider/src/lib.rs 🔗

@@ -241,7 +241,6 @@ impl SnippetProvider {
         language: SnippetKind,
         path: PathBuf,
         snippet: Vec<Arc<Snippet>>,
-        cx: &mut Context<Self>,
     ) {
         self.snippets
             .entry(language)