diff --git a/CHANGELOG.md b/CHANGELOG.md index ea47b58fd8..09a46b224a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar) - `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p) - Add or remove individual style components without replacing all styles #2929 (@eth-p) +- Support negative relative line ranges, e.g. `bat -r :-10` / `bat -r='-10:'`, see #3068 (@ajesipow) ## Bugfixes diff --git a/Cargo.toml b/Cargo.toml index 720e629b58..76234d5b54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ bytesize = { version = "1.3.0" } encoding_rs = "0.8.34" os_str_bytes = { version = "~7.0", optional = true } run_script = { version = "^0.10.1", optional = true} +itertools = "0.13.0" [dependencies.git2] version = "0.18" diff --git a/src/config.rs b/src/config.rs index eb7df8ee9f..8ff49ec100 100644 --- a/src/config.rs +++ b/src/config.rs @@ -116,17 +116,25 @@ pub fn get_pager_executable(config_pager: Option<&str>) -> Option { #[test] fn default_config_should_include_all_lines() { + use crate::line_range::MaxBufferedLineNumber; use crate::line_range::RangeCheckResult; - assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange); + assert_eq!( + LineRanges::default().check(17, MaxBufferedLineNumber::Tentative(17)), + RangeCheckResult::InRange + ); } #[test] fn default_config_should_highlight_no_lines() { + use crate::line_range::MaxBufferedLineNumber; use crate::line_range::RangeCheckResult; assert_ne!( - Config::default().highlighted_lines.0.check(17), + Config::default() + .highlighted_lines + .0 + .check(17, MaxBufferedLineNumber::Tentative(17)), RangeCheckResult::InRange ); } diff --git a/src/controller.rs b/src/controller.rs index ffc5dd5b57..25ee299590 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,5 +1,3 @@ -use std::io::{self, BufRead, Write}; - use crate::assets::HighlightingAssets; use crate::config::{Config, VisibleLines}; #[cfg(feature = "git")] @@ -10,11 +8,14 @@ use crate::input::{Input, InputReader, OpenedInput}; use crate::lessopen::LessOpenPreprocessor; #[cfg(feature = "git")] use crate::line_range::LineRange; -use crate::line_range::{LineRanges, RangeCheckResult}; +use crate::line_range::{LineRanges, MaxBufferedLineNumber, RangeCheckResult}; use crate::output::OutputType; #[cfg(feature = "paging")] use crate::paging::PagingMode; use crate::printer::{InteractivePrinter, OutputHandle, Printer, SimplePrinter}; +use std::collections::VecDeque; +use std::io::{self, BufRead, Write}; +use std::mem; use clircle::{Clircle, Identifier}; @@ -241,20 +242,63 @@ impl<'b> Controller<'b> { reader: &mut InputReader, line_ranges: &LineRanges, ) -> Result<()> { - let mut line_buffer = Vec::new(); - let mut line_number: usize = 1; - + let mut current_line_buffer: Vec = Vec::new(); + let mut current_line_number: usize = 1; + // Buffer needs to be 1 greater than the offset to have a look-ahead line for EOF + let buffer_size: usize = line_ranges.largest_offset_from_end() + 1; + // Buffers multiple line data and line number + let mut buffered_lines: VecDeque<(Vec, usize)> = VecDeque::with_capacity(buffer_size); + + let mut reached_eof: bool = false; let mut first_range: bool = true; let mut mid_range: bool = false; let style_snip = self.config.style_components.snip(); - while reader.read_line(&mut line_buffer)? { - match line_ranges.check(line_number) { + loop { + if reached_eof && buffered_lines.is_empty() { + // Done processing all lines + break; + } + if !reached_eof { + if reader.read_line(&mut current_line_buffer)? { + // Fill the buffer + buffered_lines + .push_back((mem::take(&mut current_line_buffer), current_line_number)); + current_line_number += 1; + } else { + // No more data to read + reached_eof = true; + } + } + + if buffered_lines.len() < buffer_size && !reached_eof { + // The buffer needs to be completely filled first + continue; + } + + let Some((line, line_nr)) = buffered_lines.pop_front() else { + break; + }; + + // Determine if the last line number in the buffer is the last line of the file or + // just a line somewhere in the file + let max_buffered_line_number = buffered_lines + .back() + .map(|(_, max_line_number)| { + if reached_eof { + MaxBufferedLineNumber::Final(*max_line_number) + } else { + MaxBufferedLineNumber::Tentative(*max_line_number) + } + }) + .unwrap_or(MaxBufferedLineNumber::Final(line_nr)); + + match line_ranges.check(line_nr, max_buffered_line_number) { RangeCheckResult::BeforeOrBetweenRanges => { // Call the printer in case we need to call the syntax highlighter // for this line. However, set `out_of_range` to `true`. - printer.print_line(true, writer, line_number, &line_buffer)?; + printer.print_line(true, writer, line_nr, &line, max_buffered_line_number)?; mid_range = false; } @@ -269,15 +313,12 @@ impl<'b> Controller<'b> { } } - printer.print_line(false, writer, line_number, &line_buffer)?; + printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?; } RangeCheckResult::AfterLastRange => { break; } } - - line_number += 1; - line_buffer.clear(); } Ok(()) } diff --git a/src/line_range.rs b/src/line_range.rs index 8899c70601..a6ec22c25a 100644 --- a/src/line_range.rs +++ b/src/line_range.rs @@ -1,16 +1,26 @@ use crate::error::*; +use itertools::{Itertools, MinMaxResult}; -#[derive(Debug, Clone)] +#[derive(Debug, Copy, Clone)] pub struct LineRange { - lower: usize, - upper: usize, + lower: RangeBound, + upper: RangeBound, +} + +/// Defines a boundary for a range +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum RangeBound { + // An absolute line number marking the boundary of a range + Absolute(usize), + // A relative (implicitly negative) offset from the end of the file as a boundary + OffsetFromEnd(usize), } impl Default for LineRange { fn default() -> LineRange { LineRange { - lower: usize::min_value(), - upper: usize::max_value(), + lower: RangeBound::Absolute(usize::MIN), + upper: RangeBound::Absolute(usize::MAX), } } } @@ -18,8 +28,8 @@ impl Default for LineRange { impl LineRange { pub fn new(from: usize, to: usize) -> Self { LineRange { - lower: from, - upper: to, + lower: RangeBound::Absolute(from), + upper: RangeBound::Absolute(to), } } @@ -29,31 +39,47 @@ impl LineRange { fn parse_range(range_raw: &str) -> Result { let mut new_range = LineRange::default(); - - if range_raw.bytes().next().ok_or("Empty line range")? == b':' { - new_range.upper = range_raw[1..].parse()?; + let mut raw_range_iter = range_raw.bytes(); + let first_byte = raw_range_iter.next().ok_or("Empty line range")?; + + if first_byte == b':' { + if raw_range_iter.next() == Some(b'-') { + // E.g. ':-3' + let value = range_raw[2..].parse()?; + new_range.upper = RangeBound::OffsetFromEnd(value); + } else { + let value = range_raw[1..].parse()?; + new_range.upper = RangeBound::Absolute(value); + } return Ok(new_range); } else if range_raw.bytes().last().ok_or("Empty line range")? == b':' { - new_range.lower = range_raw[..range_raw.len() - 1].parse()?; + if first_byte == b'-' { + // E.g. '-3:' + let value = range_raw[1..range_raw.len() - 1].parse()?; + new_range.lower = RangeBound::OffsetFromEnd(value); + } else { + let value = range_raw[..range_raw.len() - 1].parse()?; + new_range.lower = RangeBound::Absolute(value); + } return Ok(new_range); } let line_numbers: Vec<&str> = range_raw.split(':').collect(); match line_numbers.len() { 1 => { - new_range.lower = line_numbers[0].parse()?; + new_range.lower = RangeBound::Absolute(line_numbers[0].parse()?); new_range.upper = new_range.lower; Ok(new_range) } 2 => { - new_range.lower = line_numbers[0].parse()?; + let mut lower_absolute_bound: usize = line_numbers[0].parse()?; let first_byte = line_numbers[1].bytes().next(); - new_range.upper = if first_byte == Some(b'+') { + let upper_absolute_bound = if first_byte == Some(b'+') { let more_lines = &line_numbers[1][1..] .parse() .map_err(|_| "Invalid character after +")?; - new_range.lower.saturating_add(*more_lines) + lower_absolute_bound.saturating_add(*more_lines) } else if first_byte == Some(b'-') { // this will prevent values like "-+5" even though "+5" is valid integer if line_numbers[1][1..].bytes().next() == Some(b'+') { @@ -62,13 +88,14 @@ impl LineRange { let prior_lines = &line_numbers[1][1..] .parse() .map_err(|_| "Invalid character after -")?; - let prev_lower = new_range.lower; - new_range.lower = new_range.lower.saturating_sub(*prior_lines); + let prev_lower = lower_absolute_bound; + lower_absolute_bound = lower_absolute_bound.saturating_sub(*prior_lines); prev_lower } else { line_numbers[1].parse()? }; - + new_range.lower = RangeBound::Absolute(lower_absolute_bound); + new_range.upper = RangeBound::Absolute(upper_absolute_bound); Ok(new_range) } _ => Err( @@ -78,37 +105,107 @@ impl LineRange { } } - pub(crate) fn is_inside(&self, line: usize) -> bool { - line >= self.lower && line <= self.upper + /// Checks if a line number is inside the range. + /// For ranges with relative offsets range bounds `max_buffered_line_number` is necessary + /// to convert the offset to an absolute value. + pub(crate) fn is_inside( + &self, + line: usize, + max_buffered_line_number: MaxBufferedLineNumber, + ) -> bool { + match (self.lower, self.upper, max_buffered_line_number) { + (RangeBound::Absolute(lower), RangeBound::Absolute(upper), _) => { + lower <= line && line <= upper + } + ( + RangeBound::Absolute(lower), + RangeBound::OffsetFromEnd(offset), + MaxBufferedLineNumber::Final(last_line_number), + ) => lower <= line && line <= last_line_number.saturating_sub(offset), + ( + RangeBound::Absolute(lower), + RangeBound::OffsetFromEnd(_), + MaxBufferedLineNumber::Tentative(_), + ) => { + // We don't know the final line number yet, so the assumption is that the line is + // still far enough away from the upper end of the range + lower <= line + } + ( + RangeBound::OffsetFromEnd(offset), + RangeBound::Absolute(upper), + MaxBufferedLineNumber::Final(last_line_number), + ) => last_line_number.saturating_sub(offset) <= line && line <= upper, + ( + RangeBound::OffsetFromEnd(_), + RangeBound::Absolute(_), + MaxBufferedLineNumber::Tentative(_), + ) => { + // We don't know the final line number yet, so the assumption is that the line is + // still too far away from the having reached the lower end of the range + false + } + ( + RangeBound::OffsetFromEnd(lower), + RangeBound::OffsetFromEnd(upper), + MaxBufferedLineNumber::Final(last_line_number), + ) => { + last_line_number.saturating_sub(lower) <= line + && line <= last_line_number.saturating_sub(upper) + } + ( + RangeBound::OffsetFromEnd(_), + RangeBound::OffsetFromEnd(_), + MaxBufferedLineNumber::Tentative(_), + ) => { + // We don't know the final line number yet, so the assumption is that we're still + // too far away from the having reached the lower end of the range + false + } + } } } #[test] fn test_parse_full() { let range = LineRange::from("40:50").expect("Shouldn't fail on test!"); - assert_eq!(40, range.lower); - assert_eq!(50, range.upper); + assert_eq!(RangeBound::Absolute(40), range.lower); + assert_eq!(RangeBound::Absolute(50), range.upper); } #[test] fn test_parse_partial_min() { let range = LineRange::from(":50").expect("Shouldn't fail on test!"); - assert_eq!(usize::min_value(), range.lower); - assert_eq!(50, range.upper); + assert_eq!(RangeBound::Absolute(usize::MIN), range.lower); + assert_eq!(RangeBound::Absolute(50), range.upper); +} + +#[test] +fn test_parse_partial_relative_negative_from_back() { + let range = LineRange::from(":-5").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(usize::MIN), range.lower); + assert_eq!(RangeBound::OffsetFromEnd(5), range.upper); +} + +#[test] +fn test_parse_relative_negative_from_back_partial() { + let range = LineRange::from("-5:").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::OffsetFromEnd(5), range.lower); + assert_eq!(RangeBound::Absolute(usize::MAX), range.upper); } #[test] fn test_parse_partial_max() { let range = LineRange::from("40:").expect("Shouldn't fail on test!"); - assert_eq!(40, range.lower); - assert_eq!(usize::max_value(), range.upper); + assert_eq!(RangeBound::Absolute(40), range.lower); + assert_eq!(RangeBound::Absolute(usize::MAX), range.upper); } #[test] fn test_parse_single() { let range = LineRange::from("40").expect("Shouldn't fail on test!"); - assert_eq!(40, range.lower); - assert_eq!(40, range.upper); + assert_eq!(RangeBound::Absolute(40), range.lower); + assert_eq!(RangeBound::Absolute(40), range.upper); } #[test] @@ -117,6 +214,8 @@ fn test_parse_fail() { assert!(range.is_err()); let range = LineRange::from("40::80"); assert!(range.is_err()); + let range = LineRange::from("-2:5"); + assert!(range.is_err()); let range = LineRange::from(":40:"); assert!(range.is_err()); } @@ -124,15 +223,15 @@ fn test_parse_fail() { #[test] fn test_parse_plus() { let range = LineRange::from("40:+10").expect("Shouldn't fail on test!"); - assert_eq!(40, range.lower); - assert_eq!(50, range.upper); + assert_eq!(RangeBound::Absolute(40), range.lower); + assert_eq!(RangeBound::Absolute(50), range.upper); } #[test] fn test_parse_plus_overflow() { let range = LineRange::from(&format!("{}:+1", usize::MAX)).expect("Shouldn't fail on test!"); - assert_eq!(usize::MAX, range.lower); - assert_eq!(usize::MAX, range.upper); + assert_eq!(RangeBound::Absolute(usize::MAX), range.lower); + assert_eq!(RangeBound::Absolute(usize::MAX), range.upper); } #[test] @@ -148,21 +247,21 @@ fn test_parse_plus_fail() { #[test] fn test_parse_minus_success() { let range = LineRange::from("40:-10").expect("Shouldn't fail on test!"); - assert_eq!(30, range.lower); - assert_eq!(40, range.upper); + assert_eq!(RangeBound::Absolute(30), range.lower); + assert_eq!(RangeBound::Absolute(40), range.upper); } #[test] fn test_parse_minus_edge_cases_success() { let range = LineRange::from("5:-4").expect("Shouldn't fail on test!"); - assert_eq!(1, range.lower); - assert_eq!(5, range.upper); + assert_eq!(RangeBound::Absolute(1), range.lower); + assert_eq!(RangeBound::Absolute(5), range.upper); let range = LineRange::from("5:-5").expect("Shouldn't fail on test!"); - assert_eq!(0, range.lower); - assert_eq!(5, range.upper); + assert_eq!(RangeBound::Absolute(0), range.lower); + assert_eq!(RangeBound::Absolute(5), range.upper); let range = LineRange::from("5:-100").expect("Shouldn't fail on test!"); - assert_eq!(0, range.lower); - assert_eq!(5, range.upper); + assert_eq!(RangeBound::Absolute(0), range.lower); + assert_eq!(RangeBound::Absolute(5), range.upper); } #[test] @@ -187,10 +286,24 @@ pub enum RangeCheckResult { AfterLastRange, } +/// Represents the maximum line number in the buffer when reading a file. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum MaxBufferedLineNumber { + // The currently known maximum line number, may not be the final line number + Tentative(usize), + // The final line number, when EOF has been reached + Final(usize), +} + #[derive(Debug, Clone)] pub struct LineRanges { ranges: Vec, - largest_upper_bound: usize, + // The largest absolute upper line number of all ranges + largest_absolute_upper_bound: usize, + // The smallest relative offset from the end of all ranges + smallest_offset_from_end: usize, + // The largest relative offset from the end of all ranges + largest_offset_from_end: usize, } impl LineRanges { @@ -203,26 +316,62 @@ impl LineRanges { } pub fn from(ranges: Vec) -> LineRanges { - let largest_upper_bound = ranges + let largest_absolute_upper_bound = ranges .iter() - .map(|r| r.upper) + .filter_map(|r| match r.upper { + RangeBound::Absolute(upper) => Some(upper), + _ => None, + }) .max() - .unwrap_or(usize::max_value()); + .unwrap_or(usize::MAX); + + let offsets_min_max = ranges + .iter() + .flat_map(|r| [r.lower, r.upper]) + .filter_map(|r| match r { + RangeBound::OffsetFromEnd(offset) => Some(offset), + _ => None, + }) + .minmax(); + + let (smallest_offset_from_end, largest_offset_from_end) = match offsets_min_max { + MinMaxResult::NoElements => (usize::MIN, usize::MIN), + MinMaxResult::OneElement(offset) => (offset, offset), + MinMaxResult::MinMax(min, max) => (min, max), + }; + LineRanges { ranges, - largest_upper_bound, + largest_absolute_upper_bound, + smallest_offset_from_end, + largest_offset_from_end, } } - pub(crate) fn check(&self, line: usize) -> RangeCheckResult { - if self.ranges.iter().any(|r| r.is_inside(line)) { + pub(crate) fn check( + &self, + line: usize, + max_buffered_line_number: MaxBufferedLineNumber, + ) -> RangeCheckResult { + if self + .ranges + .iter() + .any(|r| r.is_inside(line, max_buffered_line_number)) + { RangeCheckResult::InRange - } else if line < self.largest_upper_bound { + } else if matches!(max_buffered_line_number, MaxBufferedLineNumber::Final(final_line_number) if line > final_line_number.saturating_sub(self.smallest_offset_from_end)) + { + RangeCheckResult::AfterLastRange + } else if line < self.largest_absolute_upper_bound { RangeCheckResult::BeforeOrBetweenRanges } else { RangeCheckResult::AfterLastRange } } + + pub(crate) fn largest_offset_from_end(&self) -> usize { + self.largest_offset_from_end + } } impl Default for LineRanges { @@ -249,54 +398,292 @@ fn ranges(rs: &[&str]) -> LineRanges { fn test_ranges_simple() { let ranges = ranges(&["3:8"]); - assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2)); - assert_eq!(RangeCheckResult::InRange, ranges.check(5)); - assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(9)); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(2, MaxBufferedLineNumber::Tentative(2)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(5)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(9, MaxBufferedLineNumber::Tentative(9)) + ); } #[test] fn test_ranges_advanced() { let ranges = ranges(&["3:8", "11:20", "25:30"]); - assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2)); - assert_eq!(RangeCheckResult::InRange, ranges.check(5)); - assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(9)); - assert_eq!(RangeCheckResult::InRange, ranges.check(11)); - assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(22)); - assert_eq!(RangeCheckResult::InRange, ranges.check(28)); - assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(31)); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(2, MaxBufferedLineNumber::Tentative(2)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(5)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(9, MaxBufferedLineNumber::Tentative(9)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(11, MaxBufferedLineNumber::Tentative(11)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(22, MaxBufferedLineNumber::Tentative(22)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(28, MaxBufferedLineNumber::Tentative(28)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(31, MaxBufferedLineNumber::Tentative(31)) + ); } #[test] fn test_ranges_open_low() { let ranges = ranges(&["3:8", ":5"]); - assert_eq!(RangeCheckResult::InRange, ranges.check(1)); - assert_eq!(RangeCheckResult::InRange, ranges.check(3)); - assert_eq!(RangeCheckResult::InRange, ranges.check(7)); - assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(9)); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Tentative(3)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(7, MaxBufferedLineNumber::Tentative(7)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(9, MaxBufferedLineNumber::Tentative(9)) + ); } #[test] fn test_ranges_open_high() { let ranges = ranges(&["3:", "2:5"]); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); + + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(1, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(2, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(9, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(10, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Tentative(3)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(5)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(9, MaxBufferedLineNumber::Tentative(9)) + ); +} + +#[test] +fn test_ranges_open_up_to_3_from_end() { + let ranges = ranges(&[":-3"]); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Tentative(3)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(8)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Final(6)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(2, MaxBufferedLineNumber::Final(6)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Final(6)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(4, MaxBufferedLineNumber::Final(6)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(5, MaxBufferedLineNumber::Final(6)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(6, MaxBufferedLineNumber::Final(6)) + ); +} - assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(1)); - assert_eq!(RangeCheckResult::InRange, ranges.check(3)); - assert_eq!(RangeCheckResult::InRange, ranges.check(5)); - assert_eq!(RangeCheckResult::InRange, ranges.check(9)); +#[test] +fn test_ranges_multiple_negative_from_back() { + let ranges = ranges(&[":-3", ":-9"]); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Tentative(3)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(14)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Final(16)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(7, MaxBufferedLineNumber::Final(16)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(13, MaxBufferedLineNumber::Final(16)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(14, MaxBufferedLineNumber::Final(16)) + ); + assert_eq!( + RangeCheckResult::AfterLastRange, + ranges.check(16, MaxBufferedLineNumber::Final(16)) + ); +} + +#[test] +fn test_ranges_3_from_back_up_to_end() { + let ranges = ranges(&["-3:"]); + + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(3, MaxBufferedLineNumber::Tentative(3)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(5, MaxBufferedLineNumber::Tentative(8)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(1, MaxBufferedLineNumber::Final(5)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(2, MaxBufferedLineNumber::Final(5)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(3, MaxBufferedLineNumber::Final(5)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(4, MaxBufferedLineNumber::Final(5)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Final(5)) + ); +} + +#[test] +fn test_ranges_multiple_negative_offsets_to_end() { + let ranges = ranges(&["-3:", "-12:"]); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(5, MaxBufferedLineNumber::Tentative(8)) + ); + assert_eq!( + RangeCheckResult::BeforeOrBetweenRanges, + ranges.check(5, MaxBufferedLineNumber::Tentative(17)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(8, MaxBufferedLineNumber::Final(20)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(9, MaxBufferedLineNumber::Final(20)) + ); +} + +#[test] +fn test_ranges_absolute_bound_and_offset() { + let ranges = ranges(&["5:", ":-2"]); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(4, MaxBufferedLineNumber::Tentative(6)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(5, MaxBufferedLineNumber::Tentative(7)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(8, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(9, MaxBufferedLineNumber::Final(10)) + ); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(10, MaxBufferedLineNumber::Final(10)) + ); } #[test] fn test_ranges_all() { let ranges = LineRanges::all(); - assert_eq!(RangeCheckResult::InRange, ranges.check(1)); + assert_eq!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); } #[test] fn test_ranges_none() { let ranges = LineRanges::none(); - assert_ne!(RangeCheckResult::InRange, ranges.check(1)); + assert_ne!( + RangeCheckResult::InRange, + ranges.check(1, MaxBufferedLineNumber::Tentative(1)) + ); } diff --git a/src/printer.rs b/src/printer.rs index e9bea3fd8f..64e89e76b9 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -28,7 +28,7 @@ use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration} use crate::diff::LineChanges; use crate::error::*; use crate::input::OpenedInput; -use crate::line_range::RangeCheckResult; +use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::preprocessor::strip_ansi; use crate::preprocessor::{expand_tabs, replace_nonprintable}; use crate::style::StyleComponent; @@ -98,6 +98,7 @@ pub(crate) trait Printer { handle: &mut OutputHandle, line_number: usize, line_buffer: &[u8], + max_buffered_line_number: MaxBufferedLineNumber, ) -> Result<()>; } @@ -139,6 +140,7 @@ impl<'a> Printer for SimplePrinter<'a> { handle: &mut OutputHandle, _line_number: usize, line_buffer: &[u8], + _max_buffered_line_number: MaxBufferedLineNumber, ) -> Result<()> { // Skip squeezed lines. if let Some(squeeze_limit) = self.config.squeeze_lines { @@ -589,6 +591,7 @@ impl<'a> Printer for InteractivePrinter<'a> { handle: &mut OutputHandle, line_number: usize, line_buffer: &[u8], + max_buffered_line_number: MaxBufferedLineNumber, ) -> Result<()> { let line = if self.config.show_nonprintable { replace_nonprintable( @@ -648,8 +651,12 @@ impl<'a> Printer for InteractivePrinter<'a> { let mut panel_wrap: Option = None; // Line highlighting - let highlight_this_line = - self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange; + let highlight_this_line = self + .config + .highlighted_lines + .0 + .check(line_number, max_buffered_line_number) + == RangeCheckResult::InRange; if highlight_this_line && self.config.theme == "ansi" { self.ansi_style.update(ANSI_UNDERLINE_ENABLE); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8df4327cd7..1502c7c146 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -177,6 +177,46 @@ fn line_range_2_3() { .stdout("line 2\nline 3\n"); } +#[test] +fn line_range_up_to_2_from_back() { + bat() + .arg("multiline.txt") + .arg("--line-range=:-2") + .assert() + .success() + .stdout("line 1\nline 2\n"); +} + +#[test] +fn line_range_up_to_2_from_back_single_line_is_empty() { + bat() + .arg("single-line.txt") + .arg("--line-range=:-2") + .assert() + .success() + .stdout(""); +} + +#[test] +fn line_range_from_back_last_two() { + bat() + .arg("multiline.txt") + .arg("--line-range=-2:") + .assert() + .success() + .stdout("line 3\nline 4\n"); +} + +#[test] +fn line_range_from_back_last_two_single_line() { + bat() + .arg("single-line.txt") + .arg("--line-range=-2:") + .assert() + .success() + .stdout("Single Line"); +} + #[test] fn line_range_first_two() { bat()