skim 4.0.0

Fuzzy Finder in rust!
Documentation
use regex::Regex;

use crate::engine::all::MatchAllEngine;
use crate::engine::andor::{AndEngine, OrEngine};
use crate::engine::exact::{ExactEngine, ExactMatchingParam};
use crate::engine::fuzzy::{FuzzyAlgorithm, FuzzyEngine};
use crate::engine::regexp::RegexEngine;
use crate::item::RankBuilder;
use crate::{CaseMatching, MatchEngine, MatchEngineFactory, Typos};
use std::sync::{Arc, LazyLock};

static RE_OR_WITH_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" *\|+ *").unwrap());

//------------------------------------------------------------------------------
// Exact engine factory
/// Factory for creating exact or fuzzy match engines based on configuration
pub struct ExactOrFuzzyEngineFactory {
    exact_mode: bool,
    fuzzy_algorithm: FuzzyAlgorithm,
    rank_builder: Arc<RankBuilder>,
    typos: Typos,
    filter_mode: bool,
    last_match: bool,
}

impl ExactOrFuzzyEngineFactory {
    /// Creates a new builder with default settings
    pub fn builder() -> Self {
        Self {
            exact_mode: false,
            fuzzy_algorithm: FuzzyAlgorithm::SkimV2,
            rank_builder: Default::default(),
            typos: Typos::Disabled,
            filter_mode: false,
            last_match: false,
        }
    }

    /// Sets whether to use exact matching mode
    pub fn exact_mode(mut self, exact_mode: bool) -> Self {
        self.exact_mode = exact_mode;
        self
    }

    /// Sets the fuzzy matching algorithm to use
    pub fn fuzzy_algorithm(mut self, fuzzy_algorithm: FuzzyAlgorithm) -> Self {
        self.fuzzy_algorithm = fuzzy_algorithm;
        self
    }

    /// Sets the rank builder for scoring matches
    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
        self.rank_builder = rank_builder;
        self
    }

    /// Sets the typo tolerance configuration
    ///
    /// - `Typos::Disabled`: no typo tolerance
    /// - `Typos::Smart`: adaptive typo tolerance (pattern_length / 4)
    /// - `Typos::Fixed(n)`: exactly n typos allowed
    pub fn typos(mut self, typos: Typos) -> Self {
        self.typos = typos;
        self
    }

    /// Sets filter mode (skips per-character match indices for faster matching)
    pub fn filter_mode(mut self, filter_mode: bool) -> Self {
        self.filter_mode = filter_mode;
        self
    }

    /// When true, prefer the last (rightmost) occurrence on tied scores
    pub fn last_match(mut self, last_match: bool) -> Self {
        self.last_match = last_match;
        self
    }

    /// Builds the factory (currently a no-op, returns self)
    pub fn build(self) -> Self {
        self
    }
}

impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
        // 'abc => match exact "abc"
        // ^abc => starts with "abc"
        // abc$ => ends with "abc"
        // ^abc$ => match exact "abc"
        // !^abc => items not starting with "abc"
        // !abc$ => items not ending with "abc"
        // !^abc$ => not "abc"

        let mut query = query;
        let mut exact = self.exact_mode;
        let mut param = ExactMatchingParam::default();
        param.case = case;

        if query.starts_with('\'') {
            exact = !exact;
            query = &query[1..];
        }

        if query.starts_with('!') {
            query = &query[1..];
            exact = true;
            param.inverse = true;
        }

        if query.is_empty() {
            // if only "!" was provided, will still show all items
            return Box::new(
                MatchAllEngine::builder()
                    .rank_builder(self.rank_builder.clone())
                    .build(),
            );
        }

        if query.starts_with('^') {
            query = &query[1..];
            exact = true;
            param.prefix = true;
        }

        if query.ends_with('$') {
            query = &query[..(query.len() - 1)];
            exact = true;
            param.postfix = true;
        }

        if exact {
            Box::new(
                ExactEngine::builder(query, param)
                    .rank_builder(self.rank_builder.clone())
                    .build(),
            )
        } else {
            Box::new(
                FuzzyEngine::builder()
                    .query(query)
                    .algorithm(self.fuzzy_algorithm)
                    .case(case)
                    .typos(self.typos)
                    .filter_mode(self.filter_mode)
                    .last_match(self.last_match)
                    .rank_builder(self.rank_builder.clone())
                    .build(),
            )
        }
    }
}

//------------------------------------------------------------------------------
/// Factory for creating AND/OR composite match engines
pub struct AndOrEngineFactory {
    inner: Box<dyn MatchEngineFactory>,
}

impl AndOrEngineFactory {
    /// Creates a new AND/OR engine factory wrapping another factory
    pub fn new(factory: impl MatchEngineFactory + 'static) -> Self {
        Self {
            inner: Box::new(factory),
        }
    }

    fn parse_andor(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
        if query.trim().is_empty() {
            return self.inner.create_engine_with_case(query, case);
        }
        let and_engines = RE_OR_WITH_SPACES
            .replace_all(&self.mask_escape_space(query), "|")
            .split(' ')
            .filter_map(|and_term| {
                if and_term.is_empty() {
                    return None;
                }
                let or_engines = and_term
                    .split('|')
                    .filter_map(|term| {
                        if term.is_empty() {
                            return None;
                        }
                        debug!("Creating Or engine for {term}");
                        Some(
                            self.inner
                                .create_engine_with_case(&self.unmask_escape_space(term), case),
                        )
                    })
                    .collect::<Vec<_>>();
                debug!("Building or matcher engine from Ors");
                if or_engines.len() == 1 {
                    return Some(or_engines.into_iter().next().unwrap());
                }
                Some(Box::new(OrEngine::builder().engines(or_engines).build()) as Box<dyn MatchEngine>)
            })
            .collect();
        debug!("Creating and matcher engine from Ors");
        Box::new(AndEngine::builder().engines(and_engines).build())
    }

    fn mask_escape_space(&self, string: &str) -> String {
        string.replace("\\ ", "\0")
    }

    fn unmask_escape_space(&self, string: &str) -> String {
        string.replace('\0', " ")
    }
}

impl MatchEngineFactory for AndOrEngineFactory {
    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
        self.parse_andor(query, case)
    }
}

//------------------------------------------------------------------------------
/// Factory for creating regex-based match engines
pub struct RegexEngineFactory {
    rank_builder: Arc<RankBuilder>,
}

impl RegexEngineFactory {
    /// Creates a new builder with default settings
    pub fn builder() -> Self {
        Self {
            rank_builder: Default::default(),
        }
    }

    /// Sets the rank builder for scoring matches
    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
        self.rank_builder = rank_builder;
        self
    }

    /// Builds the factory (currently a no-op, returns self)
    pub fn build(self) -> Self {
        self
    }
}

impl MatchEngineFactory for RegexEngineFactory {
    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
        Box::new(
            RegexEngine::builder(query, case)
                .rank_builder(self.rank_builder.clone())
                .build(),
        )
    }
}

#[cfg(test)]
mod test {
    #[test]
    fn test_engine_factory() {
        use super::*;
        let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build();
        let x = exact_or_fuzzy.create_engine("'abc");
        assert_eq!(format!("{x}"), "(Exact|(?i)abc)");

        let x = exact_or_fuzzy.create_engine("^abc");
        assert_eq!(format!("{x}"), "(Exact|(?i)^abc)");

        let x = exact_or_fuzzy.create_engine("abc$");
        assert_eq!(format!("{x}"), "(Exact|(?i)abc$)");

        let x = exact_or_fuzzy.create_engine("^abc$");
        assert_eq!(format!("{x}"), "(Exact|(?i)^abc$)");

        let x = exact_or_fuzzy.create_engine("!abc");
        assert_eq!(format!("{x}"), "(Exact|!(?i)abc)");

        let x = exact_or_fuzzy.create_engine("!^abc");
        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc)");

        let x = exact_or_fuzzy.create_engine("!abc$");
        assert_eq!(format!("{x}"), "(Exact|!(?i)abc$)");

        let x = exact_or_fuzzy.create_engine("!^abc$");
        assert_eq!(format!("{x}"), "(Exact|!(?i)^abc$)");

        let regex_factory = RegexEngineFactory::builder();
        let and_or_factory = AndOrEngineFactory::new(exact_or_fuzzy);

        let x = and_or_factory.create_engine("'abc | def ^gh ij | kl mn");
        assert_eq!(
            format!("{x}"),
            "(And: (Or: (Exact|(?i)abc), (Fuzzy: def)), (Exact|(?i)^gh), (Or: (Fuzzy: ij), (Fuzzy: kl)), (Fuzzy: mn))"
        );

        let x = regex_factory.create_engine("'abc | def ^gh ij | kl mn");
        assert_eq!(format!("{x}"), "(Regex: 'abc | def ^gh ij | kl mn)");

        let x = and_or_factory.create_engine("readme .md$ | .markdown$");
        assert_eq!(
            format!("{x}"),
            "(And: (Fuzzy: readme), (Or: (Exact|(?i)\\.md$), (Exact|(?i)\\.markdown$)))"
        );
    }
}