From b13537a912a48537b28c298bb10c3214f31730e2 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 8 Jul 2020 14:18:12 -0400 Subject: [PATCH] Add side-by-side diff view Closes #86 --- src/cli.rs | 5 + src/config.rs | 12 + src/edits.rs | 23 +- src/features/line_numbers.rs | 49 ++-- src/features/mod.rs | 5 + src/features/side_by_side.rs | 428 +++++++++++++++++++++++++++++++++++ src/options/set.rs | 13 +- src/paint.rs | 121 ++++++---- 8 files changed, 588 insertions(+), 68 deletions(-) create mode 100644 src/features/side_by_side.rs diff --git a/src/cli.rs b/src/cli.rs index fbbcd6f50..b81fdebaf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -212,6 +212,10 @@ pub struct Opt { #[structopt(short = "n", long = "line-numbers")] pub line_numbers: bool, + /// Display a side-by-side diff view instead of the traditional view. + #[structopt(long = "side-by-side")] + pub side_by_side: bool, + #[structopt(long = "diff-highlight")] /// Emulate diff-highlight (https://github.com/git/git/tree/master/contrib/diff-highlight) pub diff_highlight: bool, @@ -516,6 +520,7 @@ pub struct ComputedValues { pub syntax_theme: Option, pub syntax_dummy_theme: SyntaxTheme, pub true_color: bool, + pub available_terminal_width: usize, pub decorations_width: Width, pub background_color_extends_to_terminal_width: bool, pub paging_mode: PagingMode, diff --git a/src/config.rs b/src/config.rs index cfa03be89..30ddd6bb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,9 +12,11 @@ use crate::cli; use crate::color; use crate::delta::State; use crate::env; +use crate::features::side_by_side; use crate::style::Style; pub struct Config { + pub available_terminal_width: usize, pub background_color_extends_to_terminal_width: bool, pub commit_style: Style, pub decorations_width: cli::Width, @@ -50,11 +52,14 @@ pub struct Config { pub plus_non_emph_style: Style, pub plus_style: Style, pub line_numbers: bool, + pub side_by_side: bool, + pub side_by_side_data: side_by_side::SideBySideData, pub syntax_dummy_theme: SyntaxTheme, pub syntax_set: SyntaxSet, pub syntax_theme: Option, pub tab_width: usize, pub true_color: bool, + pub truncation_symbol: String, pub tokenization_regex: Regex, pub whitespace_error_style: Style, pub zero_style: Style, @@ -112,7 +117,11 @@ impl From for Config { process::exit(1); }); + let side_by_side_data = + side_by_side::SideBySideData::new(opt.computed.available_terminal_width); + Self { + available_terminal_width: opt.computed.available_terminal_width, background_color_extends_to_terminal_width: opt .computed .background_color_extends_to_terminal_width, @@ -150,12 +159,15 @@ impl From for Config { plus_non_emph_style, plus_style, line_numbers: opt.line_numbers, + side_by_side: opt.side_by_side, + side_by_side_data, syntax_dummy_theme: SyntaxTheme::default(), syntax_set: opt.computed.syntax_set, syntax_theme: opt.computed.syntax_theme, tab_width: opt.tab_width, tokenization_regex, true_color: opt.computed.true_color, + truncation_symbol: "→".to_string(), whitespace_error_style, zero_style, } diff --git a/src/edits.rs b/src/edits.rs index b05505e2c..510b6f765 100644 --- a/src/edits.rs +++ b/src/edits.rs @@ -8,7 +8,8 @@ use crate::align; /// Infer the edit operations responsible for the differences between a collection of old and new /// lines. A "line" is a string. An annotated line is a Vec of (op, &str) pairs, where the &str /// slices are slices of the line, and their concatenation equals the line. Return the input minus -/// and plus lines, in annotated form. +/// and plus lines, in annotated form. Also return a specification of the inferred alignment of +/// minus and plus lines. pub fn infer_edits<'a, EditOperation>( minus_lines: &'a [String], plus_lines: &'a [String], @@ -20,8 +21,9 @@ pub fn infer_edits<'a, EditOperation>( max_line_distance: f64, max_line_distance_for_naively_paired_lines: f64, ) -> ( - Vec>, // annotated minus lines - Vec>, // annotated plus lines + Vec>, // annotated minus lines + Vec>, // annotated plus lines + Vec<(Option, Option)>, // line alignment ) where EditOperation: Copy, @@ -29,10 +31,11 @@ where { let mut annotated_minus_lines = Vec::>::new(); let mut annotated_plus_lines = Vec::>::new(); + let mut line_alignment = Vec::<(Option, Option)>::new(); let mut plus_index = 0; // plus lines emitted so far - 'minus_lines_loop: for minus_line in minus_lines { + 'minus_lines_loop: for (minus_index, minus_line) in minus_lines.iter().enumerate() { let mut considered = 0; // plus lines considered so far as match for minus_line for plus_line in &plus_lines[plus_index..] { let alignment = align::Alignment::new( @@ -57,10 +60,12 @@ where // Emit as unpaired the plus lines already considered and rejected for plus_line in &plus_lines[plus_index..(plus_index + considered)] { annotated_plus_lines.push(vec![(noop_insertion, plus_line)]); + line_alignment.push((None, Some(plus_index))); + plus_index += 1; } - plus_index += considered; annotated_minus_lines.push(annotated_minus_line); annotated_plus_lines.push(annotated_plus_line); + line_alignment.push((Some(minus_index), Some(plus_index))); plus_index += 1; // Greedy: move on to the next minus line. @@ -71,13 +76,16 @@ where } // No homolog was found for minus i; emit as unpaired. annotated_minus_lines.push(vec![(noop_deletion, minus_line)]); + line_alignment.push((Some(minus_index), None)); } // Emit any remaining plus lines for plus_line in &plus_lines[plus_index..] { annotated_plus_lines.push(vec![(noop_insertion, plus_line)]); + line_alignment.push((None, Some(plus_index))); + plus_index += 1; } - (annotated_minus_lines, annotated_plus_lines) + (annotated_minus_lines, annotated_plus_lines, line_alignment) } /// Split line into tokens for alignment. The alignment algorithm aligns sequences of substrings; @@ -736,7 +744,8 @@ mod tests { 0.0, ); // compare_annotated_lines(actual_edits, expected_edits); - assert_eq!(actual_edits, expected_edits); + // TODO: test line alignment + assert_eq!((actual_edits.0, actual_edits.1), expected_edits); } // Assert that no edits are inferred for the supplied minus and plus lines. diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs index 332db853f..9c76b9851 100644 --- a/src/features/line_numbers.rs +++ b/src/features/line_numbers.rs @@ -6,6 +6,7 @@ use regex::Regex; use crate::config; use crate::delta::State; +use crate::features::side_by_side; use crate::features::OptionValueFunction; use crate::style::Style; @@ -63,6 +64,7 @@ pub fn make_feature() -> Vec<(String, OptionValueFunction)> { pub fn format_and_paint_line_numbers<'a>( line_numbers_data: &'a mut LineNumbersData, state: &State, + side_by_side_panel: Option, config: &'a config::Config, ) -> Vec> { let m_ref = &mut line_numbers_data.hunk_minus_line_number; @@ -94,25 +96,36 @@ pub fn format_and_paint_line_numbers<'a>( let mut formatted_numbers = Vec::new(); - formatted_numbers.extend(format_and_paint_line_number_field( - &line_numbers_data.left_format_data, - &config.line_numbers_left_style, - minus_number, - plus_number, - line_numbers_data.hunk_max_line_number_width, - &minus_style, - &plus_style, - )); - formatted_numbers.extend(format_and_paint_line_number_field( - &line_numbers_data.right_format_data, - &config.line_numbers_right_style, - minus_number, - plus_number, - line_numbers_data.hunk_max_line_number_width, - &minus_style, - &plus_style, - )); + let (emit_left, emit_right) = match (config.side_by_side, side_by_side_panel) { + (false, _) => (true, true), + (true, Some(side_by_side::PanelSide::Left)) => (true, false), + (true, Some(side_by_side::PanelSide::Right)) => (false, true), + (true, None) => unreachable!(), + }; + + if emit_left { + formatted_numbers.extend(format_and_paint_line_number_field( + &line_numbers_data.left_format_data, + &config.line_numbers_left_style, + minus_number, + plus_number, + line_numbers_data.hunk_max_line_number_width, + &minus_style, + &plus_style, + )); + } + if emit_right { + formatted_numbers.extend(format_and_paint_line_number_field( + &line_numbers_data.right_format_data, + &config.line_numbers_right_style, + minus_number, + plus_number, + line_numbers_data.hunk_max_line_number_width, + &minus_style, + &plus_style, + )); + } formatted_numbers } diff --git a/src/features/mod.rs b/src/features/mod.rs index 4f9e76574..30b3a7a57 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -47,6 +47,10 @@ pub fn make_builtin_features() -> HashMap { navigate::make_feature().into_iter().collect(), ), ("raw".to_string(), raw::make_feature().into_iter().collect()), + ( + "side-by-side".to_string(), + side_by_side::make_feature().into_iter().collect(), + ), ] .into_iter() .collect() @@ -80,6 +84,7 @@ pub mod diff_so_fancy; pub mod line_numbers; pub mod navigate; pub mod raw; +pub mod side_by_side; #[cfg(test)] pub mod tests { diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs new file mode 100644 index 000000000..84d7f0730 --- /dev/null +++ b/src/features/side_by_side.rs @@ -0,0 +1,428 @@ +use console; + +use itertools::Itertools; +use syntect::highlighting::Style as SyntectStyle; + +use crate::config::Config; +use crate::delta::State; +use crate::features::line_numbers; +use crate::features::OptionValueFunction; +use crate::paint::Painter; +use crate::style::Style; + +pub fn make_feature() -> Vec<(String, OptionValueFunction)> { + builtin_feature!([ + ( + "side-by-side", + bool, + None, + _opt => true + ), + ("features", bool, None, _opt => "line-numbers"), + ("line-numbers-left-format", String, None, _opt => "│{nm:^4}│".to_string()), + ("line-numbers-right-format", String, None, _opt => "│{np:^4}│".to_string()), + ("minus-style", String, None, _opt => "syntax auto"), + ("minus-emph-style", String, None, _opt => "syntax auto"), + ("minus-non-emph-style", String, None, _opt => "syntax auto") + ]) +} + +pub enum PanelSide { + Left, + Right, +} + +pub struct SideBySideData { + pub left_panel: Panel, + pub right_panel: Panel, +} + +pub struct Panel { + pub width: usize, + pub offset: usize, +} + +impl SideBySideData { + pub fn new(available_terminal_width: usize) -> Self { + let panel_width = available_terminal_width / 2; + Self { + left_panel: Panel { + width: panel_width, + offset: 0, + }, + right_panel: Panel { + width: panel_width, + offset: 0, + }, + } + } +} + +/// Emit a sequence of minus and plus lines in side-by-side mode. +pub fn paint_minus_and_plus_lines_side_by_side( + minus_syntax_style_sections: Vec>, + minus_diff_style_sections: Vec>, + plus_syntax_style_sections: Vec>, + plus_diff_style_sections: Vec>, + line_alignment: Vec<(Option, Option)>, + output_buffer: &mut String, + config: &Config, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + prefix: &str, + background_color_extends_to_terminal_width: Option, +) { + for (minus_line_index, plus_line_index) in line_alignment { + output_buffer.push_str(&paint_left_panel_minus_line( + minus_line_index, + &minus_syntax_style_sections, + &minus_diff_style_sections, + line_numbers_data, + prefix, + background_color_extends_to_terminal_width, + config, + )); + output_buffer.push_str(&paint_right_panel_plus_line( + plus_line_index, + &plus_syntax_style_sections, + &plus_diff_style_sections, + line_numbers_data, + prefix, + background_color_extends_to_terminal_width, + config, + )); + output_buffer.push_str("\n"); + } +} + +pub fn paint_zero_lines_side_by_side( + syntax_style_sections: Vec>, + diff_style_sections: Vec>, + state: &State, + output_buffer: &mut String, + config: &Config, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + prefix: &str, + background_color_extends_to_terminal_width: Option, +) { + for (line_index, (syntax_sections, diff_sections)) in syntax_style_sections + .iter() + .zip_eq(diff_style_sections.iter()) + .enumerate() + { + let (mut left_panel_line, left_panel_line_is_empty) = Painter::paint_line( + syntax_sections, + diff_sections, + state, + line_numbers_data, + Some(PanelSide::Left), + prefix, + config, + ); + // TODO: Avoid doing the superimpose_style_sections work twice. + // HACK: These are getting incremented twice, so knock them back down once. + line_numbers_data.as_mut().map(|d| { + d.hunk_minus_line_number -= 1; + d.hunk_plus_line_number -= 1 + }); + right_pad_left_panel_line( + &mut left_panel_line, + left_panel_line_is_empty, + Some(line_index), + &diff_style_sections, + &State::HunkZero, + background_color_extends_to_terminal_width, + config, + ); + output_buffer.push_str(&left_panel_line); + + let (mut right_panel_line, right_panel_line_is_empty) = Painter::paint_line( + syntax_sections, + diff_sections, + state, + line_numbers_data, + Some(PanelSide::Right), + prefix, + config, + ); + right_fill_right_panel_line( + &mut right_panel_line, + right_panel_line_is_empty, + Some(line_index), + &diff_style_sections, + &State::HunkZero, + background_color_extends_to_terminal_width, + config, + ); + output_buffer.push_str(&right_panel_line); + output_buffer.push_str("\n"); + } +} + +fn paint_left_panel_minus_line( + line_index: Option, + syntax_style_sections: &Vec>, + diff_style_sections: &Vec>, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + prefix: &str, + background_color_extends_to_terminal_width: Option, + config: &Config, +) -> String { + let (mut panel_line, panel_line_is_empty) = paint_minus_or_plus_panel_line( + line_index, + &syntax_style_sections, + &diff_style_sections, + &State::HunkMinus, + line_numbers_data, + PanelSide::Left, + prefix, + config, + ); + right_pad_left_panel_line( + &mut panel_line, + panel_line_is_empty, + line_index, + diff_style_sections, + &State::HunkMinus, + background_color_extends_to_terminal_width, + config, + ); + + panel_line +} + +fn paint_right_panel_plus_line( + line_index: Option, + syntax_style_sections: &Vec>, + diff_style_sections: &Vec>, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + prefix: &str, + background_color_extends_to_terminal_width: Option, + config: &Config, +) -> String { + let (mut panel_line, panel_line_is_empty) = paint_minus_or_plus_panel_line( + line_index, + &syntax_style_sections, + &diff_style_sections, + &State::HunkPlus, + line_numbers_data, + PanelSide::Right, + prefix, + config, + ); + right_fill_right_panel_line( + &mut panel_line, + panel_line_is_empty, + line_index, + diff_style_sections, + &State::HunkPlus, + background_color_extends_to_terminal_width, + config, + ); + panel_line +} + +fn get_right_fill_style_for_left_panel( + line_is_empty: bool, + line_index: Option, + diff_style_sections: &Vec>, + state: &State, + background_color_extends_to_terminal_width: Option, + config: &Config, +) -> Style { + match (line_is_empty, line_index) { + (true, _) => config.null_style, + (false, None) => config.null_style, + (false, Some(index)) => { + let (should_fill, fill_style) = + Painter::get_should_right_fill_background_color_and_fill_style( + &diff_style_sections[index], + state, + background_color_extends_to_terminal_width, + config, + ); + if should_fill { + fill_style + } else { + config.null_style + } + } + } +} + +/// Construct half of a minus or plus line under side-by-side mode, i.e. the half line that +/// goes in one or other panel. Return a tuple `(painted_half_line, is_empty)`. +// Suppose the line being displayed is a minus line with a paired plus line. Then both times +// this function is called, `line_index` will be `Some`. This case proceeds as one would +// expect: on the first call, we are constructing the left panel line, and we are passed +// `(Some(index), HunkMinus, Left)`. We pass `(HunkMinus, Left)` to +// `paint_line`. This has two consequences: +// 1. `format_and_paint_line_numbers` will increment the minus line number. +// 2. `format_and_paint_line_numbers` will emit the left line number field, and not the right. +// +// The second call does the analogous thing for the plus line to be displayed in the right panel: +// we are passed `(Some(index), HunkPlus, Right)` and we pass `(HunkPlus, Right)` to `paint_line`, +// causing it to increment the plus line number and emit the right line number field. +// +// Now consider the case where the line being displayed is a minus line with no paired plus line. +// The first call is as before. On the second call, we are passed `(None, HunkPlus, Right)` and we +// wish to display the right panel, with its line number container, but without any line number +// (and without any line contents). We do this by passing (HunkMinus, Right) to `paint_line`, since +// what this will do is set the line number pair in that function to `(Some(minus_number), None)`, +// 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. +fn paint_minus_or_plus_panel_line( + line_index: Option, + syntax_style_sections: &Vec>, + diff_style_sections: &Vec>, + state: &State, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + panel_side: PanelSide, + prefix: &str, + config: &Config, +) -> (String, bool) { + let (empty_line_syntax_sections, empty_line_diff_sections) = (Vec::new(), Vec::new()); + + let (line_syntax_sections, line_diff_sections, state_for_line_numbers_field) = + if let Some(index) = line_index { + ( + &syntax_style_sections[index], + &diff_style_sections[index], + state, + ) + } else { + let opposite_state = match *state { + State::HunkMinus => &State::HunkPlus, + State::HunkPlus => &State::HunkMinus, + _ => unreachable!(), + }; + ( + &empty_line_syntax_sections, + &empty_line_diff_sections, + opposite_state, + ) + }; + + let (line, line_is_empty) = Painter::paint_line( + line_syntax_sections, + line_diff_sections, + &state_for_line_numbers_field, + line_numbers_data, + Some(panel_side), + prefix, + config, + ); + + // Knock back down spuriously incremented line numbers. See comment above. + match (state, state_for_line_numbers_field) { + (s, t) if s == t => {} + (State::HunkPlus, State::HunkMinus) => { + line_numbers_data + .as_mut() + .map(|d| d.hunk_minus_line_number -= 1); + } + (State::HunkMinus, State::HunkPlus) => { + line_numbers_data + .as_mut() + .map(|d| d.hunk_plus_line_number -= 1); + } + _ => unreachable!(), + } + (line, line_is_empty) +} + +fn right_pad_left_panel_line( + panel_line: &mut String, + panel_line_is_empty: bool, + line_index: Option, + diff_style_sections: &Vec>, + state: &State, + background_color_extends_to_terminal_width: Option, + config: &Config, +) { + // The left panel uses spaces to pad to the midpoint. This differs from the right panel, + // and from the non-side-by-side implementation. + + // Emit empty line marker if the panel line is empty but not empty-by-construction. IOW if the + // other panel contains a real line, and we are currently emitting an empty counterpart panel + // to form the other half of the line, then don't emit the empty line marker. + if panel_line_is_empty && line_index.is_some() { + match state { + State::HunkMinus => Painter::mark_empty_line( + &config.minus_empty_line_marker_style, + panel_line, + Some(" "), + ), + State::HunkZero => {} + _ => unreachable!(), + }; + }; + // Pad with (maybe painted) spaces to the panel width. + let text_width = console::measure_text_width(&panel_line); + let panel_width = config.side_by_side_data.left_panel.width; + if text_width < panel_width { + let fill_style = get_right_fill_style_for_left_panel( + panel_line_is_empty, + line_index, + &diff_style_sections, + state, + background_color_extends_to_terminal_width, + config, + ); + panel_line.push_str( + &fill_style + .paint(" ".repeat(panel_width - text_width)) + .to_string(), + ); + } else if text_width > panel_width { + *panel_line = + console::truncate_str(panel_line, panel_width, &config.truncation_symbol).to_string(); + }; +} + +/// Right-fill a line in the right panel. A line in the right panel is either a zero line or a plus +/// line. +fn right_fill_right_panel_line( + panel_line: &mut String, + panel_line_is_empty: bool, + line_index: Option, + diff_style_sections: &Vec>, + state: &State, + background_color_extends_to_terminal_width: Option, + config: &Config, +) { + *panel_line = console::truncate_str( + &panel_line, + config.side_by_side_data.right_panel.width, + &config.truncation_symbol, + ) + .to_string(); + + // Unlike the left panel, the line-end emissions here are basically the same as the non + // side-by-side implementation in Painter::paint_lines. + let (should_right_fill_background_color, fill_style) = if let Some(index) = line_index { + Painter::get_should_right_fill_background_color_and_fill_style( + &diff_style_sections[index], + state, + background_color_extends_to_terminal_width, + config, + ) + } else { + (false, config.null_style) + }; + + // Emit empty line marker if the panel line is empty but not empty-by-construction. See + // parallel comment in `paint_left_panel_minus_line`. + if panel_line_is_empty && line_index.is_some() { + match state { + State::HunkPlus => Painter::mark_empty_line( + &config.plus_empty_line_marker_style, + panel_line, + Some(" "), + ), + State::HunkZero => {} + _ => unreachable!(), + } + } else if should_right_fill_background_color { + Painter::right_fill_background_color(panel_line, fill_style); + }; +} diff --git a/src/options/set.rs b/src/options/set.rs index a8f4b888e..5175c3964 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -137,6 +137,7 @@ pub fn set_options( ("plus-empty-line-marker-style", plus_empty_line_marker_style), ("plus-non-emph-style", plus_non_emph_style), ("raw", raw), + ("side-by-side", side_by_side), ("tabs", tab_width), ("whitespace-error-style", whitespace_error_style), ("width", width), @@ -273,6 +274,9 @@ fn gather_features<'a>( if opt.navigate { features.push_front("navigate".to_string()); } + if opt.side_by_side { + features.push_front("side-by-side".to_string()); + } if let Some(git_config) = git_config { // Gather features from [delta] section if --features was not passed. @@ -379,7 +383,7 @@ fn set_paging_mode(opt: &mut cli::Opt) { fn set_widths(opt: &mut cli::Opt) { // Allow one character in case e.g. `less --status-column` is in effect. See #41 and #10. - let available_terminal_width = (Term::stdout().size().1 - 1) as usize; + opt.computed.available_terminal_width = (Term::stdout().size().1 - 1) as usize; let (decorations_width, background_color_extends_to_terminal_width) = match opt.width.as_deref() { Some("variable") => (cli::Width::Variable, false), @@ -389,11 +393,14 @@ fn set_widths(opt: &mut cli::Opt) { process::exit(1); }); ( - cli::Width::Fixed(min(width, available_terminal_width)), + cli::Width::Fixed(min(width, opt.computed.available_terminal_width)), true, ) } - None => (cli::Width::Fixed(available_terminal_width), true), + None => ( + cli::Width::Fixed(opt.computed.available_terminal_width), + true, + ), }; opt.computed.decorations_width = decorations_width; opt.computed.background_color_extends_to_terminal_width = diff --git a/src/paint.rs b/src/paint.rs index ccc5572b4..1e867ade1 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -3,6 +3,7 @@ use regex::Regex; use std::io::Write; use ansi_term; +use itertools::Itertools; use syntect::easy::HighlightLines; use syntect::highlighting::Style as SyntectStyle; use syntect::parsing::{SyntaxReference, SyntaxSet}; @@ -12,6 +13,7 @@ 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::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; @@ -130,43 +132,61 @@ impl<'a> Painter<'a> { &mut self.highlighter, self.config, ); - let (minus_line_diff_style_sections, plus_line_diff_style_sections) = + let (minus_line_diff_style_sections, plus_line_diff_style_sections, line_alignment) = Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config); - // TODO: lines and style sections contain identical line text - if !self.minus_lines.is_empty() { - Painter::paint_lines( + if self.config.side_by_side { + side_by_side::paint_minus_and_plus_lines_side_by_side( minus_line_syntax_style_sections, minus_line_diff_style_sections, - &State::HunkMinus, - &mut self.output_buffer, - self.config, - &mut Some(&mut self.line_numbers_data), - if self.config.keep_plus_minus_markers { - "-" - } else { - "" - }, - Some(self.config.minus_empty_line_marker_style), - None, - ); - } - if !self.plus_lines.is_empty() { - Painter::paint_lines( plus_line_syntax_style_sections, plus_line_diff_style_sections, - &State::HunkPlus, + line_alignment, &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), if self.config.keep_plus_minus_markers { - "+" + "-" } else { "" }, - Some(self.config.plus_empty_line_marker_style), None, ); + } else { + if !self.minus_lines.is_empty() { + Painter::paint_lines( + minus_line_syntax_style_sections, + minus_line_diff_style_sections, + &State::HunkMinus, + &mut self.output_buffer, + self.config, + &mut Some(&mut self.line_numbers_data), + if self.config.keep_plus_minus_markers { + "-" + } else { + "" + }, + Some(self.config.minus_empty_line_marker_style), + None, + ); + } + if !self.plus_lines.is_empty() { + Painter::paint_lines( + plus_line_syntax_style_sections, + plus_line_diff_style_sections, + &State::HunkPlus, + &mut self.output_buffer, + self.config, + &mut Some(&mut self.line_numbers_data), + if self.config.keep_plus_minus_markers { + "+" + } else { + "" + }, + Some(self.config.plus_empty_line_marker_style), + None, + ); + } } self.minus_lines.clear(); self.plus_lines.clear(); @@ -187,17 +207,30 @@ impl<'a> Painter<'a> { ); let diff_style_sections = vec![(self.config.zero_style, lines[0].as_str())]; - Painter::paint_lines( - syntax_style_sections, - vec![diff_style_sections], - &State::HunkZero, - &mut self.output_buffer, - self.config, - &mut Some(&mut self.line_numbers_data), - prefix, - None, - None, - ); + if self.config.side_by_side { + side_by_side::paint_zero_lines_side_by_side( + syntax_style_sections, + vec![diff_style_sections], + &State::HunkZero, + &mut self.output_buffer, + self.config, + &mut Some(&mut self.line_numbers_data), + prefix, + None, + ); + } else { + Painter::paint_lines( + syntax_style_sections, + vec![diff_style_sections], + &State::HunkZero, + &mut self.output_buffer, + self.config, + &mut Some(&mut self.line_numbers_data), + prefix, + None, + None, + ); + } } /// Superimpose background styles and foreground syntax @@ -221,14 +254,16 @@ impl<'a> Painter<'a> { // 2. We must ensure that we fill rightwards with the appropriate // non-emph background color. In that case we don't use the last // style of the line, because this might be emph. - for (syntax_sections, diff_sections) in - syntax_style_sections.iter().zip(diff_style_sections.iter()) + for (syntax_sections, diff_sections) in syntax_style_sections + .iter() + .zip_eq(diff_style_sections.iter()) { let (mut line, line_is_empty) = Painter::paint_line( syntax_sections, diff_sections, state, line_numbers_data, + None, prefix, config, ); @@ -257,7 +292,7 @@ impl<'a> Painter<'a> { /// Determine whether the terminal should fill the line rightwards with a background color, and /// the style for doing so. - fn get_should_right_fill_background_color_and_fill_style( + pub fn get_should_right_fill_background_color_and_fill_style( diff_sections: &Vec<(Style, &str)>, state: &State, background_color_extends_to_terminal_width: Option, @@ -283,7 +318,7 @@ impl<'a> Painter<'a> { } /// Emit line with ANSI sequences that extend the background color to the terminal width. - fn right_fill_background_color(line: &mut String, fill_style: Style) { + pub fn right_fill_background_color(line: &mut String, fill_style: Style) { // HACK: How to properly incorporate the ANSI_CSI_CLEAR_TO_EOL into ansi_strings? line.push_str(&ansi_term::ANSIStrings(&[fill_style.paint("")]).to_string()); if line @@ -301,7 +336,7 @@ impl<'a> Painter<'a> { /// to the line. This is typically appropriate only when the `line` buffer is empty, since /// otherwise the ANSI_CSI_CLEAR_TO_BOL instruction would overwrite the text to the left of the /// current buffer position. - fn mark_empty_line(empty_line_style: &Style, line: &mut String, marker: Option<&str>) { + pub fn mark_empty_line(empty_line_style: &Style, line: &mut String, marker: Option<&str>) { line.push_str( &empty_line_style .paint(marker.unwrap_or(ANSI_CSI_CLEAR_TO_BOL)) @@ -310,11 +345,12 @@ impl<'a> Painter<'a> { } /// Return painted line (maybe prefixed with line numbers field) and an is_empty? boolean. - fn paint_line( + pub fn paint_line( syntax_sections: &Vec<(SyntectStyle, &str)>, diff_sections: &Vec<(Style, &str)>, state: &State, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + side_by_side_panel: Option, prefix: &str, config: &config::Config, ) -> (String, bool) { @@ -325,6 +361,7 @@ impl<'a> Painter<'a> { ansi_strings.extend(line_numbers::format_and_paint_line_numbers( line_numbers_data.as_mut().unwrap(), state, + side_by_side_panel, config, )) } @@ -404,7 +441,11 @@ impl<'a> Painter<'a> { minus_lines: &'b Vec, plus_lines: &'b Vec, config: &config::Config, - ) -> (Vec>, Vec>) { + ) -> ( + Vec>, + Vec>, + Vec<(Option, Option)>, + ) { let mut diff_sections = edits::infer_edits( minus_lines, plus_lines,