Repository: dandavison/delta (rust)

Issue:
🚀 empty line handling and getting rid of the space leftover from -/+
Hi, I use delta as part of git in the terminal. I find it really useful to be able to copy chunks of deleted or added code and paste them in my IDE. Problem is there is always one space before each row. So I have to copy, paste and then remove that space for each line. Is it possible to get rid of this one-character column altogether in order to streamline the copy&paste? As default behavior or as an option?

Pull request:
Drop leading space, highlight empty lines, highlight whitespace errors
Fixes #179 cc @phillipwood

- Unless `--keep-plus-minus-markers` is in effect, drop the first column so that the real code starts in the first column.

- Highlight addition of trailing whitespace as a whitespace error (i.e. git's `blank-at-eol`). Style determined by new option `--whitespace-error-style` which defaults to git's `color.diff.whitespace`.

- Highlight removals and additions of empty lines with background color in the first column if these would not otherwise be visible (i.e. if the applicable style lacks a background color).

- Additions of empty lines at end-of-file are _not_ currently highlighted as whitespace errors, but rather as normal addition-of-empty line. This is contra git's `blank-at-eof` and is a bug to be addressed in subsequent work.

See git documentation: https://git-scm.com/docs/git-config#Documentation/git-config.txt-corewhitespace

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5619288..0682814 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -313,7 +313,7 @@ dependencies = [
  "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "shell-words 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "structopt 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "syntect 4.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -719,17 +719,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "structopt"
-version = "0.3.15"
+version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "structopt-derive 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "structopt-derive"
-version = "0.4.8"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1035,8 +1035,8 @@ dependencies = [
 "checksum shell-words 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
 "checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
 "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-"checksum structopt 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "de2f5e239ee807089b62adce73e48c625e0ed80df02c7ab3f068f5db5281065c"
-"checksum structopt-derive 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "510413f9de616762a4fbeab62509bf15c729603b72d7cd71280fbca431b1c118"
+"checksum structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef"
+"checksum structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a"
 "checksum syn 0.15.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ee06ea4b620ab59a2267c6b48be16244a3389f8bfa0986bdd15c35b890b00af3"
 "checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
 "checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
diff --git a/Cargo.toml b/Cargo.toml
index 519b487..f2a171c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ itertools = "0.9.0"
 lazy_static = "1.4"
 regex = "1.3.9"
 shell-words = "1.0.0"
-structopt = "0.3.15"
+structopt = "0.3.14"
 unicode-segmentation = "1.6.0"
 unicode-width = "0.1.7"
 
diff --git a/src/cli.rs b/src/cli.rs
index 52cbe0e..ec45fff 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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 c5299e1..e05d7b8 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 b498bd0..366520e 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 d1505c7..f946853 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 6ce912c..7dd1320 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 fe5705c..b6d8c0a 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 6b11725..7e7bea3 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


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index 7d0ccf3..1b4a8c8 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 01f08a4..0259e25 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", "/dev/null");
         let mut args = vec![dummy_minus_file, dummy_plus_file];
@@ -20,11 +20,11 @@ pub mod integration_test_utils {
         config::Config::from_args(&args, &mut None)
     }
 
-    pub fn get_line_of_code_from_delta<'a>(
+    pub fn get_line_of_code_from_delta(
         input: &str,
         line_number: usize,
         expected_text: &str,
-        config: &config::Config<'a>,
+        config: &config::Config,
     ) -> String {
         let output = run_delta(&input, config);
         let line_of_code = output.lines().nth(line_number).unwrap();
@@ -32,7 +32,7 @@ pub mod integration_test_utils {
         line_of_code.to_string()
     }
 
-    pub fn run_delta<'a>(input: &str, config: &config::Config<'a>) -> String {
+    pub fn run_delta(input: &str, config: &config::Config) -> String {
         let mut writer: Vec<u8> = Vec::new();
 
         delta(
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index e427d96..668645b 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -2,6 +2,8 @@
 mod tests {
     use console::strip_ansi_codes;
 
+    use crate::paint;
+    use crate::style;
     use crate::tests::ansi_test_utils::ansi_test_utils;
     use crate::tests::integration_test_utils::integration_test_utils;
 
@@ -51,7 +53,7 @@ mod tests {
         let output = integration_test_utils::get_line_of_code_from_delta(
             &ADDED_FILE_INPUT,
             12,
-            " class X:",
+            "class X:",
             &config,
         );
         ansi_test_utils::assert_has_color_other_than_plus_color(&output, &config);
@@ -64,7 +66,7 @@ mod tests {
         let config = integration_test_utils::make_config(&[]);
         let input = ADDED_FILE_INPUT.replace("a.py", "a");
         let output =
-            integration_test_utils::get_line_of_code_from_delta(&input, 12, " class X:", &config);
+            integration_test_utils::get_line_of_code_from_delta(&input, 12, "class X:", &config);
         ansi_test_utils::assert_has_color_other_than_plus_color(&output, &config);
     }
 
@@ -76,7 +78,7 @@ mod tests {
             integration_test_utils::make_config(&["--syntax-theme", "none", "--width", "variable"]);
         let input = ADDED_FILE_INPUT.replace("a.py", "a");
         let output =
-            integration_test_utils::get_line_of_code_from_delta(&input, 12, " class X:", &config);
+            integration_test_utils::get_line_of_code_from_delta(&input, 12, "class X:", &config);
         ansi_test_utils::assert_has_plus_color_only(&output, &config);
     }
 
@@ -92,11 +94,11 @@ mod tests {
         // Line
         assert_eq!(lines.nth(2).unwrap(), "5");
         // Change
-        assert_eq!(lines.nth(2).unwrap(), " println!(\"Hello ruster\");");
+        assert_eq!(lines.nth(2).unwrap(), "println!(\"Hello ruster\");");
         // Next chunk
         assert_eq!(lines.nth(2).unwrap(), "43");
         // Unchanged in second chunk
-        assert_eq!(lines.nth(2).unwrap(), " Unchanged");
+        assert_eq!(lines.nth(2).unwrap(), "Unchanged");
     }
 
     #[test]
@@ -114,7 +116,7 @@ mod tests {
         // Line number
         assert_eq!(lines.nth(2).unwrap(), "1");
         // Change
-        assert_eq!(lines.nth(2).unwrap(), " This is different from b");
+        assert_eq!(lines.nth(2).unwrap(), "This is different from b");
         // File uniqueness
         assert_eq!(lines.nth(2).unwrap(), "Only in a/: just_a");
         // FileMeta divider
@@ -191,9 +193,7 @@ mod tests {
         let output =
             integration_test_utils::run_delta(TRIPLE_DASH_AT_BEGINNING_OF_LINE_IN_CODE, &config);
         let output = strip_ansi_codes(&output);
-        assert!(
-            output.contains(" -- instance (Category p, Category q) => Category (p ∧ q) where\n")
-        );
+        assert!(output.contains("-- instance (Category p, Category q) => Category (p ∧ q) where\n"));
     }
 
     #[test]
@@ -209,8 +209,8 @@ mod tests {
         let config = integration_test_utils::make_config(&[]);
         let output = integration_test_utils::run_delta(DIFF_IN_DIFF, &config);
         let output = strip_ansi_codes(&output);
-        assert!(output.contains("\n ---\n"));
-        assert!(output.contains("\n Subject: [PATCH] Init\n"));
+        assert!(output.contains("\n---\n"));
+        assert!(output.contains("\nSubject: [PATCH] Init\n"));
     }
 
     #[test]
@@ -806,7 +806,7 @@ src/align.rs
         );
         // An additional newline is inserted under anything other than `style=raw,
         // decoration-style=omit`, to better separate the hunks. Hence 9 + 1.
-        ansi_test_utils::assert_line_has_no_color(&output, 9 + 1, " impl<'a> Alignment<'a> {");
+        ansi_test_utils::assert_line_has_no_color(&output, 9 + 1, "impl<'a> Alignment<'a> {");
     }
 
     #[test]
@@ -849,12 +849,12 @@ src/align.rs
         let config = integration_test_utils::make_config(args);
         let output = integration_test_utils::run_delta(GIT_DIFF_SINGLE_HUNK, &config);
         let output = strip_ansi_codes(&output);
-        assert!(output.contains(" impl<'a> Alignment<'a> {"));
-        assert!(!output.contains(" impl<'a> Alignment<'a> { │"));
+        assert!(output.contains("impl<'a> Alignment<'a> {"));
+        assert!(!output.contains("impl<'a> Alignment<'a> { │"));
         assert!(!output.contains(
             "
- impl<'a> Alignment<'a> {
-─────────────────────────"
+impl<'a> Alignment<'a> {
+────────────────────────"
         ));
     }
 
@@ -879,7 +879,7 @@ src/align.rs
     fn _do_test_hunk_header_empty_style(args: &[&str]) {
         let config = integration_test_utils::make_config(args);
         let output = integration_test_utils::run_delta(GIT_DIFF_SINGLE_HUNK, &config);
-        assert!(output.contains(" impl<'a> Alignment<'a> {"));
+        assert!(output.contains("impl<'a> Alignment<'a> {"));
         assert!(!output.contains("@@"));
     }
 
@@ -899,23 +899,23 @@ src/align.rs
         ansi_test_utils::assert_line_has_style(
             &output,
             10,
-            "──────────────────────────┐",
+            "─────────────────────────┐",
             "white",
             &config,
         );
         ansi_test_utils::assert_line_has_style(
             &output,
             12,
-            "──────────────────────────┘",
+            "─────────────────────────┘",
             "white",
             &config,
         );
         let output = strip_ansi_codes(&output);
         assert!(output.contains(
             "
-──────────────────────────┐
- impl<'a> Alignment<'a> { │
-──────────────────────────┘
+─────────────────────────┐
+impl<'a> Alignment<'a> { │
+─────────────────────────┘
 "
         ));
     }
@@ -974,8 +974,8 @@ src/align.rs
         let output = strip_ansi_codes(&output);
         assert!(output.contains(
             "
- impl<'a> Alignment<'a> { 
-─────────────────────────"
+impl<'a> Alignment<'a> { 
+────────────────────────"
         ));
     }
 
@@ -990,25 +990,142 @@ src/align.rs
             "box",
         ]);
         let output = integration_test_utils::run_delta(GIT_DIFF_SINGLE_HUNK, &config);
-        ansi_test_utils::assert_line_has_no_color(&output, 10, "──────────────────────────┐");
+        ansi_test_utils::assert_line_has_no_color(&output, 10, "─────────────────────────┐");
         ansi_test_utils::assert_line_is_syntax_highlighted(
             &output,
             11,
-            " impl<'a> Alignment<'a> { ",
+            "impl<'a> Alignment<'a> { ",
             "rs",
             &config,
         );
-        ansi_test_utils::assert_line_has_no_color(&output, 12, "──────────────────────────┘");
+        ansi_test_utils::assert_line_has_no_color(&output, 12, "─────────────────────────┘");
         let output = strip_ansi_codes(&output);
         assert!(output.contains(
             "
-──────────────────────────┐
- impl<'a> Alignment<'a> { │
-──────────────────────────┘
+─────────────────────────┐
+impl<'a> Alignment<'a> { │
+─────────────────────────┘
 "
         ));
     }
 
+    #[test]
+    fn test_removed_empty_line_highlight() {
+        let minus_empty_line_marker_style = "bold yellow magenta ul";
+        _do_test_removed_empty_line_highlight(minus_empty_line_marker_style, "red reverse", true);
+        _do_test_removed_empty_line_highlight(minus_empty_line_marker_style, "normal red", true);
+        _do_test_removed_empty_line_highlight(minus_empty_line_marker_style, "red", false);
+        _do_test_removed_empty_line_highlight(
+            minus_empty_line_marker_style,
+            "normal red reverse",
+            false,
+        );
+    }
+
+    fn _do_test_removed_empty_line_highlight(
+        empty_line_marker_style: &str,
+        base_style: &str,
+        base_style_has_background_color: bool,
+    ) {
+        _do_test_empty_line_highlight(
+            "--minus-empty-line-marker-style",
+            empty_line_marker_style,
+            "--minus-style",
+            base_style,
+            base_style_has_background_color,
+            DIFF_WITH_REMOVED_EMPTY_LINE,
+        );
+    }
+
+    #[test]
+    fn test_added_empty_line_highlight() {
+        let plus_empty_line_marker_style = "bold yellow magenta ul";
+        _do_test_added_empty_line_highlight(plus_empty_line_marker_style, "green reverse", true);
+        _do_test_added_empty_line_highlight(plus_empty_line_marker_style, "normal green", true);
+        _do_test_added_empty_line_highlight(plus_empty_line_marker_style, "green", false);
+        _do_test_added_empty_line_highlight(
+            plus_empty_line_marker_style,
+            "normal green reverse",
+            false,
+        );
+    }
+
+    fn _do_test_added_empty_line_highlight(
+        empty_line_marker_style: &str,
+        base_style: &str,
+        base_style_has_background_color: bool,
+    ) {
+        _do_test_empty_line_highlight(
+            "--plus-empty-line-marker-style",
+            empty_line_marker_style,
+            "--plus-style",
+            base_style,
+            base_style_has_background_color,
+            DIFF_WITH_ADDED_EMPTY_LINE,
+        );
+    }
+
+    fn _do_test_empty_line_highlight(
+        empty_line_marker_style_name: &str,
+        empty_line_marker_style: &str,
+        base_style_name: &str,
+        base_style: &str,
+        base_style_has_background_color: bool,
+        example_diff: &str,
+    ) {
+        let config = integration_test_utils::make_config(&[
+            base_style_name,
+            base_style,
+            empty_line_marker_style_name,
+            empty_line_marker_style,
+        ]);
+        let output = integration_test_utils::run_delta(example_diff, &config);
+        let line = output.lines().nth(6).unwrap();
+        if base_style_has_background_color {
+            let style = style::Style::from_str(base_style, None, None, None, true, false);
+            assert_eq!(
+                line,
+                &style
+                    .ansi_term_style
+                    .paint(paint::ANSI_CSI_CLEAR_TO_EOL)
+                    .to_string()
+            );
+        } else {
+            let style =
+                style::Style::from_str(empty_line_marker_style, None, None, None, true, false);
+            assert_eq!(
+                line,
+                &style
+                    .ansi_term_style
+                    .paint(format!(
+                        "{}{}",
+                        paint::ANSI_CSI_CLEAR_TO_BOL,
+                        paint::ANSI_CSI_CURSOR_BACK_1
+                    ))
+                    .to_string()
+            );
+        }
+    }
+
+    #[test]
+    fn test_whitespace_error() {
+        let whitespace_error_style = "bold yellow magenta ul";
+        let config = integration_test_utils::make_config(&[
+            "--whitespace-error-style",
+            whitespace_error_style,
+        ]);
+        let output = integration_test_utils::run_delta(DIFF_WITH_WHITESPACE_ERROR, &config);
+        ansi_test_utils::assert_line_has_style(&output, 6, " ", whitespace_error_style, &config);
+        let output = integration_test_utils::run_delta(DIFF_WITH_REMOVED_WHITESPACE_ERROR, &config);
+        ansi_test_utils::assert_line_does_not_have_style(
+            &output,
+            6,
+            " ",
+            whitespace_error_style,
+            &config,
+        );
+    }
+
     const GIT_DIFF_SINGLE_HUNK: &str = "\
 commit 94907c0f136f46dc46ffae2dc92dca9af7eb7c2e
 Author: Dan Davison <dandavison7@gmail.com>
@@ -1444,4 +1561,42 @@ index cba6064..ba1a4de 100644
 - Co
 + let col = Co
 "#;
+
+    const DIFF_WITH_REMOVED_EMPTY_LINE: &str = r"
+diff --git i/a w/a
+index 8b13789..e69de29 100644
+--- i/a
++++ w/a
+@@ -1 +0,0 @@
+-
+";
+
+    const DIFF_WITH_ADDED_EMPTY_LINE: &str = r"
+diff --git i/a w/a
+index e69de29..8b13789 100644
+--- i/a
++++ w/a
+@@ -0,0 +1 @@
++
+";
+
+    const DIFF_WITH_WHITESPACE_ERROR: &str = r"
+diff --git c/a i/a
+new file mode 100644
+index 0000000..8d1c8b6
+--- /dev/null
++++ i/a
+@@ -0,0 +1 @@
++ 
+";
+
+    const DIFF_WITH_REMOVED_WHITESPACE_ERROR: &str = r"
+diff --git i/a w/a
+index 8d1c8b6..8b13789 100644
+--- i/a
++++ w/a
+@@ -1 +1 @@
+- 
++
+";
 }