use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use nucleo::pattern::{CaseMatching, MultiPattern, Normalization, Pattern};
use nucleo::{Config, Injector, Matcher, Nucleo};
fn matcher_threads() -> usize {
matcher_thread_count(
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2),
)
}
pub(crate) fn matcher_thread_count(n: usize) -> usize {
match n {
0..=4 => 2,
5..=8 => 3,
_ => 4,
}
}
#[derive(Debug, Clone, Copy)]
pub struct EntryMeta {
pub is_dir: bool,
}
#[derive(Debug, Clone, Default)]
pub struct FileMatch {
pub rel: String,
pub is_dir: bool,
pub score: u32,
pub indices: Vec<u32>,
pub root: u16,
}
fn min_score(query: &str) -> u32 {
7 + query.chars().count() as u32 * 14
}
const SETTLE_MS: u64 = 50;
pub(crate) fn below_floor(score: u32, floor: u32) -> bool {
score < floor
}
pub struct FuzzyEngine {
nucleo: Nucleo<EntryMeta>,
matcher: Matcher,
query: String,
running: bool,
}
impl FuzzyEngine {
pub fn new(results_dirty: Arc<AtomicBool>) -> Self {
let config = Config::DEFAULT.match_paths();
let notify = move || results_dirty.store(true, Ordering::Release);
let mut nucleo = Nucleo::new(config.clone(), Arc::new(notify), Some(matcher_threads()), 1);
nucleo.pattern = MultiPattern::new(1);
Self {
nucleo,
matcher: Matcher::new(config),
query: String::new(),
running: false,
}
}
pub fn injector(&self) -> Injector<EntryMeta> {
self.nucleo.injector()
}
pub fn push(injector: &Injector<EntryMeta>, rel: &str, is_dir: bool) {
let text = if is_dir {
format!("{rel}/")
} else {
rel.to_owned()
};
injector.push(EntryMeta { is_dir }, move |_, cols| {
cols[0] = text.into();
});
}
pub fn restart(&mut self) {
self.nucleo.restart(true);
self.query.clear();
}
pub fn nudge(&mut self) -> bool {
let status = self.nucleo.tick(0);
self.running = status.running || self.nucleo.active_injectors() > 0;
self.running
}
pub fn rearm(&mut self) {
if self.query.is_empty() {
return;
}
let pattern = self.query.clone();
self.nucleo.pattern.reparse(
0,
&pattern,
CaseMatching::Smart,
Normalization::Smart,
false,
);
let status = self.nucleo.tick(SETTLE_MS);
self.running = status.running || self.nucleo.active_injectors() > 0;
}
pub fn set_query(&mut self, pattern: &str) {
if pattern == self.query {
let status = self.nucleo.tick(0);
self.running = status.running || self.nucleo.active_injectors() > 0;
return;
}
let append = pattern.as_bytes().starts_with(self.query.as_bytes())
&& !pattern.ends_with('\\')
&& !pattern
.as_bytes()
.last()
.is_some_and(|ch| ch.is_ascii_whitespace());
self.nucleo.pattern.reparse(
0,
pattern,
CaseMatching::Smart,
Normalization::Smart,
append,
);
self.query.clear();
self.query.push_str(pattern);
self.nucleo.tick(0);
}
pub fn read(&mut self, limit: usize) -> (Vec<FileMatch>, bool) {
let pattern = self.query.clone();
if pattern.is_empty() {
let status = self.nucleo.tick(0);
self.running = status.running;
return (Vec::new(), self.running); }
let status = self.nucleo.tick(0);
self.running = status.running;
let Self {
nucleo, matcher, ..
} = self;
let snapshot = nucleo.snapshot();
let col_pattern = Pattern::parse(&pattern, CaseMatching::Smart, Normalization::Smart);
let floor = min_score(&pattern);
let total = snapshot.matched_item_count();
let mut out = Vec::with_capacity(limit.min(256));
for item in snapshot.matched_items(0..total.min(limit as u32)) {
let text = &item.matcher_columns[0];
let mut indices = Vec::new();
let score = col_pattern
.indices(text.slice(..), matcher, &mut indices)
.unwrap_or(0);
if below_floor(score, floor) {
continue;
}
let mut rel = text.to_string();
let is_dir = item.data.is_dir;
if is_dir && rel.ends_with('/') {
rel.pop();
}
out.push(FileMatch {
rel,
is_dir,
score,
indices,
root: 0, });
}
(out, self.running)
}
pub fn query_blocking(&mut self, pattern: &str, limit: usize) -> Vec<FileMatch> {
self.set_query(pattern);
self.nucleo.tick(SETTLE_MS);
self.read(limit).0
}
}
impl Default for FuzzyEngine {
fn default() -> Self {
Self::new(Arc::new(AtomicBool::new(false)))
}
}
#[cfg(test)]
#[path = "fuzzy_tests.rs"]
mod tests;