1use 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 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 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 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 assert_eq!(s, " 1: cdefg\n ^\n");
106 }
107}