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 self.opts.widen_for_line(emit.line.no);
47 let marker = match emit.role {
48 Role::Target if self.opts.target_marker => {
49 ansi::paint(self.opts.color, ansi::GREEN, ">") + " "
50 }
51 _ => String::new(),
52 };
53 let prefix = self.opts.prefix(emit.line.no);
54 let content = self.render_content(&emit.line.bytes, &emit.match_info.spans);
55 writeln!(sink, "{marker}{prefix}{content}")
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::{Line, MatchInfo};
63
64 fn opts(color: bool) -> FormatOpts {
65 FormatOpts {
66 show_line_numbers: true,
67 show_filename: false,
68 filename: None,
69 color,
70 target_marker: true,
71 line_number_width: 4,
72 }
73 }
74
75 #[test]
76 fn target_gets_marker_and_prefix() {
77 let line = Line::new(7, b"hello".to_vec());
78 let mi = MatchInfo {
79 hit: true,
80 ..Default::default()
81 };
82 let emit = Emit {
83 line: &line,
84 role: Role::Target,
85 match_info: &mi,
86 };
87 let mut f = PlainFormatter::new(opts(false));
88 let mut buf: Vec<u8> = Vec::new();
89 f.write(&mut buf, &emit).unwrap();
90 assert_eq!(String::from_utf8(buf).unwrap(), "> 7: hello\n");
91 }
92
93 #[test]
94 fn context_has_no_marker() {
95 let line = Line::new(3, b"ctx".to_vec());
96 let mi = MatchInfo::default();
97 let emit = Emit {
98 line: &line,
99 role: Role::Context,
100 match_info: &mi,
101 };
102 let mut f = PlainFormatter::new(opts(false));
103 let mut buf: Vec<u8> = Vec::new();
104 f.write(&mut buf, &emit).unwrap();
105 assert_eq!(String::from_utf8(buf).unwrap(), " 3: ctx\n");
106 }
107
108 #[test]
109 fn filename_prefix_keeps_padded_line_number() {
110 let line = Line::new(7, b"hello".to_vec());
111 let mi = MatchInfo {
112 hit: true,
113 ..Default::default()
114 };
115 let emit = Emit {
116 line: &line,
117 role: Role::Target,
118 match_info: &mi,
119 };
120 let mut opts = opts(false);
121 opts.show_filename = true;
122 opts.filename = Some("input.txt".to_string());
123 let mut f = PlainFormatter::new(opts);
124 let mut buf: Vec<u8> = Vec::new();
125 f.write(&mut buf, &emit).unwrap();
126 assert_eq!(String::from_utf8(buf).unwrap(), "> input.txt: 7: hello\n");
127 }
128
129 #[test]
130 fn width_grows_for_large_line_numbers() {
131 let line = Line::new(10000, b"wide".to_vec());
132 let mi = MatchInfo {
133 hit: true,
134 ..Default::default()
135 };
136 let emit = Emit {
137 line: &line,
138 role: Role::Target,
139 match_info: &mi,
140 };
141 let mut f = PlainFormatter::new(opts(false));
142 let mut buf: Vec<u8> = Vec::new();
143 f.write(&mut buf, &emit).unwrap();
144 assert_eq!(String::from_utf8(buf).unwrap(), "> 10000: wide\n");
145 }
146
147 #[test]
148 #[allow(clippy::single_range_in_vec_init)]
149 fn spans_highlight_when_color_enabled() {
150 let line = Line::new(1, b"an ERROR today".to_vec());
151 let mi = MatchInfo {
152 hit: true,
153 spans: vec![3..8],
154 ..Default::default()
155 };
156 let emit = Emit {
157 line: &line,
158 role: Role::Target,
159 match_info: &mi,
160 };
161 let mut f = PlainFormatter::new(opts(true));
162 let mut buf: Vec<u8> = Vec::new();
163 f.write(&mut buf, &emit).unwrap();
164 let s = String::from_utf8(buf).unwrap();
165 assert!(s.contains("\x1b[7mERROR\x1b[0m"));
166 }
167}