[meld] Add detailed cursor handling, and use for Up/Down sensitivity setting



commit bafa162612031596861febe99f6c5f73a9c6b481
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Sat Sep 26 18:10:08 2009 +1000

    Add detailed cursor handling, and use for Up/Down sensitivity setting
    
    There are several pieces of cursor-related information that we need: its
    current location, which chunk (if any) it is currently in, and which chunks
    are above and below it. The first is used for the status bar and for
    determining the other two. The second may be used for a status-bar display
    of where in the list of changes we currently are, and also for keyboard
    operations (such as insert/delete/replace chunk). The third is useful in
    skipping between changes, and in sensitivity setting.
    
    This patch introduces a CursorDetails structure that stores all of the
    interesting information, updates these details as necessary, and uses the
    next/previous chunk information for sensitivity setting.
    
    Differ: Add chunk by-line locator and by-index accessor for cursor handling
    CursorDetails: New simple class holding cursor data
    FileDiff: Use CursorDetails to keep track of cursor and its
              current/previous/next diff chunks
    MeldApp: Use new next-diff-changed signal to update Up/Down sensitivity
    MeldDoc: Add new cursor-related signals
    
    Closes bgo#533752 and bgo#609629

 meld/diffutil.py |   71 ++++++++++++++++++++++++++++++++++++
 meld/filediff.py |  106 +++++++++++++++++++++++++++++-------------------------
 meld/meldapp.py  |    8 ++++
 meld/melddoc.py  |    3 +-
 4 files changed, 138 insertions(+), 50 deletions(-)
---
diff --git a/meld/diffutil.py b/meld/diffutil.py
index b1a2047..9109855 100644
--- a/meld/diffutil.py
+++ b/meld/diffutil.py
@@ -76,6 +76,7 @@ class Differ(object):
         self.seqlength = [0, 0, 0]
         self.diffs = [[], []]
         self._merge_cache = []
+        self._line_cache = [[], [], []]
         self.ignore_blanks = False
 
     def _update_merge_cache(self, texts):
@@ -92,6 +93,49 @@ class Differ(object):
                                         self._consume_blank_lines(c[1], texts, 1, 2))
             self._merge_cache = [x for x in self._merge_cache if x != (None, None)]
 
+        self._update_line_cache()
+
+    def _update_line_cache(self):
+        for i, l in enumerate(self.seqlength):
+            self._line_cache[i] = [(None, None, None)] * l
+
+        last_chunk = len(self._merge_cache)
+        def find_next(diff, seq, current):
+            next_chunk = None
+            if seq == 1 and current + 1 < last_chunk:
+                next_chunk = current + 1
+            else:
+                for j in range(current + 1, last_chunk):
+                    if self._merge_cache[j][diff] is not None:
+                        next_chunk = j
+                        break
+            return next_chunk
+
+        prev = [None, None, None]
+        next = [find_next(0, 0, -1), find_next(0, 1, -1), find_next(1, 2, -1)]
+        old_end = [0, 0, 0]
+
+        for i, c in enumerate(self._merge_cache):
+            seq_params = ((0, 0, 3, 4), (0, 1, 1, 2), (1, 2, 3, 4))
+            for (diff, seq, lo, hi) in seq_params:
+                if c[diff] is None:
+                    if seq == 1:
+                        diff = 1
+                    else:
+                        continue
+
+                start, end, last = c[diff][lo], c[diff][hi], old_end[seq]
+                if (start > last):
+                    self._line_cache[seq][last:start] = [(None, prev[seq], next[seq])] * (start - last)
+
+                # For insert chunks, claim the subsequent line.
+                if start == end:
+                    end += 1
+
+                next[seq] = find_next(diff, seq, i)
+                self._line_cache[seq][start:end] = [(i, prev[seq], next[seq])] * (end - start)
+                prev[seq], old_end[seq] = i, end
+
     def _consume_blank_lines(self, c, texts, pane1, pane2):
         if c is None:
             return None
@@ -134,6 +178,33 @@ class Differ(object):
                 return i
         return len(self.diffs[whichdiffs])
 
+    def get_chunk(self, index, from_pane, to_pane=None):
+        """Return the index-th change in from_pane
+        
+        If to_pane is provided, then only changes between from_pane and to_pane
+        are considered, otherwise all changes starting at from_pane are used.
+        """
+        sequence = int(from_pane == 2 or to_pane == 2)
+        chunk = self._merge_cache[index][sequence]
+        if from_pane in (0, 2):
+            if chunk is None:
+                return None
+            return reverse_chunk(chunk)
+        else:
+            if to_pane is None and chunk is None:
+                chunk = self._merge_cache[index][1]
+            return chunk
+
+    def locate_chunk(self, pane, line):
+        """Find the index of the chunk which contains line."""
+        try:
+            return self._line_cache[pane][line]
+        except IndexError:
+            return (None, None, None)
+
+    def diff_count(self):
+        return len(self._merge_cache)
+
     def _change_sequence(self, which, sequence, startidx, sizechange, texts):
         diffs = self.diffs[which]
         lines_added = [0,0,0]
diff --git a/meld/filediff.py b/meld/filediff.py
index 8a7e5ff..e8d6063 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -90,6 +90,14 @@ def insert_with_tags_by_name(buffer, line, text, tag):
         text = "\n" + text
     buffer.insert_with_tags_by_name(get_iter_at_line_or_eof(buffer, line), text, tag)
 
+class CursorDetails(object):
+    __slots__ = ("pane", "pos", "line", "offset", "chunk", "prev", "next")
+
+    def __init__(self):
+        for var in self.__slots__:
+            setattr(self, var, None)
+
+
 class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
     """Two or three way diff of text files.
     """
@@ -181,6 +189,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         gobject.idle_add( lambda *args: self.load_font()) # hack around Bug 316730
         gnomeglade.connect_signal_handlers(self)
         self.findbar = self.findbar.get_data("pyobject")
+        self.cursor = CursorDetails()
 
     def on_focus_change(self):
         self.keymask = 0
@@ -216,36 +225,48 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             id1 = buf.connect("delete-range", self.on_text_delete_range)
             id2 = buf.connect_after("insert-text", self.after_text_insert_text)
             id3 = buf.connect_after("delete-range", self.after_text_delete_range)
-            id4 = buf.connect("mark-set", self.on_textbuffer_mark_set)
+            id4 = buf.connect("notify::cursor-position",
+                              self.on_cursor_position_changed)
             buf.handlers = id0, id1, id2, id3, id4
 
-    def _update_cursor_status(self, buf):
-        def update():
-            it = buf.get_iter_at_mark( buf.get_insert() )
-            # Abbreviation for insert,overwrite so that it will fit in the status bar
-            insert_overwrite = _("INS,OVR").split(",")[ self.textview_overwrite ]
-            # Abbreviation for line, column so that it will fit in the status bar
-            line_column = _("Ln %i, Col %i") % (it.get_line()+1, it.get_line_offset()+1)
-            status = "%s : %s" % ( insert_overwrite, line_column )
-            self.emit("status-changed", status)
-            return False
-        self.scheduler.add_task(update)
+    def on_cursor_position_changed(self, buf, pspec, force=False):
+        pane = self.textbuffer.index(buf)
+        pos = buf.props.cursor_position
+        if pane == self.cursor.pane and pos == self.cursor.pos and not force:
+            return
+        self.cursor.pane, self.cursor.pos = pane, pos
+
+        cursor_it = buf.get_iter_at_offset(pos)
+        offset = cursor_it.get_line_offset()
+        line = cursor_it.get_line()
+
+        # Abbreviations for insert and overwrite that fit in the status bar
+        insert_overwrite = (_("INS"), _("OVR"))[self.textview_overwrite]
+        # Abbreviation for line, column so that it will fit in the status bar
+        line_column = _("Ln %i, Col %i") % (line + 1, offset + 1)
+        status = "%s : %s" % (insert_overwrite, line_column)
+        self.emit("status-changed", status)
+
+        if line != self.cursor.line or force:
+            chunk, prev, next = self.linediffer.locate_chunk(pane, line)
+            if prev != self.cursor.prev or next != self.cursor.next:
+                self.emit("next-diff-changed", prev is not None,
+                          next is not None)
+            self.cursor.chunk, self.cursor.prev, self.cursor.next = chunk, prev, next
+        self.cursor.line, self.cursor.offset = line, offset
 
-    def on_textbuffer_mark_set(self, buffer, it, mark):
-        if mark.get_name() == "insert":
-            self._update_cursor_status(buffer)
     def on_textview_focus_in_event(self, view, event):
         self.textview_focussed = view
         self.findbar.textview = view
-        self._update_cursor_status(view.get_buffer())
+        self.on_cursor_position_changed(view.get_buffer(), None, True)
 
     def _after_text_modified(self, buffer, startline, sizechange):
         if self.num_panes > 1:
             pane = self.textbuffer.index(buffer)
             self.linediffer.change_sequence(pane, startline, sizechange, self._get_texts())
+            self.on_cursor_position_changed(buffer, None, True)
             self.scheduler.add_task(self._update_highlighting().next)
             self.queue_draw()
-        self._update_cursor_status(buffer)
 
     def _get_texts(self, raw=0):
         class FakeText(object):
@@ -510,7 +531,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             if v != view:
                 v.emit("toggle-overwrite")
         self.textview_overwrite_handlers = [ t.connect("toggle-overwrite", self.on_textview_toggle_overwrite) for t in self.textview ]
-        self._update_cursor_status(view.get_buffer())
+        self.on_cursor_position_changed(view.get_buffer(), None, True)
 
 
         #
@@ -669,7 +690,11 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
                 msgarea.connect("response", self.on_msgarea_identical_response)
                 msgarea.show_all()
 
-        self.scheduler.add_task( lambda: self.next_diff(gdk.SCROLL_DOWN, jump_to_first=True), True )
+        chunk, prev, next = self.linediffer.locate_chunk(1, 0)
+        self.cursor.next = chunk
+        if self.cursor.next is None:
+            self.cursor.next = next
+        self.scheduler.add_task(lambda: self.next_diff(gdk.SCROLL_DOWN), True)
         self.queue_draw()
         self.scheduler.add_task(self._update_highlighting().next)
         self._connect_buffer_handlers()
@@ -1133,29 +1158,17 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
     def _pixel_to_line(self, pane, pixel ):
         return self.textview[pane].get_line_at_y( pixel )[0].get_line()
 
-    def _find_next_chunk(self, direction, curline, pane):
-        c = None
-        for chunk in self.linediffer.single_changes(pane):
-            assert chunk[0] != "equal"
-            if direction == gdk.SCROLL_DOWN:
-                # Take the first chunk which is starting after curline
-                if chunk[1] > curline:
-                    c = chunk
-                    break
-            else: # direction == gdk.SCROLL_UP
-                # Skip 'delete' blocks when we are at the warp position,
-                # i.e. on the line just after the block, because that may
-                # be where we ended up at the previous 'UP' button press
-                if chunk[2] == chunk[1] == curline:
-                    continue
-                # Take the last chunk which is ending before curline
-                if chunk[2] - 1 < curline:
-                    c = chunk
-                elif c:
-                    break
-        return c
+    def _find_next_chunk(self, direction, pane):
+        if direction == gtk.gdk.SCROLL_DOWN:
+            target = self.cursor.next
+        else: # direction == gtk.gdk.SCROLL_UP
+            target = self.cursor.prev
 
-    def next_diff(self, direction, jump_to_first=False):
+        if target is None:
+            return None
+        return self.linediffer.get_chunk(target, pane)
+
+    def next_diff(self, direction):
         pane = self._get_focused_pane()
         if pane == -1:
             if len(self.textview) > 1:
@@ -1164,15 +1177,10 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
                 pane = 0
         buf = self.textbuffer[pane]
 
-        if jump_to_first:
-            cursorline = -1
-        else:
-            cursorline = buf.get_iter_at_mark(buf.get_insert()).get_line()
-
-        c = self._find_next_chunk(direction, cursorline, pane)
+        c = self._find_next_chunk(direction, pane)
         if c:
             # Warp the cursor to the first line of next chunk
-            if cursorline != c[1]:
+            if self.cursor.line != c[1]:
                 buf.place_cursor(buf.get_iter_at_line(c[1]))
             self.textview[pane].scroll_to_mark(buf.get_insert(), 0.1)
 
diff --git a/meld/meldapp.py b/meld/meldapp.py
index 81e6991..7de8ab3 100644
--- a/meld/meldapp.py
+++ b/meld/meldapp.py
@@ -500,6 +500,7 @@ class MeldApp(gnomeglade.Component):
         self.widget.set_default_size(self.prefs.window_size_x, self.prefs.window_size_y)
         self.ui.ensure_update()
         self.widget.show()
+        self.diff_handler = None
         self.widget.connect('focus_in_event', self.on_focus_change)
         self.widget.connect('focus_out_event', self.on_focus_change)
 
@@ -590,6 +591,7 @@ class MeldApp(gnomeglade.Component):
         oldidx = notebook.get_current_page()
         if oldidx >= 0:
             olddoc = notebook.get_nth_page(oldidx).get_data("pyobject")
+            olddoc.disconnect(self.diff_handler)
             olddoc.on_container_switch_out_event(self.ui)
         self.actiongroup.get_action("Undo").set_sensitive(newseq.can_undo())
         self.actiongroup.get_action("Redo").set_sensitive(newseq.can_redo())
@@ -597,6 +599,8 @@ class MeldApp(gnomeglade.Component):
         self.widget.set_title(nbl.get_label_text() + " - Meld")
         self.statusbar.set_doc_status("")
         newdoc.on_container_switch_in_event(self.ui)
+        self.diff_handler = newdoc.connect("next-diff-changed",
+                                           self.on_next_diff_changed)
         self.scheduler.add_task( newdoc.scheduler )
 
     def on_notebook_label_changed(self, component, text):
@@ -611,6 +615,10 @@ class MeldApp(gnomeglade.Component):
     def on_can_redo(self, undosequence, can):
         self.actiongroup.get_action("Redo").set_sensitive(can)
 
+    def on_next_diff_changed(self, doc, have_prev, have_next):
+        self.actiongroup.get_action("Up").set_sensitive(have_prev)
+        self.actiongroup.get_action("Down").set_sensitive(have_next)
+
     def on_size_allocate(self, window, rect):
         self.prefs.window_size_x = rect.width
         self.prefs.window_size_y = rect.height
diff --git a/meld/melddoc.py b/meld/melddoc.py
index 062cec6..32223c6 100644
--- a/meld/melddoc.py
+++ b/meld/melddoc.py
@@ -32,7 +32,8 @@ class MeldDoc(gobject.GObject):
         'label-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)),
         'file-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)),
         'create-diff': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
-        'status-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
+        'status-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+        'next-diff-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (bool, bool)),
     }
 
     def __init__(self, prefs):



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