lesser 0.1.0

A lesser pager (even less than less), for everyday use
use regex::Regex;

pub struct Search {
    pub pattern: Regex,
    last_match: Option<usize>,
}

impl Search {
    pub fn new(pattern: &str) -> Result<Self, regex::Error> {
        Ok(Self {
            pattern: Regex::new(pattern)?,
            last_match: None,
        })
    }

    /// Find the next line (at or after `from`) that contains a match.
    /// Wraps to the start if not found in `[from, end)`.
    pub fn find_forward(&mut self, lines: &[String], from: usize) -> Option<usize> {
        let n = lines.len();
        for i in from..n {
            if self.pattern.is_match(&lines[i]) {
                self.last_match = Some(i);
                return Some(i);
            }
        }
        for i in 0..from.min(n) {
            if self.pattern.is_match(&lines[i]) {
                self.last_match = Some(i);
                return Some(i);
            }
        }
        None
    }

    /// Find the next match after the last one (or from the start if none yet).
    pub fn find_next(&mut self, lines: &[String]) -> Option<usize> {
        let from = self.last_match.map(|i| i + 1).unwrap_or(0);
        self.find_forward(lines, from)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn lines(items: &[&str]) -> Vec<String> {
        items.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn finds_first_occurrence_from_top() {
        let mut s = Search::new("foo").unwrap();
        let l = lines(&["bar", "foo bar", "baz"]);
        assert_eq!(s.find_forward(&l, 0), Some(1));
    }

    #[test]
    fn find_next_advances_past_last_match() {
        let mut s = Search::new("x").unwrap();
        let l = lines(&["x", "y", "x"]);
        assert_eq!(s.find_forward(&l, 0), Some(0));
        assert_eq!(s.find_next(&l), Some(2));
    }

    #[test]
    fn find_forward_wraps_to_start() {
        let mut s = Search::new("x").unwrap();
        let l = lines(&["x", "y", "y"]);
        assert_eq!(s.find_forward(&l, 1), Some(0));
    }

    #[test]
    fn find_next_wraps() {
        let mut s = Search::new("x").unwrap();
        let l = lines(&["x", "y", "y"]);
        s.find_forward(&l, 0); // last_match = 0
        assert_eq!(s.find_next(&l), Some(0)); // wraps back to 0
    }

    #[test]
    fn returns_none_when_no_match_anywhere() {
        let mut s = Search::new("nope").unwrap();
        let l = lines(&["abc", "def"]);
        assert_eq!(s.find_forward(&l, 0), None);
    }

    #[test]
    fn invalid_regex_errors() {
        assert!(Search::new("(unclosed").is_err());
    }
}