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:
parent
a7d26c8f14
commit
ece1f50cfe
@ -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):
|
||||
|
3
GUIDE.md
3
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
|
||||
|
@ -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'
|
||||
|
@ -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();
|
||||
|
@ -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]
|
||||
}
|
||||
|
25
src/app.rs
25
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!("\
|
||||
|
@ -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))
|
||||
|
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user