1use super::{FormatOpts, Formatter, ansi, push_lossy};
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 if !self.opts.color || spans.is_empty() {
19 return String::from_utf8_lossy(bytes).to_string();
20 }
21 let mut sorted = spans.to_vec();
22 sorted.sort_by_key(|r| r.start);
23 let mut out = String::new();
24 let mut cursor = 0usize;
25 for r in sorted {
26 let s = r.start.min(bytes.len());
27 let e = r.end.min(bytes.len());
28 if s < cursor {
29 continue;
30 }
31 push_lossy(&mut out, &bytes[cursor..s]);
32 out.push_str(ansi::INVERSE);
33 push_lossy(&mut out, &bytes[s..e]);
34 out.push_str(ansi::RESET);
35 cursor = e;
36 }
37 push_lossy(&mut out, &bytes[cursor..]);
38 out
39 }
40}
41
42impl Formatter for PlainFormatter {
43 fn write(&mut self, sink: &mut dyn Write, emit: &Emit) -> io::Result<()> {
44 self.opts.widen_for_line(emit.line.no);
45 let marker = match emit.role {
46 Role::Target if self.opts.target_marker => {
47 ansi::paint(self.opts.color, ansi::GREEN, ">") + " "
48 }
49 _ => String::new(),
50 };
51 let prefix = self.opts.prefix(emit.line.no);
52 let content = self.render_content(&emit.line.bytes, &emit.match_info.spans);
53 writeln!(sink, "{marker}{prefix}{content}")
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::{Line, MatchInfo};
61
62 fn opts(color: bool) -> FormatOpts {
63 FormatOpts {
64 show_line_numbers: true,
65 show_filename: false,
66 filename: None,
67 color,
68 target_marker: true,
69 line_number_width: 4,
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 fn filename_prefix_keeps_padded_line_number() {
108 let line = Line::new(7, b"hello".to_vec());
109 let mi = MatchInfo {
110 hit: true,
111 ..Default::default()
112 };
113 let emit = Emit {
114 line: &line,
115 role: Role::Target,
116 match_info: &mi,
117 };
118 let mut opts = opts(false);
119 opts.show_filename = true;
120 opts.filename = Some("input.txt".to_string());
121 let mut f = PlainFormatter::new(opts);
122 let mut buf: Vec<u8> = Vec::new();
123 f.write(&mut buf, &emit).unwrap();
124 assert_eq!(String::from_utf8(buf).unwrap(), "> input.txt: 7: hello\n");
125 }
126
127 #[test]
128 fn width_grows_for_large_line_numbers() {
129 let line = Line::new(10000, b"wide".to_vec());
130 let mi = MatchInfo {
131 hit: true,
132 ..Default::default()
133 };
134 let emit = Emit {
135 line: &line,
136 role: Role::Target,
137 match_info: &mi,
138 };
139 let mut f = PlainFormatter::new(opts(false));
140 let mut buf: Vec<u8> = Vec::new();
141 f.write(&mut buf, &emit).unwrap();
142 assert_eq!(String::from_utf8(buf).unwrap(), "> 10000: wide\n");
143 }
144
145 #[test]
146 #[allow(clippy::single_range_in_vec_init)]
147 fn spans_highlight_when_color_enabled() {
148 let line = Line::new(1, b"an ERROR today".to_vec());
149 let mi = MatchInfo {
150 hit: true,
151 spans: vec![3..8],
152 ..Default::default()
153 };
154 let emit = Emit {
155 line: &line,
156 role: Role::Target,
157 match_info: &mi,
158 };
159 let mut f = PlainFormatter::new(opts(true));
160 let mut buf: Vec<u8> = Vec::new();
161 f.write(&mut buf, &emit).unwrap();
162 let s = String::from_utf8(buf).unwrap();
163 assert!(s.contains("\x1b[7mERROR\x1b[0m"));
164 }
165}