3

Let's say I have two files with the following contents:

this is some important text
another different line
if another is needed
here it is

and

this is some important text
we now first add two new lines
in place of the old second
if another is sorely needed
here it is

Now it should be visible that the second file has substituted the second line in the first file with two new lines, and it has a minor edit on the third line (sorely). My problem is that when vimdiffing these files, the result is quite suboptimal:

erroneous vimdiff

vimdiff reports the second and third line in both files as completely changed, and a whole new line added in the second file. Instead, I'd have preferred to see the second line in file 1 marked as deleted, and its 3rd line marked as edited. Is there a way to make vimdiff do so?

I'm using vim 9.0, and my current diffopt is

internal,filler,closeoff,algorithm:histogram,indent-heuristic,vertical
4
  • Welcome to Vi and Vim SE. Note that diff tools run on lines. What you see is just surprising syntax highlighting. I added the respective tag. I hope this will help our highlight-savvy contributors to find an answer.
    – Friedrich
    Jan 17 at 9:31
  • And another note: you don't show your diffopt but what you add to it. Use :set diffopt? to show the value. As this question is about highlighting, the option's value doesn't matter anyway.
    – Friedrich
    Jan 17 at 9:35
  • 1
    @Friedrich Is it really only about syntax highlighting? The way the lines are compared and matched surely is influenced by the diff algorithm and related options, no? The diff shown here is not wrong, per se, it's just that it's not a good one, which would result by realizing that the second line on the left side is what has been deleted and matching the third line with its edited counterpart.
    – Svalorzen
    Jan 17 at 11:46
  • I do think so but I could be wrong. I ran your example through (among other things) GNU diff and git diff --no-index, both with and without the --minimal option. Result is always the same: +3 lines -2 lines, as above. I'd think the highlighting is applied on the line level.
    – Friedrich
    Jan 17 at 11:54

2 Answers 2

1

The classic diff algorithm focuses on line comparison and produced diff that group equal lines and not lines that are similar. This lead to the line de-synchronization you observe.

I have tested the various builtin algorithm of Vim and it seems that none of them provide the comparison you need.

Vim supports the diffexpr option that let you specify the logic (VimScript function) that compare the files.

More information in :help diff-diffexpr

The git-diff algorithm is able to compare within the line and group similar lines together using the --word-diff option:

git-diff --word-diff file1.txt file2.txt

Remark: The output syntax of git-diff is not the one expected by Vim. A conversion within the diffexpr would be needed.

Update:

  • I confirmed that the unified format -U0 is supported by Vim
  • A @Brabant says Vim perform twice the comparison
    • The first time with two simple files (whose content are respectively line1 and line2) to check the diff
    • The second time with the content of the buffers
  • I have tried to post process the unified format to add synchronization block to help Vim to identify modification inline but that doesn't seems to work.

Here is my try:

set diffexpr=MyDiff()

function MyDiff()
  let prediff = tempname()

  let command = '!git diff --word-diff=plain -U0 ' .. v:fname_in .. ' ' .. v:fname_new .. ' --output ' .. prediff
  silent execute command

  let command = '!post.py --input ' .. prediff .. ' --output ' .. v:fname_out .. ''
  silent execute command

  redraw!
endfunction

Where the post.py that post-process the unified diff file is:

import sys
import argparse
import datetime
import logging
import os
import re


class Sync:
    def __init__(self):
        self.context = ""
        self.apos = 0
        self.bpos = 0

        self.alen = 0
        self.blen = 0

        self.lines = []

    def print(self, g):
        # if not g:
        #     g = sys.stdout

        g.write(f"@@ -{self.apos},{self.alen} +{self.bpos},{self.blen} @@ {self.context}\n")
        for line in self.lines:
            g.write(line + "\n")

def post(args):
    in_path = args.input
    out_path = args.output
    if out_path:
        g = open(out_path, mode="wt")
    else:
        g = sys.stdout

    with open(in_path, mode="rt") as f:
        sync = None
        acur = 0
        bcur = 0
        for line in f:
            line = line.rstrip("\r\n")
            m = re.match(r'^@@ -(\d*),(\d+) \+(\d*),(\d+) @@ (.*)', line)
            if m:
                if sync:
                    sync.print(g)

                sync = Sync()
                sync.apos = int(m.group(1))
                sync.bpos = int(m.group(3))
                sync.context = m.group(5)

                acur = sync.apos
                bcur = sync.bpos
                continue

            if not sync:
                # print the header
                g.write(line + "\n")
                continue

            new_sync = True
            if line.startswith('[-'):
                new_sync = False
                acur += 1
                sync.alen += 1

            if line.endswith('+}'):
                new_sync = False
                bcur += 1
                sync.blen += 1

            if acur == bcur:
                new_sync = False

            if not new_sync:
                sync.lines.append(line)
                continue

            sync.print(g)
            sync = Sync()
            sync.apos = acur
            sync.bpos = bcur
            sync.lines.append(line)
            sync.alen += 1
            sync.blen += 1

    if sync:
        sync.print(g)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", action="store", help="Input", default="")
    parser.add_argument("--output", action="store", help="Output", default="")

    try:
        args = parser.parse_args()

    except Exception as e:
        logging.error(str(e))
        return

    post(args)


if __name__ == "__main__":
    main()
7
  • It seems that --word-diff=porcelain does output a format that vim can read, but even after trying it the displayed output is the same. I've set diffexpr to a function that calls silent execute "!git diff --no-index -U0 --word-diff=porcelain " .. v:fname_in .. " " .. v:fname_new .. " > " .. v:fname_out
    – Svalorzen
    Jan 17 at 12:33
  • @Svalorzen if that fixes your issue, feel free to post an answer of your own. Self-answers are encouraged. Your code will also be much more readable.
    – Friedrich
    Jan 17 at 12:40
  • @Friedrich I have mentioned that the displayed output is the same, i.e. the problem is not fixed, although I guess now I can make vim use git-diff as the diff source.
    – Svalorzen
    Jan 17 at 12:43
  • 1
    that is expected output. Vim will always first test the diff output with dummy input line1 and line2 Jan 17 at 12:48
  • 2
    BTW: word-diff is not supported by Vim at all Jan 17 at 12:48
1

How would you like to try spotdiff.vim plugin? You can select area to compare partially. Here is your example.

enter image description here

As a side note, you can improve the vimdiff as word or character level with diffchar.vim plugin.

2
  • Interesting I just recommended your spotdiff plugin :-). Nice to see you here. Thanks for your nice diff plugins! Jan 18 at 5:33
  • Looks cool thanks! In my case unfortunately the problem happened within a larger diff when using fugitive's :Gdiffsplit, so I don't think this would fix my issue in general, but it's good to know about :)
    – Svalorzen
    Jan 18 at 9:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.