broot 1.56.0

File browser and launcher
Documentation
use {
    super::*,
    crate::content_search::ContentMatch,
    bet::*,
    smallvec::smallvec,
    std::path::Path,
};

/// A pattern composing other ones with operators
#[derive(Debug, Clone)]
pub struct CompositePattern {
    pub expr: BeTree<PatternOperator, Pattern>,
    pub content_search: bool,
}

impl CompositePattern {
    pub fn new(expr: BeTree<PatternOperator, Pattern>) -> Self {
        let content_search = expr.iter_atoms().any(|p| p.is_content_search());
        Self {
            expr,
            content_search,
        }
    }

    pub fn is_content_search(&self) -> bool {
        self.content_search
    }

    pub fn score_of_string(
        &self,
        candidate: &str,
    ) -> Option<i32> {
        use PatternOperator as PO;
        let composite_result: Option<Option<i32>> = self.expr.eval(
            // score evaluation
            |pat| pat.score_of_string(candidate),
            // operator
            |op, a, b| {
                match (op, a, b) {
                    (PO::And, None, _) => None, // normally not called due to short-circuit
                    (PO::And, Some(sa), Some(Some(sb))) => Some(sa + sb),
                    (PO::Or, None, Some(Some(sb))) => Some(sb),
                    (PO::Or, Some(sa), Some(None)) => Some(sa),
                    (PO::Or, Some(sa), Some(Some(sb))) => Some(sa + sb),
                    (PO::Not, Some(_), _) => None,
                    (PO::Not, None, _) => Some(1),
                    _ => None,
                }
            },
            // short-circuit. We don't short circuit on 'or' because
            // we want to use both scores
            |op, a| match (op, a) {
                (PO::And, None) => true,
                _ => false,
            },
        );
        composite_result.unwrap_or_else(|| {
            warn!("unexpectedly missing result ");
            None
        })
    }

    pub fn score_of(
        &self,
        candidate: Candidate,
    ) -> Option<i32> {
        use PatternOperator as PO;
        if self.is_content_search() && candidate.path.is_dir() {
            // we can't score content on a directory
            return None;
        }
        let composite_result: Option<Option<i32>> = self.expr.eval(
            // score evaluation
            |pat| pat.score_of(candidate),
            // operator
            |op, a, b| {
                match (op, a, b) {
                    (PO::And, None, _) => None, // normally not called due to short-circuit
                    (PO::And, Some(sa), Some(Some(sb))) => Some(sa + sb),
                    (PO::Or, None, Some(Some(sb))) => Some(sb),
                    (PO::Or, Some(sa), Some(None)) => Some(sa),
                    (PO::Or, Some(sa), Some(Some(sb))) => Some(sa + sb),
                    (PO::Not, Some(_), _) => None,
                    (PO::Not, None, _) => Some(1),
                    _ => None,
                }
            },
            // short-circuit. We don't short circuit on 'or' because
            // we want to use both scores
            |op, a| match (op, a) {
                (PO::And, None) => true,
                _ => false,
            },
        );
        composite_result.unwrap_or_else(|| {
            warn!("unexpectedly missing result ");
            None
        })
    }

    pub fn search_string(
        &self,
        candidate: &str,
    ) -> Option<NameMatch> {
        // an ideal algorithm would call score_of on patterns when the object is different
        // to deal with exclusions but I'll start today with something simpler
        use PatternOperator::*;
        let composite_result: Option<Option<NameMatch>> = self.expr.eval(
            // score evaluation
            |pat| pat.search_string(candidate),
            // operator
            |op, a, b| match (op, a, b) {
                (And, None, _) => None, // normally not called due to short-circuit
                (And, Some(sa), Some(Some(_))) => Some(sa), // we have to choose a match
                (Or, None, Some(Some(sb))) => Some(sb),
                (Or, Some(sa), Some(None)) => Some(sa),
                (Or, Some(sa), Some(Some(_))) => Some(sa), // we have to choose
                (Not, Some(_), _) => None,
                (Not, None, _) => {
                    // this is quite arbitrary. Matching the whole string might be
                    // costly for some use, so we match only the start
                    Some(NameMatch {
                        score: 1,
                        pos: smallvec![0],
                    })
                }
                _ => None,
            },
            |op, a| match (op, a) {
                (Or, Some(_)) => true,
                (And, None) => true,
                _ => false,
            },
        );
        // it's possible we didn't find a result because the composition
        composite_result.unwrap_or_else(|| {
            warn!("unexpectedly missing result ");
            None
        })
    }

    pub fn search_content(
        &self,
        candidate: &Path,
        desired_len: usize, // available space for content match display
    ) -> Option<ContentMatch> {
        use PatternOperator::*;
        let composite_result: Option<Option<ContentMatch>> = self.expr.eval(
            // score evaluation
            |pat| pat.search_content(candidate, desired_len),
            // operator
            |op, a, b| match (op, a, b) {
                (And, None, _) => None, // normally not called due to short-circuit
                (And, Some(sa), Some(Some(_))) => Some(sa), // we have to choose
                (Or, None, Some(Some(sb))) => Some(sb),
                (Or, Some(sa), Some(None)) => Some(sa),
                (Or, Some(sa), Some(Some(_))) => Some(sa), // we have to choose
                (Not, Some(_), _) => None,
                (Not, None, _) => {
                    // We can't generate a content match for a whole file
                    // content, so we build one of length 0.
                    Some(ContentMatch {
                        extract: "".to_string(),
                        needle_start: 0,
                        needle_end: 0,
                    })
                }
                _ => None,
            },
            |op, a| match (op, a) {
                (Or, Some(_)) => true,
                (And, None) => true,
                _ => false,
            },
        );
        composite_result.unwrap_or_else(|| {
            warn!("unexpectedly missing result ");
            None
        })
    }

    /// Search for a string, trying to return a match
    ///
    /// It's used when a composite returns something but may be matching also on other parts
    /// that we can't compute, like the content.
    ///
    /// This function isn't called on all potential candidates, only on those that we want
    /// to show so we're allowed to do more costly operations here.
    pub fn find_string(
        &self,
        candidate: &str,
    ) -> Option<NameMatch> {
        // an ideal algorithm would call score_of on patterns when the object is different
        // to deal with exclusions but I'll start today with something simpler
        use PatternOperator::*;
        let composite_result: Option<Option<NameMatch>> = self.expr.eval(
            // score evaluation
            |pat| pat.search_string(candidate),
            // operator
            |op, a, b| match (op, a, b) {
                (Not, Some(_), _) => None,
                (Or | And, Some(ma), Some(Some(mb))) => {
                    Some(ma.merge_with(mb))
                },
                (_, Some(ma), _) => Some(ma),
                (_, None, Some(omb)) => omb,
                _ => None,
            },
            // short-circuit: we don't short circuit on 'or' because we want to use
            // both matches.
            |_op, _a| false,
        );
        // it's possible we didn't find a result because the composition matches
        // on non name parts
        composite_result.unwrap_or(None)
    }

    /// Search for a string in content, trying to return a match as soon as some
    /// part of the composite matches
    pub fn find_content(
        &self,
        candidate: &Path,
        desired_len: usize, // available space for content match display
    ) -> Option<ContentMatch> {
        use PatternOperator::*;
        let composite_result: Option<Option<ContentMatch>> = self.expr.eval(
            // score evaluation
            |pat| pat.search_content(candidate, desired_len),
            // operator
            |op, a, b| match (op, a, b) {
                (Not, Some(_), _) => None,
                (_, Some(ma), _) => Some(ma),
                (_, None, Some(omb)) => omb,
                _ => None,
            },
            |op, a| match (op, a) {
                (Or, Some(_)) => true,
                _ => false,
            },
        );
        // it's possible we didn't find a result because the composition matches
        // on non content parts
        composite_result.unwrap_or(None)
    }

    pub fn get_match_line_count(
        &self,
        candidate: &Path,
    ) -> Option<usize> {
        use PatternOperator::*;
        let composite_result: Option<Option<usize>> = self.expr.eval(
            // score evaluation
            |pat| pat.get_match_line_count(candidate),
            // operator
            |op, a, b| match (op, a, b) {
                (Not, Some(_), _) => None,
                (_, Some(ma), _) => Some(ma),
                (_, None, Some(omb)) => omb,
                _ => None,
            },
            |op, a| match (op, a) {
                (Or, Some(_)) => true,
                _ => false,
            },
        );
        composite_result.unwrap_or(None)
    }

    pub fn has_real_scores(&self) -> bool {
        self.expr.iter_atoms().fold(false, |r, p| match p {
            Pattern::NameFuzzy(_) | Pattern::PathFuzzy(_) => true,
            _ => r,
        })
    }

    pub fn is_empty(&self) -> bool {
        let is_not_empty = self.expr.iter_atoms().any(|p| p.is_some());
        !is_not_empty
    }
}