wip

HactarCE and Cole Miller created

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

Change summary

crates/agent_ui/src/acp/completion_provider.rs            |  8 +
crates/agent_ui/src/context_picker/completion_provider.rs |  7 
crates/agent_ui/src/slash_command.rs                      |  2 
crates/debugger_ui/src/session/running/console.rs         |  2 
crates/editor/src/code_completion_tests.rs                |  1 
crates/editor/src/code_context_menus.rs                   | 65 ++++----
crates/editor/src/editor.rs                               | 40 ++---
crates/editor/src/editor_tests.rs                         | 24 +-
crates/editor/src/test/editor_test_context.rs             | 11 +
crates/inspector_ui/src/div_inspector.rs                  |  1 
crates/keymap_editor/src/keymap_editor.rs                 |  1 
crates/project/src/lsp_store.rs                           |  3 
crates/project/src/project.rs                             |  3 
13 files changed, 107 insertions(+), 61 deletions(-)

Detailed changes

crates/agent_ui/src/acp/completion_provider.rs šŸ”—

@@ -108,6 +108,7 @@ impl ContextPickerCompletionProvider {
                 icon_path: Some(mode.icon().path().into()),
                 documentation: None,
                 source: project::CompletionSource::Custom,
+                buffer_match: None,
                 insert_text_mode: None,
                 // This ensures that when a user accepts this completion, the
                 // completion menu will still be shown after "@category " is
@@ -145,6 +146,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
+            buffer_match: None,
             icon_path: Some(icon_for_completion),
             confirm: Some(confirm_completion_callback(
                 thread_entry.title().clone(),
@@ -176,6 +178,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
+            buffer_match: None,
             icon_path: Some(icon_path),
             confirm: Some(confirm_completion_callback(
                 rule.title,
@@ -232,6 +235,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(completion_icon_path),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 file_name,
@@ -278,6 +282,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_path),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 symbol.name.into(),
@@ -310,6 +315,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_path),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 url_to_fetch.to_string().into(),
@@ -378,6 +384,7 @@ impl ContextPickerCompletionProvider {
             icon_path: Some(action.icon().path().into()),
             documentation: None,
             source: project::CompletionSource::Custom,
+            buffer_match: None,
             insert_text_mode: None,
             // This ensures that when a user accepts this completion, the
             // completion menu will still be shown after "@category " is
@@ -749,6 +756,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 )),
                                 source: project::CompletionSource::Custom,
                                 icon_path: None,
+                                buffer_match: None, // todo! is this right?
                                 insert_text_mode: None,
                                 confirm: Some(Arc::new({
                                     let editor = editor.clone();

crates/agent_ui/src/context_picker/completion_provider.rs šŸ”—

@@ -287,6 +287,7 @@ impl ContextPickerCompletionProvider {
                 icon_path: Some(mode.icon().path().into()),
                 documentation: None,
                 source: project::CompletionSource::Custom,
+                buffer_match: None,
                 insert_text_mode: None,
                 // This ensures that when a user accepts this completion, the
                 // completion menu will still be shown after "@category " is
@@ -395,6 +396,7 @@ impl ContextPickerCompletionProvider {
                     icon_path: Some(action.icon().path().into()),
                     documentation: None,
                     source: project::CompletionSource::Custom,
+                    buffer_match: None,
                     insert_text_mode: None,
                     // This ensures that when a user accepts this completion, the
                     // completion menu will still be shown after "@category " is
@@ -426,6 +428,7 @@ impl ContextPickerCompletionProvider {
             replace_range: source_range.clone(),
             new_text,
             label: CodeLabel::plain(thread_entry.title().to_string(), None),
+            buffer_match: None,
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
@@ -495,6 +498,7 @@ impl ContextPickerCompletionProvider {
             replace_range: source_range.clone(),
             new_text,
             label: CodeLabel::plain(rules.title.to_string(), None),
+            buffer_match: None,
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
@@ -535,6 +539,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::ToolWeb.path().into()),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::ToolWeb.path().into(),
@@ -623,6 +628,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(completion_icon_path),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 crease_icon_path,
@@ -701,6 +707,7 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::Code.path().into()),
+            buffer_match: None,
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
                 IconName::Code.path().into(),

crates/agent_ui/src/slash_command.rs šŸ”—

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

crates/debugger_ui/src/session/running/console.rs šŸ”—

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

crates/editor/src/code_context_menus.rs šŸ”—

@@ -34,8 +34,8 @@ use util::ResultExt;
 use crate::CodeActionSource;
 use crate::hover_popover::{hover_markdown_style, open_markdown_url};
 use crate::{
-    CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
-    EditorStyle, ResolvedTasks,
+    CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
+    ResolvedTasks,
     actions::{ConfirmCodeAction, ConfirmCompletion},
     split_words, styled_runs_for_code_label,
 };
@@ -217,7 +217,10 @@ 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]>,
     pub entries: Rc<RefCell<Box<[StringMatch]>>>,
     pub selected_item: usize,
     filter_task: Task<()>,
@@ -281,8 +284,18 @@ 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)
+            })
+            .collect();
 
         let completions_menu = Self {
             id,
@@ -295,6 +308,7 @@ 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(()),
@@ -329,6 +343,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,
                 icon_path: None,
                 documentation: None,
                 confirm: None,
@@ -361,6 +376,7 @@ 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,
@@ -912,7 +928,7 @@ impl CompletionsMenu {
         }
 
         let mat = &self.entries.borrow()[self.selected_item];
-        let completions = self.completions.borrow_mut();
+        let completions = self.completions.borrow();
         let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
             Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
             Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
@@ -1027,10 +1043,11 @@ impl CompletionsMenu {
         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 {
-                fuzzy::match_strings(
+                let mut matches = fuzzy::match_strings(
                     &match_candidates,
                     &query,
                     query.chars().any(|c| c.is_uppercase()),
@@ -1039,7 +1056,9 @@ impl CompletionsMenu {
                     &cancel_filter,
                     background_executor,
                 )
-                .await
+                .await;
+                matches.extend(precomputed_entries.iter().cloned());
+                matches
             }
         });
 
@@ -1074,6 +1093,7 @@ impl CompletionsMenu {
                 positions: Default::default(),
                 string: candidate.string.clone(),
             })
+            .chain(self.precomputed_entries.iter().cloned())
             .collect();
 
         if self.sort_completions {
@@ -1130,28 +1150,12 @@ impl CompletionsMenu {
             .and_then(|c| c.to_lowercase().next());
 
         if snippet_sort_order == SnippetSortOrder::None {
-            matches.retain(|string_match| {
-                let completion = &completions[string_match.candidate_id];
-
-                let is_snippet = matches!(
-                    &completion.source,
-                    CompletionSource::Lsp { lsp_completion, .. }
-                    if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
-                );
-
-                !is_snippet
-            });
+            matches.retain(|string_match| !completions[string_match.candidate_id].is_snippet());
         }
 
         matches.sort_unstable_by_key(|string_match| {
             let completion = &completions[string_match.candidate_id];
 
-            let is_snippet = matches!(
-                &completion.source,
-                CompletionSource::Lsp { lsp_completion, .. }
-                if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
-            );
-
             let sort_text = match &completion.source {
                 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
                 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
@@ -1163,14 +1167,17 @@ impl CompletionsMenu {
             let score = string_match.score;
             let sort_score = Reverse(OrderedFloat(score));
 
-            let query_start_doesnt_match_split_words = query_start_lower
-                .map(|query_char| {
-                    !split_words(&string_match.string).any(|word| {
-                        word.chars().next().and_then(|c| c.to_lowercase().next())
-                            == Some(query_char)
+            // Snippets do their own first-letter matching logic elsewhere.
+            let is_snippet = completion.is_snippet();
+            let query_start_doesnt_match_split_words = !is_snippet
+                && query_start_lower
+                    .map(|query_char| {
+                        !split_words(&string_match.string).any(|word| {
+                            word.chars().next().and_then(|c| c.to_lowercase().next())
+                                == Some(query_char)
+                        })
                     })
-                })
-                .unwrap_or(false);
+                    .unwrap_or(false);
 
             if query_start_doesnt_match_split_words {
                 MatchTier::OtherMatch { sort_score }

crates/editor/src/editor.rs šŸ”—

@@ -119,7 +119,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
 use itertools::{Either, Itertools};
 use language::{
     AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
-    BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
+    BufferSnapshot, Capability, CharKind, CharScopeContext, CodeLabel, CursorShape,
     DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
     IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
     TextObject, TransactionId, TreeSitterOptions, WordsQuery,
@@ -5772,6 +5772,7 @@ impl Editor {
                 replace_range: word_replace_range.clone(),
                 new_text: word.clone(),
                 label: CodeLabel::plain(word, None),
+                buffer_match: None,
                 icon_path: None,
                 documentation: None,
                 source: CompletionSource::BufferWord {
@@ -22997,6 +22998,8 @@ fn snippet_completions(
         const MAX_PREFIX_LEN: usize = 128;
         let buffer_offset = text::ToOffset::to_offset(&buffer_anchor, &snapshot);
         let window_start = buffer_offset.saturating_sub(MAX_PREFIX_LEN);
+        let window_start = snapshot.clip_offset(window_start, Bias::Left);
+
         let max_buffer_window: String = snapshot
             .text_for_range(window_start..buffer_offset)
             .collect();
@@ -23015,17 +23018,13 @@ fn snippet_completions(
                 .iter()
                 .enumerate()
                 .flat_map(|(snippet_ix, snippet)| {
-                    snippet
-                        .prefix
-                        .iter()
-                        .enumerate()
-                        .map(move |(prefix_ix, prefix)| {
-                            (
-                                (snippet_ix, prefix_ix),
-                                prefix,
-                                snippet_candidate_suffixes(prefix).count(),
-                            )
-                        })
+                    snippet.prefix.iter().map(move |prefix| {
+                        (
+                            snippet_ix,
+                            prefix,
+                            snippet_candidate_suffixes(prefix).count(),
+                        )
+                    })
                 })
                 .collect_vec();
             sorted_snippet_candidates
@@ -23066,9 +23065,10 @@ fn snippet_completions(
 
                 let candidates = snippet_candidates_at_word_len
                     .iter()
+                    .map(|(snippet_ix, prefix, snippet_word_count)| prefix)
                     .enumerate() // index in `sorted_snippet_candidates`
                     // First char must match
-                    .filter(|(_ix, (_, prefix, _snippet_word_count))| {
+                    .filter(|(_ix, prefix)| {
                         itertools::equal(
                             prefix
                                 .chars()
@@ -23083,12 +23083,8 @@ fn snippet_completions(
                         )
                     })
                     // Match each prefix only once
-                    .filter(|(ix, (_, _prefix, _snippet_word_count))| {
-                        sorted_snippet_candidates_seen.insert(*ix)
-                    })
-                    .map(|(ix, (_, prefix, _snippet_word_count))| {
-                        StringMatchCandidate::new(ix, prefix)
-                    })
+                    .filter(|(ix, _prefix)| sorted_snippet_candidates_seen.insert(*ix))
+                    .map(|(ix, prefix)| StringMatchCandidate::new(ix, prefix))
                     .collect::<Vec<StringMatchCandidate>>();
 
                 matches.extend(
@@ -23128,10 +23124,9 @@ fn snippet_completions(
                 matches
                     .iter()
                     .filter_map(|(string_match, buffer_window_len)| {
-                        let (snippet_index, prefix_index) =
-                            sorted_snippet_candidates[string_match.candidate_id].0;
+                        let (snippet_index, matching_prefix) =
+                            sorted_snippet_candidates[string_match.candidate_id];
                         let snippet = &snippets[snippet_index];
-                        let matching_prefix = &snippet.prefix[prefix_index];
                         let start = buffer_offset - buffer_window_len;
                         let start = snapshot.anchor_before(start);
                         let range = start..buffer_anchor;
@@ -23187,6 +23182,7 @@ fn snippet_completions(
                             ),
                             insert_text_mode: None,
                             confirm: None,
+                            buffer_match: Some(string_match.clone()),
                         })
                     }),
             );

crates/editor/src/editor_tests.rs šŸ”—

@@ -11088,13 +11088,13 @@ async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let mut cx = EditorTestContext::new(cx).await;
-    cx.update_editor(|editor, window, cx| {
+    cx.update_editor(|editor, _, cx| {
         editor.project().unwrap().update(cx, |project, cx| {
             project.snippets().update(cx, |snippets, cx| {
                 let snippet = project::snippet_provider::Snippet {
-                    prefix: "multi word".to_string(),
+                    prefix: vec!["multi word".to_string()],
                     body: "this is many words".to_string(),
-                    description: "description".to_string(),
+                    description: Some("description".to_string()),
                     name: "multi-word snippet test".to_string(),
                 };
                 snippets.add_snippet_for_test(
@@ -11107,21 +11107,25 @@ async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
         })
     });
 
-    cx.set_state("mˇ");
-    cx.simulate_input("u");
+    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, window, cx| {
-        let CodeContextMenu::Completions(context_menu) = editor.context_menu.borrow().unwrap()
+    cx.update_editor(|editor, _, _| {
+        let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
         else {
-            panic!("expected completion menu")
+            panic!("expected completion menu");
         };
         assert!(context_menu.visible());
-        let completions = context_menu;
+        let completions = context_menu.completions.borrow();
 
         assert!(
             completions
                 .iter()
-                .any(|c| c.string.as_str() == "multi word"),
+                .any(|c| c.new_text == "this is many words"),
             "Expected to find 'multi word' snippet in completions"
         );
     });

crates/editor/src/test/editor_test_context.rs šŸ”—

@@ -58,6 +58,17 @@ impl EditorTestContext {
             })
             .await
             .unwrap();
+
+        let language = project
+            .read_with(cx, |project, cx| {
+                project.languages().language_for_name("Plain Text")
+            })
+            .await
+            .unwrap();
+        buffer.update(cx, |buffer, cx| {
+            buffer.set_language(Some(language), cx);
+        });
+
         let editor = cx.add_window(|window, cx| {
             let editor = build_editor_with_project(
                 project,

crates/inspector_ui/src/div_inspector.rs šŸ”—

@@ -665,6 +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, // todo! is this right?
                     icon_path: None,
                     documentation: method.documentation.map(|documentation| {
                         CompletionDocumentation::MultiLineMarkdown(documentation.into())

crates/keymap_editor/src/keymap_editor.rs šŸ”—

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

crates/project/src/lsp_store.rs šŸ”—

@@ -9568,6 +9568,7 @@ impl LspStore {
                     source: completion.source,
                     documentation: None,
                     label: CodeLabel::default(),
+                    buffer_match: None,
                     insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
@@ -12052,6 +12053,7 @@ async fn populate_labels_for_completions(
                     source: completion.source,
                     icon_path: None,
                     confirm: None,
+                    buffer_match: None,
                 });
             }
             None => {
@@ -12066,6 +12068,7 @@ async fn populate_labels_for_completions(
                     insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
+                    buffer_match: None,
                 });
             }
         }

crates/project/src/project.rs šŸ”—

@@ -27,6 +27,7 @@ mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
 pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
+use fuzzy::StringMatch;
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
 pub mod search_history;
@@ -456,6 +457,8 @@ 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>,
     /// Whether to adjust indentation (the default) or not.
     pub insert_text_mode: Option<InsertTextMode>,
     /// An optional callback to invoke when this completion is confirmed.