Repository: dandavison/delta
Pull request #1145 (2022-08-02 03:06:20)

try fix bad alignment in unicode (#1144)
Add spaces in `truncate_str` doesn't satisify the function description. So I change the function `pad_panel_line_to_width` (maybe not right)

th1000s: Thank you, great find. Indeed, the length of Chinese characters is usually 2 (do you know any unicode points which map to 3 or more cells?), but here the assumption is <= 1.

Now however `measure_text_width()` is called called twice, the first time in `truncate_str()`. The fill-up logic would be better placed in there. It would also mean that this function now returns something with the _exact_ display length if something had to be cut off, i.e. the rest is filled up. Because placing a space _after_ the `→` would look uneven, it is better to place them before or even use a blue symbol (`·→`, similar to the `↵`) to make it clear that this is not the original input.

And somewhere in the wrapping logic the same miscounting happens which results in truncation instead of wrapping, probably when `line_is_too_long()` assumes that `line.graphemes(true).count()` equals the display width. Then spaces or `·` will also have to be placed before the `WrapConfig::left_symbol` (`↵`).

Save the diff below as 'badwrap' and run:
`for i in {34..22}; do target/debug/delta --width=$i < badwrap | tail -n +4; done`

``` diff
@@ -1,5 +0,0 @@
-一二三四
-1一二三四
-12一二三四
-123一二三四
-123456789
```
SheldonNico: > do you know any unicode points which map to 3 or more cells?), but here the assumption is <= 1.

I didn't find any Chinese character map to 3 or more. But from the code in [unicode-width](https://github.com/unicode-rs/unicode-width/blob/master/src/tables.rs#L50), 3 means ambiguous width.

 > The fill-up logic would be better placed in there. 

Yes, Add a symbol before is much better. 

I will try in my next commit.

dandavison: Hi @SheldonNico, thanks very much for working on this. I'm sorry I let it slip and then forgot about it.
SheldonNico: Add space(no custom symbol for simplicity) in `truncate_str` is easy. 

But change how width calculated in wrapping need too much modifications, and some modifications may not be right since I didn't figure out all function logical. I try to fix broken tests, and it seems to work. Feel free to not accept this PR, if you have plan in the future. 

![image](https://user-images.githubusercontent.com/11974052/197205164-4ba6f194-b957-4da3-84c9-16e7f4075b94.png)

SheldonNico: > do you know any unicode points which map to 3 or more cells?

Yes, original test find one: [क्षि ](https://github.com/dandavison/delta/blob/master/src/wrapping.rs#L942), and [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=58c299aab7c10063c40c834666cc65fa). It break my text editor. But in browser, it take 2 columns.
th1000s: Very nice, looks good to me. Also, you may add [this commit](https://github.com/th1000s/delta/commit/e245a8bbf54cb7a02fe792f2001d8e141ec0e3a9) which will correct the tests instead of adapting the code to work with the old tests.

> Add space(no custom symbol for simplicity)

Yes, that would have to become customizable. I will do that later when making the wrapping aware of word boundaries.

Patch:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index b5c8b04..225c527 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -12,19 +12,37 @@ jobs:
     strategy:
       matrix:
         job:
-          - { os: macos-latest,   target: x86_64-apple-darwin,         use-cross: false }
-          - { os: windows-latest, target: x86_64-pc-windows-msvc,      use-cross: false }
-          - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu,    use-cross: false }
-          - { os: ubuntu-latest,  target: x86_64-unknown-linux-musl,   use-cross: true }
-          - { os: ubuntu-latest,  target: i686-unknown-linux-gnu,      use-cross: true }
-          - { os: ubuntu-latest,  target: arm-unknown-linux-gnueabihf, use-cross: true }
-          - { os: ubuntu-latest,  target: aarch64-unknown-linux-gnu,   use-cross: true }
+          - os: macos-latest
+            target: x86_64-apple-darwin
+            use-cross: false
+          - os: macos-latest
+            target: aarch64-apple-darwin
+            use-cross: false
+          - os: windows-latest
+            target: x86_64-pc-windows-msvc
+            use-cross: false
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-gnu
+            use-cross: false
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-musl
+            use-cross: true
+          - os: ubuntu-latest
+            target: i686-unknown-linux-gnu
+            use-cross: true
+          - os: ubuntu-latest
+            target: arm-unknown-linux-gnueabihf
+            use-cross: true
+          - os: ubuntu-latest
+            target: aarch64-unknown-linux-gnu
+            use-cross: true
 
     steps:
       - name: Installing Rust toolchain
         uses: actions-rs/toolchain@v1
         with:
           toolchain: stable
+          target: ${{ matrix.job.target }}
           profile: minimal
           override: true
       - name: Checkout repository
@@ -82,7 +100,7 @@ jobs:
         with:
           command: publish
           args: --token ${{ secrets.CARGO_API_KEY }} --allow-dirty
-          
+
   bump-homebrew-formula:
     runs-on: macos-latest
     steps:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c78ae78..f1824ba 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,13 +14,27 @@ jobs:
     strategy:
       matrix:
         job:
-          - { os: macos-latest,   target: x86_64-apple-darwin,         use-cross: false }
-          - { os: windows-latest, target: x86_64-pc-windows-msvc,      use-cross: false }
-          - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu,    use-cross: false }
-          - { os: ubuntu-latest,  target: x86_64-unknown-linux-musl,   use-cross: true }
-          - { os: ubuntu-latest,  target: i686-unknown-linux-gnu,      use-cross: true }
-          - { os: ubuntu-latest,  target: arm-unknown-linux-gnueabihf, use-cross: true }
-          - { os: ubuntu-latest,  target: aarch64-unknown-linux-gnu,   use-cross: true }
+          - os: macos-latest
+            target: x86_64-apple-darwin
+            use-cross: false
+          - os: windows-latest
+            target: x86_64-pc-windows-msvc
+            use-cross: false
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-gnu
+            use-cross: false
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-musl
+            use-cross: true
+          - os: ubuntu-latest
+            target: i686-unknown-linux-gnu
+            use-cross: true
+          - os: ubuntu-latest
+            target: arm-unknown-linux-gnueabihf
+            use-cross: true
+          - os: ubuntu-latest
+            target: aarch64-unknown-linux-gnu
+            use-cross: true
     steps:
       - name: Checkout repository
         uses: actions/checkout@v2
diff --git a/README.md b/README.md
index ce043f7..9582598 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,6 @@
 
 [interactive]
     diffFilter = delta --color-only
-[add.interactive]
-    useBuiltin = false # required for git 2.37.0
 
 [delta]
     navigate = true    # use n and N to move between diff sections
diff --git a/etc/ci/before_deploy.sh b/etc/ci/before_deploy.sh
index 8e188ed..8945f39 100755
--- a/etc/ci/before_deploy.sh
+++ b/etc/ci/before_deploy.sh
@@ -156,9 +156,8 @@ Priority: optional
 Maintainer: Dan Davison <dandavison7@gmail.com>
 Architecture: $architecture
 Depends: $depends
-Provides: $PROJECT_NAME
 Conflicts: $conflictname
-Description: Syntax highlighter for git.
+Description: Syntax highlighter for git
  Delta provides language syntax-highlighting, within-line insertion/deletion
  detection, and restructured diff output for git on the command line.
 EOF
diff --git a/src/align.rs b/src/align.rs
index 92262cb..b2e2bdf 100644
--- a/src/align.rs
+++ b/src/align.rs
@@ -1,14 +1,14 @@
 use std::cmp::max;
 use std::collections::VecDeque;
 
-const SUBSTITUTION_COST: usize = 1;
-const DELETION_COST: usize = 1;
-const INSERTION_COST: usize = 1;
+const DELETION_COST: usize = 2;
+const INSERTION_COST: usize = 2;
+// extra cost for starting a new group of changed tokens
+const INITIAL_MISMATCH_PENALITY: usize = 1;
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum Operation {
     NoOp,
-    Substitution,
     Deletion,
     Insertion,
 }
@@ -61,14 +61,14 @@ impl<'a> Alignment<'a> {
             self.table[i] = Cell {
                 parent: 0,
                 operation: Deletion,
-                cost: i,
+                cost: i * DELETION_COST + INITIAL_MISMATCH_PENALITY,
             };
         }
         for j in 1..self.dim[0] {
             self.table[j * self.dim[1]] = Cell {
                 parent: 0,
                 operation: Insertion,
-                cost: j,
+                cost: j * INSERTION_COST + INITIAL_MISMATCH_PENALITY,
             };
         }
 
@@ -76,22 +76,32 @@ impl<'a> Alignment<'a> {
             for (j, y_j) in self.y.iter().enumerate() {
                 let (left, diag, up) =
                     (self.index(i, j + 1), self.index(i, j), self.index(i + 1, j));
+                // The order of the candidates matters if two of them have the
+                // same cost as in that case we choose the first one. Consider
+                // insertions and deletions before matches in order to group
+                // changes together. Insertions are preferred to deletions in
+                // order to highlight moved tokens as a deletion followed by an
+                // insertion (as the edit sequence is read backwards we need to
+                // choose the insertion first)
                 let candidates = [
+                    Cell {
+                        parent: up,
+                        operation: Insertion,
+                        cost: self.mismatch_cost(up, INSERTION_COST),
+                    },
                     Cell {
                         parent: left,
                         operation: Deletion,
-                        cost: self.table[left].cost + DELETION_COST,
+                        cost: self.mismatch_cost(left, DELETION_COST),
                     },
                     Cell {
                         parent: diag,
-                        operation: if x_i == y_j { NoOp } else { Substitution },
-                        cost: self.table[diag].cost
-                            + if x_i == y_j { 0 } else { SUBSTITUTION_COST },
-                    },
-                    Cell {
-                        parent: up,
-                        operation: Insertion,
-                        cost: self.table[up].cost + INSERTION_COST,
+                        operation: NoOp,
+                        cost: if x_i == y_j {
+                            self.table[diag].cost
+                        } else {
+                            usize::MAX
+                        },
                     },
                 ];
                 let index = self.index(i + 1, j + 1);
@@ -104,6 +114,16 @@ impl<'a> Alignment<'a> {
         }
     }
 
+    fn mismatch_cost(&self, parent: usize, basic_cost: usize) -> usize {
+        self.table[parent].cost
+            + basic_cost
+            + if self.table[parent].operation == NoOp {
+                INITIAL_MISMATCH_PENALITY
+            } else {
+                0
+            }
+    }
+
     /// Read edit operations from the table.
     pub fn operations(&self) -> Vec<Operation> {
         let mut ops = VecDeque::with_capacity(max(self.x.len(), self.y.len()));
@@ -161,7 +181,6 @@ impl<'a> Alignment<'a> {
         let parent = &self.table[cell.parent];
         let op = match cell.operation {
             Deletion => "-",
-            Substitution => "*",
             Insertion => "+",
             NoOp => ".",
         };
@@ -240,68 +259,176 @@ mod tests {
 
     #[test]
     fn test_0() {
-        let (before, after) = ("aaa", "aba");
-        assert_string_distance_parts(before, after, (1, 3));
-        assert_eq!(operations(before, after), vec![NoOp, Substitution, NoOp,]);
+        TestCase {
+            before: "aaa",
+            after: "aba",
+            distance: 5,
+            parts: (2, 4),
+            operations: vec![NoOp, Deletion, Insertion, NoOp],
+        }
+        .run();
     }
 
     #[test]
     fn test_0_nonascii() {
-        let (before, after) = ("ááb", "áaa");
-        assert_string_distance_parts(before, after, (2, 3));
-        assert_eq!(
-            operations(before, after),
-            vec![NoOp, Substitution, Substitution,]
-        );
+        TestCase {
+            before: "ááb",
+            after: "áaa",
+            distance: 9,
+            parts: (4, 5),
+            operations: vec![NoOp, Deletion, Deletion, Insertion, Insertion],
+        }
+        .run();
     }
 
     #[test]
     fn test_1() {
-        let (before, after) = ("kitten", "sitting");
-        assert_string_distance_parts(before, after, (3, 7));
-        assert_eq!(
-            operations(before, after),
-            vec![
-                Substitution, // K S
-                NoOp,         // I I
-                NoOp,         // T T
-                NoOp,         // T T
-                Substitution, // E I
-                NoOp,         // N N
-                Insertion     // - G
-            ]
-        );
+        TestCase {
+            before: "kitten",
+            after: "sitting",
+            distance: 13,
+            parts: (5, 9),
+            operations: vec![
+                Deletion,  // K -
+                Insertion, // - S
+                NoOp,      // I I
+                NoOp,      // T T
+                NoOp,      // T T
+                Deletion,  // E -
+                Insertion, // - I
+                NoOp,      // N N
+                Insertion, // - G
+            ],
+        }
+        .run();
     }
 
     #[test]
     fn test_2() {
-        let (before, after) = ("saturday", "sunday");
-        assert_string_distance_parts(before, after, (3, 8));
-        assert_eq!(
-            operations(before, after),
-            vec![
-                NoOp,         // S S
-                Deletion,     // A -
-                Deletion,     // T -
-                NoOp,         // U U
-                Substitution, // R N
-                NoOp,         // D D
-                NoOp,         // A A
-                NoOp          // Y Y
-            ]
-        );
+        TestCase {
+            before: "saturday",
+            after: "sunday",
+            distance: 10,
+            parts: (4, 9),
+            operations: vec![
+                NoOp,      // S S
+                Deletion,  // A -
+                Deletion,  // T -
+                NoOp,      // U U
+                Deletion,  // R -
+                Insertion, // - N
+                NoOp,      // D D
+                NoOp,      // A A
+                NoOp,      // Y Y
+            ],
+        }
+        .run();
     }
 
-    fn assert_string_distance_parts(s1: &str, s2: &str, parts: (usize, usize)) {
-        let (numer, _) = parts;
-        assert_string_levenshtein_distance(s1, s2, numer);
-        assert_eq!(string_distance_parts(s1, s2), parts);
-        assert_eq!(string_distance_parts(s2, s1), parts);
+    #[test]
+    fn test_3() {
+        TestCase {
+            // Prefer [Deletion NoOp Insertion] over [Insertion NoOp Deletion]
+            before: "ab",
+            after: "ba",
+            distance: 6,
+            parts: (2, 3),
+            operations: vec![
+                Deletion,  // a -
+                NoOp,      // b b
+                Insertion, // - a
+            ],
+        }
+        .run();
+    }
+
+    #[test]
+    fn test_4() {
+        // Deletions are grouped together.
+        TestCase {
+            before: "AABB",
+            after: "AB",
+            distance: 5,
+            parts: (2, 4),
+            operations: vec![
+                NoOp,     // A A
+                Deletion, // A -
+                Deletion, // B -
+                NoOp,     // B B
+            ],
+        }
+        .run();
     }
 
-    fn assert_string_levenshtein_distance(s1: &str, s2: &str, d: usize) {
-        assert_eq!(string_levenshtein_distance(s1, s2), d);
-        assert_eq!(string_levenshtein_distance(s2, s1), d);
+    #[test]
+    fn test_5() {
+        // Insertions are grouped together.
+        TestCase {
+            before: "AB",
+            after: "AABB",
+            distance: 5,
+            parts: (2, 4),
+            operations: vec![
+                NoOp,      // A A
+                Insertion, // - A
+                Insertion, // - B
+                NoOp,      // B B
+            ],
+        }
+        .run();
+    }
+
+    #[test]
+    fn test_6() {
+        // Insertion and Deletion are grouped together.
+        TestCase {
+            before: "AAABBB",
+            after: "ACB",
+            distance: 11,
+            parts: (5, 7),
+            operations: vec![
+                NoOp,      // A A
+                Deletion,  // A -
+                Deletion,  // A -
+                Deletion,  // B -
+                Deletion,  // B -
+                Insertion, // - C
+                NoOp,      // B B
+            ],
+        }
+        .run();
+    }
+
+    struct TestCase<'a> {
+        before: &'a str,
+        after: &'a str,
+        distance: usize,
+        parts: (usize, usize),
+        operations: Vec<Operation>,
+    }
+
+    impl<'a> TestCase<'a> {
+        pub fn run(&self) -> () {
+            self.assert_string_distance_parts();
+            assert_eq!(operations(self.before, self.after), self.operations);
+        }
+
+        fn assert_string_distance_parts(&self) {
+            self.assert_string_levenshtein_distance();
+            assert_eq!(string_distance_parts(self.before, self.after), self.parts);
+            assert_eq!(string_distance_parts(self.after, self.before), self.parts);
+        }
+
+        fn assert_string_levenshtein_distance(&self) {
+            assert_eq!(
+                string_levenshtein_distance(self.before, self.after),
+                self.distance
+            );
+            assert_eq!(
+                string_levenshtein_distance(self.after, self.before),
+                self.distance
+            );
+        }
     }
 
     fn string_distance_parts(x: &str, y: &str) -> (usize, usize) {
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs
index 90eac1d..2b7b2f6 100644
--- a/src/ansi/mod.rs
+++ b/src/ansi/mod.rs
@@ -21,7 +21,9 @@ pub fn strip_ansi_codes(s: &str) -> String {
 
 pub fn measure_text_width(s: &str) -> usize {
     // TODO: how should e.g. '\n' be handled?
-    strip_ansi_codes(s).width()
+    ansi_strings_iterator(s).fold(0, |acc, (element, is_ansi)| {
+        acc + if is_ansi { 0 } else { element.width() }
+    })
 }
 
 /// Truncate string such that `tail` is present as a suffix, preceded by as much of `s` as can be
@@ -52,6 +54,10 @@ pub fn truncate_str<'a, 'b>(s: &'a str, display_width: usize, tail: &'b str) ->
             for g in t.graphemes(true) {
                 let w = g.width();
                 if used + w > display_width {
+                    // use space to fill the gap left by truncate. Need another
+                    // option to custom this or just use `WrapOption.wrap_right_prefix_symbol`
+                    // in the future.
+                    result.push_str(&" ".repeat(display_width.saturating_sub(used)));
                     break;
                 }
                 result.push_str(g);
diff --git a/src/delta.rs b/src/delta.rs
index 741d3c6..1b0b298 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -202,7 +202,7 @@ impl<'a> StateMachine<'a> {
         // \r and \n, in which case byte_lines does not remove the \r. Remove it now.
         // TODO: Limit the number of characters we examine when looking for the \r?
         if let Some(cr_index) = self.raw_line.rfind('\r') {
-            if ansi::strip_ansi_codes(&self.raw_line[cr_index + 1..]).is_empty() {
+            if ansi::measure_text_width(&self.raw_line[cr_index + 1..]) == 0 {
                 self.raw_line = format!(
                     "{}{}",
                     &self.raw_line[..cr_index],
diff --git a/src/edits.rs b/src/edits.rs
index 9ee4d19..bcbead1 100644
--- a/src/edits.rs
+++ b/src/edits.rs
@@ -191,23 +191,11 @@ where
         *substrings_offset += n;
         &line[old_offset..*line_offset]
     };
-    let mut minus_section = |n| {
-        get_section(
-            n,
-            &mut minus_line_offset,
-            &mut x_offset,
-            &alignment.x,
-            minus_line,
-        )
+    let mut minus_section = |n: usize, offset: &mut usize| {
+        get_section(n, &mut minus_line_offset, offset, &alignment.x, minus_line)
     };
-    let mut plus_section = |n| {
-        get_section(
-            n,
-            &mut plus_line_offset,
-            &mut y_offset,
-            &alignment.y,
-            plus_line,
-        )
+    let mut plus_section = |n: usize, offset: &mut usize| {
+        get_section(n, &mut plus_line_offset, offset, &alignment.y, plus_line)
     };
     let distance_contribution = |section: &str| UnicodeWidthStr::width(section.trim());
 
@@ -215,7 +203,7 @@ where
     for (op, n) in alignment.coalesced_operations() {
         match op {
             align::Operation::Deletion => {
-                let minus_section = minus_section(n);
+                let minus_section = minus_section(n, &mut x_offset);
                 let n_d = distance_contribution(minus_section);
                 d_denom += n_d;
                 d_numer += n_d;
@@ -223,12 +211,14 @@ where
                 minus_op_prev = deletion;
             }
             align::Operation::NoOp => {
-                let minus_section = minus_section(n);
+                let minus_section = minus_section(n, &mut x_offset);
                 let n_d = distance_contribution(minus_section);
-                d_denom += n_d;
+                d_denom += 2 * n_d;
                 let is_space = minus_section.trim().is_empty();
                 let coalesce_space_with_previous = is_space
-                    && ((minus_op_prev == deletion && plus_op_prev == insertion)
+                    && ((minus_op_prev == deletion
+                        && plus_op_prev == insertion
+                        && (x_offset < alignment.x.len() - 1 || y_offset < alignment.y.len() - 1))
                         || (minus_op_prev == noop_deletion && plus_op_prev == noop_insertion));
                 annotated_minus_line.push((
                     if coalesce_space_with_previous {
@@ -244,23 +234,13 @@ where
                     } else {
                         noop_insertion
                     },
-                    plus_section(n),
+                    plus_section(n, &mut y_offset),
                 ));
                 minus_op_prev = noop_deletion;
                 plus_op_prev = noop_insertion;
             }
-            align::Operation::Substitution => {
-                let minus_section = minus_section(n);
-                let n_d = distance_contribution(minus_section);
-                d_denom += n_d;
-                d_numer += n_d;
-                annotated_minus_line.push((deletion, minus_section));
-                annotated_plus_line.push((insertion, plus_section(n)));
-                minus_op_prev = deletion;
-                plus_op_prev = insertion;
-            }
             align::Operation::Insertion => {
-                let plus_section = plus_section(n);
+                let plus_section = plus_section(n, &mut y_offset);
                 let n_d = distance_contribution(plus_section);
                 d_denom += n_d;
                 d_numer += n_d;
@@ -631,13 +611,13 @@ mod tests {
             vec!["fn coalesce_edits<'a, 'b, EditOperation>("],
             (
                 vec![vec![
-                    (MinusNoop, "fn coalesce_edits<'a"),
-                    (MinusNoop, ", EditOperation>("),
+                    (MinusNoop, "fn coalesce_edits<'a, "),
+                    (MinusNoop, "EditOperation>("),
                 ]],
                 vec![vec![
-                    (PlusNoop, "fn coalesce_edits<'a"),
-                    (Insertion, ", 'b"),
-                    (PlusNoop, ", EditOperation>("),
+                    (PlusNoop, "fn coalesce_edits<'a, "),
+                    (Insertion, "'b, "),
+                    (PlusNoop, "EditOperation>("),
                 ]],
             ),
             0.66,
@@ -652,15 +632,15 @@ mod tests {
             (
                 vec![vec![
                     (MinusNoop, "for _ in range(0, "),
-                    (MinusNoop, "options[\"count\"]"),
-                    (MinusNoop, "):"),
+                    (MinusNoop, "options[\"count\"])"),
+                    (MinusNoop, ":"),
                 ]],
                 vec![vec![
                     (PlusNoop, "for _ in range(0, "),
                     (Insertion, "int("),
-                    (PlusNoop, "options[\"count\"]"),
+                    (PlusNoop, "options[\"count\"])"),
                     (Insertion, ")"),
-                    (PlusNoop, "):"),
+                    (PlusNoop, ":"),
                 ]],
             ),
             0.3,
@@ -673,8 +653,8 @@ mod tests {
             vec!["a a"],
             vec!["a b a"],
             (
-                vec![vec![(MinusNoop, "a"), (MinusNoop, " a")]],
-                vec![vec![(PlusNoop, "a"), (Insertion, " b"), (PlusNoop, " a")]],
+                vec![vec![(MinusNoop, "a "), (MinusNoop, "a")]],
+                vec![vec![(PlusNoop, "a "), (Insertion, "b "), (PlusNoop, "a")]],
             ),
             1.0,
         );
@@ -682,8 +662,8 @@ mod tests {
             vec!["a a"],
             vec!["a b b a"],
             (
-                vec![vec![(MinusNoop, "a"), (MinusNoop, " a")]],
-                vec![vec![(PlusNoop, "a"), (Insertion, " b b"), (PlusNoop, " a")]],
+                vec![vec![(MinusNoop, "a "), (MinusNoop, "a")]],
+                vec![vec![(PlusNoop, "a "), (Insertion, "b b "), (PlusNoop, "a")]],
             ),
             1.0,
         );
@@ -698,20 +678,17 @@ mod tests {
                 // TODO: Coalesce runs of the same operation.
                 vec![vec![
                     (MinusNoop, "so it is safe to read "),
-                    (Deletion, "the"),
+                    (Deletion, "the commit"),
                     (Deletion, " "),
-                    (Deletion, "commit"),
-                    (Deletion, " "),
-                    (Deletion, "number "),
-                    (MinusNoop, "from any one of them."),
+                    (Deletion, "number"),
+                    (MinusNoop, " from any one of them."),
                 ]],
                 vec![vec![
                     (PlusNoop, "so it is safe to read "),
                     (Insertion, "build"),
                     (Insertion, " "),
                     (Insertion, "info"),
-                    (Insertion, " "),
-                    (PlusNoop, "from any one of them."),
+                    (PlusNoop, " from any one of them."),
                 ]],
             ),
             1.0,
@@ -735,7 +712,6 @@ mod tests {
     }
 
     #[test]
-    #[ignore]
     fn test_infer_edits_12() {
         assert_edits(
             vec!["                     (xxxxxxxxx, \"build info\"),"],
@@ -755,6 +731,119 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_infer_edits_13() {
+        assert_paired_edits(
+            vec!["'b '", "[element,]"],
+            vec!["' b'", "[element],"],
+            (
+                vec![
+                    vec![
+                        (MinusNoop, "'"),
+                        (Deletion, "b"),
+                        (MinusNoop, " "),
+                        (MinusNoop, "'"),
+                    ],
+                    vec![(MinusNoop, "[element"), (Deletion, ","), (MinusNoop, "]")],
+                ],
+                vec![
+                    vec![
+                        (PlusNoop, "'"),
+                        (PlusNoop, " "),
+                        (Insertion, "b"),
+                        (PlusNoop, "'"),
+                    ],
+                    vec![(PlusNoop, "[element"), (PlusNoop, "]"), (Insertion, ",")],
+                ],
+            ),
+        );
+    }
+
+    #[test]
+    fn test_infer_edits_14() {
+        assert_paired_edits(
+            vec!["a b c d ", "p "],
+            vec!["x y c z ", "q r"],
+            (
+                vec![
+                    vec![
+                        (MinusNoop, ""),
+                        (Deletion, "a"),
+                        (Deletion, " "),
+                        (Deletion, "b"),
+                        (MinusNoop, " c "),
+                        (Deletion, "d"),
+                        (MinusNoop, " "),
+                    ],
+                    vec![(MinusNoop, ""), (Deletion, "p"), (Deletion, " ")],
+                ],
+                vec![
+                    vec![
+                        (PlusNoop, ""),
+                        (Insertion, "x"),
+                        (Insertion, " "),
+                        (Insertion, "y"),
+                        (PlusNoop, " c "),
+                        (Insertion, "z"),
+                        (PlusNoop, " "),
+                    ],
+                    vec![
+                        (PlusNoop, ""),
+                        (Insertion, "q"),
+                        (Insertion, " "),
+                        (Insertion, "r"),
+                    ],
+                ],
+            ),
+        );
+    }
+
+    #[test]
+    fn test_infer_edits_15() {
+        assert_paired_edits(
+            vec![r#"printf "%s\n" s y y | git add -p &&"#],
+            vec!["test_write_lines s y y | git add -p &&"],
+            (
+                vec![vec![
+                    (MinusNoop, ""),
+                    (Deletion, r#"printf "%s\n""#),
+                    (MinusNoop, " s y y | git add -p &&"),
+                ]],
+                vec![vec![
+                    (PlusNoop, ""),
+                    (Insertion, "test_write_lines"),
+                    (PlusNoop, " s y y | git add -p &&"),
+                ]],
+            ),
+        );
+    }
+
+    #[test]
+    fn test_infer_edits_16() {
+        assert_edits(
+            vec!["a a a a a a b b b"],
+            vec!["c a a a a a a c c"],
+            (
+                vec![vec![
+                    (MinusNoop, ""),
+                    (MinusNoop, "a a a a a a "),
+                    (Deletion, "b b"),
+                    (Deletion, " "),
+                    (Deletion, "b"),
+                ]],
+                vec![vec![
+                    (PlusNoop, ""),
+                    (Insertion, "c "),
+                    (PlusNoop, "a a a a a a "),
+                    (Insertion, "c"),
+                    (Insertion, " "),
+                    (Insertion, "c"),
+                ]],
+            ),
+            1.0,
+        );
+    }
+
     fn assert_edits(
         minus_lines: Vec<&str>,
         plus_lines: Vec<&str>,
diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs
index 8885043..bbdeb59 100644
--- a/src/features/line_numbers.rs
+++ b/src/features/line_numbers.rs
@@ -167,12 +167,12 @@ impl<'a> LineNumbersData<'a> {
             format_data: MinusPlus::new(
                 format::parse_line_number_format(
                     &format[Left],
-                    &*LINE_NUMBERS_PLACEHOLDER_REGEX,
+                    &LINE_NUMBERS_PLACEHOLDER_REGEX,
                     false,
                 ),
                 format::parse_line_number_format(
                     &format[Right],
-                    &*LINE_NUMBERS_PLACEHOLDER_REGEX,
+                    &LINE_NUMBERS_PLACEHOLDER_REGEX,
                     insert_center_space_on_odd_width,
                 ),
             ),
@@ -797,6 +797,8 @@ pub mod tests {
             "@",
             "--wrap-right-symbol",
             "@",
+            "--wrap-right-prefix-symbol",
+            ">",
         ])
         .with_input(DIFF_PLUS_MINUS_WITH_1_CONTEXT_DIFF)
         .expect_after_header(
@@ -816,6 +818,8 @@ pub mod tests {
             "@",
             "--wrap-right-symbol",
             "@",
+            "--wrap-right-prefix-symbol",
+            ">",
         ];
 
         DeltaTest::with_args(cfg)
@@ -824,7 +828,7 @@ pub mod tests {
                 r#"
                 │  1 │abc            │  1 │abc
                 │  2 │a = one side   │  2 │a = one longer@
-                │    │               │    │ side
+                │    │               │    │         > side
                 │  3 │xyz            │  3 │xyz"#,
             );
 
@@ -834,7 +838,7 @@ pub mod tests {
                 r#"
                 │  1 │abc            │  1 │abc
                 │  2 │a = one longer@│  2 │a = one side
-                │    │ side          │    │
+                │    │         > side│    │
                 │  3 │xyz            │  3 │xyz"#,
             );
 
@@ -975,4 +979,13 @@ index 223ca50..367a6f6 100644
 -a = other one
 +a = right side which is longer
  xyz";
+
+    pub const TWO_MINUS_LINES_UNICODE_DIFF: &str = "\
+diff --git a/a.py b/a.py
+index 8b0d958..e69de29 100644
+--- a/a.txt
++++ b/b.txt
+@@ -1,1 +0,0 @@
+-一二三
+";
 }
diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs
index f175fa2..ac2de08 100644
--- a/src/features/side_by_side.rs
+++ b/src/features/side_by_side.rs
@@ -1,6 +1,6 @@
 use itertools::Itertools;
 use syntect::highlighting::Style as SyntectStyle;
-use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
 
 use crate::ansi;
 use crate::cli;
@@ -75,12 +75,10 @@ pub fn available_line_width(
 }
 
 pub fn line_is_too_long(line: &str, line_width: usize) -> bool {
-    let line_sum = line.graphemes(true).count();
-
     debug_assert!(line.ends_with('\n'));
-    // `line_sum` is too large because a trailing newline is present,
-    // so allow one more character.
-    line_sum > line_width + 1
+
+    // graphemes will take care of newlines
+    line.width() > line_width
 }
 
 /// Return whether any of the input lines is too long, and a data
@@ -715,4 +713,58 @@ pub mod tests {
             │  2 │b = 2         │  2 │bb = 2        "#,
         );
     }
+
+    #[test]
+    fn test_two_minus_lines_unicode_truncated() {
+        DeltaTest::with_args(&[
+            "--side-by-side",
+            "--wrap-max-lines",
+            "2",
+            "--width",
+            "16",
+            "--line-fill-method=spaces",
+        ])
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
+        .with_input(TWO_MINUS_LINES_UNICODE_DIFF)
+        .expect_after_header(
+            r#"
+            │  1 │↵ │    │
+            │    │↵ │    │
+            │    │ >│    │"#,
+        );
+
+        DeltaTest::with_args(&[
+            "--side-by-side",
+            "--wrap-max-lines",
+            "2",
+            "--width",
+            "17",
+            "--line-fill-method=spaces",
+        ])
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
+        .with_input(TWO_MINUS_LINES_UNICODE_DIFF)
+        .expect_after_header(
+            r#"
+            │  1 │↵ │    │
+            │    │↵ │    │
+            │    │ >│    │"#,
+        );
+
+        DeltaTest::with_args(&[
+            "--side-by-side",
+            "--wrap-max-lines",
+            "2",
+            "--width",
+            "18",
+            "--line-fill-method=spaces",
+        ])
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
+        .with_input(TWO_MINUS_LINES_UNICODE_DIFF)
+        .expect_after_header(
+            r#"
+            │  1 │一↵│    │
+            │    │二↵│    │
+            │    │三 │    │"#,
+        );
+    }
 }
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs
index 0e21f6b..a1ec97b 100644
--- a/src/handlers/blame.rs
+++ b/src/handlers/blame.rs
@@ -43,7 +43,7 @@ impl<'a> StateMachine<'a> {
                 // Format blame metadata
                 let format_data = format::parse_line_number_format(
                     &self.config.blame_format,
-                    &*BLAME_PLACEHOLDER_REGEX,
+                    &BLAME_PLACEHOLDER_REGEX,
                     false,
                 );
                 let mut formatted_blame_metadata =
diff --git a/src/handlers/diff_header.rs b/src/handlers/diff_header.rs
index 5038efd..77daf84 100644
--- a/src/handlers/diff_header.rs
+++ b/src/handlers/diff_header.rs
@@ -88,7 +88,7 @@ impl<'a> StateMachine<'a> {
             parse_diff_header_line(&self.line, self.source == Source::GitDiff);
 
         self.minus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config)
-            .map(|p| p.to_string_lossy().to_owned().to_string())
+            .map(|p| p.to_string_lossy().into_owned())
             .unwrap_or(path_or_mode);
         self.minus_file_event = file_event;
 
@@ -125,7 +125,7 @@ impl<'a> StateMachine<'a> {
             parse_diff_header_line(&self.line, self.source == Source::GitDiff);
 
         self.plus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config)
-            .map(|p| p.to_string_lossy().to_owned().to_string())
+            .map(|p| p.to_string_lossy().into_owned())
             .unwrap_or(path_or_mode);
         self.plus_file_event = file_event;
         self.painter
@@ -162,8 +162,7 @@ impl<'a> StateMachine<'a> {
         let mut handled_line = false;
         let (_mode_info, file_event) =
             parse_diff_header_line(&self.line, self.source == Source::GitDiff);
-        let name = get_repeated_file_path_from_diff_line(&self.diff_line)
-            .unwrap_or_else(|| "".to_string());
+        let name = get_repeated_file_path_from_diff_line(&self.diff_line).unwrap_or_default();
         match file_event {
             FileEvent::Removed => {
                 self.minus_file = name;
@@ -242,8 +241,7 @@ impl<'a> StateMachine<'a> {
                 _ => Cow::from(file),
             };
             let label = format_label(&self.config.file_modified_label);
-            let name = get_repeated_file_path_from_diff_line(&self.diff_line)
-                .unwrap_or_else(|| "".to_string());
+            let name = get_repeated_file_path_from_diff_line(&self.diff_line).unwrap_or_default();
             let line = format!("{}{}", label, format_file(&name));
             write_generic_diff_header_header_line(
                 &line,
@@ -374,6 +372,15 @@ fn get_repeated_file_path_from_diff_line(line: &str) -> Option<String> {
     None
 }
 
+fn remove_surrounding_quotes(path: &str) -> &str {
+    if path.starts_with('"') && path.ends_with('"') {
+        // Indexing into the UTF-8 string is safe because of the previous test
+        &path[1..path.len() - 1]
+    } else {
+        path
+    }
+}
+
 fn _parse_file_path(s: &str, git_diff_name: bool) -> String {
     // It appears that, if the file name contains a space, git appends a tab
     // character in the diff metadata lines, e.g.
@@ -382,13 +389,15 @@ fn _parse_file_path(s: &str, git_diff_name: bool) -> String {
     // index·d00491f..0cfbf08·100644␊
     // ---·a/a·b├──┤␊
     // +++·b/c·d├──┤␊
-    match s.strip_suffix('\t').unwrap_or(s) {
+    let path = match s.strip_suffix('\t').unwrap_or(s) {
         path if path == "/dev/null" => "/dev/null",
         path if git_diff_name && DIFF_PREFIXES.iter().any(|s| path.starts_with(s)) => &path[2..],
         path if git_diff_name => path,
         path => path.split('\t').next().unwrap_or(""),
-    }
-    .to_string()
+    };
+    // When a path contains non-ASCII characters, a backslash, or a quote then it is quoted,
+    // so remove these quotes. Characters may also be escaped, but these are left as-is.
+    remove_surrounding_quotes(path).to_string()
 }
 
 pub fn get_file_change_description_from_file_paths(
@@ -551,6 +560,11 @@ mod tests {
             parse_diff_header_line("+++ src/delta.rs", true),
             ("src/delta.rs".to_string(), FileEvent::Change)
         );
+
+        assert_eq!(
+            parse_diff_header_line("+++ \".\\delta.rs\"", true),
+            (".\\delta.rs".to_string(), FileEvent::Change)
+        );
     }
 
     #[test]
diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs
index 986de38..2eadd1f 100644
--- a/src/handlers/grep.rs
+++ b/src/handlers/grep.rs
@@ -439,7 +439,7 @@ pub fn parse_grep_line(line: &str) -> Option<GrepLine> {
                 &*GREP_LINE_REGEX_ASSUMING_NO_INTERNAL_SEPARATOR_CHARS,
             ]
             .iter()
-            .find_map(|regex| _parse_grep_line(*regex, line)),
+            .find_map(|regex| _parse_grep_line(regex, line)),
             _ => None,
         }
     }
diff --git a/src/subcommands/diff.rs b/src/subcommands/diff.rs
index 6c7fbf0..30d4e64 100644
--- a/src/subcommands/diff.rs
+++ b/src/subcommands/diff.rs
@@ -40,7 +40,7 @@ pub fn diff(
 
     let diff_process = process::Command::new(diff_path)
         .args(&diff_cmd[1..])
-        .args(&[minus_file, plus_file])
+        .args([minus_file, plus_file])
         .stdout(process::Stdio::piped())
         .spawn();
 
diff --git a/src/subcommands/list_syntax_themes.rs b/src/subcommands/list_syntax_themes.rs
index 34a7b0b..e8bdc31 100644
--- a/src/subcommands/list_syntax_themes.rs
+++ b/src/subcommands/list_syntax_themes.rs
@@ -19,11 +19,11 @@ pub fn _list_syntax_themes_for_humans(writer: &mut dyn Write) -> std::io::Result
     let assets = utils::bat::assets::load_highlighting_assets();
 
     writeln!(writer, "Light syntax themes:")?;
-    for theme in assets.themes().filter(|t| is_light_syntax_theme(*t)) {
+    for theme in assets.themes().filter(|t| is_light_syntax_theme(t)) {
         writeln!(writer, "    {}", theme)?;
     }
     writeln!(writer, "\nDark syntax themes:")?;
-    for theme in assets.themes().filter(|t| !is_light_syntax_theme(*t)) {
+    for theme in assets.themes().filter(|t| !is_light_syntax_theme(t)) {
         writeln!(writer, "    {}", theme)?;
     }
     writeln!(
@@ -35,7 +35,7 @@ pub fn _list_syntax_themes_for_humans(writer: &mut dyn Write) -> std::io::Result
 
 pub fn _list_syntax_themes_for_machines(writer: &mut dyn Write) -> std::io::Result<()> {
     let assets = utils::bat::assets::load_highlighting_assets();
-    for theme in assets.themes().sorted_by_key(|t| is_light_syntax_theme(*t)) {
+    for theme in assets.themes().sorted_by_key(|t| is_light_syntax_theme(t)) {
         writeln!(
             writer,
             "{}\t{}",
diff --git a/src/utils/path.rs b/src/utils/path.rs
index 6891e81..524ef91 100644
--- a/src/utils/path.rs
+++ b/src/utils/path.rs
@@ -29,7 +29,7 @@ pub fn absolute_path(relative_path: &str, config: &Config) -> Option<PathBuf> {
 pub fn relativize_path_maybe(path: &str, config: &Config) -> Option<PathBuf> {
     if config.relative_paths && !calling_process().paths_in_input_are_relative_to_cwd() {
         if let Some(base) = config.cwd_relative_to_repo_root.as_deref() {
-            pathdiff::diff_paths(&path, base)
+            pathdiff::diff_paths(path, base)
         } else {
             None
         }
diff --git a/src/wrapping.rs b/src/wrapping.rs
index 771d7f7..d9b0ff8 100644
--- a/src/wrapping.rs
+++ b/src/wrapping.rs
@@ -1,5 +1,6 @@
 use syntect::highlighting::Style as SyntectStyle;
 use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
 
 use crate::cli;
 use crate::config::INLINE_SYMBOL_WIDTH_1;
@@ -202,11 +203,21 @@ where
 
         let (style, text, graphemes) = stack
             .pop()
-            .map(|(style, text)| (style, text, text.grapheme_indices(true).collect::<Vec<_>>()))
+            .map(|(style, text)| {
+                (
+                    style,
+                    text,
+                    text.graphemes(true)
+                        .map(|item| (item.len(), item.width()))
+                        .collect::<Vec<_>>(),
+                )
+            })
             .unwrap();
 
-        let new_len = curr_line.len + graphemes.len();
+        let graphemes_width: usize = graphemes.iter().map(|(_, w)| w).sum();
+        let new_len = curr_line.len + graphemes_width;
 
+        #[allow(clippy::comparison_chain)]
         let must_split = if new_len < line_width {
             curr_line.push_and_set_len((style, text), new_len);
             false
@@ -228,15 +239,6 @@ where
                 }
                 _ => true,
             }
-        } else if new_len == line_width + 1 && stack.is_empty() {
-            // If the one overhanging char is '\n' then keep it on the current line.
-            if text.ends_with('\n') {
-                // Do not count the included '\n': - 1
-                curr_line.push_and_set_len((style, text), new_len - 1);
-                false
-            } else {
-                true
-            }
         } else {
             true
         };
@@ -244,16 +246,29 @@ where
         // Text must be split, one part (or just `wrap_symbol`) is added to the
         // current line, the other is pushed onto the stack.
         if must_split {
-            let grapheme_split_pos = graphemes.len() - (new_len - line_width) - 1;
+            let mut width_left = graphemes_width
+                .saturating_sub(new_len - line_width)
+                .saturating_sub(wrap_config.left_symbol.width());
 
             // The length does not matter anymore and `curr_line` will be reset
             // at the end, so move the line segments out.
             let mut line_segments = curr_line.line_segments;
 
-            let next_line = if grapheme_split_pos == 0 {
+            let next_line = if width_left == 0 {
                 text
             } else {
-                let byte_split_pos = graphemes[grapheme_split_pos].0;
+                let mut byte_split_pos = 0;
+                // After loop byte_split_pos may still equal to 0. If width_left
+                // is less than the width of first character, We can't display it.
+                for &(item_len, item_width) in graphemes.iter() {
+                    if width_left >= item_width {
+                        byte_split_pos += item_len;
+                        width_left -= item_width;
+                    } else {
+                        break;
+                    }
+                }
+
                 let this_line = &text[..byte_split_pos];
                 line_segments.push((style, this_line));
                 &text[byte_split_pos..]
@@ -273,7 +288,9 @@ where
     if result.len() == 1 && curr_line.has_text() {
         let current_permille = (curr_line.text_len() * 1000) / line_width;
 
-        let pad_len = line_width.saturating_sub(curr_line.text_len());
+        // pad line will add a wrap_config.right_prefix_symbol
+        let pad_len = line_width
+            .saturating_sub(curr_line.text_len() + wrap_config.right_prefix_symbol.width());
 
         if wrap_config.use_wrap_right_permille > current_permille && pad_len > 0 {
             // The inserted spaces, which align a line to the right, point into this string.
@@ -785,7 +802,7 @@ mod tests {
         let lines = wrap_test(&cfg, line, 11);
         assert_eq!(lines.len(), 2);
         assert_eq!(lines[0].last().unwrap().1, WR);
-        assert_eq!(lines[1], [(*SD, "         "), (*SD, ">"), (*S1, "ab")]);
+        assert_eq!(lines[1], [(*SD, "        "), (*SD, ">"), (*S1, "ab")]);
     }
 
     #[test]
@@ -801,7 +818,7 @@ mod tests {
                 lines,
                 vec![
                     vec![(*S1, "012"), (*S2, "34"), (*SD, WR)],
-                    vec![(*SD, "    "), (*SD, RA), (*S2, "56")]
+                    vec![(*SD, "   "), (*SD, RA), (*S2, "56")]
                 ]
             );
         }
@@ -940,13 +957,19 @@ mod tests {
         );
 
         // Not working: Tailored grapheme clusters: क्षि  = क् + षि
-        let line = vec![(*S1, "abc"), (*S2, "deநி"), (*S1, "ghij")];
+        //
+        // Difference compare to previous example (even they may look like the
+        // same width in text editor.) :
+        //
+        // width நி: 2
+        // width ö̲: 1
+        let line = vec![(*S1, "abc"), (*S2, "dநி"), (*S1, "ghij")];
         let lines = wrap_test(&cfg, line, 4);
         assert_eq!(
             lines,
             vec![
                 vec![(*S1, "abc"), (*SD, W)],
-                vec![(*S2, "deநி"), (*SD, W)],
+                vec![(*S2, "dநி"), (*SD, W)],
                 vec![(*S1, "ghij")]
             ]
         );


Test patch:
diff --git a/src/ansi/console_tests.rs b/src/ansi/console_tests.rs
index d637231..09f5647 100644
--- a/src/ansi/console_tests.rs
+++ b/src/ansi/console_tests.rs
@@ -54,10 +54,11 @@ mod tests {
             &truncate_str(&s, 10, "..."),
             &format!("foo {}...", style("bar").red().force_styling(true))
         );
+        // `バ`(width = 2) will be truncate to 1, we use space to fill
         let s = format!("foo {}", style("バー").red().force_styling(true));
         assert_eq!(
             &truncate_str(&s, 5, ""),
-            &format!("foo {}", style("").red().force_styling(true))
+            &format!("foo {}", style(" ").red().force_styling(true))
         );
         let s = format!("foo {}", style("バー").red().force_styling(true));
         assert_eq!(