use crate::thread_pool::{self, ThreadPool};
use crate::tui::item_list::{MergeStrategy, ProcessedItems};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::engine::normalized::NormalizedEngineFactory;
use crate::engine::split::SplitMatchEngineFactory;
use crate::item::{ItemPool, MatchedItem, RankBuilder};
use crate::prelude::{AndOrEngineFactory, ExactOrFuzzyEngineFactory, RegexEngineFactory};
use crate::{CaseMatching, MatchEngineFactory, SkimItem, SkimOptions};
fn input_index(tac: bool, start: usize, batch_len: usize, batch_index: usize) -> usize {
debug_assert!(batch_index < batch_len);
if tac {
start + batch_len - 1 - batch_index
} else {
start + batch_index
}
}
fn merge_worker_results(
worker_results: Vec<Vec<MatchedItem>>,
no_sort: bool,
processed_items: &Mutex<Option<ProcessedItems>>,
merge_strategy: MergeStrategy,
generation: usize,
current_generation: &AtomicUsize,
needs_render: &AtomicBool,
) {
if current_generation.load(Ordering::Acquire) != generation {
return;
}
let total_len: usize = worker_results.iter().map(Vec::len).sum();
let mut items = Vec::with_capacity(total_len);
for chunk in worker_results {
items.extend(chunk);
}
if !no_sort {
items.sort();
}
trace!("matcher stop, total matched: {}", items.len());
let mut guard = processed_items
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if current_generation.load(Ordering::Acquire) != generation {
return;
}
if matches!(merge_strategy, MergeStrategy::Replace) {
*guard = Some(ProcessedItems {
items,
merge: MergeStrategy::Replace,
generation,
});
drop(guard);
needs_render.store(true, Ordering::Relaxed);
return;
}
match &mut *guard {
Some(existing) if existing.generation != generation => {
*guard = Some(ProcessedItems {
items,
merge: MergeStrategy::Replace,
generation,
});
}
Some(existing) => {
if no_sort {
if matches!(merge_strategy, MergeStrategy::Prepend) {
items.append(&mut existing.items);
existing.items = items;
} else {
existing.items.extend(items);
}
} else {
MatchedItem::merge_into_sorted(&mut existing.items, items);
}
}
None => {
*guard = Some(ProcessedItems {
items,
merge: merge_strategy,
generation,
});
}
}
needs_render.store(true, Ordering::Relaxed);
}
pub struct MatcherControl {
stopped: Arc<AtomicBool>,
interrupt: Arc<AtomicBool>,
processed: Arc<AtomicUsize>,
matched: Arc<AtomicUsize>,
}
impl Default for MatcherControl {
fn default() -> Self {
Self {
stopped: Arc::new(AtomicBool::new(true)),
interrupt: Arc::new(AtomicBool::new(false)),
processed: Default::default(),
matched: Default::default(),
}
}
}
impl MatcherControl {
#[must_use]
pub fn get_num_processed(&self) -> usize {
self.processed.load(Ordering::Relaxed)
}
#[must_use]
pub fn get_num_matched(&self) -> usize {
self.matched.load(Ordering::Relaxed)
}
pub fn kill(&mut self) {
self.interrupt.store(true, Ordering::Relaxed);
}
#[must_use]
pub fn stopped(&self) -> bool {
self.stopped.load(Ordering::Relaxed)
}
}
impl Drop for MatcherControl {
fn drop(&mut self) {
self.kill();
}
}
pub struct Matcher {
engine_factory: Rc<dyn MatchEngineFactory>,
case_matching: CaseMatching,
pub rank_builder: Arc<RankBuilder>,
}
impl Matcher {
pub fn builder(engine_factory: Rc<dyn MatchEngineFactory>) -> Self {
Self {
engine_factory,
case_matching: CaseMatching::default(),
rank_builder: Arc::new(RankBuilder::default()),
}
}
#[must_use]
pub fn case(mut self, case_matching: CaseMatching) -> Self {
self.case_matching = case_matching;
self
}
#[must_use]
pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
self.rank_builder = rank_builder;
self
}
#[must_use]
pub fn build(self) -> Self {
self
}
#[must_use]
pub fn create_engine_factory(options: &SkimOptions) -> Rc<dyn MatchEngineFactory> {
Self::create_engine_factory_with_builder(options).0
}
#[must_use]
pub fn create_engine_factory_with_builder(options: &SkimOptions) -> (Rc<dyn MatchEngineFactory>, Arc<RankBuilder>) {
if options.regex {
let rank_builder = Arc::new(RankBuilder::new(options.tiebreak.clone()).tac(options.tac));
let regex_factory = RegexEngineFactory::builder().rank_builder(rank_builder.clone());
let factory: Rc<dyn MatchEngineFactory> = if options.normalize {
Rc::new(NormalizedEngineFactory::new(regex_factory))
} else {
Rc::new(regex_factory)
};
(factory, rank_builder)
} else {
let rank_builder = Arc::new(RankBuilder::new(options.tiebreak.clone()).tac(options.tac));
log::debug!("Creating matcher for algo {:?}", options.algorithm);
let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder()
.fuzzy_algorithm(options.algorithm)
.exact_mode(options.exact)
.typos(options.typos)
.filter_mode(options.filter.is_some())
.last_match(options.last_match)
.rank_builder(rank_builder.clone())
.build();
let mut factory: Box<dyn MatchEngineFactory> = Box::new(fuzzy_engine_factory);
if let Some(delimiter) = options.split_match {
factory = Box::new(SplitMatchEngineFactory::new(factory, delimiter));
}
factory = Box::new(AndOrEngineFactory::new(factory));
if options.normalize {
factory = Box::new(NormalizedEngineFactory::new(factory));
}
let factory: Rc<dyn MatchEngineFactory> = Rc::new(factory);
(factory, rank_builder)
}
}
#[must_use]
pub fn from_options(options: &SkimOptions) -> Self {
let (engine_factory, rank_builder) = Self::create_engine_factory_with_builder(options);
Matcher::builder(engine_factory)
.case(options.case)
.rank_builder(rank_builder)
.build()
}
#[must_use]
pub fn case_matching(&self) -> CaseMatching {
self.case_matching
}
#[must_use]
pub fn engine_factory(&self) -> &Rc<dyn MatchEngineFactory> {
&self.engine_factory
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run(
&self,
query: &str,
item_pool: &Arc<ItemPool>,
thread_pool: &Arc<ThreadPool>,
processed_items: Arc<Mutex<Option<ProcessedItems>>>,
merge_strategy: MergeStrategy,
no_sort: bool,
tac: bool,
generation: usize,
current_generation: Arc<AtomicUsize>,
needs_render: Arc<AtomicBool>,
) -> MatcherControl {
let matcher_engine = self.engine_factory.create_engine_with_case(query, self.case_matching);
debug!("engine: {matcher_engine}");
let stopped = Arc::new(AtomicBool::new(false));
let stopped_clone = stopped.clone();
let interrupt = Arc::new(AtomicBool::new(false));
let interrupt_clone = interrupt.clone();
let processed = Arc::new(AtomicUsize::new(0));
let processed_clone = processed.clone();
let matched = Arc::new(AtomicUsize::new(0));
let matched_clone = matched.clone();
let rank_builder = self.rank_builder.clone();
let start = item_pool.num_taken();
let items = item_pool.take();
let total = items.len();
trace!("matcher start, total: {total}");
let num_workers = thread_pool.num_threads();
let pool_for_work = Arc::clone(thread_pool);
std::thread::spawn(move || {
const CHUNK_SIZE: usize = 1 << 12;
let shared_items: Arc<[Arc<dyn SkimItem>]> = items.into();
let matcher_engine: Arc<dyn crate::MatchEngine> = Arc::from(matcher_engine);
let interrupt_for_work = Arc::clone(&interrupt);
let processed_for_work = Arc::clone(&processed);
let matched_for_work = Arc::clone(&matched);
let rank_builder_for_work = Arc::clone(&rank_builder);
thread_pool::parallel_work_queue(
&pool_for_work,
num_workers,
&shared_items,
CHUNK_SIZE,
no_sort,
Vec::<MatchedItem>::new,
move |chunk_start, chunk: &[Arc<dyn crate::SkimItem>]| {
if interrupt_for_work.load(Ordering::Relaxed) {
return Vec::new();
}
let mut local_matches = Vec::new();
let mut chunk_matched: usize = 0;
for (i, item) in chunk.iter().enumerate() {
if let Some(match_result) = matcher_engine.match_item(item.as_ref()) {
chunk_matched += 1;
let mut rank = match_result.rank;
let batch_index = chunk_start + i;
let index = input_index(tac, start, total, batch_index);
rank.index = i32::try_from(index).unwrap_or(i32::MAX);
local_matches.push(MatchedItem::new(
Arc::clone(item),
rank,
Some(match_result.matched_range),
&rank_builder_for_work,
));
}
}
processed_for_work.fetch_add(chunk.len(), Ordering::Relaxed);
if chunk_matched > 0 {
matched_for_work.fetch_add(chunk_matched, Ordering::Relaxed);
}
local_matches
},
|acc: &mut Vec<MatchedItem>, mut partial: Vec<MatchedItem>| {
if acc.len() >= partial.len() {
acc.extend(partial);
} else {
partial.append(acc);
*acc = partial;
}
},
|acc: &mut Vec<MatchedItem>| acc.sort_unstable(),
|worker_results: Vec<Vec<MatchedItem>>| {
if interrupt.load(Ordering::SeqCst) {
return;
}
merge_worker_results(
worker_results,
no_sort,
&processed_items,
merge_strategy,
generation,
¤t_generation,
&needs_render,
);
},
);
stopped.store(true, Ordering::Relaxed);
});
MatcherControl {
stopped: stopped_clone,
interrupt: interrupt_clone,
matched: matched_clone,
processed: processed_clone,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
use crate::Rank;
use crate::item::RankBuilder;
use crate::options::SkimOptionsBuilder;
fn matched(text: &str, index: i32) -> MatchedItem {
MatchedItem::new(
Arc::new(text.to_string()),
Rank {
index,
..Default::default()
},
None,
&RankBuilder::default(),
)
}
#[test]
fn from_options_exposes_case_and_factory() {
let options = SkimOptionsBuilder::default()
.case(CaseMatching::Ignore)
.build()
.unwrap();
let matcher = Matcher::from_options(&options);
assert_eq!(matcher.case_matching(), CaseMatching::Ignore);
let engine = matcher.engine_factory().create_engine("foo");
assert!(engine.match_item(&"foobar".to_string()).is_some());
}
#[test]
fn create_engine_factory_builds_fuzzy_engine() {
let options = SkimOptions::default();
let factory = Matcher::create_engine_factory(&options);
let engine = factory.create_engine("fb");
assert!(engine.match_item(&"foobar".to_string()).is_some());
}
#[test]
fn create_engine_factory_regex_with_normalize() {
let options = SkimOptionsBuilder::default()
.regex(true)
.normalize(true)
.build()
.unwrap();
let (factory, _rank) = Matcher::create_engine_factory_with_builder(&options);
let engine = factory.create_engine("ba.");
assert!(engine.match_item(&"foobar".to_string()).is_some());
}
#[test]
fn regex_factory_uses_configured_tiebreak() {
let options = SkimOptionsBuilder::default()
.regex(true)
.tiebreak(vec![crate::RankCriteria::Length])
.build()
.unwrap();
let (factory, rank_builder) = Matcher::create_engine_factory_with_builder(&options);
let engine = factory.create_engine("a");
let mut matches: Vec<_> = ["aaaa", "a", "aaa"]
.into_iter()
.enumerate()
.map(|(index, text)| {
let item: Arc<dyn SkimItem> = Arc::new(text.to_string());
let mut result = engine.match_item(item.as_ref()).unwrap();
result.rank.index = i32::try_from(index).unwrap();
MatchedItem::new(item, result.rank, Some(result.matched_range), &rank_builder)
})
.collect();
matches.sort();
let output: Vec<_> = matches.iter().map(|item| item.text().into_owned()).collect();
assert_eq!(output, ["a", "aaa", "aaaa"]);
}
fn merge_test_results(
worker_results: Vec<Vec<MatchedItem>>,
no_sort: bool,
processed_items: &Mutex<Option<ProcessedItems>>,
merge_strategy: MergeStrategy,
needs_render: &AtomicBool,
) {
let generation = AtomicUsize::new(0);
merge_worker_results(
worker_results,
no_sort,
processed_items,
merge_strategy,
0,
&generation,
needs_render,
);
}
#[test]
fn merge_worker_results_replace_sorts() {
let processed = Mutex::new(None);
let needs_render = AtomicBool::new(false);
let workers = vec![vec![matched("b", 1)], vec![matched("a", 0)]];
merge_test_results(workers, false, &processed, MergeStrategy::Replace, &needs_render);
assert!(needs_render.load(Ordering::Relaxed));
let guard = processed.lock().unwrap();
let items = &guard.as_ref().unwrap().items;
assert_eq!(items.len(), 2);
}
#[test]
fn stale_generation_cannot_publish_results() {
let processed = Mutex::new(None);
let needs_render = AtomicBool::new(false);
let generation = AtomicUsize::new(2);
merge_worker_results(
vec![vec![matched("stale", 0)]],
false,
&processed,
MergeStrategy::Replace,
1,
&generation,
&needs_render,
);
assert!(processed.lock().unwrap().is_none());
assert!(!needs_render.load(Ordering::Relaxed));
}
#[test]
fn current_results_replace_pending_stale_generation() {
let processed = Mutex::new(Some(ProcessedItems {
items: vec![matched("stale", 0)],
merge: MergeStrategy::SortedMerge,
generation: 0,
}));
let needs_render = AtomicBool::new(false);
let generation = AtomicUsize::new(1);
merge_worker_results(
vec![vec![matched("current", 1)]],
false,
&processed,
MergeStrategy::SortedMerge,
1,
&generation,
&needs_render,
);
let guard = processed.lock().unwrap();
let result = guard.as_ref().unwrap();
assert_eq!(result.generation, 1);
assert_eq!(result.items.len(), 1);
assert_eq!(result.items[0].text(), "current");
assert!(matches!(result.merge, MergeStrategy::Replace));
}
#[test]
fn merge_worker_results_no_sort_preserves_chunk_order() {
let processed = Mutex::new(None);
let needs_render = AtomicBool::new(false);
let workers = vec![
vec![matched("a", 0), matched("b", 1)],
vec![matched("c", 2), matched("d", 3)],
vec![matched("e", 4), matched("f", 5)],
];
merge_test_results(workers, true, &processed, MergeStrategy::Replace, &needs_render);
let guard = processed.lock().unwrap();
let items = &guard.as_ref().unwrap().items;
let indexes: Vec<i32> = items.iter().map(|item| item.rank.index).collect();
assert_eq!(indexes, vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn merge_worker_results_append_no_sort_extends_existing() {
let processed = Mutex::new(None);
let needs_render = AtomicBool::new(false);
merge_test_results(
vec![vec![matched("a", 0)]],
true,
&processed,
MergeStrategy::Append,
&needs_render,
);
merge_test_results(
vec![vec![matched("b", 1)]],
true,
&processed,
MergeStrategy::Append,
&needs_render,
);
let guard = processed.lock().unwrap();
assert_eq!(guard.as_ref().unwrap().items.len(), 2);
}
#[test]
fn tac_input_index_spans_incremental_batches() {
let first_batch: Vec<_> = (0..3).map(|index| input_index(true, 0, 3, index)).collect();
let second_batch: Vec<_> = (0..2).map(|index| input_index(true, 3, 2, index)).collect();
assert_eq!(first_batch, [2, 1, 0]);
assert_eq!(second_batch, [4, 3]);
assert_eq!(input_index(false, 3, 2, 0), 3);
assert_eq!(input_index(false, 3, 2, 1), 4);
}
#[test]
fn merge_worker_results_prepend_no_sort_places_new_batch_first() {
let processed = Mutex::new(None);
let needs_render = AtomicBool::new(false);
merge_test_results(
vec![vec![matched("c", 2), matched("b", 1), matched("a", 0)]],
true,
&processed,
MergeStrategy::Prepend,
&needs_render,
);
merge_test_results(
vec![vec![matched("e", 4), matched("d", 3)]],
true,
&processed,
MergeStrategy::Prepend,
&needs_render,
);
let guard = processed.lock().unwrap();
let indexes: Vec<i32> = guard
.as_ref()
.unwrap()
.items
.iter()
.map(|item| item.rank.index)
.collect();
assert_eq!(indexes, [4, 3, 2, 1, 0]);
}
}