# Objective Closes #61646. For code completions, Zed currently fuzzy-matches against `CodeLabel::filter_text()`: https://github.com/zed-industries/zed/blob/d4010e91ccc9635853698af160f15bb479a484d0/crates/editor/src/code_context_menus.rs#L337-L343 `CodeLabel::filter_text()` is a substring of `CodeLabel.text`, which is essentially the text itself. `CodeLabel.text` is constructed by Zed's per-language adapters from the `label` and `detail` fields of the completion items returned by the language server — the exact construction differs from adapter to adapter, but the source data is the same. In effect, `CodeLabel.text` ≈ `label` + `detail`. Zed therefore filters on the server-returned `label` and `detail`, while the server-returned `filterText` field is silently ignored.Per the LSP spec: ``` /** * A string that should be used when filtering a set of * completion items. When omitted, the label is used as the * filter text for this item. */ filterText?: string; ``` we should use `filterText` when it is provided. Normally, language servers populate `filterText` as a substring of `label`, so the current behavior works fine. But for certain language servers or functions, `filterText` can be entirely unrelated to `label` and `detail`. For example, for `std::path::Path::parent()` in Rust, rust-analyzer returns: ```json { "label": "parent()", "labelDetails": { "detail": "(alias dirname)", "description": "fn(&self) -> Option<&Path>" }, "kind": 2, "preselect": true, "sortText": "7ffffff6", "filterText": "parentdirname", ... } ``` Typing `dirname` therefore never surfaces this completion. The root design issue behind this bug is that `CodeLabel` is not well-suited to filtering LSP completions. ## Solution `CodeLabel` and its related methods are kept untouched: the struct is reused across the repo and is only unsuitable for filtering LSP completions. Instead, the changes are made in `CompletionSource` and `Completion`, each gaining a `filter_text()` method: - `CompletionSource::filter_text()` handles LSP completions, returning the server-provided `filterText` and falling back to the `label` when `filterText` is absent. - `Completion::filter_text()` is the general entry point used for fuzzy matching; for non-LSP completions it falls back to the existing `label.filter_text()`. The fuzzy match target is switched from `CodeLabel::filter_text()` to `Completion::filter_text()` — that is the core change. Since the fuzzy match target is no longer guaranteed to be a substring of the displayed `CodeLabel.text`, the matched characters no longer have a direct position in the displayed text to highlight. Bold highlights are therefore only rendered when `CodeLabel::filter_text()` equals `Completion::filter_text()`. This is a safe choice, though not an ideal one. ## Testing Added a new GPUI test covering the new behavior; also built and tested with a before/after comparison, attached in the Showcase section. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase | Before | After | |:--:|:--:| | <img width="708" height="308" alt="Before" src="https://github.com/user-attachments/assets/ca2e3820-7ea1-4dcc-a91f-28aab71aecc5" /> | <img width="696" height="248" alt="After" src="https://github.com/user-attachments/assets/334e240b-64d5-495b-aef6-772456b993ba" /> | --- Release Notes: - Improved completion filtering for lsp completions.
520 lines
18 KiB
Rust
520 lines
18 KiB
Rust
use crate::code_context_menus::CompletionsMenu;
|
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
|
use gpui::TestAppContext;
|
|
use language::CodeLabel;
|
|
use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
|
|
use project::{Completion, CompletionSource};
|
|
use settings::SnippetSortOrder;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use text::{Anchor, BufferId};
|
|
|
|
#[gpui::test]
|
|
async fn test_sort_kind(cx: &mut TestAppContext) {
|
|
let completions = vec![
|
|
CompletionBuilder::function("floorf128", None, "80000000"),
|
|
CompletionBuilder::constant("foo_bar_baz", None, "80000000"),
|
|
CompletionBuilder::variable("foo_bar_qux", None, "80000000"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
|
|
|
|
// variable takes precedence over constant
|
|
// constant take precedence over function
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]
|
|
);
|
|
|
|
// fuzzy score should match for first two items as query is common prefix
|
|
assert_eq!(matches[0].score, matches[1].score);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_fuzzy_score(cx: &mut TestAppContext) {
|
|
// first character sensitive over sort_text and sort_kind
|
|
{
|
|
let completions = vec![
|
|
CompletionBuilder::variable("element_type", None, "7ffffffe"),
|
|
CompletionBuilder::constant("ElementType", None, "7fffffff"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["ElementType", "element_type"]
|
|
);
|
|
assert!(matches[0].score > matches[1].score);
|
|
}
|
|
|
|
// fuzzy takes over sort_text and sort_kind
|
|
{
|
|
let completions = vec![
|
|
CompletionBuilder::function("onAbort?", None, "12"),
|
|
CompletionBuilder::function("onAuxClick?", None, "12"),
|
|
CompletionBuilder::variable("onPlay?", None, "12"),
|
|
CompletionBuilder::variable("onLoad?", None, "12"),
|
|
CompletionBuilder::variable("onDrag?", None, "12"),
|
|
CompletionBuilder::function("onPause?", None, "10"),
|
|
CompletionBuilder::function("onPaste?", None, "10"),
|
|
CompletionBuilder::function("onAnimationEnd?", None, "12"),
|
|
CompletionBuilder::function("onAbortCapture?", None, "12"),
|
|
CompletionBuilder::constant("onChange?", None, "12"),
|
|
CompletionBuilder::constant("onWaiting?", None, "12"),
|
|
CompletionBuilder::function("onCanPlay?", None, "12"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
|
|
for i in 0..4 {
|
|
assert!(matches[i].string.to_lowercase().starts_with("ona"));
|
|
}
|
|
}
|
|
|
|
// plain fuzzy prefix match
|
|
{
|
|
let completions = vec![
|
|
CompletionBuilder::function("set_text", None, "7fffffff"),
|
|
CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),
|
|
CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),
|
|
CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),
|
|
CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),
|
|
CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
|
|
assert_eq!(matches[0].string, "set_text");
|
|
assert_eq!(matches[1].string, "set_text_style_refinement");
|
|
assert_eq!(matches[2].string, "set_placeholder_text");
|
|
}
|
|
|
|
// fuzzy filter text over label, sort_text and sort_kind
|
|
{
|
|
// Case 1: "awa"
|
|
let completions = vec![
|
|
CompletionBuilder::method("await", Some("await"), "7fffffff"),
|
|
CompletionBuilder::method("await.ne", Some("ne"), "80000010"),
|
|
CompletionBuilder::method("await.eq", Some("eq"), "80000010"),
|
|
CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),
|
|
CompletionBuilder::method("await.zip", Some("zip"), "80000006"),
|
|
CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),
|
|
CompletionBuilder::method("await.and", Some("and"), "80000006"),
|
|
CompletionBuilder::method("await.map", Some("map"), "80000006"),
|
|
];
|
|
|
|
test_for_each_prefix("await", &completions, cx, |matches| {
|
|
// for each prefix, first item should always be one with lower sort_text
|
|
assert_eq!(matches[0].string, "await");
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_sort_text(cx: &mut TestAppContext) {
|
|
// sort text takes precedance over sort_kind, when fuzzy is same
|
|
{
|
|
let completions = vec![
|
|
CompletionBuilder::variable("unreachable", None, "80000000"),
|
|
CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),
|
|
CompletionBuilder::function("unchecked_rem", None, "80000010"),
|
|
CompletionBuilder::function("unreachable_unchecked", None, "80000020"),
|
|
];
|
|
|
|
test_for_each_prefix("unreachabl", &completions, cx, |matches| {
|
|
// for each prefix, first item should always be one with lower sort_text
|
|
assert_eq!(matches[0].string, "unreachable!(…)");
|
|
assert_eq!(matches[1].string, "unreachable");
|
|
|
|
// fuzzy score should match for first two items as query is common prefix
|
|
assert_eq!(matches[0].score, matches[1].score);
|
|
})
|
|
.await;
|
|
|
|
let matches =
|
|
filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
|
|
// exact match comes first
|
|
assert_eq!(matches[0].string, "unreachable");
|
|
assert_eq!(matches[1].string, "unreachable!(…)");
|
|
|
|
// fuzzy score should match for first two items as query is common prefix
|
|
assert_eq!(matches[0].score, matches[1].score);
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_sort_snippet(cx: &mut TestAppContext) {
|
|
let completions = vec![
|
|
CompletionBuilder::constant("println", None, "7fffffff"),
|
|
CompletionBuilder::snippet("println!(…)", None, "80000000"),
|
|
];
|
|
let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
|
|
|
|
// snippet take precedence over sort_text and sort_kind
|
|
assert_eq!(matches[0].string, "println!(…)");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_sort_exact(cx: &mut TestAppContext) {
|
|
// sort_text takes over if no exact match
|
|
let completions = vec![
|
|
CompletionBuilder::function("into", None, "80000004"),
|
|
CompletionBuilder::function("try_into", None, "80000004"),
|
|
CompletionBuilder::snippet("println", None, "80000004"),
|
|
CompletionBuilder::function("clone_into", None, "80000004"),
|
|
CompletionBuilder::function("into_searcher", None, "80000000"),
|
|
CompletionBuilder::snippet("eprintln", None, "80000004"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(matches[0].string, "into_searcher");
|
|
|
|
// exact match takes over sort_text
|
|
let completions = vec![
|
|
CompletionBuilder::function("into", None, "80000004"),
|
|
CompletionBuilder::function("try_into", None, "80000004"),
|
|
CompletionBuilder::function("clone_into", None, "80000004"),
|
|
CompletionBuilder::function("into_searcher", None, "80000000"),
|
|
CompletionBuilder::function("split_terminator", None, "7fffffff"),
|
|
CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
|
|
];
|
|
let matches =
|
|
filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(matches[0].string, "into");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_sort_positions(cx: &mut TestAppContext) {
|
|
// positions take precedence over fuzzy score and sort_text
|
|
let completions = vec![
|
|
CompletionBuilder::function("rounded-full", None, "15788"),
|
|
CompletionBuilder::variable("rounded-t-full", None, "15846"),
|
|
CompletionBuilder::variable("rounded-b-full", None, "15731"),
|
|
CompletionBuilder::function("rounded-tr-full", None, "15866"),
|
|
];
|
|
|
|
let matches = filter_and_sort_matches(
|
|
"rounded-full",
|
|
&completions,
|
|
SnippetSortOrder::default(),
|
|
cx,
|
|
)
|
|
.await;
|
|
assert_eq!(matches[0].string, "rounded-full");
|
|
|
|
let matches =
|
|
filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(matches[0].string, "rounded-full");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_case_sensitive_match_tie_breaker(cx: &mut TestAppContext) {
|
|
let completions = vec![
|
|
CompletionBuilder::variable("abc", None, "11"),
|
|
CompletionBuilder::variable("ABC", None, "11"),
|
|
];
|
|
|
|
let matches = filter_and_sort_matches("a", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["abc", "ABC"]
|
|
);
|
|
|
|
let matches = filter_and_sort_matches("A", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["ABC", "abc"]
|
|
);
|
|
|
|
let matches =
|
|
filter_and_sort_matches("ab", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["abc", "ABC"]
|
|
);
|
|
|
|
let matches =
|
|
filter_and_sort_matches("AB", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["ABC", "abc"]
|
|
);
|
|
|
|
let completions = vec![
|
|
CompletionBuilder::variable("aBc", None, "11"),
|
|
CompletionBuilder::variable("Abc", None, "11"),
|
|
];
|
|
|
|
let matches =
|
|
filter_and_sort_matches("Ab", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["Abc", "aBc"]
|
|
);
|
|
|
|
let matches =
|
|
filter_and_sort_matches("aB", &completions, SnippetSortOrder::default(), cx).await;
|
|
assert_eq!(
|
|
matches
|
|
.iter()
|
|
.map(|m| m.string.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["aBc", "Abc"]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
|
|
let completions = vec![
|
|
CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
|
|
CompletionBuilder::function(
|
|
"language_servers_running_disk_based_diagnostics",
|
|
None,
|
|
"7fffffff",
|
|
), // 0.168 fuzzy score
|
|
CompletionBuilder::function("code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
|
CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
|
CompletionBuilder::function("fetch_code_lens", None, "7fffffff"), // 3.2 fuzzy score
|
|
];
|
|
|
|
let matches =
|
|
filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
|
|
|
|
assert_eq!(matches[0].string, "code_lens");
|
|
assert_eq!(matches[1].string, "lsp_code_lens");
|
|
assert_eq!(matches[2].string, "fetch_code_lens");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_semver_label_sort_by_latest_version(cx: &mut TestAppContext) {
|
|
let mut versions = [
|
|
"10.4.112",
|
|
"10.4.22",
|
|
"10.4.2",
|
|
"10.4.20",
|
|
"10.4.21",
|
|
"10.4.12",
|
|
// Pre-release versions
|
|
"10.4.22-alpha",
|
|
"10.4.22-beta.1",
|
|
"10.4.22-rc.1",
|
|
// Build metadata versions
|
|
"10.4.21+build.123",
|
|
"10.4.20+20210327",
|
|
];
|
|
versions.sort_by(|a, b| {
|
|
match (
|
|
semver::Version::parse(a).ok(),
|
|
semver::Version::parse(b).ok(),
|
|
) {
|
|
(Some(a_ver), Some(b_ver)) => b_ver.cmp(&a_ver),
|
|
_ => std::cmp::Ordering::Equal,
|
|
}
|
|
});
|
|
let completions: Vec<_> = versions
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, version)| {
|
|
// This sort text would come from the LSP
|
|
let sort_text = format!("{:08}", i);
|
|
CompletionBuilder::new(version, None, &sort_text, None)
|
|
})
|
|
.collect();
|
|
|
|
// Case 1: User types just the major and minor version
|
|
let matches =
|
|
filter_and_sort_matches("10.4.", &completions, SnippetSortOrder::default(), cx).await;
|
|
// Versions are ordered by recency (latest first)
|
|
let expected_versions = [
|
|
"10.4.112",
|
|
"10.4.22",
|
|
"10.4.22-rc.1",
|
|
"10.4.22-beta.1",
|
|
"10.4.22-alpha",
|
|
"10.4.21+build.123",
|
|
"10.4.21",
|
|
"10.4.20+20210327",
|
|
"10.4.20",
|
|
"10.4.12",
|
|
"10.4.2",
|
|
];
|
|
for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
|
|
assert_eq!(match_item.string.as_ref() as &str, *expected);
|
|
}
|
|
|
|
// Case 2: User types the major, minor, and patch version
|
|
let matches =
|
|
filter_and_sort_matches("10.4.2", &completions, SnippetSortOrder::default(), cx).await;
|
|
let expected_versions = [
|
|
// Exact match comes first
|
|
"10.4.2",
|
|
// Ordered by recency with exact major, minor, and patch versions
|
|
"10.4.22",
|
|
"10.4.22-rc.1",
|
|
"10.4.22-beta.1",
|
|
"10.4.22-alpha",
|
|
"10.4.21+build.123",
|
|
"10.4.21",
|
|
"10.4.20+20210327",
|
|
"10.4.20",
|
|
// Versions with non-exact patch versions are ordered by fuzzy score
|
|
// Higher fuzzy score than 112 patch version since "2" appears before "1"
|
|
// in "12", making it rank higher than "112"
|
|
"10.4.12",
|
|
"10.4.112",
|
|
];
|
|
for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
|
|
assert_eq!(match_item.string.as_ref() as &str, *expected);
|
|
}
|
|
}
|
|
|
|
async fn test_for_each_prefix<F>(
|
|
target: &str,
|
|
completions: &Vec<Completion>,
|
|
cx: &mut TestAppContext,
|
|
mut test_fn: F,
|
|
) where
|
|
F: FnMut(Vec<StringMatch>),
|
|
{
|
|
for i in 1..=target.len() {
|
|
let prefix = &target[..i];
|
|
let matches =
|
|
filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
|
|
test_fn(matches);
|
|
}
|
|
}
|
|
|
|
struct CompletionBuilder;
|
|
|
|
impl CompletionBuilder {
|
|
fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
|
|
Self::new(
|
|
label,
|
|
filter_text,
|
|
sort_text,
|
|
Some(CompletionItemKind::CONSTANT),
|
|
)
|
|
}
|
|
|
|
fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
|
|
Self::new(
|
|
label,
|
|
filter_text,
|
|
sort_text,
|
|
Some(CompletionItemKind::FUNCTION),
|
|
)
|
|
}
|
|
|
|
fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
|
|
Self::new(
|
|
label,
|
|
filter_text,
|
|
sort_text,
|
|
Some(CompletionItemKind::METHOD),
|
|
)
|
|
}
|
|
|
|
fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
|
|
Self::new(
|
|
label,
|
|
filter_text,
|
|
sort_text,
|
|
Some(CompletionItemKind::VARIABLE),
|
|
)
|
|
}
|
|
|
|
fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
|
|
Self::new(
|
|
label,
|
|
filter_text,
|
|
sort_text,
|
|
Some(CompletionItemKind::SNIPPET),
|
|
)
|
|
}
|
|
|
|
fn new(
|
|
label: &str,
|
|
filter_text: Option<&str>,
|
|
sort_text: &str,
|
|
kind: Option<CompletionItemKind>,
|
|
) -> Completion {
|
|
Completion {
|
|
replace_range: Anchor::min_max_range_for_buffer(BufferId::new(1).unwrap()),
|
|
new_text: label.to_string(),
|
|
label: CodeLabel::plain(label.to_string(), filter_text),
|
|
documentation: None,
|
|
source: CompletionSource::Lsp {
|
|
insert_range: None,
|
|
server_id: LanguageServerId(0),
|
|
lsp_completion: Box::new(CompletionItem {
|
|
label: label.to_string(),
|
|
kind: kind,
|
|
sort_text: Some(sort_text.to_string()),
|
|
filter_text: filter_text.map(|text| text.to_string()),
|
|
..Default::default()
|
|
}),
|
|
lsp_defaults: None,
|
|
resolved: false,
|
|
},
|
|
icon_path: None,
|
|
icon_color: None,
|
|
insert_text_mode: None,
|
|
confirm: None,
|
|
match_start: None,
|
|
snippet_deduplication_key: None,
|
|
group: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn filter_and_sort_matches(
|
|
query: &str,
|
|
completions: &Vec<Completion>,
|
|
snippet_sort_order: SnippetSortOrder,
|
|
cx: &mut TestAppContext,
|
|
) -> Vec<StringMatch> {
|
|
let candidates: Arc<[StringMatchCandidate]> = completions
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(id, completion)| StringMatchCandidate::new(id, completion.filter_text()))
|
|
.collect();
|
|
let cancel_flag = Arc::new(AtomicBool::new(false));
|
|
let background_executor = cx.executor();
|
|
let matches = fuzzy::match_strings(
|
|
&candidates,
|
|
query,
|
|
query.chars().any(|c| c.is_uppercase()),
|
|
false,
|
|
100,
|
|
&cancel_flag,
|
|
background_executor,
|
|
)
|
|
.await;
|
|
CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
|
|
}
|