1# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> 2# Copyright (C) 2009-2013 Kai Willadsen <kai.willadsen@gmail.com> 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 2 of the License, or (at 7# your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, but 10# WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12# General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17import logging 18import sys 19 20from gi.repository import Gio 21from gi.repository import GLib 22from gi.repository import GObject 23from gi.repository import GtkSource 24 25from meld.conf import _ 26from meld.settings import bind_settings, meldsettings 27 28log = logging.getLogger(__name__) 29 30 31class MeldBuffer(GtkSource.Buffer): 32 33 __gtype_name__ = "MeldBuffer" 34 35 __gsettings_bindings__ = ( 36 ('highlight-syntax', 'highlight-syntax'), 37 ) 38 39 def __init__(self): 40 super().__init__() 41 bind_settings(self) 42 self.data = MeldBufferData() 43 self.undo_sequence = None 44 meldsettings.connect('changed', self.on_setting_changed) 45 self.set_style_scheme(meldsettings.style_scheme) 46 47 def on_setting_changed(self, meldsettings, key): 48 if key == 'style-scheme': 49 self.set_style_scheme(meldsettings.style_scheme) 50 51 def do_begin_user_action(self, *args): 52 if self.undo_sequence: 53 self.undo_sequence.begin_group() 54 55 def do_end_user_action(self, *args): 56 if self.undo_sequence: 57 self.undo_sequence.end_group() 58 59 def get_iter_at_line_or_eof(self, line): 60 """Return a Gtk.TextIter at the given line, or the end of the buffer. 61 62 This method is like get_iter_at_line, but if asked for a position past 63 the end of the buffer, this returns the end of the buffer; the 64 get_iter_at_line behaviour is to return the start of the last line in 65 the buffer. 66 """ 67 if line >= self.get_line_count(): 68 return self.get_end_iter() 69 return self.get_iter_at_line(line) 70 71 def insert_at_line(self, line, text): 72 """Insert text at the given line, or the end of the buffer. 73 74 This method is like insert, but if asked to insert something past the 75 last line in the buffer, this will insert at the end, and will add a 76 linebreak before the inserted text. The last line in a Gtk.TextBuffer 77 is guaranteed never to have a newline, so we need to handle this. 78 """ 79 if line >= self.get_line_count(): 80 # TODO: We need to insert a linebreak here, but there is no 81 # way to be certain what kind of linebreak to use. 82 text = "\n" + text 83 it = self.get_iter_at_line_or_eof(line) 84 self.insert(it, text) 85 return it 86 87 88class MeldBufferData(GObject.GObject): 89 90 __gsignals__ = { 91 str('file-changed'): (GObject.SignalFlags.RUN_FIRST, None, ()), 92 } 93 94 encoding = GObject.Property( 95 type=GtkSource.Encoding, 96 nick="The file encoding of the linked GtkSourceFile", 97 default=None, 98 ) 99 100 def __init__(self): 101 super().__init__() 102 self._gfile = None 103 self._label = None 104 self._monitor = None 105 self._sourcefile = None 106 self.reset(gfile=None) 107 108 def reset(self, gfile): 109 same_file = gfile and self._gfile and gfile.equal(self._gfile) 110 self.gfile = gfile 111 if same_file: 112 self.label = self._label 113 else: 114 self.label = gfile.get_parse_name() if gfile else None 115 self.loaded = False 116 self.savefile = None 117 118 def __del__(self): 119 self.disconnect_monitor() 120 121 @property 122 def label(self): 123 # TRANSLATORS: This is the label of a new, currently-unnamed file. 124 return self._label or _("<unnamed>") 125 126 @label.setter 127 def label(self, value): 128 if not value: 129 return 130 if not isinstance(value, str): 131 log.warning('Invalid label ignored "%r"', value) 132 return 133 self._label = value 134 135 def connect_monitor(self): 136 if not self._gfile: 137 return 138 monitor = self._gfile.monitor_file(Gio.FileMonitorFlags.NONE, None) 139 handler_id = monitor.connect('changed', self._handle_file_change) 140 self._monitor = monitor, handler_id 141 142 def disconnect_monitor(self): 143 if not self._monitor: 144 return 145 monitor, handler_id = self._monitor 146 monitor.disconnect(handler_id) 147 monitor.cancel() 148 self._monitor = None 149 150 def _query_mtime(self, gfile): 151 try: 152 time_query = ",".join((Gio.FILE_ATTRIBUTE_TIME_MODIFIED, 153 Gio.FILE_ATTRIBUTE_TIME_MODIFIED_USEC)) 154 info = gfile.query_info(time_query, 0, None) 155 except GLib.GError: 156 return None 157 mtime = info.get_modification_time() 158 return (mtime.tv_sec, mtime.tv_usec) 159 160 def _handle_file_change(self, monitor, f, other_file, event_type): 161 mtime = self._query_mtime(f) 162 if self._disk_mtime and mtime and mtime > self._disk_mtime: 163 self.emit('file-changed') 164 self._disk_mtime = mtime or self._disk_mtime 165 166 @property 167 def gfile(self): 168 return self._gfile 169 170 @gfile.setter 171 def gfile(self, value): 172 self.disconnect_monitor() 173 self._gfile = value 174 self._sourcefile = GtkSource.File() 175 self._sourcefile.set_location(value) 176 self._sourcefile.bind_property( 177 'encoding', self, 'encoding', GObject.BindingFlags.DEFAULT) 178 179 self.update_mtime() 180 self.connect_monitor() 181 182 @property 183 def sourcefile(self): 184 return self._sourcefile 185 186 @property 187 def gfiletarget(self): 188 return self.savefile or self.gfile 189 190 @property 191 def is_special(self): 192 try: 193 info = self._gfile.query_info( 194 Gio.FILE_ATTRIBUTE_STANDARD_TYPE, 0, None) 195 return info.get_file_type() == Gio.FileType.SPECIAL 196 except (AttributeError, GLib.GError): 197 return False 198 199 @property 200 def writable(self): 201 try: 202 info = self.gfiletarget.query_info( 203 Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE, 0, None) 204 except GLib.GError as err: 205 if err.code == Gio.IOErrorEnum.NOT_FOUND: 206 return True 207 return False 208 except AttributeError: 209 return False 210 return info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE) 211 212 def update_mtime(self): 213 if self._gfile: 214 self._disk_mtime = self._query_mtime(self._gfile) 215 self._mtime = self._disk_mtime 216 217 def current_on_disk(self): 218 return self._mtime == self._disk_mtime 219 220 221class BufferLines: 222 """Gtk.TextBuffer shim with line-based access and optional filtering 223 224 This class allows a Gtk.TextBuffer to be treated as a list of lines of 225 possibly-filtered text. If no filter is given, the raw output from the 226 Gtk.TextBuffer is used. 227 228 The logic here (and in places in FileDiff) requires that Python's 229 unicode splitlines() implementation and Gtk.TextBuffer agree on where 230 linebreaks occur. Happily, this is usually the case. 231 """ 232 233 def __init__(self, buf, textfilter=None): 234 self.buf = buf 235 if textfilter is not None: 236 self.textfilter = textfilter 237 else: 238 self.textfilter = lambda x, buf, start_iter, end_iter: x 239 240 def __getitem__(self, key): 241 if isinstance(key, slice): 242 lo, hi, _ = key.indices(self.buf.get_line_count()) 243 244 # FIXME: If we ask for arbitrary slices past the end of the buffer, 245 # this will return the last line. 246 start = self.buf.get_iter_at_line_or_eof(lo) 247 end = self.buf.get_iter_at_line_or_eof(hi) 248 txt = self.buf.get_text(start, end, False) 249 250 filter_txt = self.textfilter(txt, self.buf, start, end) 251 lines = filter_txt.splitlines() 252 ends = filter_txt.splitlines(True) 253 254 # The last line in a Gtk.TextBuffer is guaranteed never to end in a 255 # newline. As splitlines() discards an empty line at the end, we 256 # need to artificially add a line if the requested slice is past 257 # the end of the buffer, and the last line in the slice ended in a 258 # newline. 259 if hi >= self.buf.get_line_count() and \ 260 lo < self.buf.get_line_count() and \ 261 (len(lines) == 0 or len(lines[-1]) != len(ends[-1])): 262 lines.append("") 263 ends.append("") 264 265 hi = self.buf.get_line_count() if hi == sys.maxsize else hi 266 if hi - lo != len(lines): 267 # These codepoints are considered line breaks by Python, but 268 # not by GtkTextStore. 269 additional_breaks = set(('\x0b', '\x0c', '\x85', '\u2028')) 270 i = 0 271 while i < len(ends): 272 line, end = lines[i], ends[i] 273 # It's possible that the last line in a file would end in a 274 # line break character, which requires no joining. 275 if end and end[-1] in additional_breaks and \ 276 (not line or line[-1] not in additional_breaks): 277 assert len(ends) >= i + 1 278 lines[i:i + 2] = [line + end[-1] + lines[i + 1]] 279 ends[i:i + 2] = [end + ends[i + 1]] 280 else: 281 # We only increment if we don't correct a line, to 282 # handle the case of a single line having multiple 283 # additional_breaks characters that need correcting. 284 i += 1 285 286 return lines 287 288 elif isinstance(key, int): 289 if key >= len(self): 290 raise IndexError 291 line_start = self.buf.get_iter_at_line_or_eof(key) 292 line_end = line_start.copy() 293 if not line_end.ends_line(): 294 line_end.forward_to_line_end() 295 txt = self.buf.get_text(line_start, line_end, False) 296 return self.textfilter(txt, self.buf, line_start, line_end) 297 298 def __len__(self): 299 return self.buf.get_line_count() 300 301 302class BufferAction: 303 """A helper to undo/redo text insertion/deletion into/from a text buffer""" 304 305 def __init__(self, buf, offset, text): 306 self.buffer = buf 307 self.offset = offset 308 self.text = text 309 310 def delete(self): 311 start = self.buffer.get_iter_at_offset(self.offset) 312 end = self.buffer.get_iter_at_offset(self.offset + len(self.text)) 313 self.buffer.delete(start, end) 314 self.buffer.place_cursor(end) 315 return [self] 316 317 def insert(self): 318 start = self.buffer.get_iter_at_offset(self.offset) 319 self.buffer.place_cursor(start) 320 self.buffer.insert(start, self.text) 321 return [self] 322 323 324class BufferInsertionAction(BufferAction): 325 undo = BufferAction.delete 326 redo = BufferAction.insert 327 328 329class BufferDeletionAction(BufferAction): 330 undo = BufferAction.insert 331 redo = BufferAction.delete 332