From ece1f50cfe49fea324d96f97a2ae00ebbd0cad03 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sat, 13 Apr 2019 18:35:24 -0400 Subject: [PATCH] printer: support previews for long lines This commit adds support for showing a preview of long lines. While the default still remains as completely suppressing the entire line, this new functionality will show the first N graphemes of a matching line, including the number of matches that are suppressed. This was unfortunately a fairly invasive change to the printer that required a bit of refactoring. On the bright side, the single line and multi-line coloring are now more unified than they were before. Closes #1078 --- CHANGELOG.md | 2 + GUIDE.md | 3 +- complete/_rg | 4 + grep-printer/src/standard.rs | 542 ++++++++++++++++++++++++++++------- grep-printer/src/util.rs | 13 +- src/app.rs | 25 ++ src/args.rs | 7 + tests/feature.rs | 35 +++ 8 files changed, 520 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad6dc11..0d737baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Feature enhancements: * [FEATURE #855](https://github.com/BurntSushi/ripgrep/issues/855): Add `--binary` flag for disabling binary file filtering. +* [FEATURE #1078](https://github.com/BurntSushi/ripgrep/pull/1078): + Add `--max-column-preview` flag for showing a preview of long lines. * [FEATURE #1099](https://github.com/BurntSushi/ripgrep/pull/1099): Add support for Brotli and Zstd to the `-z/--search-zip` flag. * [FEATURE #1138](https://github.com/BurntSushi/ripgrep/pull/1138): diff --git a/GUIDE.md b/GUIDE.md index 907ab382..f6316137 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -538,8 +538,9 @@ formatting peculiarities: ``` $ cat $HOME/.ripgreprc -# Don't let ripgrep vomit really long lines to my terminal. +# Don't let ripgrep vomit really long lines to my terminal, and show a preview. --max-columns=150 +--max-column-preview # Add my 'web' type. --type-add diff --git a/complete/_rg b/complete/_rg index 882a38d6..6c2ee2bb 100644 --- a/complete/_rg +++ b/complete/_rg @@ -148,6 +148,10 @@ _rg() { $no"--no-crlf[don't use CRLF as line terminator]" '(text)--null-data[use NUL as line terminator]' + + '(max-column-preview)' # max column preview options + '--max-column-preview[show preview for long lines (with -M)]' + $no"--no-max-column-preview[don't show preview for long lines (with -M)]" + + '(max-depth)' # Directory-depth options '--max-depth=[specify max number of directories to descend]:number of directories' '!--maxdepth=:number of directories' diff --git a/grep-printer/src/standard.rs b/grep-printer/src/standard.rs index 068f96a4..f21b3675 100644 --- a/grep-printer/src/standard.rs +++ b/grep-printer/src/standard.rs @@ -17,10 +17,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor}; use color::ColorSpecs; use counter::CounterWriter; use stats::Stats; -use util::{ - PrinterPath, Replacer, Sunk, - trim_ascii_prefix, trim_ascii_prefix_range, -}; +use util::{PrinterPath, Replacer, Sunk, trim_ascii_prefix}; /// The configuration for the standard printer. /// @@ -37,6 +34,7 @@ struct Config { per_match: bool, replacement: Arc>>, max_columns: Option, + max_column_preview: bool, max_matches: Option, column: bool, byte_offset: bool, @@ -60,6 +58,7 @@ impl Default for Config { per_match: false, replacement: Arc::new(None), max_columns: None, + max_column_preview: false, max_matches: None, column: false, byte_offset: false, @@ -264,6 +263,21 @@ impl StandardBuilder { self } + /// When enabled, if a line is found to be over the configured maximum + /// column limit (measured in terms of bytes), then a preview of the long + /// line will be printed instead. + /// + /// The preview will correspond to the first `N` *grapheme clusters* of + /// the line, where `N` is the limit configured by `max_columns`. + /// + /// If no limit is set, then enabling this has no effect. + /// + /// This is disabled by default. + pub fn max_column_preview(&mut self, yes: bool) -> &mut StandardBuilder { + self.config.max_column_preview = yes; + self + } + /// Set the maximum amount of matching lines that are printed. /// /// If multi line search is enabled and a match spans multiple lines, then @@ -1023,43 +1037,11 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { )?; count += 1; if self.exceeds_max_columns(&bytes[line]) { - self.write_exceeded_line()?; - continue; + self.write_exceeded_line(bytes, line, matches, &mut midx)?; + } else { + self.write_colored_matches(bytes, line, matches, &mut midx)?; + self.write_line_term()?; } - if self.has_line_terminator(&bytes[line]) { - line = line.with_end(line.end() - 1); - } - if self.config().trim_ascii { - line = self.trim_ascii_prefix_range(bytes, line); - } - - while !line.is_empty() { - if matches[midx].end() <= line.start() { - if midx + 1 < matches.len() { - midx += 1; - continue; - } else { - self.end_color_match()?; - self.write(&bytes[line])?; - break; - } - } - let m = matches[midx]; - - if line.start() < m.start() { - let upto = cmp::min(line.end(), m.start()); - self.end_color_match()?; - self.write(&bytes[line.with_end(upto)])?; - line = line.with_start(upto); - } else { - let upto = cmp::min(line.end(), m.end()); - self.start_color_match()?; - self.write(&bytes[line.with_end(upto)])?; - line = line.with_start(upto); - } - } - self.end_color_match()?; - self.write_line_term()?; } Ok(()) } @@ -1074,12 +1056,8 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { let mut stepper = LineStep::new(line_term, 0, bytes.len()); while let Some((start, end)) = stepper.next(bytes) { let mut line = Match::new(start, end); - if self.has_line_terminator(&bytes[line]) { - line = line.with_end(line.end() - 1); - } - if self.config().trim_ascii { - line = self.trim_ascii_prefix_range(bytes, line); - } + self.trim_line_terminator(bytes, &mut line); + self.trim_ascii_prefix(bytes, &mut line); while !line.is_empty() { if matches[midx].end() <= line.start() { if midx + 1 < matches.len() { @@ -1102,14 +1080,19 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { Some(m.start() as u64 + 1), )?; - let buf = &bytes[line.with_end(upto)]; + let this_line = line.with_end(upto); line = line.with_start(upto); - if self.exceeds_max_columns(&buf) { - self.write_exceeded_line()?; - continue; + if self.exceeds_max_columns(&bytes[this_line]) { + self.write_exceeded_line( + bytes, + this_line, + matches, + &mut midx, + )?; + } else { + self.write_spec(spec, &bytes[this_line])?; + self.write_line_term()?; } - self.write_spec(spec, buf)?; - self.write_line_term()?; } } count += 1; @@ -1140,15 +1123,11 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { )?; count += 1; if self.exceeds_max_columns(&bytes[line]) { - self.write_exceeded_line()?; + self.write_exceeded_line(bytes, line, &[m], &mut 0)?; continue; } - if self.has_line_terminator(&bytes[line]) { - line = line.with_end(line.end() - 1); - } - if self.config().trim_ascii { - line = self.trim_ascii_prefix_range(bytes, line); - } + self.trim_line_terminator(bytes, &mut line); + self.trim_ascii_prefix(bytes, &mut line); while !line.is_empty() { if m.end() <= line.start() { @@ -1205,7 +1184,10 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { line: &[u8], ) -> io::Result<()> { if self.exceeds_max_columns(line) { - self.write_exceeded_line()?; + let range = Match::new(0, line.len()); + self.write_exceeded_line( + line, range, self.sunk.matches(), &mut 0, + )?; } else { self.write_trim(line)?; if !self.has_line_terminator(line) { @@ -1218,50 +1200,114 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { fn write_colored_line( &self, matches: &[Match], - line: &[u8], + bytes: &[u8], ) -> io::Result<()> { // If we know we aren't going to emit color, then we can go faster. let spec = self.config().colors.matched(); if !self.wtr().borrow().supports_color() || spec.is_none() { - return self.write_line(line); - } - if self.exceeds_max_columns(line) { - return self.write_exceeded_line(); + return self.write_line(bytes); } - let mut last_written = - if !self.config().trim_ascii { - 0 - } else { - self.trim_ascii_prefix_range( - line, - Match::new(0, line.len()), - ).start() - }; - for mut m in matches.iter().map(|&m| m) { - if last_written < m.start() { + let line = Match::new(0, bytes.len()); + if self.exceeds_max_columns(bytes) { + self.write_exceeded_line(bytes, line, matches, &mut 0) + } else { + self.write_colored_matches(bytes, line, matches, &mut 0)?; + self.write_line_term()?; + Ok(()) + } + } + + /// Write the `line` portion of `bytes`, with appropriate coloring for + /// each `match`, starting at `match_index`. + /// + /// This accounts for trimming any whitespace prefix and will *never* print + /// a line terminator. If a match exceeds the range specified by `line`, + /// then only the part of the match within `line` (if any) is printed. + fn write_colored_matches( + &self, + bytes: &[u8], + mut line: Match, + matches: &[Match], + match_index: &mut usize, + ) -> io::Result<()> { + self.trim_line_terminator(bytes, &mut line); + self.trim_ascii_prefix(bytes, &mut line); + if matches.is_empty() { + self.write(&bytes[line])?; + return Ok(()); + } + while !line.is_empty() { + if matches[*match_index].end() <= line.start() { + if *match_index + 1 < matches.len() { + *match_index += 1; + continue; + } else { + self.end_color_match()?; + self.write(&bytes[line])?; + break; + } + } + + let m = matches[*match_index]; + if line.start() < m.start() { + let upto = cmp::min(line.end(), m.start()); self.end_color_match()?; - self.write(&line[last_written..m.start()])?; - } else if last_written < m.end() { - m = m.with_start(last_written); + self.write(&bytes[line.with_end(upto)])?; + line = line.with_start(upto); } else { - continue; - } - if !m.is_empty() { + let upto = cmp::min(line.end(), m.end()); self.start_color_match()?; - self.write(&line[m])?; + self.write(&bytes[line.with_end(upto)])?; + line = line.with_start(upto); } - last_written = m.end(); } self.end_color_match()?; - self.write(&line[last_written..])?; - if !self.has_line_terminator(line) { - self.write_line_term()?; - } Ok(()) } - fn write_exceeded_line(&self) -> io::Result<()> { + fn write_exceeded_line( + &self, + bytes: &[u8], + mut line: Match, + matches: &[Match], + match_index: &mut usize, + ) -> io::Result<()> { + if self.config().max_column_preview { + let original = line; + let end = BStr::new(&bytes[line]) + .grapheme_indices() + .map(|(_, end, _)| end) + .take(self.config().max_columns.unwrap_or(0) as usize) + .last() + .unwrap_or(0) + line.start(); + line = line.with_end(end); + self.write_colored_matches(bytes, line, matches, match_index)?; + + if matches.is_empty() { + self.write(b" [... omitted end of long line]")?; + } else { + let remaining = matches + .iter() + .filter(|m| { + m.start() >= line.end() && m.start() < original.end() + }) + .count(); + let tense = + if remaining == 1 { + "match" + } else { + "matches" + }; + write!( + self.wtr().borrow_mut(), + " [... {} more {}]", + remaining, tense, + )?; + } + self.write_line_term()?; + return Ok(()); + } if self.sunk.original_matches().is_empty() { if self.is_context() { self.write(b"[Omitted long context line]")?; @@ -1444,13 +1490,26 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { if !self.config().trim_ascii { return self.write(buf); } - self.write(self.trim_ascii_prefix(buf)) + let mut range = Match::new(0, buf.len()); + self.trim_ascii_prefix(buf, &mut range); + self.write(&buf[range]) } fn write(&self, buf: &[u8]) -> io::Result<()> { self.wtr().borrow_mut().write_all(buf) } + fn trim_line_terminator(&self, buf: &[u8], line: &mut Match) { + let lineterm = self.searcher.line_terminator(); + if lineterm.is_suffix(&buf[*line]) { + let mut end = line.end() - 1; + if lineterm.is_crlf() && buf[end - 1] == b'\r' { + end -= 1; + } + *line = line.with_end(end); + } + } + fn has_line_terminator(&self, buf: &[u8]) -> bool { self.searcher.line_terminator().is_suffix(buf) } @@ -1506,14 +1565,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> { /// /// This stops trimming a prefix as soon as it sees non-whitespace or a /// line terminator. - fn trim_ascii_prefix_range(&self, slice: &[u8], range: Match) -> Match { - trim_ascii_prefix_range(self.searcher.line_terminator(), slice, range) - } - - /// Trim prefix ASCII spaces from the given slice and return the - /// corresponding sub-slice. - fn trim_ascii_prefix<'s>(&self, slice: &'s [u8]) -> &'s [u8] { - trim_ascii_prefix(self.searcher.line_terminator(), slice) + fn trim_ascii_prefix(&self, slice: &[u8], range: &mut Match) { + if !self.config().trim_ascii { + return; + } + let lineterm = self.searcher.line_terminator(); + *range = trim_ascii_prefix(lineterm, slice, *range) } } @@ -2280,6 +2337,31 @@ but Doctor Watson has to have it taken out for him and dusted, assert_eq_printed!(expected, got); } + #[test] + fn max_columns_preview() { + let matcher = RegexMatcher::new("exhibited|dusted").unwrap(); + let mut printer = StandardBuilder::new() + .max_columns(Some(46)) + .max_column_preview(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +but Doctor Watson has to have it taken out for [... omitted end of long line] +and exhibited clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + #[test] fn max_columns_with_count() { let matcher = RegexMatcher::new("cigar|ash|dusted").unwrap(); @@ -2305,6 +2387,86 @@ but Doctor Watson has to have it taken out for him and dusted, assert_eq_printed!(expected, got); } + #[test] + fn max_columns_with_count_preview_no_match() { + let matcher = RegexMatcher::new("exhibited|has to have it").unwrap(); + let mut printer = StandardBuilder::new() + .stats(true) + .max_columns(Some(46)) + .max_column_preview(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +but Doctor Watson has to have it taken out for [... 0 more matches] +and exhibited clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + + #[test] + fn max_columns_with_count_preview_one_match() { + let matcher = RegexMatcher::new("exhibited|dusted").unwrap(); + let mut printer = StandardBuilder::new() + .stats(true) + .max_columns(Some(46)) + .max_column_preview(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +but Doctor Watson has to have it taken out for [... 1 more match] +and exhibited clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + + #[test] + fn max_columns_with_count_preview_two_matches() { + let matcher = RegexMatcher::new( + "exhibited|dusted|has to have it", + ).unwrap(); + let mut printer = StandardBuilder::new() + .stats(true) + .max_columns(Some(46)) + .max_column_preview(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +but Doctor Watson has to have it taken out for [... 1 more match] +and exhibited clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + #[test] fn max_columns_multi_line() { let matcher = RegexMatcher::new("(?s)ash.+dusted").unwrap(); @@ -2330,6 +2492,36 @@ but Doctor Watson has to have it taken out for him and dusted, assert_eq_printed!(expected, got); } + #[test] + fn max_columns_multi_line_preview() { + let matcher = RegexMatcher::new( + "(?s)clew|cigar ash.+have it|exhibited", + ).unwrap(); + let mut printer = StandardBuilder::new() + .stats(true) + .max_columns(Some(46)) + .max_column_preview(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .multi_line(true) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +can extract a clew from a wisp of straw or a f [... 1 more match] +but Doctor Watson has to have it taken out for [... 0 more matches] +and exhibited clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + #[test] fn max_matches() { let matcher = RegexMatcher::new("Sherlock").unwrap(); @@ -2619,8 +2811,40 @@ Holmeses, success in the province of detective work must always assert_eq_printed!(expected, got); } + #[test] + fn only_matching_max_columns_preview() { + let matcher = RegexMatcher::new("Doctor Watsons|Sherlock").unwrap(); + let mut printer = StandardBuilder::new() + .only_matching(true) + .max_columns(Some(10)) + .max_column_preview(true) + .column(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(true) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +1:9:Doctor Wat [... 0 more matches] +1:57:Sherlock +3:49:Sherlock +"; + assert_eq_printed!(expected, got); + } + #[test] fn only_matching_max_columns_multi_line1() { + // The `(?s:.{0})` trick fools the matcher into thinking that it + // can match across multiple lines without actually doing so. This is + // so we can test multi-line handling in the case of a match on only + // one line. let matcher = RegexMatcher::new( r"(?s:.{0})(Doctor Watsons|Sherlock)" ).unwrap(); @@ -2649,6 +2873,41 @@ Holmeses, success in the province of detective work must always assert_eq_printed!(expected, got); } + #[test] + fn only_matching_max_columns_preview_multi_line1() { + // The `(?s:.{0})` trick fools the matcher into thinking that it + // can match across multiple lines without actually doing so. This is + // so we can test multi-line handling in the case of a match on only + // one line. + let matcher = RegexMatcher::new( + r"(?s:.{0})(Doctor Watsons|Sherlock)" + ).unwrap(); + let mut printer = StandardBuilder::new() + .only_matching(true) + .max_columns(Some(10)) + .max_column_preview(true) + .column(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .multi_line(true) + .line_number(true) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +1:9:Doctor Wat [... 0 more matches] +1:57:Sherlock +3:49:Sherlock +"; + assert_eq_printed!(expected, got); + } + #[test] fn only_matching_max_columns_multi_line2() { let matcher = RegexMatcher::new( @@ -2680,6 +2939,38 @@ Holmeses, success in the province of detective work must always assert_eq_printed!(expected, got); } + #[test] + fn only_matching_max_columns_preview_multi_line2() { + let matcher = RegexMatcher::new( + r"(?s)Watson.+?(Holmeses|clearly)" + ).unwrap(); + let mut printer = StandardBuilder::new() + .only_matching(true) + .max_columns(Some(50)) + .max_column_preview(true) + .column(true) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .multi_line(true) + .line_number(true) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +1:16:Watsons of this world, as opposed to the Sherlock +2:16:Holmeses +5:12:Watson has to have it taken out for him and dusted [... 0 more matches] +6:12:and exhibited clearly +"; + assert_eq_printed!(expected, got); + } + #[test] fn per_match() { let matcher = RegexMatcher::new("Doctor Watsons|Sherlock").unwrap(); @@ -2875,6 +3166,61 @@ Holmeses, success in the province of detective work must always assert_eq_printed!(expected, got); } + #[test] + fn replacement_max_columns_preview1() { + let matcher = RegexMatcher::new(r"Sherlock|Doctor (\w+)").unwrap(); + let mut printer = StandardBuilder::new() + .max_columns(Some(67)) + .max_column_preview(true) + .replacement(Some(b"doctah $1 MD".to_vec())) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(true) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +1:For the doctah Watsons MD of this world, as opposed to the doctah [... 0 more matches] +3:be, to a very large extent, the result of luck. doctah MD Holmes +5:but doctah Watson MD has to have it taken out for him and dusted, +"; + assert_eq_printed!(expected, got); + } + + #[test] + fn replacement_max_columns_preview2() { + let matcher = RegexMatcher::new( + "exhibited|dusted|has to have it", + ).unwrap(); + let mut printer = StandardBuilder::new() + .max_columns(Some(43)) + .max_column_preview(true) + .replacement(Some(b"xxx".to_vec())) + .build(NoColor::new(vec![])); + SearcherBuilder::new() + .line_number(false) + .build() + .search_reader( + &matcher, + SHERLOCK.as_bytes(), + printer.sink(&matcher), + ) + .unwrap(); + + let got = printer_contents(&mut printer); + let expected = "\ +but Doctor Watson xxx taken out for him and [... 1 more match] +and xxx clearly, with a label attached. +"; + assert_eq_printed!(expected, got); + } + #[test] fn replacement_only_matching() { let matcher = RegexMatcher::new(r"Sherlock|Doctor (\w+)").unwrap(); diff --git a/grep-printer/src/util.rs b/grep-printer/src/util.rs index 16d23685..7a20f3b7 100644 --- a/grep-printer/src/util.rs +++ b/grep-printer/src/util.rs @@ -346,7 +346,7 @@ impl Serialize for NiceDuration { /// /// This stops trimming a prefix as soon as it sees non-whitespace or a line /// terminator. -pub fn trim_ascii_prefix_range( +pub fn trim_ascii_prefix( line_term: LineTerminator, slice: &[u8], range: Match, @@ -366,14 +366,3 @@ pub fn trim_ascii_prefix_range( .count(); range.with_start(range.start() + count) } - -/// Trim prefix ASCII spaces from the given slice and return the corresponding -/// sub-slice. -pub fn trim_ascii_prefix(line_term: LineTerminator, slice: &[u8]) -> &[u8] { - let range = trim_ascii_prefix_range( - line_term, - slice, - Match::new(0, slice.len()), - ); - &slice[range] -} diff --git a/src/app.rs b/src/app.rs index d062699f..a3c14d95 100644 --- a/src/app.rs +++ b/src/app.rs @@ -582,6 +582,7 @@ pub fn all_args_and_flags() -> Vec { flag_line_number(&mut args); flag_line_regexp(&mut args); flag_max_columns(&mut args); + flag_max_column_preview(&mut args); flag_max_count(&mut args); flag_max_depth(&mut args); flag_max_filesize(&mut args); @@ -1443,6 +1444,30 @@ When this flag is omitted or is set to 0, then it has no effect. args.push(arg); } +fn flag_max_column_preview(args: &mut Vec) { + const SHORT: &str = "Print a preview for lines exceeding the limit."; + const LONG: &str = long!("\ +When the '--max-columns' flag is used, ripgrep will by default completely +replace any line that is too long with a message indicating that a matching +line was removed. When this flag is combined with '--max-columns', a preview +of the line (corresponding to the limit size) is shown instead, where the part +of the line exceeding the limit is not shown. + +If the '--max-columns' flag is not set, then this has no effect. + +This flag can be disabled with '--no-max-column-preview'. +"); + let arg = RGArg::switch("max-column-preview") + .help(SHORT).long_help(LONG) + .overrides("no-max-column-preview"); + args.push(arg); + + let arg = RGArg::switch("no-max-column-preview") + .hidden() + .overrides("max-column-preview"); + args.push(arg); +} + fn flag_max_count(args: &mut Vec) { const SHORT: &str = "Limit the number of matches."; const LONG: &str = long!("\ diff --git a/src/args.rs b/src/args.rs index 6a5f09f9..1a5b8a31 100644 --- a/src/args.rs +++ b/src/args.rs @@ -746,6 +746,7 @@ impl ArgMatches { .per_match(self.is_present("vimgrep")) .replacement(self.replacement()) .max_columns(self.max_columns()?) + .max_column_preview(self.max_column_preview()) .max_matches(self.max_count()?) .column(self.column()) .byte_offset(self.is_present("byte-offset")) @@ -1142,6 +1143,12 @@ impl ArgMatches { Ok(self.usize_of_nonzero("max-columns")?.map(|n| n as u64)) } + /// Returns true if and only if a preview should be shown for lines that + /// exceed the maximum column limit. + fn max_column_preview(&self) -> bool { + self.is_present("max-column-preview") + } + /// The maximum number of matches permitted. fn max_count(&self) -> Result> { Ok(self.usize_of("max-count")?.map(|n| n as u64)) diff --git a/tests/feature.rs b/tests/feature.rs index a3e2f441..6ee2bf87 100644 --- a/tests/feature.rs +++ b/tests/feature.rs @@ -630,6 +630,41 @@ rgtest!(f993_null_data, |dir: Dir, mut cmd: TestCommand| { eqnice!(expected, cmd.stdout()); }); +// See: https://github.com/BurntSushi/ripgrep/issues/1078 +// +// N.B. There are many more tests in the grep-printer crate. +rgtest!(f1078_max_column_preview1, |dir: Dir, mut cmd: TestCommand| { + dir.create("sherlock", SHERLOCK); + cmd.args(&[ + "-M46", "--max-column-preview", + "exhibited|dusted|has to have it", + ]); + + let expected = "\ +sherlock:but Doctor Watson has to have it taken out for [... omitted end of long line] +sherlock:and exhibited clearly, with a label attached. +"; + eqnice!(expected, cmd.stdout()); +}); + +rgtest!(f1078_max_column_preview2, |dir: Dir, mut cmd: TestCommand| { + dir.create("sherlock", SHERLOCK); + cmd.args(&[ + "-M43", "--max-column-preview", + // Doing a replacement forces ripgrep to show the number of remaining + // matches. Normally, this happens by default when printing a tty with + // colors. + "-rxxx", + "exhibited|dusted|has to have it", + ]); + + let expected = "\ +sherlock:but Doctor Watson xxx taken out for him and [... 1 more match] +sherlock:and xxx clearly, with a label attached. +"; + eqnice!(expected, cmd.stdout()); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/1138 rgtest!(f1138_no_ignore_dot, |dir: Dir, mut cmd: TestCommand| { dir.create_dir(".git");