diff options
author | Thomas Otto <th1000s@posteo.net> | 2020-12-30 21:03:22 +0100 |
---|---|---|
committer | Thomas Otto <th1000s@posteo.net> | 2021-04-19 22:37:41 +0200 |
commit | f71f4da9bae1c05432297d7b050edca26614d0ed (patch) | |
tree | 9a0539c996dc8001cf82361e183d59c9c388249f | |
parent | 1b20df46486e7d7ae4b5e7283961c1f6aebf2b70 (diff) |
Add side-by-side line wrapping mode
If the current line does not fit into the panel, then
it is not truncated but split into multiple lines. A
wrapping symbol is placed at the end of the line. If
the new line is short enought, it is right-aligned.
Wrapping is limited to a certain number of lines, if this
is exceeded the line is truncated by a now highlighted
truncation symbol.
Commandline argument `-S` / `--side-by-side-wrapped`.
Also adapted `--keep-plus-minus-markers` logic, required
to calculate the exact remaining panel width.
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/ansi/mod.rs | 1 | ||||
-rw-r--r-- | src/cli.rs | 4 | ||||
-rw-r--r-- | src/config.rs | 30 | ||||
-rw-r--r-- | src/delta.rs | 4 | ||||
-rw-r--r-- | src/features/line_numbers.rs | 5 | ||||
-rw-r--r-- | src/features/mod.rs | 5 | ||||
-rw-r--r-- | src/features/side_by_side.rs | 87 | ||||
-rw-r--r-- | src/features/side_by_side_wrap.rs | 880 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/options/set.rs | 9 | ||||
-rw-r--r-- | src/paint.rs | 66 | ||||
-rw-r--r-- | src/style.rs | 8 |
14 files changed, 1073 insertions, 35 deletions
@@ -274,6 +274,7 @@ dependencies = [ "lazy_static", "regex", "shell-words", + "static_assertions", "structopt", "syntect", "unicode-segmentation", @@ -653,6 +654,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -27,6 +27,7 @@ itertools = "0.10.0" lazy_static = "1.4" regex = "1.4.5" shell-words = "1.0.0" +static_assertions = "1.1.0" structopt = "0.3.21" unicode-segmentation = "1.7.1" unicode-width = "0.1.8" diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs index 33f4e3e7..88212ef1 100644 --- a/src/ansi/mod.rs +++ b/src/ansi/mod.rs @@ -12,6 +12,7 @@ use iterator::{AnsiElementIterator, Element}; pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K"; pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K"; pub const ANSI_SGR_RESET: &str = "\x1b[0m"; +pub const ANSI_SGR_REVERSE: &str = "\x1b[7m"; pub fn strip_ansi_codes(s: &str) -> String { strip_ansi_codes_from_strings_iterator(ansi_strings_iterator(s)) @@ -217,6 +217,10 @@ pub struct Opt { #[structopt(short = "s", long = "side-by-side")] pub side_by_side: bool, + /// Display a side-by-side diff and wrap overlong lines instead of truncating them. + #[structopt(short = "S", long = "side-by-side-wrapped")] + pub side_by_side_wrapped: bool, + #[structopt(long = "diff-highlight")] /// Emulate diff-highlight (https://github.com/git/git/tree/master/contrib/diff-highlight) pub diff_highlight: bool, diff --git a/src/config.rs b/src/config.rs index c0808b01..1c892ec3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ use syntect::highlighting::Style as SyntectStyle; use syntect::highlighting::Theme as SyntaxTheme; use syntect::parsing::SyntaxSet; +use crate::ansi; use crate::bat_utils::output::PagingMode; use crate::cli; use crate::color; @@ -15,8 +16,10 @@ use crate::delta::State; use crate::env; use crate::features::navigate; use crate::features::side_by_side; +use crate::features::side_by_side_wrap; use crate::git_config::GitConfigEntry; use crate::style::{self, Style}; +use crate::syntect_color; pub struct Config { pub available_terminal_width: usize, @@ -39,6 +42,7 @@ pub struct Config { pub hunk_header_style_include_line_number: bool, pub hyperlinks: bool, pub hyperlinks_file_link_format: String, + pub inline_hint_color: Option<SyntectStyle>, pub inspect_raw_lines: cli::InspectRawLines, pub keep_plus_minus_markers: bool, pub line_numbers: bool, @@ -72,6 +76,7 @@ pub struct Config { pub git_plus_style: Style, pub show_themes: bool, pub side_by_side: bool, + pub side_by_side_wrapped: bool, pub side_by_side_data: side_by_side::SideBySideData, pub syntax_dummy_theme: SyntaxTheme, pub syntax_set: SyntaxSet, @@ -81,6 +86,7 @@ pub struct Config { pub true_color: bool, pub truncation_symbol: String, pub whitespace_error_style: Style, + pub wrap_config: side_by_side_wrap::WrapConfig, pub zero_style: Style, } @@ -205,6 +211,11 @@ impl From<cli::Opt> for Config { hyperlinks: opt.hyperlinks, hyperlinks_file_link_format: opt.hyperlinks_file_link_format, inspect_raw_lines: opt.computed.inspect_raw_lines, + inline_hint_color: Some(SyntectStyle { + // TODO: color from theme? + foreground: syntect_color::syntect_color_from_ansi_name("blue").unwrap(), + ..SyntectStyle::default() + }), keep_plus_minus_markers: opt.keep_plus_minus_markers, line_numbers: opt.line_numbers, line_numbers_left_format: opt.line_numbers_left_format, @@ -237,6 +248,7 @@ impl From<cli::Opt> for Config { git_plus_style, show_themes: opt.show_themes, side_by_side: opt.side_by_side, + side_by_side_wrapped: opt.side_by_side_wrapped, side_by_side_data, syntax_dummy_theme: SyntaxTheme::default(), syntax_set: opt.computed.syntax_set, @@ -244,7 +256,23 @@ impl From<cli::Opt> for Config { tab_width: opt.tab_width, tokenization_regex, true_color: opt.computed.true_color, - truncation_symbol: "→".to_string(), + truncation_symbol: { + let sym = "→"; + if opt.side_by_side_wrapped { + format!("{}{}{}", ansi::ANSI_SGR_REVERSE, sym, ansi::ANSI_SGR_RESET) + } else { + sym.to_string() + } + }, + wrap_config: side_by_side_wrap::WrapConfig { + wrap_symbol: "↵".to_string(), + wrap_right_symbol: "↴".to_string(), + right_align_symbol: "…".to_string(), + // TODO, support multi-character symbols, and thus store + // right_align_symbol_len here? + use_wrap_right_permille: 370, + max_lines: 3, + }, whitespace_error_style, zero_style, } diff --git a/src/delta.rs b/src/delta.rs index 7d52eae1..43eb6ad1 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -25,6 +25,10 @@ pub enum State { HunkMinus(Option<String>), // In hunk; removed line (raw_line) HunkPlus(Option<String>), // In hunk; added line (raw_line) Unknown, + // The following elements are created when a line is wrapped to display it: + HunkZeroWrapped, // Wrapped unchanged line in side-by-side mode + HunkMinusWrapped, // Wrapped removed line in side-by-side mode + HunkPlusWrapped, // Wrapped added line in side-by-side mode } #[derive(Debug, PartialEq)] diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs index e71bb5cf..d0f49435 100644 --- a/src/features/line_numbers.rs +++ b/src/features/line_numbers.rs @@ -83,15 +83,18 @@ pub fn format_and_paint_line_numbers<'a>( line_numbers_data.line_number[Left] += 1; ((Some(nr_left), None), (minus_style, plus_style)) } + State::HunkMinusWrapped => ((None, None), (minus_style, plus_style)), State::HunkZero => { line_numbers_data.line_number[Left] += 1; line_numbers_data.line_number[Right] += 1; ((Some(nr_left), Some(nr_right)), (zero_style, zero_style)) } + State::HunkZeroWrapped => ((None, None), (zero_style, zero_style)), State::HunkPlus(_) => { line_numbers_data.line_number[Right] += 1; ((None, Some(nr_right)), (minus_style, plus_style)) } + State::HunkPlusWrapped => ((None, None), (minus_style, plus_style)), _ => return Vec::new(), }; @@ -153,7 +156,7 @@ lazy_static! { .unwrap(); } -#[derive(Default)] +#[derive(Default, Debug)] pub struct LineNumbersData<'a> { pub format_data: LeftRight<LineNumberFormatData<'a>>, pub line_number: LeftRight<usize>, diff --git a/src/features/mod.rs b/src/features/mod.rs index 67dbd6ba..00c36ae6 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -55,6 +55,10 @@ pub fn make_builtin_features() -> HashMap<String, BuiltinFeature> { "side-by-side".to_string(), side_by_side::make_feature().into_iter().collect(), ), + ( + "side-by-side-wrapped".to_string(), + side_by_side_wrap::make_feature().into_iter().collect(), + ), ] .into_iter() .collect() @@ -90,6 +94,7 @@ pub mod line_numbers; pub mod navigate; pub mod raw; pub mod side_by_side; +pub mod side_by_side_wrap; #[cfg(test)] pub mod tests { diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs index 872959f0..861000e5 100644 --- a/src/features/side_by_side.rs +++ b/src/features/side_by_side.rs @@ -9,6 +9,7 @@ use crate::cli; use crate::config::Config; use crate::delta::State; use crate::features::line_numbers; +use crate::features::side_by_side_wrap; use crate::features::OptionValueFunction; use crate::paint::BgFillMethod; use crate::paint::BgFillWidth; @@ -77,6 +78,7 @@ impl<T: Default> Default for LeftRight<T> { } } +#[derive(Debug)] pub struct Panel { pub width: usize, pub offset: usize, @@ -129,12 +131,38 @@ pub fn line_is_too_long(line: &str, line_width: usize) -> bool { line_sum > line_width + 2 } +// Return whether any of the input lines is too long, and a data +// structure indicating which are too long. This is done to avoid +// calculating the length again later. +pub fn has_long_lines( + lines: &LeftRight<&Vec<(String, State)>>, + line_width: &line_numbers::SideBySideLineWidth, +) -> (bool, LeftRight<Vec<bool>>) { + let check_lines = |lines: &Vec<(String, State)>, line_width| { + let mut wrap_any = false; + let wrapping_lines = lines + .iter() + .map(|(line, _)| line_is_too_long(line, line_width)) + .inspect(|b| wrap_any |= b) + .collect(); + (wrap_any, wrapping_lines) + }; + + let (wrap_left, left_wrapping_lines) = check_lines(&lines.left, line_width.left); + let (wrap_right, right_wrapping_lines) = check_lines(&lines.right, line_width.right); + + ( + wrap_left || wrap_right, + LeftRight::new(left_wrapping_lines, right_wrapping_lines), + ) +} + /// Emit a sequence of minus and plus lines in side-by-side mode. #[allow(clippy::too_many_arguments)] -pub fn paint_minus_and_plus_lines_side_by_side<'a>( +pub fn paint_minus_and_plus_lines_side_by_side( syntax_left_right: LeftRight<Vec<Vec<(SyntectStyle, &str)>>>, diff_left_right: LeftRight<Vec<Vec<(Style, &str)>>>, - states_left_right: LeftRight<Vec<&'a State>>, + states_left_right: LeftRight<Vec<State>>, line_alignment: Vec<(Option<usize>, Option<usize>)>, output_buffer: &mut String, config: &Config, @@ -147,15 +175,10 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>( &syntax_left_right[Left], &diff_left_right[Left], match minus_line_index { - Some(i) => states_left_right[Left][i], + Some(i) => &states_left_right[Left][i], None => &State::HunkMinus(None), }, line_numbers_data, - if config.keep_plus_minus_markers { - Some(config.minus_style.paint("-")) - } else { - None - }, background_color_extends_to_terminal_width[Left], config, )); @@ -164,15 +187,10 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>( &syntax_left_right[Right], &diff_left_right[Right], match plus_line_index { - Some(i) => states_left_right[Right][i], + Some(i) => &states_left_right[Right][i], None => &State::HunkPlus(None), }, line_numbers_data, - if config.keep_plus_minus_markers { - Some(config.plus_style.paint("+")) - } else { - None - }, background_color_extends_to_terminal_width[Right], config, )); @@ -182,6 +200,7 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>( #[allow(clippy::too_many_arguments)] pub fn paint_zero_lines_side_by_side( + raw_line: &str, syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>, diff_style_sections: Vec<Vec<(Style, &str)>>, output_buffer: &mut String, @@ -190,16 +209,30 @@ pub fn paint_zero_lines_side_by_side( painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgFillWidth, ) { - let state = State::HunkZero; + let states = vec![State::HunkZero]; + + let (states, syntax_style_sections, diff_style_sections) = if config.side_by_side_wrapped { + side_by_side_wrap::wrap_zero_block( + &config, + &raw_line, + states, + syntax_style_sections, + diff_style_sections, + &line_numbers_data, + ) + } else { + (states, syntax_style_sections, diff_style_sections) + }; - for (line_index, (syntax_sections, diff_sections)) in syntax_style_sections - .iter() + for (line_index, ((syntax_sections, diff_sections), state)) in syntax_style_sections + .into_iter() .zip_eq(diff_style_sections.iter()) + .zip_eq(states.into_iter()) .enumerate() { for panel_side in &[PanelSide::Left, PanelSide::Right] { let (mut panel_line, panel_line_is_empty) = Painter::paint_line( - syntax_sections, + &syntax_sections, diff_sections, &state, line_numbers_data, @@ -219,7 +252,7 @@ pub fn paint_zero_lines_side_by_side( ); output_buffer.push_str(&panel_line); - if panel_side == &PanelSide::Left { + if panel_side == &PanelSide::Left && state != State::HunkZeroWrapped { // TODO: Avoid doing the superimpose_style_sections work twice. // HACK: These are getting incremented twice, so knock them back down once. if let Some(d) = line_numbers_data.as_mut() { @@ -239,7 +272,6 @@ fn paint_left_panel_minus_line<'a>( diff_style_sections: &[Vec<(Style, &str)>], state: &'a State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, - painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgFillWidth, config: &Config, ) -> String { @@ -250,7 +282,6 @@ fn paint_left_panel_minus_line<'a>( state, line_numbers_data, PanelSide::Left, - painted_prefix, config, ); pad_panel_line_to_width( @@ -274,7 +305,6 @@ fn paint_right_panel_plus_line<'a>( diff_style_sections: &[Vec<(Style, &str)>], state: &'a State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, - painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgFillWidth, config: &Config, ) -> String { @@ -285,7 +315,6 @@ fn paint_right_panel_plus_line<'a>( state, line_numbers_data, PanelSide::Right, - painted_prefix, config, ); @@ -369,7 +398,6 @@ fn paint_minus_or_plus_panel_line( state: &State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, panel_side: PanelSide, - painted_prefix: Option<ansi_term::ANSIString>, config: &Config, ) -> (String, bool) { let (empty_line_syntax_sections, empty_line_diff_sections) = (Vec::new(), Vec::new()); @@ -394,6 +422,15 @@ fn paint_minus_or_plus_panel_line( ) }; + let painted_prefix = match (config.keep_plus_minus_markers, panel_side, state) { + (true, _, State::HunkPlusWrapped) | (true, _, State::HunkMinusWrapped) => { + Some(config.plus_style.paint(" ")) + } + (true, PanelSide::Left, _) => Some(config.minus_style.paint("-")), + (true, PanelSide::Right, _) => Some(config.plus_style.paint("+")), + _ => None, + }; + let (line, line_is_empty) = Painter::paint_line( line_syntax_sections, line_diff_sections, @@ -537,7 +574,7 @@ pub mod tests { let output = run_delta(TWO_PLUS_LINES_DIFF, &config); let mut lines = output.lines().skip(7); let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap()); - assert_eq!("│ │ │ 1 │a = 1 ", strip_ansi_codes(line_1)); + assert_eq!("│ │ │ 1 │a = 1", strip_ansi_codes(line_1)); assert_eq!("│ │ │ 2 │b = 2345>", strip_ansi_codes(line_2)); } diff --git a/src/features/side_by_side_wrap.rs b/src/features/side_by_side_wrap.rs new file mode 100644 index 00000000..edd76db0 --- /dev/null +++ b/src/features/side_by_side_wrap.rs @@ -0,0 +1,880 @@ +use syntect::highlighting::Style as SyntectStyle; +use unicode_segmentation::UnicodeSegmentation; + +use crate::config::Config; +use crate::delta::State; +use crate::features::line_numbers; +use crate::features::side_by_side::line_is_too_long; +use crate::features::side_by_side::LeftRight; +use crate::features::side_by_side::PanelSide::*; +use crate::features::OptionValueFunction; +use crate::style::Style; + +use super::{line_numbers::SideBySideLineWidth, side_by_side::available_line_width}; + +#[derive(Clone)] +pub struct WrapConfig { + pub wrap_symbol: String, + pub wrap_right_symbol: String, + pub right_align_symbol: String, + pub use_wrap_right_permille: usize, + pub max_lines: usize, +} + +pub fn make_feature() -> Vec<(String, OptionValueFunction)> { + builtin_feature!([ + ( + "side-by-side-wrapped", + bool, + None, + _opt => true + ), + ( + "side-by-side", + bool, + None, + _opt => true + ) + ]) +} + +// Wrap the given `line` if it is longer than `line_width`. Wrap to at most +// `wrap_config.max_lines` lines, then truncate again. Place `wrap_config.wrap_symbol` +// at then end of wrapped lines. However if wrapping results in only one extra line +// and if the width of the wrapped line is less than `wrap_config.use_wrap_right_permille` +// right-align the second line and use `wrap_config.wrap_right_symbol`. +// +// The input `line` is expected to start with an (ultimately not printed) "+/-/ " prefix. +// A prefix ("_") is also added to the start of wrapped lines. +pub fn wrap_line<'a, I, S>( + config: &'a Config, + line: I, + line_width: usize, + fill_style: &S, + inline_hint_style: &Option<S>, +) -> Vec<Vec<(S, &'a str)>> +where + I: IntoIterator<Item = (S, &'a str)> + std::fmt::Debug, + <I as IntoIterator>::IntoIter: DoubleEndedIterator, + S: Copy + Default + std::fmt::Debug, +{ + let mut result = Vec::new(); + + let wrap_config = &config.wrap_config; + + // Symbol which: + // - represents the additional "+/-/ " prefix on the unwrapped input line, its + // length is added to the line_width. + // - can be more prominent than a space because syntax highlighting has already + // been done. + // - is added to the beginning of wrapped lines so the wrapped lines also have + // a prefix (which is not printed). + const LINEPREFIX: &str = "_"; + static_assertions::const_assert_eq!(LINEPREFIX.len(), 1); // must be a 1-byte char + + let max_len = line_width + LINEPREFIX.len(); + + // Stay defensive just in case: guard against infinite loops. + let mut n = max_len * wrap_config.max_lines * 2; + + let mut curr_line = Vec::new(); + let mut curr_len = 0; + + // Determine the background (diff) and color (syntax) of + // an inserted symbol. + let symbol_style = match inline_hint_style { + Some(style) => *style, + None => *fill_style, + }; + + let mut stack = line.into_iter().rev().collect::<Vec<_>>(); + + while !stack.is_empty() + && result.len() + 1 < wrap_config.max_lines + && max_len > LINEPREFIX.len() + && n > 0 + { + n -= 1; + + let (style, text, graphemes) = stack + .pop() + .map(|(style, text)| (style, text, text.grapheme_indices(true).collect::<Vec<_>>())) + .unwrap(); + let new_sum = curr_len + graphemes.len(); + + let must_split = if new_sum < max_len { + curr_line.push((style, text)); + curr_len = new_sum; + false + } else if new_sum == max_len { + match stack.last() { + // Perfect fit, no need to make space for a `wrap_symbol`. + None => { + curr_line.push((style, text)); + curr_len = new_sum; + false + } + // A single '\n' left on the stack can be pushed onto the current line. + Some((next_style, nl)) if stack.len() == 1 && *nl == "\n" => { + curr_line.push((style, text)); + curr_line.push((*next_style, *nl)); + stack.pop(); + curr_len = new_sum; // do not count the '\n' + false + } + _ => true, + } + } else if new_sum == max_len + 1 && stack.is_empty() { + // If the one overhanging char is '\n' then keep it on the current line. + if !text.is_empty() && *text.as_bytes().last().unwrap() == b'\n' { + curr_line.push((style, text)); + curr_len = new_sum - 1; // do not count the '\n' + false + } else { + true + } + } else { + true + }; + + // Text must be split, one part (or just `wrap_symbol`) is added to the + // current line, the other is pushed onto the stack. + if must_split { + let grapheme_split_pos = graphemes.len() - (new_sum - max_len) - 1; + + let next_line = if grapheme_split_pos == 0 { + text + } else { + let byte_split_pos = graphemes[grapheme_split_pos].0; + let this_line = &text[..byte_split_pos]; + curr_line.push((style, this_line)); + &text[byte_split_pos..] + }; + stack.push((style, next_line)); + + curr_line.push((symbol_style, &wrap_config.wrap_symbol)); + result.push(curr_line); + + curr_line = vec![(S::default(), LINEPREFIX)]; + curr_len = LINEPREFIX.len(); + } + } + + // Right-align wrapped line: + // Done if wrapping adds exactly one line and it is less than the + // given permille of panel width wide. Also change the wrap symbol at the + // end of the previous (first) line. + if result.len() == 1 && !curr_line.is_empty() { + let current_permille = (curr_len * 1000) / max_len; + + // &config.wrap_config.right_align_symbol length + const RIGHT_ALIGN_SYMBOL_LEN: usize = 1; + let pad_len = max_len.saturating_sub(curr_len - LINEPREFIX.len() + RIGHT_ALIGN_SYMBOL_LEN); + + if wrap_config.use_wrap_right_permille > current_permille + && result.len() == 1 + && pad_len > RIGHT_ALIGN_SYMBOL_LEN + { + const SPACES: &str = " "; + + match result.last_mut() { + Some(ref mut vec) if !vec.is_empty() => { + vec.last_mut().unwrap().1 = &wrap_config.wrap_right_symbol + } + _ => unreachable!("wrap result must not be empty"), + } + + let mut right_aligned_line = vec![(S::default(), LINEPREFIX)]; + + for _ in 0..(pad_len / SPACES.len()) { + right_aligned_line.push((*fill_style, SPACES)); + } + + match pad_len % SPACES.len() { + 0 => (), + n => right_aligned_line.push((*fill_style, &SPACES[0..n])), + } + + right_aligned_line.push((symbol_style, &wrap_config.right_align_symbol)); + + // skip LINEPREFIX + right_aligned_line.extend(curr_line.into_iter().skip(1)); + + curr_line = right_aligned_line; + } + } + + if !curr_line.is_empty() { + result.push(curr_line); + } + + if !stack.is_empty() { + if result.is_empty() { + result.push(Vec::new()); + } + result + .last_mut() + .map(|vec| vec.extend(stack.into_iter().rev())); + } + + result +} + +fn wrap_if_too_long<'a, S>( + config: &'a Config, + wrapped: &mut Vec<Vec<(S, &'a str)>>, + input_vec: Vec<(S, &'a str)>, + must_wrap: bool, + line_width: usize, + fill_style: &S, + inline_hint_style: &Option<S>, +) -> (usize, usize) +where + S: Copy + Default + std::fmt::Debug, +{ + let size_prev = wrapped.len(); + + if must_wrap { + wrapped.append(&mut wrap_line( + &config, + input_vec.into_iter(), + line_width, + fill_style, + &inline_hint_style, + )); + } else { + wrapped.push(input_vec.to_vec()); + } + + (size_prev, wrapped.len()) +} + +#[allow(clippy::comparison_chain, clippy::type_complexity)] +pub fn wrap_plusminus_block<'c: 'a, 'a>( + config: &'c Config, + syntax: LeftRight<Vec<Vec<(SyntectStyle, &'a str)>>>, + diff: LeftRight<Vec<Vec<(Style, &'a str)>>>, + alignment: &[(Option<usize>, Option<usize>)], + line_width: &SideBySideLineWidth, + wrapinfo: &'a LeftRight<Vec<bool>>, +) -> ( + Vec<(Option<usize>, Option<usize>)>, + LeftRight<Vec<State>>, + LeftRight<Vec<Vec<(SyntectStyle, &'a str)>>>, + LeftRight<Vec<Vec<(Style, &'a str)>>>, +) { + let mut new_alignment = Vec::new(); + let mut new_states = LeftRight::<Vec<State>>::default(); + let mut new_wrapped_syntax = LeftRight::default(); + let mut new_wrapped_diff = LeftRight::default(); + + // Turn all these into iterators so they can be advanced according + // to the alignment. + let mut syntax = LeftRight::new(syntax.left.into_iter(), syntax.right.into_iter()); + let mut diff = LeftRight::new(diff.left.into_iter(), diff.right.into_iter()); + let mut wrapinfo = LeftRight::new(wrapinfo.left.iter(), wrapinfo.right.iter()); + + let fill_style = LeftRight::new(&config.minus_style, &config.plus_style); + + // Internal helper function to perform wrapping for both the syntax and the + // diff highlighting (SyntectStyle and Style). + #[allow(clippy::too_many_arguments)] + pub fn wrap_syntax_and_diff<'a, ItSyn, ItDiff, ItWrap>( + config: &'a Config, + wrapped_syntax: &mut Vec<Vec<(SyntectStyle, &'a str)>>, + wrapped_diff: &mut Vec<Vec<(Style, &'a str)>>, + syntax_iter: &mut ItSyn, + diff_iter: &mut ItDiff, + wrapinfo_iter: &mut ItWrap, + line_width: usize, + fill_style: &Style, + errhint: &'a str, + ) -> (usize, usize) + where + ItSyn: Iterator<Item = Vec<(SyntectStyle, &'a str)>>, + ItDiff: Iterator<Item = Vec<(Style, &'a str)>>, + ItWrap: Iterator<Item = &'a bool>, + { + let must_wrap = *wrapinfo_iter + .next() + .unwrap_or_else(|| panic!("bad wrap info {}", errhint)); + + let (start, extended_to) = wrap_if_too_long( + &config, + wrapped_syntax, + syntax_iter + .next() + .unwrap_or_else(|| panic!("bad syntax alignment {}", errhint)), + must_wrap, + line_width, + &SyntectStyle::default(), + &config.inline_hint_color, + ); + |