1use super::{FormatOpts, Formatter, ansi};
4use crate::{Emit, Role};
5use std::io::{self, Write};
6use std::ops::Range;
7
8pub struct PlainFormatter {
9 pub opts: FormatOpts,
10}
11
12impl PlainFormatter {
13 pub fn new(opts: FormatOpts) -> Self {
14 Self { opts }
15 }
16
17 fn render_content(&self, bytes: &[u8], spans: &[Range<usize>]) -> String {
18 let text = String::from_utf8_lossy(bytes);
19 if !self.opts.color || spans.is_empty() {
20 return text.to_string();
21 }
22 let mut sorted = spans.to_vec();
23 sorted.sort_by_key(|r| r.start);
24 let mut out = String::new();
25 let mut cursor = 0usize;
26 let t: &str = text.as_ref();
27 for r in sorted {
28 let s = r.start.min(t.len());
29 let e = r.end.min(t.len());
30 if s < cursor {
31 continue;
32 }
33 out.push_str(&t[cursor..s]);
34 out.push_str(ansi::INVERSE);
35 out.push_str(&t[s..e]);
36 out.push_str(ansi::RESET);
37 cursor = e;
38 }
39 out.push_str(&t[cursor..]);
40 out
41 }
42}
43
44impl Formatter for PlainFormatter {
45 fn write(&mut self, sink: &mut dyn Write, emit: &Emit) -> io::Result<()> {
46 let marker = match emit.role {
47 Role::Target if self.opts.target_marker => {
48 ansi::paint(self.opts.color, ansi::GREEN, ">") + " "
49 }
50 _ => String::new(),
51 };
52 let prefix = self.opts.prefix(emit.line.no);
53 let content = self.render_content(&emit.line.bytes, &emit.match_info.spans);
54 writeln!(sink, "{marker}{prefix}{content}")
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::{Line, MatchInfo};
62
63 fn opts(color: bool) -> FormatOpts {
64 FormatOpts {
65 show_line_numbers: true,
66 show_filename: false,
67 filename: None,
68 color,
69 target_marker: true,
70 }
71 }
72
73 #[test]
74 fn target_gets_marker_and_prefix() {
75 let line = Line::new(7, b"hello".to_vec());
76 let mi = MatchInfo {
77 hit: true,
78 ..Default::default()
79 };
80 let emit = Emit {
81 line: &line,
82 role: Role::Target,
83 match_info: &mi,
84 };
85 let mut f = PlainFormatter::new(opts(false));
86 let mut buf: Vec<u8> = Vec::new();
87 f.write(&mut buf, &emit).unwrap();
88 assert_eq!(String::from_utf8(buf).unwrap(), "> 7:hello\n");
89 }
90
91 #[test]
92 fn context_has_no_marker() {
93 let line = Line::new(3, b"ctx".to_vec());
94 let mi = MatchInfo::default();
95 let emit = Emit {
96 line: &line,
97 role: Role::Context,
98 match_info: &mi,
99 };
100 let mut f = PlainFormatter::new(opts(false));
101 let mut buf: Vec<u8> = Vec::new();
102 f.write(&mut buf, &emit).unwrap();
103 assert_eq!(String::from_utf8(buf).unwrap(), "3:ctx\n");
104 }
105
106 #[test]
107 #[allow(clippy::single_range_in_vec_init)]
108 fn spans_highlight_when_color_enabled() {
109 let line = Line::new(1, b"an ERROR today".to_vec());
110 let mi = MatchInfo {
111 hit: true,
112 spans: vec![3..8],
113 ..Default::default()
114 };
115 let emit = Emit {
116 line: &line,
117 role: Role::Target,
118 match_info: &mi,
119 };
120 let mut f = PlainFormatter::new(opts(true));
121 let mut buf: Vec<u8> = Vec::new();
122 f.write(&mut buf, &emit).unwrap();
123 let s = String::from_utf8(buf).unwrap();
124 assert!(s.contains("\x1b[7mERROR\x1b[0m"));
125 }
126}