diff options
Diffstat (limited to 'src/wrapping.rs')
-rw-r--r-- | src/wrapping.rs | 1090 |
1 files changed, 1090 insertions, 0 deletions
diff --git a/src/wrapping.rs b/src/wrapping.rs new file mode 100644 index 00000000..abb19db1 --- /dev/null +++ b/src/wrapping.rs @@ -0,0 +1,1090 @@ +use syntect::highlighting::Style as SyntectStyle; +use unicode_segmentation::UnicodeSegmentation; + +use crate::config::INLINE_SYMBOL_WIDTH_1; + +use crate::config::Config; +use crate::delta::State; +use crate::features::line_numbers; +use crate::features::line_numbers::SideBySideLineWidth; +use crate::features::side_by_side::available_line_width; +use crate::features::side_by_side::line_is_too_long; +use crate::features::side_by_side::LineSegments; +use crate::features::side_by_side::{Left, Right}; +use crate::plusminus::*; +use crate::style::Style; + +/// See [`wrap_line`] for documentation. +#[derive(Clone, Debug)] +pub struct WrapConfig { + pub left_symbol: String, + pub right_symbol: String, + pub right_prefix_symbol: String, + // In fractions of 1000 so that a >100 wide panel can + // still be configured down to a single character. + pub use_wrap_right_permille: usize, + // This value is --wrap-max-lines + 1, and unlimited is 0, see + // adapt_wrap_max_lines_argument() + pub max_lines: usize, +} + +/// Wrap the given `line` if it is longer than `line_width`. Wrap to at most +/// [Config::WrapConfig::max_lines](WrapConfig::max_lines) lines, +/// then truncate again - but never truncate if it is `0`. Place +/// [left_symbol](WrapConfig::left_symbol) at the end of wrapped lines. +/// If wrapping results in only *one* extra line and if the width of the wrapped +/// line is less than [use_wrap_right_permille](WrapConfig::use_wrap_right_permille) +/// then right-align the second line and use the symbols +/// [right_symbol](WrapConfig::right_symbol) and +/// on the next line [right_prefix_symbol](WrapConfig::right_prefix_symbol). +/// The inserted characters will follow the +/// [inline_hint_syntect_style](WrapConfig::inline_hint_syntect_style). +/// +/// The input `line` is expected to start with an (ultimately not printed) `+`, `-` or ` ` prefix. +/// The 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<LineSegments<'a, S>> +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 at the beginning of wrapped lines so the wrapped lines also have + // a prefix (which is not printed). + const LINEPREFIX: &str = "_"; + assert_eq!(LINEPREFIX.len(), INLINE_SYMBOL_WIDTH_1); // (args are const, optimized out) + + let max_len = line_width + LINEPREFIX.len(); + + // The current line being assembled from the input to fit exactly into the given width. + // A somewhat leaky abstraction as the fields are also accessed directly. + struct CurrLine<'a, S: Default> { + line_segments: LineSegments<'a, S>, + len: usize, + } + impl<'a, S: Default> CurrLine<'a, S> { + fn reset() -> Self { + CurrLine { + line_segments: vec![(S::default(), LINEPREFIX)], + len: LINEPREFIX.len(), + } + } + fn push_and_set_len(&mut self, text: (S, &'a str), len: usize) { + self.line_segments.push(text); + self.len = len; + } + fn has_text(&self) -> bool { + self.len > LINEPREFIX.len() + } + fn text_len(&self) -> usize { + if self.len > LINEPREFIX.len() { + self.len - LINEPREFIX.len() + } else { + debug_assert!(false, "push or reset first"); + 0 + } + } + } + + // The first `push_and_set_len` will include the "+/-/ " prefix, subsequent + // `reset()` add `LINEPREFIX`. Thus each line starts with a prefix. + let mut curr_line: CurrLine<S> = CurrLine { + line_segments: Vec::new(), + 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<_>>(); + + let line_limit_reached = |result: &Vec<_>| { + // If only the wrap symbol and no extra text fits, then wrapping is not possible. + let max_lines = if line_width <= INLINE_SYMBOL_WIDTH_1 { + 1 + } else { + wrap_config.max_lines + }; + + max_lines > 0 && result.len() + 1 >= max_lines + }; + + while !stack.is_empty() && !line_limit_reached(&result) && max_len > LINEPREFIX.len() { + let (style, text, graphemes) = stack + .pop() + .map(|(style, text)| (style, text, text.grapheme_indices(true).collect::<Vec<_>>())) + .unwrap(); + + let new_len = curr_line.len + graphemes.len(); + + let must_split = if new_len < max_len { + curr_line.push_and_set_len((style, text), new_len); + false + } else if new_len == max_len { + match stack.last() { + // Perfect fit, no need to make space for a `wrap_symbol`. + None => { + curr_line.push_and_set_len((style, text), new_len); + false + } + #[allow(clippy::identity_op)] + // 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_and_set_len((style, text), new_len); + // Do not count the '\n': + 0 + curr_line.push_and_set_len((*next_style, *nl), new_len + 0); + stack.pop(); + false + } + _ => true, + } + } else if new_len == max_len + 1 && stack.is_empty() { + // If the one overhanging char is '\n' then keep it on the current line. + if text.ends_with('\n') { + // Do not count the included '\n': - 1 + curr_line.push_and_set_len((style, text), new_len - 1); + 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_len - max_len) - 1; + + // The length does not matter anymore and `curr_line` will be reset + // at the end, so move the line segments out. + let mut line_segments = curr_line.line_segments; + + 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]; + line_segments.push((style, this_line)); + &text[byte_split_pos..] + }; + stack.push((style, next_line)); + + line_segments.push((symbol_style, &wrap_config.left_symbol)); + result.push(line_segments); + + curr_line = CurrLine::reset(); + } + } + + // Right-align wrapped line: + // Done if wrapping adds exactly one line and this line is less than the given + // permille wide. Also change the wrap symbol at the end of the previous (first) line. + if result.len() == 1 && curr_line.has_text() { + let current_permille = (curr_line.text_len() * 1000) / max_len; + + let pad_len = max_len.saturating_sub(curr_line.text_len() + INLINE_SYMBOL_WIDTH_1); + + if wrap_config.use_wrap_right_permille > current_permille && pad_len > INLINE_SYMBOL_WIDTH_1 + { + // The inserted spaces, which align a line to the right, point into this string. + const SPACES: &str = " "; + + match result.last_mut() { + Some(ref mut vec) if !vec.is_empty() => { + vec.last_mut().unwrap().1 = &wrap_config.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_prefix_symbol)); + + // skip LINEPREFIX which `CurrLine::reset()` adds + right_aligned_line.extend(curr_line.line_segments.into_iter().skip(1)); + + curr_line.line_segments = right_aligned_line; + + // curr_line.len not updated, as only 0 / 1 / > 1 is required now + } + } + + if curr_line.len > 0 { + result.push(curr_line.line_segments); + } + + // Anything that is left will be added to the (last) line. If this is too long it will + // be truncated later. + if !stack.is_empty() { + if result.is_empty() { + result.push(Vec::new()); + } + + // unwrap: previous `if` ensures result can not be empty + result.last_mut().unwrap().extend(stack.into_iter().rev()); + } + + result +} + +fn wrap_if_too_long<'a, S>( + config: &'a Config, + wrapped: &mut Vec<LineSegments<'a, S>>, + input_vec: LineSegments<'a, S>, + 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()) +} + +/// Call [`wrap_line`] for the `syntax` and the `diff` lines if `wrapinfo` says +/// a specific line was longer than `line_width`. Return an adjusted `alignment` +/// with regard to the added wrapped lines. +#[allow(clippy::comparison_chain, clippy::type_complexity)] +pub fn wrap_plusminus_block<'c: 'a, 'a>( + config: &'c Config, + syntax: PlusMinus<Vec<LineSegments<'a, SyntectStyle>>>, + diff: PlusMinus<Vec<LineSegments<'a, Style>>>, + alignment: &[(Option<usize>, Option<usize>)], + line_width: &SideBySideLineWidth, + wrapinfo: &'a PlusMinus<Vec<bool>>, +) -> ( + Vec<(Option<usize>, Option<usize>)>, + PlusMinus<Vec<State>>, + PlusMinus<Vec<LineSegments<'a, SyntectStyle>>>, + PlusMinus<Vec<LineSegments<'a, Style>>>, +) { + let mut new_alignment = Vec::new(); + let mut new_states = PlusMinus::<Vec<State>>::default(); + let mut new_wrapped_syntax = PlusMinus::default(); + let mut new_wrapped_diff = PlusMinus::default(); + + // Turn all these into pairs of iterators so they can be advanced according + // to the alignment and independently. + let mut syntax = PlusMinus::new(syntax.minus.into_iter(), syntax.plus.into_iter()); + let mut diff = PlusMinus::new(diff.minus.into_iter(), diff.plus.into_iter()); + let mut wrapinfo = PlusMinus::new(wrapinfo[Left].iter(), wrapinfo[Right].iter()); + + let fill_style = PlusMinus::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<LineSegments<'a, SyntectStyle>>, + wrapped_diff: &mut Vec<LineSegments<'a, Style>>, + 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 = LineSegments<'a, SyntectStyle>>, + ItDiff: Iterator<Item = LineSegments<'a, Style>>, + 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, + ); + + let (start2, extended_to2) = wrap_if_too_long( + config, + wrapped_diff, + diff_iter + .next() + .unwrap_or_else(|| panic!("bad diff alignment {}", errhint)), + must_wrap, + line_width, + fill_style, + &None, + ); + + // The underlying text is the same for the style and diff, so + // the length of the wrapping should be identical: + assert_eq!( + (start, extended_to), + (start2, extended_to2), + "syntax and diff wrapping differs {}", + errhint + ); + + (start, extended_to) + } + + // This macro avoids having the same code block 4x in the alignment processing + macro_rules! wrap_and_assert { + ($side:tt, $errhint:tt, $have:tt, $expected:tt) => {{ + assert_eq!(*$have, $expected, "bad alignment index {}", $errhint); + $expected += 1; + + wrap_syntax_and_diff( + &config, + &mut new_wrapped_syntax[$side], + &mut new_wrapped_diff[$side], + &mut syntax[$side], + &mut diff[$side], + &mut wrapinfo[$side], + line_width[$side], + &fill_style[$side], + $errhint, + ) + }}; + } + + let mut m_expected = 0; + let mut p_expected = 0; + + // Process blocks according to the alignment and build a new alignment. + // If lines get added via wrapping these are assigned the state HunkMinusWrapped/HunkPlusWrapped. + for (minus, plus) in alignment { + let (minus_extended, plus_extended) = match (minus, plus) { + (Some(m), None) => { + let (minus_start, extended_to) = wrap_and_assert!(Left, "[*l*] (-)", m, m_expected); + + for i in minus_start..extended_to { + new_alignment.push((Some(i), None)); + } + + (extended_to - minus_start, 0) + } + (None, Some(p)) => { + let (plus_start, extended_to) = wrap_and_assert!(Right, "(-) [*r*]", p, p_expected); + + for i in plus_start..extended_to { + new_alignment.push((None, Some(i))); + } + + (0, extended_to - plus_start) + } + (Some(m), Some(p)) => { + let (minus_start, m_extended_to) = + wrap_and_assert!(Left, "[*l*] (r)", m, m_expected); + let (plus_start, p_extended_to) = + wrap_and_assert!(Right, "(l) [*r*]", p, p_expected); + + for (new_m, new_p) in (minus_start..m_extended_to).zip(plus_start..p_extended_to) { + new_alignment.push((Some(new_m), Some(new_p))); + } + + // This Some(m):Some(p) alignment might have become uneven, so fill + // up the shorter side with None. + + let minus_extended = m_extended_to - minus_start; + let plus_extended = p_extended_to - plus_start; + + let plus_minus = (minus_extended as isize) - (plus_extended as isize); + + if plus_minus > 0 { + for m in (m_extended_to as isize - plus_minus) as usize..m_extended_to { + new_alignment.push((Some(m), None)); + } + } else if plus_minus < 0 { + for p in (p_extended_to as isize + plus_minus) as usize..p_extended_to { + new_alignment.push((None, Some(p))); + } + } + + (minus_extended, plus_extended) + } + _ => unreachable!("None-None alignment"), + }; + + if minus_extended > 0 { + new_states[Left].push(State::HunkMinus(None)); + for _ in 1..minus_extended { + new_states[Left].push(State::HunkMinusWrapped); + } + } + if plus_extended > 0 { + new_states[Right].push(State::HunkPlus(None)); + for _ in 1..plus_extended { + new_states[Right].push(State::HunkPlusWrapped); + } + } + } + + ( + new_alignment, + new_states, + new_wrapped_syntax, + new_wrapped_diff, + ) +} + +#[allow(clippy::comparison_chain, clippy::type_complexity)] +pub fn wrap_zero_block<'c: 'a, 'a>( + config: &'c Config, + raw_line: &str, + mut states: Vec<State>, + syntax_style_sections: Vec<LineSegments<'a, SyntectStyle>>, + diff_style_sections: Vec<LineSegments<'a, Style>>, + line_numbers_data: &Option<&mut line_numbers::LineNumbersData>, +) -> ( + Vec<State>, + Vec<LineSegments<'a, SyntectStyle>>, + Vec<LineSegments<'a, Style>>, +) { + // The width is the minimum of the left/right side. The panels should be equally sized, + // but in rare cases the remaining panel width might differ due to the space the line + // numbers take up. + let line_width = if let Some(line_numbers_data) = line_numbers_data { + let width = available_line_width(config, line_numbers_data); + std::cmp::min(width[Left], width[Right]) + } else { + std::cmp::min( + config.side_by_side_data[Left].width, + config.side_by_side_data[Right].width, + ) + }; + + // Called with a single line, so no need to use the 1-sized bool vector. + // If that changes the wrapping logic should be updated as well. + debug_assert_eq!(diff_style_sections.len(), 1); + + let should_wrap = line_is_too_long(raw_line, line_width); + + if should_wrap { + let syntax_style = wrap_line( + config, + syntax_style_sections.into_iter().flatten(), + line_width, + &SyntectStyle::default(), + &config.inline_hint_color, + ); + let diff_style = wrap_line( + config, + diff_style_sections.into_iter().flatten(), + line_width, + // To actually highlight `config.inline_hint_color` characters: + &Style { + is_syntax_highlighted: true, + ..config.null_style + }, + &None, + ); + + states.resize_with(syntax_style.len(), || State::HunkZeroWrapped); + + (states, syntax_style, diff_style) + } else { + (states, syntax_style_sections, diff_style_sections) + } +} + +#[cfg(test)] +mod tests { + use lazy_static::lazy_static; + use syntect::highlighting::Style as SyntectStyle; + + use super::wrap_line; + use super::WrapConfig; + use crate::ansi::strip_ansi_codes; + use crate::config::Config; + use crate::features::side_by_side::LineSegments; + use crate::style::Style; + use crate::tests::integration_test_utils::{make_config_from_args, run_delta}; + + lazy_static! { + static ref S1: Style = Style { + is_syntax_highlighted: true, + ..Default::default() + }; + } + lazy_static! { + static ref S2: Style = Style { + is_emph: true, + ..Default::default() + }; + } + lazy_static! { + static ref SY: SyntectStyle = SyntectStyle::default(); + } + lazy_static! { + static ref SD: Style = Style::default(); + } + + const W: &str = &"+"; // wrap + const WR: &str = &"<"; // wrap-right + const RA: &str = &">"; // right-align + + lazy_static! { + static ref WRAP_DEFAULT_ARGS: Vec<&'static str> = vec![ + "--wrap-left-symbol", + W, + "--wrap-right-symbol", + WR, + "--wrap-right-prefix-symbol", + RA, + "--wrap-max-lines", + "4", + "--wrap-right-percent", + "37.0%", + ]; + } + + lazy_static! { + static ref TEST_WRAP_CFG: WrapConfig = + make_config_from_args(&WRAP_DEFAULT_ARGS).wrap_config; + } + + fn default_wrap_cfg_plus<'a>(args: &[&'a str]) -> Vec<&'a str> { + let mut result = WRAP_DEFAULT_ARGS.clone(); + result.extend_from_slice(args); + result + } + + fn mk_wrap_cfg(wrap_cfg: &WrapConfig) -> Config { + let mut cfg: Config = Config::from(make_config_from_args(&[])); + cfg.wrap_config = wrap_cfg.clone(); + cfg + } + + fn wrap_test<'a, I, S>(cfg: &'a Config, line: I, line_width: usize) -> Vec<LineSegments<'a, S>> + where + I: IntoIterator<Item = (S, &'a str)> + std::fmt::Debug, + <I as IntoIterator>::IntoIter: DoubleEndedIterator, + S: Copy + Default + std::fmt::Debug, + { + wrap_line(&cfg, line, line_width, &S::default(), &None) + } + + #[test] + fn test_wrap_line_single() { + let cfg = mk_wrap_cfg(&TEST_WRAP_CFG); + + { + // Empty input without a "+/-/ "-prefix usually does not happen + let line = vec![(*S1, "")]; + let lines = wrap_test(&cfg, line, 6); + assert!(lines.is_empty()); + } + + { + let line = vec![(*SY, "_0")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*SY, "_0")]]); + } + + { + let line = vec![(*S1, "_")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*S1, "_")]]); + } + + { + let line = vec![(*S1, "_"), (*S2, "0")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*S1, "_"), (*S2, "0")]]); + } + + { + let line = vec![(*S1, "_012"), (*S2, "34")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*S1, "_012"), (*S2, "34")]]); + } + + { + let line = vec![(*S1, "_012"), (*S2, "345")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*S1, "_012"), (*S2, "345")]]); + } + } + + #[test] + fn test_wrap_line_align_right() { + let cfg = mk_wrap_cfg(&TEST_WRAP_CFG); + + let line = vec![(*S1, "_0123456789ab")]; + let lines = wrap_test(&cfg, line, 11); + assert_eq!(lines.len(), 2); + assert_eq!(lines[0].last().unwrap().1, WR); + assert_eq!( + lines[1], + vec![(*SD, "_"), (*SD, " "), (*SD, ">"), (*S1, "ab")] + ); + } + + #[test] + fn test_wrap_line_align_right_2() { + let line = vec![(*S1, "_012"), (*S2, "3456")]; + + { + // Right align lines on the second line + let cfg = mk_wrap_cfg(&TEST_WRAP_CFG); + + let lines = wrap_test(&cfg, line.clone(), 6); + assert_eq!( + lines, + vec![ + vec![(*S1, "_012"), (*S2, "34"), (*SD, WR)], + vec![(*SD, "_"), (*SD, " "), (*SD, RA), (*S2, "56")] + ] + ); + } + + { + // Set right align percentage lower, normal wrapping + let mut no_align_right = TEST_WRAP_CFG.clone(); + no_align_right.use_wrap_right_permille = 1; // 0.1% + let cfg_no_align_right = mk_wrap_cfg(&no_align_right); + + let lines = wrap_test(&cfg_no_align_right, line, 6); + assert_eq!( + lines, + vec![ + vec![(*S1, "_012"), (*S2, "34"), (*SD, W)], + vec![(*SD, "_"), (*S2, "56")] + ] + ); + } + } + + #[test] + fn test_wrap_line_newlines<'a>() { + fn mk_input(len: usize) -> LineSegments<'static, Style> { + const IN: &str = "_0123456789abcdefZ"; + let v = &[*S1, *S2]; + let s1s2 = v.iter().cycle(); + let text: Vec<_> = IN.matches(|_| true).take(len + 1).collect(); + s1s2.zip(text.iter()) + .map(|(style, text)| (style.clone(), *text)) + .collect() + } + fn mk_input_nl(len: usize) -> LineSegments<'static, Style> { + const NL: &str = "\n"; + let mut line = mk_input(len); + line.push((*S2, NL)); + line + } + fn mk_expected<'a>( + prepend: Option<(Style, &'a str)>, + vec: &LineSegments<'a, Style>, + from: usize, + to: usize, + append: Option<(Style, &'a str)>, + ) -> LineSegments<'a, Style> { + let mut result: Vec<_> = vec[from..to].iter().cloned().collect(); + if let Some(val) = append { + result.push(val); + } + if let Some(val) = prepend { + result.insert(0, val); + } + result + } + + let cfg = mk_wrap_cfg(&TEST_WRAP_CFG); + + { + let line = vec![(*S1, "_012"), (*S2, "345\n")]; + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![vec![(*S1, "_012"), (*S2, "345\n")]]); + } + + { + for i in 0..=6 { + let line = mk_input(i); + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![mk_input(i)]); + + let line = mk_input_nl(i); + let lines = wrap_test(&cfg, line, 6); + assert_eq!(lines, vec![mk_input_nl(i)]); + } + } + + { + let line = mk_input_nl(9); + let lines = wrap_test(&cfg, line, 3); + let expected = mk_input_nl(9); + let line1 = mk_expected(None, &expected, 0, 3, Some((*SD, &W))); + let line2 = mk_expected(Some((*SD, "_")), &expected, 3, 5, Some((*SD, &W))); + let line3 = mk_expected(Some((*SD, "_")), &expected, 5, 7, Some((*SD, &W))); + let line4 = mk_expected(Some((*SD, "_")), &expected, 7, 11, None); + assert_eq!(lines, vec![line1, line2, line3, line4]); + } + + { + let line = mk_input_nl(10); + let lines = wrap_test(&cfg, line, 3); + let expected = mk_input_nl(10); + let line1 = mk_expected(None, &expected, 0, 3, Some((*SD, &W))); + let line2 = mk_expected(Some((*SD, "_")), &expected, 3, 5, Some((*SD, &W))); + let line3 = mk_expected(Some((*SD, "_")), &expected, 5, 7, Some((*SD, &W))); + let line4 = mk_expected(Some((*SD, "_")), &expected, 7, 9, Some((*SD, &W))); + let line5 = mk_expected(Some((*SD, "_")), &expected, 9, 11, Some((*S2, "\n"))); + assert_eq!(lines, vec![line1, line2, line3, line4, line5]); + } + + { + let line = vec![(*S1, "_abc"), (*S2, "01230123012301230123"), (*S1, "ZZZZZ")]; + + let wcfg1 = mk_wrap_cfg(&WrapConfig { + max_lines: 1, + ..TEST_WRAP_CFG.clone() + }); + let wcfg2 = mk_wrap_cfg(&WrapConfig { + max_lines: 2, + ..TEST_WRAP_CFG.clone() + }); + let wcfg3 = mk_wrap_cfg(&WrapConfig { + max_lines: 3, + ..TEST_WRAP_CFG.clone() + }); + + let lines = wrap_line(&wcfg1, line.clone(), 4, &Style::default(), &None); + assert_eq!(lines.len(), 1); + assert_eq!(lines.last().unwrap().last().unwrap().1, "ZZZZZ"); + let lines = wrap_line(&wcfg2, line.clone(), 4, &Style::default(), &None); + assert_eq!(lines.len(), 2); + assert_eq!(lines.last().unwrap().last().unwrap().1, "ZZZZZ"); + let lines = wrap_line(&wcfg3, line.clone(), 4, &Style::default(), &None); + assert_eq!(lines.len(), 3); + assert_eq!(lines.last().unwrap().last().unwrap().1, "ZZZZZ"); + } + } + + #[test] + fn test_wrap_line_unicode() { + let cfg = mk_wrap_cfg(&TEST_WRAP_CFG); + + // from UnicodeSegmentation documentation and the linked + // Unicode Standard Annex #29 + let line = vec![(*S1, "_abc"), (*S2, "mnö̲"), (*S1, "xyz")]; + let lines = wrap_test(&cfg, line, 4); + assert_eq!( + lines, + vec![ + vec![(*S1, "_abc"), (*SD, &W)], + vec![(*SD, "_"), (*S2, "mnö̲"), (*SD, &W)], + vec![(*SD, "_"), (*S1, "xyz")] + ] + ); + + // Not working: Tailored grapheme clusters: क्षि = क् + षि + let line = vec![(*S1, "_abc"), (*S2, "deநி"), (*S1, "ghij")]; + let lines = wrap_test(&cfg, line, 4); + assert_eq!( + lines, + vec![ + vec![(*S1, "_abc"), (*SD, &W)], + vec![(*SD, "_"), (*S2, "deநி"), (*SD, &W)], + vec![(*SD, "_"), (*S1, "ghij")] + ] + ); + } + + const HUNK_ZERO_DIFF: &str = "\ +diff --git i/a.py w/a.py +index 223ca50..e69de29 100644 +--- i/a.py ++++ w/a.py +@@ -4,3 +15,3 @@ + abcdefghijklmnopqrstuvwxzy 0123456789 0123456789 0123456789 0123456789 0123456789 +-a = 1 ++a = 2 +"; + + const HUNK_ZERO_LARGE_LINENUMBERS_DIFF: &str = "\ +diff --git i/a.py w/a.py +index 223ca50..e69de29 100644 +--- i/a.py ++++ w/a.py +@@ -10,3 +101999,3 @@ + abcdefghijklmnopqrstuvwxzy 0123456789 0123456789 0123456789 0123456789 0123456789 +-a = 1 ++a = 2 +"; + + const HUNK_MP_DIFF: &str = "\ +diff --git i/a.py w/a.py +index 223ca50..e69de29 100644 +--- i/a.py ++++ w/a.py +@@ -4,3 +15,3 @@ + abcdefghijklmnopqrstuvwxzy 0123456789 0123456789 0123456789 0123456789 0123456789 +-a = 0123456789 0123456789 0123456789 0123456789 0123456789 ++b = 0123456789 0123456789 0123456789 0123456789 0123456789 +"; + + const HUNK_ALIGN_DIFF_HEADER: &str = "--- a\n+++ b\n@@ -1,1 +1,1 @@\n"; + const HUNK_ALIGN_DIFF_SHORT: &str = ".........1.........2....\n"; + const HUNK_ALIGN_DIFF_LONG: &str = + ".........1.........2.........3.........4.........5.........6\n"; + + #[test] + fn test_wrap_with_unequal_hunk_zero_width() { + let mut config = make_ |