diff options
author | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
download | ydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz |
add ydb deps
Diffstat (limited to 'contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py')
-rw-r--r-- | contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py | 1001 |
1 files changed, 1001 insertions, 0 deletions
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py new file mode 100644 index 0000000000..25d817ddd0 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py @@ -0,0 +1,1001 @@ +""" +The `Document` that implements all the text operations/querying. +""" +from __future__ import unicode_literals + +import bisect +import re +import six +import string +import weakref +from six.moves import range, map + +from .selection import SelectionType, SelectionState, PasteMode +from .clipboard import ClipboardData + +__all__ = ('Document',) + + +# Regex for finding "words" in documents. (We consider a group of alnum +# characters a word, but also a group of special characters a word, as long as +# it doesn't contain a space.) +# (This is a 'word' in Vi.) +_FIND_WORD_RE = re.compile(r'([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)') +_FIND_CURRENT_WORD_RE = re.compile(r'^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)') +_FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r'^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)') + +# Regex for finding "WORDS" in documents. +# (This is a 'WORD in Vi.) +_FIND_BIG_WORD_RE = re.compile(r'([^\s]+)') +_FIND_CURRENT_BIG_WORD_RE = re.compile(r'^([^\s]+)') +_FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r'^([^\s]+\s*)') + +# Share the Document._cache between all Document instances. +# (Document instances are considered immutable. That means that if another +# `Document` is constructed with the same text, it should have the same +# `_DocumentCache`.) +_text_to_document_cache = weakref.WeakValueDictionary() # Maps document.text to DocumentCache instance. + + +class _ImmutableLineList(list): + """ + Some protection for our 'lines' list, which is assumed to be immutable in the cache. + (Useful for detecting obvious bugs.) + """ + def _error(self, *a, **kw): + raise NotImplementedError('Attempt to modifiy an immutable list.') + + __setitem__ = _error + append = _error + clear = _error + extend = _error + insert = _error + pop = _error + remove = _error + reverse = _error + sort = _error + + +class _DocumentCache(object): + def __init__(self): + #: List of lines for the Document text. + self.lines = None + + #: List of index positions, pointing to the start of all the lines. + self.line_indexes = None + + +class Document(object): + """ + This is a immutable class around the text and cursor position, and contains + methods for querying this data, e.g. to give the text before the cursor. + + This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer` + object, and accessed as the `document` property of that class. + + :param text: string + :param cursor_position: int + :param selection: :class:`.SelectionState` + """ + __slots__ = ('_text', '_cursor_position', '_selection', '_cache') + + def __init__(self, text='', cursor_position=None, selection=None): + assert isinstance(text, six.text_type), 'Got %r' % text + assert selection is None or isinstance(selection, SelectionState) + + # Check cursor position. It can also be right after the end. (Where we + # insert text.) + assert cursor_position is None or cursor_position <= len(text), AssertionError( + 'cursor_position=%r, len_text=%r' % (cursor_position, len(text))) + + # By default, if no cursor position was given, make sure to put the + # cursor position is at the end of the document. This is what makes + # sense in most places. + if cursor_position is None: + cursor_position = len(text) + + # Keep these attributes private. A `Document` really has to be + # considered to be immutable, because otherwise the caching will break + # things. Because of that, we wrap these into read-only properties. + self._text = text + self._cursor_position = cursor_position + self._selection = selection + + # Cache for lines/indexes. (Shared with other Document instances that + # contain the same text. + try: + self._cache = _text_to_document_cache[self.text] + except KeyError: + self._cache = _DocumentCache() + _text_to_document_cache[self.text] = self._cache + + # XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'. + # This fails in Pypy3. `self._cache` becomes None, because that's what + # 'setdefault' returns. + # self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache()) + # assert self._cache + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.text, self.cursor_position) + + @property + def text(self): + " The document text. " + return self._text + + @property + def cursor_position(self): + " The document cursor position. " + return self._cursor_position + + @property + def selection(self): + " :class:`.SelectionState` object. " + return self._selection + + @property + def current_char(self): + """ Return character under cursor or an empty string. """ + return self._get_char_relative_to_cursor(0) or '' + + @property + def char_before_cursor(self): + """ Return character before the cursor or an empty string. """ + return self._get_char_relative_to_cursor(-1) or '' + + @property + def text_before_cursor(self): + return self.text[:self.cursor_position:] + + @property + def text_after_cursor(self): + return self.text[self.cursor_position:] + + @property + def current_line_before_cursor(self): + """ Text from the start of the line until the cursor. """ + _, _, text = self.text_before_cursor.rpartition('\n') + return text + + @property + def current_line_after_cursor(self): + """ Text from the cursor until the end of the line. """ + text, _, _ = self.text_after_cursor.partition('\n') + return text + + @property + def lines(self): + """ + Array of all the lines. + """ + # Cache, because this one is reused very often. + if self._cache.lines is None: + self._cache.lines = _ImmutableLineList(self.text.split('\n')) + + return self._cache.lines + + @property + def _line_start_indexes(self): + """ + Array pointing to the start indexes of all the lines. + """ + # Cache, because this is often reused. (If it is used, it's often used + # many times. And this has to be fast for editing big documents!) + if self._cache.line_indexes is None: + # Create list of line lengths. + line_lengths = map(len, self.lines) + + # Calculate cumulative sums. + indexes = [0] + append = indexes.append + pos = 0 + + for line_length in line_lengths: + pos += line_length + 1 + append(pos) + + # Remove the last item. (This is not a new line.) + if len(indexes) > 1: + indexes.pop() + + self._cache.line_indexes = indexes + + return self._cache.line_indexes + + @property + def lines_from_current(self): + """ + Array of the lines starting from the current line, until the last line. + """ + return self.lines[self.cursor_position_row:] + + @property + def line_count(self): + r""" Return the number of lines in this document. If the document ends + with a trailing \n, that counts as the beginning of a new line. """ + return len(self.lines) + + @property + def current_line(self): + """ Return the text on the line where the cursor is. (when the input + consists of just one line, it equals `text`. """ + return self.current_line_before_cursor + self.current_line_after_cursor + + @property + def leading_whitespace_in_current_line(self): + """ The leading whitespace in the left margin of the current line. """ + current_line = self.current_line + length = len(current_line) - len(current_line.lstrip()) + return current_line[:length] + + def _get_char_relative_to_cursor(self, offset=0): + """ + Return character relative to cursor position, or empty string + """ + try: + return self.text[self.cursor_position + offset] + except IndexError: + return '' + + @property + def on_first_line(self): + """ + True when we are at the first line. + """ + return self.cursor_position_row == 0 + + @property + def on_last_line(self): + """ + True when we are at the last line. + """ + return self.cursor_position_row == self.line_count - 1 + + @property + def cursor_position_row(self): + """ + Current row. (0-based.) + """ + row, _ = self._find_line_start_index(self.cursor_position) + return row + + @property + def cursor_position_col(self): + """ + Current column. (0-based.) + """ + # (Don't use self.text_before_cursor to calculate this. Creating + # substrings and doing rsplit is too expensive for getting the cursor + # position.) + _, line_start_index = self._find_line_start_index(self.cursor_position) + return self.cursor_position - line_start_index + + def _find_line_start_index(self, index): + """ + For the index of a character at a certain line, calculate the index of + the first character on that line. + + Return (row, index) tuple. + """ + indexes = self._line_start_indexes + + pos = bisect.bisect_right(indexes, index) - 1 + return pos, indexes[pos] + + def translate_index_to_position(self, index): + """ + Given an index for the text, return the corresponding (row, col) tuple. + (0-based. Returns (0, 0) for index=0.) + """ + # Find start of this line. + row, row_index = self._find_line_start_index(index) + col = index - row_index + + return row, col + + + def translate_row_col_to_index(self, row, col): + """ + Given a (row, col) tuple, return the corresponding index. + (Row and col params are 0-based.) + + Negative row/col values are turned into zero. + """ + try: + result = self._line_start_indexes[row] + line = self.lines[row] + except IndexError: + if row < 0: + result = self._line_start_indexes[0] + line = self.lines[0] + else: + result = self._line_start_indexes[-1] + line = self.lines[-1] + + result += max(0, min(col, len(line))) + + # Keep in range. (len(self.text) is included, because the cursor can be + # right after the end of the text as well.) + result = max(0, min(result, len(self.text))) + return result + + @property + def is_cursor_at_the_end(self): + """ True when the cursor is at the end of the text. """ + return self.cursor_position == len(self.text) + + @property + def is_cursor_at_the_end_of_line(self): + """ True when the cursor is at the end of this line. """ + return self.current_char in ('\n', '') + + def has_match_at_current_position(self, sub): + """ + `True` when this substring is found at the cursor position. + """ + return self.text.find(sub, self.cursor_position) == self.cursor_position + + def find(self, sub, in_current_line=False, include_current_position=False, + ignore_case=False, count=1): + """ + Find `text` after the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurance. + """ + assert isinstance(ignore_case, bool) + + if in_current_line: + text = self.current_line_after_cursor + else: + text = self.text_after_cursor + + if not include_current_position: + if len(text) == 0: + return # (Otherwise, we always get a match for the empty string.) + else: + text = text[1:] + + flags = re.IGNORECASE if ignore_case else 0 + iterator = re.finditer(re.escape(sub), text, flags) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + if include_current_position: + return match.start(0) + else: + return match.start(0) + 1 + except StopIteration: + pass + + def find_all(self, sub, ignore_case=False): + """ + Find all occurances of the substring. Return a list of absolute + positions in the document. + """ + flags = re.IGNORECASE if ignore_case else 0 + return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)] + + def find_backwards(self, sub, in_current_line=False, ignore_case=False, count=1): + """ + Find `text` before the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurance. + """ + if in_current_line: + before_cursor = self.current_line_before_cursor[::-1] + else: + before_cursor = self.text_before_cursor[::-1] + + flags = re.IGNORECASE if ignore_case else 0 + iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return - match.start(0) - len(sub) + except StopIteration: + pass + + def get_word_before_cursor(self, WORD=False): + """ + Give the word before the cursor. + If we have whitespace before the cursor this returns an empty string. + """ + if self.text_before_cursor[-1:].isspace(): + return '' + else: + return self.text_before_cursor[self.find_start_of_previous_word(WORD=WORD):] + + def find_start_of_previous_word(self, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the start + of the previous word. Return `None` if nothing was found. + """ + # Reverse the text before the cursor, in order to do an efficient + # backwards search. + text_before_cursor = self.text_before_cursor[::-1] + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(text_before_cursor) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return - match.end(1) + except StopIteration: + pass + + def find_boundaries_of_current_word(self, WORD=False, include_leading_whitespace=False, + include_trailing_whitespace=False): + """ + Return the relative boundaries (startpos, endpos) of the current word under the + cursor. (This is at the current line, because line boundaries obviously + don't belong to any word.) + If not on a word, this returns (0,0) + """ + text_before_cursor = self.current_line_before_cursor[::-1] + text_after_cursor = self.current_line_after_cursor + + def get_regex(include_whitespace): + return { + (False, False): _FIND_CURRENT_WORD_RE, + (False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE, + (True, False): _FIND_CURRENT_BIG_WORD_RE, + (True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE, + }[(WORD, include_whitespace)] + + match_before = get_regex(include_leading_whitespace).search(text_before_cursor) + match_after = get_regex(include_trailing_whitespace).search(text_after_cursor) + + # When there is a match before and after, and we're not looking for + # WORDs, make sure that both the part before and after the cursor are + # either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part + # before the cursor. + if not WORD and match_before and match_after: + c1 = self.text[self.cursor_position - 1] + c2 = self.text[self.cursor_position] + alphabet = string.ascii_letters + '0123456789_' + + if (c1 in alphabet) != (c2 in alphabet): + match_before = None + + return ( + - match_before.end(1) if match_before else 0, + match_after.end(1) if match_after else 0 + ) + + def get_word_under_cursor(self, WORD=False): + """ + Return the word, currently below the cursor. + This returns an empty string when the cursor is on a whitespace region. + """ + start, end = self.find_boundaries_of_current_word(WORD=WORD) + return self.text[self.cursor_position + start: self.cursor_position + end] + + def find_next_word_beginning(self, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the start + of the next word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_previous_word_beginning(count=-count, WORD=WORD) + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(self.text_after_cursor) + + try: + for i, match in enumerate(iterator): + # Take first match, unless it's the word on which we're right now. + if i == 0 and match.start(1) == 0: + count += 1 + + if i + 1 == count: + return match.start(1) + except StopIteration: + pass + + def find_next_word_ending(self, include_current_position=False, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the end + of the next word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_previous_word_ending(count=-count, WORD=WORD) + + if include_current_position: + text = self.text_after_cursor + else: + text = self.text_after_cursor[1:] + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterable = regex.finditer(text) + + try: + for i, match in enumerate(iterable): + if i + 1 == count: + value = match.end(1) + + if include_current_position: + return value + else: + return value + 1 + + except StopIteration: + pass + + def find_previous_word_beginning(self, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the start + of the previous word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_next_word_beginning(count=-count, WORD=WORD) + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(self.text_before_cursor[::-1]) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return - match.end(1) + except StopIteration: + pass + + def find_previous_word_ending(self, count=1, WORD=False): + """ + Return an index relative to the cursor position pointing to the end + of the previous word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_next_word_ending(count=-count, WORD=WORD) + + text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1] + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(text_before_cursor) + + try: + for i, match in enumerate(iterator): + # Take first match, unless it's the word on which we're right now. + if i == 0 and match.start(1) == 0: + count += 1 + + if i + 1 == count: + return -match.start(1) + 1 + except StopIteration: + pass + + def find_next_matching_line(self, match_func, count=1): + """ + Look downwards for empty lines. + Return the line index, relative to the current line. + """ + result = None + + for index, line in enumerate(self.lines[self.cursor_position_row + 1:]): + if match_func(line): + result = 1 + index + count -= 1 + + if count == 0: + break + + return result + + def find_previous_matching_line(self, match_func, count=1): + """ + Look upwards for empty lines. + Return the line index, relative to the current line. + """ + result = None + + for index, line in enumerate(self.lines[:self.cursor_position_row][::-1]): + if match_func(line): + result = -1 - index + count -= 1 + + if count == 0: + break + + return result + + def get_cursor_left_position(self, count=1): + """ + Relative position for cursor left. + """ + if count < 0: + return self.get_cursor_right_position(-count) + + return - min(self.cursor_position_col, count) + + def get_cursor_right_position(self, count=1): + """ + Relative position for cursor_right. + """ + if count < 0: + return self.get_cursor_left_position(-count) + + return min(count, len(self.current_line_after_cursor)) + + def get_cursor_up_position(self, count=1, preferred_column=None): + """ + Return the relative cursor position (character index) where we would be if the + user pressed the arrow-up button. + + :param preferred_column: When given, go to this column instead of + staying at the current column. + """ + assert count >= 1 + column = self.cursor_position_col if preferred_column is None else preferred_column + + return self.translate_row_col_to_index( + max(0, self.cursor_position_row - count), column) - self.cursor_position + + def get_cursor_down_position(self, count=1, preferred_column=None): + """ + Return the relative cursor position (character index) where we would be if the + user pressed the arrow-down button. + + :param preferred_column: When given, go to this column instead of + staying at the current column. + """ + assert count >= 1 + column = self.cursor_position_col if preferred_column is None else preferred_column + + return self.translate_row_col_to_index( + self.cursor_position_row + count, column) - self.cursor_position + + def find_enclosing_bracket_right(self, left_ch, right_ch, end_pos=None): + """ + Find the right bracket enclosing current position. Return the relative + position to the cursor position. + + When `end_pos` is given, don't look past the position. + """ + if self.current_char == right_ch: + return 0 + + if end_pos is None: + end_pos = len(self.text) + else: + end_pos = min(len(self.text), end_pos) + + stack = 1 + + # Look forward. + for i in range(self.cursor_position + 1, end_pos): + c = self.text[i] + + if c == left_ch: + stack += 1 + elif c == right_ch: + stack -= 1 + + if stack == 0: + return i - self.cursor_position + + def find_enclosing_bracket_left(self, left_ch, right_ch, start_pos=None): + """ + Find the left bracket enclosing current position. Return the relative + position to the cursor position. + + When `start_pos` is given, don't look past the position. + """ + if self.current_char == left_ch: + return 0 + + if start_pos is None: + start_pos = 0 + else: + start_pos = max(0, start_pos) + + stack = 1 + + # Look backward. + for i in range(self.cursor_position - 1, start_pos - 1, -1): + c = self.text[i] + + if c == right_ch: + stack += 1 + elif c == left_ch: + stack -= 1 + + if stack == 0: + return i - self.cursor_position + + def find_matching_bracket_position(self, start_pos=None, end_pos=None): + """ + Return relative cursor position of matching [, (, { or < bracket. + + When `start_pos` or `end_pos` are given. Don't look past the positions. + """ + + # Look for a match. + for A, B in '()', '[]', '{}', '<>': + if self.current_char == A: + return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0 + elif self.current_char == B: + return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0 + + return 0 + + def get_start_of_document_position(self): + """ Relative position for the start of the document. """ + return - self.cursor_position + + def get_end_of_document_position(self): + """ Relative position for the end of the document. """ + return len(self.text) - self.cursor_position + + def get_start_of_line_position(self, after_whitespace=False): + """ Relative position for the start of this line. """ + if after_whitespace: + current_line = self.current_line + return len(current_line) - len(current_line.lstrip()) - self.cursor_position_col + else: + return - len(self.current_line_before_cursor) + + def get_end_of_line_position(self): + """ Relative position for the end of this line. """ + return len(self.current_line_after_cursor) + + def last_non_blank_of_current_line_position(self): + """ + Relative position for the last non blank character of this line. + """ + return len(self.current_line.rstrip()) - self.cursor_position_col - 1 + + def get_column_cursor_position(self, column): + """ + Return the relative cursor position for this column at the current + line. (It will stay between the boundaries of the line in case of a + larger number.) + """ + line_length = len(self.current_line) + current_column = self.cursor_position_col + column = max(0, min(line_length, column)) + + return column - current_column + + def selection_range(self): # XXX: shouldn't this return `None` if there is no selection??? + """ + Return (from, to) tuple of the selection. + start and end position are included. + + This doesn't take the selection type into account. Use + `selection_ranges` instead. + """ + if self.selection: + from_, to = sorted([self.cursor_position, self.selection.original_cursor_position]) + else: + from_, to = self.cursor_position, self.cursor_position + + return from_, to + + def selection_ranges(self): + """ + Return a list of (from, to) tuples for the selection or none if nothing + was selected. start and end position are always included in the + selection. + + This will yield several (from, to) tuples in case of a BLOCK selection. + """ + if self.selection: + from_, to = sorted([self.cursor_position, self.selection.original_cursor_position]) + + if self.selection.type == SelectionType.BLOCK: + from_line, from_column = self.translate_index_to_position(from_) + to_line, to_column = self.translate_index_to_position(to) + from_column, to_column = sorted([from_column, to_column]) + lines = self.lines + + for l in range(from_line, to_line + 1): + line_length = len(lines[l]) + if from_column < line_length: + yield (self.translate_row_col_to_index(l, from_column), + self.translate_row_col_to_index(l, min(line_length - 1, to_column))) + else: + # In case of a LINES selection, go to the start/end of the lines. + if self.selection.type == SelectionType.LINES: + from_ = max(0, self.text.rfind('\n', 0, from_) + 1) + + if self.text.find('\n', to) >= 0: + to = self.text.find('\n', to) + else: + to = len(self.text) - 1 + + yield from_, to + + def selection_range_at_line(self, row): + """ + If the selection spans a portion of the given line, return a (from, to) tuple. + Otherwise, return None. + """ + if self.selection: + row_start = self.translate_row_col_to_index(row, 0) + row_end = self.translate_row_col_to_index(row, max(0, len(self.lines[row]) - 1)) + + from_, to = sorted([self.cursor_position, self.selection.original_cursor_position]) + + # Take the intersection of the current line and the selection. + intersection_start = max(row_start, from_) + intersection_end = min(row_end, to) + + if intersection_start <= intersection_end: + if self.selection.type == SelectionType.LINES: + intersection_start = row_start + intersection_end = row_end + elif self.selection.type == SelectionType.BLOCK: + _, col1 = self.translate_index_to_position(from_) + _, col2 = self.translate_index_to_position(to) + col1, col2 = sorted([col1, col2]) + intersection_start = self.translate_row_col_to_index(row, col1) + intersection_end = self.translate_row_col_to_index(row, col2) + + _, from_column = self.translate_index_to_position(intersection_start) + _, to_column = self.translate_index_to_position(intersection_end) + + return from_column, to_column + + def cut_selection(self): + """ + Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the + document represents the new document when the selection is cut, and the + clipboard data, represents whatever has to be put on the clipboard. + """ + if self.selection: + cut_parts = [] + remaining_parts = [] + new_cursor_position = self.cursor_position + + last_to = 0 + for from_, to in self.selection_ranges(): + if last_to == 0: + new_cursor_position = from_ + + remaining_parts.append(self.text[last_to:from_]) + cut_parts.append(self.text[from_:to + 1]) + last_to = to + 1 + + remaining_parts.append(self.text[last_to:]) + + cut_text = '\n'.join(cut_parts) + remaining_text = ''.join(remaining_parts) + + # In case of a LINES selection, don't include the trailing newline. + if self.selection.type == SelectionType.LINES and cut_text.endswith('\n'): + cut_text = cut_text[:-1] + + return (Document(text=remaining_text, cursor_position=new_cursor_position), + ClipboardData(cut_text, self.selection.type)) + else: + return self, ClipboardData('') + + def paste_clipboard_data(self, data, paste_mode=PasteMode.EMACS, count=1): + """ + Return a new :class:`.Document` instance which contains the result if + we would paste this data at the current cursor position. + + :param paste_mode: Where to paste. (Before/after/emacs.) + :param count: When >1, Paste multiple times. + """ + assert isinstance(data, ClipboardData) + assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS) + + before = (paste_mode == PasteMode.VI_BEFORE) + after = (paste_mode == PasteMode.VI_AFTER) + + if data.type == SelectionType.CHARACTERS: + if after: + new_text = (self.text[:self.cursor_position + 1] + data.text * count + + self.text[self.cursor_position + 1:]) + else: + new_text = self.text_before_cursor + data.text * count + self.text_after_cursor + + new_cursor_position = self.cursor_position + len(data.text) * count + if before: + new_cursor_position -= 1 + + elif data.type == SelectionType.LINES: + l = self.cursor_position_row + if before: + lines = self.lines[:l] + [data.text] * count + self.lines[l:] + new_text = '\n'.join(lines) + new_cursor_position = len(''.join(self.lines[:l])) + l + else: + lines = self.lines[:l + 1] + [data.text] * count + self.lines[l + 1:] + new_cursor_position = len(''.join(self.lines[:l + 1])) + l + 1 + new_text = '\n'.join(lines) + + elif data.type == SelectionType.BLOCK: + lines = self.lines[:] + start_line = self.cursor_position_row + start_column = self.cursor_position_col + (0 if before else 1) + + for i, line in enumerate(data.text.split('\n')): + index = i + start_line + if index >= len(lines): + lines.append('') + + lines[index] = lines[index].ljust(start_column) + lines[index] = lines[index][:start_column] + line * count + lines[index][start_column:] + + new_text = '\n'.join(lines) + new_cursor_position = self.cursor_position + (0 if before else 1) + + return Document(text=new_text, cursor_position=new_cursor_position) + + def empty_line_count_at_the_end(self): + """ + Return number of empty lines at the end of the document. + """ + count = 0 + for line in self.lines[::-1]: + if not line or line.isspace(): + count += 1 + else: + break + + return count + + def start_of_paragraph(self, count=1, before=False): + """ + Return the start of the current paragraph. (Relative cursor position.) + """ + def match_func(text): + return not text or text.isspace() + + line_index = self.find_previous_matching_line(match_func=match_func, count=count) + + if line_index: + add = 0 if before else 1 + return min(0, self.get_cursor_up_position(count=-line_index) + add) + else: + return -self.cursor_position + + def end_of_paragraph(self, count=1, after=False): + """ + Return the end of the current paragraph. (Relative cursor position.) + """ + def match_func(text): + return not text or text.isspace() + + line_index = self.find_next_matching_line(match_func=match_func, count=count) + + if line_index: + add = 0 if after else 1 + return max(0, self.get_cursor_down_position(count=line_index) - add) + else: + return len(self.text_after_cursor) + + # Modifiers. + + def insert_after(self, text): + """ + Create a new document, with this text inserted after the buffer. + It keeps selection ranges and cursor position in sync. + """ + return Document( + text=self.text + text, + cursor_position=self.cursor_position, + selection=self.selection) + + def insert_before(self, text): + """ + Create a new document, with this text inserted before the buffer. + It keeps selection ranges and cursor position in sync. + """ + selection_state = self.selection + + if selection_state: + selection_state = SelectionState( + original_cursor_position=selection_state.original_cursor_position + len(text), + type=selection_state.type) + + return Document( + text=text + self.text, + cursor_position=self.cursor_position + len(text), + selection=selection_state) |