[meld] Identify changed chunks in comparison updates and use when highlighting



commit aa615a291534fad4b4870481a34a9a5754c62dfb
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Mon Jun 25 07:38:38 2012 +1000

    Identify changed chunks in comparison updates and use when highlighting
    
    With this change, we now identify which chunks in an existing
    comparison are changed by a comparison update. This information is
    then passed on to the inline highlighting updater and used to only
    re-highlight changed regions of our text buffers.
    
    As a result of these changes, we now also avoid manually updating
    highlighting and instead simply perform highlighting in a callback.

 meld/diffutil.py |   55 ++++++++++++++++++-
 meld/filediff.py |  158 ++++++++++++++++++++++++++++--------------------------
 2 files changed, 135 insertions(+), 78 deletions(-)
---
diff --git a/meld/diffutil.py b/meld/diffutil.py
index af6f3e2..62ebe4d 100644
--- a/meld/diffutil.py
+++ b/meld/diffutil.py
@@ -64,7 +64,8 @@ class Differ(gobject.GObject):
     """Utility class to hold diff2 or diff3 chunks"""
 
     __gsignals__ = {
-        'diffs-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
+        'diffs-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                                                    (object,)),
     }
 
     _matcher = MyersSequenceMatcher
@@ -76,6 +77,8 @@ class Differ(gobject.GObject):
         self.seqlength = [0, 0, 0]
         self.diffs = [[], []]
         self.conflicts = []
+        self._old_merge_cache = set()
+        self._changed_chunks = tuple()
         self._merge_cache = []
         self._line_cache = [[], [], []]
         self.ignore_blanks = False
@@ -96,6 +99,18 @@ class Differ(gobject.GObject):
                                         consume_blank_lines(c[1], texts, 1, 2))
             self._merge_cache = [x for x in self._merge_cache if x != (None, None)]
 
+        # Calculate chunks that were added (in the new but not the old merge
+        # cache), removed (in the old but not the new merge cache) and changed
+        # (where the edit actually occurred, *and* the chunk is still around).
+        # This information is used by the inline highlighting mechanism to
+        # avoid re-highlighting existing chunks.
+        removed_chunks = self._old_merge_cache - set(self._merge_cache)
+        added_chunks = set(self._merge_cache) - self._old_merge_cache
+        modified_chunks = self._changed_chunks
+        if modified_chunks in removed_chunks:
+            modified_chunks = tuple()
+        chunk_changes = (removed_chunks, added_chunks, modified_chunks)
+
         mergeable0, mergeable1 = False, False
         for (c0, c1) in self._merge_cache:
             mergeable0 = mergeable0 or (c0 is not None and c0[0] != 'conflict')
@@ -113,7 +128,7 @@ class Differ(gobject.GObject):
                 self.conflicts.append(i)
 
         self._update_line_cache()
-        self.emit("diffs-changed")
+        self.emit("diffs-changed", chunk_changes)
 
     def _update_line_cache(self):
         for i, l in enumerate(self.seqlength):
@@ -170,6 +185,42 @@ class Differ(gobject.GObject):
         if sequence == 2 or (sequence == 1 and self.num_sequences == 3):
             self._change_sequence(1, sequence, startidx, sizechange, texts)
         self.seqlength[sequence] += sizechange
+
+        def offset(c, start, o1, o2):
+            """Offset a chunk by o1/o2 if it's after the inserted lines"""
+            start_a = c.start_a + (o1 if c.start_a > start else 0)
+            end_a = c.end_a + (o1 if c.end_a > start else 0)
+            start_b = c.start_b + (o2 if c.start_b > start else 0)
+            end_b = c.end_b + (o2 if c.end_b > start else 0)
+            return DiffChunk._make((c.tag, start_a, end_a, start_b, end_b))
+
+        # Calculate the expected differences in the chunk set if no cascading
+        # changes occur, making sure to not include the changed chunk itself
+        self._old_merge_cache = set()
+        self._changed_chunks = tuple()
+        chunk_changed = False
+        for (c1, c2) in self._merge_cache:
+            print c1, c2
+            if sequence == 0:
+                if c1.start_b <= startidx < c1.end_b:
+                    chunk_changed = True
+                c1 = offset(c1, startidx, 0, sizechange)
+            elif sequence == 2:
+                if c2.start_b <= startidx < c2.end_b:
+                    chunk_changed = True
+                c2 = offset(c2, startidx, 0, sizechange)
+            else:  # sequence == 1
+                if c1.start_a <= startidx < c1.end_a:
+                    chunk_changed = True
+                c1 = offset(c1, startidx, sizechange, 0)
+                if self.num_sequences == 3:
+                    c2 = offset(c2, startidx, sizechange, 0)
+            if chunk_changed:
+                assert not self._changed_chunks
+                self._changed_chunks = (c1, c2)
+                chunk_changed = False
+            self._old_merge_cache.add((c1, c2))
+
         self._update_merge_cache(texts)
 
     def _locate_chunk(self, whichdiffs, sequence, line):
diff --git a/meld/filediff.py b/meld/filediff.py
index 2b1398f..7f5b857 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -176,7 +176,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         self.linediffer = self.differ()
         self.linediffer.ignore_blanks = self.prefs.ignore_blank_lines
         self.in_nested_textview_gutter_expose = False
-        self._inline_cache = set()
         self._cached_match = CachedSequenceMatcher()
         self.anim_source_id = [None for buf in self.textbuffer]
         self.animating_chunks = [[] for buf in self.textbuffer]
@@ -633,7 +632,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             if focused_pane != -1:
                 self.on_cursor_position_changed(self.textbuffer[focused_pane],
                                                 None, True)
-            self.update_highlighting()
             self.queue_draw()
 
     def _filter_text(self, txt):
@@ -790,7 +788,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
 
     def on_textbuffer__end_user_action(self, *buffer):
         self.undosequence.end_group()
-        self.update_highlighting()
 
     def on_text_insert_text(self, buf, it, text, textlen):
         text = unicode(text, 'utf8')
@@ -925,7 +922,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
            If an element is None, the text of a pane is left as is.
         """
         self._disconnect_buffer_handlers()
-        self._inline_cache = set()
         for i,f in enumerate(files):
             if f:
                 absfile = os.path.abspath(f)
@@ -1039,7 +1035,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             buf.place_cursor(buf.get_start_iter())
         self.scheduler.add_task(lambda: self.next_diff(gtk.gdk.SCROLL_DOWN), True)
         self.queue_draw()
-        self.update_highlighting()
         self._connect_buffer_handlers()
         self._set_merge_action_sensitivity()
 
@@ -1071,7 +1066,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
     def refresh_comparison(self):
         """Refresh the view by clearing and redoing all comparisons"""
         self._disconnect_buffer_handlers()
-        self._inline_cache = set()
         self.linediffer.clear()
         self.queue_draw()
         self.scheduler.add_task(self._diff_files().next)
@@ -1092,7 +1086,88 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             mergeable = (False, False)
         self.actiongroup.get_action("MergeAll").set_sensitive(mergeable[0] or mergeable[1])
 
-    def on_diffs_changed(self, linediffer):
+    def on_diffs_changed(self, linediffer, chunk_changes):
+        removed_chunks, added_chunks, modified_chunks = chunk_changes
+
+        # We need to clear removed and modified chunks, and need to
+        # re-highlight added and modified chunks.
+        need_clearing = sorted(list(removed_chunks))
+        need_highlighting = sorted(list(added_chunks) + [modified_chunks])
+
+        alltags = [b.get_tag_table().lookup("inline") for b in self.textbuffer]
+
+        for chunk in need_clearing:
+            for i, c in enumerate(chunk):
+                if not c or c[0] != "replace":
+                    continue
+                to_idx = 2 if i == 1 else 0
+                bufs = self.textbuffer[1], self.textbuffer[to_idx]
+                tags = alltags[1], alltags[to_idx]
+
+                starts = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[1], c[3]))]
+                ends = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[2], c[4]))]
+                bufs[0].remove_tag(tags[0], starts[0], ends[0])
+                bufs[1].remove_tag(tags[1], starts[1], ends[1])
+
+        for chunk in need_highlighting:
+            clear = chunk == modified_chunks
+            for i, c in enumerate(chunk):
+                if not c or c[0] != "replace":
+                    continue
+                to_idx = 2 if i == 1 else 0
+                bufs = self.textbuffer[1], self.textbuffer[to_idx]
+                tags = alltags[1], alltags[to_idx]
+
+                starts = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[1], c[3]))]
+                ends = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[2], c[4]))]
+
+                # We don't use self.buffer_texts here, as removing line
+                # breaks messes with inline highlighting in CRLF cases
+                text1 = bufs[0].get_text(starts[0], ends[0], False)
+                text1 = unicode(text1, 'utf8')
+                textn = bufs[1].get_text(starts[1], ends[1], False)
+                textn = unicode(textn, 'utf8')
+
+                # For very long sequences, bail rather than trying a very slow comparison
+                inline_limit = 8000 # arbitrary constant
+                if len(text1) + len(textn) > inline_limit:
+                    for i in range(2):
+                        bufs[i].apply_tag(tags[i], starts[i], ends[i])
+                    continue
+
+                def apply_highlight(bufs, tags, starts, matches):
+                    # Remove equal matches of size greater than 3; highlight
+                    # the remainder.
+                    matches = [m for m in matches if m.tag != "equal" or
+                        (m.end_a - m.start_a < 3) or (m.end_b - m.start_b < 3)]
+
+                    # FIXME: At this point, there's no guarantee that the
+                    # gtk.TextIters are valid. Any rapid typing and they're
+                    # not... Should used marks instead, and also probably keep
+                    # a list of being-highlighted blocks, and ensure that
+                    # there's only ever one callback per block, cancelling the
+                    # old one and restarting it on each change.
+
+                    for i in range(2):
+                        start, end = starts[i].copy(), starts[i].copy()
+                        offset = start.get_offset()
+                        for o in matches:
+                            start.set_offset(offset + o[1 + 2 * i])
+                            end.set_offset(offset + o[2 + 2 * i])
+                            bufs[i].apply_tag(tags[i], start, end)
+
+                def highlight_cb(bufs, tags, starts, ends, text1, textn, clear):
+                    matches = self._cached_match(text1, textn)
+                    if clear:
+                        bufs[0].remove_tag(tags[0], starts[0], ends[0])
+                        bufs[1].remove_tag(tags[1], starts[1], ends[1])
+                    apply_highlight(bufs, tags, starts, matches)
+                    return False
+                gobject.idle_add(highlight_cb, bufs, tags, starts, ends, text1, textn, clear)
+
+
+        self._cached_match.clean(self.linediffer.diff_count())
+
         self._set_merge_action_sensitivity()
         if self.linediffer.sequences_identical():
             error_message = True in [m.has_message() for m in self.msgarea_mgr]
@@ -1139,75 +1214,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             self.text_filters = []
             self.refresh_comparison()
 
-    def update_highlighting(self):
-        if not self.undosequence.in_grouped_action():
-            self.scheduler.add_task(self._update_highlighting().next)
-
-    def _update_highlighting(self):
-        alltexts = self.buffer_texts
-        alltags = [b.get_tag_table().lookup("inline") for b in self.textbuffer]
-        progress = [b.create_mark("progress", b.get_start_iter()) for b in self.textbuffer]
-        newcache = set()
-        for chunk in self.linediffer.all_changes():
-            for i,c in enumerate(chunk):
-                if c and c[0] == "replace":
-                    bufs = self.textbuffer[1], self.textbuffer[i*2]
-                    tags = alltags[1], alltags[i*2]
-                    cacheitem = (i, c, tuple(alltexts[1][c[1]:c[2]]), tuple(alltexts[i*2][c[3]:c[4]]))
-                    newcache.add(cacheitem)
-
-                    # Clean interim chunks
-                    starts = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[1], c[3]))]
-                    prog_it0 = bufs[0].get_iter_at_mark(progress[1])
-                    prog_it1 = bufs[1].get_iter_at_mark(progress[i * 2])
-                    bufs[0].remove_tag(tags[0], prog_it0, starts[0])
-                    bufs[1].remove_tag(tags[1], prog_it1, starts[1])
-                    bufs[0].move_mark(progress[1], bufs[0].get_iter_at_line_or_eof(c[2]))
-                    bufs[1].move_mark(progress[i * 2], bufs[1].get_iter_at_line_or_eof(c[4]))
-
-                    if cacheitem in self._inline_cache:
-                        continue
-
-                    ends = [b.get_iter_at_line_or_eof(l) for b, l in zip(bufs, (c[2], c[4]))]
-                    bufs[0].remove_tag(tags[0], starts[0], ends[0])
-                    bufs[1].remove_tag(tags[1], starts[1], ends[1])
-
-                    # We don't use self.buffer_texts here, as removing line
-                    # breaks messes with inline highlighting in CRLF cases
-                    text1 = bufs[0].get_text(starts[0], ends[0], False)
-                    text1 = unicode(text1, 'utf8')
-                    textn = bufs[1].get_text(starts[1], ends[1], False)
-                    textn = unicode(textn, 'utf8')
-
-                    # For very long sequences, bail rather than trying a very slow comparison
-                    inline_limit = 8000 # arbitrary constant
-                    if len(text1) + len(textn) > inline_limit:
-                        for i in range(2):
-                            bufs[i].apply_tag(tags[i], starts[i], ends[i])
-                        continue
-
-                    #print "<<<\n%s\n---\n%s\n>>>" % (text1, textn)
-                    back = (0,0)
-                    for o in self._cached_match(text1, textn):
-                        if o[0] == "equal":
-                            if (o[2]-o[1] < 3) or (o[4]-o[3] < 3):
-                                back = o[4]-o[3], o[2]-o[1]
-                            continue
-                        for i in range(2):
-                            s,e = starts[i].copy(), starts[i].copy()
-                            s.forward_chars( o[1+2*i] - back[i] )
-                            e.forward_chars( o[2+2*i] )
-                            bufs[i].apply_tag(tags[i], s, e)
-                        back = (0,0)
-                    yield 1
-
-        # Clean up trailing lines
-        prog_it = [b.get_iter_at_mark(p) for b, p in zip(self.textbuffer, progress)]
-        for b, tag, start in zip(self.textbuffer, alltags, prog_it):
-            b.remove_tag(tag, start, b.get_end_iter())
-        self._inline_cache = newcache
-        self._cached_match.clean(len(self._inline_cache))
-
     def on_textview_expose_event(self, textview, event):
         if self.num_panes == 1:
             return



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]