Repository: dandavison/delta
Pull request #642 (2021-06-24 21:41:48)

Fix empty line highlighting

dandavison: From offline discussion: consider the following diff

```
diff·--git·a/a·b/b␊
index·8cb7cb9..e69de29·100644␊
---·a/a␊
+++·b/b␊
@@·-1,2·+0,0·@@␊
-·␊
-␊
```

Prior to this PR, under `side-by-side`, the line that contained no whitespace is highlighted as a single removed block:

![image](https://user-images.githubusercontent.com/52205/130307626-a358968c-f86a-47d1-97a5-e799d1bb78b3.png)


With this PR, it is styled the same as the line that did have some whitespace:

![image](https://user-images.githubusercontent.com/52205/130307636-f4d29645-7a68-4944-be89-5dd3f0a6023c.png)


Patch:
diff --git a/src/cli.rs b/src/cli.rs
index 1b88358..c711673 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -13,7 +13,7 @@ use crate::bat_utils::output::PagingMode;
 use crate::git_config::{GitConfig, GitConfigEntry};
 use crate::options;
 
-#[derive(StructOpt, Clone, Default)]
+#[derive(StructOpt, Default)]
 #[structopt(
     name = "delta",
     about = "A viewer for git and diff output",
@@ -678,13 +678,13 @@ impl Default for PagingMode {
 
 impl Opt {
     pub fn from_args_and_git_config(
-        git_config: &mut Option<GitConfig>,
+        git_config: Option<GitConfig>,
         assets: HighlightingAssets,
     ) -> Self {
         Self::from_clap_and_git_config(Self::clap().get_matches(), git_config, assets)
     }
 
-    pub fn from_iter_and_git_config<I>(iter: I, git_config: &mut Option<GitConfig>) -> Self
+    pub fn from_iter_and_git_config<I>(iter: I, git_config: Option<GitConfig>) -> Self
     where
         I: IntoIterator,
         I::Item: Into<OsString> + Clone,
@@ -695,12 +695,12 @@ impl Opt {
 
     fn from_clap_and_git_config(
         arg_matches: clap::ArgMatches,
-        git_config: &mut Option<GitConfig>,
+        mut git_config: Option<GitConfig>,
         assets: HighlightingAssets,
     ) -> Self {
         let mut opt = Opt::from_clap(&arg_matches);
         options::rewrite::apply_rewrite_rules(&mut opt, &arg_matches);
-        options::set::set_options(&mut opt, git_config, &arg_matches, assets);
+        options::set::set_options(&mut opt, &mut git_config, &arg_matches, assets);
         opt
     }
 
diff --git a/src/features/mod.rs b/src/features/mod.rs
index 1fe2091..8b169fc 100644
--- a/src/features/mod.rs
+++ b/src/features/mod.rs
@@ -102,7 +102,7 @@ pub mod tests {
         let builtin_features = make_builtin_features();
         let mut args = vec!["delta".to_string()];
         args.extend(builtin_features.keys().map(|s| format!("--{}", s)));
-        let opt = cli::Opt::from_iter_and_git_config(args, &mut None);
+        let opt = cli::Opt::from_iter_and_git_config(args, None);
         let features: HashSet<&str> = opt.features.split_whitespace().collect();
         for feature in builtin_features.keys() {
             assert!(features.contains(feature.as_str()))
diff --git a/src/main.rs b/src/main.rs
index 1b77808..3fb223e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -58,7 +58,7 @@ pub mod errors {
 // arguments and without standard input; 2 is used to report a real problem.
 fn run_app() -> std::io::Result<i32> {
     let assets = HighlightingAssets::new();
-    let opt = cli::Opt::from_args_and_git_config(&mut git_config::GitConfig::try_create(), assets);
+    let opt = cli::Opt::from_args_and_git_config(git_config::GitConfig::try_create(), assets);
 
     if opt.list_languages {
         list_languages()?;
@@ -351,19 +351,17 @@ fn show_themes(dark: bool, light: bool, computed_theme_is_light: bool) -> std::i
         }
     };
 
-    let mut git_config = git_config::GitConfig::try_create();
-    let opt = cli::Opt::from_iter_and_git_config(
-        &["", "", "--navigate", "--show-themes"],
-        &mut git_config,
-    );
+    let git_config = git_config::GitConfig::try_create();
+    let opt =
+        cli::Opt::from_iter_and_git_config(&["", "", "--navigate", "--show-themes"], git_config);
     let mut output_type =
         OutputType::from_mode(PagingMode::Always, None, &config::Config::from(opt)).unwrap();
     let title_style = ansi_term::Style::new().bold();
     let writer = output_type.handle().unwrap();
 
     for theme in &get_themes(git_config::GitConfig::try_create()) {
-        let opt =
-            cli::Opt::from_iter_and_git_config(&["", "", "--features", theme], &mut git_config);
+        let git_config = git_config::GitConfig::try_create();
+        let opt = cli::Opt::from_iter_and_git_config(&["", "", "--features", theme], git_config);
         let is_dark_theme = opt.dark;
         let is_light_theme = opt.light;
         let config = config::Config::from(opt);
@@ -389,7 +387,6 @@ fn show_themes(dark: bool, light: bool, computed_theme_is_light: bool) -> std::i
 
 #[cfg(not(tarpaulin_include))]
 fn show_syntax_themes() -> std::io::Result<()> {
-    let mut opt = cli::Opt::from_args();
     let assets = HighlightingAssets::new();
     let mut output_type = OutputType::from_mode(
         PagingMode::QuitIfOneScreen,
@@ -398,7 +395,6 @@ fn show_syntax_themes() -> std::io::Result<()> {
     )
     .unwrap();
     let mut writer = output_type.handle().unwrap();
-    opt.computed.syntax_set = assets.syntax_set;
 
     let stdin_data = if !atty::is(atty::Stream::Stdin) {
         let mut buf = Vec::new();
@@ -412,9 +408,16 @@ fn show_syntax_themes() -> std::io::Result<()> {
         None
     };
 
+    let make_opt = || {
+        let mut opt = cli::Opt::from_args();
+        opt.computed.syntax_set = assets.syntax_set.clone();
+        opt
+    };
+    let opt = make_opt();
+
     if !(opt.dark || opt.light) {
-        _show_syntax_themes(opt.clone(), false, &mut writer, stdin_data.as_ref())?;
-        _show_syntax_themes(opt, true, &mut writer, stdin_data.as_ref())?;
+        _show_syntax_themes(opt, false, &mut writer, stdin_data.as_ref())?;
+        _show_syntax_themes(make_opt(), true, &mut writer, stdin_data.as_ref())?;
     } else if opt.light {
         _show_syntax_themes(opt, true, &mut writer, stdin_data.as_ref())?;
     } else {
diff --git a/src/paint.rs b/src/paint.rs
index 3b5010b..0e56923 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -381,9 +381,9 @@ impl<'a> Painter<'a> {
         painted_prefix: Option<ansi_term::ANSIString>,
         config: &config::Config,
     ) -> (String, bool) {
-        let output_line_numbers = config.line_numbers && line_numbers_data.is_some();
-        let mut handled_prefix = false;
         let mut ansi_strings = Vec::new();
+
+        let output_line_numbers = config.line_numbers && line_numbers_data.is_some();
         if output_line_numbers {
             ansi_strings.extend(line_numbers::format_and_paint_line_numbers(
                 line_numbers_data.as_mut().unwrap(),
@@ -407,27 +407,40 @@ impl<'a> Painter<'a> {
             }
             _ => {}
         }
-        let mut is_empty = true;
-        for (section_style, mut text) in superimpose_style_sections(
+
+        let superimposed = superimpose_style_sections(
             syntax_sections,
             diff_sections,
             config.true_color,
             config.null_syntect_style,
-        ) {
-            if !handled_prefix {
-                if let Some(painted_prefix) = painted_prefix.clone() {
-                    ansi_strings.push(painted_prefix);
+        );
+
+        let mut handled_prefix = false;
+        for (section_style, text) in &superimposed {
+            let text = if handled_prefix {
+                &text
+            } else {
+                // Remove what was originally the +/- prefix, see `prepare()`, after
+                // (if requested) re-inserting it with proper styling.
+                if let Some(ref painted_prefix) = painted_prefix {
+                    ansi_strings.push(painted_prefix.clone());
                 }
+
                 if !text.is_empty() {
-                    text.remove(0);
+                    &text[1..]
+                } else {
+                    &text
                 }
-                handled_prefix = true;
-            }
+            };
+
             if !text.is_empty() {
                 ansi_strings.push(section_style.paint(text));
-                is_empty = false;
             }
+            handled_prefix = true;
         }
+
+        // Only if syntax is empty (implies diff empty) can a line actually be empty.
+        let is_empty = syntax_sections.is_empty();
         (ansi_term::ANSIStrings(&ansi_strings).to_string(), is_empty)
     }
 


Test patch:
diff --git a/src/tests/integration_test_utils.rs b/src/tests/integration_test_utils.rs
index 7cab6f8..bdb2851 100644
--- a/src/tests/integration_test_utils.rs
+++ b/src/tests/integration_test_utils.rs
@@ -38,14 +38,14 @@ fn _make_options_from_args_and_git_config(
     let mut args: Vec<&str> = itertools::chain(&["/dev/null", "/dev/null"], args)
         .map(|s| *s)
         .collect();
-    let mut git_config = match (git_config_contents, git_config_path) {
+    let git_config = match (git_config_contents, git_config_path) {
         (Some(contents), Some(path)) => Some(make_git_config(contents, path, honor_env_var)),
         _ => {
             args.push("--no-gitconfig");
             None
         }
     };
-    cli::Opt::from_iter_and_git_config(args, &mut git_config)
+    cli::Opt::from_iter_and_git_config(args, git_config)
 }
 
 pub fn make_options_from_args(args: &[&str]) -> cli::Opt {
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index c605715..3f28d20 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -1472,14 +1472,7 @@ src/align.rs:71: impl<'a> Alignment<'a> { │
                     .to_string()
             );
         } else {
-            let style = style::Style::from_str(empty_line_marker_style, None, None, true, false);
-            assert_eq!(
-                line,
-                &style
-                    .ansi_term_style
-                    .paint(ansi::ANSI_CSI_CLEAR_TO_BOL)
-                    .to_string()
-            );
+            assert_eq!(line, "");
         }
     }