[gedit-plugins/gnome-3-8] Add git plugin



commit 8406b61bb640ab878f30d52bf3c117288ee1cc61
Author: Ignacio Casal Quinteiro <icq gnome org>
Date:   Tue Apr 23 09:51:52 2013 +0200

    Add git plugin

 README                               |    1 +
 configure.ac                         |   34 ++++-
 plugins/git/Makefile.am              |   20 +++
 plugins/git/git.plugin.desktop.in.in |    9 ++
 plugins/git/git/Makefile.am          |   10 ++
 plugins/git/git/__init__.py          |   22 +++
 plugins/git/git/diffrenderer.py      |  127 ++++++++++++++++++
 plugins/git/git/viewactivatable.py   |  241 ++++++++++++++++++++++++++++++++++
 8 files changed, 460 insertions(+), 4 deletions(-)
---
diff --git a/README b/README
index ab459d4..42b3402 100644
--- a/README
+++ b/README
@@ -26,6 +26,7 @@ commander             Command gedit from a command line like interface
 dashboard              A Dashboard created in every new tab for quick finding of
                        recently and frequently used items, with an ability to search
 drawspaces             Draw spaces and tabs.
+git                    Shows document changes related to git's HEAD.
 joinlines              Join several lines or split long ones.
 multiedit              Edit document in multiple places at once
 textsize               Easily increase and decrease the text size
diff --git a/configure.ac b/configure.ac
index e701449..647b9d6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -92,9 +92,9 @@ ALL_PLUGINS="bookmarks drawspaces wordcompletion"
 USEFUL_PLUGINS="bookmarks drawspaces wordcompletion"
 DEFAULT_PLUGINS="bookmarks drawspaces wordcompletion"
 
-PYTHON_ALL_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard 
joinlines multiedit textsize smartspaces terminal synctex"
-PYTHON_USEFUL_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard 
joinlines multiedit textsize smartspaces terminal synctex"
-PYTHON_DEFAULT_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard 
joinlines multiedit textsize smartspaces terminal synctex"
+PYTHON_ALL_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard git 
joinlines multiedit textsize smartspaces terminal synctex"
+PYTHON_USEFUL_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard 
git joinlines multiedit textsize smartspaces terminal synctex"
+PYTHON_DEFAULT_PLUGINS="bracketcompletion charmap codecomment colorpicker colorschemer commander dashboard 
git joinlines multiedit textsize smartspaces terminal synctex"
 
 DIST_PLUGINS="$ALL_PLUGINS $PYTHON_ALL_PLUGINS"
 
@@ -115,7 +115,7 @@ AC_ARG_WITH([plugins],
 [  --with-plugins=plugin1,plugin2,...
                          build the specified plugins. Available:
                          bracketcompletion, charmap, codecomment,
-                         colorpicker, drawspaces, joinlines, multiedit,
+                         colorpicker, drawspaces, git, joinlines, multiedit,
                          textsize, smartspaces,
                          terminal, wordcompletion, as well as the aliases
                          default, all, and really-all],
@@ -266,6 +266,29 @@ then
        fi
 fi
 
+# ================================================================
+# Git (libgit2-glib)
+# ================================================================
+plugin_defined synctex
+if test "$?" = 1
+then
+       LIBGIT2_GLIB_REQUIRED=0.0.2
+       PKG_CHECK_MODULES([GIT2_GLIB],
+               [libgit2-glib >= $LIBGIT2_GLIB_REQUIRED],
+               [have_git2=yes],[have_git2=no])
+
+       if test "x$have_git2" = "xno"; then
+               plugin_defined_explicit git
+               if test "$?" = 1
+               then
+                       AC_MSG_ERROR([libgit2-glib could not be found, needed for git plugin])
+               else
+                       AC_MSG_WARN([libgit2-glib could not be found, git plugin will be disabled])
+                       undef_plugin git "libgit2-glib not found"
+               fi
+       fi
+fi
+
 if test -z "$disabled_plugins"
 then
        disabled_plugins="none"
@@ -340,6 +363,9 @@ plugins/dashboard/Makefile
 plugins/drawspaces/drawspaces.plugin.desktop.in
 plugins/drawspaces/Makefile
 plugins/drawspaces/org.gnome.gedit.plugins.drawspaces.gschema.xml.in
+plugins/git/git.plugin.desktop.in
+plugins/git/git/Makefile
+plugins/git/Makefile
 plugins/joinlines/joinlines.plugin.desktop.in
 plugins/joinlines/Makefile
 plugins/multiedit/Makefile
diff --git a/plugins/git/Makefile.am b/plugins/git/Makefile.am
new file mode 100644
index 0000000..ac233a0
--- /dev/null
+++ b/plugins/git/Makefile.am
@@ -0,0 +1,20 @@
+# Git
+
+SUBDIRS = git
+
+plugindir = $(GEDIT_PLUGINS_LIBS_DIR)
+
+plugin_in_files = git.plugin.desktop.in
+
+%.plugin: %.plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po)
+       $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.plugin.desktop.in=.plugin)
+
+EXTRA_DIST = $(plugin_in_files)
+
+CLEANFILES = $(plugin_DATA)
+
+DISTCLEANFILES = $(plugin_DATA)
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/git/git.plugin.desktop.in.in b/plugins/git/git.plugin.desktop.in.in
new file mode 100644
index 0000000..755388d
--- /dev/null
+++ b/plugins/git/git.plugin.desktop.in.in
@@ -0,0 +1,9 @@
+[Plugin]
+Loader=python3
+Module=git
+IAge=3
+Name=Git
+Description=Git differences
+Authors=Ignacio Casal Quintiero <icq gnome org>;Garrett Regier <garrettregier gmail com>
+Copyright=Copyright © 2013 Ignacio Casal Quinteiro
+Website=http://www.gedit.org
diff --git a/plugins/git/git/Makefile.am b/plugins/git/git/Makefile.am
new file mode 100644
index 0000000..c3ab9af
--- /dev/null
+++ b/plugins/git/git/Makefile.am
@@ -0,0 +1,10 @@
+# Git
+
+plugindir = $(GEDIT_PLUGINS_LIBS_DIR)/git
+
+plugin_PYTHON =                \
+       diffrenderer.py         \
+       __init__.py             \
+       viewactivatable.py
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/git/git/__init__.py b/plugins/git/git/__init__.py
new file mode 100644
index 0000000..9ee99c5
--- /dev/null
+++ b/plugins/git/git/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+
+#  Copyright (C) 2013 - Ignacio Casal Quinteiro
+#
+#  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.
+
+from .viewactivatable import GitPlugin
+
+# ex:ts=4:et:
diff --git a/plugins/git/git/diffrenderer.py b/plugins/git/git/diffrenderer.py
new file mode 100644
index 0000000..709f0b9
--- /dev/null
+++ b/plugins/git/git/diffrenderer.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+
+#  Copyright (C) 2013 - Ignacio Casal Quinteiro
+#
+#  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.
+
+from gi.repository import Gdk, Gtk, GtkSource
+
+
+class DiffType:
+    (NONE,
+     ADDED,
+     MODIFIED,
+     REMOVED) = range(4)
+
+
+class DiffRenderer(GtkSource.GutterRenderer):
+
+    backgrounds = {}
+    backgrounds[DiffType.ADDED] = Gdk.RGBA()
+    backgrounds[DiffType.MODIFIED] = Gdk.RGBA()
+    backgrounds[DiffType.REMOVED] = Gdk.RGBA()
+    backgrounds[DiffType.ADDED].parse("#8ae234")
+    backgrounds[DiffType.MODIFIED].parse("#fcaf3e")
+    backgrounds[DiffType.REMOVED].parse("#ef2929")
+
+    def __init__(self):
+        GtkSource.GutterRenderer.__init__(self)
+
+        self.set_size(8)
+        self.set_padding(3, 0)
+
+        self.file_context = {}
+        self.tooltip = None
+        self.tooltip_line = 0
+
+    def do_draw(self, cr, bg_area, cell_area, start, end, state):
+        GtkSource.GutterRenderer.do_draw(self, cr, bg_area, cell_area,
+                                         start, end, state)
+
+        line_context = self.file_context.get(start.get_line() + 1, None)
+        if line_context is None or line_context.line_type == DiffType.NONE:
+            return
+
+        background = self.backgrounds[line_context.line_type]
+
+        Gdk.cairo_set_source_rgba(cr, background)
+        cr.rectangle(cell_area.x, cell_area.y,
+                     cell_area.width, cell_area.height)
+        cr.fill()
+
+    def do_query_tooltip(self, it, area, x, y, tooltip):
+        line = it.get_line() + 1
+
+        line_context = self.file_context.get(line, None)
+        if line_context is None:
+            return False
+
+        # Check that the context is the same not the line this
+        # way contexts that span multiple times are handled correctly
+        if self.file_context.get(self.tooltip_line, None) is line_context:
+            tooltip.set_custom(None)
+            tooltip.set_custom(self.tooltip)
+            return True
+
+        if line_context.line_type not in (DiffType.REMOVED, DiffType.MODIFIED):
+            return False
+
+        tooltip_buffer = GtkSource.Buffer()
+        tooltip_view = GtkSource.View.new_with_buffer(tooltip_buffer)
+
+        # Propagate the view's settings
+        content_view = self.get_view()
+        tooltip_view.set_indent_width(content_view.get_indent_width())
+        tooltip_view.set_tab_width(content_view.get_tab_width())
+
+        # Propagate the buffer's settings
+        content_buffer = content_view.get_buffer()
+        tooltip_buffer.set_highlight_syntax(content_buffer.get_highlight_syntax())
+        tooltip_buffer.set_language(content_buffer.get_language())
+        tooltip_buffer.set_style_scheme(content_buffer.get_style_scheme())
+
+        # Fix some styling issues
+        tooltip_buffer.set_highlight_matching_brackets(False)
+        tooltip_view.set_border_width(4)
+        tooltip_view.set_cursor_visible(False)
+
+        # Set the font
+        content_style_context = content_view.get_style_context()
+        content_font = content_style_context.get_font(Gtk.StateFlags.NORMAL)
+        tooltip_view.override_font(content_font)
+
+        # Only add what can be shown, we
+        # don't want to add hundreds of lines
+        allocation = content_view.get_allocation()
+        lines = allocation.height // area.height
+        removed = '\n'.join(map(str, line_context.removed_lines[:lines]))
+        tooltip_buffer.set_text(removed)
+
+        # Avoid having to create the tooltip multiple times
+        self.tooltip = tooltip_view
+        self.tooltip_line = line
+
+        tooltip.set_custom(tooltip_view)
+        return True
+
+    def set_file_context(self, file_context):
+        self.file_context = file_context
+        self.tooltip = None
+        self.tooltip_line = 0
+
+        self.queue_draw()
+
+# ex:ts=4:et:
diff --git a/plugins/git/git/viewactivatable.py b/plugins/git/git/viewactivatable.py
new file mode 100644
index 0000000..84600f6
--- /dev/null
+++ b/plugins/git/git/viewactivatable.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+
+#  Copyright (C) 2013 - Ignacio Casal Quinteiro
+#
+#  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.
+
+from gi.repository import GLib, GObject, Gtk, Gedit, Ggit
+from .diffrenderer import DiffType, DiffRenderer
+
+import difflib
+
+
+class LineContext:
+    __slots__ = ('removed_lines', 'line_type')
+
+    def __init__(self):
+        self.removed_lines = []
+        self.line_type = DiffType.NONE
+
+
+class GitPlugin(GObject.Object, Gedit.ViewActivatable):
+    view = GObject.property(type=Gedit.View)
+
+    def __init__(self):
+        GObject.Object.__init__(self)
+
+        Ggit.init()
+
+        self.diff_timeout = 0
+        self.file_contents_list = None
+        self.file_context = None
+
+    def do_activate(self):
+        self.diff_renderer = DiffRenderer()
+        self.gutter = self.view.get_gutter(Gtk.TextWindowType.LEFT)
+
+        self.view_signals = [
+            self.view.connect('notify::buffer', self.on_notify_buffer),
+            self.view.connect('focus-in-event', self.update_location)
+        ]
+
+        self.buffer = None
+        self.on_notify_buffer(self.view)
+
+    def do_deactivate(self):
+        if self.diff_timeout != 0:
+            GLib.source_remove(self.diff_timeout)
+
+        self.disconnect_buffer()
+        self.buffer = None
+
+        self.disconnect_view()
+        self.gutter.remove(self.diff_renderer)
+
+    def disconnect(self, obj, signals):
+        for sid in signals:
+            obj.disconnect(sid)
+
+        signals[:] = []
+
+    def disconnect_buffer(self):
+        self.disconnect(self.buffer, self.buffer_signals)
+
+    def disconnect_view(self):
+        self.disconnect(self.view, self.view_signals)
+
+    def on_notify_buffer(self, view, gspec=None):
+        if self.diff_timeout != 0:
+            GLib.source_remove(self.diff_timeout)
+
+        if self.buffer:
+            self.disconnect_buffer()
+
+        self.buffer = view.get_buffer()
+
+        # The changed signal is connected to in update_location()
+        self.buffer_signals = [
+            self.buffer.connect('loaded', self.update_location),
+            self.buffer.connect('saved', self.update_location)
+        ]
+
+        # We wait and let the loaded signal call
+        # update_location() as the buffer is currently empty
+
+    def update_location(self, *args):
+        self.location = self.buffer.get_location()
+        if self.location is None:
+            return
+
+        try:
+            repo_file = Ggit.Repository.discover(self.location)
+            repo = Ggit.Repository.open(repo_file)
+            head = repo.get_head()
+            commit = repo.lookup(head.get_target(), Ggit.Commit.__gtype__)
+            tree = commit.get_tree()
+
+        except Exception:
+            # Not a git repository
+            if self.file_contents_list is not None:
+                self.file_contents_list = None
+                self.gutter.remove(self.diff_renderer)
+                self.diff_renderer.set_file_context({})
+                self.buffer.disconnect(self.buffer_signals.pop())
+
+            return
+
+        if self.file_contents_list is None:
+            self.gutter.insert(self.diff_renderer, 40)
+            self.buffer_signals.append(self.buffer.connect('changed',
+                                                           self.update))
+
+        try:
+            relative_path = repo.get_workdir().get_relative_path(self.location)
+
+            entry = tree.get_by_path(relative_path)
+            file_blob = repo.lookup(entry.get_id(), Ggit.Blob.__gtype__)
+            file_contents = file_blob.get_raw_content()
+            self.file_contents_list = file_contents.decode('utf-8').splitlines()
+
+            # Remove the last empty line added by gedit automatically
+            last_item = self.file_contents_list[-1]
+            if last_item[-1:] == '\n':
+                self.file_contents_list[-1] = last_item[:-1]
+
+        except Exception:
+            # New file in a git repository
+            self.file_contents_list = []
+
+        self.update()
+
+    def update(self, *unused):
+        # We don't let the delay accumulate
+        if self.diff_timeout != 0:
+            return
+
+        # Do the initial diff without a delay
+        if self.file_context is None:
+            self.on_diff_timeout()
+
+        else:
+            n_lines = self.buffer.get_line_count()
+            delay = min(10000, 200 * (n_lines // 2000 + 1))
+
+            self.diff_timeout = GLib.timeout_add(delay,
+                                                 self.on_diff_timeout)
+
+    def on_diff_timeout(self):
+        self.diff_timeout = 0
+
+        # Must be a new file
+        if not self.file_contents_list:
+            n_lines = self.buffer.get_line_count()
+            if len(self.diff_renderer.file_context) == n_lines:
+                return False
+
+            line_context = LineContext()
+            line_context.line_type = DiffType.ADDED
+            file_context = dict(zip(range(1, n_lines + 1),
+                                    [line_context] * n_lines))
+
+            self.diff_renderer.set_file_context(file_context)
+            return False
+
+        start_iter, end_iter = self.buffer.get_bounds()
+        src_contents = start_iter.get_visible_text(end_iter)
+        src_contents_list = src_contents.splitlines()
+
+        # GtkTextBuffer does not consider a trailing "\n" to be text
+        if len(src_contents_list) != self.buffer.get_line_count():
+            src_contents_list.append('')
+
+        diff = difflib.unified_diff(self.file_contents_list,
+                                    src_contents_list, n=0)
+
+        # Skip the first 2 lines: ---, +++
+        try:
+            next(diff)
+            next(diff)
+
+        except StopIteration:
+            # Nothing has changed
+            pass
+
+        file_context = {}
+        for line_data in diff:
+            if line_data[0] == '@':
+                for token in line_data.split():
+                    if token[0] == '+':
+                        hunk_point = int(token.split(',', 1)[0])
+                        line_context = LineContext()
+                        break
+
+            elif line_data[0] == '-':
+                if line_context.line_type == DiffType.NONE:
+                    line_context.line_type = DiffType.REMOVED
+
+                line_context.removed_lines.append(line_data[1:])
+
+                # No hunk point increase
+                file_context[hunk_point] = line_context
+
+            elif line_data[0] == '+':
+                if line_context.line_type == DiffType.NONE:
+                    line_context.line_type = DiffType.ADDED
+                    file_context[hunk_point] = line_context
+
+                elif line_context.line_type == DiffType.REMOVED:
+                    # Why is this the only one that does
+                    # not add it to file_context?
+
+                    line_context.line_type = DiffType.MODIFIED
+
+                else:
+                    file_context[hunk_point] = line_context
+
+                hunk_point += 1
+
+        # Occurs when all of the original content is deleted
+        if 0 in file_context:
+            for i in reversed(list(file_context.keys())):
+                file_context[i + 1] = file_context[i]
+                del file_context[i]
+
+        self.file_context = file_context
+        self.diff_renderer.set_file_context(file_context)
+        return False
+
+# ex:ts=4:et:


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