filediff.py 74.5 KB
Newer Older
Stephen Kennedy's avatar
Stephen Kennedy committed
1
### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
2
### Copyright (C) 2009-2012 Kai Willadsen <kai.willadsen@gmail.com>
steve9000's avatar
steve9000 committed
3 4 5 6 7 8 9 10 11 12 13 14 15

### This program is free software; you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation; either version 2 of the License, or
### (at your option) any later version.

### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.

### You should have received a copy of the GNU General Public License
### along with this program; if not, write to the Free Software
16 17
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
### USA.
steve9000's avatar
steve9000 committed
18 19

import codecs
20
import copy
21 22
import functools
import multiprocessing
steve9000's avatar
steve9000 committed
23
import os
24
from gettext import gettext as _
25
import signal
26
import sys
27
import time
steve9000's avatar
steve9000 committed
28

29
import pango
30
import glib
steve9000's avatar
steve9000 committed
31 32
import gobject
import gtk
33
import gtk.keysyms
34

35 36 37 38 39 40 41 42
from . import diffutil
from . import matchers
from . import meldbuffer
from . import melddoc
from . import merge
from . import misc
from . import patchdialog
from . import paths
43
from . import recent
44 45 46 47 48
from . import undo
from .ui import findbar
from .ui import gnomeglade

from .meldapp import app
49
from .util.compat import text_type
50
from .util.sourceviewer import srcviewer
51

52

53 54 55 56
def init_worker():
    signal.signal(signal.SIGINT, signal.SIG_IGN)


57 58 59 60
def matcher_worker(text1, textn):
    matcher = matchers.InlineMyersSequenceMatcher(None, text1, textn)
    return matcher.get_opcodes()

61 62 63 64 65
# maxtasksperchild is new in Python 2.7; for 2.6 compat we do this
try:
    process_pool = multiprocessing.Pool(None, init_worker, maxtasksperchild=1)
except TypeError:
    process_pool = multiprocessing.Pool(None, init_worker)
66 67


68 69 70 71 72 73 74 75 76 77 78
class CachedSequenceMatcher(object):
    """Simple class for caching diff results, with LRU-based eviction

    Results from the SequenceMatcher are cached and timestamped, and
    subsequently evicted based on least-recent generation/usage. The LRU-based
    eviction is overly simplistic, but is okay for our usage pattern.
    """

    def __init__(self):
        self.cache = {}

79
    def match(self, text1, textn, cb):
80 81
        try:
            self.cache[(text1, textn)][1] = time.time()
82
            cb(self.cache[(text1, textn)][0])
83
        except KeyError:
84 85 86 87
            def inline_cb(opcodes):
                self.cache[(text1, textn)] = [opcodes, time.time()]
                cb(opcodes)
            process_pool.apply_async(matcher_worker, (text1, textn),
Kai Willadsen's avatar
Kai Willadsen committed
88
                                     callback=inline_cb)
89 90 91 92 93 94 95 96 97 98 99 100 101

    def clean(self, size_hint):
        """Clean the cache if necessary

        @param size_hint: the recommended minimum number of cache entries
        """
        if len(self.cache) < size_hint * 3:
            return
        items = self.cache.items()
        items.sort(key=lambda it: it[1][1])
        for item in items[:-size_hint * 2]:
            del self.cache[item[0]]

102

103
MASK_SHIFT, MASK_CTRL = 1, 2
104 105
MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2

106

107
class CursorDetails(object):
108 109
    __slots__ = ("pane", "pos", "line", "offset", "chunk", "prev", "next",
                 "prev_conflict", "next_conflict")
110 111 112 113 114 115

    def __init__(self):
        for var in self.__slots__:
            setattr(self, var, None)


Kai Willadsen's avatar
Kai Willadsen committed
116 117 118 119 120 121 122 123
class TaskEntry(object):
    __slots__ = ("filename", "file", "buf", "codec", "pane", "was_cr")

    def __init__(self, *args):
        for var, val in zip(self.__slots__, args):
            setattr(self, var, val)


124 125 126 127 128 129 130 131 132 133 134 135 136
class TextviewLineAnimation(object):
    __slots__ = ("start_mark", "end_mark", "start_rgba", "end_rgba",
                 "start_time", "duration")

    def __init__(self, mark0, mark1, rgba0, rgba1, duration):
        self.start_mark = mark0
        self.end_mark = mark1
        self.start_rgba = rgba0
        self.end_rgba = rgba1
        self.start_time = glib.get_current_time()
        self.duration = duration


137 138
class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
    """Two or three way diff of text files.
139 140
    """

141 142
    differ = diffutil.Differ

143 144 145
    keylookup = {gtk.keysyms.Shift_L : MASK_SHIFT,
                 gtk.keysyms.Control_L : MASK_CTRL,
                 gtk.keysyms.Shift_R : MASK_SHIFT,
146
                 gtk.keysyms.Control_R : MASK_CTRL}
Stephen Kennedy's avatar
Stephen Kennedy committed
147

148
    # Identifiers for MsgArea messages
149
    (MSG_SAME,) = list(range(1))
150

151 152
    __gsignals__ = {
        'next-conflict-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (bool, bool)),
153
        'action-mode-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (int,)),
154 155
    }

steve9000's avatar
steve9000 committed
156 157 158
    def __init__(self, prefs, num_panes):
        """Start up an filediff with num_panes empty contents.
        """
159
        melddoc.MeldDoc.__init__(self, prefs)
160
        gnomeglade.Component.__init__(self, paths.ui_dir("filediff.ui"), "filediff")
161 162 163 164 165 166 167 168 169 170 171 172
        self.map_widgets_into_lists(["textview", "fileentry", "diffmap",
                                     "scrolledwindow", "linkmap",
                                     "statusimage", "msgarea_mgr", "vbox",
                                     "selector_hbox"])

        # This SizeGroup isn't actually necessary for FileDiff; it's for
        # handling non-homogenous selectors in FileComp. It's also fragile.
        column_sizes = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        column_sizes.set_ignore_hidden(True)
        for widget in self.selector_hbox:
            column_sizes.add_widget(widget)

173
        self.warned_bad_comparison = False
174 175 176 177 178
        # Some sourceviews bind their own undo mechanism, which we replace
        gtk.binding_entry_remove(srcviewer.GtkTextView, gtk.keysyms.z,
                                 gtk.gdk.CONTROL_MASK)
        gtk.binding_entry_remove(srcviewer.GtkTextView, gtk.keysyms.z,
                                 gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK)
179
        for v in self.textview:
180
            v.set_buffer(meldbuffer.MeldBuffer())
181 182
            v.set_show_line_numbers(self.prefs.show_line_numbers)
            v.set_insert_spaces_instead_of_tabs(self.prefs.spaces_instead_of_tabs)
183
            v.set_wrap_mode(self.prefs.edit_wrap_lines)
184 185
            if self.prefs.show_whitespace:
                v.set_draw_spaces(srcviewer.spaces_flag)
186
            srcviewer.set_tab_width(v, self.prefs.tab_size)
187
        self._keymask = 0
188
        self.load_font()
189
        self.deleted_lines_pending = -1
190
        self.textview_overwrite = 0
191
        self.focus_pane = None
192
        self.textview_overwrite_handlers = [ t.connect("toggle-overwrite", self.on_textview_toggle_overwrite) for t in self.textview ]
193
        self.textbuffer = [v.get_buffer() for v in self.textview]
194
        self.buffer_texts = [meldbuffer.BufferLines(b) for b in self.textbuffer]
195
        self.undosequence = undo.UndoSequence()
196 197
        self.text_filters = []
        self.create_text_filters()
198 199
        self.app_handlers = [app.connect("text-filters-changed",
                             self.on_text_filters_changed)]
200 201
        self.buffer_filtered = [meldbuffer.BufferLines(b, self._filter_text)
                                for b in self.textbuffer]
202 203 204
        for (i, w) in enumerate(self.scrolledwindow):
            w.get_vadjustment().connect("value-changed", self._sync_vscroll, i)
            w.get_hadjustment().connect("value-changed", self._sync_hscroll)
205
        self._connect_buffer_handlers()
206 207
        self._sync_vscroll_lock = False
        self._sync_hscroll_lock = False
208
        self._scroll_lock = False
209
        self.linediffer = self.differ()
210
        self.linediffer.ignore_blanks = self.prefs.ignore_blank_lines
211
        self.in_nested_textview_gutter_expose = False
212
        self._cached_match = CachedSequenceMatcher()
213 214
        self.anim_source_id = [None for buf in self.textbuffer]
        self.animating_chunks = [[] for buf in self.textbuffer]
215
        for buf in self.textbuffer:
216
            buf.create_tag("inline")
217

218 219 220 221 222 223 224 225 226
        # We need to keep track of gtk.TextIter validity, but this isn't
        # exposed anywhere. Instead, we keep our own counter for changes
        # across all of our buffers.
        self._buffer_changed_stamp = 0
        def buffer_change(buf):
            self._buffer_changed_stamp += 1
        for buf in self.textbuffer:
            buf.connect("changed", buffer_change)

227
        actions = (
228
            ("MakePatch", None, _("Format as patch..."), None, _("Create a patch using differences between files"), self.make_patch),
229 230
            ("PrevConflict", None, _("Previous conflict"), "<Ctrl>I", _("Go to the previous conflict"), lambda x: self.on_next_conflict(gtk.gdk.SCROLL_UP)),
            ("NextConflict", None, _("Next conflict"), "<Ctrl>K", _("Go to the next conflict"), lambda x: self.on_next_conflict(gtk.gdk.SCROLL_DOWN)),
231 232 233 234 235
            ("PushLeft",  gtk.STOCK_GO_BACK,    _("Push to left"),    "<Alt>Left", _("Push current change to the left"), lambda x: self.push_change(-1)),
            ("PushRight", gtk.STOCK_GO_FORWARD, _("Push to right"),   "<Alt>Right", _("Push current change to the right"), lambda x: self.push_change(1)),
            # FIXME: using LAST and FIRST is terrible and unreliable icon abuse
            ("PullLeft",  gtk.STOCK_GOTO_LAST,  _("Pull from left"),  "<Alt><Shift>Right", _("Pull change from the left"), lambda x: self.pull_change(-1)),
            ("PullRight", gtk.STOCK_GOTO_FIRST, _("Pull from right"), "<Alt><Shift>Left", _("Pull change from the right"), lambda x: self.pull_change(1)),
236 237 238 239
            ("CopyLeftUp", None, _("Copy above left"), "<Alt>bracketleft", _("Copy change above the left chunk"), lambda x: self.copy_change(-1, -1)),
            ("CopyLeftDown", None, _("Copy below left"), "<Alt>semicolon", _("Copy change below the left chunk"), lambda x: self.copy_change(-1, 1)),
            ("CopyRightUp", None, _("Copy above right"), "<Alt>bracketright", _("Copy change above the right chunk"), lambda x: self.copy_change(1, -1)),
            ("CopyRightDown", None, _("Copy below right"), "<Alt>quoteright", _("Copy change below the right chunk"), lambda x: self.copy_change(1, 1)),
240
            ("Delete",    gtk.STOCK_DELETE,     _("Delete"),     "<Alt>Delete", _("Delete change"), self.delete_change),
241 242 243
            ("MergeFromLeft",  None, _("Merge all changes from left"),  None, _("Merge all non-conflicting changes from the left"), lambda x: self.pull_all_non_conflicting_changes(-1)),
            ("MergeFromRight", None, _("Merge all changes from right"), None, _("Merge all non-conflicting changes from the right"), lambda x: self.pull_all_non_conflicting_changes(1)),
            ("MergeAll",       None, _("Merge all non-conflicting"),    None, _("Merge all non-conflicting changes from left and right panes"), lambda x: self.merge_all_non_conflicting_changes()),
244
            ("CycleDocuments", None, _("Cycle through documents"), "<control>Escape", _("Move keyboard focus to the next document in this comparison"), self.action_cycle_documents),
245 246
        )

247 248 249 250 251 252
        toggle_actions = (
            ("LockScrolling", None, _("Lock scrolling"), None,
             _("Lock scrolling of all panes"),
             self.on_action_lock_scrolling_toggled, True),
        )

253
        self.ui_file = paths.ui_dir("filediff-ui.xml")
254 255 256
        self.actiongroup = gtk.ActionGroup('FilediffPopupActions')
        self.actiongroup.set_translation_domain("meld")
        self.actiongroup.add_actions(actions)
257
        self.actiongroup.add_toggle_actions(toggle_actions)
258
        self.findbar = findbar.FindBar(self.table)
259 260 261 262

        self.widget.connect("style-set", self.on_style_set)
        self.widget.ensure_style()

263
        self.set_num_panes(num_panes)
264
        gobject.idle_add( lambda *args: self.load_font()) # hack around Bug 316730
265
        gnomeglade.connect_signal_handlers(self)
266
        self.cursor = CursorDetails()
267 268 269 270
        self.connect("current-diff-changed", self.on_current_diff_changed)
        for t in self.textview:
            t.connect("focus-in-event", self.on_current_diff_changed)
            t.connect("focus-out-event", self.on_current_diff_changed)
271
        self.linediffer.connect("diffs-changed", self.on_diffs_changed)
272
        self.undosequence.connect("checkpointed", self.on_undo_checkpointed)
273
        self.connect("next-conflict-changed", self.on_next_conflict_changed)
Stephen Kennedy's avatar
Stephen Kennedy committed
274

275 276 277 278 279 280 281 282
        overwrite_label = gtk.Label()
        overwrite_label.set_size_request(50, -1)
        overwrite_label.show()
        cursor_label = gtk.Label()
        cursor_label.set_size_request(150, -1)
        cursor_label.show()
        self.status_info_labels = [overwrite_label, cursor_label]

283 284 285 286 287 288 289 290 291 292 293 294 295
    def get_keymask(self):
        return self._keymask
    def set_keymask(self, value):
        if value & MASK_SHIFT:
            mode = MODE_DELETE
        elif value & MASK_CTRL:
            mode = MODE_INSERT
        else:
            mode = MODE_REPLACE
        self._keymask = value
        self.emit("action-mode-changed", mode)
    keymask = property(get_keymask, set_keymask)

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    def on_style_set(self, widget, prev_style):
        style = widget.get_style()

        lookup = lambda color_id, default: style.lookup_color(color_id) or \
                                           gtk.gdk.color_parse(default)

        for buf in self.textbuffer:
            tag = buf.get_tag_table().lookup("inline")
            tag.props.background = lookup("inline-bg", "LightSteelBlue2")
            tag.props.foreground = lookup("inline-fg", "Red")

        self.fill_colors = {"insert"  : lookup("insert-bg", "DarkSeaGreen1"),
                            "delete"  : lookup("insert-bg", "DarkSeaGreen1"),
                            "conflict": lookup("conflict-bg", "Pink"),
                            "replace" : lookup("replace-bg", "#ddeeff")}
        self.line_colors = {"insert"  : lookup("insert-outline", "#77f077"),
                            "delete"  : lookup("insert-outline", "#77f077"),
                            "conflict": lookup("conflict-outline", "#f0768b"),
                            "replace" : lookup("replace-outline", "#8bbff3")}
        self.highlight_color = lookup("highlight-bg", "#ffff00")

317 318 319
        for associated in self.diffmap + self.linkmap:
            associated.set_color_scheme([self.fill_colors, self.line_colors])

320 321
        self.queue_draw()

322 323 324
    def on_focus_change(self):
        self.keymask = 0

325 326
    def on_container_switch_in_event(self, ui):
        melddoc.MeldDoc.on_container_switch_in_event(self, ui)
327
        # FIXME: If no focussed textview, action sensitivity will be unset
328

329 330 331
    def on_text_filters_changed(self, app):
        relevant_change = self.create_text_filters()
        if relevant_change:
332
            self.refresh_comparison()
333 334 335 336 337 338 339 340 341 342 343

    def create_text_filters(self):
        # In contrast to file filters, ordering of text filters can matter
        old_active = [f.filter_string for f in self.text_filters if f.active]
        new_active = [f.filter_string for f in app.text_filters if f.active]
        active_filters_changed = old_active != new_active

        self.text_filters = [copy.copy(f) for f in app.text_filters]

        return active_filters_changed

344 345 346
    def _disconnect_buffer_handlers(self):
        for textview in self.textview:
            textview.set_editable(0)
347 348
        for buf in self.textbuffer:
            assert hasattr(buf,"handlers")
349 350 351 352 353 354
            for h in buf.handlers:
                buf.disconnect(h)

    def _connect_buffer_handlers(self):
        for textview in self.textview:
            textview.set_editable(1)
355
        for buf in self.textbuffer:
356 357 358 359
            id0 = buf.connect("insert-text", self.on_text_insert_text)
            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)
360 361
            id4 = buf.connect("notify::cursor-position",
                              self.on_cursor_position_changed)
362
            buf.handlers = id0, id1, id2, id3, id4
363

364 365 366 367 368
    # Abbreviations for insert and overwrite that fit in the status bar
    _insert_overwrite_text = (_("INS"), _("OVR"))
    # Abbreviation for line, column so that it will fit in the status bar
    _line_column_text = _("Ln %i, Col %i")

369 370 371 372 373 374 375 376 377 378 379
    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()

380 381
        insert_overwrite = self._insert_overwrite_text[self.textview_overwrite]
        line_column = self._line_column_text % (line + 1, offset + 1)
382
        status = "%s : %s" % (insert_overwrite, line_column)
383 384 385 386 387
        # FIXME: Think this status-changed is wrong...
        # self.emit("status-changed", status)

        self.status_info_labels[0].set_text(insert_overwrite)
        self.status_info_labels[1].set_text(line_column)
388 389

        if line != self.cursor.line or force:
Kai Willadsen's avatar
Kai Willadsen committed
390
            chunk, prev, next_ = self.linediffer.locate_chunk(pane, line)
391
            if chunk != self.cursor.chunk or force:
392 393
                self.cursor.chunk = chunk
                self.emit("current-diff-changed")
Kai Willadsen's avatar
Kai Willadsen committed
394
            if prev != self.cursor.prev or next_ != self.cursor.next or force:
395
                self.emit("next-diff-changed", prev is not None,
Kai Willadsen's avatar
Kai Willadsen committed
396
                          next_ is not None)
397 398 399 400 401

            prev_conflict, next_conflict = None, None
            for conflict in self.linediffer.conflicts:
                if prev is not None and conflict <= prev:
                    prev_conflict = conflict
Kai Willadsen's avatar
Kai Willadsen committed
402
                if next_ is not None and conflict >= next_:
403 404 405
                    next_conflict = conflict
                    break
            if prev_conflict != self.cursor.prev_conflict or \
406
               next_conflict != self.cursor.next_conflict or force:
407 408 409
                self.emit("next-conflict-changed", prev_conflict is not None,
                          next_conflict is not None)

Kai Willadsen's avatar
Kai Willadsen committed
410
            self.cursor.prev, self.cursor.next = prev, next_
411 412
            self.cursor.prev_conflict = prev_conflict
            self.cursor.next_conflict = next_conflict
413
        self.cursor.line, self.cursor.offset = line, offset
414

415
    def on_current_diff_changed(self, widget, *args):
416 417 418 419 420 421 422
        pane = self._get_focused_pane()
        if pane != -1:
            # While this *should* be redundant, it's possible for focus pane
            # and cursor pane to be different in several situations.
            pane = self.cursor.pane
            chunk_id = self.cursor.chunk

423
        if pane == -1 or chunk_id is None:
424 425
            push_left, push_right, pull_left, pull_right, delete, \
                copy_left, copy_right = (False,) * 7
426
        else:
427 428 429
            push_left, push_right, pull_left, pull_right, delete, \
                copy_left, copy_right = (True,) * 7

430 431 432 433 434 435
            # Push and Delete are active if the current pane has something to
            # act on, and the target pane exists and is editable. Pull is
            # sensitive if the source pane has something to get, and the
            # current pane is editable. Copy actions are sensitive if the
            # conditions for push are met, *and* there is some content in the
            # target pane.
436 437
            editable = self.textview[pane].get_editable()
            editable_left = pane > 0 and self.textview[pane - 1].get_editable()
438 439
            editable_right = pane < self.num_panes - 1 and \
                             self.textview[pane + 1].get_editable()
440 441
            if pane == 0 or pane == 2:
                chunk = self.linediffer.get_chunk(chunk_id, pane)
442 443 444 445 446 447 448 449 450
                insert_chunk = chunk[1] == chunk[2]
                delete_chunk = chunk[3] == chunk[4]
                push_left = editable_left and not insert_chunk
                push_right = editable_right and not insert_chunk
                pull_left = pane == 2 and editable and not delete_chunk
                pull_right = pane == 0 and editable and not delete_chunk
                delete = editable and not insert_chunk
                copy_left = push_left and not delete_chunk
                copy_right = push_right and not delete_chunk
451
            elif pane == 1:
452
                chunk0 = self.linediffer.get_chunk(chunk_id, 1, 0)
453 454
                chunk2 = None
                if self.num_panes == 3:
455 456 457 458 459 460 461 462 463 464 465 466
                    chunk2 = self.linediffer.get_chunk(chunk_id, 1, 2)
                left_mid_exists = chunk0 is not None and chunk0[1] != chunk0[2]
                left_exists = chunk0 is not None and chunk0[3] != chunk0[4]
                right_mid_exists = chunk2 is not None and chunk2[1] != chunk2[2]
                right_exists = chunk2 is not None and chunk2[3] != chunk2[4]
                push_left = editable_left and left_mid_exists
                push_right = editable_right and right_mid_exists
                pull_left = editable and left_exists
                pull_right = editable and right_exists
                delete = editable and (left_mid_exists or right_mid_exists)
                copy_left = push_left and left_exists
                copy_right = push_right and right_exists
467 468 469 470 471
        self.actiongroup.get_action("PushLeft").set_sensitive(push_left)
        self.actiongroup.get_action("PushRight").set_sensitive(push_right)
        self.actiongroup.get_action("PullLeft").set_sensitive(pull_left)
        self.actiongroup.get_action("PullRight").set_sensitive(pull_right)
        self.actiongroup.get_action("Delete").set_sensitive(delete)
472 473 474 475
        self.actiongroup.get_action("CopyLeftUp").set_sensitive(copy_left)
        self.actiongroup.get_action("CopyLeftDown").set_sensitive(copy_left)
        self.actiongroup.get_action("CopyRightUp").set_sensitive(copy_right)
        self.actiongroup.get_action("CopyRightDown").set_sensitive(copy_right)
476 477
        # FIXME: don't queue_draw() on everything... just on what changed
        self.queue_draw()
478

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    def on_next_conflict_changed(self, doc, have_prev, have_next):
        self.actiongroup.get_action("PrevConflict").set_sensitive(have_prev)
        self.actiongroup.get_action("NextConflict").set_sensitive(have_next)

    def on_next_conflict(self, direction):
        if direction == gtk.gdk.SCROLL_DOWN:
            target = self.cursor.next_conflict
        else: # direction == gtk.gdk.SCROLL_UP
            target = self.cursor.prev_conflict

        if target is None:
            return

        buf = self.textbuffer[self.cursor.pane]
        chunk = self.linediffer.get_chunk(target, self.cursor.pane)
        buf.place_cursor(buf.get_iter_at_line(chunk[1]))
        self.textview[self.cursor.pane].scroll_to_mark(buf.get_insert(), 0.1)

497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
    def push_change(self, direction):
        src = self._get_focused_pane()
        dst = src + direction
        chunk = self.linediffer.get_chunk(self.cursor.chunk, src, dst)
        assert(src != -1 and self.cursor.chunk is not None)
        assert(dst in (0, 1, 2))
        assert(chunk is not None)
        self.replace_chunk(src, dst, chunk)

    def pull_change(self, direction):
        dst = self._get_focused_pane()
        src = dst + direction
        chunk = self.linediffer.get_chunk(self.cursor.chunk, src, dst)
        assert(dst != -1 and self.cursor.chunk is not None)
        assert(src in (0, 1, 2))
        assert(chunk is not None)
        self.replace_chunk(src, dst, chunk)

515 516 517 518 519 520 521 522 523 524
    def copy_change(self, direction, copy_direction):
        src = self._get_focused_pane()
        dst = src + direction
        chunk = self.linediffer.get_chunk(self.cursor.chunk, src, dst)
        assert(src != -1 and self.cursor.chunk is not None)
        assert(dst in (0, 1, 2))
        assert(chunk is not None)
        copy_up = True if copy_direction < 0 else False
        self.copy_chunk(src, dst, chunk, copy_up)

525 526 527 528 529 530 531
    def pull_all_non_conflicting_changes(self, direction):
        assert direction in (-1, 1)
        dst = self._get_focused_pane()
        src = dst + direction
        assert src in range(self.num_panes)
        merger = merge.Merger()
        merger.differ = self.linediffer
532
        merger.texts = self.buffer_texts
533 534
        for mergedfile in merger.merge_2_files(src, dst):
            pass
535
        self._sync_vscroll_lock = True
536 537 538
        self.on_textbuffer__begin_user_action()
        self.textbuffer[dst].set_text(mergedfile)
        self.on_textbuffer__end_user_action()
539 540 541 542
        def resync():
            self._sync_vscroll_lock = False
            self._sync_vscroll(self.scrolledwindow[src].get_vadjustment(), src)
        self.scheduler.add_task(resync)
543 544 545 546 547

    def merge_all_non_conflicting_changes(self):
        dst = 1
        merger = merge.Merger()
        merger.differ = self.linediffer
548
        merger.texts = self.buffer_texts
549 550
        for mergedfile in merger.merge_3_files(False):
            pass
551
        self._sync_vscroll_lock = True
552 553 554
        self.on_textbuffer__begin_user_action()
        self.textbuffer[dst].set_text(mergedfile)
        self.on_textbuffer__end_user_action()
555 556 557 558
        def resync():
            self._sync_vscroll_lock = False
            self._sync_vscroll(self.scrolledwindow[0].get_vadjustment(), 0)
        self.scheduler.add_task(resync)
559

560 561 562 563 564 565 566
    def delete_change(self, widget):
        pane = self._get_focused_pane()
        chunk = self.linediffer.get_chunk(self.cursor.chunk, pane)
        assert(pane != -1 and self.cursor.chunk is not None)
        assert(chunk is not None)
        self.delete_chunk(pane, chunk)

567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
    def _synth_chunk(self, pane0, pane1, line):
        """Returns the Same chunk that would exist at
           the given location if we didn't remove Same chunks"""

        # This method is a hack around our existing diffutil data structures;
        # getting rid of the Same chunk removal is difficult, as several places
        # have baked in the assumption of only being given changed blocks.

        buf0, buf1 = self.textbuffer[pane0], self.textbuffer[pane1]
        start0, end0 = 0, buf0.get_line_count() - 1
        start1, end1 = 0, buf1.get_line_count() - 1

        # This hack is required when pane0's prev/next chunk doesn't exist
        # (i.e., is Same) between pane0 and pane1.
        prev_chunk0, prev_chunk1, next_chunk0, next_chunk1 = (None,) * 4
Kai Willadsen's avatar
Kai Willadsen committed
582
        _, prev, next_ = self.linediffer.locate_chunk(pane0, line)
583 584 585 586 587 588 589 590 591 592
        if prev is not None:
            while prev >= 0:
                prev_chunk0 = self.linediffer.get_chunk(prev, pane0, pane1)
                prev_chunk1 = self.linediffer.get_chunk(prev, pane1, pane0)
                if None not in (prev_chunk0, prev_chunk1):
                    start0 = prev_chunk0[2]
                    start1 = prev_chunk1[2]
                    break
                prev -= 1

Kai Willadsen's avatar
Kai Willadsen committed
593 594 595 596
        if next_ is not None:
            while next_ < self.linediffer.diff_count():
                next_chunk0 = self.linediffer.get_chunk(next_, pane0, pane1)
                next_chunk1 = self.linediffer.get_chunk(next_, pane1, pane0)
597 598 599 600
                if None not in (next_chunk0, next_chunk1):
                    end0 = next_chunk0[1]
                    end1 = next_chunk1[1]
                    break
Kai Willadsen's avatar
Kai Willadsen committed
601
                next_ += 1
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664

        return "Same", start0, end0, start1, end1

    def _corresponding_chunk_line(self, chunk, line, pane, new_pane):
        """Approximates the corresponding line between panes"""

        old_buf, new_buf = self.textbuffer[pane], self.textbuffer[new_pane]

        # Special-case cross-pane jumps
        if (pane == 0 and new_pane == 2) or (pane == 2 and new_pane == 0):
            proxy = self._corresponding_chunk_line(chunk, line, pane, 1)
            return self._corresponding_chunk_line(chunk, proxy, 1, new_pane)

        # Either we are currently in a identifiable chunk, or we are in a Same
        # chunk; if we establish the start/end of that chunk in both panes, we
        # can figure out what our new offset should be.
        cur_chunk = None
        if chunk is not None:
            cur_chunk = self.linediffer.get_chunk(chunk, pane, new_pane)

        if cur_chunk is None:
            cur_chunk = self._synth_chunk(pane, new_pane, line)
        cur_start, cur_end, new_start, new_end = cur_chunk[1:5]

        # If the new buffer's current cursor is already in the correct chunk,
        # assume that we have in-progress editing, and don't move it.
        cursor_it = new_buf.get_iter_at_mark(new_buf.get_insert())
        cursor_line = cursor_it.get_line()

        cursor_chunk, _, _ = self.linediffer.locate_chunk(new_pane, cursor_line)
        if cursor_chunk is not None:
            already_in_chunk = cursor_chunk == chunk
        else:
            cursor_chunk = self._synth_chunk(pane, new_pane, cursor_line)
            already_in_chunk = cursor_chunk[3] == new_start and \
                               cursor_chunk[4] == new_end

        if already_in_chunk:
            new_line = cursor_line
        else:
            # Guess where to put the cursor: in the same chunk, at about the
            # same place within the chunk, calculated proportionally by line.
            # Insert chunks and one-line chunks are placed at the top.
            if cur_end == cur_start:
                chunk_offset = 0.0
            else:
                chunk_offset = (line - cur_start) / float(cur_end - cur_start)
            new_line = new_start + int(chunk_offset * (new_end - new_start))

        return new_line

    def action_cycle_documents(self, widget):
        pane = self._get_focused_pane()
        new_pane = (pane + 1) % self.num_panes
        chunk, line = self.cursor.chunk, self.cursor.line

        new_line = self._corresponding_chunk_line(chunk, line, pane, new_pane)

        new_buf = self.textbuffer[new_pane]
        self.textview[new_pane].grab_focus()
        new_buf.place_cursor(new_buf.get_iter_at_line(new_line))
        self.textview[new_pane].scroll_to_mark(new_buf.get_insert(), 0.1)

665
    def on_textview_focus_in_event(self, view, event):
666
        self.focus_pane = view
667
        self.findbar.textview = view
668
        self.on_cursor_position_changed(view.get_buffer(), None, True)
669
        self._set_merge_action_sensitivity()
670

671 672 673
    def on_textview_focus_out_event(self, view, event):
        self._set_merge_action_sensitivity()

steve9000's avatar
steve9000 committed
674
    def _after_text_modified(self, buffer, startline, sizechange):
675
        if self.num_panes > 1:
676
            pane = self.textbuffer.index(buffer)
677 678
            self.linediffer.change_sequence(pane, startline, sizechange,
                                            self.buffer_filtered)
679 680 681 682 683
            # FIXME: diff-changed signal for the current buffer would be cleaner
            focused_pane = self._get_focused_pane()
            if focused_pane != -1:
                self.on_cursor_position_changed(self.textbuffer[focused_pane],
                                                None, True)
Stephen Kennedy's avatar
Stephen Kennedy committed
684
            self.queue_draw()
steve9000's avatar
steve9000 committed
685

686 687 688 689 690 691 692 693 694 695 696 697
    def _filter_text(self, txt):
        def killit(m):
            assert m.group().count("\n") == 0
            if len(m.groups()):
                s = m.group()
                for g in m.groups():
                    if g:
                        s = s.replace(g,"")
                return s
            else:
                return ""
        try:
698
            for filt in self.text_filters:
699 700
                if filt.active:
                    txt = filt.filter.sub(killit, txt)
701
        except AssertionError:
702
            if not self.warned_bad_comparison:
703 704
                misc.run_dialog(_("Filter '%s' changed the number of lines in the file. "
                    "Comparison will be incorrect. See the user manual for more details.") % filt.label)
705 706 707
                self.warned_bad_comparison = True
        return txt

708 709 710 711 712 713
    def after_text_insert_text(self, buf, it, newtext, textlen):
        start_mark = buf.get_mark("insertion-start")
        starting_at = buf.get_iter_at_mark(start_mark).get_line()
        buf.delete_mark(start_mark)
        lines_added = it.get_line() - starting_at
        self._after_text_modified(buf, starting_at, lines_added)
714

715 716
    def after_text_delete_range(self, buffer, it0, it1):
        starting_at = it0.get_line()
717 718 719
        assert self.deleted_lines_pending != -1
        self._after_text_modified(buffer, starting_at, -self.deleted_lines_pending)
        self.deleted_lines_pending = -1
steve9000's avatar
steve9000 committed
720

steve9000's avatar
steve9000 committed
721 722 723 724
    def load_font(self):
        fontdesc = pango.FontDescription(self.prefs.get_current_font())
        context = self.textview0.get_pango_context()
        metrics = context.get_metrics( fontdesc, context.get_language() )
725 726
        line_height_points = metrics.get_ascent() + metrics.get_descent()
        self.pixels_per_line = line_height_points // 1024
steve9000's avatar
steve9000 committed
727 728
        self.pango_char_width = metrics.get_approximate_char_width()
        tabs = pango.TabArray(10, 0)
729
        tab_size = self.prefs.tab_size
steve9000's avatar
steve9000 committed
730 731 732 733 734 735 736
        for i in range(10):
            tabs.set_tab(i, pango.TAB_LEFT, i*tab_size*self.pango_char_width)
        for i in range(3):
            self.textview[i].modify_font(fontdesc)
            self.textview[i].set_tabs(tabs)
        for i in range(2):
            self.linkmap[i].queue_draw()
737

steve9000's avatar
steve9000 committed
738
    def on_preference_changed(self, key, value):
739
        if key == "tab_size":
steve9000's avatar
steve9000 committed
740 741 742 743 744
            tabs = pango.TabArray(10, 0)
            for i in range(10):
                tabs.set_tab(i, pango.TAB_LEFT, i*value*self.pango_char_width)
            for i in range(3):
                self.textview[i].set_tabs(tabs)
745 746
            for t in self.textview:
                srcviewer.set_tab_width(t, value)
747
        elif key == "use_custom_font" or key == "custom_font":
steve9000's avatar
steve9000 committed
748
            self.load_font()
749
        elif key == "show_line_numbers":
750 751
            for t in self.textview:
                t.set_show_line_numbers( value )
752
        elif key == "show_whitespace":
753
            spaces_flag = srcviewer.spaces_flag if value else 0
754 755
            for v in self.textview:
                v.set_draw_spaces(spaces_flag)
756
        elif key == "use_syntax_highlighting":
757
            for i in range(self.num_panes):
758
                srcviewer.set_highlight_syntax(self.textbuffer[i], value)
759
        elif key == "edit_wrap_lines":
760 761
            for t in self.textview:
                t.set_wrap_mode(self.prefs.edit_wrap_lines)
762 763 764 765 766
            # FIXME: On changing wrap mode, we get one redraw using cached
            # coordinates, followed by a second redraw (e.g., on refocus) with
            # correct coordinates. Overly-aggressive textview lazy calculation?
            self.diffmap0.queue_draw()
            self.diffmap1.queue_draw()
767
        elif key == "spaces_instead_of_tabs":
768 769
            for t in self.textview:
                t.set_insert_spaces_instead_of_tabs(value)
770 771
        elif key == "ignore_blank_lines":
            self.linediffer.ignore_blanks = self.prefs.ignore_blank_lines
772
            self.refresh_comparison()
773

774
    def on_key_press_event(self, object, event):
775 776
        # The correct way to handle these modifiers would be to use
        # gdk_keymap_get_modifier_state method, available from GDK 3.4.
777 778 779
        keymap = gtk.gdk.keymap_get_default()
        x = self.keylookup.get(keymap.translate_keyboard_state(
                               event.hardware_keycode, 0, event.group)[0], 0)
780 781
        if self.keymask | x != self.keymask:
            self.keymask |= x
782 783
        elif event.keyval == gtk.keysyms.Escape:
            self.findbar.hide()
steve9000's avatar
steve9000 committed
784

785
    def on_key_release_event(self, object, event):
786 787 788
        keymap = gtk.gdk.keymap_get_default()
        x = self.keylookup.get(keymap.translate_keyboard_state(
                               event.hardware_keycode, 0, event.group)[0], 0)
789 790
        if self.keymask & ~x != self.keymask:
            self.keymask &= ~x
steve9000's avatar
steve9000 committed
791

792
    def check_save_modified(self, label=None):
793
        response = gtk.RESPONSE_OK
794
        modified = [b.data.modified for b in self.textbuffer]
795
        if True in modified:
796 797
            ui_path = paths.ui_dir("filediff.ui")
            dialog = gnomeglade.Component(ui_path, "check_save_dialog")
798
            dialog.widget.set_transient_for(self.widget.get_toplevel())
799 800 801 802
            if label:
                dialog.widget.props.text = label
            # FIXME: Should be packed into dialog.widget.get_message_area(),
            # but this is unbound on currently required PyGTK.
803 804
            buttons = []
            for i in range(self.num_panes):
805 806 807 808 809 810 811
                button = gtk.CheckButton(self.textbuffer[i].data.label)
                button.set_use_underline(False)
                button.set_sensitive(modified[i])
                button.set_active(modified[i])
                dialog.extra_vbox.pack_start(button, expand=True, fill=True)
                buttons.append(button)
            dialog.extra_vbox.show_all()
812
            response = dialog.widget.run()
813
            try_save = [b.get_active() for b in buttons]
814
            dialog.widget.destroy()
815
            if response == gtk.RESPONSE_OK:
816 817
                for i in range(self.num_panes):
                    if try_save[i]:
818
                        if not self.save_file(i):
819
                            return gtk.RESPONSE_CANCEL
820 821
            elif response == gtk.RESPONSE_DELETE_EVENT:
                response = gtk.RESPONSE_CANCEL
822 823 824 825
        return response

    def on_delete_event(self, appquit=0):
        response = self.check_save_modified()
826 827 828
        if response == gtk.RESPONSE_OK:
            for h in self.app_handlers:
                app.disconnect(h)
829
        return response
steve9000's avatar
steve9000 committed
830

831 832 833
        #
        # text buffer undo/redo
        #
834 835 836 837 838 839 840 841 842

    def on_undo_activate(self):
        if self.undosequence.can_undo():
            self.undosequence.undo()

    def on_redo_activate(self):
        if self.undosequence.can_redo():
            self.undosequence.redo()

843
    def on_textbuffer__begin_user_action(self, *buffer):
844
        self.undosequence.begin_group()
steve9000's avatar
steve9000 committed
845

846
    def on_textbuffer__end_user_action(self, *buffer):
847
        self.undosequence.end_group()
848

849
    def on_text_insert_text(self, buf, it, text, textlen):
850
        text = text_type(text, 'utf8')
851
        self.undosequence.add_action(
852
            meldbuffer.BufferInsertionAction(buf, it.get_offset(), text))
853
        buf.create_mark("insertion-start", it, True)
steve9000's avatar
steve9000 committed
854

855
    def on_text_delete_range(self, buf, it0, it1):
856
        text = text_type(buf.get_text(it0, it1, False), 'utf8')
steve9000's avatar
steve9000 committed
857
        assert self.deleted_lines_pending == -1
858
        self.deleted_lines_pending = it1.get_line() - it0.get_line()
859
        self.undosequence.add_action(
860
            meldbuffer.BufferDeletionAction(buf, it0.get_offset(), text))
861 862 863

    def on_undo_checkpointed(self, undosequence, buf, checkpointed):
        self.set_buffer_modified(buf, not checkpointed)
864

steve9000's avatar
steve9000 committed
865 866 867
        #
        #
        #
steve9000's avatar
steve9000 committed
868

869
    def open_external(self):
870 871
        pane = self._get_focused_pane()
        if pane >= 0:
872 873
            if self.textbuffer[pane].data.filename:
                self._open_files([self.textbuffer[pane].data.filename])
874

875 876 877 878
    def get_selected_text(self):
        """Returns selected text of active pane"""
        pane = self._get_focused_pane()
        if pane != -1:
879 880 881
            buf = self.textbuffer[pane]
            sel = buf.get_selection_bounds()
            if sel:
882
                return text_type(buf.get_text(sel[0], sel[1], False), 'utf8')
883 884
        return None

885
    def on_find_activate(self, *args):
886
        selected_text = self.get_selected_text()
887
        self.findbar.start_find(self.focus_pane, selected_text)
888 889 890
        self.keymask = 0

    def on_replace_activate(self, *args):
891
        selected_text = self.get_selected_text()
892
        self.findbar.start_replace(self.focus_pane, selected_text)
893 894 895
        self.keymask = 0

    def on_find_next_activate(self, *args):
896
        self.findbar.start_find_next(self.focus_pane)
897

898
    def on_find_previous_activate(self, *args):
899
        self.findbar.start_find_previous(self.focus_pane)
900

901 902 903
    def on_filediff__key_press_event(self, entry, event):
        if event.keyval == gtk.keysyms.Escape:
            self.findbar.hide()
904

905 906 907 908 909 910 911
    def on_scrolledwindow__size_allocate(self, scrolledwindow, allocation):
        index = self.scrolledwindow.index(scrolledwindow)
        if index == 0 or index == 1:
            self.linkmap[0].queue_draw()
        if index == 1 or index == 2:
            self.linkmap[1].queue_draw()

912 913 914 915 916
    def on_textview_popup_menu(self, textview):
        self.popup_menu.popup(None, None, None, 0,
                              gtk.get_current_event_time())
        return True

917
    def on_textview_button_press_event(self, textview, event):
918 919
        if event.button == 3:
            textview.grab_focus()
920 921 922
            self.popup_menu.popup(None, None, None, event.button, event.time)
            return True
        return False
923 924 925 926 927 928 929 930

    def on_textview_toggle_overwrite(self, view):
        self.textview_overwrite = not self.textview_overwrite
        for v,h in zip(self.textview, self.textview_overwrite_handlers):
            v.disconnect(h)
            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 ]
931
        self.on_cursor_position_changed(view.get_buffer(), None, True)
932

933

934 935 936
        #
        # text buffer loading/saving
        #
steve9000's avatar
steve9000 committed
937

Kai Willadsen's avatar
Kai Willadsen committed
938 939 940 941 942
    def set_labels(self, labels):
        labels = labels[:len(self.textbuffer)]
        for label, buf in zip(labels, self.textbuffer):
            if label:
                buf.data.label = label
943

944
    def set_merge_output_file(self, filename):
945
        if len(self.textbuffer) < 2:
946
            return
947
        self.textbuffer[1].data.savefile = os.path.abspath(filename)
948 949 950
        self.textbuffer[1].data.set_label(filename)
        self.fileentry[1].set_filename(os.path.abspath(filename))
        self.recompute_label()
951

steve9000's avatar
steve9000 committed
952
    def recompute_label(self):
953
        filenames = []
954
        for i in range(self.num_panes):
955
            filenames.append(self.textbuffer[i].data.label)
956 957
        shortnames = misc.shorten_names(*filenames)
        for i in range(self.num_panes):
958
            stock = None
959
            if self.textbuffer[i].data.modified:
960
                shortnames[i] += "*"
961
                if self.textbuffer[i].data.writable:
962 963 964
                    stock = gtk.STOCK_SAVE
                else:
                    stock = gtk.STOCK_SAVE_AS
965
            elif not self.textbuffer[i].data.writable:
966 967
                stock = gtk.STOCK_NO
            if stock:
968
                self.statusimage[i].show()
969
                self.statusimage[i].set_from_stock(stock, gtk.ICON_SIZE_MENU)
970
                self.statusimage[i].set_size_request(self.diffmap[0].size_request()[0],-1)
steve9000's avatar
steve9000 committed
971
            else:
972
                self.statusimage[i].hide()
steve9000's avatar
steve9000 committed
973
        self.label_text = " : ".join(shortnames)
974
        self.tooltip_text = self.label_text
975
        self.label_changed()
steve9000's avatar
steve9000 committed
976 977

    def set_files(self, files):
steve9000's avatar
steve9000 committed
978
        """Set num panes to len(files) and load each file given.
979
           If an element is None, the text of a pane is left as is.
steve9000's avatar
steve9000 committed
980
        """
981
        self._disconnect_buffer_handlers()
982
        for i,f in enumerate(files):
steve9000's avatar
steve9000 committed
983
            if f:
984
                absfile = os.path.abspath(f)
985
                self.fileentry[i].set_filename(absfile)
986
                self.fileentry[i].prepend_history(absfile)
987
                self.textbuffer[i].reset_buffer(absfile)
988
                self.msgarea_mgr[i].clear()
steve9000's avatar
steve9000 committed
989
        self.recompute_label()
Stephen Kennedy's avatar
Stephen Kennedy committed
990
        self.textview[len(files) >= 2].grab_focus()
991
        self._connect_buffer_handlers()
992
        self.scheduler.add_task(self._set_files_internal(files))
steve9000's avatar
steve9000 committed
993

994 995 996 997
    def get_comparison(self):
        files = [b.data.filename for b in self.textbuffer[:self.num_panes]]
        return recent.TYPE_FILE, files

998
    def _load_files(self, files, textbuffers):
999
        self.undosequence.clear()
steve9000's avatar
steve9000 committed
1000
        yield _("[%s] Set num panes") % self.label_text
1001 1002
        self.set_num_panes( len(files) )
        self._disconnect_buffer_handlers()
1003
        self.linediffer.clear()
1004
        self.queue_draw()
1005
        try_codecs = self.prefs.text_codecs.split() or ['utf_8', 'utf_16']
steve9000's avatar
steve9000 committed
1006
        yield _("[%s] Opening files") % self.label_text
steve9000's avatar
steve9000 committed
1007
        tasks = []
1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018

        def add_dismissable_msg(pane, icon, primary, secondary):
            msgarea = self.msgarea_mgr[pane].new_from_text_and_icon(
                            icon, primary, secondary)
            button = msgarea.add_stock_button_with_text(_("Hi_de"),
                            gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
            msgarea.connect("response",
                            lambda *args: self.msgarea_mgr[pane].clear())
            msgarea.show_all()
            return msgarea

1019 1020 1021
        for pane, filename in enumerate(files):
            buf = textbuffers[pane]
            if filename:
steve9000's avatar
steve9000 committed
1022
                try:
1023 1024 1025
                    handle = codecs.open(filename, "rU", try_codecs[0])
                    task = TaskEntry(filename, handle, buf, try_codecs[:],
                                     pane, False)
steve9000's avatar
steve9000 committed
1026
                    tasks.append(task)
1027
                except (IOError, LookupError) as e:
1028
                    buf.delete(*buf.get_bounds())
1029
                    add_dismissable_msg(pane, gtk.STOCK_DIALOG_ERROR,
1030
                                        _("Could not read file"), str(e))
steve9000's avatar
steve9000 committed
1031
        yield _("[%s] Reading files") % self.label_text
steve9000's avatar
steve9000 committed
1032 1033
        while len(tasks):
            for t in tasks[:]:
steve9000's avatar
steve9000 committed
1034 1035
                try:
                    nextbit = t.file.read(4096)
1036
                    if nextbit.find("\x00") != -1:
1037
                        t.buf.delete(*t.buf.get_bounds())
1038
                        filename = gobject.markup_escape_text(t.filename)
1039
                        add_dismissable_msg(t.pane, gtk.STOCK_DIALOG_ERROR,
1040
                            _("Could not read file"),
1041
                            _("%s appears to be a binary file.") % filename)
1042
                        tasks.remove(t)
1043
                        continue
1044
                except ValueError as err:
steve9000's avatar
steve9000 committed
1045 1046
                    t.codec.pop(0)
                    if len(t.codec):
1047
                        t.buf.delete(*t.buf.get_bounds())
1048
                        t.file = codecs.open(t.filename, "rU", t.codec[0])
steve9000's avatar
steve9000 committed
1049
                    else:
1050
                        t.buf.delete(*t.buf.get_bounds())
1051
                        filename = gobject.markup_escape_text(t.filename)
1052 1053 1054
                        add_dismissable_msg(t.pane, gtk.STOCK_DIALOG_ERROR,
                                        _("Could not read file"),
                                        _("%s is not in encodings: %s") %
1055
                                            (filename, try_codecs))
steve9000's avatar
steve9000 committed
1056
                        tasks.remove(t)
1057
                except IOError as ioerr:
1058 1059
                    add_dismissable_msg(t.pane, gtk.STOCK_DIALOG_ERROR,
                                    _("Could not read file"), str(ioerr))
1060
                    tasks.remove(t)
steve9000's avatar
steve9000 committed
1061
                else:
1062 1063 1064 1065 1066 1067
                    # The handling here avoids inserting split CR/LF pairs into
                    # GtkTextBuffers; this is relevant only when universal
                    # newline support is unavailable or broken.
                    if t.was_cr:
                        nextbit = "\r" + nextbit
                        t.was_cr = False
steve9000's avatar
steve9000 committed
1068
                    if len(nextbit):
1069
                        if nextbit[-1] == "\r" and len(nextbit) > 1:
1070 1071
                            t.was_cr = True
                            nextbit = nextbit[0:-1]
1072
                        t.buf.insert(t.buf.get_end_iter(), nextbit)
steve9000's avatar
steve9000 committed
1073
                    else:
1074 1075 1076
                        writable = os.access(t.filename, os.W_OK)
                        self.set_buffer_writable(t.buf, writable)
                        t.buf.data.encoding = t.codec[0]
1077
                        if hasattr(t.file, "newlines"):
1078
                            t.buf.data.newlines = t.file.newlines
steve9000's avatar
steve9000 committed
1079
                        tasks.remove(t)
steve9000's avatar
steve9000 committed
1080
            yield 1
1081 1082
        for b in self.textbuffer:
            self.undosequence.checkpoint(b)
1083

1084
    def _diff_files(self):
steve9000's avatar
steve9000 committed
1085
        yield _("[%s] Computing differences") % self.label_text
1086
        texts = self.buffer_filtered[:self.num_panes]
1087
        step = self.linediffer.set_sequences_iter(texts)
Kai Willadsen's avatar
Kai Willadsen committed
1088
        while next(step) is None:
steve9000's avatar
steve9000 committed
1089
            yield 1
1090

Kai Willadsen's avatar
Kai Willadsen committed
1091
        chunk, prev, next_ = self.linediffer.locate_chunk(1, 0)
1092 1093
        self.cursor.next = chunk
        if self.cursor.next is None:
Kai Willadsen's avatar
Kai Willadsen committed
1094
            self.cursor.next = next_
1095 1096
        for buf in self.textbuffer:
            buf.place_cursor(buf.get_start_iter())
1097
        self.scheduler.add_task(lambda: self.next_diff(gtk.gdk.SCROLL_DOWN), True)
1098 1099
        self.queue_draw()
        self._connect_buffer_handlers()
1100
        self._set_merge_action_sensitivity()
1101 1102

        langs = []
1103
        for i in range(self.num_panes):
1104
            filename = self.textbuffer[i].data.filename
1105
            if filename:
1106
                langs.append(srcviewer.get_language_from_file(filename))
1107 1108
            else:
                langs.append(None)
1109 1110 1111 1112 1113 1114 1115 1116 1117

        # If we have only one identified language then we assume that all of
        # the files are actually of that type.
        real_langs = [l for l in langs if l]
        if real_langs and real_langs.count(real_langs[0]) == len(real_langs):
            langs = (real_langs[0],) * len(langs)

        for i in range(self.num_panes):
            srcviewer.set_language(self.textbuffer[i], langs[i])
1118 1119
            srcviewer.set_highlight_syntax(self.textbuffer[i],
                                           self.prefs.use_syntax_highlighting)
1120

1121
    def _set_files_internal(self, files):
1122
        for i in self._load_files(files, self.textbuffer):
1123
            yield i
1124
        for i in self._diff_files():
1125 1126
            yield i

1127 1128 1129 1130 1131
    def refresh_comparison(self):
        """Refresh the view by clearing and redoing all comparisons"""
        self._disconnect_buffer_handlers()
        self.linediffer.clear()
        self.queue_draw()
1132
        self.scheduler.add_task(self._diff_files())
1133

1134 1135
    def _set_merge_action_sensitivity(self):
        pane = self._get_focused_pane()
1136 1137 1138 1139 1140 1141
        if pane != -1:
            editable = self.textview[pane].get_editable()
            mergeable = self.linediffer.has_mergeable_changes(pane)
        else:
            editable = False
            mergeable = (False, False)
1142 1143
        self.actiongroup.get_action("MergeFromLeft").set_sensitive(mergeable[0] and editable)
        self.actiongroup.get_action("MergeFromRight").set_sensitive(mergeable[1] and editable)
1144 1145 1146 1147
        if self.num_panes == 3 and self.textview[1].get_editable():
            mergeable = self.linediffer.has_mergeable_changes(1)
        else:
            mergeable = (False, False)
1148
        self.actiongroup.get_action("MergeAll").set_sensitive(mergeable[0] or mergeable[1])
1149

1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187
    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]