use std::borrow::Cow;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use unicode_segmentation::UnicodeSegmentation;
use crate::ansi;
use crate::delta::{State, StateMachine};
use crate::handlers::{self, ripgrep_json};
use crate::paint::{self, expand_tabs, BgShouldFill, StyleSectionSpecifier};
use crate::style::Style;
use crate::utils::process;
#[derive(Debug, PartialEq)]
pub struct GrepLine<'b> {
pub path: Cow<'b, str>,
pub line_number: Option<usize>,
pub line_type: LineType,
pub code: Cow<'b, str>,
pub submatches: Option<Vec<(usize, usize)>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LineType {
ContextHeader,
Context,
Match,
Ignore,
}
struct GrepOutputConfig {
add_navigate_marker_to_matches: bool,
render_context_header_as_hunk_header: bool,
pad_line_number: bool,
}
lazy_static! {
static ref OUTPUT_CONFIG: GrepOutputConfig = make_output_config();
}
impl<'a> StateMachine<'a> {
// If this is a line of git grep output then render it accordingly.
pub fn handle_grep_line(&mut self) -> std::io::Result<bool> {
self.painter.emit()?;
let mut handled_line = false;
let try_parse = matches!(&self.state, State::Grep | State::Unknown);
if try_parse {
if let Some(mut grep_line) = parse_grep_line(&self.line) {
if matches!(grep_line.line_type, LineType::Ignore) {
handled_line = true;
return Ok(handled_line);
}
// Emit syntax-highlighted code
// TODO: Determine the language less frequently, e.g. only when the file changes.
if let Some(lang) = handlers::diff_header::get_extension(&grep_line.path)
.or_else(|| self.config.default_language.as_deref())
{
self.painter.set_syntax(Some(lang));
self.painter.set_highlighter();
}
self.state = State::Grep;
match (
&grep_line.line_type,
OUTPUT_CONFIG.render_context_header_as_hunk_header,
) {
// Emit context header line
(LineType::ContextHeader, true) => handlers::hunk_header::write_hunk_header(
&grep_line.code,
&[(grep_line.line_number.unwrap_or(0), 0)],
&mut self.painter,
&self.line,
&grep_line.path,
self.config,
)?,
_ => {
if self.config.navigate {
write!(
self.painter.writer,
"{}",
match (
&grep_line.line_type,
OUTPUT_CONFIG.add_navigate_marker_to_matches
) {
(LineType::Match, true) => "• ",
(_, true) => " ",
_ => "",
}
)?
}
// Emit file & line-number
let separator = if self.config.grep_separator_symbol == "keep" {
// grep, rg, and git grep use ":" for matching lines
// and "-" for non-matching lines (and `git grep -W`
// uses "=" for a context header line).
match grep_line.line_type {
LineType::Match => ":",
LineType::Context => "-",
LineType::ContextHeader => "=",
LineType::Ignore => "",
}
} else {
// But ":" results in a "file/path:number:"
// construct that terminal emulators are more likely
// to recognize and render as a clickable link. If
// navigate is enabled then there is already a good
// visual indicator of match lines (in addition to
// the grep-match-style highlighting) and so we use
// ":" for matches and non-matches alike.
&self.config.grep_separator_symbol
};
write!(
self.painter.writer,
"{}",
paint::paint_file_path_with_line_number(
grep_line.line_number,
&grep_line.path,
OUTPUT_CONFIG.pad_line_number,
separator,
true,
Some(self.config.grep_file_style),
Some(self.config.grep_line_number_style),
self.config