1# -*- coding: utf-8 -*- 2 3# Copyright (C) 2013 - Ignacio Casal Quinteiro 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 18# 02110-1301 USA. 19 20from gi.repository import GLib, GObject, Gtk, Gedit, Ggit 21 22from .appactivatable import GitAppActivatable 23from .diffrenderer import DiffType, DiffRenderer 24from .windowactivatable import GitWindowActivatable 25 26import sys 27import os.path 28import difflib 29 30 31class LineContext: 32 __slots__ = ('removed_lines', 'line_type') 33 34 def __init__(self): 35 self.removed_lines = [] 36 self.line_type = DiffType.NONE 37 38 39class GitViewActivatable(GObject.Object, Gedit.ViewActivatable): 40 view = GObject.Property(type=Gedit.View) 41 42 status = GObject.Property(type=Ggit.StatusFlags, 43 default=Ggit.StatusFlags.CURRENT) 44 45 def __init__(self): 46 super().__init__() 47 48 self.diff_timeout = 0 49 self.file_contents_list = None 50 self.file_context = None 51 52 def do_activate(self): 53 GitWindowActivatable.register_view_activatable(self) 54 55 self.app_activatable = GitAppActivatable.get_instance() 56 57 self.diff_renderer = DiffRenderer() 58 self.gutter = self.view.get_gutter(Gtk.TextWindowType.LEFT) 59 60 # Note: GitWindowActivatable will call 61 # update_location() for us when needed 62 self.view_signals = [ 63 self.view.connect('notify::buffer', self.on_notify_buffer) 64 ] 65 66 self.buffer = None 67 self.on_notify_buffer(self.view) 68 69 def do_deactivate(self): 70 if self.diff_timeout != 0: 71 GLib.source_remove(self.diff_timeout) 72 73 self.disconnect_buffer() 74 self.buffer = None 75 76 self.disconnect_view() 77 self.gutter.remove(self.diff_renderer) 78 79 def disconnect(self, obj, signals): 80 for sid in signals: 81 obj.disconnect(sid) 82 83 signals[:] = [] 84 85 def disconnect_buffer(self): 86 self.disconnect(self.buffer, self.buffer_signals) 87 88 def disconnect_view(self): 89 self.disconnect(self.view, self.view_signals) 90 91 def on_notify_buffer(self, view, gspec=None): 92 if self.diff_timeout != 0: 93 GLib.source_remove(self.diff_timeout) 94 95 if self.buffer: 96 self.disconnect_buffer() 97 98 self.buffer = view.get_buffer() 99 100 # The changed signal is connected to in update_location(). 101 # The saved signal is pointless as the window activatable 102 # will see the change and call update_location(). 103 self.buffer_signals = [ 104 self.buffer.connect('loaded', self.update_location) 105 ] 106 107 # We wait and let the loaded signal call 108 # update_location() as the buffer is currently empty 109 110 # TODO: This can be called many times and by idles, 111 # should instead do the work in another thread 112 def update_location(self, *args): 113 self.location = self.buffer.get_file().get_location() 114 115 if self.location is not None: 116 repo = self.app_activatable.get_repository(self.location, False) 117 118 if self.location is None or repo is None: 119 if self.file_contents_list is not None: 120 self.file_contents_list = None 121 self.gutter.remove(self.diff_renderer) 122 self.diff_renderer.set_file_context({}) 123 self.buffer.disconnect(self.buffer_signals.pop()) 124 125 return 126 127 if self.file_contents_list is None: 128 self.gutter.insert(self.diff_renderer, 40) 129 self.buffer_signals.append(self.buffer.connect('changed', 130 self.update)) 131 132 try: 133 head = repo.get_head() 134 commit = repo.lookup(head.get_target(), Ggit.Commit) 135 tree = commit.get_tree() 136 relative_path = os.path.relpath( 137 os.path.realpath(self.location.get_path()), 138 repo.get_workdir().get_path() 139 ) 140 141 entry = tree.get_by_path(relative_path) 142 file_blob = repo.lookup(entry.get_id(), Ggit.Blob) 143 try: 144 gitconfig = repo.get_config() 145 encoding = gitconfig.get_string('gui.encoding') 146 except GLib.Error: 147 encoding = 'utf8' 148 file_contents = file_blob.get_raw_content().decode(encoding) 149 self.file_contents_list = file_contents.splitlines() 150 151 # Remove the last empty line added by gedit automatically 152 if self.file_contents_list: 153 last_item = self.file_contents_list[-1] 154 if last_item[-1:] == '\n': 155 self.file_contents_list[-1] = last_item[:-1] 156 157 except GLib.Error: 158 # New file in a git repository 159 self.file_contents_list = [] 160 161 self.update() 162 163 def update(self, *unused): 164 # We don't let the delay accumulate 165 if self.diff_timeout != 0: 166 return 167 168 # Do the initial diff without a delay 169 if self.file_context is None: 170 self.on_diff_timeout() 171 172 else: 173 n_lines = self.buffer.get_line_count() 174 delay = min(10000, 200 * (n_lines // 2000 + 1)) 175 176 self.diff_timeout = GLib.timeout_add(delay, 177 self.on_diff_timeout) 178 179 def on_diff_timeout(self): 180 self.diff_timeout = 0 181 182 # Must be a new file 183 if not self.file_contents_list: 184 self.status = Ggit.StatusFlags.WORKING_TREE_NEW 185 186 n_lines = self.buffer.get_line_count() 187 if len(self.diff_renderer.file_context) == n_lines: 188 return False 189 190 line_context = LineContext() 191 line_context.line_type = DiffType.ADDED 192 file_context = dict(zip(range(1, n_lines + 1), 193 [line_context] * n_lines)) 194 195 self.diff_renderer.set_file_context(file_context) 196 return False 197 198 start_iter, end_iter = self.buffer.get_bounds() 199 src_contents = start_iter.get_visible_text(end_iter) 200 src_contents_list = src_contents.splitlines() 201 202 # GtkTextBuffer does not consider a trailing "\n" to be text 203 if len(src_contents_list) != self.buffer.get_line_count(): 204 src_contents_list.append('') 205 206 diff = difflib.unified_diff(self.file_contents_list, 207 src_contents_list, n=0) 208 209 # Skip the first 2 lines: ---, +++ 210 try: 211 next(diff) 212 next(diff) 213 214 except StopIteration: 215 # Nothing has changed 216 self.status = Ggit.StatusFlags.CURRENT 217 218 else: 219 self.status = Ggit.StatusFlags.WORKING_TREE_MODIFIED 220 221 file_context = {} 222 for line_data in diff: 223 if line_data[0] == '@': 224 for token in line_data.split(): 225 if token[0] == '+': 226 hunk_point = int(token.split(',', 1)[0]) 227 line_context = LineContext() 228 break 229 230 elif line_data[0] == '-': 231 if line_context.line_type == DiffType.NONE: 232 line_context.line_type = DiffType.REMOVED 233 234 line_context.removed_lines.append(line_data[1:]) 235 236 # No hunk point increase 237 file_context[hunk_point] = line_context 238 239 elif line_data[0] == '+': 240 if line_context.line_type == DiffType.NONE: 241 line_context.line_type = DiffType.ADDED 242 file_context[hunk_point] = line_context 243 244 elif line_context.line_type == DiffType.REMOVED: 245 # Why is this the only one that does 246 # not add it to file_context? 247 248 line_context.line_type = DiffType.MODIFIED 249 250 else: 251 file_context[hunk_point] = line_context 252 253 hunk_point += 1 254 255 # Occurs when all of the original content is deleted 256 if 0 in file_context: 257 for i in reversed(list(file_context.keys())): 258 file_context[i + 1] = file_context[i] 259 del file_context[i] 260 261 self.file_context = file_context 262 self.diff_renderer.set_file_context(file_context) 263 return False 264 265# ex:ts=4:et: 266