summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2023-05-04 22:53:53 -0400
committerDan Davison <dandavison7@gmail.com>2023-06-02 14:36:30 -0700
commit83ca127e9e6a1e78d28e83c7ebe11ac1a5cc5faf (patch)
tree3fc27436d1d581e201de1b1654ebf0f085dbae02
parent46e44d3ba9396cf3256de6d592dcf8f17ba141c2 (diff)
Introduce grep-output-type option: ripgrep or classic
-rw-r--r--README.md4
-rw-r--r--manual/src/grep.md22
-rw-r--r--src/cli.rs46
-rw-r--r--src/config.rs56
-rw-r--r--src/delta.rs4
-rw-r--r--src/features/hyperlinks.rs2
-rw-r--r--src/handlers/grep.rs323
-rw-r--r--src/handlers/hunk_header.rs147
-rw-r--r--src/handlers/ripgrep_json.rs2
-rw-r--r--src/options/set.rs4
-rw-r--r--src/paint.rs2
-rw-r--r--src/parse_styles.rs70
12 files changed, 555 insertions, 127 deletions
diff --git a/README.md b/README.md
index 302de0de..1aa2a6f4 100644
--- a/README.md
+++ b/README.md
@@ -184,7 +184,9 @@ Side-by-side view wraps long lines automatically:
[[User manual](https://dandavison.github.io/delta/grep.html)]
-<table><tr><td><img width=400px src="https://user-images.githubusercontent.com/52205/225271024-a01367f0-af1b-466a-9b9e-b1ced7f80031.png" alt="image" /></td></tr></table>
+<table><tr><td>
+<img width="600px" alt="image" src="https://github.com/dandavison/open-in-editor/assets/52205/d203d380-5acb-4296-aeb9-e38c73d6c27f">
+</td></tr></table>
### Installation and usage
diff --git a/manual/src/grep.md b/manual/src/grep.md
index 60a61d07..a090389c 100644
--- a/manual/src/grep.md
+++ b/manual/src/grep.md
@@ -2,19 +2,33 @@
Delta applies syntax-highlighting and other enhancements to standard grep output such as from [ripgrep](https://github.com/BurntSushi/ripgrep/) (aka `rg`), `git grep`, grep, etc.
If you don't need special features of `git grep`, then for best results pipe `rg --json` output to delta: this avoids parsing ambiguities that are inevitable with the output of `git grep` and `grep`.
-To customize the colors and syntax highlighting, see `grep-match-line-style`, `grep-match-word-style`, `grep-context-line-style`, `grep-file-style`, `grep-line-number-style`.
+To customize the colors and syntax highlighting, see the `grep-*` options in `delta --help`.
+
Note that `git grep` can display the "function context" for matches and that delta handles this output specially: see the `-p` and `-W` options of `git grep`.
```sh
-rg --json handle | delta
+rg --json -C 2 handle | delta
```
-<table><tr><td><img width=400px src="https://user-images.githubusercontent.com/52205/225271024-a01367f0-af1b-466a-9b9e-b1ced7f80031.png" alt="image" /></td></tr></table>
+<table><tr><td>
+<img width="600px" alt="image" src="https://github.com/dandavison/open-in-editor/assets/52205/d203d380-5acb-4296-aeb9-e38c73d6c27f">
+</td></tr></table>
+
+If you enable hyperlinks then grep hits will be formatted as [OSC8 hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) in terminal emulators that support the feature. If you're using VSCode, IntelliJ, or PyCharm, then use the dedicated URL handlers. I.e. one of the following lines:
+
+```gitconfig
+[delta]
+ hyperlinks = true
+ hyperlinks-file-link-format = "vscode://file/{path}:{line}"
+ # or: hyperlinks-file-link-format = "idea://open?file={path}&line={line}"
+ # or: hyperlinks-file-link-format = "pycharm://open?file={path}&line={line}"
+```
-If you enable hyperlinks then grep hits will be formatted as [OSC8 hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) in terminal emulators that support the feature. It is then possible to make your OS handle a click on those links by opening your editor at the correct file and line number (e.g. via <https://github.com/dandavison/open-in-editor/>).
+For editors that don't have special URL handlers, it is possible to use a tool like <https://github.com/dandavison/open-in-editor/> to make your OS handle a click on those links by opening your editor at the correct file and line number, e.g.
```gitconfig
[delta]
hyperlinks = true
hyperlinks-file-link-format = "file-line://{path}:{line}"
+ # Now configure your OS to handle "file-line" URLs
```
diff --git a/src/cli.rs b/src/cli.rs
index 80c73b90..50dab5a6 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -396,17 +396,51 @@ pub struct Opt {
/// See STYLES section. Defaults to zero-style.
pub grep_context_line_style: Option<String>,
- #[arg(long = "grep-file-style", value_name = "STYLE")]
+ #[arg(
+ long = "grep-file-style",
+ default_value = "magenta",
+ value_name = "STYLE"
+ )]
/// Style string for file paths in grep output.
///
- /// See STYLES section. Defaults to hunk-header-file-path-style.
- pub grep_file_style: Option<String>,
+ /// See STYLES section.
+ pub grep_file_style: String,
+
+ #[arg(long = "grep-header-decoration-style", value_name = "STYLE")]
+ /// Style string for the header decoration in grep output.
+ ///
+ /// Default is "none" when grep-ouput-type-is "ripgrep", otherwise defaults
+ /// to value of header-decoration-style. See hunk-header-decoration-style.
+ pub grep_header_decoration_style: Option<String>,
+
+ #[arg(long = "grep-header-file-style", value_name = "STYLE")]
+ /// Style string for the file path part of the header in grep output.
+ ///
+ /// See hunk_header_file_style.
+ pub grep_header_file_style: Option<String>,
+
+ #[arg(long = "grep-header-style", value_name = "STYLE")]
+ /// Style string for the header in grep output.
+ ///
+ /// See hunk-header-style.
+ pub grep_header_style: Option<String>,
- #[arg(long = "grep-line-number-style", value_name = "STYLE")]
+ #[arg(
+ long = "grep-line-number-style",
+ default_value = "green",
+ value_name = "STYLE"
+ )]
/// Style string for line numbers in grep output.
///
- /// See STYLES section. Defaults to hunk-header-line-number-style.
- pub grep_line_number_style: Option<String>,
+ /// See STYLES section.
+ pub grep_line_number_style: String,
+
+ #[arg(long = "grep-output-type", value_name = "OUTPUT_TYPE")]
+ /// Grep output format. Possible values:
+ /// "ripgrep" - file name printed once, followed by matching lines within that file, each preceded by a line number.
+ /// "classic" - file name:line number, followed by matching line.
+ /// Default is "ripgrep" if `rg --json` format is detected, otherwise "classic".
+ pub grep_output_type: Option<String>,
#[arg(long = "grep-match-line-style", value_name = "STYLE")]
/// Style string for matching lines of grep output.
diff --git a/src/config.rs b/src/config.rs
index 3d411ea4..cab21eb7 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -65,15 +65,19 @@ pub struct Config {
pub git_plus_style: Style,
pub grep_context_line_style: Style,
pub grep_file_style: Style,
+ pub classic_grep_header_file_style: Style,
+ pub classic_grep_header_style: Style,
+ pub ripgrep_header_style: Style,
pub grep_line_number_style: Style,
pub grep_match_line_style: Style,
pub grep_match_word_style: Style,
+ pub grep_output_type: Option<GrepType>,
pub grep_separator_symbol: String,
pub handle_merge_conflicts: bool,
pub hunk_header_file_style: Style,
pub hunk_header_line_number_style: Style,
- pub hunk_header_style_include_file_path: bool,
- pub hunk_header_style_include_line_number: bool,
+ pub hunk_header_style_include_file_path: HunkHeaderIncludeFilePath,
+ pub hunk_header_style_include_line_number: HunkHeaderIncludeLineNumber,
pub hunk_header_style: Style,
pub hunk_label: String,
pub hyperlinks_commit_link_format: Option<String>,
@@ -129,6 +133,24 @@ pub struct Config {
pub zero_style: Style,
}
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum GrepType {
+ Ripgrep,
+ Classic,
+}
+
+#[cfg_attr(test, derive(Clone))]
+pub enum HunkHeaderIncludeFilePath {
+ Yes,
+ No,
+}
+
+#[cfg_attr(test, derive(Clone))]
+pub enum HunkHeaderIncludeLineNumber {
+ Yes,
+ No,
+}
+
impl Config {
pub fn get_style(&self, state: &State) -> &Style {
match state {
@@ -137,6 +159,7 @@ impl Config {
State::HunkPlus(_, _) => &self.plus_style,
State::CommitMeta => &self.commit_style,
State::DiffHeader(_) => &self.file_style,
+ State::Grep(GrepType::Ripgrep, _, _, _) => &self.classic_grep_header_style,
State::HunkHeader(_, _, _, _) => &self.hunk_header_style,
State::SubmoduleLog => &self.file_style,
_ => delta_unreachable("Unreachable code reached in get_style."),
@@ -222,6 +245,13 @@ impl From<cli::Opt> for Config {
opt.navigate_regex
};
+ let grep_output_type = match opt.grep_output_type.as_deref() {
+ Some("ripgrep") => Some(GrepType::Ripgrep),
+ Some("classic") => Some(GrepType::Classic),
+ None => None,
+ _ => fatal("Invalid option for grep-output-type: Expected \"ripgrep\" or \"classic\"."),
+ };
+
#[cfg(not(test))]
let cwd_of_delta_process = opt.env.current_dir;
#[cfg(test)]
@@ -271,22 +301,36 @@ impl From<cli::Opt> for Config {
git_config: opt.git_config,
grep_context_line_style: styles["grep-context-line-style"],
grep_file_style: styles["grep-file-style"],
+ classic_grep_header_file_style: styles["classic-grep-header-file-style"],
+ classic_grep_header_style: styles["classic-grep-header-style"],
+ ripgrep_header_style: styles["ripgrep-header-style"],
grep_line_number_style: styles["grep-line-number-style"],
grep_match_line_style: styles["grep-match-line-style"],
grep_match_word_style: styles["grep-match-word-style"],
+ grep_output_type,
grep_separator_symbol: opt.grep_separator_symbol,
handle_merge_conflicts: !opt.raw,
hunk_header_file_style: styles["hunk-header-file-style"],
hunk_header_line_number_style: styles["hunk-header-line-number-style"],
hunk_header_style: styles["hunk-header-style"],
- hunk_header_style_include_file_path: opt
+ hunk_header_style_include_file_path: if opt
.hunk_header_style
.split(' ')
- .any(|s| s == "file"),
- hunk_header_style_include_line_number: opt
+ .any(|s| s == "file")
+ {
+ HunkHeaderIncludeFilePath::Yes
+ } else {
+ HunkHeaderIncludeFilePath::No
+ },
+ hunk_header_style_include_line_number: if opt
.hunk_header_style
.split(' ')
- .any(|s| s == "line-number"),
+ .any(|s| s == "line-number")
+ {
+ HunkHeaderIncludeLineNumber::Yes
+ } else {
+ HunkHeaderIncludeLineNumber::No
+ },
hyperlinks: opt.hyperlinks,
hyperlinks_commit_link_format: opt.hyperlinks_commit_link_format,
hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
diff --git a/src/delta.rs b/src/delta.rs
index 1b0b2980..7db15575 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -8,7 +8,9 @@ use bytelines::ByteLines;
use crate::ansi;
use crate::config::delta_unreachable;
use crate::config::Config;
+use crate::config::GrepType;
use crate::features;
+use crate::handlers::grep;
use crate::handlers::hunk_header::ParsedHunkHeader;
use crate::handlers::{self, merge_conflict};
use crate::paint::Painter;
@@ -28,7 +30,7 @@ pub enum State {
SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short
Blame(String), // In a line of `git blame` output (key).
GitShowFile, // In a line of `git show $revision:./path/to/file.ext` output
- Grep, // In a line of `git grep` output
+ Grep(GrepType, grep::LineType, String, Option<usize>), // In a line of `git grep` output (grep_type, line_type, path, line_number)
Unknown,
// The following elements are created when a line is wrapped to display it:
HunkZeroWrapped, // Wrapped unchanged line
diff --git a/src/features/hyperlinks.rs b/src/features/hyperlinks.rs
index 4747c131..f96922b4 100644
--- a/src/features/hyperlinks.rs
+++ b/src/features/hyperlinks.rs
@@ -332,6 +332,8 @@ __path__: some matching line
"raw",
"--grep-line-number-style",
"raw",
+ "--grep-output-type",
+ "classic",
"--hunk-header-file-style",
"raw",
"--hunk-header-line-number-style",
diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs
index eec32c8a..f8614f7d 100644
--- a/src/handlers/grep.rs
+++ b/src/handlers/grep.rs
@@ -1,18 +1,25 @@
use std::borrow::Cow;
+use std::fmt::Write;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use crate::ansi;
+use crate::config::{
+ delta_unreachable, GrepType, HunkHeaderIncludeFilePath, HunkHeaderIncludeLineNumber,
+};
use crate::delta::{State, StateMachine};
use crate::handlers::{self, ripgrep_json};
use crate::paint::{self, BgShouldFill, StyleSectionSpecifier};
use crate::style::Style;
use crate::utils::{process, tabs};
+use super::hunk_header::HunkHeaderIncludeHunkLabel;
+
#[derive(Debug, PartialEq, Eq)]
pub struct GrepLine<'b> {
+ pub grep_type: GrepType,
pub path: Cow<'b, str>,
pub line_number: Option<usize>,
pub line_type: LineType,
@@ -25,6 +32,7 @@ pub struct GrepLine<'b> {
pub enum LineType {
ContextHeader,
Context,
+ FileHeader,
Match,
Ignore,
}
@@ -39,84 +47,228 @@ lazy_static! {
static ref OUTPUT_CONFIG: GrepOutputConfig = make_output_config();
}
+impl LineType {
+ fn file_path_separator(&self) -> &str {
+ // 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 self {
+ LineType::Match => ":",
+ LineType::Context => "-",
+ LineType::ContextHeader => "=",
+ LineType::Ignore | LineType::FileHeader => "",
+ }
+ }
+}
+
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);
+ let (previous_path, previous_line_type, previous_line, try_parse) = match &self.state {
+ State::Grep(_, line_type, path, line_number) => {
+ (Some(path.clone()), Some(line_type), line_number, true)
+ }
+ State::Unknown => (None, None, &None, true),
+ _ => (None, None, &None, false),
+ };
+ let mut handled_line = false;
if try_parse {
let line = self.line.clone(); // TODO: avoid clone
- if let Some(mut grep_line) = parse_grep_line(&line) {
+ if let Some(grep_line) = parse_grep_line(&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(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,
- )?,
- _ => self._handle_grep_line(&mut grep_line)?,
+ let first_path = previous_path.is_none();
+ let new_path = first_path || previous_path.as_deref() != Some(&grep_line.path);
+ // Emit a '--' section separator when output contains context lines (i.e. *grep option -A, -B, -C is in effect).
+ let new_section = !new_path
+ && (previous_line_type == Some(&LineType::Context)
+ || grep_line.line_type == LineType::Context)
+ && previous_line < &grep_line.line_number.as_ref().map(|n| n - 1);
+ self.state = State::Grep(
+ self.config
+ .grep_output_type
+ .clone()
+ .unwrap_or_else(|| grep_line.grep_type.clone()),
+ grep_line.line_type,
+ grep_line.path.to_string(),
+ grep_line.line_number,
+ );
+ if new_path {
+ if let Some(lang) = handlers::diff_header::get_extension(&grep_line.path)
+ .or(self.config.default_language.as_deref())
+ {
+ self.painter.set_syntax(Some(lang));
+ self.painter.set_highlighter();
+ }
}
+ match &self.state {
+ State::Grep(GrepType::Ripgrep, _, _, _) => self.emit_ripgrep_format_grep_line(
+ grep_line,
+ new_path,
+ first_path,
+ new_section,
+ ),
+ State::Grep(GrepType::Classic, _, _, _) => {
+ self.emit_classic_format_grep_line(grep_line)
+ }
+ _ => delta_unreachable("Impossible state while handling grep line."),
+ }?;
handled_line = true
}
}
Ok(handled_line)
}
- fn _handle_grep_line(&mut self, grep_line: &mut GrepLine) -> std::io::Result<()> {
- if self.config.navigate {
- write!(
- self.painter.writer,
- "{}",
- match (
- &grep_line.line_type,
- OUTPUT_CONFIG.add_navigate_marker_to_matches
- ) {
- (LineType::Match, true) => "• ",
- (_, true) => " ",
- _ => "",
- }
+ // Emulate ripgrep output: each section of hits from the same path has a header line,
+ // and sections are separated by a blank line. Set language whenever path changes.
+ fn emit_ripgrep_format_grep_line(
+ &mut self,
+ mut grep_line: GrepLine,
+ new_path: bool,
+ first_path: bool,
+ new_section: bool,
+ ) -> std::io::Result<()> {
+ if new_path {
+ // Emit new path header line
+ if !first_path {
+ let _ = self.painter.output_buffer.write_char('\n');
+ }
+ handlers::hunk_header::write_line_of_code_with_optional_path_and_line_number(
+ "",
+ &[(0, 0)],
+ None,
+ &mut self.painter,
+ &self.line,
+ &grep_line.path,
+ self.config.ripgrep_header_style.decoration_style,
+ &self.config.grep_file_style,
+ &self.config.grep_line_number_style,
+ &HunkHeaderIncludeFilePath::Yes,
+ &HunkHeaderIncludeLineNumber::No,
+ &HunkHeaderIncludeHunkLabel::Yes,
+ "",
+ self.config,
)?
}
- self._emit_file_and_line_number(grep_line)?;
- self._emit_code(grep_line)?;
+ if new_section {
+ let _ = self.painter.output_buffer.write_str("--\n");
+ }
+ // Emit the actual grep hit line
+ let code_style_sections = match (&grep_line.line_type, &grep_line.submatches) {
+ (LineType::Match, Some(submatches)) => {
+ // We expand tabs at this late stage because
+ // the tabs are escaped in the JSON, so
+ // expansion must come after JSON parsing.
+ // (At the time of writing, we are in this
+ // arm iff we are handling `ripgrep --json`
+ // output.)
+ grep_line.code = tabs::expand(&grep_line.code, &self.config.tab_cfg).into();
+ make_style_sections(
+ &grep_line.code,
+ submatches,
+ self.config.grep_match_word_style,
+ self.config.grep_match_line_style,
+ )
+ }
+ (LineType::Match, None) => {
+ // HACK: We need tabs expanded, and we need
+ // the &str passed to
+ // `get_code_style_sections` to live long
+ // enough. But at this point it is guaranteed
+ // that this handler is going to handle this
+ // line, so mutating it is acceptable.
+ self.raw_line = tabs::expand(&self.raw_line, &self.config.tab_cfg);
+ get_code_style_sections(
+ &self.raw_line,
+ self.config.grep_match_word_style,
+ self.config.grep_match_line_style,
+ &grep_line,
+ )
+ .unwrap_or(StyleSectionSpecifier::Style(
+ self.config.grep_match_line_style,
+ ))
+ }
+ _ => StyleSectionSpecifier::Style(self.config.grep_context_line_style),
+ };
+ handlers::hunk_header::write_line_of_code_with_optional_path_and_line_number(
+ &grep_line.code,
+ &[(grep_line.line_number.unwrap_or(0), 0)],
+ Some(code_style_sections),
+ &mut self.painter,
+ &self.line,
+ &grep_line.path,
+ crate::style::DecorationStyle::NoDecoration,
+ &self.config.grep_file_style,
+ &self.config.grep_line_number_style,
+ &HunkHeaderIncludeFilePath::No,
+ if grep_line.line_number.is_some() {
+ &HunkHeaderIncludeLineNumber::Yes
+ } else {
+ &HunkHeaderIncludeLineNumber::No
+ },
+ &HunkHeaderIncludeHunkLabel::No,
+ grep_line.line_type.file_path_separator(),
+ self