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):
|
* [FEATURE #855](https://github.com/BurntSushi/ripgrep/issues/855):
|
||||||
Add `--binary` flag for disabling binary file filtering.
|
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):
|
* [FEATURE #1099](https://github.com/BurntSushi/ripgrep/pull/1099):
|
||||||
Add support for Brotli and Zstd to the `-z/--search-zip` flag.
|
Add support for Brotli and Zstd to the `-z/--search-zip` flag.
|
||||||
* [FEATURE #1138](https://github.com/BurntSushi/ripgrep/pull/1138):
|
* [FEATURE #1138](https://github.com/BurntSushi/ripgrep/pull/1138):
|
||||||
|
3
GUIDE.md
3
GUIDE.md
@ -538,8 +538,9 @@ formatting peculiarities:
|
|||||||
|
|
||||||
```
|
```
|
||||||
$ cat $HOME/.ripgreprc
|
$ 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-columns=150
|
||||||
|
--max-column-preview
|
||||||
|
|
||||||
# Add my 'web' type.
|
# Add my 'web' type.
|
||||||
--type-add
|
--type-add
|
||||||
|
@ -148,6 +148,10 @@ _rg() {
|
|||||||
$no"--no-crlf[don't use CRLF as line terminator]"
|
$no"--no-crlf[don't use CRLF as line terminator]"
|
||||||
'(text)--null-data[use NUL 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)' # Directory-depth options
|
||||||
'--max-depth=[specify max number of directories to descend]:number of directories'
|
'--max-depth=[specify max number of directories to descend]:number of directories'
|
||||||
'!--maxdepth=:number of directories'
|
'!--maxdepth=:number of directories'
|
||||||
|
@ -17,10 +17,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor};
|
|||||||
use color::ColorSpecs;
|
use color::ColorSpecs;
|
||||||
use counter::CounterWriter;
|
use counter::CounterWriter;
|
||||||
use stats::Stats;
|
use stats::Stats;
|
||||||
use util::{
|
use util::{PrinterPath, Replacer, Sunk, trim_ascii_prefix};
|
||||||
PrinterPath, Replacer, Sunk,
|
|
||||||
trim_ascii_prefix, trim_ascii_prefix_range,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The configuration for the standard printer.
|
/// The configuration for the standard printer.
|
||||||
///
|
///
|
||||||
@ -37,6 +34,7 @@ struct Config {
|
|||||||
per_match: bool,
|
per_match: bool,
|
||||||
replacement: Arc<Option<Vec<u8>>>,
|
replacement: Arc<Option<Vec<u8>>>,
|
||||||
max_columns: Option<u64>,
|
max_columns: Option<u64>,
|
||||||
|
max_column_preview: bool,
|
||||||
max_matches: Option<u64>,
|
max_matches: Option<u64>,
|
||||||
column: bool,
|
column: bool,
|
||||||
byte_offset: bool,
|
byte_offset: bool,
|
||||||
@ -60,6 +58,7 @@ impl Default for Config {
|
|||||||
per_match: false,
|
per_match: false,
|
||||||
replacement: Arc::new(None),
|
replacement: Arc::new(None),
|
||||||
max_columns: None,
|
max_columns: None,
|
||||||
|
max_column_preview: false,
|
||||||
max_matches: None,
|
max_matches: None,
|
||||||
column: false,
|
column: false,
|
||||||
byte_offset: false,
|
byte_offset: false,
|
||||||
@ -264,6 +263,21 @@ impl StandardBuilder {
|
|||||||
self
|
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.
|
/// Set the maximum amount of matching lines that are printed.
|
||||||
///
|
///
|
||||||
/// If multi line search is enabled and a match spans multiple lines, then
|
/// 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;
|
count += 1;
|
||||||
if self.exceeds_max_columns(&bytes[line]) {
|
if self.exceeds_max_columns(&bytes[line]) {
|
||||||
self.write_exceeded_line()?;
|
self.write_exceeded_line(bytes, line, matches, &mut midx)?;
|
||||||
continue;
|
} 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(())
|
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());
|
let mut stepper = LineStep::new(line_term, 0, bytes.len());
|
||||||
while let Some((start, end)) = stepper.next(bytes) {
|
while let Some((start, end)) = stepper.next(bytes) {
|
||||||
let mut line = Match::new(start, end);
|
let mut line = Match::new(start, end);
|
||||||
if self.has_line_terminator(&bytes[line]) {
|
self.trim_line_terminator(bytes, &mut line);
|
||||||
line = line.with_end(line.end() - 1);
|
self.trim_ascii_prefix(bytes, &mut line);
|
||||||
}
|
|
||||||
if self.config().trim_ascii {
|
|
||||||
line = self.trim_ascii_prefix_range(bytes, line);
|
|
||||||
}
|
|
||||||
while !line.is_empty() {
|
while !line.is_empty() {
|
||||||
if matches[midx].end() <= line.start() {
|
if matches[midx].end() <= line.start() {
|
||||||
if midx + 1 < matches.len() {
|
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),
|
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);
|
line = line.with_start(upto);
|
||||||
if self.exceeds_max_columns(&buf) {
|
if self.exceeds_max_columns(&bytes[this_line]) {
|
||||||
self.write_exceeded_line()?;
|
self.write_exceeded_line(
|
||||||
continue;
|
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;
|
count += 1;
|
||||||
@ -1140,15 +1123,11 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
)?;
|
)?;
|
||||||
count += 1;
|
count += 1;
|
||||||
if self.exceeds_max_columns(&bytes[line]) {
|
if self.exceeds_max_columns(&bytes[line]) {
|
||||||
self.write_exceeded_line()?;
|
self.write_exceeded_line(bytes, line, &[m], &mut 0)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.has_line_terminator(&bytes[line]) {
|
self.trim_line_terminator(bytes, &mut line);
|
||||||
line = line.with_end(line.end() - 1);
|
self.trim_ascii_prefix(bytes, &mut line);
|
||||||
}
|
|
||||||
if self.config().trim_ascii {
|
|
||||||
line = self.trim_ascii_prefix_range(bytes, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
while !line.is_empty() {
|
while !line.is_empty() {
|
||||||
if m.end() <= line.start() {
|
if m.end() <= line.start() {
|
||||||
@ -1205,7 +1184,10 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
|
|||||||
line: &[u8],
|
line: &[u8],
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
if self.exceeds_max_columns(line) {
|
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 {
|
} else {
|
||||||
self.write_trim(line)?;
|
self.write_trim(line)?;
|
||||||
if !self.has_line_terminator(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(
|
fn write_colored_line(
|
||||||
&self,
|
&self,
|
||||||
matches: &[Match],
|
matches: &[Match],
|
||||||
line: &[u8],
|
bytes: &[u8],
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// If we know we aren't going to emit color, then we can go faster.
|
// If we know we aren't going to emit color, then we can go faster.
|
||||||
let spec = self.config().colors.matched();
|
let spec = self.config().colors.matched();
|
||||||
if !self.wtr().borrow().supports_color() || spec.is_none() {
|
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 mut last_written =
|
let line = Match::new(0, bytes.len());
|
||||||
if !self.config().trim_ascii {
|
if self.exceeds_max_columns(bytes) {
|
||||||
0
|
self.write_exceeded_line(bytes, line, matches, &mut 0)
|
||||||
} else {
|
} else {
|
||||||
self.trim_ascii_prefix_range(
|
self.write_colored_matches(bytes, line, matches, &mut 0)?;
|
||||||
line,
|
self.write_line_term()?;
|
||||||
Match::new(0, line.len()),
|
Ok(())
|
||||||
).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.end_color_match()?;
|
||||||
self.write(&line[last_written..m.start()])?;
|
self.write(&bytes[line.with_end(upto)])?;
|
||||||
} else if last_written < m.end() {
|
line = line.with_start(upto);
|
||||||
m = m.with_start(last_written);
|
|
||||||
} else {
|
} else {
|
||||||
continue;
|
let upto = cmp::min(line.end(), m.end());
|
||||||
}
|
|
||||||
if !m.is_empty() {
|
|
||||||
self.start_color_match()?;
|
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.end_color_match()?;
|
||||||
self.write(&line[last_written..])?;
|
|
||||||
if !self.has_line_terminator(line) {
|
|
||||||
self.write_line_term()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
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.sunk.original_matches().is_empty() {
|
||||||
if self.is_context() {
|
if self.is_context() {
|
||||||
self.write(b"[Omitted long context line]")?;
|
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 {
|
if !self.config().trim_ascii {
|
||||||
return self.write(buf);
|
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<()> {
|
fn write(&self, buf: &[u8]) -> io::Result<()> {
|
||||||
self.wtr().borrow_mut().write_all(buf)
|
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 {
|
fn has_line_terminator(&self, buf: &[u8]) -> bool {
|
||||||
self.searcher.line_terminator().is_suffix(buf)
|
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
|
/// This stops trimming a prefix as soon as it sees non-whitespace or a
|
||||||
/// line terminator.
|
/// line terminator.
|
||||||
fn trim_ascii_prefix_range(&self, slice: &[u8], range: Match) -> Match {
|
fn trim_ascii_prefix(&self, slice: &[u8], range: &mut Match) {
|
||||||
trim_ascii_prefix_range(self.searcher.line_terminator(), slice, range)
|
if !self.config().trim_ascii {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
/// Trim prefix ASCII spaces from the given slice and return the
|
let lineterm = self.searcher.line_terminator();
|
||||||
/// corresponding sub-slice.
|
*range = trim_ascii_prefix(lineterm, slice, *range)
|
||||||
fn trim_ascii_prefix<'s>(&self, slice: &'s [u8]) -> &'s [u8] {
|
|
||||||
trim_ascii_prefix(self.searcher.line_terminator(), slice)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2280,6 +2337,31 @@ but Doctor Watson has to have it taken out for him and dusted,
|
|||||||
assert_eq_printed!(expected, got);
|
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]
|
#[test]
|
||||||
fn max_columns_with_count() {
|
fn max_columns_with_count() {
|
||||||
let matcher = RegexMatcher::new("cigar|ash|dusted").unwrap();
|
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);
|
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]
|
#[test]
|
||||||
fn max_columns_multi_line() {
|
fn max_columns_multi_line() {
|
||||||
let matcher = RegexMatcher::new("(?s)ash.+dusted").unwrap();
|
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);
|
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]
|
#[test]
|
||||||
fn max_matches() {
|
fn max_matches() {
|
||||||
let matcher = RegexMatcher::new("Sherlock").unwrap();
|
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);
|
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]
|
#[test]
|
||||||
fn only_matching_max_columns_multi_line1() {
|
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(
|
let matcher = RegexMatcher::new(
|
||||||
r"(?s:.{0})(Doctor Watsons|Sherlock)"
|
r"(?s:.{0})(Doctor Watsons|Sherlock)"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
@ -2649,6 +2873,41 @@ Holmeses, success in the province of detective work must always
|
|||||||
assert_eq_printed!(expected, got);
|
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]
|
#[test]
|
||||||
fn only_matching_max_columns_multi_line2() {
|
fn only_matching_max_columns_multi_line2() {
|
||||||
let matcher = RegexMatcher::new(
|
let matcher = RegexMatcher::new(
|
||||||
@ -2680,6 +2939,38 @@ Holmeses, success in the province of detective work must always
|
|||||||
assert_eq_printed!(expected, got);
|
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]
|
#[test]
|
||||||
fn per_match() {
|
fn per_match() {
|
||||||
let matcher = RegexMatcher::new("Doctor Watsons|Sherlock").unwrap();
|
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);
|
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]
|
#[test]
|
||||||
fn replacement_only_matching() {
|
fn replacement_only_matching() {
|
||||||
let matcher = RegexMatcher::new(r"Sherlock|Doctor (\w+)").unwrap();
|
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
|
/// This stops trimming a prefix as soon as it sees non-whitespace or a line
|
||||||
/// terminator.
|
/// terminator.
|
||||||
pub fn trim_ascii_prefix_range(
|
pub fn trim_ascii_prefix(
|
||||||
line_term: LineTerminator,
|
line_term: LineTerminator,
|
||||||
slice: &[u8],
|
slice: &[u8],
|
||||||
range: Match,
|
range: Match,
|
||||||
@ -366,14 +366,3 @@ pub fn trim_ascii_prefix_range(
|
|||||||
.count();
|
.count();
|
||||||
range.with_start(range.start() + 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_number(&mut args);
|
||||||
flag_line_regexp(&mut args);
|
flag_line_regexp(&mut args);
|
||||||
flag_max_columns(&mut args);
|
flag_max_columns(&mut args);
|
||||||
|
flag_max_column_preview(&mut args);
|
||||||
flag_max_count(&mut args);
|
flag_max_count(&mut args);
|
||||||
flag_max_depth(&mut args);
|
flag_max_depth(&mut args);
|
||||||
flag_max_filesize(&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);
|
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>) {
|
fn flag_max_count(args: &mut Vec<RGArg>) {
|
||||||
const SHORT: &str = "Limit the number of matches.";
|
const SHORT: &str = "Limit the number of matches.";
|
||||||
const LONG: &str = long!("\
|
const LONG: &str = long!("\
|
||||||
|
@ -746,6 +746,7 @@ impl ArgMatches {
|
|||||||
.per_match(self.is_present("vimgrep"))
|
.per_match(self.is_present("vimgrep"))
|
||||||
.replacement(self.replacement())
|
.replacement(self.replacement())
|
||||||
.max_columns(self.max_columns()?)
|
.max_columns(self.max_columns()?)
|
||||||
|
.max_column_preview(self.max_column_preview())
|
||||||
.max_matches(self.max_count()?)
|
.max_matches(self.max_count()?)
|
||||||
.column(self.column())
|
.column(self.column())
|
||||||
.byte_offset(self.is_present("byte-offset"))
|
.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))
|
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.
|
/// The maximum number of matches permitted.
|
||||||
fn max_count(&self) -> Result<Option<u64>> {
|
fn max_count(&self) -> Result<Option<u64>> {
|
||||||
Ok(self.usize_of("max-count")?.map(|n| n as 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());
|
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
|
// See: https://github.com/BurntSushi/ripgrep/issues/1138
|
||||||
rgtest!(f1138_no_ignore_dot, |dir: Dir, mut cmd: TestCommand| {
|
rgtest!(f1138_no_ignore_dot, |dir: Dir, mut cmd: TestCommand| {
|
||||||
dir.create_dir(".git");
|
dir.create_dir(".git");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user