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 13, 2021
1 parent 1d8ce11 commit e5ead88
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 2 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
2 changes: 1 addition & 1 deletion src/handlers/file_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ fn get_file_extension_from_file_meta_line_file_path(path: &str) -> Option<&str>
}

/// Attempt to parse input as a file path and return extension as a &str.
fn get_extension(s: &str) -> Option<&str> {
pub fn get_extension(s: &str) -> Option<&str> {
let path = Path::new(s);
path.extension()
.and_then(|e| e.to_str())
Expand Down
310 changes: 310 additions & 0 deletions src/handlers/grep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
use std::convert::{TryFrom, TryInto};

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

use crate::delta::{State, StateMachine};
use crate::handlers;
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> {
self.painter.emit()?;
let mut handled_line = false;

// TODO: It should be possible to eliminate some of the .clone()s and
// .to_owned()s.
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 output_config = make_output_config();

// Emit syntax-highlighted code
// TODO: Determine the language less frequently, e.g. only when the file changes.
if let Some(lang) = handlers::file_meta::get_extension(grep.file)
.or_else(|| self.config.default_language.as_deref())
{
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) => handlers::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 hit_marker = match (&grep.line_type, output_config.highlight_hits) {
(LineType::Hit, true) => "• ",
(LineType::NoHit, true) => " ",
_ => "",
};
let grep_line = match grep.line_number {
Some(n) => {
format!("{}{}:{:<3}:", hit_marker, grep.file, n)
}
None => format!("{}{}:", hit_marker, grep.file),
};
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)
}
}

fn make_output_config() -> GrepOutputConfig {
match utils::parent_command_options() {
Some((longs, shorts)) 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.
GrepOutputConfig {
render_context_header_as_hunk_header: false,
highlight_hits: true,
}
}
Some((longs, shorts)) 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.
GrepOutputConfig {
render_context_header_as_hunk_header: true,
highlight_hits: false,
}
}
_ => GrepOutputConfig {
render_context_header_as_hunk_header: true,
highlight_hits: false,
},
}
}

#[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 (TODO: it must have an extension)
(?:
[-=:]([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;
}

#[test]
fn test_parse_grep_line_filenames() {
assert_eq!(
parse_git_grep_line("src/con-fig.rs:use crate::minusplus::MinusPlus;"),
Some(GrepLine {
file: "src/con-fig.rs",
line_number: None,
line_type: LineType::Hit,
code: "use crate::minusplus::MinusPlus;",
})
);
assert_eq!(
parse_git_grep_line("src/con-fig.rs-use crate::minusplus::MinusPlus;"),
Some(GrepLine {
file: "src/con-fig.rs",
line_number: None,
line_type: LineType::NoHit,
code: "use crate::minusplus::MinusPlus;",
})
);
assert_eq!(
parse_git_grep_line("de-lta.rs- if self.source == Source::Unknown {"),
Some(GrepLine {
file: "de-lta.rs",
line_number: None,
line_type: LineType::NoHit,
code: " if self.source == Source::Unknown {",
})
);
}
}
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 e5ead88

Please sign in to comment.