diff --git a/data/ui/filediff.ui b/data/ui/filediff.ui
index f39d6fa..9619c4a 100644
--- a/data/ui/filediff.ui
+++ b/data/ui/filediff.ui
@@ -241,18 +241,12 @@
               </packing>
             </child>
             <child>
-              <object class="GtkDrawingArea" id="linkmap0">
+              <object class="LinkMap" id="linkmap0">
                 <property name="width_request">50</property>
                 <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="has_focus">True</property>
+                <property name="can_focus">False</property>
+                <property name="has_focus">False</property>
                 <property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK</property>
-                <signal handler="on_linkmap_expose_event" name="expose_event"/>
-                <signal handler="on_linkmap_button_press_event" name="button_press_event"/>
-                <signal handler="on_key_press_event" name="key_press_event"/>
-                <signal handler="on_linkmap_button_release_event" name="button_release_event"/>
-                <signal handler="on_key_release_event" name="key_release_event"/>
-                <signal handler="on_linkmap_scroll_event" name="scroll_event"/>
               </object>
               <packing>
                 <property name="left_attach">2</property>
@@ -274,18 +268,12 @@
               </packing>
             </child>
             <child>
-              <object class="GtkDrawingArea" id="linkmap1">
+              <object class="LinkMap" id="linkmap1">
                 <property name="width_request">50</property>
                 <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="has_focus">True</property>
+                <property name="can_focus">False</property>
+                <property name="has_focus">False</property>
                 <property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK</property>
-                <signal handler="on_linkmap_expose_event" name="expose_event"/>
-                <signal handler="on_linkmap_button_press_event" name="button_press_event"/>
-                <signal handler="on_key_press_event" name="key_press_event"/>
-                <signal handler="on_linkmap_button_release_event" name="button_release_event"/>
-                <signal handler="on_key_release_event" name="key_release_event"/>
-                <signal handler="on_linkmap_scroll_event" name="scroll_event"/>
               </object>
               <packing>
                 <property name="left_attach">4</property>
diff --git a/meld/filediff.py b/meld/filediff.py
index e9252f4..52585aa 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -139,6 +139,8 @@ class BufferLines(object):
 
 MASK_SHIFT, MASK_CTRL = 1, 2
 
+MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
+
 def get_iter_at_line_or_eof(buffer, line):
     if line >= buffer.get_line_count():
         return buffer.get_end_iter()
@@ -184,6 +186,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
 
     __gsignals__ = {
         'next-conflict-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (bool, bool)),
+        'action-mode-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (int,)),
     }
 
     def __init__(self, prefs, num_panes):
@@ -206,7 +209,7 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             if self.prefs.show_whitespace:
                 v.set_draw_spaces(srcviewer.spaces_flag)
             srcviewer.set_tab_width(v, self.prefs.tab_size)
-        self.keymask = 0
+        self._keymask = 0
         self.load_font()
         self.deleted_lines_pending = -1
         self.textview_overwrite = 0
@@ -296,7 +299,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         gnomeglade.connect_signal_handlers(self)
         self.findbar = findbar.FindBar()
         self.filediff.pack_end(self.findbar.widget, False)
-        self.focus_before_click = None
         self.cursor = CursorDetails()
         self.connect("current-diff-changed", self.on_current_diff_changed)
         for t in self.textview:
@@ -306,9 +308,21 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         self.undosequence.connect("checkpointed", self.on_undo_checkpointed)
         self.connect("next-conflict-changed", self.on_next_conflict_changed)
 
+    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)
+
     def on_focus_change(self):
         self.keymask = 0
-        self._update_linkmap_buttons()
 
     def on_container_switch_in_event(self, ui):
         melddoc.MeldDoc.on_container_switch_in_event(self, ui)
@@ -602,17 +616,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         for i in range(2):
             self.linkmap[i].queue_draw()
 
-        icon_theme = gtk.icon_theme_get_default()
-        load = lambda x: icon_theme.load_icon(x, self.pixels_per_line, 0)
-        self.pixbuf_apply0 = load("button_apply0")
-        self.pixbuf_apply1 = load("button_apply1")
-        self.pixbuf_delete = load("button_delete")
-        # FIXME: this is a somewhat bizarre action to take, but our non-square
-        # icons really make this kind of handling difficult
-        load = lambda x: icon_theme.load_icon(x, self.pixels_per_line * 2, 0)
-        self.pixbuf_copy0  = load("button_copy0")
-        self.pixbuf_copy1  = load("button_copy1")
-
     def on_preference_changed(self, key, value):
         if key == "tab_size":
             tabs = pango.TabArray(10, 0)
@@ -644,18 +647,10 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
             self.linediffer.ignore_blanks = self.prefs.ignore_blank_lines
             self.set_files([None] * self.num_panes) # Refresh
 
-    def _update_linkmap_buttons(self):
-        for l in self.linkmap[:self.num_panes - 1]:
-            a = l.get_allocation()
-            w = self.pixbuf_copy0.get_width()
-            l.queue_draw_area(0,      0, w, a[3])
-            l.queue_draw_area(a[2]-w, 0, w, a[3])
-
     def on_key_press_event(self, object, event):
         x = self.keylookup.get(event.keyval, 0)
         if self.keymask | x != self.keymask:
             self.keymask |= x
-            self._update_linkmap_buttons()
         elif event.keyval == gtk.keysyms.Escape:
             self.findbar.hide()
 
@@ -663,11 +658,9 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         x = self.keylookup.get(event.keyval, 0)
         if self.keymask & ~x != self.keymask:
             self.keymask &= ~x
-            self._update_linkmap_buttons()
         # Ugly workaround for bgo#584342
         elif event.keyval == gtk.keysyms.ISO_Prev_Group:
             self.keymask = 0
-            self._update_linkmap_buttons()
 
     def _get_pane_label(self, i):
         #TRANSLATORS: this is the name of a new file which has not yet been saved
@@ -753,17 +746,13 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
     def on_find_activate(self, *args):
         self.findbar.start_find( self.textview_focussed )
         self.keymask = 0
-        self.queue_draw()
 
     def on_replace_activate(self, *args):
         self.findbar.start_replace( self.textview_focussed )
         self.keymask = 0
-        self.queue_draw()
 
     def on_find_next_activate(self, *args):
-        self.findbar.start_find_next( self.textview_focussed )
-        self.keymask = 0
-        self.queue_draw()
+        self.findbar.start_find_next(self.textview_focussed)
 
     def on_filediff__key_press_event(self, entry, event):
         if event.keyval == gtk.keysyms.Escape:
@@ -1381,6 +1370,9 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
                 scroll = self.scrolledwindow[i].get_vscrollbar()
                 w.setup(scroll, self.textbuffer[i], chunk_change_fn(i))
 
+            for (w, i) in zip(self.linkmap, (0, self.num_panes - 2)):
+                w.associate(self, self.textview[i], self.textview[i + 1])
+
             for i in range(self.num_panes):
                 if self.bufferdata[i].modified:
                     self.statusimage[i].show()
@@ -1423,165 +1415,6 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
                 buf.place_cursor(buf.get_iter_at_line(c[1]))
             self.textview[pane].scroll_to_mark(buf.get_insert(), 0.1)
 
-    def paint_pixbuf_at(self, context, pixbuf, x, y):
-        context.translate(x, y)
-        context.set_source_pixbuf(pixbuf, 0, 0)
-        context.paint()
-        context.identity_matrix()
-
-    def _linkmap_draw_icon(self, context, which, change, x, f0, t0):
-        if self.keymask & MASK_SHIFT:
-            pix0 = self.pixbuf_delete
-            pix1 = self.pixbuf_delete
-        elif self.keymask & MASK_CTRL and \
-             change[0] not in ('insert', 'delete'):
-            pix0 = self.pixbuf_copy0
-            pix1 = self.pixbuf_copy1
-        else: # self.keymask == 0:
-            pix0 = self.pixbuf_apply0
-            pix1 = self.pixbuf_apply1
-        if change[0] in ("insert", "replace") or (change[0] == "conflict" and
-                change[3] - change[4] != 0):
-            self.paint_pixbuf_at(context, pix1, x, t0)
-        if change[0] in ("delete", "replace") or (change[0] == "conflict" and
-                change[1] - change[2] != 0):
-            self.paint_pixbuf_at(context, pix0, 0, f0)
-
-        #
-        # linkmap drawing
-        #
-    def on_linkmap_expose_event(self, widget, event):
-        wtotal, htotal = widget.allocation.width, widget.allocation.height
-        yoffset = widget.allocation.y
-        context = widget.window.cairo_create()
-        context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
-        context.clip()
-        context.set_line_width(1.0)
-
-        which = self.linkmap.index(widget)
-        pix_start = [t.get_visible_rect().y for t in self.textview]
-        rel_offset = [t.allocation.y - yoffset for t in self.textview]
-
-        def bounds(idx):
-            return [self._pixel_to_line(idx, pix_start[idx]), self._pixel_to_line(idx, pix_start[idx]+htotal)]
-        visible = [None] + bounds(which) + bounds(which+1)
-
-        # For bezier control points
-        x_steps = [-0.5, (1. / 3) * wtotal, (2. / 3) * wtotal, wtotal + 0.5]
-
-        for c in self.linediffer.pair_changes(which, which + 1, visible[1:5]):
-            # f and t are short for "from" and "to"
-            f0, f1 = [self._line_to_pixel(which, l) - pix_start[which] + rel_offset[which] for l in c[1:3]]
-            t0, t1 = [self._line_to_pixel(which + 1, l) - pix_start[which + 1] + rel_offset[which + 1] for l in c[3:5]]
-
-            context.move_to(x_steps[0], f0 - 0.5)
-            context.curve_to(x_steps[1], f0 - 0.5,
-                             x_steps[2], t0 - 0.5,
-                             x_steps[3], t0 - 0.5)
-            context.line_to(x_steps[3], t1 - 0.5)
-            context.curve_to(x_steps[2], t1 - 0.5,
-                             x_steps[1], f1 - 0.5,
-                             x_steps[0], f1 - 0.5)
-            context.close_path()
-
-            context.set_source_rgb(*self.fill_colors[c[0]])
-            context.fill_preserve()
-
-            if self.linediffer.locate_chunk(which, c[1])[0] == self.cursor.chunk:
-                context.set_source_rgba(1.0, 1.0, 1.0, 0.5)
-                context.fill_preserve()
-
-            context.set_source_rgb(*self.line_colors[c[0]])
-            context.stroke()
-
-            x = wtotal-self.pixbuf_apply0.get_width()
-            self._linkmap_draw_icon(context, which, c, x, f0, t0)
-
-        # allow for scrollbar at end of textview
-        mid = int(0.5 * self.textview[0].allocation.height) + 0.5
-        context.set_source_rgba(0., 0., 0., 0.5)
-        context.move_to(.35 * wtotal, mid)
-        context.line_to(.65 * wtotal, mid)
-        context.stroke()
-
-    def on_linkmap_scroll_event(self, area, event):
-        self.next_diff(event.direction)
-
-    def _linkmap_process_event(self, event, which, side, htotal, rect_x, pix_width, pix_height):
-        src = which + side
-        dst = which + 1 - side
-        yoffset = self.linkmap[which].allocation.y
-        rel_offset = self.textview[src].allocation.y - yoffset
-        adj = self.scrolledwindow[src].get_vadjustment()
-
-        for c in self.linediffer.pair_changes(src, dst):
-            if c[0] == "insert" or (c[0] == "conflict" and c[1] - c[2] == 0):
-                continue
-            h = self._line_to_pixel(src, c[1]) - adj.value + rel_offset
-            if h < 0: # find first visible chunk
-                continue
-            elif h > htotal: # we've gone past last visible
-                break
-            elif h < event.y and event.y < h + pix_height:
-                self.mouse_chunk = ((src, dst), (rect_x, h, pix_width, pix_height), c)
-                break
-
-    def on_linkmap_button_press_event(self, area, event):
-        if event.button == 1:
-            self.focus_before_click = None
-            for t in self.textview:
-                if t.is_focus():
-                    self.focus_before_click = t
-                    break
-            area.grab_focus()
-            self.mouse_chunk = None
-            wtotal, htotal = area.allocation.width, area.allocation.height
-            pix_width = self.pixbuf_apply0.get_width()
-            pix_height = self.pixbuf_apply0.get_height()
-            if self.keymask == MASK_CTRL: # hack
-                pix_height *= 2
-
-            which = self.linkmap.index(area)
-
-            # quick reject are we near the gutter?
-            if event.x < pix_width:
-                side = 0
-                rect_x = 0
-            elif event.x > wtotal - pix_width:
-                side = 1
-                rect_x = wtotal - pix_width
-            else:
-                return True
-            self._linkmap_process_event(event, which, side, htotal, rect_x, pix_width, pix_height)
-            #print self.mouse_chunk
-            return True
-        return False
-
-    def on_linkmap_button_release_event(self, area, event):
-        if event.button == 1:
-            if self.focus_before_click:
-                self.focus_before_click.grab_focus()
-                self.focus_before_click = None
-            if self.mouse_chunk:
-                (src,dst), rect, chunk = self.mouse_chunk
-                self.mouse_chunk = None
-                # check we're still in button
-                inrect = lambda p, r: (r[0] < p.x < r[0] + r[2]) and (r[1] < p.y < r[1] + r[3])
-                if inrect(event, rect):
-                    # gtk tries to jump back to where the cursor was unless we move the cursor
-                    self.textview[src].place_cursor_onscreen()
-                    self.textview[dst].place_cursor_onscreen()
-
-                    if self.keymask & MASK_SHIFT: # delete
-                        self.delete_chunk(src, chunk)
-                    elif self.keymask & MASK_CTRL: # copy up or down
-                        copy_up = event.y - rect[1] < 0.5 * rect[3]
-                        self.copy_chunk(src, dst, chunk, copy_up)
-                    else: # replace
-                        self.replace_chunk(src, dst, chunk)
-            return True
-        return False
-
     def copy_chunk(self, src, dst, chunk, copy_up):
         b0, b1 = self.textbuffer[src], self.textbuffer[dst]
         start = get_iter_at_line_or_eof(b0, chunk[1])
diff --git a/meld/filemerge.py b/meld/filemerge.py
index aaf7113..4cf27df 100644
--- a/meld/filemerge.py
+++ b/meld/filemerge.py
@@ -87,54 +87,3 @@ class FileMerge(filediff.FileDiff):
         self.bufferdata[1].modified = 1
         self.recompute_label()
         yield 1
-
-    def _linkmap_draw_icon(self, context, which, change, x, f0, t0):
-        pix0 = self.pixbuf_delete
-        if which:
-            if self.keymask & MASK_CTRL:
-                pix1 = self.pixbuf_copy1
-            else:
-                pix1 = self.pixbuf_apply1
-        else:
-            if self.keymask & MASK_CTRL:
-                pix1 = self.pixbuf_copy0
-            else:
-                pix1 = self.pixbuf_apply0
-
-        if which:
-            if change[0] in ("delete"):
-                self.paint_pixbuf_at(context, pix0, 0, f0)
-            if change[0] in ("insert", "replace", "conflict"):
-                self.paint_pixbuf_at(context, pix1, x, t0)
-        else:
-            if change[0] in ("insert"):
-                self.paint_pixbuf_at(context, pix0, x, t0)
-            if change[0] in ("delete", "replace", "conflict"):
-                self.paint_pixbuf_at(context, pix1, 0, f0)
-
-    def _linkmap_process_event(self, event, which, side, htotal, rect_x, pix_width, pix_height):
-        origsrc = which + side
-        src = 2 * which
-        dst = 1
-        srcadj = self.scrolledwindow[src].get_vadjustment()
-        dstadj = self.scrolledwindow[dst].get_vadjustment()
-        yoffset = self.linkmap[which].allocation.y
-        dst_offset = self.textview[dst].allocation.y - yoffset
-        src_offset = self.textview[src].allocation.y - yoffset
-
-        for c in self.linediffer.pair_changes(src, dst):
-            if c[0] == "insert":
-                if origsrc != 1:
-                    continue
-                h = self._line_to_pixel(dst, c[3]) - dstadj.value + dst_offset
-            else:
-                if origsrc == 1:
-                    continue
-                h = self._line_to_pixel(src, c[1]) - srcadj.value + src_offset
-            if h < 0: # find first visible chunk
-                continue
-            elif h > htotal: # we've gone past last visible
-                break
-            elif h < event.y and event.y < h + pix_height:
-                self.mouse_chunk = ((src, dst), (rect_x, h, pix_width, pix_height), c)
-                break
diff --git a/meld/linkmap.py b/meld/linkmap.py
new file mode 100644
index 0000000..d0f5c78
--- /dev/null
+++ b/meld/linkmap.py
@@ -0,0 +1,288 @@
+### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
+### Copyright (C) 2009-2011 Kai Willadsen <kai.willadsen@gmail.com>
+
+### 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
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+import gtk
+
+import diffutil
+
+
+# FIXME: import order issues
+MODE_REPLACE, MODE_DELETE, MODE_INSERT = 0, 1, 2
+
+
+class LinkMap(gtk.DrawingArea):
+
+    __gtype_name__ = "LinkMap"
+
+    __gsignals__ = {
+        'expose-event': 'override',
+        'scroll-event': 'override',
+        'button-press-event': 'override',
+        'button-release-event': 'override',
+    }
+
+    def __init__(self):
+        self.mode = MODE_REPLACE
+
+    def associate(self, filediff, left_view, right_view):
+        self.filediff = filediff
+        self.views = [left_view, right_view]
+        if self.get_direction() == gtk.TEXT_DIR_RTL:
+            self.views.reverse()
+        self.view_indices = [filediff.textview.index(t) for t in self.views]
+
+        self.fill_colors = filediff.fill_colors
+        self.line_colors = filediff.line_colors
+
+        pixels_per_line = filediff.pixels_per_line
+        icon_theme = gtk.icon_theme_get_default()
+        load = lambda x: icon_theme.load_icon(x, pixels_per_line, 0)
+        pixbuf_apply0 = load("button_apply0")
+        pixbuf_apply1 = load("button_apply1")
+        pixbuf_delete = load("button_delete")
+        # FIXME: this is a somewhat bizarre action to take, but our non-square
+        # icons really make this kind of handling difficult
+        load = lambda x: icon_theme.load_icon(x, pixels_per_line * 2, 0)
+        pixbuf_copy0  = load("button_copy0")
+        pixbuf_copy1  = load("button_copy1")
+
+        self.action_map_left = {
+            MODE_REPLACE: pixbuf_apply0,
+            MODE_DELETE: pixbuf_delete,
+            MODE_INSERT: pixbuf_copy0,
+        }
+
+        self.action_map_right = {
+            MODE_REPLACE: pixbuf_apply1,
+            MODE_DELETE: pixbuf_delete,
+            MODE_INSERT: pixbuf_copy1,
+        }
+
+        self.button_width = pixbuf_apply0.get_width()
+        self.button_height = pixbuf_apply0.get_height()
+
+        filediff.connect("action-mode-changed", self.on_container_mode_changed)
+
+    def on_container_mode_changed(self, container, mode):
+        # On mode change, set our local copy of the mode, and cancel any mouse
+        # actions in progress. Otherwise, if someone clicks, then releases
+        # Shift, then releases the button... what do we do?
+        self.mode = mode
+        self.mouse_chunk = None
+        x, y, width, height = self.allocation
+        pixbuf_width = self.button_width
+        self.queue_draw_area(0, 0, pixbuf_width, height)
+        self.queue_draw_area(width - pixbuf_width, 0, pixbuf_width, height)
+
+    def paint_pixbuf_at(self, context, pixbuf, x, y):
+        context.translate(x, y)
+        context.set_source_pixbuf(pixbuf, 0, 0)
+        context.paint()
+        context.identity_matrix()
+
+    def _classify_change_actions(self, change):
+        """Classify possible actions for the given change
+
+        Returns a tuple containing actions that can be performed given the
+        content and context of the change. The tuple gives the actions for
+        the left and right sides of the LinkMap.
+        """
+        left_editable, right_editable = [v.get_editable() for v in self.views]
+
+        if not left_editable and not right_editable:
+            return None, None
+
+        # Reclassify conflict changes, since we treat them the same as a
+        # normal two-way change as far as actions are concerned
+        change_type = change[0]
+        if change_type == "conflict":
+            if change[1] == change[2]:
+                change_type = "insert"
+            elif change[3] == change[4]:
+                change_type = "delete"
+            else:
+                change_type = "replace"
+
+        left_act, right_act = None, None
+        if change_type == "delete":
+            left_act = MODE_REPLACE
+            if self.mode == MODE_DELETE and left_editable:
+                left_act = MODE_DELETE
+        elif change_type == "insert":
+            right_act = MODE_REPLACE
+            if self.mode == MODE_DELETE and right_editable:
+                right_act = MODE_DELETE
+        elif change_type == "replace":
+            if not left_editable:
+                left_act, right_act = MODE_REPLACE, MODE_DELETE
+                if self.mode == MODE_INSERT:
+                    left_act = MODE_INSERT
+            elif not right_editable:
+                left_act, right_act = MODE_DELETE, MODE_REPLACE
+                if self.mode == MODE_INSERT:
+                    right_act = MODE_INSERT
+            else:
+                left_act, right_act = MODE_REPLACE, MODE_REPLACE
+                if self.mode == MODE_DELETE:
+                    left_act, right_act = MODE_DELETE, MODE_DELETE
+                elif self.mode == MODE_INSERT:
+                    left_act, right_act = MODE_INSERT, MODE_INSERT
+
+        return left_act, right_act
+
+    def _linkmap_draw_icon(self, context, change, x, f0, t0):
+        left_act, right_act = self._classify_change_actions(change)
+        if left_act is not None:
+            pix0 = self.action_map_left[left_act]
+            self.paint_pixbuf_at(context, pix0, 0, f0)
+        if right_act is not None:
+            pix1 = self.action_map_right[right_act]
+            self.paint_pixbuf_at(context, pix1, x, t0)
+
+    def do_expose_event(self, event):
+        context = self.window.cairo_create()
+        context.rectangle(event.area.x, event.area.y, event.area.width, \
+                          event.area.height)
+        context.clip()
+        context.set_line_width(1.0)
+
+        pix_start = [t.get_visible_rect().y for t in self.views]
+        rel_offset = [t.allocation.y - self.allocation.y for t in self.views]
+
+        height = self.allocation.height
+        visible = [self.views[0].get_line_num_for_y(pix_start[0]),
+                   self.views[0].get_line_num_for_y(pix_start[0] + height),
+                   self.views[1].get_line_num_for_y(pix_start[1]),
+                   self.views[1].get_line_num_for_y(pix_start[1] + height)]
+
+        wtotal = self.allocation.width
+        # For bezier control points
+        x_steps = [-0.5, (1. / 3) * wtotal, (2. / 3) * wtotal, wtotal + 0.5]
+
+        left, right = self.view_indices
+        view_offset_line = lambda v, l: self.views[v].get_y_for_line_num(l) - \
+                                        pix_start[v] + rel_offset[v]
+        for c in self.filediff.linediffer.pair_changes(left, right, visible):
+            # f and t are short for "from" and "to"
+            f0, f1 = [view_offset_line(0, l) for l in c[1:3]]
+            t0, t1 = [view_offset_line(1, l) for l in c[3:5]]
+
+            context.move_to(x_steps[0], f0 - 0.5)
+            context.curve_to(x_steps[1], f0 - 0.5,
+                             x_steps[2], t0 - 0.5,
+                             x_steps[3], t0 - 0.5)
+            context.line_to(x_steps[3], t1 - 0.5)
+            context.curve_to(x_steps[2], t1 - 0.5,
+                             x_steps[1], f1 - 0.5,
+                             x_steps[0], f1 - 0.5)
+            context.close_path()
+
+            context.set_source_rgb(*self.fill_colors[c[0]])
+            context.fill_preserve()
+
+            chunk_idx = self.filediff.linediffer.locate_chunk(left, c[1])[0]
+            if chunk_idx == self.filediff.cursor.chunk:
+                context.set_source_rgba(1.0, 1.0, 1.0, 0.5)
+                context.fill_preserve()
+
+            context.set_source_rgb(*self.line_colors[c[0]])
+            context.stroke()
+
+            x = wtotal - self.button_width
+            self._linkmap_draw_icon(context, c, x, f0, t0)
+
+        # allow for scrollbar at end of textview
+        mid = int(0.5 * self.views[0].allocation.height) + 0.5
+        context.set_source_rgba(0., 0., 0., 0.5)
+        context.move_to(.35 * wtotal, mid)
+        context.line_to(.65 * wtotal, mid)
+        context.stroke()
+
+    def do_scroll_event(self, event):
+        self.filediff.next_diff(event.direction)
+
+    def _linkmap_process_event(self, event, side, x, pix_width, pix_height):
+        src_idx, dst_idx = side, 1 if side == 0 else 0
+        src, dst = self.view_indices[src_idx], self.view_indices[dst_idx]
+
+        yoffset = self.allocation.y
+        rel_offset = self.views[side].allocation.y - yoffset
+        vis_offset = self.views[side].get_visible_rect().y
+
+        bounds = []
+        for v in (self.views[src_idx], self.views[dst_idx]):
+            visible = v.get_visible_rect()
+            bounds.append(v.get_line_num_for_y(visible.y))
+            bounds.append(v.get_line_num_for_y(visible.y + visible.height))
+
+        for c in self.filediff.linediffer.pair_changes(src, dst, bounds):
+            h = self.views[src_idx].get_y_for_line_num(c[1]) - \
+                vis_offset + rel_offset
+            if h < event.y < h + pix_height:
+                # _classify_change_actions assumes changes are left->right
+                action_change = diffutil.reverse_chunk(c) if dst < src else c
+                actions = self._classify_change_actions(action_change)
+                if actions[side] is not None:
+                    rect = gtk.gdk.Rectangle(x, h, pix_width, pix_height)
+                    self.mouse_chunk = ((src, dst), rect, c, actions[side])
+                break
+
+    def do_button_press_event(self, event):
+        if event.button == 1:
+            self.mouse_chunk = None
+            pix_width = self.button_width
+            pix_height = self.button_height
+            # Hack to deal with our non-square insert-mode icons
+            if self.mode == MODE_INSERT:
+                pix_height *= 2
+
+            # Quick reject if not in the area used to draw our buttons
+            right_gutter_x = self.allocation.width - pix_width
+            if event.x >= pix_width and event.x <= right_gutter_x:
+                return True
+
+            # side = 0 means left side of linkmap, so action from left -> right
+            side = 0 if event.x < pix_width else 1
+            x = 0 if event.x < pix_width else right_gutter_x
+            self._linkmap_process_event(event, side, x, pix_width, pix_height)
+            return True
+        return False
+
+    def do_button_release_event(self, event):
+        if event.button == 1:
+            if self.mouse_chunk:
+                (src, dst), rect, chunk, action = self.mouse_chunk
+                self.mouse_chunk = None
+                # Check that we're still in the same button we started in
+                if rect.x <= event.x < rect.x + rect.width and \
+                   rect.y <= event.y < rect.y + rect.height:
+                    # Unless we move the cursor, the view scrolls back to
+                    # its old position
+                    self.views[0].place_cursor_onscreen()
+                    self.views[1].place_cursor_onscreen()
+
+                    if action == MODE_DELETE:
+                        self.filediff.delete_chunk(src, chunk)
+                    elif action == MODE_INSERT:
+                        copy_up = event.y - rect[1] < 0.5 * rect[3]
+                        self.filediff.copy_chunk(src, dst, chunk, copy_up)
+                    else:
+                        self.filediff.replace_chunk(src, dst, chunk)
+            return True
+        return False
+
diff --git a/meld/ui/catalog.xml b/meld/ui/catalog.xml
index 9f8f254..b932c11 100644
--- a/meld/ui/catalog.xml
+++ b/meld/ui/catalog.xml
@@ -4,6 +4,7 @@
 
     <glade-widget-classes>
         <glade-widget-class title="DiffMap" name="DiffMap" generic-name="diffmap"/>
+        <glade-widget-class title="LinkMap" name="LinkMap" generic-name="linkmap"/>
         <glade-widget-class title="HistoryEntry" name="HistoryEntry" generic-name="historyentry"/>
         <glade-widget-class title="HistoryFileEntry" name="HistoryFileEntry" generic-name="historyfileentry"/>
         <glade-widget-class title="MsgArea" name="MsgArea" generic-name="msgarea"/>
@@ -12,6 +13,7 @@
 
     <glade-widget-group name="meld" title="Meld">
         <glade-widget-class-ref name="DiffMap"/>
+        <glade-widget-class-ref name="LinkMap"/>
         <glade-widget-class-ref name="HistoryEntry"/>
         <glade-widget-class-ref name="HistoryFileEntry"/>
         <glade-widget-class-ref name="MsgArea"/>
diff --git a/meld/ui/gladesupport.py b/meld/ui/gladesupport.py
index 6a14f3d..4afbf6d 100644
--- a/meld/ui/gladesupport.py
+++ b/meld/ui/gladesupport.py
@@ -1,5 +1,6 @@
 
 import historyentry
 import msgarea
+import meld.linkmap
 import meld.diffmap
 import meld.util.sourceviewer
diff --git a/meld/util/sourceviewer.py b/meld/util/sourceviewer.py
index fe7267b..a36c236 100644
--- a/meld/util/sourceviewer.py
+++ b/meld/util/sourceviewer.py
@@ -186,3 +186,14 @@ srcviewer = _get_srcviewer()
 
 class MeldSourceView(srcviewer.GtkTextView):
     __gtype_name__ = "MeldSourceView"
+
+    def get_y_for_line_num(self, line):
+        buf = self.get_buffer()
+        it = buf.get_iter_at_line(line)
+        y, h = self.get_line_yrange(it)
+        if line >= buf.get_line_count():
+            return y + h - 1
+        return y
+
+    def get_line_num_for_y(self, y):
+        return self.get_line_at_y(y)[0].get_line()
