summaryrefslogtreecommitdiffstats
path: root/src/features/side_by_side.rs
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-07-11 12:09:18 -0400
committerGitHub <noreply@github.com>2020-07-11 12:09:18 -0400
commit4c8f5f5835956e9f5b852b9adbf0a5ffbea1e9be (patch)
treefa20d7e780cb67cd67976524f0404b6e73983a6f /src/features/side_by_side.rs
parentca44215ac6bff9ca1ac2497247ac2286e630bde5 (diff)
Add side-by-side diff view (#243)
Closes #86
Diffstat (limited to 'src/features/side_by_side.rs')
-rw-r--r--src/features/side_by_side.rs478
1 files changed, 478 insertions, 0 deletions
diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs
new file mode 100644
index 00000000..ada02c71
--- /dev/null
+++ b/src/features/side_by_side.rs
@@ -0,0 +1,478 @@
+use console;
+
+use itertools::Itertools;
+use syntect::highlighting::Style as SyntectStyle;
+
+use crate::cli;
+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())
+ ])
+}
+
+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(decorations_width: &cli::Width, available_terminal_width: &usize) -> Self {
+ let panel_width = match decorations_width {
+ cli::Width::Fixed(w) => w / 2,
+ _ => 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<Vec<(SyntectStyle, &str)>>,
+ minus_diff_style_sections: Vec<Vec<(Style, &str)>>,
+ plus_syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
+ plus_diff_style_sections: Vec<Vec<(Style, &str)>>,
+ 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: Option<bool>,
+) {
+ 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,
+ if config.keep_plus_minus_markers {
+ "-"
+ } else {
+ ""
+ },
+ 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,
+ if config.keep_plus_minus_markers {
+ "+"
+ } else {
+ ""
+ },
+ background_color_extends_to_terminal_width,
+ config,
+ ));
+ output_buffer.push_str("\n");
+ }
+}
+
+pub fn paint_zero_lines_side_by_side(
+ syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
+ diff_style_sections: Vec<Vec<(Style, &str)>>,
+ 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<bool>,
+) {
+ 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<usize>,
+ syntax_style_sections: &Vec<Vec<(SyntectStyle, &str)>>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
+ prefix: &str,
+ background_color_extends_to_terminal_width: Option<bool>,
+ 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<usize>,
+ syntax_style_sections: &Vec<Vec<(SyntectStyle, &str)>>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
+ prefix: &str,
+ background_color_extends_to_terminal_width: Option<bool>,
+ 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<usize>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ state: &State,
+ background_color_extends_to_terminal_width: Option<bool>,
+ 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<usize>,
+ syntax_style_sections: &Vec<Vec<(SyntectStyle, &str)>>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ 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)
+}
+
+/// Right-pad a line in the left panel with (possibly painted) spaces. A line in the left panel is
+/// either a minus line or a zero line.
+fn right_pad_left_panel_line(
+ panel_line: &mut String,
+ panel_line_is_empty: bool,
+ line_index: Option<usize>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ state: &State,
+ background_color_extends_to_terminal_width: Option<bool>,
+ 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 the background color of a line in the right panel. A line in the right panel is
+/// either a zero line or a plus line. The fill is achieved using ANSI sequences instructing the
+/// terminal emulator to fill the background color rightwards; it does not involve appending spaces
+/// to the line.
+fn right_fill_right_panel_line(
+ panel_line: &mut String,
+ panel_line_is_empty: bool,
+ line_index: Option<usize>,
+ diff_style_sections: &Vec<Vec<(Style, &str)>>,
+ state: &State,
+ background_color_extends_to_terminal_width: Option<bool>,
+ config: &Config,
+) {
+ *panel_line = console::truncate_str(
+ &panel_line,
+ config.side_by_side_data.right_panel.width,
+ &config.truncation_symbol,
+ )
+ .to_string();
+
+ // Unlike `right_pad_left_panel_line`, 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)
+ };
+
+ if should_right_fill_background_color {
+ Painter::right_fill_background_color(panel_line, fill_style);
+ } else if panel_line_is_empty && line_index.is_some() {
+ // Emit empty line marker when the panel line is empty but not empty-by-construction. See
+ // parallel comment in `paint_left_panel_minus_line`.
+ match state {
+ State::HunkPlus => Painter::mark_empty_line(
+ &config.plus_empty_line_marker_style,
+ panel_line,
+ Some(" "),
+ ),
+ State::HunkZero => {}
+ _ => unreachable!(),
+ }
+ };
+}
+
+#[cfg(test)]
+pub mod tests {
+ use console::strip_ansi_codes;
+
+ use crate::features::line_numbers::tests::*;
+ use crate::tests::integration_test_utils::integration_test_utils::{make_config, run_delta};
+
+ #[test]
+ fn test_two_minus_lines() {
+ let config = make_config(&["--side-by-side", "--width", "40"]);
+ let output = run_delta(TWO_MINUS_LINES_DIFF, &config);
+ let mut lines = output.lines().skip(4);
+ let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
+ assert_eq!("│ 1 │a = 1 │ │", strip_ansi_codes(line_1));
+ assert_eq!("│ 2 │b = 2 │ │", strip_ansi_codes(line_2));
+ }
+
+ #[test]
+ fn test_two_plus_lines() {
+ let config = make_config(&["--side-by-side", "--width", "40"]);
+ let output = run_delta(TWO_PLUS_LINES_DIFF, &config);
+ let mut lines = output.lines().skip(4);
+ let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
+ assert_eq!("│ │ │ 1 │a = 1", strip_ansi_codes(line_1));
+ assert_eq!("│ │ │ 2 │b = 2", strip_ansi_codes(line_2));
+ }
+
+ #[test]
+ fn test_one_minus_one_plus_line() {
+ let config = make_config(&["--side-by-side", "--width", "40"]);
+ let output = run_delta(ONE_MINUS_ONE_PLUS_LINE_DIFF, &config);
+ let output = strip_ansi_codes(&output);
+ let mut lines = output.lines().skip(4);
+ assert_eq!("│ 1 │a = 1 │ 1 │a = 1", lines.next().unwrap());
+ assert_eq!("│ 2 │b = 2 │ 2 │bb = 2", lines.next().unwrap());
+ }
+}