diff options
author | Ivan Blinkov <ivan@blinkov.ru> | 2022-02-10 16:47:10 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:47:10 +0300 |
commit | 1aeb9a455974457866f78722ad98114bafc84e8a (patch) | |
tree | e4340eaf1668684d83a0a58c36947c5def5350ad /contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py | |
parent | bd5ef432f5cfb1e18851381329d94665a4c22470 (diff) | |
download | ydb-1aeb9a455974457866f78722ad98114bafc84e8a.tar.gz |
Restoring authorship annotation for Ivan Blinkov <ivan@blinkov.ru>. Commit 1 of 2.
Diffstat (limited to 'contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py')
-rw-r--r-- | contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py | 1308 |
1 files changed, 654 insertions, 654 deletions
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py index 25d817ddd0d..68d6829bad3 100644 --- a/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/document.py @@ -1,41 +1,41 @@ -""" +""" The `Document` that implements all the text operations/querying. -""" -from __future__ import unicode_literals - +""" +from __future__ import unicode_literals + import bisect -import re -import six -import string +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*)') - +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): """ @@ -65,42 +65,42 @@ class _DocumentCache(object): 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` - """ +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) - + + 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: @@ -115,10 +115,10 @@ class Document(object): # 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 __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.text, self.cursor_position) + + @property def text(self): " The document text. " return self._text @@ -134,47 +134,47 @@ class Document(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. """ + 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. """ + + @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): + + @property + def lines(self): """ Array of all the lines. """ - # Cache, because this one is reused very often. + # 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 + + @property def _line_start_indexes(self): """ Array pointing to the start indexes of all the lines. @@ -203,75 +203,75 @@ class Document(object): 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.) - """ + 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.) - """ + + @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. @@ -284,23 +284,23 @@ class Document(object): 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.) - """ + 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 + - 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.) + 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] @@ -311,239 +311,239 @@ class Document(object): 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. """ + # 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. - """ + + 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, + + 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. - """ + """ + 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. - """ + 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 + 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 - + 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 @@ -568,93 +568,93 @@ class Document(object): 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. - """ + 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. - """ + 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)) - + 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. + """ + 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 + """ + 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 @@ -678,137 +678,137 @@ class Document(object): 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. + """ + 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: + 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: + 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 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 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: + """ + 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: + else: to = len(self.text) - 1 - - yield from_, to - + + 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. @@ -840,107 +840,107 @@ class Document(object): 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 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. - + """ + 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) + :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 data.type == SelectionType.CHARACTERS: if after: - new_text = (self.text[:self.cursor_position + 1] + data.text * count + - self.text[self.cursor_position + 1:]) + 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: + 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 - + 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.) @@ -971,31 +971,31 @@ class Document(object): 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) + # 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) |