Skip to content

Commit

Permalink
Handle git grep output
Browse files Browse the repository at this point in the history
Fixes #769
  • Loading branch information
dandavison committed Nov 11, 2021
1 parent 776b746 commit 89b9d67
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>), // In a line of `git blame` output (commit, repeat_blame_line).
Grep(String, Option<String>), // 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
Expand Down Expand Up @@ -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()?;
}
Expand Down
217 changes: 217 additions & 0 deletions src/handlers/grep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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<bool> {
// 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}│ ",
format!(
"{}:{}",
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),
if matches!(grep.line_type, LineType::Hit) {
self.config.plus_style
} else {
self.config.zero_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<usize>,
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<GrepLine> {
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<Option<&str>> for LineType {
type Error = ();
fn try_from(from: Option<&str>) -> Result<Self, Self::Error> {
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<Style>,"
),
Some(GrepLine {
file: "src/config.rs",
line_number: None,
line_type: LineType::Hit,
code: " pub line_numbers_style_minusplus: MinusPlus<Style>,",
})
);

// git grep -n -W MinusPlus [with line numbers and function context]
assert_eq!(
parse_git_grep_line("src/config.rs=57=pub struct Config {"),
Some(GrepLine {
file: "src/config.rs",
line_number: Some(57),
line_type: LineType::ContextHeader,
code: "pub struct Config {",
})
);
assert_eq!(
parse_git_grep_line("src/config.rs-58- pub available_terminal_width: usize,"),
Some(GrepLine {
file: "src/config.rs",
line_number: Some(58),
line_type: LineType::NoHit,
code: " pub available_terminal_width: usize,",
})
);
assert_eq!(
parse_git_grep_line(
"src/config.rs:95: pub line_numbers_style_minusplus: MinusPlus<Style>,"
),
Some(GrepLine {
file: "src/config.rs",
line_number: Some(95),
line_type: LineType::Hit,
code: " pub line_numbers_style_minusplus: MinusPlus<Style>,",
})
);

// git grep -h MinusPlus [no file names: TODO: handle this?]
//use crate::minusplus::MinusPlus;
}
}
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod draw;
pub mod file_meta;
pub mod file_meta_diff;
pub mod file_meta_misc;
pub mod grep;
pub mod hunk;
pub mod hunk_header;
pub mod submodule;
Expand Down
1 change: 1 addition & 0 deletions src/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ impl<'p> Painter<'p> {
State::HunkHeader(_, _) => true,
State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => false,
State::Blame(_, _) => true,
State::Grep(_, _) => true,
_ => panic!(
"should_compute_syntax_highlighting is undefined for state {:?}",
state
Expand Down

0 comments on commit 89b9d67

Please sign in to comment.