File: base.py

package info (click to toggle)
vim-ultisnips 3.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,924 kB
  • sloc: python: 8,353; sh: 64; makefile: 38
file content (405 lines) | stat: -rw-r--r-- 13,265 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/usr/bin/env python
# encoding: utf-8

"""Base classes for all text objects."""

from UltiSnips import vim_helper
from UltiSnips.position import Position


def _calc_end(text, start):
    """Calculate the end position of the 'text' starting at 'start."""
    if len(text) == 1:
        new_end = start + Position(0, len(text[0]))
    else:
        new_end = Position(start.line + len(text) - 1, len(text[-1]))
    return new_end


def _replace_text(buf, start, end, text):
    """Copy the given text to the current buffer, overwriting the span 'start'
    to 'end'."""
    lines = text.split("\n")

    new_end = _calc_end(lines, start)

    before = buf[start.line][: start.col]
    after = buf[end.line][end.col :]

    new_lines = []
    if len(lines):
        new_lines.append(before + lines[0])
        new_lines.extend(lines[1:])
        new_lines[-1] += after
    buf[start.line : end.line + 1] = new_lines

    return new_end


# These classes use their subclasses a lot and we really do not want to expose
# their functions more globally.
# pylint: disable=protected-access


class TextObject(object):

    """Represents any object in the text that has a span in any ways."""

    def __init__(
        self, parent, token_or_start, end=None, initial_text="", tiebreaker=None
    ):
        self._parent = parent

        if end is not None:  # Took 4 arguments
            self._start = token_or_start
            self._end = end
            self._initial_text = initial_text
        else:  # Initialize from token
            self._start = token_or_start.start
            self._end = token_or_start.end
            self._initial_text = token_or_start.initial_text
        self._tiebreaker = tiebreaker or Position(self._start.line, self._end.line)
        if parent is not None:
            parent._add_child(self)

    def _move(self, pivot, diff):
        """Move this object by 'diff' while 'pivot' is the point of change."""
        self._start.move(pivot, diff)
        self._end.move(pivot, diff)

    def __lt__(self, other):
        me_tuple = (
            self.start.line,
            self.start.col,
            self._tiebreaker.line,
            self._tiebreaker.col,
        )
        other_tuple = (
            other._start.line,
            other._start.col,
            other._tiebreaker.line,
            other._tiebreaker.col,
        )
        return me_tuple < other_tuple

    def __le__(self, other):
        me_tuple = (
            self._start.line,
            self._start.col,
            self._tiebreaker.line,
            self._tiebreaker.col,
        )
        other_tuple = (
            other._start.line,
            other._start.col,
            other._tiebreaker.line,
            other._tiebreaker.col,
        )
        return me_tuple <= other_tuple

    def __repr__(self):
        ct = ""
        try:
            ct = self.current_text
        except IndexError:
            ct = "<err>"

        return "%s(%r->%r,%r)" % (self.__class__.__name__, self._start, self._end, ct)

    @property
    def current_text(self):
        """The current text of this object."""
        if self._start.line == self._end.line:
            return vim_helper.buf[self._start.line][self._start.col : self._end.col]
        else:
            lines = [vim_helper.buf[self._start.line][self._start.col :]]
            lines.extend(vim_helper.buf[self._start.line + 1 : self._end.line])
            lines.append(vim_helper.buf[self._end.line][: self._end.col])
            return "\n".join(lines)

    @property
    def start(self):
        """The start position."""
        return self._start

    @property
    def end(self):
        """The end position."""
        return self._end

    def overwrite_with_initial_text(self, buf):
        self.overwrite(buf, self._initial_text)

    def overwrite(self, buf, gtext):
        """Overwrite the text of this object in the Vim Buffer and update its
        length information.

        If 'gtext' is None use the initial text of this object.

        """
        # We explicitly do not want to move our children around here as we
        # either have non or we are replacing text initially which means we do
        # not want to mess with their positions
        if self.current_text == gtext:
            return
        old_end = self._end
        self._end = _replace_text(buf, self._start, self._end, gtext)
        if self._parent:
            self._parent._child_has_moved(
                self._parent._children.index(self),
                min(old_end, self._end),
                self._end.delta(old_end),
            )

    def _update(self, done, buf):
        """Update this object inside 'buf' which is a list of lines.

        Return False if you need to be called again for this edit cycle.
        Otherwise return True.

        """
        raise NotImplementedError("Must be implemented by subclasses.")


class EditableTextObject(TextObject):

    """This base class represents any object in the text that can be changed by
    the user."""

    def __init__(self, *args, **kwargs):
        TextObject.__init__(self, *args, **kwargs)
        self._children = []
        self._tabstops = {}

    ##############
    # Properties #
    ##############
    @property
    def children(self):
        """List of all children."""
        return self._children

    @property
    def _editable_children(self):
        """List of all children that are EditableTextObjects."""
        return [
            child for child in self._children if isinstance(child, EditableTextObject)
        ]

    ####################
    # Public Functions #
    ####################
    def find_parent_for_new_to(self, pos):
        """Figure out the parent object for something at 'pos'."""
        for children in self._editable_children:
            if children._start <= pos < children._end:
                return children.find_parent_for_new_to(pos)
            if children._start == pos and pos == children._end:
                return children.find_parent_for_new_to(pos)
        return self

    ###############################
    # Private/Protected functions #
    ###############################
    def _do_edit(self, cmd, ctab=None):
        """Apply the edit 'cmd' to this object."""
        ctype, line, col, text = cmd
        assert ("\n" not in text) or (text == "\n")
        pos = Position(line, col)

        to_kill = set()
        new_cmds = []
        for child in self._children:
            if ctype == "I":  # Insertion
                if child._start < pos < Position(
                    child._end.line, child._end.col
                ) and isinstance(child, NoneditableTextObject):
                    to_kill.add(child)
                    new_cmds.append(cmd)
                    break
                elif (child._start <= pos <= child._end) and isinstance(
                    child, EditableTextObject
                ):
                    if pos == child.end and not child.children:
                        try:
                            if ctab.number != child.number:
                                continue
                        except AttributeError:
                            pass
                    child._do_edit(cmd, ctab)
                    return
            else:  # Deletion
                delend = (
                    pos + Position(0, len(text))
                    if text != "\n"
                    else Position(line + 1, 0)
                )
                if (child._start <= pos < child._end) and (
                    child._start < delend <= child._end
                ):
                    # this edit command is completely for the child
                    if isinstance(child, NoneditableTextObject):
                        to_kill.add(child)
                        new_cmds.append(cmd)
                        break
                    else:
                        child._do_edit(cmd, ctab)
                        return
                elif (
                    pos < child._start and child._end <= delend and child.start < delend
                ) or (pos <= child._start and child._end < delend):
                    # Case: this deletion removes the child
                    to_kill.add(child)
                    new_cmds.append(cmd)
                    break
                elif pos < child._start and (child._start < delend <= child._end):
                    # Case: partially for us, partially for the child
                    my_text = text[: (child._start - pos).col]
                    c_text = text[(child._start - pos).col :]
                    new_cmds.append((ctype, line, col, my_text))
                    new_cmds.append((ctype, line, col, c_text))
                    break
                elif delend >= child._end and (child._start <= pos < child._end):
                    # Case: partially for us, partially for the child
                    c_text = text[(child._end - pos).col :]
                    my_text = text[: (child._end - pos).col]
                    new_cmds.append((ctype, line, col, c_text))
                    new_cmds.append((ctype, line, col, my_text))
                    break

        for child in to_kill:
            self._del_child(child)
        if len(new_cmds):
            for child in new_cmds:
                self._do_edit(child)
            return

        # We have to handle this ourselves
        delta = Position(1, 0) if text == "\n" else Position(0, len(text))
        if ctype == "D":
            # Makes no sense to delete in empty textobject
            if self._start == self._end:
                return
            delta.line *= -1
            delta.col *= -1
        pivot = Position(line, col)
        idx = -1
        for cidx, child in enumerate(self._children):
            if child._start < pivot <= child._end:
                idx = cidx
        self._child_has_moved(idx, pivot, delta)

    def _move(self, pivot, diff):
        TextObject._move(self, pivot, diff)

        for child in self._children:
            child._move(pivot, diff)

    def _child_has_moved(self, idx, pivot, diff):
        """Called when a the child with 'idx' has moved behind 'pivot' by
        'diff'."""
        self._end.move(pivot, diff)

        for child in self._children[idx + 1 :]:
            child._move(pivot, diff)

        if self._parent:
            self._parent._child_has_moved(
                self._parent._children.index(self), pivot, diff
            )

    def _get_next_tab(self, number):
        """Returns the next tabstop after 'number'."""
        if not len(self._tabstops.keys()):
            return
        tno_max = max(self._tabstops.keys())

        possible_sol = []
        i = number + 1
        while i <= tno_max:
            if i in self._tabstops:
                possible_sol.append((i, self._tabstops[i]))
                break
            i += 1

        child = [c._get_next_tab(number) for c in self._editable_children]
        child = [c for c in child if c]

        possible_sol += child

        if not len(possible_sol):
            return None

        return min(possible_sol)

    def _get_prev_tab(self, number):
        """Returns the previous tabstop before 'number'."""
        if not len(self._tabstops.keys()):
            return
        tno_min = min(self._tabstops.keys())

        possible_sol = []
        i = number - 1
        while i >= tno_min and i > 0:
            if i in self._tabstops:
                possible_sol.append((i, self._tabstops[i]))
                break
            i -= 1

        child = [c._get_prev_tab(number) for c in self._editable_children]
        child = [c for c in child if c]

        possible_sol += child

        if not len(possible_sol):
            return None

        return max(possible_sol)

    def _get_tabstop(self, requester, number):
        """Returns the tabstop 'number'.

        'requester' is the class that is interested in this.

        """
        if number in self._tabstops:
            return self._tabstops[number]
        for child in self._editable_children:
            if child is requester:
                continue
            rv = child._get_tabstop(self, number)
            if rv is not None:
                return rv
        if self._parent and requester is not self._parent:
            return self._parent._get_tabstop(self, number)

    def _update(self, done, buf):
        if all((child in done) for child in self._children):
            assert self not in done
            done.add(self)
        return True

    def _add_child(self, child):
        """Add 'child' as a new child of this text object."""
        self._children.append(child)
        self._children.sort()

    def _del_child(self, child):
        """Delete this 'child'."""
        child._parent = None
        self._children.remove(child)

        # If this is a tabstop, delete it. Might have been deleted already if
        # it was nested.
        try:
            del self._tabstops[child.number]
        except (AttributeError, KeyError):
            pass


class NoneditableTextObject(TextObject):

    """All passive text objects that the user can't edit by hand."""

    def _update(self, done, buf):
        return True