mirror of
				https://github.com/sharkdp/bat.git
				synced 2025-11-04 00:51:56 +00:00 
			
		
		
		
	Merge pull request #3068 from ajesipow/read-from-tail
Support relative negative line ranges
This commit is contained in:
		@@ -3,6 +3,7 @@
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Add paging to `--list-themes`, see PR #3239 (@einfachIrgendwer0815)
 | 
			
		||||
- Support negative relative line ranges, e.g. `bat -r :-10` / `bat -r='-10:'`, see #3068 (@ajesipow)
 | 
			
		||||
 | 
			
		||||
## Bugfixes
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -133,7 +133,8 @@ dependencies = [
 | 
			
		||||
 "grep-cli",
 | 
			
		||||
 "home",
 | 
			
		||||
 "indexmap",
 | 
			
		||||
 "itertools",
 | 
			
		||||
 "itertools 0.13.0",
 | 
			
		||||
 "itertools 0.14.0",
 | 
			
		||||
 "nix",
 | 
			
		||||
 "nu-ansi-term",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
@@ -860,6 +861,15 @@ version = "1.70.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itertools"
 | 
			
		||||
version = "0.13.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "either",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itertools"
 | 
			
		||||
version = "0.14.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ encoding_rs = "0.8.35"
 | 
			
		||||
execute = { version = "0.2.13", optional = true }
 | 
			
		||||
terminal-colorsaurus = "0.4"
 | 
			
		||||
unicode-segmentation = "1.12.0"
 | 
			
		||||
itertools = "0.13.0"
 | 
			
		||||
 | 
			
		||||
[dependencies.git2]
 | 
			
		||||
version = "0.20"
 | 
			
		||||
 
 | 
			
		||||
@@ -119,17 +119,25 @@ pub fn get_pager_executable(config_pager: Option<&str>) -> Option<String> {
 | 
			
		||||
 | 
			
		||||
#[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
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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::{OutputHandle, OutputType};
 | 
			
		||||
#[cfg(feature = "paging")]
 | 
			
		||||
use crate::paging::PagingMode;
 | 
			
		||||
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
 | 
			
		||||
use std::collections::VecDeque;
 | 
			
		||||
use std::io::{self, BufRead, Write};
 | 
			
		||||
use std::mem;
 | 
			
		||||
 | 
			
		||||
use clircle::{Clircle, Identifier};
 | 
			
		||||
 | 
			
		||||
@@ -238,20 +239,63 @@ impl Controller<'_> {
 | 
			
		||||
        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<u8> = 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<u8>, 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;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -266,15 +310,12 @@ impl Controller<'_> {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    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(())
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
            upper: usize::MAX,
 | 
			
		||||
            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<LineRange> {
 | 
			
		||||
        let mut new_range = LineRange::default();
 | 
			
		||||
        let mut raw_range_iter = range_raw.bytes();
 | 
			
		||||
        let first_byte = raw_range_iter.next().ok_or("Empty line range")?;
 | 
			
		||||
 | 
			
		||||
        if range_raw.bytes().next().ok_or("Empty line range")? == b':' {
 | 
			
		||||
            new_range.upper = range_raw[1..].parse()?;
 | 
			
		||||
        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, 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, 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<LineRange>,
 | 
			
		||||
    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,22 +316,62 @@ impl LineRanges {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from(ranges: Vec<LineRange>) -> LineRanges {
 | 
			
		||||
        let largest_upper_bound = ranges.iter().map(|r| r.upper).max().unwrap_or(usize::MAX);
 | 
			
		||||
        let largest_absolute_upper_bound = ranges
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter_map(|r| match r.upper {
 | 
			
		||||
                RangeBound::Absolute(upper) => Some(upper),
 | 
			
		||||
                _ => None,
 | 
			
		||||
            })
 | 
			
		||||
            .max()
 | 
			
		||||
            .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 {
 | 
			
		||||
@@ -245,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));
 | 
			
		||||
    assert_eq!(RangeCheckResult::InRange, ranges.check(3));
 | 
			
		||||
    assert_eq!(RangeCheckResult::InRange, ranges.check(5));
 | 
			
		||||
    assert_eq!(RangeCheckResult::InRange, ranges.check(9));
 | 
			
		||||
    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))
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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))
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,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::output::OutputHandle;
 | 
			
		||||
use crate::preprocessor::strip_ansi;
 | 
			
		||||
use crate::preprocessor::{expand_tabs, replace_nonprintable};
 | 
			
		||||
@@ -85,6 +85,7 @@ pub(crate) trait Printer {
 | 
			
		||||
        handle: &mut OutputHandle,
 | 
			
		||||
        line_number: usize,
 | 
			
		||||
        line_buffer: &[u8],
 | 
			
		||||
        max_buffered_line_number: MaxBufferedLineNumber,
 | 
			
		||||
    ) -> Result<()>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -126,6 +127,7 @@ impl Printer for SimplePrinter<'_> {
 | 
			
		||||
        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 Printer for InteractivePrinter<'_> {
 | 
			
		||||
        handle: &mut OutputHandle,
 | 
			
		||||
        line_number: usize,
 | 
			
		||||
        line_buffer: &[u8],
 | 
			
		||||
        max_buffered_line_number: MaxBufferedLineNumber,
 | 
			
		||||
    ) -> Result<()> {
 | 
			
		||||
        let line = if self.config.show_nonprintable {
 | 
			
		||||
            replace_nonprintable(
 | 
			
		||||
@@ -650,8 +653,12 @@ impl Printer for InteractivePrinter<'_> {
 | 
			
		||||
        let mut panel_wrap: Option<String> = 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);
 | 
			
		||||
 
 | 
			
		||||
@@ -176,6 +176,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()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user