File: buffer_proxy.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 (225 lines) | stat: -rw-r--r-- 7,482 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
# coding=utf8

import vim
from UltiSnips import vim_helper
from UltiSnips.compatibility import as_unicode, as_vimencoding
from UltiSnips.position import Position
from UltiSnips.diff import diff

from contextlib import contextmanager


@contextmanager
def use_proxy_buffer(snippets_stack, vstate):
    """
    Forward all changes made in the buffer to the current snippet stack while
    function call.
    """
    buffer_proxy = VimBufferProxy(snippets_stack, vstate)
    old_buffer = vim_helper.buf
    try:
        vim_helper.buf = buffer_proxy
        yield
    finally:
        vim_helper.buf = old_buffer
    buffer_proxy.validate_buffer()


@contextmanager
def suspend_proxy_edits():
    """
    Prevents changes being applied to the snippet stack while function call.
    """
    if not isinstance(vim_helper.buf, VimBufferProxy):
        yield
    else:
        try:
            vim_helper.buf._disable_edits()
            yield
        finally:
            vim_helper.buf._enable_edits()


class VimBufferProxy(vim_helper.VimBuffer):
    """
    Proxy object used for tracking changes that made from snippet actions.

    Unfortunately, vim by itself lacks of the API for changing text in
    trackable maner.

    Vim marks offers limited functionality for tracking line additions and
    deletions, but nothing offered for tracking changes withing single line.

    Instance of this class is passed to all snippet actions and behaves as
    internal vim.current.window.buffer.

    All changes that are made by user passed to diff algorithm, and resulting
    diff applied to internal snippet structures to ensure they are in sync with
    actual buffer contents.
    """

    def __init__(self, snippets_stack, vstate):
        """
        Instantiate new object.

        snippets_stack is a slice of currently active snippets.
        """
        self._snippets_stack = snippets_stack
        self._buffer = vim.current.buffer
        self._change_tick = int(vim.eval("b:changedtick"))
        self._forward_edits = True
        self._vstate = vstate

    def is_buffer_changed_outside(self):
        """
        Returns true, if buffer was changed without using proxy object, like
        with vim.command() or through internal vim.current.window.buffer.
        """
        return self._change_tick < int(vim.eval("b:changedtick"))

    def validate_buffer(self):
        """
        Raises exception if buffer is changes beyound proxy object.
        """
        if self.is_buffer_changed_outside():
            raise RuntimeError(
                "buffer was modified using vim.command or "
                + "vim.current.buffer; that changes are untrackable and leads to "
                + "errors in snippet expansion; use special variable `snip.buffer` "
                "for buffer modifications.\n\n"
                + "See :help UltiSnips-buffer-proxy for more info."
            )

    def __setitem__(self, key, value):
        """
        Behaves as vim.current.window.buffer.__setitem__ except it tracks
        changes and applies them to the current snippet stack.
        """
        if isinstance(key, slice):
            value = [as_vimencoding(line) for line in value]
            changes = list(self._get_diff(key.start, key.stop, value))
            self._buffer[key.start : key.stop] = [line.strip("\n") for line in value]
        else:
            value = as_vimencoding(value)
            changes = list(self._get_line_diff(key, self._buffer[key], value))
            self._buffer[key] = value

        self._change_tick += 1

        if self._forward_edits:
            for change in changes:
                self._apply_change(change)
            if self._snippets_stack:
                self._vstate.remember_buffer(self._snippets_stack[0])

    def __setslice__(self, i, j, text):
        """
        Same as __setitem__.
        """
        self.__setitem__(slice(i, j), text)

    def __getitem__(self, key):
        """
        Just passing call to the vim.current.window.buffer.__getitem__.
        """
        if isinstance(key, slice):
            return [as_unicode(l) for l in self._buffer[key.start : key.stop]]
        else:
            return as_unicode(self._buffer[key])

    def __getslice__(self, i, j):
        """
        Same as __getitem__.
        """
        return self.__getitem__(slice(i, j))

    def __len__(self):
        """
        Same as len(vim.current.window.buffer).
        """
        return len(self._buffer)

    def append(self, line, line_number=-1):
        """
        Same as vim.current.window.buffer.append(), but with tracking changes.
        """
        if line_number < 0:
            line_number = len(self)
        if not isinstance(line, list):
            line = [line]
        self[line_number:line_number] = [as_vimencoding(l) for l in line]

    def __delitem__(self, key):
        if isinstance(key, slice):
            self.__setitem__(key, [])
        else:
            self.__setitem__(slice(key, key + 1), [])

    def _get_diff(self, start, end, new_value):
        """
        Very fast diffing algorithm when changes are across many lines.
        """
        for line_number in range(start, end):
            if line_number < 0:
                line_number = len(self._buffer) + line_number
            yield ("D", line_number, 0, self._buffer[line_number], True)

        if start < 0:
            start = len(self._buffer) + start
        for line_number in range(0, len(new_value)):
            yield ("I", start + line_number, 0, new_value[line_number], True)

    def _get_line_diff(self, line_number, before, after):
        """
        Use precise diffing for tracking changes in single line.
        """
        if before == "":
            for change in self._get_diff(line_number, line_number + 1, [after]):
                yield change
        else:
            for change in diff(before, after):
                yield (change[0], line_number, change[2], change[3])

    def _apply_change(self, change):
        """
        Apply changeset to current snippets stack, correctly moving around
        snippet itself or its child.
        """
        if not self._snippets_stack:
            return

        change_type, line_number, column_number, change_text = change[0:4]

        line_before = line_number <= self._snippets_stack[0]._start.line
        column_before = column_number <= self._snippets_stack[0]._start.col
        if line_before and column_before:
            direction = 1
            if change_type == "D":
                direction = -1

            diff = Position(direction, 0)
            if len(change) != 5:
                diff = Position(0, direction * len(change_text))
            print(change, diff)

            self._snippets_stack[0]._move(Position(line_number, column_number), diff)
        else:
            if line_number > self._snippets_stack[0]._end.line:
                return
            if column_number >= self._snippets_stack[0]._end.col:
                return
            self._snippets_stack[0]._do_edit(change[0:4])

    def _disable_edits(self):
        """
        Temporary disable applying changes to snippets stack. Should be done
        while expanding anonymous snippet in the middle of jump to prevent
        double tracking.
        """
        self._forward_edits = False

    def _enable_edits(self):
        """
        Enables changes forwarding back.
        """
        self._forward_edits = True