Skip to content

Commit 2ff2a81

Browse files
authored
Merge pull request #3068 from ajesipow/read-from-tail
Support relative negative line ranges
2 parents 77de516 + c4461f7 commit 2ff2a81

File tree

8 files changed

+584
-85
lines changed

8 files changed

+584
-85
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Features
44

55
- Add paging to `--list-themes`, see PR #3239 (@einfachIrgendwer0815)
6+
- Support negative relative line ranges, e.g. `bat -r :-10` / `bat -r='-10:'`, see #3068 (@ajesipow)
67

78
## Bugfixes
89

Diff for: Cargo.lock

+11-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ encoding_rs = "0.8.35"
7070
execute = { version = "0.2.13", optional = true }
7171
terminal-colorsaurus = "0.4"
7272
unicode-segmentation = "1.12.0"
73+
itertools = "0.13.0"
7374

7475
[dependencies.git2]
7576
version = "0.20"

Diff for: src/config.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,25 @@ pub fn get_pager_executable(config_pager: Option<&str>) -> Option<String> {
119119

120120
#[test]
121121
fn default_config_should_include_all_lines() {
122+
use crate::line_range::MaxBufferedLineNumber;
122123
use crate::line_range::RangeCheckResult;
123124

124-
assert_eq!(LineRanges::default().check(17), RangeCheckResult::InRange);
125+
assert_eq!(
126+
LineRanges::default().check(17, MaxBufferedLineNumber::Tentative(17)),
127+
RangeCheckResult::InRange
128+
);
125129
}
126130

127131
#[test]
128132
fn default_config_should_highlight_no_lines() {
133+
use crate::line_range::MaxBufferedLineNumber;
129134
use crate::line_range::RangeCheckResult;
130135

131136
assert_ne!(
132-
Config::default().highlighted_lines.0.check(17),
137+
Config::default()
138+
.highlighted_lines
139+
.0
140+
.check(17, MaxBufferedLineNumber::Tentative(17)),
133141
RangeCheckResult::InRange
134142
);
135143
}

Diff for: src/controller.rs

+54-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::io::{self, BufRead, Write};
2-
31
use crate::assets::HighlightingAssets;
42
use crate::config::{Config, VisibleLines};
53
#[cfg(feature = "git")]
@@ -10,11 +8,14 @@ use crate::input::{Input, InputReader, OpenedInput};
108
use crate::lessopen::LessOpenPreprocessor;
119
#[cfg(feature = "git")]
1210
use crate::line_range::LineRange;
13-
use crate::line_range::{LineRanges, RangeCheckResult};
11+
use crate::line_range::{LineRanges, MaxBufferedLineNumber, RangeCheckResult};
1412
use crate::output::{OutputHandle, OutputType};
1513
#[cfg(feature = "paging")]
1614
use crate::paging::PagingMode;
1715
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
16+
use std::collections::VecDeque;
17+
use std::io::{self, BufRead, Write};
18+
use std::mem;
1819

1920
use clircle::{Clircle, Identifier};
2021

@@ -238,20 +239,63 @@ impl Controller<'_> {
238239
reader: &mut InputReader,
239240
line_ranges: &LineRanges,
240241
) -> Result<()> {
241-
let mut line_buffer = Vec::new();
242-
let mut line_number: usize = 1;
243-
242+
let mut current_line_buffer: Vec<u8> = Vec::new();
243+
let mut current_line_number: usize = 1;
244+
// Buffer needs to be 1 greater than the offset to have a look-ahead line for EOF
245+
let buffer_size: usize = line_ranges.largest_offset_from_end() + 1;
246+
// Buffers multiple line data and line number
247+
let mut buffered_lines: VecDeque<(Vec<u8>, usize)> = VecDeque::with_capacity(buffer_size);
248+
249+
let mut reached_eof: bool = false;
244250
let mut first_range: bool = true;
245251
let mut mid_range: bool = false;
246252

247253
let style_snip = self.config.style_components.snip();
248254

249-
while reader.read_line(&mut line_buffer)? {
250-
match line_ranges.check(line_number) {
255+
loop {
256+
if reached_eof && buffered_lines.is_empty() {
257+
// Done processing all lines
258+
break;
259+
}
260+
if !reached_eof {
261+
if reader.read_line(&mut current_line_buffer)? {
262+
// Fill the buffer
263+
buffered_lines
264+
.push_back((mem::take(&mut current_line_buffer), current_line_number));
265+
current_line_number += 1;
266+
} else {
267+
// No more data to read
268+
reached_eof = true;
269+
}
270+
}
271+
272+
if buffered_lines.len() < buffer_size && !reached_eof {
273+
// The buffer needs to be completely filled first
274+
continue;
275+
}
276+
277+
let Some((line, line_nr)) = buffered_lines.pop_front() else {
278+
break;
279+
};
280+
281+
// Determine if the last line number in the buffer is the last line of the file or
282+
// just a line somewhere in the file
283+
let max_buffered_line_number = buffered_lines
284+
.back()
285+
.map(|(_, max_line_number)| {
286+
if reached_eof {
287+
MaxBufferedLineNumber::Final(*max_line_number)
288+
} else {
289+
MaxBufferedLineNumber::Tentative(*max_line_number)
290+
}
291+
})
292+
.unwrap_or(MaxBufferedLineNumber::Final(line_nr));
293+
294+
match line_ranges.check(line_nr, max_buffered_line_number) {
251295
RangeCheckResult::BeforeOrBetweenRanges => {
252296
// Call the printer in case we need to call the syntax highlighter
253297
// for this line. However, set `out_of_range` to `true`.
254-
printer.print_line(true, writer, line_number, &line_buffer)?;
298+
printer.print_line(true, writer, line_nr, &line, max_buffered_line_number)?;
255299
mid_range = false;
256300
}
257301

@@ -266,15 +310,12 @@ impl Controller<'_> {
266310
}
267311
}
268312

269-
printer.print_line(false, writer, line_number, &line_buffer)?;
313+
printer.print_line(false, writer, line_nr, &line, max_buffered_line_number)?;
270314
}
271315
RangeCheckResult::AfterLastRange => {
272316
break;
273317
}
274318
}
275-
276-
line_number += 1;
277-
line_buffer.clear();
278319
}
279320
Ok(())
280321
}

0 commit comments

Comments
 (0)