diff --git a/src/delta.rs b/src/delta.rs index fbf45c53d..fe8be9260 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -23,6 +23,7 @@ pub enum State { SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short Blame(String, Option), // In a line of `git blame` output (commit, repeat_blame_line). + Grep(String, Option), // In a line of `git grep` output (file, repeat_grep_line). Unknown, // The following elements are created when a line is wrapped to display it: HunkZeroWrapped, // Wrapped unchanged line @@ -121,6 +122,7 @@ impl<'a> StateMachine<'a> { || self.handle_submodule_short_line()? || self.handle_hunk_line()? || self.handle_blame_line()? + || self.handle_grep_line()? || self.should_skip_line() || self.emit_line_unchanged()?; } diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs new file mode 100644 index 000000000..e55bcc443 --- /dev/null +++ b/src/handlers/grep.rs @@ -0,0 +1,210 @@ +use std::convert::{TryFrom, TryInto}; + +use lazy_static::lazy_static; +use regex::Regex; + +use crate::delta::{State, StateMachine}; +use crate::paint::BgShouldFill; + +impl<'a> StateMachine<'a> { + /// If this is a line of git grep output then render it accordingly. If this + /// is the first grep line, then set the syntax-highlighter language. + pub fn handle_grep_line(&mut self) -> std::io::Result { + // TODO: It should be possible to eliminate some of the .clone()s and + // .to_owned()s. + let mut handled_line = false; + self.painter.emit()?; + let (_previous_file, repeat_grep_line, try_parse) = match &self.state { + State::Grep(file, repeat_grep_line) => { + (Some(file.as_str()), repeat_grep_line.clone(), true) + } + State::Unknown => (None, None, true), + _ => (None, None, false), + }; + if try_parse { + if let Some(grep) = parse_git_grep_line(&self.line) { + // let is_repeat = previous_file == Some(grep.file); + let grep_line = format!( + "{:<35}:{:<4}│ ", + grep.file, + grep.line_number + .map(|n| format!("{}", n)) + .unwrap_or_else(|| "".into()) + ); + let style = self.config.file_style; + write!(self.painter.writer, "{}", style.paint(grep_line))?; + + // Emit syntax-highlighted code + if matches!(self.state, State::Unknown) { + if let Some(lang) = self.config.default_language.as_ref() { + self.painter.set_syntax(Some(lang)); + self.painter.set_highlighter(); + } + } + self.state = State::Grep(grep.file.to_owned(), repeat_grep_line); + self.painter.syntax_highlight_and_paint_line( + &format!("{}\n", grep.code), + style, + self.state.clone(), + BgShouldFill::default(), + ); + handled_line = true + } + } + Ok(handled_line) + } +} + +#[derive(Debug, PartialEq)] +pub struct GrepLine<'a> { + pub file: &'a str, + pub line_number: Option, + pub line_type: LineType, + pub code: &'a str, +} + +#[derive(Debug, PartialEq)] +pub enum LineType { + ContextHeader, + Hit, + NoHit, +} + +// See tests for example grep lines +lazy_static! { + static ref GREP_LINE_REGEX: Regex = Regex::new( + r"(?x) +^ +(.+?) # 1. file name (non-greedy: stop at line-type marker) +(?: + [-=:]([0-9]+) # 2. optional line number +)? +([-=:]) # 3. line-type marker +(.*) # 4. code (i.e. line contents) +$ +" + ) + .unwrap(); +} + +pub fn parse_git_grep_line(line: &str) -> Option { + let caps = GREP_LINE_REGEX.captures(line)?; + let file = caps.get(1).unwrap().as_str(); + let line_number = caps.get(2).map(|m| m.as_str().parse().ok()).flatten(); + let line_type = caps.get(3).map(|m| m.as_str()).try_into().ok()?; + let code = caps.get(4).unwrap().as_str(); + + Some(GrepLine { + file, + line_number, + line_type, + code, + }) +} + +impl TryFrom> for LineType { + type Error = (); + fn try_from(from: Option<&str>) -> Result { + match from { + Some(marker) if marker == "=" => Ok(LineType::ContextHeader), + Some(marker) if marker == ":" => Ok(LineType::Hit), + Some(marker) if marker == "-" => Ok(LineType::NoHit), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::handlers::grep::{parse_git_grep_line, GrepLine, LineType}; + + #[test] + fn test_parse_grep_line() { + // git grep MinusPlus + assert_eq!( + parse_git_grep_line("src/config.rs:use crate::minusplus::MinusPlus;"), + Some(GrepLine { + file: "src/config.rs", + line_number: None, + line_type: LineType::Hit, + code: "use crate::minusplus::MinusPlus;", + }) + ); + + // git grep -n MinusPlus [with line numbers] + assert_eq!( + parse_git_grep_line("src/config.rs:21:use crate::minusplus::MinusPlus;"), + Some(GrepLine { + file: "src/config.rs", + line_number: Some(21), + line_type: LineType::Hit, + code: "use crate::minusplus::MinusPlus;", + }) + ); + + // git grep -W MinusPlus [with function context] + assert_eq!( + parse_git_grep_line("src/config.rs=pub struct Config {"), // hit + Some(GrepLine { + file: "src/config.rs", + line_number: None, + line_type: LineType::ContextHeader, + code: "pub struct Config {", + }) + ); + assert_eq!( + parse_git_grep_line("src/config.rs- pub available_terminal_width: usize,"), + Some(GrepLine { + file: "src/config.rs", + line_number: None, + line_type: LineType::NoHit, + code: " pub available_terminal_width: usize,", + }) + ); + assert_eq!( + parse_git_grep_line( + "src/config.rs: pub line_numbers_style_minusplus: MinusPlus