mercurial/patch.py
changeset 35277 6ba79cf34f5e
parent 35191 a1d2fc32bb99
child 35310 10cce12fdcd3
--- a/mercurial/patch.py	Thu Sep 22 18:23:58 2016 +0900
+++ b/mercurial/patch.py	Thu Oct 26 00:13:38 2017 +0900
@@ -10,6 +10,7 @@
 
 import collections
 import copy
+import difflib
 import email
 import errno
 import hashlib
@@ -2252,6 +2253,7 @@
         'showfunc': get('show_function', 'showfunc'),
         'context': get('unified', getter=ui.config),
     }
+    buildopts['worddiff'] = ui.configbool('experimental', 'worddiff')
 
     if git:
         buildopts['git'] = get('git')
@@ -2463,6 +2465,9 @@
 
 def difflabel(func, *args, **kw):
     '''yields 2-tuples of (output, label) based on the output of func()'''
+    inlinecolor = False
+    if kw.get('opts'):
+        inlinecolor = kw['opts'].worddiff
     headprefixes = [('diff', 'diff.diffline'),
                     ('copy', 'diff.extended'),
                     ('rename', 'diff.extended'),
@@ -2479,6 +2484,9 @@
     head = False
     for chunk in func(*args, **kw):
         lines = chunk.split('\n')
+        matches = {}
+        if inlinecolor:
+            matches = _findmatches(lines)
         for i, line in enumerate(lines):
             if i != 0:
                 yield ('\n', '')
@@ -2506,7 +2514,14 @@
                             if '\t' == token[0]:
                                 yield (token, 'diff.tab')
                             else:
-                                yield (token, label)
+                                if i in matches:
+                                    for l, t in _inlinediff(
+                                                  lines[i].rstrip(),
+                                                  lines[matches[i]].rstrip(),
+                                                  label):
+                                        yield (t, l)
+                                else:
+                                    yield (token, label)
                     else:
                         yield (stripline, label)
                     break
@@ -2515,6 +2530,70 @@
             if line != stripline:
                 yield (line[len(stripline):], 'diff.trailingwhitespace')
 
+def _findmatches(slist):
+    '''Look for insertion matches to deletion and returns a dict of
+    correspondences.
+    '''
+    lastmatch = 0
+    matches = {}
+    for i, line in enumerate(slist):
+        if line == '':
+            continue
+        if line[0] == '-':
+            lastmatch = max(lastmatch, i)
+            newgroup = False
+            for j, newline in enumerate(slist[lastmatch + 1:]):
+                if newline == '':
+                    continue
+                if newline[0] == '-' and newgroup: # too far, no match
+                    break
+                if newline[0] == '+': # potential match
+                    newgroup = True
+                    sim = difflib.SequenceMatcher(None, line, newline).ratio()
+                    if sim > 0.7:
+                        lastmatch = lastmatch + 1 + j
+                        matches[i] = lastmatch
+                        matches[lastmatch] = i
+                        break
+    return matches
+
+def _inlinediff(s1, s2, operation):
+    '''Perform string diff to highlight specific changes.'''
+    operation_skip = '+?' if operation == 'diff.deleted' else '-?'
+    if operation == 'diff.deleted':
+        s2, s1 = s1, s2
+
+    buff = []
+    # we never want to higlight the leading +-
+    if operation == 'diff.deleted' and s2.startswith('-'):
+        label = operation
+        token = '-'
+        s2 = s2[1:]
+        s1 = s1[1:]
+    elif operation == 'diff.inserted' and s1.startswith('+'):
+        label = operation
+        token = '+'
+        s2 = s2[1:]
+        s1 = s1[1:]
+
+    s = difflib.ndiff(re.split(br'(\W)', s2), re.split(br'(\W)', s1))
+    for part in s:
+        if part[0] in operation_skip:
+            continue
+        l = operation + '.highlight'
+        if part[0] in ' ':
+            l = operation
+        if l == label: # contiguous token with same label
+            token += part[2:]
+            continue
+        else:
+            buff.append((label, token))
+            label = l
+            token = part[2:]
+    buff.append((label, token))
+
+    return buff
+
 def diffui(*args, **kw):
     '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
     return difflabel(diff, *args, **kw)