From d5ca6d944dbf62e71fe2b75242e342f8b0b953b3 Mon Sep 17 00:00:00 2001 From: Catherine Noll Date: Sun, 7 Jun 2020 14:54:22 -0400 Subject: [PATCH] Add --number option for showing line numbers. Also adds related options: - format string for specifying minus number line - format string for specifying plus number line - minus number style - plus number style - minus format string style - plus format string style --- src/cli.rs | 64 ++++++++++++++++++++++ src/config.rs | 79 +++++++++++++++++++++++++++ src/delta.rs | 23 ++++++-- src/paint.rs | 103 ++++++++++++++++++++++++++++++++++- src/parse.rs | 80 ++++++++++++++++++++++----- src/rewrite.rs | 9 ++- src/tests/ansi_test_utils.rs | 1 + 7 files changed, 338 insertions(+), 21 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 479eea8f2..0226832db 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -127,6 +127,24 @@ within a style string): Specifying colors like this is useful if your terminal only supports 256 colors (i.e. doesn\'t support 24-bit color). +LINE NUMBERS +------------ + +Options that have a name like --*-format allow you to specify a string to display for the line +number columns. The string should specify the location of the line number using the placeholder +%ln. + +For example, to display the line numbers divided by specific characters: + + 8 ⋮ 9 │ Here is an output line + 9 ⋮ 10 │ Here is another output line + 10 ⋮ 11 │ Here is the line number + +you would use the following input: + +--number-minus-format '%ln ⋮' +--number-plus-format '%ln │' + If something isn't working correctly, or you have a feature request, please open an issue at https://github.com/dandavison/delta/issues. " @@ -245,6 +263,52 @@ pub struct Opt { /// given. pub hunk_header_decoration_style: String, + /// Display line numbers next to the diff. The first column contains line + /// numbers in the previous version of the file, and the second column contains + /// line number in the new version of the file. A blank cell in the first or + /// second column indicates that the line does not exist in that file (it was + /// added or removed, respectively). + #[structopt(short = "n", long = "number")] + pub show_line_numbers: bool, + + /// Style (foreground, background, attributes) for the left (minus) column of line numbers + /// (--number), if --number is set. See STYLES section. + /// Defaults to --hunk-style. + #[structopt(long = "number-minus-style", default_value = "auto")] + pub number_minus_style: String, + + /// Style (foreground, background, attributes) for the right (plus) column of line numbers + /// (--number), if --number is set. See STYLES section. + /// Defaults to --hunk-style. + #[structopt(long = "number-plus-style", default_value = "auto")] + pub number_plus_style: String, + + /// Format string for the left (minus) column of line numbers (--number), if --number is set. + /// Should include the placeholder %ln to indicate the position of the line number. + /// See the LINE NUMBERS section. + /// Defaults to '%ln⋮' + #[structopt(long = "number-minus-format", default_value = "%ln⋮")] + pub number_minus_format: String, + + /// Format string for the right (plus) column of line numbers (--number), if --number is set. + /// Should include the placeholder %ln to indicate the position of the line number. + /// See the LINE NUMBERS section. + /// Defaults to '%ln│ ' + #[structopt(long = "number-plus-format", default_value = "%ln│ ")] + pub number_plus_format: String, + + /// Style (foreground, background, attributes) for the left (minus) line number format string + /// (--number), if --number is set. See STYLES section. + /// Defaults to --hunk-style. + #[structopt(long = "number-minus-format-style", default_value = "auto")] + pub number_minus_format_style: String, + + /// Style (foreground, background, attributes) for the right (plus) line number format string + /// (--number), if --number is set. See STYLES section. + /// Defaults to --hunk-style. + #[structopt(long = "number-plus-format-style", default_value = "auto")] + pub number_plus_format_style: String, + #[structopt(long = "color-only")] /// Do not alter the input in any way other than applying colors. Equivalent to /// `--keep-plus-minus-markers --width variable --tabs 0 --commit-decoration '' diff --git a/src/config.rs b/src/config.rs index 4c522ea85..a248f2947 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,12 +41,19 @@ pub struct Config<'a> { pub navigate: bool, pub null_style: Style, pub null_syntect_style: SyntectStyle, + pub number_minus_format: String, + pub number_minus_format_style: Style, + pub number_minus_style: Style, + pub number_plus_format: String, + pub number_plus_format_style: Style, + pub number_plus_style: Style, pub paging_mode: PagingMode, pub plus_emph_style: Style, pub plus_file: Option, pub plus_line_marker: &'a str, pub plus_non_emph_style: Style, pub plus_style: Style, + pub show_line_numbers: bool, pub syntax_dummy_theme: SyntaxTheme, pub syntax_set: SyntaxSet, pub syntax_theme: Option, @@ -120,6 +127,17 @@ pub fn get_config<'a>( let (commit_style, file_style, hunk_header_style) = make_commit_file_hunk_header_styles(&opt, true_color); + let ( + number_minus_format_style, + number_minus_style, + number_plus_format_style, + number_plus_style, + ) = make_line_number_styles( + &opt, + hunk_header_style.decoration_ansi_term_style(), + true_color, + ); + let syntax_theme = if syntax_theme::is_no_syntax_highlighting_theme_name(&syntax_theme_name) { None } else { @@ -164,12 +182,19 @@ pub fn get_config<'a>( navigate: opt.navigate, null_style: Style::new(), null_syntect_style: SyntectStyle::default(), + number_minus_format: opt.number_minus_format, + number_minus_format_style: number_minus_format_style, + number_minus_style: number_minus_style, + number_plus_format: opt.number_plus_format, + number_plus_format_style: number_plus_format_style, + number_plus_style: number_plus_style, paging_mode, plus_emph_style, plus_file: opt.plus_file.map(|s| s.clone()), plus_line_marker, plus_non_emph_style, plus_style, + show_line_numbers: opt.show_line_numbers, syntax_dummy_theme, syntax_set, syntax_theme, @@ -264,6 +289,60 @@ fn make_hunk_styles<'a>( ) } +fn make_line_number_styles<'a>( + opt: &'a cli::Opt, + default_style: Option, + true_color: bool, +) -> (Style, Style, Style, Style) { + let (default_foreground, default_background) = match default_style { + Some(default_style) => (default_style.foreground, default_style.background), + None => (None, None), + }; + + let number_minus_format_style = Style::from_str( + &opt.number_minus_format_style, + default_foreground, + default_background, + None, + true_color, + false, + ); + + let number_minus_style = Style::from_str( + &opt.number_minus_style, + default_foreground, + default_background, + None, + true_color, + false, + ); + + let number_plus_format_style = Style::from_str( + &opt.number_plus_format_style, + default_foreground, + default_background, + None, + true_color, + false, + ); + + let number_plus_style = Style::from_str( + &opt.number_plus_style, + default_foreground, + default_background, + None, + true_color, + false, + ); + + ( + number_minus_format_style, + number_minus_style, + number_plus_format_style, + number_plus_style, + ) +} + fn make_commit_file_hunk_header_styles(opt: &cli::Opt, true_color: bool) -> (Style, Style, Style) { ( Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg( diff --git a/src/delta.rs b/src/delta.rs index bab266796..b498bd0a4 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -369,7 +369,9 @@ fn handle_hunk_header_line( draw::write_no_decoration } }; - let (raw_code_fragment, line_number) = parse::parse_hunk_metadata(&line); + let (raw_code_fragment, line_numbers) = parse::parse_hunk_metadata(&line); + painter.minus_line_number = line_numbers[0]; + painter.plus_line_number = line_numbers[line_numbers.len() - 1]; if config.hunk_header_style.is_raw { writeln!(painter.writer)?; draw_fn( @@ -397,6 +399,7 @@ fn handle_hunk_header_line( Painter::paint_lines( syntax_style_sections, vec![vec![(config.hunk_header_style, &lines[0])]], + vec![None], &mut painter.output_buffer, config, "", @@ -418,10 +421,14 @@ fn handle_hunk_header_line( }; } }; - match config.hunk_header_style.decoration_ansi_term_style() { - Some(style) => writeln!(painter.writer, "{}", style.paint(line_number))?, - None => writeln!(painter.writer, "{}", line_number)?, - }; + + if !config.show_line_numbers { + let line_number = &format!("{}", painter.plus_line_number); + match config.hunk_header_style.decoration_ansi_term_style() { + Some(style) => writeln!(painter.writer, "{}", style.paint(line_number))?, + None => writeln!(painter.writer, "{}", line_number)?, + } + } Ok(()) } @@ -474,6 +481,10 @@ fn handle_hunk_line( Painter::paint_lines( syntax_style_sections, vec![diff_style_sections], + vec![Some(( + Some(painter.minus_line_number), + Some(painter.plus_line_number), + ))], &mut painter.output_buffer, config, prefix, @@ -481,6 +492,8 @@ fn handle_hunk_line( config.zero_style, None, ); + painter.minus_line_number += 1; + painter.plus_line_number += 1; state } _ => { diff --git a/src/paint.rs b/src/paint.rs index bfd029efb..b18a4399b 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,3 +1,5 @@ +use lazy_static::lazy_static; +use regex::Regex; use std::io::Write; use ansi_term; @@ -22,6 +24,8 @@ pub struct Painter<'a> { pub highlighter: HighlightLines<'a>, pub config: &'a config::Config<'a>, pub output_buffer: String, + pub minus_line_number: usize, + pub plus_line_number: usize, } impl<'a> Painter<'a> { @@ -37,6 +41,8 @@ impl<'a> Painter<'a> { highlighter: dummy_highlighter, writer, config, + minus_line_number: 0, + plus_line_number: 0, } } @@ -71,11 +77,23 @@ impl<'a> Painter<'a> { ); let (minus_line_diff_style_sections, plus_line_diff_style_sections) = Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config); + + let mut minus_line_numbers = Vec::new(); + let mut plus_line_numbers = Vec::new(); + for _line in &self.minus_lines { + minus_line_numbers.push(Some((Some(self.minus_line_number), None))); + self.minus_line_number += 1; + } + for _line in &self.plus_lines { + plus_line_numbers.push(Some((None, Some(self.plus_line_number)))); + self.plus_line_number += 1; + } // TODO: lines and style sections contain identical line text if !self.minus_lines.is_empty() { Painter::paint_lines( minus_line_syntax_style_sections, minus_line_diff_style_sections, + minus_line_numbers, &mut self.output_buffer, self.config, self.config.minus_line_marker, @@ -88,6 +106,7 @@ impl<'a> Painter<'a> { Painter::paint_lines( plus_line_syntax_style_sections, plus_line_diff_style_sections, + plus_line_numbers, &mut self.output_buffer, self.config, self.config.plus_line_marker, @@ -105,6 +124,7 @@ impl<'a> Painter<'a> { pub fn paint_lines( syntax_style_sections: Vec>, diff_style_sections: Vec>, + line_number_sections: Vec, Option)>>, output_buffer: &mut String, config: &config::Config, prefix: &str, @@ -120,8 +140,11 @@ 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), line_numbers) in syntax_style_sections + .iter() + .zip(diff_style_sections.iter()) + .zip(line_number_sections.iter()) { let non_emph_style = if style_sections_contain_more_than_one_style(diff_sections) { non_emph_style // line contains an emph section @@ -130,6 +153,45 @@ impl<'a> Painter<'a> { }; let mut ansi_strings = Vec::new(); let mut handled_prefix = false; + if config.show_line_numbers && line_numbers.is_some() { + let (minus, plus) = line_numbers.unwrap(); + let (minus_before, minus_number, minus_after) = + get_line_number_components(minus, &config.number_minus_format); + let (plus_before, plus_number, plus_after) = + get_line_number_components(plus, &config.number_plus_format); + + ansi_strings.push( + config + .number_minus_format_style + .ansi_term_style + .paint(minus_before), + ); + ansi_strings.push( + config + .number_minus_style + .ansi_term_style + .paint(minus_number), + ); + ansi_strings.push( + config + .number_minus_format_style + .ansi_term_style + .paint(minus_after), + ); + ansi_strings.push( + config + .number_plus_format_style + .ansi_term_style + .paint(plus_before), + ); + ansi_strings.push(config.number_plus_style.ansi_term_style.paint(plus_number)); + ansi_strings.push( + config + .number_plus_format_style + .ansi_term_style + .paint(plus_after), + ); + } for (section_style, mut text) in superimpose_style_sections( syntax_sections, diff_sections, @@ -499,3 +561,40 @@ mod superimpose_style_sections { } } } + +lazy_static! { + static ref LINE_NUMBER_REGEXP: Regex = + Regex::new(r"(?P.*)(?P%ln)(?P.*)").unwrap(); +} + +fn format_line_number(line_number: Option) -> String { + match line_number { + Some(x) => format!("{:^4}", x), + None => format!(" "), + } +} + +fn get_line_number_components( + number: Option, + number_format: &str, +) -> (String, String, String) { + let _caps = LINE_NUMBER_REGEXP.captures(number_format); + + let caps = match _caps { + Some(_) => _caps.unwrap(), + None => return (number_format.to_string(), "".to_string(), "".to_string()), + }; + + let before = caps.name("before").unwrap().as_str(); + let ln = caps.name("ln"); + let after = caps.name("after").unwrap().as_str(); + let display_number = match ln { + Some(_) => number, + None => None, + }; + ( + before.to_string(), + format_line_number(display_number), + after.to_string(), + ) +} diff --git a/src/parse.rs b/src/parse.rs index 7bccd658f..5cc112324 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,3 +1,5 @@ +use lazy_static::lazy_static; +use regex::Regex; use std::path::Path; use crate::config::Config; @@ -87,17 +89,36 @@ pub fn get_file_change_description_from_file_paths( } } +lazy_static! { + static ref HUNK_METADATA_REGEXP: Regex = + Regex::new(r"@+ (?P([-+]\d+(?:,\d+)? ){2,4})@+(?P.*\s?)").unwrap(); +} + +lazy_static! { + static ref LINE_NUMBER_REGEXP: Regex = Regex::new(r"[-+]").unwrap(); +} + +fn _make_line_number_vec(line: &str) -> Vec { + let mut numbers = Vec::::new(); + + for s in LINE_NUMBER_REGEXP.split(line) { + let number = s.split(',').nth(0).unwrap().split_whitespace().nth(0); + match number { + Some(number) => numbers.push(number.parse::().unwrap()), + None => continue, + } + } + return numbers; +} + /// Given input like /// "@@ -74,15 +74,14 @@ pub fn delta(" -/// Return " pub fn delta(" -pub fn parse_hunk_metadata(line: &str) -> (&str, &str) { - let mut iter = line.split("@@").skip(1); - let line_number = iter - .next() - .and_then(|s| s.split('+').nth(1).and_then(|s| s.split(',').next())) - .unwrap_or(""); - let code_fragment = iter.next().unwrap_or(""); - (code_fragment, line_number) +/// Return " pub fn delta(" and a vector of line numbers +pub fn parse_hunk_metadata(line: &str) -> (&str, Vec) { + let caps = HUNK_METADATA_REGEXP.captures(line).unwrap(); + let line_numbers = _make_line_number_vec(caps.name("lns").unwrap().as_str()); + let code_fragment = caps.name("cf").unwrap().as_str(); + return (code_fragment, line_numbers); } /// Attempt to parse input as a file path and return extension as a &str. @@ -249,9 +270,42 @@ mod tests { #[test] fn test_parse_hunk_metadata() { - assert_eq!( - parse_hunk_metadata("@@ -74,15 +75,14 @@ pub fn delta(\n"), - (" pub fn delta(\n", "75") - ); + let parsed = parse_hunk_metadata("@@ -74,15 +75,14 @@ pub fn delta(\n"); + let code_fragment = parsed.0; + let line_numbers = parsed.1; + assert_eq!(code_fragment, " pub fn delta(\n",); + assert_eq!(line_numbers[0], 74,); + assert_eq!(line_numbers[1], 75,); + } + + #[test] + fn test_parse_hunk_metadata_added_file() { + let parsed = parse_hunk_metadata("@@ -1,22 +0,0 @@"); + let code_fragment = parsed.0; + let line_numbers = parsed.1; + assert_eq!(code_fragment, "",); + assert_eq!(line_numbers[0], 1,); + assert_eq!(line_numbers[1], 0,); + } + + #[test] + fn test_parse_hunk_metadata_deleted_file() { + let parsed = parse_hunk_metadata("@@ -0,0 +1,3 @@"); + let code_fragment = parsed.0; + let line_numbers = parsed.1; + assert_eq!(code_fragment, "",); + assert_eq!(line_numbers[0], 0,); + assert_eq!(line_numbers[1], 1,); + } + + #[test] + fn test_parse_hunk_metadata_merge() { + let parsed = parse_hunk_metadata("@@@ -293,11 -358,15 +358,16 @@@ dependencies ="); + let code_fragment = parsed.0; + let line_numbers = parsed.1; + assert_eq!(code_fragment, " dependencies =",); + assert_eq!(line_numbers[0], 293,); + assert_eq!(line_numbers[1], 358,); + assert_eq!(line_numbers[2], 358,); } } diff --git a/src/rewrite.rs b/src/rewrite.rs index ab75508eb..c7db5fae2 100644 --- a/src/rewrite.rs +++ b/src/rewrite.rs @@ -49,7 +49,8 @@ fn _rewrite_options_to_honor_git_config( ("dark", dark), ("navigate", navigate), ("color-only", color_only), - ("keep-plus-minus-markers", keep_plus_minus_markers) + ("keep-plus-minus-markers", keep_plus_minus_markers), + ("number", show_line_numbers) ], opt, git_config, @@ -76,6 +77,12 @@ fn _rewrite_options_to_honor_git_config( ("minus-emph-style", minus_emph_style), ("minus-non-emph-style", minus_non_emph_style), ("minus-style", minus_style), + ("number-minus-format", number_minus_format), + ("number-minus-format-style", number_minus_format_style), + ("number-minus-style", number_minus_style), + ("number-plus-format", number_plus_format), + ("number-plus-format-style", number_plus_format_style), + ("number-plus-style", number_plus_style), ("paging-mode", paging_mode), ("plus-emph-style", plus_emph_style), ("plus-non-emph-style", plus_non_emph_style), diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs index 5beb3f4e0..7d0ccf366 100644 --- a/src/tests/ansi_test_utils.rs +++ b/src/tests/ansi_test_utils.rs @@ -107,6 +107,7 @@ pub mod ansi_test_utils { paint::Painter::paint_lines( vec![syntax_style_sections], vec![vec![(syntax_highlighted_style, lines[0])]], + vec![None], &mut output_buffer, config, "",