Skip to main content

sel/
context.rs

1//! Context expander — turns hits into an emit plan, optionally including neighbors.
2
3use crate::{Emit, Line, MatchInfo, Role};
4use std::collections::VecDeque;
5
6/// An expander consumes `(Line, MatchInfo)` pairs and produces `Emit`s.
7///
8/// The expander owns the line and match info until it emits them, because
9/// it may need to buffer lines as context.
10pub trait Expander {
11    /// Feed the next line/match pair. Call `drain()` after EOF to flush remaining context.
12    fn push(&mut self, line: Line, info: MatchInfo, out: &mut dyn FnMut(EmitOwned));
13
14    /// Called once at EOF to flush any buffered trailing context.
15    fn drain(&mut self, out: &mut dyn FnMut(EmitOwned));
16}
17
18/// Owned form of `Emit` — the expander hands these to the caller.
19#[derive(Debug, Clone)]
20pub struct EmitOwned {
21    pub line: Line,
22    pub role: Role,
23    pub match_info: MatchInfo,
24}
25
26impl EmitOwned {
27    pub fn borrow(&self) -> Emit<'_> {
28        Emit {
29            line: &self.line,
30            role: self.role,
31            match_info: &self.match_info,
32        }
33    }
34}
35
36/// Emits only hits, nothing else.
37pub struct NoContext;
38
39impl Expander for NoContext {
40    fn push(&mut self, line: Line, info: MatchInfo, out: &mut dyn FnMut(EmitOwned)) {
41        if info.hit {
42            out(EmitOwned {
43                line,
44                role: Role::Target,
45                match_info: info,
46            });
47        }
48    }
49
50    fn drain(&mut self, _out: &mut dyn FnMut(EmitOwned)) {}
51}
52
53/// Emits each hit plus `n` lines before and after, merging overlapping windows.
54pub struct LineContext {
55    n: usize,
56    /// Ring buffer of the last `n` lines (oldest at front).
57    before: VecDeque<(Line, MatchInfo)>,
58    /// Lines remaining to emit as trailing context for a recent hit.
59    trailing: usize,
60    /// Highest line number already emitted (avoids duplicates on overlap).
61    last_emitted: u64,
62}
63
64impl LineContext {
65    pub fn new(n: usize) -> Self {
66        Self {
67            n,
68            before: VecDeque::with_capacity(n),
69            trailing: 0,
70            last_emitted: 0,
71        }
72    }
73
74    fn emit(&mut self, line: Line, info: MatchInfo, role: Role, out: &mut dyn FnMut(EmitOwned)) {
75        if line.no <= self.last_emitted {
76            return;
77        }
78        self.last_emitted = line.no;
79        out(EmitOwned {
80            line,
81            role,
82            match_info: info,
83        });
84    }
85}
86
87impl Expander for LineContext {
88    fn push(&mut self, line: Line, info: MatchInfo, out: &mut dyn FnMut(EmitOwned)) {
89        if info.hit {
90            // Flush stored "before" lines as context.
91            let buffered: Vec<_> = self.before.drain(..).collect();
92            for (bl, bi) in buffered {
93                self.emit(bl, bi, Role::Context, out);
94            }
95            let hit_line = line;
96            let hit_info = info;
97            self.emit(hit_line, hit_info, Role::Target, out);
98            self.trailing = self.n;
99        } else if self.trailing > 0 {
100            self.trailing -= 1;
101            self.emit(line, info, Role::Context, out);
102        } else {
103            // Record as potential "before" context.
104            if self.n > 0 {
105                if self.before.len() == self.n {
106                    self.before.pop_front();
107                }
108                self.before.push_back((line, info));
109            }
110        }
111    }
112
113    fn drain(&mut self, _out: &mut dyn FnMut(EmitOwned)) {
114        // Trailing lines were already emitted as they came. "Before" buffer is just dropped.
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn hit(n: u64) -> (Line, MatchInfo) {
123        (
124            Line::new(n, format!("line{n}").into_bytes()),
125            MatchInfo {
126                hit: true,
127                ..Default::default()
128            },
129        )
130    }
131    fn miss(n: u64) -> (Line, MatchInfo) {
132        (
133            Line::new(n, format!("line{n}").into_bytes()),
134            MatchInfo::default(),
135        )
136    }
137
138    fn collect<E: Expander>(mut e: E, inputs: Vec<(Line, MatchInfo)>) -> Vec<(u64, Role)> {
139        let mut out: Vec<(u64, Role)> = Vec::new();
140        {
141            let mut f = |emit: EmitOwned| out.push((emit.line.no, emit.role));
142            for (l, i) in inputs {
143                e.push(l, i, &mut f);
144            }
145            e.drain(&mut f);
146        }
147        out
148    }
149
150    #[test]
151    fn no_context_emits_only_hits() {
152        let out = collect(NoContext, vec![miss(1), hit(2), miss(3), hit(4)]);
153        assert_eq!(out, vec![(2, Role::Target), (4, Role::Target)]);
154    }
155
156    #[test]
157    fn line_context_emits_around_hit() {
158        let out = collect(
159            LineContext::new(1),
160            vec![miss(1), miss(2), hit(3), miss(4), miss(5)],
161        );
162        assert_eq!(
163            out,
164            vec![(2, Role::Context), (3, Role::Target), (4, Role::Context),]
165        );
166    }
167
168    #[test]
169    fn overlapping_contexts_do_not_duplicate() {
170        let out = collect(LineContext::new(1), vec![miss(1), hit(2), hit(3), miss(4)]);
171        assert_eq!(
172            out,
173            vec![
174                (1, Role::Context),
175                (2, Role::Target),
176                (3, Role::Target),
177                (4, Role::Context),
178            ]
179        );
180    }
181}