Skip to main content

sel/format/
fragment.rs

1//! Fragment formatter — char-context window with caret.
2//!
3//! Used when `-n` is set (positional selectors or `-e` + `-n`).
4
5use super::{FormatOpts, Formatter, ansi, push_lossy};
6use crate::Emit;
7use std::io::{self, Write};
8
9pub struct FragmentFormatter {
10    pub opts: FormatOpts,
11    pub char_context: usize,
12}
13
14impl FragmentFormatter {
15    pub fn new(opts: FormatOpts, char_context: usize) -> Self {
16        Self { opts, char_context }
17    }
18}
19
20impl Formatter for FragmentFormatter {
21    fn write(&mut self, sink: &mut dyn Write, emit: &Emit) -> io::Result<()> {
22        self.opts.widen_for_line(emit.line.no);
23        // Target column: from position matcher (`col`), else start of first regex span.
24        let target_col_1 = emit
25            .match_info
26            .col
27            .or_else(|| emit.match_info.spans.first().map(|r| r.start + 1))
28            .unwrap_or(1);
29
30        let bytes = &emit.line.bytes;
31        let col_idx = target_col_1
32            .saturating_sub(1)
33            .min(bytes.len().saturating_sub(1));
34        let start = col_idx.saturating_sub(self.char_context);
35        let end = bytes.len().min(col_idx + self.char_context + 1);
36
37        let frag_bytes = &bytes[start..end];
38        let prefix = self.opts.prefix(emit.line.no);
39
40        // Highlight the target span within the fragment if regex spans exist.
41        let rendered = if let Some(span) = emit.match_info.spans.first() {
42            if self.opts.color {
43                let hs = span.start.max(start).min(end) - start;
44                let he = span.end.max(start).min(end) - start;
45                if hs < he {
46                    let mut out = String::new();
47                    push_lossy(&mut out, &frag_bytes[..hs]);
48                    out.push_str(ansi::INVERSE);
49                    push_lossy(&mut out, &frag_bytes[hs..he]);
50                    out.push_str(ansi::RESET);
51                    push_lossy(&mut out, &frag_bytes[he..]);
52                    out
53                } else {
54                    String::from_utf8_lossy(frag_bytes).to_string()
55                }
56            } else {
57                String::from_utf8_lossy(frag_bytes).to_string()
58            }
59        } else {
60            String::from_utf8_lossy(frag_bytes).to_string()
61        };
62
63        writeln!(sink, "{prefix}{rendered}")?;
64
65        // Caret line, aligned under the target column within the fragment.
66        let caret_offset = col_idx - start + prefix.len();
67        let spaces = " ".repeat(caret_offset);
68        let caret = ansi::paint(self.opts.color, ansi::GREEN, "^");
69        writeln!(sink, "{spaces}{caret}")
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::{Line, MatchInfo, Role};
77
78    #[test]
79    fn renders_fragment_with_caret_under_col() {
80        let line = Line::new(1, b"abcdefghij".to_vec());
81        let mi = MatchInfo {
82            hit: true,
83            col: Some(5),
84            ..Default::default()
85        };
86        let emit = Emit {
87            line: &line,
88            role: Role::Target,
89            match_info: &mi,
90        };
91        let opts = FormatOpts {
92            show_line_numbers: true,
93            show_filename: false,
94            filename: None,
95            color: false,
96            target_marker: false,
97            line_number_width: 4,
98        };
99        let mut f = FragmentFormatter::new(opts, 2);
100        let mut buf: Vec<u8> = Vec::new();
101        f.write(&mut buf, &emit).unwrap();
102        let s = String::from_utf8(buf).unwrap();
103        // Fragment: col=5 with context=2 → bytes [2..7] = "cdefg"
104        // Caret at col 5 → offset 2 in fragment
105        assert_eq!(s, "   1: cdefg\n        ^\n");
106    }
107}