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

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:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View 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);

View 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
}

View 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));
}

View 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
}

View 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"));
}
}