From 8c1d7b18dbaccf7b25f66529e2d8e0e1bd0e2edc Mon Sep 17 00:00:00 2001 From: einfachIrgendwer0815 <85333734+einfachIrgendwer0815@users.noreply.github.com> Date: Sun, 10 Mar 2024 13:23:55 +0100 Subject: [PATCH 1/5] Limit line buffer length Discards long lines to prevent out-of-memory events. --- src/controller.rs | 36 ++++++++++--- src/error.rs | 2 + src/input.rs | 132 +++++++++++++++++++++++++++++++++++++++++----- src/printer.rs | 67 +++++++++++++++++++++++ 4 files changed, 216 insertions(+), 21 deletions(-) diff --git a/src/controller.rs b/src/controller.rs index ffc5dd5b..2be3124d 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -5,7 +5,7 @@ use crate::config::{Config, VisibleLines}; #[cfg(feature = "git")] use crate::diff::{get_git_diff, LineChanges}; use crate::error::*; -use crate::input::{Input, InputReader, OpenedInput}; +use crate::input::{Input, InputReader, OpenedInput, ReaderError}; #[cfg(feature = "lessopen")] use crate::lessopen::LessOpenPreprocessor; #[cfg(feature = "git")] @@ -249,13 +249,30 @@ impl<'b> Controller<'b> { let style_snip = self.config.style_components.snip(); - while reader.read_line(&mut line_buffer)? { + loop { + let mut soft_limit_hit = false; + let read_result = reader.read_line(&mut line_buffer); + match read_result { + Ok(res) => { + if !res { + break; + } + } + Err(err) => match err { + ReaderError::IoError(io_err) => return Err(io_err.into()), + ReaderError::SoftLimitHit => soft_limit_hit = true, + ReaderError::HardLimitHit => return Err(Error::LineTooLong(line_number)), + }, + }; + match line_ranges.check(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)?; - mid_range = false; + if !soft_limit_hit { + // 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)?; + mid_range = false; + } } RangeCheckResult::InRange => { @@ -268,8 +285,11 @@ impl<'b> Controller<'b> { printer.print_snip(writer)?; } } - - printer.print_line(false, writer, line_number, &line_buffer)?; + if soft_limit_hit { + printer.print_replaced_line(writer, line_number, "")?; + } else { + printer.print_line(false, writer, line_number, &line_buffer)?; + } } RangeCheckResult::AfterLastRange => { break; diff --git a/src/error.rs b/src/error.rs index 007737b0..3f42ea5f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,8 @@ pub enum Error { InvalidPagerValueBat, #[error("{0}")] Msg(String), + #[error("Line {0} is too long")] + LineTooLong(usize), #[cfg(feature = "lessopen")] #[error(transparent)] VarError(#[from] ::std::env::VarError), diff --git a/src/input.rs b/src/input.rs index 0ebaa4ce..cadbaa40 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use clircle::{Clircle, Identifier}; use content_inspector::{self, ContentType}; +use once_cell::unsync::Lazy; use crate::error::*; @@ -250,34 +251,50 @@ impl<'a> Input<'a> { } pub(crate) struct InputReader<'a> { - inner: Box, + inner: LimitBuf<'a>, pub(crate) first_line: Vec, pub(crate) content_type: Option, } impl<'a> InputReader<'a> { - pub(crate) fn new(mut reader: R) -> InputReader<'a> { - let mut first_line = vec![]; - reader.read_until(b'\n', &mut first_line).ok(); + pub(crate) fn new(reader: R) -> InputReader<'a> { + let mut input_reader = InputReader { + inner: LimitBuf::new(reader, 4096, 1024 * 64, 1024 * 256), + first_line: vec![], + content_type: None, + }; - let content_type = if first_line.is_empty() { + input_reader.read_first_line().ok(); + + let content_type = if input_reader.first_line.is_empty() { None } else { - Some(content_inspector::inspect(&first_line[..])) + Some(content_inspector::inspect(&input_reader.first_line[..])) }; if content_type == Some(ContentType::UTF_16LE) { - reader.read_until(0x00, &mut first_line).ok(); + input_reader + .inner + .read_until(0x00, &mut input_reader.first_line) + .ok(); } - InputReader { - inner: Box::new(reader), - first_line, - content_type, - } + input_reader.content_type = content_type; + input_reader } - pub(crate) fn read_line(&mut self, buf: &mut Vec) -> io::Result { + fn read_first_line(&mut self) -> std::result::Result { + let mut first_line = vec![]; + let res = self.read_line(&mut first_line); + self.first_line = first_line; + + res + } + + pub(crate) fn read_line( + &mut self, + buf: &mut Vec, + ) -> std::result::Result { if !self.first_line.is_empty() { buf.append(&mut self.first_line); return Ok(true); @@ -293,6 +310,95 @@ impl<'a> InputReader<'a> { } } +struct LimitBuf<'a> { + reader: Box, + inner: Box<[u8]>, + start: usize, + len: usize, + soft_limit: usize, + hard_limit: usize, +} + +impl<'a> LimitBuf<'a> { + pub fn new( + reader: R, + buf_size: usize, + soft_limit: usize, + hard_limit: usize, + ) -> Self { + Self { + reader: Box::new(reader), + inner: vec![0u8; buf_size].into_boxed_slice(), + start: 0, + len: 0, + soft_limit, + hard_limit, + } + } + + pub fn read_until( + &mut self, + byte: u8, + buf: &mut Vec, + ) -> std::result::Result { + let mut end_byte_reached = false; + let mut total_bytes = 0; + + let mut soft_limit_hit = false; + let capacity = self.inner.len(); + let mut drop_buf = Lazy::new(|| Vec::with_capacity(capacity)); + + while !end_byte_reached { + if self.len == 0 { + let bytes = self.reader.read(&mut self.inner)?; + self.len += bytes; + self.start = 0; + + if bytes == 0 { + break; + } + } + + let bytes = (&self.inner[self.start..self.start + self.len]) + .read_until(byte, if soft_limit_hit { &mut drop_buf } else { buf })?; + end_byte_reached = self.inner[self.start + bytes - 1] == byte; + + if soft_limit_hit { + drop_buf.clear(); + } + + self.len -= bytes; + self.start += bytes; + total_bytes += bytes; + + if total_bytes > self.hard_limit { + return Err(ReaderError::HardLimitHit); + } else if total_bytes > self.soft_limit { + soft_limit_hit = true; + } + } + + if soft_limit_hit { + Err(ReaderError::SoftLimitHit) + } else { + Ok(total_bytes) + } + } +} + +#[derive(Debug)] +pub(crate) enum ReaderError { + SoftLimitHit, + HardLimitHit, + IoError(io::Error), +} + +impl From for ReaderError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} + #[test] fn basic() { let content = b"#!/bin/bash\necho hello"; diff --git a/src/printer.rs b/src/printer.rs index 282f0fe1..9559c1b8 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -21,6 +21,7 @@ use unicode_width::UnicodeWidthChar; use crate::assets::{HighlightingAssets, SyntaxReferenceInSet}; use crate::config::Config; +use crate::decorations; #[cfg(feature = "git")] use crate::decorations::LineChangesDecoration; use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration}; @@ -97,6 +98,13 @@ pub(crate) trait Printer { line_number: usize, line_buffer: &[u8], ) -> Result<()>; + + fn print_replaced_line( + &mut self, + handle: &mut OutputHandle, + line_number: usize, + replace_text: &str, + ) -> Result<()>; } pub struct SimplePrinter<'a> { @@ -179,6 +187,15 @@ impl<'a> Printer for SimplePrinter<'a> { } Ok(()) } + + fn print_replaced_line( + &mut self, + _handle: &mut OutputHandle, + _line_number: usize, + _replace_text: &str, + ) -> Result<()> { + Ok(()) + } } struct HighlighterFromSet<'a> { @@ -819,6 +836,54 @@ impl<'a> Printer for InteractivePrinter<'a> { Ok(()) } + + fn print_replaced_line( + &mut self, + handle: &mut OutputHandle, + line_number: usize, + replace_text: &str, + ) -> Result<()> { + if let Some(ContentType::BINARY) | None = self.content_type { + return Ok(()); + } + + if self.panel_width > 0 { + let mut width = 0; + if self.config.style_components.numbers() { + let line_numbers = decorations::LineNumberDecoration::new(&self.colors); + + width += line_numbers.width(); + write!( + handle, + "{} ", + line_numbers.generate(line_number, false, self).text + )?; + } + + if self.config.style_components.grid() { + write!(handle, "{}", self.colors.error_indicator.paint("!"))?; + } + + if width < self.panel_width { + write!( + handle, + "{}", + " ".repeat(self.panel_width.saturating_sub(width).saturating_sub(2)) + )?; + } + + if self.config.style_components.grid() { + let grid = decorations::GridBorderDecoration::new(&self.colors); + write!(handle, "{} ", grid.generate(line_number, false, self).text)?; + } + } + writeln!( + handle, + "{}", + self.colors.error_indicator.paint(replace_text) + )?; + Ok(()) + } } const DEFAULT_GUTTER_COLOR: u8 = 238; @@ -832,6 +897,7 @@ pub struct Colors { pub git_removed: Style, pub git_modified: Style, pub line_number: Style, + pub error_indicator: Style, } impl Colors { @@ -861,6 +927,7 @@ impl Colors { git_removed: Red.normal(), git_modified: Yellow.normal(), line_number: gutter_style, + error_indicator: Red.normal(), } } } From 9594b0e4aa9283702e23670075b43667c10a4d2b Mon Sep 17 00:00:00 2001 From: einfachIrgendwer0815 <85333734+einfachIrgendwer0815@users.noreply.github.com> Date: Fri, 15 Mar 2024 19:09:07 +0100 Subject: [PATCH 2/5] Make line limits cli configurable --- doc/long-help.txt | 11 +++++++++++ doc/short-help.txt | 2 ++ src/assets.rs | 8 ++++---- src/bin/bat/app.rs | 12 ++++++++++++ src/bin/bat/clap_app.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 ++++++ src/controller.rs | 7 ++++++- src/input.rs | 25 ++++++++++++++++++------- 8 files changed, 97 insertions(+), 12 deletions(-) diff --git a/doc/long-help.txt b/doc/long-help.txt index a6ffe962..8a09db61 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -122,6 +122,17 @@ Options: --squeeze-limit Set the maximum number of consecutive empty lines to be printed. + --disable-line-limits + Disables all line limits. Short for `--soft-line-limit 0 --hard-line-limit 0`. + + --soft-line-limit + Line length (in bytes) at which the line will be ignored. Zero disables this limit. + Default: 64 kB + + --hard-line-limit + Line length (in bytes) at which bat will abort. Zero disables this limit. + Default: 256 kB + --style Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) to display in addition to the file contents. The argument is a comma-separated list of diff --git a/doc/short-help.txt b/doc/short-help.txt index 305bbf3d..5269347d 100644 --- a/doc/short-help.txt +++ b/doc/short-help.txt @@ -45,6 +45,8 @@ Options: Display all supported highlighting themes. -s, --squeeze-blank Squeeze consecutive empty lines. + --disable-line-limits + Disables all line limits. --style Comma-separated list of style elements to display (*default*, auto, full, plain, changes, header, header-filename, header-filesize, grid, rule, numbers, snip). diff --git a/src/assets.rs b/src/assets.rs index 9655553d..e7ced5cb 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -471,7 +471,7 @@ mod tests { let input = Input::ordinary_file(&file_path); let dummy_stdin: &[u8] = &[]; - let mut opened_input = input.open(dummy_stdin, None).unwrap(); + let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap(); self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) } @@ -481,7 +481,7 @@ mod tests { let input = Input::from_reader(Box::new(BufReader::new(first_line.as_bytes()))) .with_name(Some(&file_path)); let dummy_stdin: &[u8] = &[]; - let mut opened_input = input.open(dummy_stdin, None).unwrap(); + let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap(); self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) } @@ -501,7 +501,7 @@ mod tests { fn syntax_for_stdin_with_content(&self, file_name: &str, content: &[u8]) -> String { let input = Input::stdin().with_name(Some(file_name)); - let mut opened_input = input.open(content, None).unwrap(); + let mut opened_input = input.open(content, None, None, None).unwrap(); self.get_syntax_name(None, &mut opened_input, &self.syntax_mapping) } @@ -698,7 +698,7 @@ mod tests { let input = Input::ordinary_file(&file_path_symlink); let dummy_stdin: &[u8] = &[]; - let mut opened_input = input.open(dummy_stdin, None).unwrap(); + let mut opened_input = input.open(dummy_stdin, None, None, None).unwrap(); assert_eq!( test.get_syntax_name(None, &mut opened_input, &test.syntax_mapping), diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 6fc85321..ca549e8e 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -304,6 +304,18 @@ impl App { } else { None }, + soft_line_limit: self + .matches + .get_one::("soft-line-limit") + .copied() + .filter(|l| l != &0) + .filter(|_| !self.matches.get_flag("disable-line-limits")), + hard_line_limit: self + .matches + .get_one::("hard-line-limit") + .copied() + .filter(|l| l != &0) + .filter(|_| !self.matches.get_flag("disable-line-limits")), }) } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index b82762b6..c0758c04 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -402,6 +402,44 @@ pub fn build_app(interactive_output: bool) -> Command { .long_help("Set the maximum number of consecutive empty lines to be printed.") .hide_short_help(true) ) + .arg( + Arg::new("disable-line-limits") + .long("disable-line-limits") + .action(ArgAction::SetTrue) + .overrides_with_all(["soft-line-limit", "hard-line-limit"]) + .help("Disables all line limits.") + .long_help("Disables all line limits. Short for `--soft-line-limit 0 --hard-line-limit 0`.") + ) + .arg( + Arg::new("soft-line-limit") + .long("soft-line-limit") + .value_name("BYTES") + .value_parser(|s: &str| s.parse::()) + .default_value("65536") + .overrides_with("disable-line-limits") + .long_help( + "Line length (in bytes) at which the line will be ignored. \ + Zero disables this limit.\n\ + Default: 64 kB", + ) + .hide_short_help(true) + .hide_default_value(true) + ) + .arg( + Arg::new("hard-line-limit") + .long("hard-line-limit") + .value_name("BYTES") + .value_parser(|s: &str| s.parse::()) + .default_value("262144") + .overrides_with("disable-line-limits") + .long_help( + "Line length (in bytes) at which bat will abort. \ + Zero disables this limit.\n\ + Default: 256 kB" + ) + .hide_short_help(true) + .hide_default_value(true) + ) .arg( Arg::new("style") .long("style") diff --git a/src/config.rs b/src/config.rs index 0298bb2a..caf9416e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -100,6 +100,12 @@ pub struct Config<'a> { /// The maximum number of consecutive empty lines to display pub squeeze_lines: Option, + + /// Line length (in bytes) at which a line of input will be ignored + pub soft_line_limit: Option, + + /// Line length (in bytes) at which an error will be thrown + pub hard_line_limit: Option, } #[cfg(all(feature = "minimal-application", feature = "paging"))] diff --git a/src/controller.rs b/src/controller.rs index 2be3124d..e4f45993 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -142,7 +142,12 @@ impl<'b> Controller<'b> { } #[cfg(not(feature = "lessopen"))] - input.open(stdin, stdout_identifier)? + input.open( + stdin, + stdout_identifier, + self.config.soft_line_limit, + self.config.hard_line_limit, + )? }; #[cfg(feature = "git")] let line_changes = if self.config.visible_lines.diff_mode() diff --git a/src/input.rs b/src/input.rs index cadbaa40..4fd326bc 100644 --- a/src/input.rs +++ b/src/input.rs @@ -192,6 +192,8 @@ impl<'a> Input<'a> { self, stdin: R, stdout_identifier: Option<&Identifier>, + soft_limit: Option, + hard_limit: Option, ) -> Result> { let description = self.description().clone(); match self.kind { @@ -208,7 +210,7 @@ impl<'a> Input<'a> { kind: OpenedInputKind::StdIn, description, metadata: self.metadata, - reader: InputReader::new(stdin), + reader: InputReader::new(stdin, soft_limit, hard_limit), }) } @@ -237,14 +239,14 @@ impl<'a> Input<'a> { file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened..."); } - InputReader::new(BufReader::new(file)) + InputReader::new(BufReader::new(file), soft_limit, hard_limit) }, }), InputKind::CustomReader(reader) => Ok(OpenedInput { description, kind: OpenedInputKind::CustomReader, metadata: self.metadata, - reader: InputReader::new(BufReader::new(reader)), + reader: InputReader::new(BufReader::new(reader), soft_limit, hard_limit), }), } } @@ -257,9 +259,18 @@ pub(crate) struct InputReader<'a> { } impl<'a> InputReader<'a> { - pub(crate) fn new(reader: R) -> InputReader<'a> { + pub(crate) fn new( + reader: R, + soft_limit: Option, + hard_limit: Option, + ) -> InputReader<'a> { let mut input_reader = InputReader { - inner: LimitBuf::new(reader, 4096, 1024 * 64, 1024 * 256), + inner: LimitBuf::new( + reader, + 4096, + soft_limit.unwrap_or(usize::MAX), + hard_limit.unwrap_or(usize::MAX), + ), first_line: vec![], content_type: None, }; @@ -402,7 +413,7 @@ impl From for ReaderError { #[test] fn basic() { let content = b"#!/bin/bash\necho hello"; - let mut reader = InputReader::new(&content[..]); + let mut reader = InputReader::new(&content[..], None, None); assert_eq!(b"#!/bin/bash\n", &reader.first_line[..]); @@ -431,7 +442,7 @@ fn basic() { #[test] fn utf16le() { let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; - let mut reader = InputReader::new(&content[..]); + let mut reader = InputReader::new(&content[..], None, None); assert_eq!(b"\xFF\xFE\x73\x00\x0A\x00", &reader.first_line[..]); From e34f073f7b2a0549df4fa363af7beba05129b769 Mon Sep 17 00:00:00 2001 From: einfachIrgendwer0815 <85333734+einfachIrgendwer0815@users.noreply.github.com> Date: Sat, 16 Mar 2024 11:13:45 +0100 Subject: [PATCH 3/5] Test line limits --- tests/examples/too-long-lines.txt | 19 ++++++++ tests/integration_tests.rs | 78 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/examples/too-long-lines.txt diff --git a/tests/examples/too-long-lines.txt b/tests/examples/too-long-lines.txt new file mode 100644 index 00000000..c53cfef1 --- /dev/null +++ b/tests/examples/too-long-lines.txt @@ -0,0 +1,19 @@ +a +bb +ccc +dddd +eeeee +ffffff +ggggggg +hhhhhhhh +iiiiiiiii +jjjjjjjjjj +kkkkkkkkk +llllllll +mmmmmmm +nnnnnn +ooooo +pppp +qqq +rr +s diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0285ac26..e280d5f6 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -307,6 +307,84 @@ fn list_themes_without_colors() { .stdout(predicate::str::contains(default_theme_chunk).normalize()); } +#[test] +fn soft_line_limit() { + bat() + .arg("too-long-lines.txt") + .arg("--soft-line-limit=10") + .arg("--decorations=always") + .arg("--terminal-width=80") + .assert() + .success() + .stdout( + "───────┬──────────────────────────────────────────────────────────────────────── + │ File: too-long-lines.txt +───────┼──────────────────────────────────────────────────────────────────────── + 1 │ a + 2 │ bb + 3 │ ccc + 4 │ dddd + 5 │ eeeee + 6 │ ffffff + 7 │ ggggggg + 8 │ hhhhhhhh + 9 │ iiiiiiiii + 10 ! │ + 11 │ kkkkkkkkk + 12 │ llllllll + 13 │ mmmmmmm + 14 │ nnnnnn + 15 │ ooooo + 16 │ pppp + 17 │ qqq + 18 │ rr + 19 │ s +───────┴──────────────────────────────────────────────────────────────────────── +", + ); +} + +#[test] +fn soft_line_limit_style_plain() { + bat() + .arg("too-long-lines.txt") + .arg("--soft-line-limit=10") + .arg("--style=plain") + .assert() + .success() + .stdout( + "a +bb +ccc +dddd +eeeee +ffffff +ggggggg +hhhhhhhh +iiiiiiiii +kkkkkkkkk +llllllll +mmmmmmm +nnnnnn +ooooo +pppp +qqq +rr +s +", + ); +} + +#[test] +fn hard_line_limit() { + bat() + .arg("too-long-lines.txt") + .arg("--hard-line-limit=10") + .assert() + .failure() + .stderr("\u{1b}[31m[bat error]\u{1b}[0m: Line 10 is too long\n"); +} + #[test] #[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)] fn short_help() { From 1e52d10752b5b78046dc2f69e30cb6286d54abeb Mon Sep 17 00:00:00 2001 From: einfachIrgendwer0815 <85333734+einfachIrgendwer0815@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:46:04 +0100 Subject: [PATCH 4/5] Add CHANGELOG entry for line limits --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e56e34..2187aaee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar) +- Add line length soft/hard limits to prevent out-of-memory events, see issue #636 and PR #2902 (@einfachIrgendwer0815) ## Bugfixes From d0bb0ba06e92152a82de9cf64bebf6e3d5470e37 Mon Sep 17 00:00:00 2001 From: einfachIrgendwer0815 <85333734+einfachIrgendwer0815@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:26:53 +0100 Subject: [PATCH 5/5] Fix spacing issue with git feature disabled --- src/decorations.rs | 30 +++++++++++ src/printer.rs | 23 ++++++++ tests/integration_tests.rs | 29 +++++----- .../output/grid_header_numbers.snapshot.txt | 54 +++++++++---------- .../output/grid_numbers.snapshot.txt | 50 ++++++++--------- 5 files changed, 120 insertions(+), 66 deletions(-) diff --git a/src/decorations.rs b/src/decorations.rs index 5b7846c3..7f0dca0a 100644 --- a/src/decorations.rs +++ b/src/decorations.rs @@ -156,3 +156,33 @@ impl Decoration for GridBorderDecoration { self.cached.width } } + +pub(crate) struct PlaceholderDecoration { + cached: DecorationText, +} + +impl PlaceholderDecoration { + pub(crate) fn new(length: usize) -> Self { + Self { + cached: DecorationText { + text: " ".repeat(length), + width: length, + }, + } + } +} + +impl Decoration for PlaceholderDecoration { + fn generate( + &self, + _line_number: usize, + _continuation: bool, + _printer: &InteractivePrinter, + ) -> DecorationText { + self.cached.clone() + } + + fn width(&self) -> usize { + self.cached.width + } +} diff --git a/src/printer.rs b/src/printer.rs index 9559c1b8..af00e7b1 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -24,6 +24,7 @@ use crate::config::Config; use crate::decorations; #[cfg(feature = "git")] use crate::decorations::LineChangesDecoration; +use crate::decorations::PlaceholderDecoration; use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration}; #[cfg(feature = "git")] use crate::diff::LineChanges; @@ -257,6 +258,28 @@ impl<'a> InteractivePrinter<'a> { } } + let insert_placeholder = { + let git_feature_enabled = cfg!(feature = "git"); + let changes_component; + #[cfg(feature = "git")] + { + changes_component = config.style_components.changes(); + } + #[cfg(not(feature = "git"))] + { + changes_component = false; + } + + let soft_limit_active = config.soft_line_limit.is_some(); + let numbers_and_grid = + config.style_components.grid() && config.style_components.numbers(); + + (!git_feature_enabled || !changes_component) && numbers_and_grid && soft_limit_active + }; + if insert_placeholder { + decorations.push(Box::new(PlaceholderDecoration::new(1))) + } + let mut panel_width: usize = decorations.len() + decorations.iter().fold(0, |a, x| a + x.width()); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e280d5f6..cbded97f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1343,11 +1343,11 @@ fn bom_stripped_when_no_color_and_not_loop_through() { .success() .stdout( "\ -─────┬────────────────────────────────────────────────────────────────────────── - │ File: test_BOM.txt -─────┼────────────────────────────────────────────────────────────────────────── - 1 │ hello world -─────┴────────────────────────────────────────────────────────────────────────── +───────┬──────────────────────────────────────────────────────────────────────── + │ File: test_BOM.txt +───────┼──────────────────────────────────────────────────────────────────────── + 1 │ hello world +───────┴──────────────────────────────────────────────────────────────────────── ", ); } @@ -1595,15 +1595,16 @@ fn header_narrow_terminal() { .success() .stdout( "\ -─────┬──────────────────────── - │ File: this-file-path-is - │ -really-long-and-would- - │ have-broken-the-layout- - │ of-the-header.txt -─────┼──────────────────────── - 1 │ The header is not broke - │ n -─────┴──────────────────────── +───────┬────────────────────── + │ File: this-file-path- + │ is-really-long-and-wo + │ uld-have-broken-the-l + │ ayout-of-the-header.t + │ xt +───────┼────────────────────── + 1 │ The header is not bro + │ ken +───────┴────────────────────── ", ) .stderr(""); diff --git a/tests/snapshots/output/grid_header_numbers.snapshot.txt b/tests/snapshots/output/grid_header_numbers.snapshot.txt index dfbb934b..c298980c 100644 --- a/tests/snapshots/output/grid_header_numbers.snapshot.txt +++ b/tests/snapshots/output/grid_header_numbers.snapshot.txt @@ -1,27 +1,27 @@ -─────┬────────────────────────────────────────────────────────────────────────── - │ File: sample.rs -─────┼────────────────────────────────────────────────────────────────────────── - 1 │ /// A rectangle. First line is changed to prevent a regression of #1869 - 2 │ struct Rectangle { - 3 │ width: u32, - 4 │ height: u32, - 5 │ } - 6 │ - 7 │ fn main() { - 8 │ let rect1 = Rectangle { width: 30, height: 50 }; - 9 │ - 10 │ println!( - 11 │ "The perimeter of the rectangle is {} pixels.", - 12 │ perimeter(&rect1) - 13 │ ); - 14 │ println!(r#"This line contains invalid utf8: "�����"#; - 15 │ } - 16 │ - 17 │ fn area(rectangle: &Rectangle) -> u32 { - 18 │ rectangle.width * rectangle.height - 19 │ } - 20 │ - 21 │ fn perimeter(rectangle: &Rectangle) -> u32 { - 22 │ (rectangle.width + rectangle.height) * 2 - 23 │ } -─────┴────────────────────────────────────────────────────────────────────────── +───────┬──────────────────────────────────────────────────────────────────────── + │ File: sample.rs +───────┼──────────────────────────────────────────────────────────────────────── + 1 │ /// A rectangle. First line is changed to prevent a regression of #1869 + 2 │ struct Rectangle { + 3 │ width: u32, + 4 │ height: u32, + 5 │ } + 6 │ + 7 │ fn main() { + 8 │ let rect1 = Rectangle { width: 30, height: 50 }; + 9 │ + 10 │ println!( + 11 │ "The perimeter of the rectangle is {} pixels.", + 12 │ perimeter(&rect1) + 13 │ ); + 14 │ println!(r#"This line contains invalid utf8: "�����"#; + 15 │ } + 16 │ + 17 │ fn area(rectangle: &Rectangle) -> u32 { + 18 │ rectangle.width * rectangle.height + 19 │ } + 20 │ + 21 │ fn perimeter(rectangle: &Rectangle) -> u32 { + 22 │ (rectangle.width + rectangle.height) * 2 + 23 │ } +───────┴──────────────────────────────────────────────────────────────────────── diff --git a/tests/snapshots/output/grid_numbers.snapshot.txt b/tests/snapshots/output/grid_numbers.snapshot.txt index 83ee57cd..b8b86bdb 100644 --- a/tests/snapshots/output/grid_numbers.snapshot.txt +++ b/tests/snapshots/output/grid_numbers.snapshot.txt @@ -1,25 +1,25 @@ -─────┬────────────────────────────────────────────────────────────────────────── - 1 │ /// A rectangle. First line is changed to prevent a regression of #1869 - 2 │ struct Rectangle { - 3 │ width: u32, - 4 │ height: u32, - 5 │ } - 6 │ - 7 │ fn main() { - 8 │ let rect1 = Rectangle { width: 30, height: 50 }; - 9 │ - 10 │ println!( - 11 │ "The perimeter of the rectangle is {} pixels.", - 12 │ perimeter(&rect1) - 13 │ ); - 14 │ println!(r#"This line contains invalid utf8: "�����"#; - 15 │ } - 16 │ - 17 │ fn area(rectangle: &Rectangle) -> u32 { - 18 │ rectangle.width * rectangle.height - 19 │ } - 20 │ - 21 │ fn perimeter(rectangle: &Rectangle) -> u32 { - 22 │ (rectangle.width + rectangle.height) * 2 - 23 │ } -─────┴────────────────────────────────────────────────────────────────────────── +───────┬──────────────────────────────────────────────────────────────────────── + 1 │ /// A rectangle. First line is changed to prevent a regression of #1869 + 2 │ struct Rectangle { + 3 │ width: u32, + 4 │ height: u32, + 5 │ } + 6 │ + 7 │ fn main() { + 8 │ let rect1 = Rectangle { width: 30, height: 50 }; + 9 │ + 10 │ println!( + 11 │ "The perimeter of the rectangle is {} pixels.", + 12 │ perimeter(&rect1) + 13 │ ); + 14 │ println!(r#"This line contains invalid utf8: "�����"#; + 15 │ } + 16 │ + 17 │ fn area(rectangle: &Rectangle) -> u32 { + 18 │ rectangle.width * rectangle.height + 19 │ } + 20 │ + 21 │ fn perimeter(rectangle: &Rectangle) -> u32 { + 22 │ (rectangle.width + rectangle.height) * 2 + 23 │ } +───────┴────────────────────────────────────────────────────────────────────────