summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Otto <th1000s@posteo.net>2020-12-30 21:03:22 +0100
committerThomas Otto <th1000s@posteo.net>2021-10-16 14:07:30 +0200
commitdda6fdb457f347027398b79f5fe4a88cdc02ec0c (patch)
tree196547159946482ddfd44dae39623ca7bd1f6187
parentebd008c364bc43cd71ee31a3570a03f6dd80a493 (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.rs1
-rw-r--r--src/cli.rs27
-rw-r--r--src/config.rs96
-rw-r--r--src/delta.rs4
-rw-r--r--src/features/line_numbers.rs3
-rw-r--r--src/features/side_by_side.rs141
-rw-r--r--src/main.rs2
-rw-r--r--src/options/set.rs5
-rw-r--r--src/paint.rs87
-rw-r--r--src/style.rs8
-rw-r--r--src/wrapping.rs1090
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))
diff --git a/src/cli.rs b/src/cli.rs
index 5d03aa17..e2f75674 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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, line_states, syntax_left_right, diff_left_right) = if should_wrap {
+ // Calculated for syntect::highlighting::style::Style and delta::Style
+ wrap_plusminus_block(
+ self.config,
+ syntax_left_right,
+ diff_left_right,
+ &line_alignment,
+ &line_width,
+ &long_lines,
+ )
+ } else {
+ (
+ line_alignment,
+ states_left_right,