summaryrefslogtreecommitdiffstats
path: root/src/features/line_numbers.rs
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-06-25 12:59:07 -0400
committerDan Davison <dandavison7@gmail.com>2020-06-25 16:21:32 -0400
commit68071fc743ebb8126c006d66d843a6b4310bf486 (patch)
treec4d533d9851de180f70a1211acff02c820a958b9 /src/features/line_numbers.rs
parent1d19643af2d5efa0b5c5c5735233083e7e6bd6ca (diff)
Rename: --number => --line-numbers
Diffstat (limited to 'src/features/line_numbers.rs')
-rw-r--r--src/features/line_numbers.rs336
1 files changed, 336 insertions, 0 deletions
diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs
new file mode 100644
index 00000000..7602343a
--- /dev/null
+++ b/src/features/line_numbers.rs
@@ -0,0 +1,336 @@
+use ansi_term;
+use lazy_static::lazy_static;
+use regex::Regex;
+
+use crate::config;
+use crate::features::OptionValueFunction;
+use crate::style::Style;
+
+pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
+ builtin_feature!([
+ (
+ "line-numbers",
+ bool,
+ None,
+ _opt => true
+ ),
+ (
+ "line-numbers-minus-style",
+ String,
+ Some("color.diff.old"),
+ _opt => "red"
+ ),
+ (
+ "line-numbers-zero-style",
+ String,
+ None,
+ _opt => "#dddddd"
+ ),
+ (
+ "line-numbers-plus-style",
+ String,
+ Some("color.diff.new"),
+ _opt => "green"
+ )
+ ])
+}
+
+/// 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: &'a Option<(Option<usize>, Option<usize>)>,
+ config: &'a config::Config,
+) -> Vec<ansi_term::ANSIGenericString<'a, str>> {
+ let (minus_number, plus_number) = line_numbers.unwrap();
+
+ // If both minus and plus numbers are present then the line is a zero line.
+ let (minus_style, plus_style) = match (minus_number, plus_number) {
+ (Some(_), Some(_)) => (
+ config.line_numbers_zero_style,
+ config.line_numbers_zero_style,
+ ),
+ _ => (
+ config.line_numbers_minus_style,
+ config.line_numbers_plus_style,
+ ),
+ };
+
+ let mut formatted_numbers = Vec::new();
+
+ formatted_numbers.extend(format_and_paint_line_number_field(
+ &config.line_numbers_left_format,
+ &config.line_numbers_left_format_style,
+ minus_number,
+ plus_number,
+ &minus_style,
+ &plus_style,
+ ));
+ formatted_numbers.extend(format_and_paint_line_number_field(
+ &config.line_numbers_right_format,
+ &config.line_numbers_right_format_style,
+ minus_number,
+ plus_number,
+ &minus_style,
+ &plus_style,
+ ));
+
+ formatted_numbers
+}
+
+lazy_static! {
+ static ref LINE_NUMBER_FORMAT_REGEX: Regex = Regex::new(
+ r"(?x)
+\{
+(nm|np) # 1: Literal nm or np
+(?: # Start optional format spec (non-capturing)
+ : # Literal colon
+ (?: # Start optional fill/alignment spec (non-capturing)
+ ([^<^>])? # 2: Optional fill character
+ ([<^>]) # 3: Alignment spec
+ )? #
+ (\d+) # 4: Width
+)? #
+\}
+"
+ )
+ .unwrap();
+}
+
+fn format_and_paint_line_number_field<'a>(
+ format_string: &'a str,
+ number_format_style: &Style,
+ minus: Option<usize>,
+ plus: Option<usize>,
+ line_numbers_minus_style: &Style,
+ line_numbers_plus_style: &Style,
+) -> Vec<ansi_term::ANSIGenericString<'a, str>> {
+ let mut formatted_number_strings = Vec::new();
+
+ let mut offset = 0;
+ for caps in LINE_NUMBER_FORMAT_REGEX.captures_iter(&format_string) {
+ let _match = caps.get(0).unwrap();
+ formatted_number_strings
+ .push(number_format_style.paint(&format_string[offset.._match.start()]));
+
+ match &caps[1] {
+ "nm" => formatted_number_strings.push(
+ line_numbers_minus_style.paint(format_line_number(minus, &caps[3], &caps[4])),
+ ),
+ "np" => formatted_number_strings
+ .push(line_numbers_plus_style.paint(format_line_number(plus, &caps[3], &caps[4]))),
+ _ => unreachable!(),
+ }
+ offset = _match.end();
+ }
+ formatted_number_strings.push(number_format_style.paint(&format_string[offset..]));
+ formatted_number_strings
+}
+
+/// Return line number formatted according to `alignment` and `width`.
+fn format_line_number(line_number: Option<usize>, alignment: &str, width: &str) -> String {
+ let n = line_number
+ .map(|n| format!("{}", n))
+ .unwrap_or_else(|| "".to_string());
+ let default_width = 4; // Used only if \d+ cannot be parsed as usize
+ let w: usize = width.parse().unwrap_or(default_width);
+ match alignment {
+ "<" => format!("{0:<1$}", n, w),
+ "^" | "" => format!("{0:^1$}", n, w),
+ ">" => format!("{0:>1$}", n, w),
+ _ => unreachable!(),
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use console::strip_ansi_codes;
+ use regex::Captures;
+
+ use crate::tests::integration_test_utils::integration_test_utils::{make_config, run_delta};
+
+ use super::LINE_NUMBER_FORMAT_REGEX;
+
+ #[test]
+ fn test_line_number_format_regex_1() {
+ let caps = LINE_NUMBER_FORMAT_REGEX
+ .captures_iter("{nm}")
+ .collect::<Vec<Captures>>();
+ assert_eq!(caps.len(), 1);
+ assert_eq!(_get_capture(0, 1, &caps), "nm");
+ assert_eq!(_get_capture(0, 2, &caps), "");
+ assert_eq!(_get_capture(0, 3, &caps), "");
+ assert_eq!(_get_capture(0, 4, &caps), "");
+ }
+
+ #[test]
+ fn test_line_number_format_regex_2() {
+ let caps = LINE_NUMBER_FORMAT_REGEX
+ .captures_iter("{np:4}")
+ .collect::<Vec<Captures>>();
+ assert_eq!(caps.len(), 1);
+ assert_eq!(_get_capture(0, 1, &caps), "np");
+ assert_eq!(_get_capture(0, 2, &caps), "");
+ assert_eq!(_get_capture(0, 3, &caps), "");
+ assert_eq!(_get_capture(0, 4, &caps), "4");
+ }
+
+ #[test]
+ fn test_line_number_format_regex_3() {
+ let caps = LINE_NUMBER_FORMAT_REGEX
+ .captures_iter("{np:>4}")
+ .collect::<Vec<Captures>>();
+ assert_eq!(caps.len(), 1);
+ assert_eq!(_get_capture(0, 1, &caps), "np");
+ assert_eq!(_get_capture(0, 2, &caps), "");
+ assert_eq!(_get_capture(0, 3, &caps), ">");
+ assert_eq!(_get_capture(0, 4, &caps), "4");
+ }
+
+ #[test]
+ fn test_line_number_format_regex_4() {
+ let caps = LINE_NUMBER_FORMAT_REGEX
+ .captures_iter("{np:_>4}")
+ .collect::<Vec<Captures>>();
+ assert_eq!(caps.len(), 1);
+ assert_eq!(_get_capture(0, 1, &caps), "np");
+ assert_eq!(_get_capture(0, 2, &caps), "_");
+ assert_eq!(_get_capture(0, 3, &caps), ">");
+ assert_eq!(_get_capture(0, 4, &caps), "4");
+ }
+
+ fn _get_capture<'a>(i: usize, j: usize, caps: &'a Vec<Captures>) -> &'a str {
+ caps[i].get(j).map_or("", |m| m.as_str())
+ }
+
+ #[test]
+ fn test_two_minus_lines() {
+ let config = make_config(&[
+ "--line-numbers",
+ "--line-numbers-left-format",
+ "{nm:^4}⋮",
+ "--line-numbers-right-format",
+ "{np:^4}│",
+ "--line-numbers-left-format-style",
+ "0 1",
+ "--line-numbers-minus-style",
+ "0 2",
+ "--line-numbers-right-format-style",
+ "0 3",
+ "--line-numbers-plus-style",
+ "0 4",
+ ]);
+ let output = run_delta(TWO_MINUS_LINES_DIFF, &config);
+ println!("{}", &output);
+ let mut lines = output.lines().skip(4);
+ 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 = 2");
+ }
+
+ #[test]
+ fn test_two_plus_lines() {
+ let config = make_config(&[
+ "--line-numbers",
+ "--line-numbers-left-format",
+ "{nm:^4}⋮",
+ "--line-numbers-right-format",
+ "{np:^4}│",
+ "--line-numbers-left-format-style",
+ "0 1",
+ "--line-numbers-minus-style",
+ "0 2",
+ "--line-numbers-right-format-style",
+ "0 3",
+ "--line-numbers-plus-style",
+ "0 4",
+ ]);
+ 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!(strip_ansi_codes(line_1), " ⋮ 1 │a = 1");
+ assert_eq!(strip_ansi_codes(line_2), " ⋮ 2 │b = 2");
+ }
+
+ #[test]
+ fn test_one_minus_one_plus_line() {
+ let config = make_config(&[
+ "--line-numbers",
+ "--line-numbers-left-format",
+ "{nm:^4}⋮",
+ "--line-numbers-right-format",
+ "{np:^4}│",
+ "--line-numbers-left-format-style",
+ "0 1",
+ "--line-numbers-minus-style",
+ "0 2",
+ "--line-numbers-right-format-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(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");
+ }
+
+ #[test]
+ fn test_repeated_placeholder() {
+ let config = make_config(&[
+ "--line-numbers",
+ "--line-numbers-left-format",
+ "{nm:^4} {nm:^4}⋮",
+ "--line-numbers-right-format",
+ "{np:^4}│",
+ "--line-numbers-left-format-style",
+ "0 1",
+ "--line-numbers-minus-style",
+ "0 2",
+ "--line-numbers-right-format-style",
+ "0 3",
+ "--line-numbers-plus-style",
+ "0 4",
+ ]);
+ let output = run_delta(ONE_MINUS_ONE_PLUS_LINE_DIFF, &config);
+ println!("{}", output);
+ let output = strip_ansi_codes(&output);
+ let mut lines = output.lines().skip(4);
+ 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");
+ }
+
+ 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 = 2
+";
+
+ 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 = 2
+";
+
+ 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
+";
+}