logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
28
crates/fuzzy_nucleo/Cargo.toml
Normal file
28
crates/fuzzy_nucleo/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "fuzzy_nucleo"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/fuzzy_nucleo.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
fuzzy.workspace = true
|
||||
nucleo.workspace = true
|
||||
gpui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[[bench]]
|
||||
name = "match_benchmark"
|
||||
harness = false
|
||||
1
crates/fuzzy_nucleo/LICENSE-GPL
Symbolic link
1
crates/fuzzy_nucleo/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
340
crates/fuzzy_nucleo/benches/match_benchmark.rs
Normal file
340
crates/fuzzy_nucleo/benches/match_benchmark.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||
use fuzzy::CharBag;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use util::{paths::PathStyle, rel_path::RelPath};
|
||||
|
||||
const DIRS: &[&str] = &[
|
||||
"src",
|
||||
"crates/gpui/src",
|
||||
"crates/editor/src",
|
||||
"crates/fuzzy_nucleo/src",
|
||||
"crates/workspace/src",
|
||||
"crates/project/src",
|
||||
"crates/language/src",
|
||||
"crates/terminal/src",
|
||||
"crates/assistant/src",
|
||||
"crates/theme/src",
|
||||
"tests/integration",
|
||||
"tests/unit",
|
||||
"docs/architecture",
|
||||
"scripts",
|
||||
"assets/icons",
|
||||
"assets/fonts",
|
||||
"crates/git/src",
|
||||
"crates/rpc/src",
|
||||
"crates/settings/src",
|
||||
"crates/diagnostics/src",
|
||||
"crates/search/src",
|
||||
"crates/collab/src",
|
||||
"crates/db/src",
|
||||
"crates/lsp/src",
|
||||
];
|
||||
|
||||
const FILENAMES: &[&str] = &[
|
||||
"parser.rs",
|
||||
"main.rs",
|
||||
"executor.rs",
|
||||
"editor.rs",
|
||||
"strings.rs",
|
||||
"workspace.rs",
|
||||
"project.rs",
|
||||
"buffer.rs",
|
||||
"colors.rs",
|
||||
"panel.rs",
|
||||
"renderer.rs",
|
||||
"dispatcher.rs",
|
||||
"matcher.rs",
|
||||
"paths.rs",
|
||||
"context.rs",
|
||||
"toolbar.rs",
|
||||
"statusbar.rs",
|
||||
"keymap.rs",
|
||||
"config.rs",
|
||||
"settings.rs",
|
||||
"diagnostics.rs",
|
||||
"completion.rs",
|
||||
"hover.rs",
|
||||
"references.rs",
|
||||
"inlay_hints.rs",
|
||||
"git_blame.rs",
|
||||
"terminal.rs",
|
||||
"search.rs",
|
||||
"replace.rs",
|
||||
"outline.rs",
|
||||
"breadcrumbs.rs",
|
||||
"tab_bar.rs",
|
||||
"Cargo.toml",
|
||||
"README.md",
|
||||
"build.sh",
|
||||
"LICENSE",
|
||||
"overview.md",
|
||||
"string_helpers.rs",
|
||||
"test_helpers.rs",
|
||||
"fixtures.json",
|
||||
"schema.sql",
|
||||
];
|
||||
|
||||
const QUERY_WORDS: &[&str] = &[
|
||||
"par",
|
||||
"edi",
|
||||
"buf",
|
||||
"set",
|
||||
"mat",
|
||||
"con",
|
||||
"ren",
|
||||
"dis",
|
||||
"sea",
|
||||
"ter",
|
||||
"col",
|
||||
"hov",
|
||||
"out",
|
||||
"rep",
|
||||
"key",
|
||||
"too",
|
||||
"pan",
|
||||
"str",
|
||||
"dia",
|
||||
"com",
|
||||
"executor",
|
||||
"workspace",
|
||||
"settings",
|
||||
"terminal",
|
||||
"breadcrumbs",
|
||||
"git_blame",
|
||||
"fixtures",
|
||||
"schema",
|
||||
"config",
|
||||
"toolbar",
|
||||
];
|
||||
|
||||
/// Deterministic query generation from QUERY_WORDS using a simple LCG.
|
||||
/// Returns `count` queries of each arity: 1, 2, and 4 space-separated words.
|
||||
fn generate_queries(count: usize) -> (Vec<String>, Vec<String>, Vec<String>) {
|
||||
let mut state: u64 = 0xDEAD_BEEF;
|
||||
let mut next = || -> usize {
|
||||
// LCG: simple, fast, deterministic
|
||||
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
(state >> 33) as usize
|
||||
};
|
||||
let mut n_word = |n: usize| -> Vec<String> {
|
||||
(0..count)
|
||||
.map(|_| {
|
||||
(0..n)
|
||||
.map(|_| QUERY_WORDS[next() % QUERY_WORDS.len()])
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
(n_word(1), n_word(2), n_word(4))
|
||||
}
|
||||
|
||||
fn generate_candidates(count: usize) -> Vec<fuzzy_nucleo::StringMatchCandidate> {
|
||||
(0..count)
|
||||
.map(|id| {
|
||||
let dir = DIRS[id % DIRS.len()];
|
||||
let file = FILENAMES[id / DIRS.len() % FILENAMES.len()];
|
||||
fuzzy_nucleo::StringMatchCandidate::new(id, &format!("{dir}/{file}"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_fuzzy_candidates(
|
||||
candidates: &[fuzzy_nucleo::StringMatchCandidate],
|
||||
) -> Vec<fuzzy::StringMatchCandidate> {
|
||||
candidates
|
||||
.iter()
|
||||
.map(|c| fuzzy::StringMatchCandidate::new(c.id, c.string.as_ref()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bench_string_matching(criterion: &mut Criterion) {
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
let dispatcher = std::sync::Arc::new(gpui::TestDispatcher::new(0));
|
||||
let background_executor = gpui::BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = gpui::ForegroundExecutor::new(dispatcher);
|
||||
|
||||
let sizes = [100, 1000, 10_000];
|
||||
let query_count = 200;
|
||||
let (q1, q2, q4) = generate_queries(query_count);
|
||||
|
||||
for (label, queries) in [("1-word", &q1), ("2-word", &q2), ("4-word", &q4)] {
|
||||
let mut group = criterion.benchmark_group(label);
|
||||
for size in sizes {
|
||||
let candidates = generate_candidates(size);
|
||||
let fuzzy_candidates = to_fuzzy_candidates(&candidates);
|
||||
|
||||
let mut query_idx = 0usize;
|
||||
group.bench_function(BenchmarkId::new("nucleo", size), |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let query = queries[query_idx % queries.len()].as_str();
|
||||
query_idx += 1;
|
||||
query
|
||||
},
|
||||
|query| {
|
||||
foreground_executor.block_on(fuzzy_nucleo::match_strings_async(
|
||||
&candidates,
|
||||
query,
|
||||
fuzzy_nucleo::Case::Ignore,
|
||||
fuzzy_nucleo::LengthPenalty::On,
|
||||
size,
|
||||
&cancel,
|
||||
background_executor.clone(),
|
||||
))
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
let mut query_idx = 0usize;
|
||||
group.bench_function(BenchmarkId::new("fuzzy", size), |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let query = queries[query_idx % queries.len()].as_str();
|
||||
query_idx += 1;
|
||||
query
|
||||
},
|
||||
|query| {
|
||||
foreground_executor.block_on(fuzzy::match_strings(
|
||||
&fuzzy_candidates,
|
||||
query,
|
||||
false,
|
||||
true,
|
||||
size,
|
||||
&cancel,
|
||||
background_executor.clone(),
|
||||
))
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_path_strings(count: usize) -> &'static [String] {
|
||||
let paths: Box<[String]> = (0..count)
|
||||
.map(|id| {
|
||||
let dir = DIRS[id % DIRS.len()];
|
||||
let file = FILENAMES[id / DIRS.len() % FILENAMES.len()];
|
||||
format!("{dir}/{file}")
|
||||
})
|
||||
.collect();
|
||||
Box::leak(paths)
|
||||
}
|
||||
|
||||
fn generate_nucleo_path_candidates(
|
||||
paths: &'static [String],
|
||||
) -> Vec<fuzzy_nucleo::PathMatchCandidate<'static>> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
fuzzy_nucleo::PathMatchCandidate::new(RelPath::unix(path).unwrap(), false, None)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_fuzzy_path_candidates(
|
||||
paths: &'static [String],
|
||||
) -> Vec<fuzzy::PathMatchCandidate<'static>> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| fuzzy::PathMatchCandidate {
|
||||
is_dir: false,
|
||||
path: RelPath::unix(path).unwrap(),
|
||||
char_bag: CharBag::from(path.as_str()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn capitalize_each_word(query: &str) -> String {
|
||||
query
|
||||
.split_whitespace()
|
||||
.map(|w| {
|
||||
let mut chars = w.chars();
|
||||
match chars.next() {
|
||||
Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn bench_path_matching(criterion: &mut Criterion) {
|
||||
let sizes = [100, 1000, 10_000];
|
||||
let all_path_strings = sizes.map(generate_path_strings);
|
||||
let query_count = 200;
|
||||
let (q1, q2, q4) = generate_queries(query_count);
|
||||
let q1_upper: Vec<String> = q1.iter().map(|q| capitalize_each_word(q)).collect();
|
||||
let q2_upper: Vec<String> = q2.iter().map(|q| capitalize_each_word(q)).collect();
|
||||
let q4_upper: Vec<String> = q4.iter().map(|q| capitalize_each_word(q)).collect();
|
||||
|
||||
for (label, queries, case) in [
|
||||
("path/1-word", &q1, fuzzy_nucleo::Case::Ignore),
|
||||
("path/2-word", &q2, fuzzy_nucleo::Case::Ignore),
|
||||
("path/4-word", &q4, fuzzy_nucleo::Case::Ignore),
|
||||
("path_smart/1-word", &q1_upper, fuzzy_nucleo::Case::Smart),
|
||||
("path_smart/2-word", &q2_upper, fuzzy_nucleo::Case::Smart),
|
||||
("path_smart/4-word", &q4_upper, fuzzy_nucleo::Case::Smart),
|
||||
] {
|
||||
let mut group = criterion.benchmark_group(label);
|
||||
for (size_index, &size) in sizes.iter().enumerate() {
|
||||
let path_strings = all_path_strings[size_index];
|
||||
|
||||
let mut query_idx = 0usize;
|
||||
group.bench_function(BenchmarkId::new("nucleo", size), |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let query = queries[query_idx % queries.len()].as_str();
|
||||
query_idx += 1;
|
||||
(generate_nucleo_path_candidates(path_strings), query)
|
||||
},
|
||||
|(candidates, query)| {
|
||||
fuzzy_nucleo::match_fixed_path_set(
|
||||
candidates,
|
||||
0,
|
||||
None,
|
||||
query,
|
||||
case,
|
||||
size,
|
||||
PathStyle::Posix,
|
||||
)
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
let mut query_idx = 0usize;
|
||||
group.bench_function(BenchmarkId::new("fuzzy", size), |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let query = queries[query_idx % queries.len()].as_str();
|
||||
query_idx += 1;
|
||||
(generate_fuzzy_path_candidates(path_strings), query)
|
||||
},
|
||||
|(candidates, query)| {
|
||||
fuzzy::match_fixed_path_set(
|
||||
candidates,
|
||||
0,
|
||||
None,
|
||||
query,
|
||||
false,
|
||||
size,
|
||||
PathStyle::Posix,
|
||||
)
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_string_matching, bench_path_matching);
|
||||
criterion_main!(benches);
|
||||
142
crates/fuzzy_nucleo/src/fuzzy_nucleo.rs
Normal file
142
crates/fuzzy_nucleo/src/fuzzy_nucleo.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
mod matcher;
|
||||
mod paths;
|
||||
mod strings;
|
||||
|
||||
use fuzzy::CharBag;
|
||||
use nucleo::pattern::{AtomKind, CaseMatching, Normalization, Pattern};
|
||||
|
||||
pub use paths::{
|
||||
PathMatch, PathMatchCandidate, PathMatchCandidateSet, match_fixed_path_set, match_path_sets,
|
||||
};
|
||||
pub use strings::{StringMatch, StringMatchCandidate, match_strings, match_strings_async};
|
||||
|
||||
pub(crate) struct Cancelled;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Case {
|
||||
Smart,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl Case {
|
||||
pub fn smart_if_uppercase_in(query: &str) -> Self {
|
||||
if query.chars().any(|c| c.is_uppercase()) {
|
||||
Self::Smart
|
||||
} else {
|
||||
Self::Ignore
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_smart(self) -> bool {
|
||||
matches!(self, Self::Smart)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LengthPenalty {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl LengthPenalty {
|
||||
pub fn from_bool(on: bool) -> Self {
|
||||
if on { Self::On } else { Self::Off }
|
||||
}
|
||||
|
||||
pub fn is_on(self) -> bool {
|
||||
matches!(self, Self::On)
|
||||
}
|
||||
}
|
||||
|
||||
// Matching is always case-insensitive at the nucleo level — using
|
||||
// `CaseMatching::Smart` there would *reject* candidates whose capitalization
|
||||
// doesn't match the query, breaking pickers like the command palette
|
||||
// (`"Editor: Backspace"` against the action named `"editor: backspace"`).
|
||||
// `Case::Smart` is honored as a *scoring hint* instead: when the query
|
||||
// contains uppercase, candidates whose matched characters disagree in case
|
||||
// are downranked by a per-mismatch penalty rather than dropped.
|
||||
pub(crate) struct Query {
|
||||
pub(crate) pattern: Pattern,
|
||||
/// Non-whitespace query chars in input order, populated only when a smart-case
|
||||
/// penalty will actually be charged. Aligns 1:1 with the indices appended by
|
||||
/// `Pattern::indices` (atom-order, needle-order within each atom).
|
||||
pub(crate) query_chars: Option<Vec<char>>,
|
||||
pub(crate) char_bag: CharBag,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub(crate) fn build(query: &str, case: Case) -> Option<Self> {
|
||||
if query.chars().all(char::is_whitespace) {
|
||||
return None;
|
||||
}
|
||||
let normalized = query.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let pattern = Pattern::new(
|
||||
&normalized,
|
||||
CaseMatching::Ignore,
|
||||
Normalization::Smart,
|
||||
AtomKind::Fuzzy,
|
||||
);
|
||||
let wants_case_penalty = case.is_smart() && query.chars().any(|c| c.is_uppercase());
|
||||
let query_chars =
|
||||
wants_case_penalty.then(|| query.chars().filter(|c| !c.is_whitespace()).collect());
|
||||
Some(Query {
|
||||
pattern,
|
||||
query_chars,
|
||||
char_bag: CharBag::from(query),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn count_case_mismatches(
|
||||
query_chars: Option<&[char]>,
|
||||
matched_chars: &[u32],
|
||||
candidate: &str,
|
||||
candidate_chars: &mut Vec<char>,
|
||||
) -> u32 {
|
||||
let Some(query_chars) = query_chars else {
|
||||
return 0;
|
||||
};
|
||||
if query_chars.len() != matched_chars.len() {
|
||||
return 0;
|
||||
}
|
||||
candidate_chars.clear();
|
||||
candidate_chars.extend(candidate.chars());
|
||||
let mut mismatches: u32 = 0;
|
||||
for (&query_char, &pos) in query_chars.iter().zip(matched_chars) {
|
||||
if let Some(&candidate_char) = candidate_chars.get(pos as usize)
|
||||
&& candidate_char != query_char
|
||||
&& candidate_char.eq_ignore_ascii_case(&query_char)
|
||||
{
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
mismatches
|
||||
}
|
||||
|
||||
const SMART_CASE_PENALTY_PER_MISMATCH: f64 = 0.9;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn case_penalty(mismatches: u32) -> f64 {
|
||||
if mismatches == 0 {
|
||||
1.0
|
||||
} else {
|
||||
SMART_CASE_PENALTY_PER_MISMATCH.powi(mismatches as i32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct byte-offset match positions from a list of matched char offsets
|
||||
/// that is already sorted ascending and deduplicated.
|
||||
pub(crate) fn positions_from_sorted(s: &str, sorted_char_indices: &[u32]) -> Vec<usize> {
|
||||
let mut iter = sorted_char_indices.iter().copied().peekable();
|
||||
let mut out = Vec::with_capacity(sorted_char_indices.len());
|
||||
for (char_offset, (byte_offset, _)) in s.char_indices().enumerate() {
|
||||
if iter.peek().is_none() {
|
||||
break;
|
||||
}
|
||||
if iter.next_if(|&m| m == char_offset as u32).is_some() {
|
||||
out.push(byte_offset);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
52
crates/fuzzy_nucleo/src/matcher.rs
Normal file
52
crates/fuzzy_nucleo/src/matcher.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
static MATCHERS: Mutex<Vec<nucleo::Matcher>> = Mutex::new(Vec::new());
|
||||
|
||||
pub const LENGTH_PENALTY: f64 = 0.01;
|
||||
|
||||
fn pool_cap() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(8)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
pub fn get_matcher(config: nucleo::Config) -> nucleo::Matcher {
|
||||
let mut matchers = MATCHERS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match matchers.pop() {
|
||||
Some(mut matcher) => {
|
||||
matcher.config = config;
|
||||
matcher
|
||||
}
|
||||
None => nucleo::Matcher::new(config),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn return_matcher(matcher: nucleo::Matcher) {
|
||||
let mut pool = MATCHERS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if pool.len() < pool_cap() {
|
||||
pool.push(matcher);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_matchers(n: usize, config: nucleo::Config) -> Vec<nucleo::Matcher> {
|
||||
let mut matchers: Vec<_> = {
|
||||
let mut pool = MATCHERS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let available = pool.len().min(n);
|
||||
pool.drain(..available)
|
||||
.map(|mut matcher| {
|
||||
matcher.config = config.clone();
|
||||
matcher
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
matchers.resize_with(n, || nucleo::Matcher::new(config.clone()));
|
||||
matchers
|
||||
}
|
||||
|
||||
pub fn return_matchers(matchers: Vec<nucleo::Matcher>) {
|
||||
let cap = pool_cap();
|
||||
let mut pool = MATCHERS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let space = cap.saturating_sub(pool.len());
|
||||
pool.extend(matchers.into_iter().take(space));
|
||||
}
|
||||
356
crates/fuzzy_nucleo/src/paths.rs
Normal file
356
crates/fuzzy_nucleo/src/paths.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use gpui::BackgroundExecutor;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{self, AtomicBool},
|
||||
},
|
||||
};
|
||||
use util::{paths::PathStyle, rel_path::RelPath};
|
||||
|
||||
use nucleo::Utf32Str;
|
||||
use nucleo::pattern::Pattern;
|
||||
|
||||
use fuzzy::CharBag;
|
||||
|
||||
use crate::matcher::{self, LENGTH_PENALTY};
|
||||
use crate::{Cancelled, Case, Query, case_penalty, count_case_mismatches, positions_from_sorted};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMatchCandidate<'a> {
|
||||
pub is_dir: bool,
|
||||
pub path: &'a RelPath,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
|
||||
impl<'a> PathMatchCandidate<'a> {
|
||||
/// Build a candidate whose prefilter bag covers both the worktree prefix and the path.
|
||||
/// Pass `None` when matching against paths that have no worktree prefix.
|
||||
pub fn new(path: &'a RelPath, is_dir: bool, path_prefix: Option<&RelPath>) -> Self {
|
||||
let mut char_bag = CharBag::default();
|
||||
if let Some(prefix) = path_prefix
|
||||
&& !prefix.is_empty()
|
||||
{
|
||||
char_bag.extend(prefix.as_unix_str().chars().map(|c| c.to_ascii_lowercase()));
|
||||
}
|
||||
char_bag.extend(path.as_unix_str().chars().map(|c| c.to_ascii_lowercase()));
|
||||
Self {
|
||||
is_dir,
|
||||
path,
|
||||
char_bag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMatch {
|
||||
pub score: f64,
|
||||
pub positions: Vec<usize>,
|
||||
pub worktree_id: usize,
|
||||
pub path: Arc<RelPath>,
|
||||
pub path_prefix: Arc<RelPath>,
|
||||
pub is_dir: bool,
|
||||
/// Number of steps removed from a shared parent with the relative path
|
||||
/// Used to order closer paths first in the search list
|
||||
pub distance_to_relative_ancestor: usize,
|
||||
}
|
||||
|
||||
pub trait PathMatchCandidateSet<'a>: Send + Sync {
|
||||
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
|
||||
fn id(&self) -> usize;
|
||||
fn len(&self) -> usize;
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
fn root_is_file(&self) -> bool;
|
||||
fn prefix(&self) -> Arc<RelPath>;
|
||||
fn candidates(&'a self, start: usize) -> Self::Candidates;
|
||||
fn path_style(&self) -> PathStyle;
|
||||
}
|
||||
|
||||
impl PartialEq for PathMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PathMatch {}
|
||||
|
||||
impl PartialOrd for PathMatch {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PathMatch {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.score
|
||||
.total_cmp(&other.score)
|
||||
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
|
||||
.then_with(|| {
|
||||
other
|
||||
.distance_to_relative_ancestor
|
||||
.cmp(&self.distance_to_relative_ancestor)
|
||||
})
|
||||
.then_with(|| self.path.cmp(&other.path))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn distance_between_paths(path: &RelPath, relative_to: &RelPath) -> usize {
|
||||
let mut path_components = path.components();
|
||||
let mut relative_components = relative_to.components();
|
||||
|
||||
while path_components
|
||||
.next()
|
||||
.zip(relative_components.next())
|
||||
.map(|(path_component, relative_component)| path_component == relative_component)
|
||||
.unwrap_or_default()
|
||||
{}
|
||||
path_components.count() + relative_components.count() + 1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_filename_match_bonus(
|
||||
candidate_buf: &str,
|
||||
pattern: &Pattern,
|
||||
matcher: &mut nucleo::Matcher,
|
||||
) -> f64 {
|
||||
let Some(filename) = std::path::Path::new(candidate_buf)
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.filter(|f| !f.is_empty())
|
||||
else {
|
||||
return 0.0;
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
let haystack = Utf32Str::new(filename, &mut buf);
|
||||
let score: u32 = pattern
|
||||
.atoms
|
||||
.iter()
|
||||
.filter_map(|atom| atom.score(haystack, matcher))
|
||||
.map(|s| s as u32)
|
||||
.sum();
|
||||
|
||||
score as f64 / filename.len().max(1) as f64
|
||||
}
|
||||
|
||||
fn path_match_helper<'a>(
|
||||
matcher: &mut nucleo::Matcher,
|
||||
query: &Query,
|
||||
candidates: impl Iterator<Item = PathMatchCandidate<'a>>,
|
||||
results: &mut Vec<PathMatch>,
|
||||
worktree_id: usize,
|
||||
path_prefix: &Arc<RelPath>,
|
||||
root_is_file: bool,
|
||||
relative_to: &Option<Arc<RelPath>>,
|
||||
path_style: PathStyle,
|
||||
cancel_flag: &AtomicBool,
|
||||
) -> Result<(), Cancelled> {
|
||||
let mut candidate_buf = if !path_prefix.is_empty() && !root_is_file {
|
||||
let mut s = path_prefix.display(path_style).to_string();
|
||||
s.push_str(path_style.primary_separator());
|
||||
s
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let path_prefix_len = candidate_buf.len();
|
||||
let mut buf = Vec::new();
|
||||
let mut matched_chars: Vec<u32> = Vec::new();
|
||||
let mut candidate_chars: Vec<char> = Vec::new();
|
||||
for candidate in candidates {
|
||||
buf.clear();
|
||||
matched_chars.clear();
|
||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||
return Err(Cancelled);
|
||||
}
|
||||
|
||||
if !candidate.char_bag.is_superset(query.char_bag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidate_buf.truncate(path_prefix_len);
|
||||
if root_is_file {
|
||||
candidate_buf.push_str(path_prefix.as_unix_str());
|
||||
} else {
|
||||
candidate_buf.push_str(candidate.path.as_unix_str());
|
||||
}
|
||||
|
||||
let haystack = Utf32Str::new(&candidate_buf, &mut buf);
|
||||
|
||||
let Some(score) = query.pattern.indices(haystack, matcher, &mut matched_chars) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let case_mismatches = count_case_mismatches(
|
||||
query.query_chars.as_deref(),
|
||||
&matched_chars,
|
||||
&candidate_buf,
|
||||
&mut candidate_chars,
|
||||
);
|
||||
|
||||
matched_chars.sort_unstable();
|
||||
matched_chars.dedup();
|
||||
|
||||
let length_penalty = candidate_buf.len() as f64 * LENGTH_PENALTY;
|
||||
let filename_bonus = get_filename_match_bonus(&candidate_buf, &query.pattern, matcher);
|
||||
let positive = (score as f64 + filename_bonus) * case_penalty(case_mismatches);
|
||||
let adjusted_score = positive - length_penalty;
|
||||
let positions = positions_from_sorted(&candidate_buf, &matched_chars);
|
||||
|
||||
results.push(PathMatch {
|
||||
score: adjusted_score,
|
||||
positions,
|
||||
worktree_id,
|
||||
path: if root_is_file {
|
||||
Arc::clone(path_prefix)
|
||||
} else {
|
||||
candidate.path.into()
|
||||
},
|
||||
path_prefix: if root_is_file {
|
||||
RelPath::empty().into()
|
||||
} else {
|
||||
Arc::clone(path_prefix)
|
||||
},
|
||||
is_dir: candidate.is_dir,
|
||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(usize::MAX, |relative_to| {
|
||||
distance_between_paths(candidate.path, relative_to.as_ref())
|
||||
}),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn match_fixed_path_set(
|
||||
candidates: Vec<PathMatchCandidate>,
|
||||
worktree_id: usize,
|
||||
worktree_root_name: Option<Arc<RelPath>>,
|
||||
query: &str,
|
||||
case: Case,
|
||||
max_results: usize,
|
||||
path_style: PathStyle,
|
||||
) -> Vec<PathMatch> {
|
||||
let Some(query) = Query::build(query, case) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut config = nucleo::Config::DEFAULT;
|
||||
config.set_match_paths();
|
||||
let mut matcher = matcher::get_matcher(config);
|
||||
|
||||
let root_is_file = worktree_root_name.is_some() && candidates.iter().all(|c| c.path.is_empty());
|
||||
|
||||
let path_prefix = worktree_root_name.unwrap_or_else(|| RelPath::empty().into());
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
path_match_helper(
|
||||
&mut matcher,
|
||||
&query,
|
||||
candidates.into_iter(),
|
||||
&mut results,
|
||||
worktree_id,
|
||||
&path_prefix,
|
||||
root_is_file,
|
||||
&None,
|
||||
path_style,
|
||||
&AtomicBool::new(false),
|
||||
)
|
||||
.ok();
|
||||
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
|
||||
matcher::return_matcher(matcher);
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
candidate_sets: &'a [Set],
|
||||
query: &str,
|
||||
relative_to: &Option<Arc<RelPath>>,
|
||||
case: Case,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<PathMatch> {
|
||||
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
|
||||
if path_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let path_style = candidate_sets[0].path_style();
|
||||
|
||||
let query = if path_style.is_windows() {
|
||||
query.replace('\\', "/")
|
||||
} else {
|
||||
query.to_owned()
|
||||
};
|
||||
|
||||
let Some(query) = Query::build(&query, case) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let num_cpus = executor.num_cpus().min(path_count);
|
||||
let segment_size = path_count.div_ceil(num_cpus);
|
||||
let mut segment_results = (0..num_cpus)
|
||||
.map(|_| Vec::with_capacity(max_results))
|
||||
.collect::<Vec<_>>();
|
||||
let mut config = nucleo::Config::DEFAULT;
|
||||
config.set_match_paths();
|
||||
let mut matchers = matcher::get_matchers(num_cpus, config);
|
||||
executor
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, (results, matcher)) in segment_results
|
||||
.iter_mut()
|
||||
.zip(matchers.iter_mut())
|
||||
.enumerate()
|
||||
{
|
||||
let query = &query;
|
||||
let relative_to = relative_to.clone();
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
|
||||
let mut tree_start = 0;
|
||||
for candidate_set in candidate_sets {
|
||||
let tree_end = tree_start + candidate_set.len();
|
||||
|
||||
if tree_start < segment_end && segment_start < tree_end {
|
||||
let start = tree_start.max(segment_start) - tree_start;
|
||||
let end = tree_end.min(segment_end) - tree_start;
|
||||
let candidates = candidate_set.candidates(start).take(end - start);
|
||||
|
||||
if path_match_helper(
|
||||
matcher,
|
||||
query,
|
||||
candidates,
|
||||
results,
|
||||
candidate_set.id(),
|
||||
&candidate_set.prefix(),
|
||||
candidate_set.root_is_file(),
|
||||
&relative_to,
|
||||
path_style,
|
||||
cancel_flag,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if tree_end >= segment_end {
|
||||
break;
|
||||
}
|
||||
tree_start = tree_end;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
matcher::return_matchers(matchers);
|
||||
if cancel_flag.load(atomic::Ordering::Acquire) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = segment_results.concat();
|
||||
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
|
||||
results
|
||||
}
|
||||
764
crates/fuzzy_nucleo/src/strings.rs
Normal file
764
crates/fuzzy_nucleo/src/strings.rs
Normal file
@@ -0,0 +1,764 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::Ordering,
|
||||
iter,
|
||||
ops::Range,
|
||||
sync::atomic::{self, AtomicBool},
|
||||
};
|
||||
|
||||
use gpui::{BackgroundExecutor, SharedString};
|
||||
use nucleo::Utf32Str;
|
||||
|
||||
use crate::{
|
||||
Cancelled, Case, LengthPenalty, Query, case_penalty, count_case_mismatches,
|
||||
matcher::{self, LENGTH_PENALTY},
|
||||
positions_from_sorted,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatchCandidate {
|
||||
pub id: usize,
|
||||
pub string: SharedString,
|
||||
char_bag: CharBag,
|
||||
}
|
||||
|
||||
impl StringMatchCandidate {
|
||||
pub fn new(id: usize, string: impl ToString) -> Self {
|
||||
Self::from_shared(id, SharedString::new(string.to_string()))
|
||||
}
|
||||
|
||||
pub fn from_shared(id: usize, string: SharedString) -> Self {
|
||||
let char_bag = CharBag::from(string.as_ref());
|
||||
Self {
|
||||
id,
|
||||
string,
|
||||
char_bag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatch {
|
||||
pub candidate_id: usize,
|
||||
pub score: f64,
|
||||
pub positions: Vec<usize>,
|
||||
pub string: SharedString,
|
||||
}
|
||||
|
||||
impl StringMatch {
|
||||
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
|
||||
let mut positions = self.positions.iter().peekable();
|
||||
iter::from_fn(move || {
|
||||
let start = *positions.next()?;
|
||||
let char_len = self.char_len_at_index(start)?;
|
||||
let mut end = start + char_len;
|
||||
while let Some(next_start) = positions.peek() {
|
||||
if end == **next_start {
|
||||
let Some(char_len) = self.char_len_at_index(end) else {
|
||||
break;
|
||||
};
|
||||
end += char_len;
|
||||
positions.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(start..end)
|
||||
})
|
||||
}
|
||||
|
||||
fn char_len_at_index(&self, ix: usize) -> Option<usize> {
|
||||
self.string
|
||||
.get(ix..)
|
||||
.and_then(|slice| slice.chars().next().map(|c| c.len_utf8()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for StringMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for StringMatch {}
|
||||
|
||||
impl PartialOrd for StringMatch {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for StringMatch {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.score
|
||||
.total_cmp(&other.score)
|
||||
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn match_strings_async<T>(
|
||||
candidates: &[T],
|
||||
query: &str,
|
||||
case: Case,
|
||||
length_penalty: LengthPenalty,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<StringMatch>
|
||||
where
|
||||
T: Borrow<StringMatchCandidate> + Sync,
|
||||
{
|
||||
if candidates.is_empty() || max_results == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(query) = Query::build(query, case) else {
|
||||
return empty_query_results(candidates, max_results);
|
||||
};
|
||||
|
||||
let num_cpus = executor.num_cpus().min(candidates.len());
|
||||
let base_size = candidates.len() / num_cpus;
|
||||
let remainder = candidates.len() % num_cpus;
|
||||
let mut segment_results = (0..num_cpus)
|
||||
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let config = nucleo::Config::DEFAULT;
|
||||
let mut matchers = matcher::get_matchers(num_cpus, config);
|
||||
|
||||
executor
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, (results, matcher)) in segment_results
|
||||
.iter_mut()
|
||||
.zip(matchers.iter_mut())
|
||||
.enumerate()
|
||||
{
|
||||
let query = &query;
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * base_size + segment_idx.min(remainder);
|
||||
let segment_end =
|
||||
(segment_idx + 1) * base_size + (segment_idx + 1).min(remainder);
|
||||
|
||||
match_string_helper(
|
||||
&candidates[segment_start..segment_end],
|
||||
query,
|
||||
matcher,
|
||||
length_penalty,
|
||||
results,
|
||||
cancel_flag,
|
||||
)
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
matcher::return_matchers(matchers);
|
||||
|
||||
if cancel_flag.load(atomic::Ordering::Acquire) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut results = segment_results.concat();
|
||||
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
|
||||
results
|
||||
}
|
||||
|
||||
pub fn match_strings<T>(
|
||||
candidates: &[T],
|
||||
query: &str,
|
||||
case: Case,
|
||||
length_penalty: LengthPenalty,
|
||||
max_results: usize,
|
||||
) -> Vec<StringMatch>
|
||||
where
|
||||
T: Borrow<StringMatchCandidate>,
|
||||
{
|
||||
if candidates.is_empty() || max_results == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(query) = Query::build(query, case) else {
|
||||
return empty_query_results(candidates, max_results);
|
||||
};
|
||||
|
||||
let config = nucleo::Config::DEFAULT;
|
||||
let mut matcher = matcher::get_matcher(config);
|
||||
let mut results = Vec::with_capacity(max_results.min(candidates.len()));
|
||||
|
||||
match_string_helper(
|
||||
candidates,
|
||||
&query,
|
||||
&mut matcher,
|
||||
length_penalty,
|
||||
&mut results,
|
||||
&AtomicBool::new(false),
|
||||
)
|
||||
.ok();
|
||||
|
||||
matcher::return_matcher(matcher);
|
||||
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
|
||||
results
|
||||
}
|
||||
|
||||
fn empty_query_results<T: Borrow<StringMatchCandidate>>(
|
||||
candidates: &[T],
|
||||
max_results: usize,
|
||||
) -> Vec<StringMatch> {
|
||||
candidates
|
||||
.iter()
|
||||
.take(max_results)
|
||||
.map(|candidate| {
|
||||
let borrowed = candidate.borrow();
|
||||
StringMatch {
|
||||
candidate_id: borrowed.id,
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
string: borrowed.string.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn match_string_helper<T>(
|
||||
candidates: &[T],
|
||||
query: &Query,
|
||||
matcher: &mut nucleo::Matcher,
|
||||
length_penalty: LengthPenalty,
|
||||
results: &mut Vec<StringMatch>,
|
||||
cancel_flag: &AtomicBool,
|
||||
) -> Result<(), Cancelled>
|
||||
where
|
||||
T: Borrow<StringMatchCandidate>,
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
let mut matched_chars: Vec<u32> = Vec::new();
|
||||
let mut candidate_chars: Vec<char> = Vec::new();
|
||||
|
||||
for candidate in candidates {
|
||||
buf.clear();
|
||||
matched_chars.clear();
|
||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||
return Err(Cancelled);
|
||||
}
|
||||
|
||||
let borrowed = candidate.borrow();
|
||||
|
||||
if !borrowed.char_bag.is_superset(query.char_bag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let haystack: Utf32Str = Utf32Str::new(borrowed.string.as_ref(), &mut buf);
|
||||
|
||||
let Some(score) = query.pattern.indices(haystack, matcher, &mut matched_chars) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let case_mismatches = count_case_mismatches(
|
||||
query.query_chars.as_deref(),
|
||||
&matched_chars,
|
||||
borrowed.string.as_ref(),
|
||||
&mut candidate_chars,
|
||||
);
|
||||
|
||||
matched_chars.sort_unstable();
|
||||
matched_chars.dedup();
|
||||
|
||||
let positive = score as f64 * case_penalty(case_mismatches);
|
||||
let adjusted_score =
|
||||
positive - length_penalty_for(borrowed.string.as_ref(), length_penalty);
|
||||
let positions = positions_from_sorted(borrowed.string.as_ref(), &matched_chars);
|
||||
|
||||
results.push(StringMatch {
|
||||
candidate_id: borrowed.id,
|
||||
score: adjusted_score,
|
||||
positions,
|
||||
string: borrowed.string.clone(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn length_penalty_for(s: &str, length_penalty: LengthPenalty) -> f64 {
|
||||
if length_penalty.is_on() {
|
||||
s.len() as f64 * LENGTH_PENALTY
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::BackgroundExecutor;
|
||||
|
||||
fn candidates(strings: &[&str]) -> Vec<StringMatchCandidate> {
|
||||
strings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, s)| StringMatchCandidate::new(id, s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_match(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["hello", "world", "help"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"hel",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"hello"));
|
||||
assert!(matched.contains(&"help"));
|
||||
assert!(!matched.contains(&"world"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_word_query(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&[
|
||||
"src/lib/parser.rs",
|
||||
"src/bin/main.rs",
|
||||
"tests/parser_test.rs",
|
||||
]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"src parser",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].string, "src/lib/parser.rs");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_query_returns_all(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["alpha", "beta", "gamma"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 3);
|
||||
assert!(results.iter().all(|m| m.score == 0.0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_whitespace_only_query_returns_all(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["alpha", "beta", "gamma"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
" \t\n",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_candidates(executor: BackgroundExecutor) {
|
||||
let cs: Vec<StringMatchCandidate> = vec![];
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"query",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cancellation(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["hello", "world"]);
|
||||
let cancel = AtomicBool::new(true);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"hel",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_max_results_limit(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["ab", "abc", "abcd", "abcde"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"ab",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
2,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scoring_order(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&[
|
||||
"some_very_long_variable_name_fuzzy",
|
||||
"fuzzy",
|
||||
"a_fuzzy_thing",
|
||||
]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"fuzzy",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let ordered = matches!(
|
||||
(
|
||||
results[0].string.as_ref(),
|
||||
results[1].string.as_ref(),
|
||||
results[2].string.as_ref()
|
||||
),
|
||||
(
|
||||
"fuzzy",
|
||||
"a_fuzzy_thing",
|
||||
"some_very_long_variable_name_fuzzy"
|
||||
)
|
||||
);
|
||||
assert!(ordered, "matches are not in the proper order.");
|
||||
|
||||
let results_penalty = match_strings_async(
|
||||
&cs,
|
||||
"fuzzy",
|
||||
Case::Ignore,
|
||||
LengthPenalty::On,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let greater = results[2].score > results_penalty[2].score;
|
||||
assert!(greater, "penalize length not affecting long candidates");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_utf8_positions(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["café"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"caf",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 1);
|
||||
let m = &results[0];
|
||||
assert_eq!(m.positions, vec![0, 1, 2]);
|
||||
for &pos in &m.positions {
|
||||
assert!(m.string.is_char_boundary(pos));
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_smart_case(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["FooBar", "foobar", "FOOBAR"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
let case_insensitive = match_strings_async(
|
||||
&cs,
|
||||
"foobar",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(case_insensitive.len(), 3);
|
||||
|
||||
let smart = match_strings_async(
|
||||
&cs,
|
||||
"FooBar",
|
||||
Case::Smart,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert!(smart.iter().any(|m| m.string == "FooBar"));
|
||||
let foobar_score = smart.iter().find(|m| m.string == "FooBar").map(|m| m.score);
|
||||
let lower_score = smart.iter().find(|m| m.string == "foobar").map(|m| m.score);
|
||||
if let (Some(exact), Some(lower)) = (foobar_score, lower_score) {
|
||||
assert!(exact >= lower);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_smart_case_does_not_flip_order_when_length_penalty_on(
|
||||
executor: BackgroundExecutor,
|
||||
) {
|
||||
// Regression for the sign bug: with a length penalty large enough to push
|
||||
// `total_score - length_penalty` negative, case mismatches used to make
|
||||
// scores *better* (less negative). Exact-case match must still rank first.
|
||||
let cs = candidates(&[
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaa_FooBar",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaa_foobar",
|
||||
]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"FooBar",
|
||||
Case::Smart,
|
||||
LengthPenalty::On,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let exact = results
|
||||
.iter()
|
||||
.find(|m| m.string.as_ref() == "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_FooBar")
|
||||
.map(|m| m.score)
|
||||
.expect("exact-case candidate should match");
|
||||
let mismatch = results
|
||||
.iter()
|
||||
.find(|m| m.string.as_ref() == "aaaaaaaaaaaaaaaaaaaaaaaaaaaa_foobar")
|
||||
.map(|m| m.score)
|
||||
.expect("mismatch-case candidate should match");
|
||||
assert!(
|
||||
exact >= mismatch,
|
||||
"exact-case score ({exact}) should be >= mismatch-case score ({mismatch})"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_char_bag_prefilter(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["abcdef", "abc", "def", "aabbcc"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"abc",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"abcdef"));
|
||||
assert!(matched.contains(&"abc"));
|
||||
assert!(matched.contains(&"aabbcc"));
|
||||
assert!(!matched.contains(&"def"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_basic_match() {
|
||||
let cs = candidates(&["hello", "world", "help"]);
|
||||
let results = match_strings(&cs, "hel", Case::Ignore, LengthPenalty::Off, 10);
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"hello"));
|
||||
assert!(matched.contains(&"help"));
|
||||
assert!(!matched.contains(&"world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_empty_query_returns_all() {
|
||||
let cs = candidates(&["alpha", "beta", "gamma"]);
|
||||
let results = match_strings(&cs, "", Case::Ignore, LengthPenalty::Off, 10);
|
||||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_whitespace_only_query_returns_all() {
|
||||
let cs = candidates(&["alpha", "beta", "gamma"]);
|
||||
let results = match_strings(&cs, " ", Case::Ignore, LengthPenalty::Off, 10);
|
||||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_max_results() {
|
||||
let cs = candidates(&["ab", "abc", "abcd", "abcde"]);
|
||||
let results = match_strings(&cs, "ab", Case::Ignore, LengthPenalty::Off, 2);
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_query_respects_max_results(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["alpha", "beta", "gamma", "delta"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
2,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_word_with_nonmatching_word(executor: BackgroundExecutor) {
|
||||
let cs = candidates(&["src/parser.rs", "src/main.rs"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"src xyzzy",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"no candidate contains 'xyzzy', so nothing should match"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_segment_size_not_divisible_by_cpus(executor: BackgroundExecutor) {
|
||||
executor.set_num_cpus(4);
|
||||
let cs = candidates(&["alpha", "beta", "gamma", "delta", "epsilon"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"a",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"alpha"));
|
||||
assert!(matched.contains(&"gamma"));
|
||||
assert!(matched.contains(&"delta"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_segment_size_with_many_cpus_few_candidates(executor: BackgroundExecutor) {
|
||||
executor.set_num_cpus(16);
|
||||
let cs = candidates(&["one", "two", "three"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"o",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"one"));
|
||||
assert!(matched.contains(&"two"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_segment_size_single_candidate(executor: BackgroundExecutor) {
|
||||
executor.set_num_cpus(8);
|
||||
let cs = candidates(&["lonely"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"lone",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].string.as_ref(), "lonely");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_segment_size_candidates_equal_cpus(executor: BackgroundExecutor) {
|
||||
executor.set_num_cpus(4);
|
||||
let cs = candidates(&["aaa", "bbb", "ccc", "ddd"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"a",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].string.as_ref(), "aaa");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_segment_size_candidates_one_more_than_cpus(executor: BackgroundExecutor) {
|
||||
executor.set_num_cpus(3);
|
||||
let cs = candidates(&["ant", "ape", "dog", "axe"]);
|
||||
let cancel = AtomicBool::new(false);
|
||||
let results = match_strings_async(
|
||||
&cs,
|
||||
"a",
|
||||
Case::Ignore,
|
||||
LengthPenalty::Off,
|
||||
10,
|
||||
&cancel,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
let matched: Vec<&str> = results.iter().map(|m| m.string.as_ref()).collect();
|
||||
assert!(matched.contains(&"ant"));
|
||||
assert!(matched.contains(&"ape"));
|
||||
assert!(matched.contains(&"axe"));
|
||||
assert!(!matched.contains(&"dog"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user