1use super::{FormatOpts, Formatter, ansi};
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 let target_col_1 = emit
24 .match_info
25 .col
26 .or_else(|| emit.match_info.spans.first().map(|r| r.start + 1))
27 .unwrap_or(1);
28
29 let bytes = &emit.line.bytes;
30 let col_idx = target_col_1
31 .saturating_sub(1)
32 .min(bytes.len().saturating_sub(1));
33 let start = col_idx.saturating_sub(self.char_context);
34 let end = bytes.len().min(col_idx + self.char_context + 1);
35
36 let frag = String::from_utf8_lossy(&bytes[start..end]).to_string();
37 let prefix = self.opts.prefix(emit.line.no);
38
39 let rendered = if let Some(span) = emit.match_info.spans.first() {
41 if self.opts.color {
42 let hs = span.start.saturating_sub(start).min(frag.len());
43 let he = (span.end.saturating_sub(start)).min(frag.len());
44 if hs < he {
45 let (a, rest) = frag.split_at(hs);
46 let (b, c) = rest.split_at(he - hs);
47 format!("{a}{}{b}{}{c}", ansi::INVERSE, ansi::RESET)
48 } else {
49 frag.clone()
50 }
51 } else {
52 frag.clone()
53 }
54 } else {
55 frag.clone()
56 };
57
58 writeln!(sink, "{prefix}{rendered}")?;
59
60 let caret_offset = col_idx - start + prefix.len();
62 let spaces = " ".repeat(caret_offset);
63 let caret = ansi::paint(self.opts.color, ansi::GREEN, "^");
64 writeln!(sink, "{spaces}{caret}")
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::{Line, MatchInfo, Role};
72
73 #[test]
74 fn renders_fragment_with_caret_under_col() {
75 let line = Line::new(1, b"abcdefghij".to_vec());
76 let mi = MatchInfo {
77 hit: true,
78 col: Some(5),
79 ..Default::default()
80 };
81 let emit = Emit {
82 line: &line,
83 role: Role::Target,
84 match_info: &mi,
85 };
86 let opts = FormatOpts {
87 show_line_numbers: false,
88 show_filename: false,
89 filename: None,
90 color: false,
91 target_marker: false,
92 };
93 let mut f = FragmentFormatter::new(opts, 2);
94 let mut buf: Vec<u8> = Vec::new();
95 f.write(&mut buf, &emit).unwrap();
96 let s = String::from_utf8(buf).unwrap();
97 assert_eq!(s, "cdefg\n ^\n");
100 }
101}