1use super::{OutputFormat, OutputFormatter, OutputOptions};
4use crate::types::{GrepResult, Match};
5use anyhow::Result;
6use colored::*;
7
8struct LineContext<'a> {
10 notebook: Option<&'a str>,
11 cell_index: usize,
12 exec_str: &'a str,
13 match_type: &'a str,
14 line_num: usize,
15 line_content: &'a str,
16 marker: &'a str,
17}
18
19pub struct HumanFormatter {
21 options: OutputOptions,
22 use_color: bool,
23}
24
25impl HumanFormatter {
26 pub fn new(options: OutputOptions) -> Self {
27 let use_color = options.color_mode.should_use_color();
28
29 match options.color_mode {
31 super::ColorMode::Always => {
32 colored::control::set_override(true);
33 }
34 super::ColorMode::Never => {
35 colored::control::set_override(false);
36 }
37 super::ColorMode::Auto => {
38 }
40 }
41
42 Self { options, use_color }
43 }
44
45 fn highlight_match(&self, line: &str, matched_text: &str) -> String {
47 if !self.use_color || matched_text.is_empty() {
48 return line.to_string();
49 }
50
51 if let Some(pos) = line.find(matched_text) {
53 let before = &line[..pos];
54 let matched = &line[pos..pos + matched_text.len()];
55 let after = &line[pos + matched_text.len()..];
56 format!("{}{}{}", before, matched.red().bold(), after)
57 } else {
58 line.to_string()
59 }
60 }
61}
62
63impl OutputFormatter for HumanFormatter {
64 fn format_result(&self, result: &GrepResult) -> Result<String> {
65 let mut output = String::new();
66
67 if self.options.count_mode {
68 output.push_str(&format!("{}:{}\n", result.notebook, result.matches.len()));
70 } else if self.options.files_with_matches || self.options.format == OutputFormat::PathsOnly
71 {
72 if !result.matches.is_empty() {
74 output.push_str(&result.notebook);
75 output.push('\n');
76 }
77 } else if self.options.format == OutputFormat::MatchesOnly {
78 for m in &result.matches {
80 let text = if self.use_color {
81 m.matched_text.red().bold().to_string()
82 } else {
83 m.matched_text.clone()
84 };
85 output.push_str(&text);
86 output.push('\n');
87 }
88 } else if self.options.heading_mode || self.options.format == OutputFormat::Grouped {
89 if !result.matches.is_empty() {
91 output.push_str(&result.notebook);
92 output.push('\n');
93
94 if self.options.format == OutputFormat::Grouped {
95 let sep_len = result.notebook.len().min(80);
97 output.push_str(&"─".repeat(sep_len));
98 output.push('\n');
99 }
100
101 for m in &result.matches {
102 output.push_str(&self.format_match_no_filename(m));
103 }
104 output.push('\n');
105 }
106 } else {
107 for m in &result.matches {
109 if self.options.show_filename {
110 output.push_str(&self.format_match(&result.notebook, m));
111 } else {
112 output.push_str(&self.format_match_no_filename(m));
113 }
114 }
115 }
116
117 Ok(output)
118 }
119
120 fn format_results(&self, results: &[GrepResult]) -> Result<String> {
121 let mut output = String::new();
122
123 for result in results {
124 output.push_str(&self.format_result(result)?);
125 }
126
127 Ok(output)
128 }
129}
130
131impl HumanFormatter {
132 fn format_match(&self, notebook: &str, m: &Match) -> String {
134 let mut output = String::new();
135
136 let exec_str = if let Some(count) = m.execution_count {
137 format!("[{count}]")
138 } else {
139 String::new()
140 };
141
142 let highlighted_line = self.highlight_match(m.line_content.trim(), &m.matched_text);
144
145 let has_context = !m.context_before.is_empty() || !m.context_after.is_empty();
147
148 if has_context {
149 for (i, line) in m.context_before.iter().enumerate() {
151 let line_num = m.line_index.saturating_sub(m.context_before.len() - i);
152 let ctx = LineContext {
153 notebook: Some(notebook),
154 cell_index: m.cell_index,
155 exec_str: &exec_str,
156 match_type: &m.match_type.to_string(),
157 line_num,
158 line_content: line.trim(),
159 marker: "-",
160 };
161 output.push_str(&self.format_context_line(&ctx));
162 }
163
164 let ctx = LineContext {
166 notebook: Some(notebook),
167 cell_index: m.cell_index,
168 exec_str: &exec_str,
169 match_type: &m.match_type.to_string(),
170 line_num: m.line_index,
171 line_content: &highlighted_line,
172 marker: ">",
173 };
174 output.push_str(&self.format_main_line(&ctx));
175
176 for (i, line) in m.context_after.iter().enumerate() {
178 let line_num = m.line_index + i + 1;
179 let ctx = LineContext {
180 notebook: Some(notebook),
181 cell_index: m.cell_index,
182 exec_str: &exec_str,
183 match_type: &m.match_type.to_string(),
184 line_num,
185 line_content: line.trim(),
186 marker: "-",
187 };
188 output.push_str(&self.format_context_line(&ctx));
189 }
190
191 output.push_str("--\n");
192 } else {
193 let ctx = LineContext {
195 notebook: Some(notebook),
196 cell_index: m.cell_index,
197 exec_str: &exec_str,
198 match_type: &m.match_type.to_string(),
199 line_num: m.line_index,
200 line_content: &highlighted_line,
201 marker: "",
202 };
203 output.push_str(&self.format_main_line(&ctx));
204 }
205
206 output
207 }
208
209 fn format_match_no_filename(&self, m: &Match) -> String {
211 let mut output = String::new();
212
213 let exec_str = if let Some(count) = m.execution_count {
214 format!("[{count}]")
215 } else {
216 String::new()
217 };
218
219 let highlighted_line = self.highlight_match(m.line_content.trim(), &m.matched_text);
221
222 let has_context = !m.context_before.is_empty() || !m.context_after.is_empty();
224
225 if has_context {
226 for (i, line) in m.context_before.iter().enumerate() {
228 let line_num = m.line_index.saturating_sub(m.context_before.len() - i);
229 let ctx = LineContext {
230 notebook: None,
231 cell_index: m.cell_index,
232 exec_str: &exec_str,
233 match_type: &m.match_type.to_string(),
234 line_num,
235 line_content: line.trim(),
236 marker: "-",
237 };
238 output.push_str(&self.format_context_line(&ctx));
239 }
240
241 let ctx = LineContext {
243 notebook: None,
244 cell_index: m.cell_index,
245 exec_str: &exec_str,
246 match_type: &m.match_type.to_string(),
247 line_num: m.line_index,
248 line_content: &highlighted_line,
249 marker: ">",
250 };
251 output.push_str(&self.format_main_line(&ctx));
252
253 for (i, line) in m.context_after.iter().enumerate() {
255 let line_num = m.line_index + i + 1;
256 let ctx = LineContext {
257 notebook: None,
258 cell_index: m.cell_index,
259 exec_str: &exec_str,
260 match_type: &m.match_type.to_string(),
261 line_num,
262 line_content: line.trim(),
263 marker: "-",
264 };
265 output.push_str(&self.format_context_line(&ctx));
266 }
267
268 output.push_str(" --\n");
269 } else {
270 let ctx = LineContext {
272 notebook: None,
273 cell_index: m.cell_index,
274 exec_str: &exec_str,
275 match_type: &m.match_type.to_string(),
276 line_num: m.line_index,
277 line_content: &highlighted_line,
278 marker: "",
279 };
280 output.push_str(&self.format_main_line(&ctx));
281 }
282
283 output
284 }
285
286 fn format_context_line(&self, ctx: &LineContext) -> String {
288 self.format_line_impl(ctx)
289 }
290
291 fn format_main_line(&self, ctx: &LineContext) -> String {
293 self.format_line_impl(ctx)
294 }
295
296 fn format_line_impl(&self, ctx: &LineContext) -> String {
298 let indent = if ctx.notebook.is_none() { " " } else { "" };
299 let marker_str = if ctx.marker.is_empty() {
300 String::new()
301 } else {
302 format!("{} ", ctx.marker)
303 };
304
305 match self.options.format {
306 OutputFormat::Standard => {
307 let nb_prefix = if let Some(nb) = ctx.notebook {
309 format!("{nb}:")
310 } else {
311 String::new()
312 };
313
314 if self.options.no_line_number {
315 format!(
316 "{marker_str}{indent}{nb_prefix}cell{}{}{}: {}\n",
317 ctx.cell_index, ctx.exec_str, ctx.match_type, ctx.line_content
318 )
319 } else {
320 format!(
321 "{marker_str}{indent}{nb_prefix}cell{}{}:{}:{}: {}\n",
322 ctx.cell_index,
323 ctx.exec_str,
324 ctx.match_type,
325 ctx.line_num,
326 ctx.line_content
327 )
328 }
329 }
330 OutputFormat::Compact => {
331 let nb_prefix = if let Some(nb) = ctx.notebook {
333 format!("{nb}:")
334 } else {
335 String::new()
336 };
337
338 format!(
339 "{}{}{}cell{}:line{}: {}\n",
340 marker_str,
341 indent,
342 nb_prefix,
343 ctx.cell_index + 1,
344 ctx.line_num + 1,
345 ctx.line_content
346 )
347 }
348 OutputFormat::CompactNoCell => {
349 let nb_prefix = if let Some(nb) = ctx.notebook {
351 format!("{nb}:")
352 } else {
353 String::new()
354 };
355
356 format!(
357 "{}{}{}{}: {}\n",
358 marker_str,
359 indent,
360 nb_prefix,
361 ctx.line_num + 1,
362 ctx.line_content
363 )
364 }
365 _ => {
366 let nb_prefix = if let Some(nb) = ctx.notebook {
368 format!("{nb}:")
369 } else {
370 String::new()
371 };
372
373 if self.options.no_line_number {
374 format!(
375 "{marker_str}{indent}{nb_prefix}cell{}{}{}: {}\n",
376 ctx.cell_index, ctx.exec_str, ctx.match_type, ctx.line_content
377 )
378 } else {
379 format!(
380 "{marker_str}{indent}{nb_prefix}cell{}{}:{}:{}: {}\n",
381 ctx.cell_index,
382 ctx.exec_str,
383 ctx.match_type,
384 ctx.line_num,
385 ctx.line_content
386 )
387 }
388 }
389 }
390 }
391}