use std::cmp::max; use lazy_static::lazy_static; use regex::Regex; use crate::config; use crate::delta::State; use crate::features::hyperlinks; use crate::features::side_by_side::{Left, PanelSide, Right}; use crate::features::OptionValueFunction; use crate::format; use crate::plusminus::*; use crate::style::Style; pub fn make_feature() -> Vec<(String, OptionValueFunction)> { builtin_feature!([ ( "line-numbers", bool, None, _opt => true ), ( "line-numbers-left-style", String, None, _opt => "blue" ), ( "line-numbers-right-style", String, None, _opt => "blue" ), ( "line-numbers-minus-style", String, None, opt => if opt.computed.is_light_mode { "red".to_string() } else { "88".to_string() } ), ( "line-numbers-zero-style", String, None, opt => if opt.computed.is_light_mode {"#dddddd"} else {"#444444"} ), ( "line-numbers-plus-style", String, None, opt => if opt.computed.is_light_mode { "green".to_string() } else { "28".to_string() } ) ]) } /// Return a vec of `ansi_term::ANSIGenericString`s representing the left and right fields of the /// two-column line number display. 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 nr_left = line_numbers_data.line_number[Left]; let nr_right = line_numbers_data.line_number[Right]; let (minus_style, zero_style, plus_style) = ( config.line_numbers_minus_style, config.line_numbers_zero_style, config.line_numbers_plus_style, ); let ((minus_number, plus_number), (minus_style, plus_style)) = match state { State::HunkMinus(_) => { line_numbers_data.line_number[Left] += 1; ((Some(nr_left), 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::HunkPlus(_) => { line_numbers_data.line_number[Right] += 1; ((None, Some(nr_right)), (minus_style, plus_style)) } _ => return Vec::new(), }; let mut formatted_numbers = Vec::new(); let (emit_left, emit_right) = match (config.side_by_side, side_by_side_panel) { (false, _) => (true, true), (true, Some(Left)) => (true, false), (true, Some(Right)) => (false, true), (true, None) => unreachable!(), }; if emit_left { formatted_numbers.extend(format_and_paint_line_number_field( &line_numbers_data.format_data[Left], &config.line_numbers_left_style, minus_number, plus_number, line_numbers_data.hunk_max_line_number_width, &minus_style, &plus_style, &line_numbers_data.plus_file, config, )); } if emit_right { formatted_numbers.extend(format_and_paint_line_number_field( &line_numbers_data.format_data[Right], &config.line_numbers_right_style, minus_number, plus_number, line_numbers_data.hunk_max_line_number_width, &minus_style, &plus_style, &line_numbers_data.plus_file, config, )); } formatted_numbers } lazy_static! { static ref LINE_NUMBERS_PLACEHOLDER_REGEX: Regex = format::make_placeholder_regex(&["nm", "np"]); } #[derive(Default, Debug)] pub struct LineNumbersData<'a> { pub format_data: PlusMinus>, pub line_number: PlusMinus, pub hunk_max_line_number_width: usize, pub plus_file: String, } pub type SideBySideLineWidth = PlusMinus; // Although it's probably unusual, a single format string can contain multiple placeholders. E.g. // line-numbers-right-format = "{nm} {np}|" impl<'a> LineNumbersData<'a> { pub fn from_format_strings(left_format: &'a str, right_format: &'a str) -> LineNumbersData<'a> { Self { format_data: PlusMinus::new( format::parse_line_number_format(left_format, &*LINE_NUMBERS_PLACEHOLDER_REGEX), format::parse_line_number_format(right_format, &*LINE_NUMBERS_PLACEHOLDER_REGEX), ), line_number: PlusMinus::new(0, 0), hunk_max_line_number_width: 0, plus_file: "".to_string(), } } /// Initialize line number data for a hunk. pub fn initialize_hunk(&mut self, line_numbers: &[(usize, usize)], plus_file: String) { // Typically, line_numbers has length 2: an entry for the minus file, and one for the plus // file. In the case of merge commits, it may be longer. self.line_number = PlusMinus::new(line_numbers[0].0, line_numbers[line_numbers.len() - 1].0); let hunk_max_line_number = line_numbers.iter().map(|(n, d)| n + d).max().unwrap(); self.hunk_max_line_number_width = 1 + (hunk_max_line_number as f64).log10().floor() as usize; self.plus_file = plus_file; } pub fn formatted_width(&self) -> SideBySideLineWidth { let format_data_width = |format_data: &format::FormatStringData<'a>| { // Provide each Placeholder with the max_line_number_width to calculate the // actual width. Only use prefix and suffix of the last element, otherwise // only the prefix (as the suffix also contains the following prefix). format_data .last() .map(|last| { let (prefix_width, suffix_width) = last.width(self.hunk_max_line_number_width); format_data .iter() .rev() .skip(1) .map(|p| p.width(self.hunk_max_line_number_width).0) .sum::() + prefix_width + suffix_width }) .unwrap_or(0) }; PlusMinus::new( format_data_width(&self.format_data[Left]), format_data_width(&self.format_data[Right]), ) } } #[allow(clippy::too_many_arguments)] fn format_and_paint_line_number_field<'a>( format_data: &[format::FormatStringPlaceholderData<'a>], style: &Style, minus_number: Option, plus_number: Option, min_field_width: usize, minus_number_style: &Style, plus_number_style: &Style, plus_file: &str, config: &config::Config, ) -> Vec> { let mut ansi_strings = Vec::new(); let mut suffix = ""; for placeholder in format_data { ansi_strings.push(style.paint(placeholder.prefix)); let alignment_spec = placeholder.alignment_spec.unwrap_or("^"); let width = if let Some(placeholder_width) = placeholder.width { max(placeholder_width, min_field_width) } else { min_field_width }; match placeholder.placeholder { Some("nm") => ansi_strings.push(minus_number_style.paint(format_line_number( minus_number, alignment_spec, width, None, config, ))), Some("np") => ansi_strings.push(plus_number_style.paint(format_line_number( plus_number, alignment_spec, width, Some(plus_file), config, ))), None => {} Some(_) => unreachable!(), } suffix = placeholder.suffix; } ansi_strings.push(style.paint(suffix)); ansi_strings } /// Return line number formatted according to `alignment` and `width`. fn format_line_number( line_number: Option, alignment: &str, width: usize, plus_file: Option<&str>, config: &config::Config, ) -> String { let pad = |n| format::pad(n, width, alignment); match (line_number, config.hyperlinks, plus_file) { (None, _, _) => pad(""), (Some(n), true, Some(file)) => { hyperlinks::format_osc8_file_hyperlink(file, line_number, &pad(&n.to_string()), config) .to_string() } (Some(n), _, _) => pad(&n.to_string()), } } #[cfg(test)] pub mod tests { use regex::Captures; use crate::ansi::strip_ansi_codes; use crate::tests::integration_test_utils::{make_config_from_args, run_delta}; use super::*; #[test] fn test_line_number_format_regex_1() { assert_eq!( format::parse_line_number_format("{nm}", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "", placeholder: Some("nm"), alignment_spec: None, width: None, suffix: "", prefix_len: 0, suffix_len: 0, }] ) } #[test] fn test_line_number_format_regex_2() { assert_eq!( format::parse_line_number_format("{np:4}", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "", placeholder: Some("np"), alignment_spec: None, width: Some(4), suffix: "", prefix_len: 0, suffix_len: 0, }] ) } #[test] fn test_line_number_format_regex_3() { assert_eq!( format::parse_line_number_format("{np:>4}", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "", placeholder: Some("np"), alignment_spec: Some(">"), width: Some(4), suffix: "", prefix_len: 0, suffix_len: 0, }] ) } #[test] fn test_line_number_format_regex_4() { assert_eq!( format::parse_line_number_format("{np:_>4}", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "", placeholder: Some("np"), alignment_spec: Some(">"), width: Some(4), suffix: "", prefix_len: 0, suffix_len: 0, }] ) } #[test] fn test_line_number_format_regex_5() { assert_eq!( format::parse_line_number_format("__{np:_>4}@@", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "__", placeholder: Some("np"), alignment_spec: Some(">"), width: Some(4), suffix: "@@", prefix_len: 2, suffix_len: 2, }] ) } #[test] fn test_line_number_format_regex_6() { assert_eq!( format::parse_line_number_format( "__{nm:<3}@@---{np:_>4}**", &LINE_NUMBERS_PLACEHOLDER_REGEX ), vec![ format::FormatStringPlaceholderData { prefix: "__", placeholder: Some("nm"), alignment_spec: Some("<"), width: Some(3), suffix: "@@---{np:_>4}**", prefix_len: 2, suffix_len: 15, }, format::FormatStringPlaceholderData { prefix: "@@---", placeholder: Some("np"), alignment_spec: Some(">"), width: Some(4), suffix: "**", prefix_len: 5, suffix_len: 2, } ] ) } #[test] fn test_line_number_format_regex_7() { assert_eq!( format::parse_line_number_format("__@@---**", &LINE_NUMBERS_PLACEHOLDER_REGEX), vec![format::FormatStringPlaceholderData { prefix: "", placeholder: None, alignment_spec: None, width: None, suffix: "__@@---**", prefix_len: 0, suffix_len: 9, },] ) } #[test] fn test_line_number_placeholder_width_one() { use format::parse_line_number_format; let data = parse_line_number_format("", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(0), (0, 0)); let data = parse_line_number_format("", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (0, 0)); let data = parse_line_number_format("│+│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (0, 3)); let data = parse_line_number_format("{np}", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (4, 0)); let data = parse_line_number_format("│{np}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (5, 1)); let data = parse_line_number_format("│{np:2}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (5, 1)); let data = parse_line_number_format("│{np:6}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(4), (7, 1)); } #[test] fn test_line_number_placeholder_width_two() { use format::parse_line_number_format; let data = parse_line_number_format("│{nm}│{np}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(1), (2, 6)); assert_eq!(data[1].width(1), (2, 1)); let data = parse_line_number_format("│{nm:_>5}│{np:1}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(1), (6, 8)); assert_eq!(data[1].width(1), (2, 1)); let data = parse_line_number_format("│{nm}│{np:5}│", &LINE_NUMBERS_PLACEHOLDER_REGEX); assert_eq!(data[0].width(7), (8, 8)); assert_eq!(data[1].width(7), (8, 1)); } #[test] fn test_line_numbers_data() { let mut data = LineNumbersData::from_format_strings("", ""); data.initialize_hunk(&[(10, 11), (10000, 100001)], "a".into()); assert_eq!(data.formatted_width(), PlusMinus::new(0, 0)); let mut data = LineNumbersData::from_format_strings("│", "│+│"); data.initialize_hunk(&[(10, 11), (10000, 100001)], "a".into()); assert_eq!(data.formatted_width(), PlusMinus::new(1, 3)); let mut data = LineNumbersData::from_format_strings("│{nm:^3}│", "│{np:^3}│"); data.initialize_hunk(&[(10, 11), (10000, 100001)], "a".into()); assert_eq!(data.formatted_width(), PlusMinus::new(8, 8)); let mut data = LineNumbersData::from_format_strings("│{nm:^3}│ │{np:<12}│ │{nm}│", ""); data.initialize_hunk(&[(10, 11), (10000, 100001)], "a".into()); assert_eq!(data.formatted_width(), PlusMinus::new(32, 0)); let mut data = LineNumbersData::from_format_strings("│{np:^3}│ │{nm:<12}│ │{np}│", ""); data.initialize_hunk(&[(10, 11), (10000, 100001)], "a".into()); assert_eq!(data.formatted_width(), PlusMinus::new(32, 0)); } fn _get_capture<'a>(i: usize, j: usize, caps: &'a Vec) -> &'a str { caps[i].get(j).map_or("", |m| m.as_str()) } #[test] fn test_two_minus_lines() { let config = make_config_from_args(&[ "--line-numbers", "--line-numbers-left-format", "{nm:^4}⋮", "--line-numbers-right-format", "{np:^4}│", "--line-numbers-left-style", "0 1", "--line-numbers-minus-style", "0 2", "--line-numbers-right-style", "0 3", "--line-numbers-plus-style", "0 4", ]); let output = run_delta(TWO_MINUS_LINES_DIFF, &config); let mut lines = output.lines().skip(7); let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap()); assert_eq!(strip_ansi_codes(line_1), " 1 ⋮ │a = 1"); assert_eq!(strip_ansi_codes(line_2), " 2 ⋮ │b = 23456"); } #[test] fn test_two_plus_lines() { let config = make_config_from_args(&[ "--line-numbers", "--line-numbers-left-format", "{nm:^4}⋮", "--line-numbers-right-format", "{np:^4}│", "--line-numbers-left-style", "0 1", "--line-numbers-minus-style", "0 2", "--line-numbers-right-style", "0 3", "--line-numbers-plus-style", "0 4", ]); 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!(strip_ansi_codes(line_1), " ⋮ 1 │a = 1"); assert_eq!(strip_ansi_codes(line_2), " ⋮ 2 │b = 234567"); } #[test] fn test_one_minus_one_plus_line() { let config = make_config_from_args(&[ "--line-numbers", "--line-numbers-left-format", "{nm:^4}⋮", "--line-numbers-right-format", "{np:^4}│", "--line-numbers-left-style", "0 1", "--line-numbers-minus-style", "0 2", "--line-numbers-right-style", "0 3", "--line-numbers-plus-style", "0 4", ]); 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!(lines.next().unwrap(), " 1 ⋮ 1 │a = 1"); assert_eq!(lines.next().unwrap(), " 2 ⋮ │b = 2"); assert_eq!(lines.next().unwrap(), " ⋮ 2 │bb = 2"); } #[test] fn test_repeated_placeholder() { let config = make_config_from_args(&[ "--line-numbers", "--line-numbers-left-format", "{nm:^4} {nm:^4}⋮", "--line-numbers-right-format", "{np:^4}│", "--line-numbers-left-style", "0 1", "--line-numbers-minus-style", "0 2", "--line-numbers-right-style", "0 3", "--line-numbers-plus-style", "0 4", ]); 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!(lines.next().unwrap(), " 1 1 ⋮ 1 │a = 1"); assert_eq!(lines.next().unwrap(), " 2 2 ⋮ │b = 2"); assert_eq!(lines.next().unwrap(), " ⋮ 2 │bb = 2"); } #[test] fn test_five_digit_line_number() { let config = make_config_from_args(&["--line-numbers"]); let output = run_delta(FIVE_DIGIT_LINE_NUMBER_DIFF, &config); let output = strip_ansi_codes(&output); let mut lines = output.lines().skip(7); assert_eq!(lines.next().unwrap(), "10000⋮10000│a = 1"); assert_eq!(lines.next().unwrap(), "10001⋮ │b = 2"); assert_eq!(lines.next().unwrap(), " ⋮10001│bb = 2"); } #[test] fn test_unequal_digit_line_number() { let config = make_config_from_args(&["--line-numbers"]); let output = run_delta(UNEQUAL_DIGIT_DIFF, &config); let output = strip_ansi_codes(&output); let mut lines = output.lines().skip(7); assert_eq!(lines.next().unwrap(), "10000⋮9999 │a = 1"); assert_eq!(lines.next().unwrap(), "10001⋮ │b = 2"); assert_eq!(lines.next().unwrap(), " ⋮10000│bb = 2"); } #[test] fn test_color_only() { let config = make_config_from_args(&["--line-numbers", "--color-only"]); let output = run_delta(TWO_MINUS_LINES_DIFF, &config); let mut lines = output.lines().skip(5); let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap()); assert_eq!(strip_ansi_codes(line_1), " 1 ⋮ │-a = 1"); assert_eq!(strip_ansi_codes(line_2), " 2 ⋮ │-b = 23456"); } #[test] fn test_hunk_header_style_is_omit() { let config = make_config_from_args(&["--line-numbers", "--hunk-header-style", "omit"]); let output = run_delta(TWO_LINE_DIFFS, &config); let output = strip_ansi_codes(&output); let mut lines = output.lines().skip(4); assert_eq!(lines.next().unwrap(), " 1 ⋮ 1 │a = 1"); assert_eq!(lines.next().unwrap(), " 2 ⋮ │b = 2"); assert_eq!(lines.next().unwrap(), " ⋮ 2 │bb = 2"); assert_eq!(lines.next().unwrap(), ""); assert_eq!(lines.next().unwrap(), "499 ⋮499 │a = 3"); assert_eq!(lines.next().unwrap(), "500 ⋮ │b = 4"); assert_eq!(lines.next().unwrap(), " ⋮500 │bb = 4"); } pub const TWO_MINUS_LINES_DIFF: &str = "\ diff --git i/a.py w/a.py index 223ca50..e69de29 100644 --- i/a.py +++ w/a.py @@ -1,2 +0,0 @@ -a = 1 -b = 23456 "; pub const TWO_PLUS_LINES_DIFF: &str = "\ diff --git c/a.py i/a.py new file mode 100644 index 0000000..223ca50 --- /dev/null +++ i/a.py @@ -0,0 +1,2 @@ +a = 1 +b = 234567 "; pub const ONE_MINUS_ONE_PLUS_LINE_DIFF: &str = "\ diff --git i/a.py w/a.py index 223ca50..367a6f6 100644 --- i/a.py +++ w/a.py @@ -1,2 +1,2 @@ a = 1 -b = 2 +bb = 2 "; const TWO_LINE_DIFFS: &str = "\ diff --git i/a.py w/a.py index 223ca50..367a6f6 100644 --- i/a.py +++ w/a.py @@ -1,2 +1,2 @@ a = 1 -b = 2 +bb = 2 @@ -499,2 +499,2 @@ a = 3 -b = 4 +bb = 4 "; const FIVE_DIGIT_LINE_NUMBER_DIFF: &str = "\ diff --git i/a.py w/a.py index 223ca50..367a6f6 100644 --- i/a.py +++ w/a.py @@ -10000,2 +10000,2 @@ a = 1 -b = 2 +bb = 2 "; const UNEQUAL_DIGIT_DIFF: &str = "\ diff --git i/a.py w/a.py index 223ca50..367a6f6 100644 --- i/a.py +++ w/a.py @@ -10000,2 +9999,2 @@ a = 1 -b = 2 +bb = 2 "; }