Detailed changes
@@ -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();
@@ -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(),
@@ -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,
@@ -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,
@@ -305,6 +305,7 @@ impl CompletionBuilder {
icon_path: None,
insert_text_mode: None,
confirm: None,
+ buffer_match: None,
}
}
}
@@ -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 }
@@ -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()),
})
}),
);
@@ -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"
);
});
@@ -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,
@@ -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())
@@ -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,
})
@@ -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,
});
}
}
@@ -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.