Skip to main content

sel/matcher/
lines.rs

1//! Line-number matcher built from sorted, merged ranges.
2
3use super::Matcher;
4use crate::selector::{LineSpec, Selector};
5use crate::{Line, MatchInfo};
6
7/// Matches lines whose 1-indexed number falls in a set of merged ranges.
8pub struct LineMatcher {
9    /// Sorted, non-overlapping, inclusive `(start, end)` ranges (1-indexed).
10    ranges: Vec<(u64, u64)>,
11    next_range: usize,
12    last_line_no: u64,
13}
14
15impl LineMatcher {
16    /// Build from a `Selector::LineNumbers`. Panics on other variants.
17    pub fn from_selector(sel: &Selector) -> Self {
18        let normalized = sel.normalize();
19        let specs: &[LineSpec] = match &normalized {
20            Selector::LineNumbers(s) => s,
21            Selector::All => &[],
22            Selector::Positions(_) => {
23                panic!("LineMatcher::from_selector called with positional selector")
24            }
25        };
26        let ranges = specs
27            .iter()
28            .map(|s| match s {
29                LineSpec::Single(n) => (*n as u64, *n as u64),
30                LineSpec::Range(a, b) => (*a as u64, *b as u64),
31            })
32            .collect();
33        Self {
34            ranges,
35            next_range: 0,
36            last_line_no: 0,
37        }
38    }
39}
40
41impl Matcher for LineMatcher {
42    fn match_line(&mut self, line: &Line) -> MatchInfo {
43        if line.no < self.last_line_no {
44            self.next_range = 0;
45        }
46        self.last_line_no = line.no;
47        while self
48            .ranges
49            .get(self.next_range)
50            .is_some_and(|&(_, end)| line.no > end)
51        {
52            self.next_range += 1;
53        }
54        let hit = self
55            .ranges
56            .get(self.next_range)
57            .is_some_and(|&(start, end)| line.no >= start && line.no <= end);
58        MatchInfo {
59            hit,
60            ..MatchInfo::default()
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    fn mk_line(n: u64) -> Line {
70        Line::new(n, Vec::new())
71    }
72
73    #[test]
74    fn single_line_hits_only_that_line() {
75        let sel = Selector::parse("5").unwrap();
76        let mut m = LineMatcher::from_selector(&sel);
77        assert!(!m.match_line(&mk_line(4)).hit);
78        assert!(m.match_line(&mk_line(5)).hit);
79        assert!(!m.match_line(&mk_line(6)).hit);
80    }
81
82    #[test]
83    fn range_hits_inclusive() {
84        let sel = Selector::parse("10-12").unwrap();
85        let mut m = LineMatcher::from_selector(&sel);
86        assert!(!m.match_line(&mk_line(9)).hit);
87        assert!(m.match_line(&mk_line(10)).hit);
88        assert!(m.match_line(&mk_line(11)).hit);
89        assert!(m.match_line(&mk_line(12)).hit);
90        assert!(!m.match_line(&mk_line(13)).hit);
91    }
92
93    #[test]
94    fn mixed_list_merges_ranges() {
95        let sel = Selector::parse("1,5,10-15,14").unwrap();
96        let mut m = LineMatcher::from_selector(&sel);
97        assert!(m.match_line(&mk_line(1)).hit);
98        assert!(!m.match_line(&mk_line(2)).hit);
99        assert!(m.match_line(&mk_line(5)).hit);
100        assert!(m.match_line(&mk_line(12)).hit);
101        assert!(m.match_line(&mk_line(15)).hit);
102        assert!(!m.match_line(&mk_line(16)).hit);
103    }
104}