1
0
mirror of https://github.com/BurntSushi/ripgrep.git synced 2025-04-24 17:12:16 +02:00

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
This commit is contained in:
Andrew Gallant 2019-04-13 18:35:24 -04:00
parent a7d26c8f14
commit ece1f50cfe
8 changed files with 520 additions and 111 deletions

View File

@ -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):

View File

@ -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

View File

@ -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'

View File

@ -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;
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();

View File

@ -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]
}

View File

@ -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!("\

View File

@ -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))

View File

@ -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");