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