Repository: dandavison/delta
Pull request #239 (2020-07-06 08:08:23)

Bump dirs from 3.0.0 to 3.0.1
Bumps [dirs](https://github.com/soc/dirs-rs) from 3.0.0 to 3.0.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/soc/dirs-rs/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://api.dependabot.com/badges/compatibility_score?dependency-name=dirs&package-manager=cargo&previous-version=3.0.0&new-version=3.0.1)](https://dependabot.com/compatibility-score/?dependency-name=dirs&package-manager=cargo&previous-version=3.0.0&new-version=3.0.1)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
- `@dependabot badge me` will comment on this PR with code to add a "Dependabot enabled" badge to your readme

Additionally, you can set the following in your Dependabot [dashboard](https://app.dependabot.com):
- Update frequency (including time of day and day of week)
- Pull request limits (per update run and/or open at any time)
- Out-of-range updates (receive only lockfile updates, if desired)
- Security updates (receive only security updates, if desired)



</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ff9335e..92b8dc4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -218,10 +218,9 @@ dependencies = [
 
 [[package]]
 name = "dirs"
-version = "3.0.0"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -296,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "git-delta"
-version = "0.2.0"
+version = "0.2.1"
 dependencies = [
  "ansi_colours 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -305,7 +304,7 @@ dependencies = [
  "box_drawing 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytelines 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "console 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "dirs 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dirs 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "git2 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -973,7 +972,7 @@ dependencies = [
 "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120"
 "checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
 "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
-"checksum dirs 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2fddc3610d8f9552384e06ebc87f714e1d0b2b64a99194d2faf36d7ae5f48549"
+"checksum dirs 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
 "checksum dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
 "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
 "checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd"
diff --git a/Cargo.toml b/Cargo.toml
index a22f6a3..8637dee 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ homepage = "https://github.com/dandavison/delta"
 license = "MIT"
 readme = "README.md"
 repository = "https://github.com/dandavison/delta"
-version = "0.2.0"
+version = "0.2.1"
 
 [[bin]]
 name = "delta"
diff --git a/README.md b/README.md
index a91f1ac..0a4553e 100644
--- a/README.md
+++ b/README.md
@@ -211,13 +211,13 @@ Set delta to be git's pager in your `.gitconfig`. Delta accepts many command lin
 [delta]
     plus-color = "#012800"
     minus-color = "#340001"
-    theme = 'Monokai Extended'
+    syntax-theme = Monokai Extended
 
 [interactive]
     diffFilter = delta --color-only
 ```
 
-Note that delta argument values in ~/.gitconfig should be in double quotes, like `--minus-color="#340001"`. For a theme name containing a space, use single quotes, like `--theme='Monokai Extended'`.
+Note that delta color argument values in ~/.gitconfig should be in double quotes, like `--minus-color="#340001"`. For theme names and other values, do not use quotes as they will be passed on to delta, like `theme = Monokai Extended`.
 
 All git commands that display diff output should now display syntax-highlighted output. For example:
   - `git diff`
diff --git a/src/bat/.travis.yml b/src/bat/.travis.yml
index 4383acf..5a62d75 100644
--- a/src/bat/.travis.yml
+++ b/src/bat/.travis.yml
@@ -7,7 +7,7 @@ matrix:
       env: TARGET=x86_64-unknown-linux-gnu
     - os: windows
       rust: stable
-      env: TARGET=x86_64-pc-windows-gnu
+      env: TARGET=x86_64-pc-windows-msvc
     - os: linux
       rust: stable
       env:
diff --git a/src/cli.rs b/src/cli.rs
index f9dfdb8..d4ad237 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -180,7 +180,7 @@ of the line number columns. Their values are arbitrary format strings, which are
 the placeholders {nm} for the line number associated with the old version of the file and {np} for
 the line number associated with the new version of the file. The placeholders support a subset of
 the string formatting syntax documented here: https://doc.rust-lang.org/std/fmt/#formatting-parameters.
-Specifically, you can use the alignment, width, and fill syntax.
+Specifically, you can use the alignment and width syntax.
 
 For example, the default value of --line-numbers-left-format is '{nm:^4}⋮'. This means that the
 left column should display the minus line number (nm), center-aligned, padded with spaces to a
diff --git a/src/delta.rs b/src/delta.rs
index 73c94bf..4d88f65 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -71,7 +71,7 @@ where
             source = detect_source(&line);
         }
         if line.starts_with("commit ") {
-            painter.paint_buffered_lines();
+            painter.paint_buffered_minus_and_plus_lines();
             state = State::CommitMeta;
             if should_handle(&state, config) {
                 painter.emit()?;
@@ -79,7 +79,7 @@ where
                 continue;
             }
         } else if line.starts_with("diff ") {
-            painter.paint_buffered_lines();
+            painter.paint_buffered_minus_and_plus_lines();
             state = State::FileMeta;
         } else if (state == State::FileMeta || source == Source::DiffUnified)
             && (line.starts_with("--- ") || line.starts_with("rename from "))
@@ -136,7 +136,7 @@ where
             // proposal for more robust parsing logic.
 
             state = State::FileMeta;
-            painter.paint_buffered_lines();
+            painter.paint_buffered_minus_and_plus_lines();
             if should_handle(&State::FileMeta, config) {
                 painter.emit()?;
                 handle_generic_file_meta_header_line(&mut painter, &line, &raw_line, config)?;
@@ -159,7 +159,7 @@ where
         }
     }
 
-    painter.paint_buffered_lines();
+    painter.paint_buffered_minus_and_plus_lines();
     painter.emit()?;
     Ok(())
 }
@@ -373,8 +373,7 @@ fn handle_hunk_header_line(
         }
     };
     let (raw_code_fragment, line_numbers) = parse::parse_hunk_header(&line);
-    painter.minus_line_number = line_numbers[0].0;
-    painter.plus_line_number = line_numbers[line_numbers.len() - 1].0;
+    // Emit the hunk header, with any requested decoration
     if config.hunk_header_style.is_raw {
         writeln!(painter.writer)?;
         draw_fn(
@@ -386,7 +385,7 @@ fn handle_hunk_header_line(
             decoration_ansi_term_style,
         )?;
     } else {
-        let line = match prepare(&raw_code_fragment, false, config) {
+        let line = match painter.prepare(&raw_code_fragment, false) {
             s if s.len() > 0 => format!("{} ", s),
             s => s,
         };
@@ -402,9 +401,10 @@ fn handle_hunk_header_line(
             Painter::paint_lines(
                 syntax_style_sections,
                 vec![vec![(config.hunk_header_style, &lines[0])]],
-                vec![None],
+                &State::HunkHeader,
                 &mut painter.output_buffer,
                 config,
+                &mut None,
                 "",
                 config.null_style,
                 config.null_style,
@@ -425,12 +425,18 @@ fn handle_hunk_header_line(
             };
         }
     };
-
-    if !config.line_numbers {
-        let line_number = &format!("{}", painter.plus_line_number);
+    // Emit a single line number, or prepare for full line-numbering
+    if config.line_numbers {
+        painter.line_numbers_data.initialize_hunk(line_numbers);
+    } else {
+        let plus_line_number = line_numbers[line_numbers.len() - 1].0;
         match config.hunk_header_style.decoration_ansi_term_style() {
-            Some(style) => writeln!(painter.writer, "{}", style.paint(line_number))?,
-            None => writeln!(painter.writer, "{}", line_number)?,
+            Some(style) => writeln!(
+                painter.writer,
+                "{}",
+                style.paint(format!("{}", plus_line_number))
+            )?,
+            None => writeln!(painter.writer, "{}", plus_line_number)?,
         }
     }
     Ok(())
@@ -455,104 +461,35 @@ fn handle_hunk_line(
     if painter.minus_lines.len() > config.max_buffered_lines
         || painter.plus_lines.len() > config.max_buffered_lines
     {
-        painter.paint_buffered_lines();
+        painter.paint_buffered_minus_and_plus_lines();
     }
     match line.chars().next() {
         Some('-') => {
             if state == State::HunkPlus {
-                painter.paint_buffered_lines();
+                painter.paint_buffered_minus_and_plus_lines();
             }
-            painter.minus_lines.push(prepare(&line, true, config));
+            painter.minus_lines.push(painter.prepare(&line, true));
             State::HunkMinus
         }
         Some('+') => {
-            painter.plus_lines.push(prepare(&line, true, config));
+            painter.plus_lines.push(painter.prepare(&line, true));
             State::HunkPlus
         }
         Some(' ') => {
-            let state = State::HunkZero;
-            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(
-                &lines,
-                &state,
-                &mut painter.highlighter,
-                &painter.config,
-            );
-            let diff_style_sections = vec![(config.zero_style, lines[0].as_str())];
-
-            Painter::paint_lines(
-                syntax_style_sections,
-                vec![diff_style_sections],
-                vec![Some((
-                    Some(painter.minus_line_number),
-                    Some(painter.plus_line_number),
-                ))],
-                &mut painter.output_buffer,
-                config,
-                prefix,
-                config.zero_style,
-                config.zero_style,
-                None,
-                None,
-            );
-            painter.minus_line_number += 1;
-            painter.plus_line_number += 1;
-            state
+            painter.paint_buffered_minus_and_plus_lines();
+            painter.paint_zero_line(&line);
+            State::HunkZero
         }
         _ => {
             // The first character here could be e.g. '\' from '\ No newline at end of file'. This
             // is not a hunk line, but the parser does not have a more accurate state corresponding
             // to this.
-            painter.paint_buffered_lines();
+            painter.paint_buffered_minus_and_plus_lines();
             painter
                 .output_buffer
-                .push_str(&expand_tabs(raw_line.graphemes(true), config.tab_width));
+                .push_str(&painter.expand_tabs(raw_line.graphemes(true)));
             painter.output_buffer.push_str("\n");
             State::HunkZero
         }
     }
 }
-
-/// Replace initial -/+ character with ' ', expand tabs as spaces, and optionally terminate with
-/// newline.
-// Terminating with newline character is necessary for many of the sublime syntax definitions to
-// highlight correctly.
-// See https://docs.rs/syntect/3.2.0/syntect/parsing/struct.SyntaxSetBuilder.html#method.add_from_folder
-fn prepare(line: &str, append_newline: bool, config: &Config) -> String {
-    let terminator = if append_newline { "\n" } else { "" };
-    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. When emitting the line
-        // in Painter::paint_lines, we drop the space (unless --keep-plus-minus-markers is in
-        // effect in which case we replace it with the appropriate marker).
-        // TODO: Things should, but do not, work if this leading space is omitted at this stage.
-        // See comment in align::Alignment::new.
-        line.next();
-        format!(" {}{}", expand_tabs(line, config.tab_width), terminator)
-    } else {
-        terminator.to_string()
-    }
-}
-
-/// Expand tabs as spaces.
-/// tab_width = 0 is documented to mean do not replace tabs.
-fn expand_tabs<'a, I>(line: I, tab_width: usize) -> String
-where
-    I: Iterator<Item = &'a str>,
-{
-    if tab_width > 0 {
-        let tab_replacement = " ".repeat(tab_width);
-        line.map(|s| if s == "\t" { &tab_replacement } else { s })
-            .collect::<String>()
-    } else {
-        line.collect::<String>()
-    }
-}
diff --git a/src/edits.rs b/src/edits.rs
index b5ac232..b05505e 100644
--- a/src/edits.rs
+++ b/src/edits.rs
@@ -30,11 +30,11 @@ where
     let mut annotated_minus_lines = Vec::<Vec<(EditOperation, &str)>>::new();
     let mut annotated_plus_lines = Vec::<Vec<(EditOperation, &str)>>::new();
 
-    let mut emitted = 0; // plus lines emitted so far
+    let mut plus_index = 0; // plus lines emitted so far
 
     'minus_lines_loop: for minus_line in minus_lines {
         let mut considered = 0; // plus lines considered so far as match for minus_line
-        for plus_line in &plus_lines[emitted..] {
+        for plus_line in &plus_lines[plus_index..] {
             let alignment = align::Alignment::new(
                 tokenize(minus_line, tokenization_regex),
                 tokenize(plus_line, tokenization_regex),
@@ -55,13 +55,13 @@ where
                 // minus_line and plus_line are inferred to be a homologous pair.
 
                 // Emit as unpaired the plus lines already considered and rejected
-                for plus_line in &plus_lines[emitted..(emitted + considered)] {
+                for plus_line in &plus_lines[plus_index..(plus_index + considered)] {
                     annotated_plus_lines.push(vec![(noop_insertion, plus_line)]);
                 }
-                emitted += considered;
+                plus_index += considered;
                 annotated_minus_lines.push(annotated_minus_line);
                 annotated_plus_lines.push(annotated_plus_line);
-                emitted += 1;
+                plus_index += 1;
 
                 // Greedy: move on to the next minus line.
                 continue 'minus_lines_loop;
@@ -73,7 +73,7 @@ where
         annotated_minus_lines.push(vec![(noop_deletion, minus_line)]);
     }
     // Emit any remaining plus lines
-    for plus_line in &plus_lines[emitted..] {
+    for plus_line in &plus_lines[plus_index..] {
         annotated_plus_lines.push(vec![(noop_insertion, plus_line)]);
     }
 
diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs
index 3fe9513..332db85 100644
--- a/src/features/line_numbers.rs
+++ b/src/features/line_numbers.rs
@@ -1,8 +1,11 @@
+use std::cmp::max;
+
 use ansi_term;
 use lazy_static::lazy_static;
 use regex::Regex;
 
 use crate::config;
+use crate::delta::State;
 use crate::features::OptionValueFunction;
 use crate::style::Style;
 
@@ -58,40 +61,56 @@ pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
 /// 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>)>,
+    line_numbers_data: &'a mut LineNumbersData,
+    state: &State,
     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_number_style, plus_number_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 m_ref = &mut line_numbers_data.hunk_minus_line_number;
+    let p_ref = &mut line_numbers_data.hunk_plus_line_number;
+    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 => {
+            let m = *m_ref;
+            *m_ref += 1;
+            ((Some(m), None), (minus_style, plus_style))
+        }
+        State::HunkZero => {
+            let (m, p) = (*m_ref, *p_ref);
+            *m_ref += 1;
+            *p_ref += 1;
+            ((Some(m), Some(p)), (zero_style, zero_style))
+        }
+        State::HunkPlus => {
+            let p = *p_ref;
+            *p_ref += 1;
+            ((None, Some(p)), (minus_style, plus_style))
+        }
+        _ => return Vec::new(),
     };
 
     let mut formatted_numbers = Vec::new();
 
     formatted_numbers.extend(format_and_paint_line_number_field(
-        &config.line_numbers_left_format,
+        &line_numbers_data.left_format_data,
         &config.line_numbers_left_style,
         minus_number,
         plus_number,
-        &minus_number_style,
-        &plus_number_style,
+        line_numbers_data.hunk_max_line_number_width,
+        &minus_style,
+        &plus_style,
     ));
     formatted_numbers.extend(format_and_paint_line_number_field(
-        &config.line_numbers_right_format,
+        &line_numbers_data.right_format_data,
         &config.line_numbers_right_style,
         minus_number,
         plus_number,
-        &minus_number_style,
-        &plus_number_style,
+        line_numbers_data.hunk_max_line_number_width,
+        &minus_style,
+        &plus_style,
     ));
 
     formatted_numbers
@@ -105,7 +124,7 @@ lazy_static! {
 (?:             # Start optional format spec (non-capturing)
   :             #     Literal colon
   (?:           #     Start optional fill/alignment spec (non-capturing)
-    ([^<^>])?   #         2: Optional fill character
+    ([^<^>])?   #         2: Optional fill character (ignored)
     ([<^>])     #         3: Alignment spec
   )?            #
   (\d+)         #     4: Width
@@ -116,51 +135,122 @@ lazy_static! {
     .unwrap();
 }
 
+#[derive(Default)]
+pub struct LineNumbersData<'a> {
+    pub left_format_data: LineNumberFormatData<'a>,
+    pub right_format_data: LineNumberFormatData<'a>,
+    pub hunk_minus_line_number: usize,
+    pub hunk_plus_line_number: usize,
+    pub hunk_max_line_number_width: usize,
+}
+
+// Although it's probably unusual, a single format string can contain multiple placeholders. E.g.
+// line-numbers-right-format = "{nm} {np}|"
+pub type LineNumberFormatData<'a> = Vec<LineNumberPlaceholderData<'a>>;
+
+#[derive(Debug, Default, PartialEq)]
+pub struct LineNumberPlaceholderData<'a> {
+    pub prefix: &'a str,
+    pub placeholder: &'a str,
+    pub alignment_spec: Option<&'a str>,
+    pub width: Option<usize>,
+    pub suffix: &'a str,
+}
+
+impl<'a> LineNumbersData<'a> {
+    pub fn from_format_strings(left_format: &'a str, right_format: &'a str) -> LineNumbersData<'a> {
+        Self {
+            left_format_data: parse_line_number_format(left_format),
+            right_format_data: parse_line_number_format(right_format),
+            hunk_minus_line_number: 0,
+            hunk_plus_line_number: 0,
+            hunk_max_line_number_width: 0,
+        }
+    }
+
+    /// Initialize line number data for a hunk.
+    pub fn initialize_hunk(&mut self, line_numbers: Vec<(usize, usize)>) {
+        // 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.hunk_minus_line_number = line_numbers[0].0;
+        self.hunk_plus_line_number = 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;
+    }
+}
+
+fn parse_line_number_format<'a>(format_string: &'a str) -> LineNumberFormatData<'a> {
+    let mut format_data = Vec::new();
+    let mut offset = 0;
+
+    for captures in LINE_NUMBER_FORMAT_REGEX.captures_iter(format_string) {
+        let _match = captures.get(0).unwrap();
+        format_data.push(LineNumberPlaceholderData {
+            prefix: &format_string[offset.._match.start()],
+            placeholder: captures.get(1).map(|m| m.as_str()).unwrap(),
+            alignment_spec: captures.get(3).map(|m| m.as_str()),
+            width: captures.get(4).map(|m| {
+                m.as_str()
+                    .parse()
+                    .unwrap_or_else(|_| panic!("Invalid width in format string: {}", format_string))
+            }),
+            suffix: &format_string[_match.end()..],
+        });
+        offset = _match.end();
+    }
+    format_data
+}
+
 fn format_and_paint_line_number_field<'a>(
-    format_string: &'a str,
+    format_data: &Vec<LineNumberPlaceholderData<'a>>,
     style: &Style,
     minus_number: Option<usize>,
     plus_number: Option<usize>,
+    min_field_width: usize,
     minus_number_style: &Style,
     plus_number_style: &Style,
 ) -> Vec<ansi_term::ANSIGenericString<'a, str>> {
     let mut ansi_strings = Vec::new();
+    let mut suffix = "";
+    for placeholder in format_data {
+        ansi_strings.push(style.paint(placeholder.prefix));
 
-    let mut offset = 0;
-    for caps in LINE_NUMBER_FORMAT_REGEX.captures_iter(&format_string) {
-        let _match = caps.get(0).unwrap();
-        ansi_strings.push(style.paint(&format_string[offset.._match.start()]));
+        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 &caps[1] {
+        match placeholder.placeholder {
             "nm" => ansi_strings.push(minus_number_style.paint(format_line_number(
                 minus_number,
-                &caps[3],
-                &caps[4],
+                alignment_spec,
+                width,
             ))),
             "np" => ansi_strings.push(plus_number_style.paint(format_line_number(
                 plus_number,
-                &caps[3],
-                &caps[4],
+                alignment_spec,
+                width,
             ))),
             _ => unreachable!(),
         }
-        offset = _match.end();
+        suffix = placeholder.suffix;
     }
-    ansi_strings.push(style.paint(&format_string[offset..]));
+    ansi_strings.push(style.paint(suffix));
     ansi_strings
 }
 
 /// Return line number formatted according to `alignment` and `width`.
-fn format_line_number(line_number: Option<usize>, alignment: &str, width: &str) -> String {
+fn format_line_number(line_number: Option<usize>, alignment: &str, width: usize) -> 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),
+        "<" => format!("{0:<1$}", n, width),
+        "^" => format!("{0:^1$}", n, width),
+        ">" => format!("{0:>1$}", n, width),
         _ => unreachable!(),
     }
 }
@@ -172,54 +262,99 @@ pub mod tests {
 
     use crate::tests::integration_test_utils::integration_test_utils::{make_config, run_delta};
 
-    use super::LINE_NUMBER_FORMAT_REGEX;
+    use super::*;
 
     #[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), "");
+        assert_eq!(
+            parse_line_number_format("{nm}"),
+            vec![LineNumberPlaceholderData {
+                prefix: "",
+                placeholder: "nm",
+                alignment_spec: None,
+                width: None,
+                suffix: "",
+            }]
+        )
     }
 
     #[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");
+        assert_eq!(
+            parse_line_number_format("{np:4}"),
+            vec![LineNumberPlaceholderData {
+                prefix: "",
+                placeholder: "np",
+                alignment_spec: None,
+                width: Some(4),
+                suffix: "",
+            }]
+        )
     }
 
     #[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");
+        assert_eq!(
+            parse_line_number_format("{np:>4}"),
+            vec![LineNumberPlaceholderData {
+                prefix: "",
+                placeholder: "np",
+                alignment_spec: Some(">"),
+                width: Some(4),
+                suffix: "",
+            }]
+        )
     }
 
     #[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");
+        assert_eq!(
+            parse_line_number_format("{np:_>4}"),
+            vec![LineNumberPlaceholderData {
+                prefix: "",
+                placeholder: "np",
+                alignment_spec: Some(">"),
+                width: Some(4),
+                suffix: "",
+            }]
+        )
+    }
+
+    #[test]
+    fn test_line_number_format_regex_5() {
+        assert_eq!(
+            parse_line_number_format("__{np:_>4}@@"),
+            vec![LineNumberPlaceholderData {
+                prefix: "__",
+                placeholder: "np",
+                alignment_spec: Some(">"),
+                width: Some(4),
+                suffix: "@@",
+            }]
+        )
+    }
+
+    #[test]
+    fn test_line_number_format_regex_6() {
+        assert_eq!(
+            parse_line_number_format("__{nm:<3}@@---{np:_>4}**"),
+            vec![
+                LineNumberPlaceholderData {
+                    prefix: "__",
+                    placeholder: "nm",
+                    alignment_spec: Some("<"),
+                    width: Some(3),
+                    suffix: "@@---{np:_>4}**",
+                },
+                LineNumberPlaceholderData {
+                    prefix: "@@---",
+                    placeholder: "np",
+                    alignment_spec: Some(">"),
+                    width: Some(4),
+                    suffix: "**",
+                }
+            ]
+        )
     }
 
     fn _get_capture<'a>(i: usize, j: usize, caps: &'a Vec<Captures>) -> &'a str {
@@ -326,6 +461,28 @@ pub mod tests {
         assert_eq!(lines.next().unwrap(), "         ⋮ 2  │bb = 2");
     }
 
+    #[test]
+    fn test_five_digit_line_number() {
+        let config = make_config(&["--line-numbers"]);
+        let output = run_delta(FIVE_DIGIT_LINE_NUMBER_DIFF, &config);
+        let output = strip_ansi_codes(&output);
+        let mut lines = output.lines().skip(4);
+        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(&["--line-numbers"]);
+        let output = run_delta(UNEQUAL_DIGIT_DIFF, &config);
+        let output = strip_ansi_codes(&output);
+        let mut lines = output.lines().skip(4);
+        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");
+    }
+
     const TWO_MINUS_LINES_DIFF: &str = "\
 diff --git i/a.py w/a.py
 index 223ca50..e69de29 100644
@@ -356,5 +513,27 @@ index 223ca50..367a6f6 100644
  a = 1
 -b = 2
 +bb = 2
+";
+
+    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
 ";
 }
diff --git a/src/paint.rs b/src/paint.rs
index 0df02b2..6677c4a 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -6,6 +6,7 @@ use ansi_term;
 use syntect::easy::HighlightLines;
 use syntect::highlighting::Style as SyntectStyle;
 use syntect::parsing::{SyntaxReference, SyntaxSet};
+use unicode_segmentation::UnicodeSegmentation;
 
 use crate::config::{self, delta_unreachable};
 use crate::delta::State;
@@ -26,8 +27,7 @@ pub struct Painter<'a> {
     pub highlighter: HighlightLines<'a>,
     pub config: &'a config::Config,
     pub output_buffer: String,
-    pub minus_line_number: usize,
-    pub plus_line_number: usize,
+    pub line_numbers_data: line_numbers::LineNumbersData<'a>,
 }
 
 impl<'a> Painter<'a> {
@@ -35,6 +35,15 @@ impl<'a> Painter<'a> {
         let default_syntax = Self::get_syntax(&config.syntax_set, None);
         // TODO: Avoid doing this.
         let dummy_highlighter = HighlightLines::new(default_syntax, &config.syntax_dummy_theme);
+
+        let line_numbers_data = if config.line_numbers {
+            line_numbers::LineNumbersData::from_format_strings(
+                &config.line_numbers_left_format,
+                &config.line_numbers_right_format,
+            )
+        } else {
+            line_numbers::LineNumbersData::default()
+        };
         Self {
             minus_lines: Vec::new(),
             plus_lines: Vec::new(),
@@ -43,8 +52,7 @@ impl<'a> Painter<'a> {
             highlighter: dummy_highlighter,
             writer,
             config,
-            minus_line_number: 0,
-            plus_line_number: 0,
+            line_numbers_data,
         }
     }
 
@@ -71,7 +79,45 @@ impl<'a> Painter<'a> {
         };
     }
 
-    pub fn paint_buffered_lines(&mut self) {
+    /// Replace initial -/+ character with ' ', expand tabs as spaces, and optionally terminate with
+    /// newline.
+    // Terminating with newline character is necessary for many of the sublime syntax definitions to
+    // highlight correctly.
+    // See https://docs.rs/syntect/3.2.0/syntect/parsing/struct.SyntaxSetBuilder.html#method.add_from_folder
+    pub fn prepare(&self, line: &str, append_newline: bool) -> String {
+        let terminator = if append_newline { "\n" } else { "" };
+        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. When emitting the line
+            // in Painter::paint_lines, we drop the space (unless --keep-plus-minus-markers is in
+            // effect in which case we replace it with the appropriate marker).
+            // TODO: Things should, but do not, work if this leading space is omitted at this stage.
+            // See comment in align::Alignment::new.
+            line.next();
+            format!(" {}{}", self.expand_tabs(line), terminator)
+        } else {
+            terminator.to_string()
+        }
+    }
+
+    /// Expand tabs as spaces.
+    /// tab_width = 0 is documented to mean do not replace tabs.
+    pub fn expand_tabs<'b, I>(&self, line: I) -> String
+    where
+        I: Iterator<Item = &'b str>,
+    {
+        if self.config.tab_width > 0 {
+            let tab_replacement = " ".repeat(self.config.tab_width);
+            line.map(|s| if s == "\t" { &tab_replacement } else { s })
+                .collect::<String>()
+        } else {
+            line.collect::<String>()
+        }
+    }
+
+    pub fn paint_buffered_minus_and_plus_lines(&mut self) {
         let minus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines(
             &self.minus_lines,
             &State::HunkMinus,
@@ -87,24 +133,15 @@ impl<'a> Painter<'a> {
         let (minus_line_diff_style_sections, plus_line_diff_style_sections) =
             Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config);
 
-        let mut minus_line_numbers = Vec::new();
-        let mut plus_line_numbers = Vec::new();
-        for _line in &self.minus_lines {
-            minus_line_numbers.push(Some((Some(self.minus_line_number), None)));
-            self.minus_line_number += 1;
-        }
-        for _line in &self.plus_lines {
-            plus_line_numbers.push(Some((None, Some(self.plus_line_number))));
-            self.plus_line_number += 1;
-        }
         // TODO: lines and style sections contain identical line text
         if !self.minus_lines.is_empty() {
             Painter::paint_lines(
                 minus_line_syntax_style_sections,
                 minus_line_diff_style_sections,
-                minus_line_numbers,
+                &State::HunkMinus,
                 &mut self.output_buffer,
                 self.config,
+                &mut Some(&mut self.line_numbers_data),
                 if self.config.keep_plus_minus_markers {
                     "-"
                 } else {
@@ -120,9 +157,10 @@ impl<'a> Painter<'a> {
             Painter::paint_lines(
                 plus_line_syntax_style_sections,
                 plus_line_diff_style_sections,
-                plus_line_numbers,
+                &State::HunkPlus,
                 &mut self.output_buffer,
                 self.config,
+                &mut Some(&mut self.line_numbers_data),
                 if self.config.keep_plus_minus_markers {
                     "+"
                 } else {
@@ -138,14 +176,45 @@ impl<'a> Painter<'a> {
         self.plus_lines.clear();
     }
 
+    pub fn paint_zero_line(&mut self, line: &str) {
+        let prefix = if self.config.keep_plus_minus_markers && !line.is_empty() {
+            &line[..1]
+        } else {
+            ""
+        };
+        let lines = vec![self.prepare(line, true)];
+        let syntax_style_sections = Painter::get_syntax_style_sections_for_lines(
+            &lines,
+            &State::HunkZero,
+            &mut self.highlighter,
+            &self.config,
+        );
+        let diff_style_sections = vec![(self.config.zero_style, lines[0].as_str())];
+
+        Painter::paint_lines(
+            syntax_style_sections,
+            vec![diff_style_sections],
+            &State::HunkZero,
+            &mut self.output_buffer,
+            self.config,
+            &mut Some(&mut self.line_numbers_data),
+            prefix,
+            self.config.zero_style,
+            self.config.zero_style,
+            None,
+            None,
+        );
+    }
+
     /// Superimpose background styles and foreground syntax
     /// highlighting styles, and write colored lines to output buffer.
     pub fn paint_lines(
         syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
         diff_style_sections: Vec<Vec<(Style, &str)>>,
-        line_number_sections: Vec<Option<(Option<usize>, Option<usize>)>>,
+        state: &State,
         output_buffer: &mut String,
         config: &config::Config,
+        line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
         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
@@ -160,54 +229,28 @@ impl<'a> Painter<'a> {
         // 2. We must ensure that we fill rightwards with the appropriate
         //    non-emph background color. In that case we don't use the last
         //    style of the line, because this might be emph.
-
-        for ((syntax_sections, diff_sections), line_numbers) in syntax_style_sections
-            .iter()
-            .zip(diff_style_sections.iter())
-            .zip(line_number_sections.iter())
+        for (syntax_sections, diff_sections) in
+            syntax_style_sections.iter().zip(diff_style_sections.iter())
         {
+            let mut line = Painter::superimpose_style_sections_and_paint(
+                syntax_sections,
+                diff_sections,
+                state,
+                line_numbers_data,
+                prefix,
+                config,
+            );
             let non_emph_style = if style_sections_contain_more_than_one_style(diff_sections) {
                 non_emph_style // line contains an emph section
             } else {
                 style
             };
-
-            let mut handled_prefix = false;
-            let mut ansi_strings = Vec::new();
-            if config.line_numbers && line_numbers.is_some() {
-                ansi_strings.extend(line_numbers::format_and_paint_line_numbers(
-                    line_numbers,
-                    config,
-                ))
-            }
-            for (section_style, mut text) in superimpose_style_sections(
-                syntax_sections,
-                diff_sections,
-                config.true_color,
-                config.null_syntect_style,
-            ) {
-                if !handled_prefix {
-                    if prefix != "" {
-                        ansi_strings.push(section_style.paint(prefix));
-                    }
-                    if text.len() > 0 {
-                        text.remove(0);
-                    }
-                    handled_prefix = true;
-                }
-                if !text.is_empty() {
-                    ansi_strings.push(section_style.paint(text));
-                }
-            }
             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.paint(""));
-            }
-            let line = &mut ansi_term::ANSIStrings(&ansi_strings).to_string();
             if right_fill_background_color {
                 // HACK: How to properly incorporate the ANSI_CSI_CLEAR_TO_EOL into ansi_strings?
+                line.push_str(&ansi_term::ANSIStrings(&[non_emph_style.paint("")]).to_string());
                 if line
                     .to_lowercase()
                     .ends_with(&ANSI_SGR_RESET.to_lowercase())
@@ -233,6 +276,46 @@ impl<'a> Painter<'a> {
         }
     }
 
+    fn superimpose_style_sections_and_paint(
+        syntax_sections: &Vec<(SyntectStyle, &str)>,
+        diff_sections: &Vec<(Style, &str)>,
+        state: &State,
+        line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
+        prefix: &str,
+        config: &config::Config,
+    ) -> String {
+        let output_line_numbers = config.line_numbers && line_numbers_data.is_some();
+        let mut handled_prefix = false;
+        let mut ansi_strings = Vec::new();
+        if output_line_numbers {
+            ansi_strings.extend(line_numbers::format_and_paint_line_numbers(
+                line_numbers_data.as_mut().unwrap(),
+                state,
+                config,
+            ))
+        }
+        for (section_style, mut text) in superimpose_style_sections(
+            syntax_sections,
+            diff_sections,
+            config.true_color,
+            config.null_syntect_style,
+        ) {
+            if !handled_prefix {
+                if prefix != "" {
+                    ansi_strings.push(section_style.paint(prefix));
+                }
+                if text.len() > 0 {
+                    text.remove(0);
+                }
+                handled_prefix = true;
+            }
+            if !text.is_empty() {
+                ansi_strings.push(section_style.paint(text));
+            }
+        }
+        ansi_term::ANSIStrings(&ansi_strings).to_string()
+    }
+
     /// Write output buffer to output stream, and clear the buffer.
     pub fn emit(&mut self) -> std::io::Result<()> {
         write!(self.writer, "{}", self.output_buffer)?;


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index e7980e3..23a73f3 100644
--- a/src/tests/ansi_test_utils.rs
+++ b/src/tests/ansi_test_utils.rs
@@ -4,6 +4,7 @@ pub mod ansi_test_utils {
     use console::strip_ansi_codes;
 
     use crate::config::Config;
+    use crate::delta::State;
     use crate::paint;
     use crate::style::Style;
 
@@ -125,9 +126,10 @@ pub mod ansi_test_utils {
         paint::Painter::paint_lines(
             vec![syntax_style_sections],
             vec![vec![(syntax_highlighted_style, lines[0])]],
-            vec![None],
+            &State::Unknown,
             &mut output_buffer,
             config,
+            &mut None,
             "",
             config.null_style,
             config.null_style,