summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Gallant <jamslam@gmail.com>2019-04-13 18:35:24 -0400
committerAndrew Gallant <jamslam@gmail.com>2019-04-14 19:29:27 -0400
commitece1f50cfe49fea324d96f97a2ae00ebbd0cad03 (patch)
tree8615570232a16f572702619333b446a0a530772c
parenta7d26c8f144a4957b75f71087a66692d0b25759a (diff)
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
-rw-r--r--CHANGELOG.md2
-rw-r--r--GUIDE.md3
-rw-r--r--complete/_rg4
-rw-r--r--grep-printer/src/standard.rs540
-rw-r--r--grep-printer/src/util.rs13
-rw-r--r--src/app.rs25
-rw-r--r--src/args.rs7
-rw-r--r--tests/feature.rs35
8 files changed, 519 insertions, 110 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<Option<Vec<u8>>>,
max_columns: Option<u64>,
+ max_column_preview: bool,
max_matches: Option<u64>,
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;
- }
- 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.write_exceeded_line(bytes, line, matches, &mut midx)?;
+ } else {
+ self.write_colored_matches(bytes, line, matches, &mut midx)?;
+ self.write_line_term()?;
}
- 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);
+ return self.write_line(bytes);
}
- if self.exceeds_max_columns(line) {
- return self.write_exceeded_line();
+
+ 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(())
}
+ }
- 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() {
+ /// 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)
}
}
@@ -2281,6 +2338,31 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[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();
let mut printer = StandardBuilder::new()
@@ -2306,6 +2388,86 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[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();
let mut printer = StandardBuilder::new()
@@ -2331,6 +2493,36 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[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();
let mut printer = StandardBuilder::new()
@@ -2620,7 +2812,39 @@ Holmeses, success in the province of detective work must always
}
#[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();
@@ -2650,6 +2874,41 @@ Holmeses, success in the province of detective work must always
}
#[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(
r"(?s)Watson.+?(Holmeses|clearly)"
@@ -2681,6 +2940,38 @@ Holmeses, success in the province of detective work must always
}
#[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();
let mut printer = StandardBuilder::new()
@@ -2876,6 +3167,61 @@ Holmeses, success in the province of detective work must always
}
#[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();
let mut printer = StandardBuilder::new()
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<RGArg> {
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<RGArg>) {
+ 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<RGArg>) {
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<Option<u64>> {
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");