diff options
author | Dan Davison <dandavison7@gmail.com> | 2020-06-17 17:15:02 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-17 17:15:02 -0400 |
commit | 9bd9398ca6d1589033ce34bebbc02cfcda18ee6b (patch) | |
tree | aa119d7103390de21d71283bb86e6a03ec436408 | |
parent | 6e3f4d00d4ed56096980e655c2e2cab485b9c0e1 (diff) | |
parent | e8ba95d34b2738d2e76e9d1944b787b41c91fef6 (diff) |
Merge pull request #225 from dandavison/whitespace
Drop leading space, highlight empty lines, highlight whitespace errors
-rw-r--r-- | src/cli.rs | 16 | ||||
-rw-r--r-- | src/config.rs | 85 | ||||
-rw-r--r-- | src/delta.rs | 28 | ||||
-rw-r--r-- | src/paint.rs | 90 | ||||
-rw-r--r-- | src/preset.rs | 79 | ||||
-rw-r--r-- | src/set_options.rs | 15 | ||||
-rw-r--r-- | src/style.rs | 8 | ||||
-rw-r--r-- | src/tests/ansi_test_utils.rs | 32 | ||||
-rw-r--r-- | src/tests/integration_test_utils.rs | 8 | ||||
-rw-r--r-- | src/tests/test_example_diffs.rs | 215 |
10 files changed, 490 insertions, 86 deletions
@@ -404,6 +404,22 @@ pub struct Opt { #[structopt(parse(from_os_str))] pub plus_file: Option<PathBuf>, + /// Style for removed empty line marker (used only if --minus-style has no background color) + #[structopt( + long = "--minus-empty-line-marker-style", + default_value = "normal auto" + )] + pub minus_empty_line_marker_style: String, + + /// Style for added empty line marker (used only if --plus-style has no background color) + #[structopt(long = "--plus-empty-line-marker-style", default_value = "normal auto")] + pub plus_empty_line_marker_style: String, + + /// Style for whitespace errors. Defaults to color.diff.whitespace if that is set in git + /// config, or else 'magenta reverse'. + #[structopt(long = "whitespace-error-style", default_value = "auto auto")] + pub whitespace_error_style: String, + #[structopt(long = "minus-color")] /// Deprecated: use --minus-style='normal my_background_color'. pub deprecated_minus_background_color: Option<String>, diff --git a/src/config.rs b/src/config.rs index c5299e12..e05d7b8c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,7 +26,7 @@ pub enum Width { Variable, } -pub struct Config<'a> { +pub struct Config { pub background_color_extends_to_terminal_width: bool, pub commit_style: Style, pub decorations_width: Width, @@ -35,6 +35,7 @@ pub struct Config<'a> { pub file_removed_label: String, pub file_renamed_label: String, pub file_style: Style, + pub keep_plus_minus_markers: bool, pub hunk_header_style: Style, pub list_languages: bool, pub list_syntax_theme_names: bool, @@ -43,8 +44,8 @@ pub struct Config<'a> { pub max_line_distance: f64, pub max_line_distance_for_naively_paired_lines: f64, pub minus_emph_style: Style, + pub minus_empty_line_marker_style: Style, pub minus_file: Option<PathBuf>, - pub minus_line_marker: &'a str, pub minus_non_emph_style: Style, pub minus_style: Style, pub navigate: bool, @@ -58,8 +59,8 @@ pub struct Config<'a> { pub number_plus_style: Style, pub paging_mode: PagingMode, pub plus_emph_style: Style, + pub plus_empty_line_marker_style: Style, pub plus_file: Option<PathBuf>, - pub plus_line_marker: &'a str, pub plus_non_emph_style: Style, pub plus_style: Style, pub show_background_colors: bool, @@ -71,10 +72,11 @@ pub struct Config<'a> { pub tab_width: usize, pub true_color: bool, pub tokenization_regex: Regex, + pub whitespace_error_style: Style, pub zero_style: Style, } -impl<'a> Config<'a> { +impl Config { pub fn from_args(args: &[&str], git_config: &mut Option<GitConfig>) -> Self { Self::from_arg_matches(cli::Opt::clap().get_matches_from(args), git_config) } @@ -149,7 +151,7 @@ fn is_truecolor_terminal() -> bool { .unwrap_or(false) } -impl<'a> From<cli::Opt> for Config<'a> { +impl From<cli::Opt> for Config { fn from(opt: cli::Opt) -> Self { let assets = HighlightingAssets::new(); @@ -208,10 +210,13 @@ impl<'a> From<cli::Opt> for Config<'a> { minus_style, minus_emph_style, minus_non_emph_style, + minus_empty_line_marker_style, zero_style, plus_style, plus_emph_style, plus_non_emph_style, + plus_empty_line_marker_style, + whitespace_error_style, ) = make_hunk_styles(&opt, is_light_mode, true_color); let (commit_style, file_style, hunk_header_style) = @@ -236,17 +241,6 @@ impl<'a> From<cli::Opt> for Config<'a> { }; let syntax_dummy_theme = assets.theme_set.themes.values().next().unwrap().clone(); - let minus_line_marker = if opt.keep_plus_minus_markers { - "-" - } else { - " " - }; - let plus_line_marker = if opt.keep_plus_minus_markers { - "+" - } else { - " " - }; - let max_line_distance_for_naively_paired_lines = env::get_env_var("DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES") .map(|s| s.parse::<f64>().unwrap_or(0.0)) @@ -271,6 +265,7 @@ impl<'a> From<cli::Opt> for Config<'a> { file_removed_label: opt.file_removed_label, file_renamed_label: opt.file_renamed_label, file_style, + keep_plus_minus_markers: opt.keep_plus_minus_markers, hunk_header_style, list_languages: opt.list_languages, list_syntax_theme_names: opt.list_syntax_theme_names, @@ -279,8 +274,8 @@ impl<'a> From<cli::Opt> for Config<'a> { max_line_distance: opt.max_line_distance, max_line_distance_for_naively_paired_lines, minus_emph_style, + minus_empty_line_marker_style, minus_file: opt.minus_file.map(|s| s.clone()), - minus_line_marker, minus_non_emph_style, minus_style, navigate: opt.navigate, @@ -294,8 +289,8 @@ impl<'a> From<cli::Opt> for Config<'a> { number_plus_style, paging_mode, plus_emph_style, + plus_empty_line_marker_style, plus_file: opt.plus_file.map(|s| s.clone()), - plus_line_marker, plus_non_emph_style, plus_style, show_background_colors: opt.show_background_colors, @@ -307,6 +302,7 @@ impl<'a> From<cli::Opt> for Config<'a> { tab_width: opt.tab_width, tokenization_regex, true_color, + whitespace_error_style, zero_style, } } @@ -316,7 +312,18 @@ fn make_hunk_styles<'a>( opt: &'a cli::Opt, is_light_mode: bool, true_color: bool, -) -> (Style, Style, Style, Style, Style, Style, Style) { +) -> ( + Style, + Style, + Style, + Style, + Style, + Style, + Style, + Style, + Style, + Style, +) { let minus_style = Style::from_str( &opt.minus_style, None, @@ -350,6 +357,20 @@ fn make_hunk_styles<'a>( false, ); + // The style used to highlight a removed empty line when otherwise it would be invisible due to + // lack of background color in minus-style. + let minus_empty_line_marker_style = Style::from_str( + &opt.minus_empty_line_marker_style, + None, + Some(color::get_minus_background_color_default( + is_light_mode, + true_color, + )), + None, + true_color, + false, + ); + let zero_style = Style::from_str(&opt.zero_style, None, None, None, true_color, false); let plus_style = Style::from_str( @@ -385,14 +406,40 @@ fn make_hunk_styles<'a>( false, ); + // The style used to highlight an added empty line when otherwise it would be invisible due to + // lack of background color in plus-style. + let plus_empty_line_marker_style = Style::from_str( + &opt.plus_empty_line_marker_style, + None, + Some(color::get_plus_background_color_default( + is_light_mode, + true_color, + )), + None, + true_color, + false, + ); + + let whitespace_error_style = Style::from_str( + &opt.whitespace_error_style, + None, + None, + None, + true_color, + false, + ); + ( minus_style, minus_emph_style, minus_non_emph_style, + minus_empty_line_marker_style, zero_style, plus_style, plus_emph_style, plus_non_emph_style, + plus_empty_line_marker_style, + whitespace_error_style, ) } diff --git a/src/delta.rs b/src/delta.rs index b498bd0a..366520ec 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -405,6 +405,7 @@ fn handle_hunk_header_line( "", config.null_style, config.null_style, + None, Some(false), ); painter.output_buffer.pop(); // trim newline @@ -467,7 +468,11 @@ fn handle_hunk_line( } Some(' ') => { let state = State::HunkZero; - let prefix = if line.is_empty() { "" } else { &line[..1] }; + let prefix = if config.keep_plus_minus_markers && !line.is_empty() { + &line[..1] + } else { + "" + }; painter.paint_buffered_lines(); let lines = vec![prepare(&line, true, config)]; let syntax_style_sections = Painter::get_syntax_style_sections_for_lines( @@ -491,6 +496,7 @@ fn handle_hunk_line( config.zero_style, config.zero_style, None, + None, ); painter.minus_line_number += 1; painter.plus_line_number += 1; @@ -520,12 +526,22 @@ fn prepare(line: &str, append_newline: bool, config: &Config) -> String { if !line.is_empty() { let mut line = line.graphemes(true); - // The first column contains a -/+/space character, added by git. We substitute it for a - // space now, so that it is not present during syntax highlighting, and substitute again - // when emitting the line. + // The first column contains a -/+/space character, added by git. If we are retaining them + // (--keep-plus-minus-markers), then we substitute it for a space now, so that it is not + // present during syntax highlighting, and reinstate it when emitting the line in + // Painter::paint_lines. If we not retaining them, then we drop it now, and + // Painter::paint_lines doesn't inject anything. line.next(); - - format!(" {}{}", expand_tabs(line, config.tab_width), terminator) + format!( + "{}{}{}", + if config.keep_plus_minus_markers { + " " + } else { + "" + }, + expand_tabs(line, config.tab_width), + terminator + ) } else { terminator.to_string() } diff --git a/src/paint.rs b/src/paint.rs index d1505c74..f946853c 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -14,6 +14,8 @@ use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K"; +pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K"; +pub const ANSI_CSI_CURSOR_BACK_1: &str = "\x1b[1D"; pub const ANSI_SGR_RESET: &str = "\x1b[0m"; pub struct Painter<'a> { @@ -22,7 +24,7 @@ pub struct Painter<'a> { pub writer: &'a mut dyn Write, pub syntax: &'a SyntaxReference, pub highlighter: HighlightLines<'a>, - pub config: &'a config::Config<'a>, + pub config: &'a config::Config, pub output_buffer: String, pub minus_line_number: usize, pub plus_line_number: usize, @@ -96,9 +98,14 @@ impl<'a> Painter<'a> { minus_line_numbers, &mut self.output_buffer, self.config, - self.config.minus_line_marker, + 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, ); } @@ -109,9 +116,14 @@ impl<'a> Painter<'a> { plus_line_numbers, &mut self.output_buffer, self.config, - self.config.plus_line_marker, + 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, ); } @@ -130,6 +142,7 @@ impl<'a> Painter<'a> { prefix: &str, style: Style, // style for right fill if line contains no emph sections non_emph_style: Style, // style for right fill if line contains emph sections + empty_line_style: Option<Style>, // a style with background color to highlight an empty line background_color_extends_to_terminal_width: Option<bool>, ) { // There's some unfortunate hackery going on here for two reasons: @@ -204,19 +217,14 @@ impl<'a> Painter<'a> { ansi_strings.push(section_style.ansi_term_style.paint(text)); } } - // Set style for the right-fill. - let mut have_background_for_right_fill = false; - if non_emph_style.ansi_term_style.background.is_some() { + let right_fill_background_color = non_emph_style.has_background_color() + && background_color_extends_to_terminal_width + .unwrap_or(config.background_color_extends_to_terminal_width); + if right_fill_background_color { ansi_strings.push(non_emph_style.ansi_term_style.paint("")); - have_background_for_right_fill = true; } let line = &mut ansi_term::ANSIStrings(&ansi_strings).to_string(); - let background_color_extends_to_terminal_width = - match background_color_extends_to_terminal_width { - Some(boolean) => boolean, - None => config.background_color_extends_to_terminal_width, - }; - if background_color_extends_to_terminal_width && have_background_for_right_fill { + if right_fill_background_color { // HACK: How to properly incorporate the ANSI_CSI_CLEAR_TO_EOL into ansi_strings? if line .to_lowercase() @@ -227,6 +235,18 @@ impl<'a> Painter<'a> { output_buffer.push_str(&line); output_buffer.push_str(ANSI_CSI_CLEAR_TO_EOL); output_buffer.push_str(ANSI_SGR_RESET); + } else if line.is_empty() { + if let Some(empty_line_style) = empty_line_style { + output_buffer.push_str( + &empty_line_style + .ansi_term_style + .paint(format!( + "{}{}", + ANSI_CSI_CLEAR_TO_BOL, ANSI_CSI_CURSOR_BACK_1 + )) + .to_string(), + ); + } } else { output_buffer.push_str(&line); } @@ -304,13 +324,17 @@ impl<'a> Painter<'a> { } else { None }; - Self::update_styles(&mut diff_sections.0, minus_non_emph_style); + Self::update_styles(&mut diff_sections.0, None, minus_non_emph_style); let plus_non_emph_style = if config.plus_non_emph_style != config.plus_emph_style { Some(config.plus_non_emph_style) } else { None }; - Self::update_styles(&mut diff_sections.1, plus_non_emph_style); + Self::update_styles( + &mut diff_sections.1, + Some(config.whitespace_error_style), + plus_non_emph_style, + ); diff_sections } @@ -320,15 +344,31 @@ impl<'a> Painter<'a> { /// inferred edit operations and so, if there is a special non-emph style that is /// distinct from the default style, then it should be used for the non-emph style /// sections. - fn update_styles(style_sections: &mut Vec<Vec<(Style, &str)>>, non_emph_style: Option<Style>) { + /// 2. If the line constitutes a whitespace error, then the whitespace error style + /// should be applied to the added material. + fn update_styles( + style_sections: &mut Vec<Vec<(Style, &str)>>, + whitespace_error_style: Option<Style>, + non_emph_style: Option<Style>, + ) { for line_sections in style_sections { let line_has_emph_and_non_emph_sections = style_sections_contain_more_than_one_style(line_sections); let should_update_non_emph_styles = non_emph_style.is_some() && line_has_emph_and_non_emph_sections; + let is_whitespace_error = + whitespace_error_style.is_some() && is_whitespace_error(line_sections); for section in line_sections.iter_mut() { - // Update the style if this is a non-emph section that needs updating. - if should_update_non_emph_styles && !section.0.is_emph { + // If the line as a whole constitutes a whitespace error then highlight this + // section if either (a) it is an emph section, or (b) the line lacks any + // emph/non-emph distinction. + if is_whitespace_error + && (section.0.is_emph || !line_has_emph_and_non_emph_sections) + { + *section = (whitespace_error_style.unwrap(), section.1); + } + // Otherwise, update the style if this is a non-emph section that needs updating. + else if should_update_non_emph_styles && !section.0.is_emph { *section = (non_emph_style.unwrap(), section.1); } } @@ -351,6 +391,20 @@ fn style_sections_contain_more_than_one_style(sections: &Vec<(Style, &str)>) -> } } +lazy_static! { + static ref NON_WHITESPACE_REGEX: Regex = Regex::new(r"\S").unwrap(); +} + +/// True iff the line represented by `sections` constitutes a whitespace error. +// TODO: Git recognizes blank lines at end of file (blank-at-eof) as a whitespace error but delta +// does not yet. +// https://git-scm.com/docs/git-config#Documentation/git-config.txt-corewhitespace +fn is_whitespace_error(sections: &Vec<(Style, &str)>) -> bool { + !sections + .iter() + .any(|(_, s)| NON_WHITESPACE_REGEX.is_match(s)) +} + mod superimpose_style_sections { use syntect::highlighting::Style as SyntectStyle; diff --git a/src/preset.rs b/src/preset.rs index 6ce912c8..7dd13207 100644 --- a/src/preset.rs +++ b/src/preset.rs @@ -536,6 +536,81 @@ mod tests { remove_file(git_config_path).unwrap(); } + #[test] + fn test_whitespace_error_style() { + let git_config_contents = b" +[color \"diff\"] + whitespace = yellow dim ul magenta +"; + let git_config_path = "delta__test_whitespace_error_style.gitconfig"; + + // Git config disabled: hard-coded delta default wins + assert_eq!( + make_config(&[], None, None).whitespace_error_style, + make_style("magenta reverse") + ); + + // Unspecified by user: color.diff.whitespace wins + assert_eq!( + make_config(&[], Some(git_config_contents), Some(git_config_path)) + .whitespace_error_style, + make_style("yellow dim ul magenta") + ); + + // Command line argument wins + assert_eq!( + make_config( + &["--whitespace-error-style", "red reverse"], + Some(git_config_contents), + Some(git_config_path) + ) + .whitespace_error_style, + make_style("reverse red") + ); + + let git_config_contents = b" +[color \"diff\"] + whitespace = yellow dim ul magenta + +[delta] + whitespace-error-style = blue reverse + +[delta \"my-whitespace-error-style-preset\"] + whitespace-error-style = green reverse +"; + + // Command line argument wins + assert_eq!( + make_config( + &["--whitespace-error-style", "red reverse"], + Some(git_config_contents), + Some(git_config_path) + ) + .whitespace_error_style, + make_style("reverse red") + ); + + // No command line argument; main [delta] section wins + assert_eq!( + make_config(&[], Some(git_config_contents), Some(git_config_path)) + .whitespace_error_style, + make_style("blue reverse") + ); + + // No command line argument; preset section wins + assert_eq!( + make_config( + &["--presets", "my-whitespace-error-style-preset"], + Some(git_config_contents), + Some(git_config_path) + ) + .whitespace_error_style, + make_style("reverse green") + ); + + remove_file(git_config_path).unwrap(); + } + fn make_style(s: &str) -> Style { _make_style(s, false) } @@ -559,11 +634,11 @@ mod tests { GitConfig::from_path(&path) } - fn make_config<'a>( + fn make_config( args: &[&str], git_config_contents: Option<&[u8]>, path: Option<&str>, - ) -> config::Config<'a> { + ) -> config::Config { let args: Vec<&str> = itertools::chain( &["/dev/null", "/dev/null", "--24-bit-color", "always"], args, diff --git a/src/set_options.rs b/src/set_options.rs index fe5705c4..b6d8c0ae 100644 --- a/src/set_options.rs +++ b/src/set_options.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use structopt::clap; use crate::cli; +use crate::config; use crate::git_config::{self, GitConfigGet}; use crate::preset::{self, GetValueFunctionFromBuiltinPreset}; @@ -161,6 +162,17 @@ pub fn set_options( if opt.no_gitconfig { return; } + // Handle options which default to an arbitrary git config value. + // TODO: incorporate this logic into the set_options macro. + if !config::user_supplied_option("whitespace-error-style", arg_matches) { + opt.whitespace_error_style = if let Some(git_config) = git_config { + git_config.get::<String>("color.diff.whitespace") + } else { + None + } + .unwrap_or_else(|| "magenta reverse".to_string()) + } + set_options!( [ // --presets must be set first @@ -188,6 +200,7 @@ pub fn set_options( // dynamically to the value of the former. ("minus-style", String, minus_style), ("minus-emph-style", String, minus_emph_style), + ("minus-empty-line-marker-style", String, minus_empty_line_marker_style), ("minus-non-emph-style", String, minus_non_emph_style), ("navigate", bool, navigate), ("number", bool, show_line_numbers), @@ -206,10 +219,12 @@ pub fn set_options( // dynamically to the value of the former. ("plus-style", String, plus_style), ("plus-emph-style", String, plus_emph_style), + ("plus-empty-line-marker-style", String, plus_empty_line_marker_style), ("plus-non-emph-style", String, plus_non_emph_style), ("syntax_theme", Option<String>, syntax_theme), ("tabs", usize, tab_width), ("true-color", String, true_color), + ("whitespace-error-style", String, whitespace_error_style), ("width", Option<String>, width), ("word-diff-regex", String, tokenization_regex), ("zero-style", String, zero_style) diff --git a/src/style.rs b/src/style.rs index 6b11725f..7e7bea35 100644 --- a/src/style.rs +++ b/src/style.rs @@ -49,6 +49,14 @@ impl Style { } } + pub fn has_background_color(&self) -> bool { + if self.ansi_term_style.is_reverse { + self.ansi_term_style.foreground.is_some() + } else { + self.ansi_term_style.background.is_some() + } + } + /// Construct Style from style and decoration-style strings supplied on command line, together /// with defaults. A style string is a space-separated string containing 0, 1, or 2 colors /// (foreground and then background) and an arbitrary number of style attributes. See `delta diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs index 7d0ccf36..1b4a8c82 100644 --- a/src/tests/ansi_test_utils.rs +++ b/src/tests/ansi_test_utils.rs @@ -14,14 +14,31 @@ pub mod ansi_test_utils { expected_style: &str, config: &Config, ) { - _assert_line_has_style( + assert!(_line_has_style( output, line_number, expected_prefix, expected_style, config, false, - ); + )); + } + + pub fn assert_line_does_not_have_style( + output: &str, + line_number: usize, + expected_prefix: &str, + expected_style: &str, + config: &Config, + ) { + assert!(!_line_has_style( + output, + line_number, + expected_prefix, + expected_style, + config, + false, + )); } pub fn assert_line_has_4_bit_color_style( @@ -31,14 +48,14 @@ pub mod ansi_test_utils { expected_style: &str, config: &Config, ) { - _assert_line_has_style( + assert!(_line_has_style( output, line_number, expected_prefix, expected_style, config, true, - ); + )); } pub fn assert_line_has_no_color(output: &str, line_number: usize, expected_prefix: &str) { @@ -114,18 +131,19 @@ pub mod ansi_test_utils { config.null_style, config.null_style, None, + None, ); output_buffer } - fn _assert_line_has_style( + fn _line_has_style( output: &str, line_number: usize, expected_prefix: &str, expected_style: &str, config: &Config, _4_bit_color: bool, - ) { + ) -> bool { let line = output.lines().nth(line_number).unwrap(); assert!(strip_ansi_codes(line).starts_with(expected_prefix)); let mut style = Style::from_str(expected_style, None, None, None, config.true_color, false); @@ -135,7 +153,7 @@ pub mod ansi_test_utils { .foreground .map(ansi_term_fixed_foreground_to_4_bit_color); } - assert!(line.starts_with(&style.ansi_term_style.prefix().to_string())) + line.starts_with(&style.ansi_term_style.prefix().to_string()) } fn ansi_term_fixed_foreground_to_4_bit_color(color: ansi_term::Color) -> ansi_term::Color { diff --git a/src/tests/integration_test_utils.rs b/src/tests/integration_test_utils.rs index 01f08a4f..0259e25d 100644 --- a/src/tests/integration_test_utils.rs +++ b/src/tests/integration_test_utils.rs @@ -8,7 +8,7 @@ pub mod integration_test_utils { use crate::config; use crate::delta::delta; - pub fn make_config<'a>(_args: &[&str]) -> config::Config<'a> { + pub fn make_config(_args: &[&str]) -> config::Config { // FIXME: should not be necessary let (dummy_minus_file, dummy_plus_file) = ("/dev/null", "/ |