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 5e35e26 commit f5cdf9b
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 1 deletion.
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
273 changes: 273 additions & 0 deletions src/handlers/grep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use std::convert::{TryFrom, TryInto};

use lazy_static::lazy_static;
use regex::Regex;

use crate::delta::{State, StateMachine};
use crate::handlers::hunk_header;
use crate::paint::BgShouldFill;
use crate::utils;

struct GrepOutputConfig {
render_context_header_as_hunk_header: bool,
highlight_hits: bool,
}

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> {
let mut output_config = GrepOutputConfig {
render_context_header_as_hunk_header: true,
highlight_hits: true,
};
if let Some((longs, shorts)) = utils::parent_command_options() {
if shorts.contains("-W") || longs.contains("--function-context") {
// --function-context is in effect: i.e. the entire function is
// being displayed. In that case we don't render the first line
// as a header, since the second line is the true next line, and
// it will be more readable to have these displayed normally. We
// highlight hits, since these will be surrounded by non-hits.
output_config = GrepOutputConfig {
render_context_header_as_hunk_header: false,
highlight_hits: true,
}
} else if shorts.contains("-p") || longs.contains("--show-function") {
// --show-function is in effect, i.e. the function header is
// being displayed, along with hits within the function.
// Therefore we render the first line as a header, but we do not
// highlight hits, since all other lines are hits.
output_config = GrepOutputConfig {
render_context_header_as_hunk_header: true,
highlight_hits: false,
}
}
};

// 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);

// 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);

match (
&grep.line_type,
output_config.render_context_header_as_hunk_header,
) {
// Emit context header line
(LineType::ContextHeader, true) => hunk_header::write_hunk_header(
grep.code,
&[(grep.line_number.unwrap_or(0), 0)],
&mut self.painter,
&self.line,
grep.file,
self.config,
)?,
_ => {
// Emit file & line-number
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 code line
self.painter.syntax_highlight_and_paint_line(
&format!("{}\n", grep.code),
if matches!(&grep.line_type, LineType::Hit)
&& output_config.highlight_hits
{
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;
}
}
2 changes: 1 addition & 1 deletion src/handlers/hunk_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ fn write_hunk_header_raw(
Ok(())
}

fn write_hunk_header(
pub fn write_hunk_header(
code_fragment: &str,
line_numbers: &[(usize, usize)],
painter: &mut Painter,
Expand Down
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 f5cdf9b

Please sign in to comment.