diff options
author | Thomas Otto <th1000s@posteo.net> | 2020-12-30 21:03:22 +0100 |
---|---|---|
committer | Thomas Otto <th1000s@posteo.net> | 2021-10-16 14:07:30 +0200 |
commit | dda6fdb457f347027398b79f5fe4a88cdc02ec0c (patch) | |
tree | 196547159946482ddfd44dae39623ca7bd1f6187 | |
parent | ebd008c364bc43cd71ee31a3570a03f6dd80a493 (diff) |
Add side-by-side line wrapping
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 enough, it is right-aligned.
Wrapping is limited to a certain number of lines
(--wrap-max-lines), if this is exceeded the line is
truncated by a now highlighted truncation symbol.
To disable wrapping set this value to 0.
-rw-r--r-- | src/ansi/mod.rs | 1 | ||||
-rw-r--r-- | src/cli.rs | 27 | ||||
-rw-r--r-- | src/config.rs | 96 | ||||
-rw-r--r-- | src/delta.rs | 4 | ||||
-rw-r--r-- | src/features/line_numbers.rs | 3 | ||||
-rw-r--r-- | src/features/side_by_side.rs | 141 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/options/set.rs | 5 | ||||
-rw-r--r-- | src/paint.rs | 87 | ||||
-rw-r--r-- | src/style.rs | 8 | ||||
-rw-r--r-- | src/wrapping.rs | 1090 |
11 files changed, 1395 insertions, 69 deletions
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs index 442ead85..643b47f0 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)) @@ -498,6 +498,32 @@ pub struct Opt { #[structopt(long = "line-numbers-right-style", default_value = "auto")] pub line_numbers_right_style: String, + /// How often a line should be wrapped if it does not fit. Zero means to never wrap. Any content + /// which does not fit will be truncated. A value of "unlimited" means a line will be wrapped + /// as many times as required. + #[structopt(long = "wrap-max-lines", default_value = "2")] + pub wrap_max_lines: String, + + /// Symbol added to the end of a line indicating that the content has been wrapped + /// onto the next line and continues left-aligned. + #[structopt(long = "wrap-left-symbol", default_value = "↵")] + pub wrap_left_symbol: String, + + /// Symbol added to the end of a line indicating that the content has been wrapped + /// onto the next line and continues right-aligned. + #[structopt(long = "wrap-right-symbol", default_value = "↴")] + pub wrap_right_symbol: String, + + /// Threshold for right-aligning wrapped content. If the length of the remaining wrapped + /// content, as a percentage of width, is less than this quantity it will be right-aligned. + /// Otherwise it will be left-aligned. + #[structopt(long = "wrap-right-percent", default_value = "37.0")] + pub wrap_right_percent: String, + + /// Symbol displayed in front of right-aligned wrapped content. + #[structopt(long = "wrap-right-prefix-symbol", default_value = "…")] + pub wrap_right_prefix_symbol: String, + #[structopt(long = "file-modified-label", default_value = "")] /// Text to display in front of a modified file path. pub file_modified_label: String, @@ -525,6 +551,7 @@ pub struct Opt { #[structopt(long = "max-line-length", default_value = "512")] /// Truncate lines longer than this. To prevent any truncation, set to zero. Note that /// delta will be slow on very long lines (e.g. minified .js) if truncation is disabled. + /// When wrapping lines it is automatically set to fit at least all visible characters. pub max_line_length: usize, /// How to extend the background color to the end of the line in side-by-side mode. Can diff --git a/src/config.rs b/src/config.rs index 5a013a1b..4a93cfe2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,9 @@ use structopt::clap; use syntect::highlighting::Style as SyntectStyle; use syntect::highlighting::Theme as SyntaxTheme; use syntect::parsing::SyntaxSet; +use unicode_segmentation::UnicodeSegmentation; +use crate::ansi; use crate::bat_utils::output::PagingMode; use crate::cli; use crate::color; @@ -19,6 +21,37 @@ use crate::features::side_by_side; use crate::git_config::{GitConfig, GitConfigEntry}; use crate::paint::BgFillMethod; use crate::style::{self, Style}; +use crate::syntect_color; +use crate::wrapping::WrapConfig; + +pub const INLINE_SYMBOL_WIDTH_1: usize = 1; + +fn remove_percent_suffix(arg: &str) -> &str { + match &arg.strip_suffix('%') { + Some(s) => s, + None => arg, + } +} + +fn ensure_display_width_1(what: &str, arg: String) -> String { + match arg.grapheme_indices(true).count() { + INLINE_SYMBOL_WIDTH_1 => arg, + width => fatal(format!( + "Invalid value for {}, display width of \"{}\" must be {} but is {}", + what, arg, INLINE_SYMBOL_WIDTH_1, width + )), + } +} + +fn adapt_wrap_max_lines_argument(arg: String) -> usize { + if arg == "∞" || arg == "unlimited" || arg.starts_with("inf") { + 0 + } else { + arg.parse::<usize>() + .unwrap_or_else(|err| fatal(format!("Invalid wrap-max-lines argument: {}", err))) + + 1 + } +} pub struct Config { pub available_terminal_width: usize, @@ -51,6 +84,7 @@ pub struct Config { pub hyperlinks: bool, pub hyperlinks_commit_link_format: Option<String>, 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_fill_method: BgFillMethod, @@ -96,6 +130,7 @@ pub struct Config { pub true_color: bool, pub truncation_symbol: String, pub whitespace_error_style: Style, + pub wrap_config: WrapConfig, pub zero_style: Style, } @@ -210,6 +245,8 @@ impl From<cli::Opt> for Config { None }; + let wrap_max_lines_plus1 = adapt_wrap_max_lines_argument(opt.wrap_max_lines); + Self { available_terminal_width: opt.computed.available_terminal_width, background_color_extends_to_terminal_width: opt @@ -254,8 +291,20 @@ impl From<cli::Opt> for Config { hyperlinks_commit_link_format: opt.hyperlinks_commit_link_format, hyperlinks_file_link_format: opt.hyperlinks_file_link_format, inspect_raw_lines: opt.computed.inspect_raw_lines, + inline_hint_color: Some(SyntectStyle { + foreground: syntect_color::syntect_color_from_ansi_name("blue").unwrap(), + ..SyntectStyle::default() + }), keep_plus_minus_markers: opt.keep_plus_minus_markers, - line_fill_method, + line_fill_method: if opt.side_by_side { + // Panels in side-by-side always sum up to an even number, if the terminal has + // an odd width then extending the background color with an ANSI sequence + // would indicate the wrong width and extend beyond truncated or wrapped content, + // thus spaces are used here by default. + BgFillMethod::Spaces + } else { + line_fill_method + }, line_numbers: opt.line_numbers, line_numbers_left_format: opt.line_numbers_left_format, line_numbers_left_style, @@ -267,7 +316,23 @@ impl From<cli::Opt> for Config { line_buffer_size: opt.line_buffer_size, max_line_distance: opt.max_line_distance, max_line_distance_for_naively_paired_lines, - max_line_length: opt.max_line_length, + max_line_length: match (opt.side_by_side, wrap_max_lines_plus1) { + (false, _) | (true, 1) => opt.max_line_length, + // Ensure there is enough text to wrap, either don't truncate the input at all (0) + // or ensure there is enough for the requested number of lines. + // The input can contain ANSI sequences, so round up a bit. This is enough for + // normal `git diff`, but might not be with ANSI heavy input. + (true, 0) => 0, + (true, wrap_max_lines) => { + let single_pane_width = opt.computed.available_terminal_width / 2; + let add_25_percent_or_term_width = + |x| x + std::cmp::max((x * 250) / 1000, single_pane_width) as usize; + std::cmp::max( + opt.max_line_length, + add_25_percent_or_term_width(single_pane_width * wrap_max_lines), + ) + } + }, minus_emph_style, minus_empty_line_marker_style, minus_file: opt.minus_file, @@ -296,7 +361,32 @@ 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: format!("{}→{}", ansi::ANSI_SGR_REVERSE, ansi::ANSI_SGR_RESET), + wrap_config: WrapConfig { + left_symbol: ensure_display_width_1("wrap-left-symbol", opt.wrap_left_symbol), + right_symbol: ensure_display_width_1("wrap-right-symbol", opt.wrap_right_symbol), + right_prefix_symbol: ensure_display_width_1( + "wrap-right-prefix-symbol", + opt.wrap_right_prefix_symbol, + ), + use_wrap_right_permille: { + let arg = &opt.wrap_right_percent; + let percent = remove_percent_suffix(arg) + .parse::<f64>() + .unwrap_or_else(|err| { + fatal(format!( + "Could not parse wrap-right-percent argument {}: {}.", + &arg, err + )) + }); + if percent.is_finite() && percent > 0.0 && percent < 100.0 { + (percent * 10.0).round() as usize + } else { + fatal("Invalid value for wrap-right-percent, not between 0 and 100.") + } + }, + max_lines: wrap_max_lines_plus1, + }, whitespace_error_style, zero_style, } diff --git a/src/delta.rs b/src/delta.rs index f1ed2313..92d8691f 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -24,6 +24,10 @@ pub enum State { SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short Blame(String), // In a line of `git blame` output. Unknown, + // The following elements are created when a line is wrapped to display it: + HunkZeroWrapped, // Wrapped unchanged line + HunkMinusWrapped, // Wrapped removed line + HunkPlusWrapped, // Wrapped added line } #[derive(Debug, PartialEq)] diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs index c8570ad2..6d2e6168 100644 --- a/src/features/line_numbers.rs +++ b/src/features/line_numbers.rs @@ -81,15 +81,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(), }; diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs index 9b9a10f8..cc31249d 100644 --- a/src/features/side_by_side.rs +++ b/src/features/side_by_side.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use syntect::highlighting::Style as SyntectStyle; +use unicode_segmentation::UnicodeSegmentation; use crate::ansi; use crate::cli; @@ -11,6 +12,9 @@ use crate::paint::Painter; use crate::paint::{BgFillMethod, BgShouldFill}; use crate::plusminus::*; use crate::style::Style; +use crate::wrapping::wrap_zero_block; + +pub type LineSegments<'a, S> = Vec<(S, &'a str)>; pub fn make_feature() -> Vec<(String, OptionValueFunction)> { builtin_feature!([ @@ -78,17 +82,51 @@ pub fn available_line_width( LeftRight::new(line_width(Left), line_width(Right)) } +pub fn line_is_too_long(line: &str, line_width: usize) -> bool { + let line_sum = line.graphemes(true).count(); + + // `line_sum` is too large, because both a leading "+/-/ " and a trailing + // newline are present, counted, but are never printed. So allow two more + // characters. + 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 avoids +/// 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 mut wrap_any = LeftRight::default(); + let mut wrapping_lines = LeftRight::default(); + + let mut check_if_too_long = |side| { + let lines_side: &Vec<(String, State)> = lines[side]; + wrapping_lines[side] = lines_side + .iter() + .map(|(line, _)| line_is_too_long(line, line_width[side])) + .inspect(|b| wrap_any[side] |= b) + .collect(); + }; + + check_if_too_long(Left); + check_if_too_long(Right); + + (wrap_any[Left] || wrap_any[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>( - syntax_left_right: PlusMinus<Vec<Vec<(SyntectStyle, &str)>>>, - diff_left_right: PlusMinus<Vec<Vec<(Style, &str)>>>, - states_left_right: PlusMinus<Vec<&'a State>>, + syntax_left_right: LeftRight<Vec<LineSegments<'a, SyntectStyle>>>, + diff_left_right: LeftRight<Vec<LineSegments<'a, Style>>>, + states_left_right: LeftRight<Vec<State>>, line_alignment: Vec<(Option<usize>, Option<usize>)>, output_buffer: &mut String, config: &Config, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, - background_color_extends_to_terminal_width: PlusMinus<BgShouldFill>, + background_color_extends_to_terminal_width: LeftRight<BgShouldFill>, ) { for (minus_line_index, plus_line_index) in line_alignment { output_buffer.push_str(&paint_left_panel_minus_line( @@ -96,15 +134,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, )); @@ -113,15 +146,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, )); @@ -130,25 +158,36 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>( } #[allow(clippy::too_many_arguments)] -pub fn paint_zero_lines_side_by_side( - syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>, - diff_style_sections: Vec<Vec<(Style, &str)>>, +pub fn paint_zero_lines_side_by_side<'a>( + raw_line: &str, + syntax_style_sections: Vec<LineSegments<'a, SyntectStyle>>, + diff_style_sections: Vec<LineSegments<'a, Style>>, output_buffer: &mut String, config: &Config, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgShouldFill, ) { - let state = State::HunkZero; + let states = vec![State::HunkZero]; + + let (states, syntax_style_sections, diff_style_sections) = wrap_zero_block( + config, + raw_line, + states, + syntax_style_sections, + diff_style_sections, + line_numbers_data, + ); - 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 &[Left, Right] { let (mut panel_line, panel_line_is_empty) = Painter::paint_line( - syntax_sections, + &syntax_sections, diff_sections, &state, line_numbers_data, @@ -168,7 +207,7 @@ pub fn paint_zero_lines_side_by_side( ); output_buffer.push_str(&panel_line); - if panel_side == &Left { + if *panel_side == 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() { @@ -184,11 +223,10 @@ pub fn paint_zero_lines_side_by_side( #[allow(clippy::too_many_arguments)] fn paint_left_panel_minus_line<'a>( line_index: Option<usize>, - syntax_style_sections: &[Vec<(SyntectStyle, &str)>], - diff_style_sections: &[Vec<(Style, &str)>], + syntax_style_sections: &[LineSegments<'a, SyntectStyle>], + diff_style_sections: &[LineSegments<'a, Style>], state: &'a State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, - painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgShouldFill, config: &Config, ) -> String { @@ -199,7 +237,6 @@ fn paint_left_panel_minus_line<'a>( state, line_numbers_data, Left, - painted_prefix, config, ); pad_panel_line_to_width( @@ -219,11 +256,10 @@ fn paint_left_panel_minus_line<'a>( #[allow(clippy::too_many_arguments)] fn paint_right_panel_plus_line<'a>( line_index: Option<usize>, - syntax_style_sections: &[Vec<(SyntectStyle, &str)>], - diff_style_sections: &[Vec<(Style, &str)>], + syntax_style_sections: &[LineSegments<'a, SyntectStyle>], + diff_style_sections: &[LineSegments<'a, Style>], state: &'a State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, - painted_prefix: Option<ansi_term::ANSIString>, background_color_extends_to_terminal_width: BgShouldFill, config: &Config, ) -> String { @@ -234,7 +270,6 @@ fn paint_right_panel_plus_line<'a>( state, line_numbers_data, Right, - painted_prefix, config, ); @@ -252,10 +287,10 @@ fn paint_right_panel_plus_line<'a>( panel_line } -fn get_right_fill_style_for_panel( +fn get_right_fill_style_for_panel<'a>( line_is_empty: bool, line_index: Option<usize>, - diff_style_sections: &[Vec<(Style, &str)>], + diff_style_sections: &[LineSegments<'a, Style>], state: &State, panel_side: PanelSide, background_color_extends_to_terminal_width: BgShouldFill, @@ -311,14 +346,13 @@ fn get_right_fill_style_for_panel( // and then only emit the right field (which has a None number, i.e. blank). However, it will also // increment the minus line number, so we need to knock that back down. #[allow(clippy::too_many_arguments)] -fn paint_minus_or_plus_panel_line( +fn paint_minus_or_plus_panel_line<'a>( line_index: Option<usize>, - syntax_style_sections: &[Vec<(SyntectStyle, &str)>], - diff_style_sections: &[Vec<(Style, &str)>], + syntax_style_sections: &[LineSegments<'a, SyntectStyle>], + diff_style_sections: &[LineSegments<'a, Style>], 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()); @@ -343,6 +377,14 @@ fn paint_minus_or_plus_panel_line( ) }; + let painted_prefix = match (config.keep_plus_minus_markers, panel_side, state) { + (true, _, State::HunkPlusWrapped) => Some(config.plus_style.paint(" ")), + (true, _, State::HunkMinusWrapped) => Some(config.minus_style.paint(" ")), + (true, Left, _) => Some(config.minus_style.paint("-")), + (true, Right, _) => Some(config.plus_style.paint("+")), + _ => None, + }; + let (line, line_is_empty) = Painter::paint_line( line_syntax_sections, line_diff_sections, @@ -375,11 +417,11 @@ fn paint_minus_or_plus_panel_line( /// done with spaces. The right panel can be filled with spaces or using ANSI sequences /// instructing the terminal emulator to fill the background color rightwards. #[allow(clippy::too_many_arguments, clippy::comparison_chain)] -fn pad_panel_line_to_width( +fn pad_panel_line_to_width<'a>( panel_line: &mut String, panel_line_is_empty: bool, line_index: Option<usize>, - diff_style_sections: &[Vec<(Style, &str)>], + diff_style_sections: &[LineSegments<'a, Style>], state: &State, panel_side: PanelSide, background_color_extends_to_terminal_width: BgShouldFill, @@ -457,6 +499,8 @@ pub mod tests { fn test_two_minus_lines_truncated() { let mut config = make_config_from_args(&[ "--side-by-side", + "--wrap-max-lines", + "0", "--width", "28", "--line-fill-method=spaces", @@ -476,19 +520,22 @@ pub mod tests { let mut lines = output.lines().skip(7); let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap()); let sac = strip_ansi_codes; // alias to help with `cargo fmt`-ing: - assert_eq!("│ │ │ 1 │a = 1", sac(line_1)); - assert_eq!("│ │ │ 2 │b = 234567", sac(line_2)); + assert_eq!("│ │ │ 1 │a = 1 ", sac(line_1)); + assert_eq!("│ │ │ 2 │b = 234567 ", sac(line_2)); } #[test] fn test_two_plus_lines_truncated() { let mut config = make_config_from_args(&[ "--side-by-side", + "--wrap-max-lines", + "0", "--width", "30", "--line-fill-method=spaces", ]); config.truncation_symbol = ">".into(); + 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()); @@ -498,12 +545,11 @@ pub mod tests { #[test] fn test_two_plus_lines_exact_fit() { - let mut config = make_config_from_args(&["--side-by-side", "--width", "32"]); - config.truncation_symbol = ">".into(); + let config = make_config_from_args(&["--side-by-side", "--width", "32"]); 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 = 234567", strip_ansi_codes(line_2)); } @@ -513,7 +559,8 @@ pub mod tests { let output = run_delta(ONE_MINUS_ONE_PLUS_LINE_DIFF, &config); let output = strip_ansi_codes(&output); let mut lines = output.lines().skip(7); - assert_eq!("│ 1 │a = 1 │ 1 │a = 1", lines.next().unwrap()); - assert_eq!("│ 2 │b = 2 │ 2 │bb = 2", lines.next().unwrap()); + let mut lnu = move || lines.next().unwrap(); // for cargo fmt + assert_eq!("│ 1 │a = 1 │ 1 │a = 1", lnu()); + assert_eq!("│ 2 │b = 2 │ 2 │bb = 2 ", lnu()); } } diff --git a/src/main.rs b/src/main.rs index a3587341..8b382a34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,8 @@ mod paint; mod parse_style; mod plusminus; mod style; +mod wrapping; + mod subcommands; mod syntect_color; mod tests; diff --git a/src/options/set.rs b/src/options/set.rs index 1f533269..1d6f13ed 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -179,6 +179,11 @@ pub fn set_options( relative_paths, show_themes, side_by_side, + wrap_max_lines, + wrap_right_prefix_symbol, + wrap_right_percent, + wrap_right_symbol, + wrap_left_symbol, tab_width, tokenization_regex, true_color, diff --git a/src/paint.rs b/src/paint.rs index 3015187a..929b9643 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -11,11 +11,11 @@ use crate::config::{self, delta_unreachable}; use crate::delta::State; use crate::edits; use crate::features::line_numbers; -use crate::features::side_by_side; -use crate::features::side_by_side::PanelSide; +use crate::features::side_by_side::{self, available_line_width, LineSegments, PanelSide}; use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::plusminus::*; use crate::style::Style; +use crate::wrapping::wrap_plusminus_block; pub struct Painter<'a> { pub minus_lines: Vec<(String, State)>, @@ -110,6 +110,7 @@ impl<'a> Painter<'a> { // in effect in which case we replace it with the appropriate marker). // TODO: Things should, but do not, work if this leading space is omitted at this stage. // See comment in align::Alignment::new. + // Note that a wrapped line also has a leading character added to remain compatible. line.next(); format!(" {}\n", self.expand_tabs(line)) } else { @@ -172,21 +173,65 @@ impl<'a> Painter<'a> { ); let states_left_right = PlusMinus::new( - self.minus_lines.iter().map(|(_, state)| state).collect(), - self.plus_lines.iter().map(|(_, state)| state).collect(), + self.minus_lines + .iter() + .map(|(_, state)| state.clone()) + .collect(), + self.plus_lines + .iter() + .map(|(_, state)| state.clone()) + .collect(), ); let bg_fill_left_right = PlusMinus::new( - // Using an ANSI sequence to fill the left panel would not work + // Using an ANSI sequence to fill the left panel would not work. BgShouldFill::With(BgFillMethod::Spaces), - // Use the configured method for the right panel + // Use what is configured for the right side. BgShouldFill::With(self.config.line_fill_method), ); + // Only set `should_wrap` to true if wrapping is wanted and lines which are + // too long are found. + // If so, remember the calculated line width and which of the lines are too + // long for later re-use. + let (should_wrap, line_width, long_lines) = { + if self.config.wrap_config.max_lines == 1 { + (false, PlusMinus::default(), PlusMinus::default()) + } else { + let line_width = available_line_width(self.config, &self.line_numbers_data); + + let lines = PlusMinus::new(&self.minus_lines, &self.plus_lines); + + let (should_wrap, long_lines) = + side_by_side::has_long_lines(&lines, &line_width); + + (should_wrap, line_width, long_lines) + } + }; + + let (line_alignment, |