From 6c13a98f0197322b248e28609a8fdb54af0478ce Mon Sep 17 00:00:00 2001 From: John Cavanaugh <59479+cavanaug@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:17:09 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20context=20support=20t?= =?UTF-8?q?o=20line-range=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs(long-help.txt): 📚 add examples for N::C and N:M:C context syntax docs(clap_app.rs): 📚 update CLI help text with context syntax examples feat(line_range.rs): ✨ implement N::C and N:M:C parsing and add tests --- doc/long-help.txt | 2 ++ src/bin/bat/clap_app.rs | 4 ++- src/line_range.rs | 73 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/doc/long-help.txt b/doc/long-help.txt index 17d3395b..231b711e 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -194,6 +194,8 @@ Options: '--line-range 40:' prints lines 40 to the end of the file '--line-range 40' only prints line 40 '--line-range 30:+10' prints lines 30 to 40 + '--line-range 35::5' prints lines 30 to 40 (line 35 with 5 lines of context) + '--line-range 30:40:2' prints lines 28 to 42 (range 30-40 with 2 lines of context) -L, --list-languages Display a list of supported languages for syntax highlighting. diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index de2db078..67bef3d8 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -525,7 +525,9 @@ pub fn build_app(interactive_output: bool) -> Command { '--line-range :40' prints lines 1 to 40\n \ '--line-range 40:' prints lines 40 to the end of the file\n \ '--line-range 40' only prints line 40\n \ - '--line-range 30:+10' prints lines 30 to 40", + '--line-range 30:+10' prints lines 30 to 40\n \ + '--line-range 35::5' prints lines 30 to 40 (line 35 with 5 lines of context)\n \ + '--line-range 30:40:2' prints lines 28 to 42 (range 30-40 with 2 lines of context)", ), ) .arg( diff --git a/src/line_range.rs b/src/line_range.rs index a6ec22c2..b0d93594 100644 --- a/src/line_range.rs +++ b/src/line_range.rs @@ -98,8 +98,33 @@ impl LineRange { new_range.upper = RangeBound::Absolute(upper_absolute_bound); Ok(new_range) } + 3 => { + // Handle context syntax: N::C or N:M:C + if line_numbers[1].is_empty() { + // Format: N::C - single line with context + let line_number: usize = line_numbers[0].parse() + .map_err(|_| "Invalid line number in N::C format")?; + let context: usize = line_numbers[2].parse() + .map_err(|_| "Invalid context number in N::C format")?; + + new_range.lower = RangeBound::Absolute(line_number.saturating_sub(context)); + new_range.upper = RangeBound::Absolute(line_number.saturating_add(context)); + } else { + // Format: N:M:C - range with context + let start_line: usize = line_numbers[0].parse() + .map_err(|_| "Invalid start line number in N:M:C format")?; + let end_line: usize = line_numbers[1].parse() + .map_err(|_| "Invalid end line number in N:M:C format")?; + let context: usize = line_numbers[2].parse() + .map_err(|_| "Invalid context number in N:M:C format")?; + + new_range.lower = RangeBound::Absolute(start_line.saturating_sub(context)); + new_range.upper = RangeBound::Absolute(end_line.saturating_add(context)); + } + Ok(new_range) + } _ => Err( - "Line range contained more than one ':' character. Expected format: 'N' or 'N:M'" + "Line range contained too many ':' characters. Expected format: 'N', 'N:M', 'N::C', or 'N:M:C'" .into(), ), } @@ -274,6 +299,52 @@ fn test_parse_minus_fail() { assert!(range.is_err()); } +#[test] +fn test_parse_context_single_line() { + let range = LineRange::from("35::5").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(30), range.lower); + assert_eq!(RangeBound::Absolute(40), range.upper); +} + +#[test] +fn test_parse_context_range() { + let range = LineRange::from("30:40:2").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(28), range.lower); + assert_eq!(RangeBound::Absolute(42), range.upper); +} + +#[test] +fn test_parse_context_edge_cases() { + // Test with small line numbers that would underflow + let range = LineRange::from("5::10").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(0), range.lower); + assert_eq!(RangeBound::Absolute(15), range.upper); + + // Test with zero context + let range = LineRange::from("50::0").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(50), range.lower); + assert_eq!(RangeBound::Absolute(50), range.upper); + + // Test range with zero context + let range = LineRange::from("30:40:0").expect("Shouldn't fail on test!"); + assert_eq!(RangeBound::Absolute(30), range.lower); + assert_eq!(RangeBound::Absolute(40), range.upper); +} + +#[test] +fn test_parse_context_fail() { + let range = LineRange::from("40::z"); + assert!(range.is_err()); + let range = LineRange::from("::5"); + assert!(range.is_err()); + let range = LineRange::from("40::"); + assert!(range.is_err()); + let range = LineRange::from("30:40:z"); + assert!(range.is_err()); + let range = LineRange::from("30::40:5"); + assert!(range.is_err()); +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum RangeCheckResult { // Within one of the given ranges