# -*- 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. 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA.
from gi.repository import GLib, GObject, Gtk, Gedit, Ggit
from .appactivatable import GitAppActivatable
from .diffrenderer import DiffType, DiffRenderer
from .windowactivatable import GitWindowActivatable
import sys
import os.path
import difflib
class LineContext:
__slots__ = ('removed_lines', 'line_type')
def __init__(self):
self.removed_lines = []
self.line_type = DiffType.NONE
class GitViewActivatable(GObject.Object, Gedit.ViewActivatable):
view = GObject.Property(type=Gedit.View)
status = GObject.Property(type=Ggit.StatusFlags,
default=Ggit.StatusFlags.CURRENT)
def __init__(self):
super().__init__()
self.diff_timeout = 0
self.file_contents_list = None
self.file_context = None
def do_activate(self):
GitWindowActivatable.register_view_activatable(self)
self.app_activatable = GitAppActivatable.get_instance()
self.diff_renderer = DiffRenderer()
self.gutter = self.view.get_gutter(Gtk.TextWindowType.LEFT)
# Note: GitWindowActivatable will call
# update_location() for us when needed
self.view_signals = [
self.view.connect('notify::buffer', self.on_notify_buffer)
]
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().
# The saved signal is pointless as the window activatable
# will see the change and call update_location().
self.buffer_signals = [
self.buffer.connect('loaded', self.update_location)
]
# We wait and let the loaded signal call
# update_location() as the buffer is currently empty
# TODO: This can be called many times and by idles,
# should instead do the work in another thread
def update_location(self, *args):
self.location = self.buffer.get_file().get_location()
if self.location is not None:
repo = self.app_activatable.get_repository(self.location, False)
if self.location is None or repo is None:
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:
head = repo.get_head()
commit = repo.lookup(head.get_target(), Ggit.Commit)
tree = commit.get_tree()
relative_path = os.path.relpath(
os.path.realpath(self.location.get_path()),
repo.get_workdir().get_path()
)
entry = tree.get_by_path(relative_path)
file_blob = repo.lookup(entry.get_id(), Ggit.Blob)
try:
gitconfig = repo.get_config()
encoding = gitconfig.get_string('gui.encoding')
except GLib.Error:
encoding = 'utf8'
file_contents = file_blob.get_raw_content().decode(encoding)
self.file_contents_list = file_contents.splitlines()
# Remove the last empty line added by gedit automatically
if self.file_contents_list:
last_item = self.file_contents_list[-1]
if last_item[-1:] == '\n':
self.file_contents_list[-1] = last_item[:-1]
except GLib.Error:
# 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:
self.status = Ggit.StatusFlags.WORKING_TREE_NEW
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
self.status = Ggit.StatusFlags.CURRENT
else:
self.status = Ggit.StatusFlags.WORKING_TREE_MODIFIED
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: