diff --git a/src/cli.rs b/src/cli.rs index d4ad237b6..bc0592062 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, @@ -514,6 +518,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..fd3349332 --- /dev/null +++ b/src/features/side_by_side.rs @@ -0,0 +1,45 @@ +use crate::features::OptionValueFunction; + +pub fn make_feature() -> Vec<(String, OptionValueFunction)> { + builtin_feature!([ + ( + "side-by-side", + bool, + None, + _opt => true + ), + ("line-numbers-left-format", String, None, _opt => "{np:>4}│".to_string()), + ("line-numbers-right-format", String, None, _opt => "│{np:>4}│".to_string()) + ]) +} + +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, + }, + } + } +} 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 6677c4a23..93ab09956 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -3,6 +3,8 @@ use regex::Regex; use std::io::Write; use ansi_term; +use console; +use itertools::Itertools; use syntect::easy::HighlightLines; use syntect::highlighting::Style as SyntectStyle; use syntect::parsing::{SyntaxReference, SyntaxSet}; @@ -12,6 +14,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,15 +133,16 @@ 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 { + Painter::paint_buffered_minus_and_plus_lines_side_by_side( minus_line_syntax_style_sections, minus_line_diff_style_sections, - &State::HunkMinus, + plus_line_syntax_style_sections, + plus_line_diff_style_sections, + line_alignment, &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), @@ -152,25 +156,45 @@ impl<'a> Painter<'a> { 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 { - "" - }, - self.config.plus_style, - self.config.plus_non_emph_style, - 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 { + "" + }, + self.config.minus_style, + self.config.minus_non_emph_style, + 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 { + "" + }, + self.config.plus_style, + self.config.plus_non_emph_style, + Some(self.config.plus_empty_line_marker_style), + None, + ); + } } self.minus_lines.clear(); self.plus_lines.clear(); @@ -191,19 +215,152 @@ 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, - self.config.zero_style, - self.config.zero_style, - None, - None, - ); + if self.config.side_by_side { + Painter::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, + self.config.zero_style, + self.config.zero_style, + None, + 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, + self.config.zero_style, + self.config.zero_style, + None, + None, + ); + } + } + + fn paint_buffered_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::Config, + line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, + prefix: &str, + style: Style, + non_emph_style: Style, + empty_line_style: Option