summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Otto <th1000s@posteo.net>2020-12-30 21:03:22 +0100
committerThomas Otto <th1000s@posteo.net>2021-04-19 22:37:41 +0200
commitf71f4da9bae1c05432297d7b050edca26614d0ed (patch)
tree9a0539c996dc8001cf82361e183d59c9c388249f
parent1b20df46486e7d7ae4b5e7283961c1f6aebf2b70 (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.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--src/ansi/mod.rs1
-rw-r--r--src/cli.rs4
-rw-r--r--src/config.rs30
-rw-r--r--src/delta.rs4
-rw-r--r--src/features/line_numbers.rs5
-rw-r--r--src/features/mod.rs5
-rw-r--r--src/features/side_by_side.rs87
-rw-r--r--src/features/side_by_side_wrap.rs880
-rw-r--r--src/main.rs1
-rw-r--r--src/options/set.rs9
-rw-r--r--src/paint.rs66
-rw-r--r--src/style.rs8
14 files changed, 1073 insertions, 35 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b9f3707a..36f0e39d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index d6043ae4..2493cd96 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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))
diff --git a/src/cli.rs b/src/cli.rs
index 2ee73240..643168f9 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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,
+ );
+