From 99fe884536e18bcd9f3c57ce598791d5baa975b1 Mon Sep 17 00:00:00 2001 From: emrebengue Date: Wed, 23 Jul 2025 21:45:17 -0400 Subject: [PATCH] colors: add `highlight` type support for matching lines This lets users highlight non-matching text in matching lines. Closes #3024, Closes #3107 --- CHANGELOG.md | 2 ++ FAQ.md | 4 +-- crates/core/flags/complete/rg.zsh | 3 +- crates/core/flags/defs.rs | 46 +++++++++++++++++++++++++------ crates/printer/src/color.rs | 20 ++++++++++---- crates/printer/src/standard.rs | 30 +++++++++++++++++++- 6 files changed, 88 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a2f6a3..7ed953ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Feature enhancements: When using multithreading, schedule files to search in order given on CLI. * [FEATURE #2943](https://github.com/BurntSushi/ripgrep/issues/2943): Add `aarch64` release artifacts for Windows. +* [FEATURE #3024](https://github.com/BurntSushi/ripgrep/issues/3024): + Add `highlight` color type, for styling non-matching text in a matching line. * [FEATURE #3048](https://github.com/BurntSushi/ripgrep/pull/3048): Globs in ripgrep (and the `globset` crate) now support nested alternates. diff --git a/FAQ.md b/FAQ.md index 52a0ed09..0ca3760c 100644 --- a/FAQ.md +++ b/FAQ.md @@ -285,8 +285,8 @@ As a special case, `--colors '{type}:none'` will clear all colors and styles associated with `{type}`, which lets you start with a clean slate (instead of building on top of ripgrep's default color settings). -Here's an example that makes highlights the matches with a nice blue background -with bolded white text: +Here's an example that highlights the matches with a nice blue background with +bolded white text: ``` $ rg somepattern \ diff --git a/crates/core/flags/complete/rg.zsh b/crates/core/flags/complete/rg.zsh index 25de4c4f..a4b63d2b 100644 --- a/crates/core/flags/complete/rg.zsh +++ b/crates/core/flags/complete/rg.zsh @@ -363,10 +363,11 @@ _rg() { 'column:specify coloring for column numbers' 'line:specify coloring for line numbers' 'match:specify coloring for match text' + 'highlight:specify coloring for matching lines' 'path:specify coloring for file names' ) descr='color/style type' - elif [[ ${IPREFIX#--*=}$PREFIX == (column|line|match|path):[^:]# ]]; then + elif [[ ${IPREFIX#--*=}$PREFIX == (column|line|match|highlight|path):[^:]# ]]; then suf=( -qS: ) tmp=( 'none:clear color/style for type' diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs index deb3961d..f97b849b 100644 --- a/crates/core/flags/defs.rs +++ b/crates/core/flags/defs.rs @@ -751,7 +751,8 @@ the \flag{colors} flag to manually set all color styles to \fBnone\fP: \-\-colors 'path:none' \\ \-\-colors 'line:none' \\ \-\-colors 'column:none' \\ - \-\-colors 'match:none' + \-\-colors 'match:none' \\ + \-\-colors 'highlight:none' .EE .sp " @@ -829,7 +830,7 @@ impl Flag for Colors { "Configure color settings and styles." } fn doc_long(&self) -> &'static str { - r" + r#" This flag specifies color settings for use in the output. This flag may be provided multiple times. Settings are applied iteratively. Pre-existing color labels are limited to one of eight choices: \fBred\fP, \fBblue\fP, \fBgreen\fP, @@ -839,11 +840,11 @@ are limited to \fBnobold\fP, \fBbold\fP, \fBnointense\fP, \fBintense\fP, .sp The format of the flag is \fB{\fP\fItype\fP\fB}:{\fP\fIattribute\fP\fB}:{\fP\fIvalue\fP\fB}\fP. -\fItype\fP should be one of \fBpath\fP, \fBline\fP, \fBcolumn\fP or -\fBmatch\fP. \fIattribute\fP can be \fBfg\fP, \fBbg\fP or \fBstyle\fP. -\fIvalue\fP is either a color (for \fBfg\fP and \fBbg\fP) or a text style. A -special format, \fB{\fP\fItype\fP\fB}:none\fP, will clear all color settings -for \fItype\fP. +\fItype\fP should be one of \fBpath\fP, \fBline\fP, \fBcolumn\fP, +\fBhighlight\fP or \fBmatch\fP. \fIattribute\fP can be \fBfg\fP, \fBbg\fP or +\fBstyle\fP. \fIvalue\fP is either a color (for \fBfg\fP and \fBbg\fP) or a +text style. A special format, \fB{\fP\fItype\fP\fB}:none\fP, will clear all +color settings for \fItype\fP. .sp For example, the following command will change the match color to magenta and the background color for line numbers to yellow: @@ -852,6 +853,17 @@ the background color for line numbers to yellow: rg \-\-colors 'match:fg:magenta' \-\-colors 'line:bg:yellow' .EE .sp +Another example, the following command will "highlight" the non-matching text +in matching lines: +.sp +.EX + rg \-\-colors 'highlight:bg:yellow' \-\-colors 'highlight:fg:black' +.EE +.sp +The "highlight" color type is particularly useful for contrasting matching +lines with surrounding context printed by the \flag{before-context}, +\flag{after-context}, \flag{context} or \flag{passthru} flags. +.sp Extended colors can be used for \fIvalue\fP when the tty supports ANSI color sequences. These are specified as either \fIx\fP (256-color) or .IB x , x , x @@ -874,7 +886,7 @@ or, equivalently, .sp Note that the \fBintense\fP and \fBnointense\fP styles will have no effect when used alongside these extended color codes. -" +"# } fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> { @@ -908,6 +920,24 @@ fn test_colors() { "line:bg:yellow".parse().unwrap() ] ); + + let args = parse_low_raw(["--colors", "highlight:bg:240"]).unwrap(); + assert_eq!(args.colors, vec!["highlight:bg:240".parse().unwrap()]); + + let args = parse_low_raw([ + "--colors", + "match:fg:magenta", + "--colors", + "highlight:bg:blue", + ]) + .unwrap(); + assert_eq!( + args.colors, + vec![ + "match:fg:magenta".parse().unwrap(), + "highlight:bg:blue".parse().unwrap() + ] + ); } /// --column diff --git a/crates/printer/src/color.rs b/crates/printer/src/color.rs index 7d587050..49fb769c 100644 --- a/crates/printer/src/color.rs +++ b/crates/printer/src/color.rs @@ -51,13 +51,13 @@ impl std::fmt::Display for ColorError { ColorError::UnrecognizedOutType(ref name) => write!( f, "unrecognized output type '{}'. Choose from: \ - path, line, column, match.", + path, line, column, match, highlight.", name, ), ColorError::UnrecognizedSpecType(ref name) => write!( f, "unrecognized spec type '{}'. Choose from: \ - fg, bg, style, none.", + fg, bg, style, none.", name, ), ColorError::UnrecognizedColor(_, ref msg) => write!(f, "{}", msg), @@ -70,8 +70,8 @@ impl std::fmt::Display for ColorError { ), ColorError::InvalidFormat(ref original) => write!( f, - "invalid color spec format: '{}'. Valid format \ - is '(path|line|column|match):(fg|bg|style):(value)'.", + "invalid color spec format: '{}'. Valid format is \ + '(path|line|column|match|highlight):(fg|bg|style):(value)'.", original, ), } @@ -90,6 +90,7 @@ pub struct ColorSpecs { line: ColorSpec, column: ColorSpec, matched: ColorSpec, + highlight: ColorSpec, } /// A single color specification provided by the user. @@ -99,7 +100,7 @@ pub struct ColorSpecs { /// The format of a `Spec` is a triple: `{type}:{attribute}:{value}`. Each /// component is defined as follows: /// -/// * `{type}` can be one of `path`, `line`, `column` or `match`. +/// * `{type}` can be one of `path`, `line`, `column`, `match` or `highlight`. /// * `{attribute}` can be one of `fg`, `bg` or `style`. `{attribute}` may also /// be the special value `none`, in which case, `{value}` can be omitted. /// * `{value}` is either a color name (for `fg`/`bg`) or a style instruction. @@ -181,6 +182,7 @@ enum OutType { Line, Column, Match, + Highlight, } /// The specification type. @@ -216,6 +218,7 @@ impl ColorSpecs { OutType::Line => spec.merge_into(&mut merged.line), OutType::Column => spec.merge_into(&mut merged.column), OutType::Match => spec.merge_into(&mut merged.matched), + OutType::Highlight => spec.merge_into(&mut merged.highlight), } } merged @@ -249,6 +252,12 @@ impl ColorSpecs { pub fn matched(&self) -> &ColorSpec { &self.matched } + + /// Return the color specification for coloring entire line if there is a + /// matched text. + pub fn highlight(&self) -> &ColorSpec { + &self.highlight + } } impl UserColorSpec { @@ -348,6 +357,7 @@ impl std::str::FromStr for OutType { "line" => Ok(OutType::Line), "column" => Ok(OutType::Column), "match" => Ok(OutType::Match), + "highlight" => Ok(OutType::Highlight), _ => Err(ColorError::UnrecognizedOutType(s.to_string())), } } diff --git a/crates/printer/src/standard.rs b/crates/printer/src/standard.rs index 3b00e46e..b6f62ffd 100644 --- a/crates/printer/src/standard.rs +++ b/crates/printer/src/standard.rs @@ -1320,6 +1320,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { self.write(&bytes[line])?; return Ok(()); } + self.start_line_highlight()?; while !line.is_empty() { if matches[*match_index].end() <= line.start() { if *match_index + 1 < matches.len() { @@ -1346,6 +1347,7 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { } } self.end_color_match()?; + self.end_line_highlight()?; Ok(()) } @@ -1547,11 +1549,37 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { if !self.in_color_match.get() { return Ok(()); } - self.wtr().borrow_mut().reset()?; + if self.highlight_on() { + self.wtr() + .borrow_mut() + .set_color(self.config().colors.highlight())?; + } else { + self.wtr().borrow_mut().reset()?; + } self.in_color_match.set(false); Ok(()) } + fn highlight_on(&self) -> bool { + !self.config().colors.highlight().is_none() && !self.is_context() + } + + fn start_line_highlight(&self) -> io::Result<()> { + if self.highlight_on() { + self.wtr() + .borrow_mut() + .set_color(self.config().colors.highlight())?; + } + Ok(()) + } + + fn end_line_highlight(&self) -> io::Result<()> { + if self.highlight_on() { + self.wtr().borrow_mut().reset()?; + } + Ok(()) + } + fn write(&self, buf: &[u8]) -> io::Result<()> { self.wtr().borrow_mut().write_all(buf) }