use super::{OutputFormat, OutputFormatter, OutputOptions};
use crate::types::{GrepResult, Match};
use anyhow::Result;
use colored::*;
struct LineContext<'a> {
notebook: Option<&'a str>,
cell_index: usize,
exec_str: &'a str,
match_type: &'a str,
line_num: usize,
line_content: &'a str,
marker: &'a str,
}
pub struct HumanFormatter {
options: OutputOptions,
use_color: bool,
}
impl HumanFormatter {
pub fn new(options: OutputOptions) -> Self {
let use_color = options.color_mode.should_use_color();
match options.color_mode {
super::ColorMode::Always => {
colored::control::set_override(true);
}
super::ColorMode::Never => {
colored::control::set_override(false);
}
super::ColorMode::Auto => {
}
}
Self { options, use_color }
}
fn highlight_match(&self, line: &str, matched_text: &str) -> String {
if !self.use_color || matched_text.is_empty() {
return line.to_string();
}
if let Some(pos) = line.find(matched_text) {
let before = &line[..pos];
let matched = &line[pos..pos + matched_text.len()];
let after = &line[pos + matched_text.len()..];
format!("{}{}{}", before, matched.red().bold(), after)
} else {
line.to_string()
}
}
}
impl OutputFormatter for HumanFormatter {
fn format_result(&self, result: &GrepResult) -> Result<String> {
let mut output = String::new();
if self.options.count_mode {
output.push_str(&format!("{}:{}\n", result.notebook, result.matches.len()));
} else if self.options.files_with_matches || self.options.format == OutputFormat::PathsOnly
{
if !result.matches.is_empty() {
output.push_str(&result.notebook);
output.push('\n');
}
} else if self.options.format == OutputFormat::MatchesOnly {
for m in &result.matches {
let text = if self.use_color {
m.matched_text.red().bold().to_string()
} else {
m.matched_text.clone()
};
output.push_str(&text);
output.push('\n');
}
} else if self.options.heading_mode || self.options.format == OutputFormat::Grouped {
if !result.matches.is_empty() {
output.push_str(&result.notebook);
output.push('\n');
if self.options.format == OutputFormat::Grouped {
let sep_len = result.notebook.len().min(80);
output.push_str(&"─".repeat(sep_len));
output.push('\n');
}
for m in &result.matches {
output.push_str(&self.format_match_no_filename(m));
}
output.push('\n');
}
} else {
for m in &result.matches {
if self.options.show_filename {
output.push_str(&self.format_match(&result.notebook, m));
} else {
output.push_str(&self.format_match_no_filename(m));
}
}
}
Ok(output)
}
fn format_results(&self, results: &[GrepResult]) -> Result<String> {
let mut output = String::new();
for result in results {
output.push_str(&self.format_result(result)?);
}
Ok(output)
}
}
impl HumanFormatter {
fn format_match(&self, notebook: &str, m: &Match) -> String {
let mut output = String::new();
let exec_str = if let Some(count) = m.execution_count {
format!("[{count}]")
} else {
String::new()
};
let highlighted_line = self.highlight_match(m.line_content.trim(), &m.matched_text);
let has_context = !m.context_before.is_empty() || !m.context_after.is_empty();
if has_context {
for (i, line) in m.context_before.iter().enumerate() {
let line_num = m.line_index.saturating_sub(m.context_before.len() - i);
let ctx = LineContext {
notebook: Some(notebook),
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num,
line_content: line.trim(),
marker: "-",
};
output.push_str(&self.format_context_line(&ctx));
}
let ctx = LineContext {
notebook: Some(notebook),
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num: m.line_index,
line_content: &highlighted_line,
marker: ">",
};
output.push_str(&self.format_main_line(&ctx));
for (i, line) in m.context_after.iter().enumerate() {
let line_num = m.line_index + i + 1;
let ctx = LineContext {
notebook: Some(notebook),
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num,
line_content: line.trim(),
marker: "-",
};
output.push_str(&self.format_context_line(&ctx));
}
output.push_str("--\n");
} else {
let ctx = LineContext {
notebook: Some(notebook),
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num: m.line_index,
line_content: &highlighted_line,
marker: "",
};
output.push_str(&self.format_main_line(&ctx));
}
output
}
fn format_match_no_filename(&self, m: &Match) -> String {
let mut output = String::new();
let exec_str = if let Some(count) = m.execution_count {
format!("[{count}]")
} else {
String::new()
};
let highlighted_line = self.highlight_match(m.line_content.trim(), &m.matched_text);
let has_context = !m.context_before.is_empty() || !m.context_after.is_empty();
if has_context {
for (i, line) in m.context_before.iter().enumerate() {
let line_num = m.line_index.saturating_sub(m.context_before.len() - i);
let ctx = LineContext {
notebook: None,
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num,
line_content: line.trim(),
marker: "-",
};
output.push_str(&self.format_context_line(&ctx));
}
let ctx = LineContext {
notebook: None,
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num: m.line_index,
line_content: &highlighted_line,
marker: ">",
};
output.push_str(&self.format_main_line(&ctx));
for (i, line) in m.context_after.iter().enumerate() {
let line_num = m.line_index + i + 1;
let ctx = LineContext {
notebook: None,
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num,
line_content: line.trim(),
marker: "-",
};
output.push_str(&self.format_context_line(&ctx));
}
output.push_str(" --\n");
} else {
let ctx = LineContext {
notebook: None,
cell_index: m.cell_index,
exec_str: &exec_str,
match_type: &m.match_type.to_string(),
line_num: m.line_index,
line_content: &highlighted_line,
marker: "",
};
output.push_str(&self.format_main_line(&ctx));
}
output
}
fn format_context_line(&self, ctx: &LineContext) -> String {
self.format_line_impl(ctx)
}
fn format_main_line(&self, ctx: &LineContext) -> String {
self.format_line_impl(ctx)
}
fn format_line_impl(&self, ctx: &LineContext) -> String {
let indent = if ctx.notebook.is_none() { " " } else { "" };
let marker_str = if ctx.marker.is_empty() {
String::new()
} else {
format!("{} ", ctx.marker)
};
match self.options.format {
OutputFormat::Standard => {
let nb_prefix = if let Some(nb) = ctx.notebook {
format!("{nb}:")
} else {
String::new()
};
if self.options.no_line_number {
format!(
"{marker_str}{indent}{nb_prefix}cell{}{}{}: {}\n",
ctx.cell_index, ctx.exec_str, ctx.match_type, ctx.line_content
)
} else {
format!(
"{marker_str}{indent}{nb_prefix}cell{}{}:{}:{}: {}\n",
ctx.cell_index,
ctx.exec_str,
ctx.match_type,
ctx.line_num,
ctx.line_content
)
}
}
OutputFormat::Compact => {
let nb_prefix = if let Some(nb) = ctx.notebook {
format!("{nb}:")
} else {
String::new()
};
format!(
"{}{}{}cell{}:line{}: {}\n",
marker_str,
indent,
nb_prefix,
ctx.cell_index + 1,
ctx.line_num + 1,
ctx.line_content
)
}
OutputFormat::CompactNoCell => {
let nb_prefix = if let Some(nb) = ctx.notebook {
format!("{nb}:")
} else {
String::new()
};
format!(
"{}{}{}{}: {}\n",
marker_str,
indent,
nb_prefix,
ctx.line_num + 1,
ctx.line_content
)
}
_ => {
let nb_prefix = if let Some(nb) = ctx.notebook {
format!("{nb}:")
} else {
String::new()
};
if self.options.no_line_number {
format!(
"{marker_str}{indent}{nb_prefix}cell{}{}{}: {}\n",
ctx.cell_index, ctx.exec_str, ctx.match_type, ctx.line_content
)
} else {
format!(
"{marker_str}{indent}{nb_prefix}cell{}{}:{}:{}: {}\n",
ctx.cell_index,
ctx.exec_str,
ctx.match_type,
ctx.line_num,
ctx.line_content
)
}
}
}
}
}