Completed
Pull Request — master (#269)
by
unknown
21:01
created

Region.__setitem__()   A

Complexity

Conditions 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 15
ccs 0
cts 0
cp 0
crap 20
rs 9.2
1
"""API for working with a Nvim Buffer."""
2 5
from .common import Remote
3 5
from ..compat import IS_PYTHON3
4
5
from collections import Iterable
6 5
import itertools
7
8
9 5
__all__ = ('Buffer')
10 3
11
12
if IS_PYTHON3:
13 5
    basestring = str
14
15 5
16 5
def adjust_index(idx, default=None):
17 5
    """Convert from python indexing convention to nvim indexing convention."""
18 5
    if idx is None:
19
        return default
20 5
    elif idx < 0:
21
        return idx - 1
22
    else:
23 5
        return idx
24
25
26
class Buffer(Remote):
27 5
28
    """A remote Nvim buffer."""
29 5
30
    _api_prefix = "nvim_buf_"
31 5
32
    def __len__(self):
33 5
        """Return the number of lines contained in a Buffer."""
34
        return self.request('nvim_buf_line_count')
35
36
    def __getitem__(self, idx):
37
        """Get a buffer line or slice by integer index.
38
39
        Indexes may be negative to specify positions from the end of the
40
        buffer. For example, -1 is the last line, -2 is the line before that
41
        and so on.
42
43 5
        When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring
44 5
        the whole buffer.
45 5
        """
46 5
        if not isinstance(idx, slice):
47 5
            i = adjust_index(idx)
48 5
            return self.request('nvim_buf_get_lines', i, i + 1, True)[0]
49
        start = adjust_index(idx.start, 0)
50 5
        end = adjust_index(idx.stop, -1)
51
        return self.request('nvim_buf_get_lines', start, end, False)
52
53
    def __setitem__(self, idx, item):
54
        """Replace a buffer line or slice by integer index.
55
56
        Like with `__getitem__`, indexes may be negative.
57
58 5
        When replacing slices, omiting indexes(eg: `buffer[:]`) will replace
59 5
        the whole buffer.
60 5
        """
61 5
        if not isinstance(idx, slice):
62 5
            i = adjust_index(idx)
63 5
            lines = [item] if item is not None else []
64 5
            return self.request('nvim_buf_set_lines', i, i + 1, True, lines)
65 5
        lines = item if item is not None else []
66
        start = adjust_index(idx.start, 0)
67 5
        end = adjust_index(idx.stop, -1)
68
        return self.request('nvim_buf_set_lines', start, end, False, lines)
69
70
    def __iter__(self):
71
        """Iterate lines of a buffer.
72
73
        This will retrieve all lines locally before iteration starts. This
74
        approach is used because for most cases, the gain is much greater by
75
        minimizing the number of API calls by transfering all data needed to
76
        work.
77
        """
78
        lines = self[:]
79 5
        for line in lines:
80
            yield line
81
82
    def __delitem__(self, idx):
83
        """Delete line or slice of lines from the buffer.
84 5
85
        This is the same as __setitem__(idx, [])
86 5
        """
87
        self.__setitem__(idx, None)
88 5
89 5
    def append(self, lines, index=-1):
90 5
        """Append a string or list of lines to the buffer."""
91
        if isinstance(lines, (basestring, bytes)):
92 5
            lines = [lines]
93
        return self.request('nvim_buf_set_lines', index, index, True, lines)
94 5
95
    def mark(self, name):
96 5
        """Return (row, col) tuple for a named mark."""
97
        return self.request('nvim_buf_get_mark', name)
98
99
    def range(self, start, end):
100 5
        """Return a `Range` object, which represents part of the Buffer."""
101
        return Range(self, start, end)
102
103
    def add_highlight(self, hl_group, line, col_start=0,
104
                      col_end=-1, src_id=-1, async=None):
105
        """Add a highlight to the buffer."""
106
        if async is None:
107
            async = (src_id != 0)
108 5
        return self.request('nvim_buf_add_highlight', src_id, hl_group,
109
                            line, col_start, col_end, async=async)
110
111
    def clear_highlight(self, src_id, line_start=0, line_end=-1, async=True):
112
        """Clear highlights from the buffer."""
113 5
        self.request('nvim_buf_clear_highlight', src_id,
114
                     line_start, line_end, async=async)
115
116 5
    @property
117
    def name(self):
118 5
        """Get the buffer name."""
119
        return self.request('nvim_buf_get_name')
120
121 5
    @name.setter
122
    def name(self, value):
123 5
        """Set the buffer name. BufFilePre/BufFilePost are triggered."""
124
        return self.request('nvim_buf_set_name', value)
125
126 5
    @property
127
    def valid(self):
128 5
        """Return True if the buffer still exists."""
129
        return self.request('nvim_buf_is_valid')
130
131 5
    @property
132
    def number(self):
133
        """Get the buffer number."""
134 5
        return self.handle
135 5
136
    @property
137
    def visual_selection(self):
138
        """Get the current visual selection as a Region object."""
139
140 5
        startmark = self.mark('<')
141
        endmark = self.mark('>')
142
143 5
        rowstart, colstart = self.mark('<')
144
        rowend, colend     = self.mark('>')
145
146
        if rowend - rowstart == 0:
147
            # same line, one liners are easy
148
            return Region(self, rowstart, rowend, partials=(colstart, colend))
149
150
        vmode = self.nvim.funcs.visualmode()
0 ignored issues
show
Bug introduced by
The Instance of Buffer does not seem to have a member named nvim.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
151
152
        if vmode == "v":
153
            # standard visual mode
154 5
            line_count = rowend - rowstart + 1
155
            full_lines_count = line_count - 2  # all except the first and last line
156
            partiallist = [(colstart, None)] + full_lines_count*[(None, None)] + [(None, colend)]
157
            return Region(self, rowstart, rowend, partials=partiallist)
158
        elif vmode == "":
159
            # visual block mode
160
            return Region(self, rowstart, rowend, partials=(colstart, colend))
161
        elif vmode == "V":
162
            # visual line mode
163
            return Region(self, rowstart, rowend, partials=(None, None))
164
165
        return Region(self, startmark, endmark)
166 5
167
168
class Range(object):
169
    def __init__(self, buffer, start, end):
170 5
        self._buffer = buffer
171
        self.start = start - 1
172
        self.end = end - 1
173
174
    def __len__(self):
175
        return self.end - self.start + 1
176 5
177
    def __getitem__(self, idx):
178
        if not isinstance(idx, slice):
179
            return self._buffer[self._normalize_index(idx)]
180
        start = self._normalize_index(idx.start)
181
        end = self._normalize_index(idx.stop)
182
        if start is None:
183
            start = self.start
184
        if end is None:
185
            end = self.end + 1
186
        return self._buffer[start:end]
187
188
    def __setitem__(self, idx, lines):
189
        if not isinstance(idx, slice):
190
            self._buffer[self._normalize_index(idx)] = lines
191
            return
192
        start = self._normalize_index(idx.start)
193
        end = self._normalize_index(idx.stop)
194
        if start is None:
195
            start = self.start
196
        if end is None:
197
            end = self.end
198
        self._buffer[start:end + 1] = lines
199
200
    def __iter__(self):
201
        for i in range(self.start, self.end + 1):
202
            yield self._buffer[i]
203
204
    def append(self, lines, i=None):
205
        i = self._normalize_index(i)
206
        if i is None:
207
            i = self.end + 1
208
        self._buffer.append(lines, i)
209
210
    def _normalize_index(self, index):
211
        if index is None:
212
            return None
213
        if index < 0:
214
            index = self.end
215
        else:
216
            index += self.start
217
            if index > self.end:
218
                index = self.end
219
        return index
220
221
222
223
class Region(object):
224
    def __init__(self, buffer, startrow, endrow, partials=(None, None)):
225
        if not startrow <= endrow:
226
            raise ValueError("Negative span of rows provided.")
227
228
        # the range of the buffer this Region covers
229
        self._range = buffer.range(startrow, endrow)
230
231
        if not isinstance(partials[0], Iterable):
232
            # in this case we assume the provided partial
233
            # to be (int, int) and the default for all lines,
234
            # so create a list of appropriate length
235
            partials = [partials]*len(self._range)
236
        else:
237
            # we only need to assert this if we haven't created
238
            # that list ourselves
239
            if len(self._range) != len(partials):
240
                raise ValueError("length mismatch between partials and provided range")
241
242
        self._partials = [slice(*p) for p in partials]
243
        return
244
245
    def __len__(self):
246
        """
247
        Returns the number of characters covered by the Region
248
        """
249
        return sum([len(line[part]) for line, part in zip(self._range, self._partials)])
250
251
    def __getitem__(self, idx):
252
        if not isinstance(idx, slice):
253
            i = adjust_index(idx)
254
            return self.request('nvim_buf_get_lines', i, i + 1, True)[0]
0 ignored issues
show
Bug introduced by
The Instance of Region does not seem to have a member named request.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
255
        start = adjust_index(idx.start, 0)
256
        end = adjust_index(idx.stop, -1)
257
        #for i, (a, b, c) in enumerate(itertools.islice(zip(l1, l2, l3), 3, 7)):
258
        if not isinstance(idx, slice):
259
            return self._range[idx][self.partials[idx+1]]
0 ignored issues
show
Bug introduced by
The Instance of Region does not seem to have a member named partials.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
260
261
        start = idx.start or 0
262
        end = idx.stop or len(self._range)
263
264
        lineiter = itertools.islice(zip(self._range, self._partials), start, end)
265
        return [line[partial] for line, partial in lineiter]
266
267
    def __setitem__(self, idx, lineparts):
268
        if not isinstance(idx, slice):
269
            new_line = self._assemble_line(idx, lineparts[0])
270
            self._range[idx] = new_line
271
            return
272
        start = idx.start or 0
273
        end = idx.stop or len(self._range)
274
        if end - start != len(lineparts):
275
            raise ValueError("mismatch of target lines and inserts")
276
277
        lines = []
278
        for i in range(start, end):
279
            ni = i - start  # normalized index
280
            lines.append(self._assemble_line(i, lineparts[ni]))
281
        self._range[start:end] = lines
282
283
    def __iter__(self):
284
        return self
285
286
    def __next__(self):
287
        for line, partial in zip(self._range, self._partials):
288
            yield line[partial]
289
290
    def _assemble_line(self, i, replacement):
291
        start = self._partials[i].start or 0
292
        stop = self._partials[i].stop
293
        orig_prefix = self._range[i][:start]
294
295
        if stop:
296
            orig_suffix = self._range[i][stop:]
297
        else:
298
            orig_suffix = ""
299
        new_line = orig_prefix + replacement + orig_suffix
300
        return new_line
301