1""" 
    2The `Document` that implements all the text operations/querying. 
    3""" 
    4 
    5from __future__ import annotations 
    6 
    7import bisect 
    8import re 
    9import string 
    10import weakref 
    11from typing import Callable, Dict, Iterable, List, NoReturn, Pattern, cast 
    12 
    13from .clipboard import ClipboardData 
    14from .filters import vi_mode 
    15from .selection import PasteMode, SelectionState, SelectionType 
    16 
    17__all__ = [ 
    18    "Document", 
    19] 
    20 
    21 
    22# Regex for finding "words" in documents. (We consider a group of alnum 
    23# characters a word, but also a group of special characters a word, as long as 
    24# it doesn't contain a space.) 
    25# (This is a 'word' in Vi.) 
    26_FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") 
    27_FIND_CURRENT_WORD_RE = re.compile(r"^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") 
    28_FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile( 
    29    r"^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)" 
    30) 
    31 
    32# Regex for finding "WORDS" in documents. 
    33# (This is a 'WORD in Vi.) 
    34_FIND_BIG_WORD_RE = re.compile(r"([^\s]+)") 
    35_FIND_CURRENT_BIG_WORD_RE = re.compile(r"^([^\s]+)") 
    36_FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r"^([^\s]+\s*)") 
    37 
    38# Share the Document._cache between all Document instances. 
    39# (Document instances are considered immutable. That means that if another 
    40# `Document` is constructed with the same text, it should have the same 
    41# `_DocumentCache`.) 
    42_text_to_document_cache: dict[str, _DocumentCache] = cast( 
    43    Dict[str, "_DocumentCache"], 
    44    weakref.WeakValueDictionary(),  # Maps document.text to DocumentCache instance. 
    45) 
    46 
    47 
    48class _ImmutableLineList(List[str]): 
    49    """ 
    50    Some protection for our 'lines' list, which is assumed to be immutable in the cache. 
    51    (Useful for detecting obvious bugs.) 
    52    """ 
    53 
    54    def _error(self, *a: object, **kw: object) -> NoReturn: 
    55        raise NotImplementedError("Attempt to modify an immutable list.") 
    56 
    57    __setitem__ = _error 
    58    append = _error 
    59    clear = _error 
    60    extend = _error 
    61    insert = _error 
    62    pop = _error 
    63    remove = _error 
    64    reverse = _error 
    65    sort = _error 
    66 
    67 
    68class _DocumentCache: 
    69    def __init__(self) -> None: 
    70        #: List of lines for the Document text. 
    71        self.lines: _ImmutableLineList | None = None 
    72 
    73        #: List of index positions, pointing to the start of all the lines. 
    74        self.line_indexes: list[int] | None = None 
    75 
    76 
    77class Document: 
    78    """ 
    79    This is a immutable class around the text and cursor position, and contains 
    80    methods for querying this data, e.g. to give the text before the cursor. 
    81 
    82    This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer` 
    83    object, and accessed as the `document` property of that class. 
    84 
    85    :param text: string 
    86    :param cursor_position: int 
    87    :param selection: :class:`.SelectionState` 
    88    """ 
    89 
    90    __slots__ = ("_text", "_cursor_position", "_selection", "_cache") 
    91 
    92    def __init__( 
    93        self, 
    94        text: str = "", 
    95        cursor_position: int | None = None, 
    96        selection: SelectionState | None = None, 
    97    ) -> None: 
    98        # Check cursor position. It can also be right after the end. (Where we 
    99        # insert text.) 
    100        assert cursor_position is None or cursor_position <= len(text), AssertionError( 
    101            f"cursor_position={cursor_position!r}, len_text={len(text)!r}" 
    102        ) 
    103 
    104        # By default, if no cursor position was given, make sure to put the 
    105        # cursor position is at the end of the document. This is what makes 
    106        # sense in most places. 
    107        if cursor_position is None: 
    108            cursor_position = len(text) 
    109 
    110        # Keep these attributes private. A `Document` really has to be 
    111        # considered to be immutable, because otherwise the caching will break 
    112        # things. Because of that, we wrap these into read-only properties. 
    113        self._text = text 
    114        self._cursor_position = cursor_position 
    115        self._selection = selection 
    116 
    117        # Cache for lines/indexes. (Shared with other Document instances that 
    118        # contain the same text. 
    119        try: 
    120            self._cache = _text_to_document_cache[self.text] 
    121        except KeyError: 
    122            self._cache = _DocumentCache() 
    123            _text_to_document_cache[self.text] = self._cache 
    124 
    125        # XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'. 
    126        #     This fails in Pypy3. `self._cache` becomes None, because that's what 
    127        #     'setdefault' returns. 
    128        # self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache()) 
    129        # assert self._cache 
    130 
    131    def __repr__(self) -> str: 
    132        return f"{self.__class__.__name__}({self.text!r}, {self.cursor_position!r})" 
    133 
    134    def __eq__(self, other: object) -> bool: 
    135        if not isinstance(other, Document): 
    136            return False 
    137 
    138        return ( 
    139            self.text == other.text 
    140            and self.cursor_position == other.cursor_position 
    141            and self.selection == other.selection 
    142        ) 
    143 
    144    @property 
    145    def text(self) -> str: 
    146        "The document text." 
    147        return self._text 
    148 
    149    @property 
    150    def cursor_position(self) -> int: 
    151        "The document cursor position." 
    152        return self._cursor_position 
    153 
    154    @property 
    155    def selection(self) -> SelectionState | None: 
    156        ":class:`.SelectionState` object." 
    157        return self._selection 
    158 
    159    @property 
    160    def current_char(self) -> str: 
    161        """Return character under cursor or an empty string.""" 
    162        return self._get_char_relative_to_cursor(0) or "" 
    163 
    164    @property 
    165    def char_before_cursor(self) -> str: 
    166        """Return character before the cursor or an empty string.""" 
    167        return self._get_char_relative_to_cursor(-1) or "" 
    168 
    169    @property 
    170    def text_before_cursor(self) -> str: 
    171        return self.text[: self.cursor_position :] 
    172 
    173    @property 
    174    def text_after_cursor(self) -> str: 
    175        return self.text[self.cursor_position :] 
    176 
    177    @property 
    178    def current_line_before_cursor(self) -> str: 
    179        """Text from the start of the line until the cursor.""" 
    180        _, _, text = self.text_before_cursor.rpartition("\n") 
    181        return text 
    182 
    183    @property 
    184    def current_line_after_cursor(self) -> str: 
    185        """Text from the cursor until the end of the line.""" 
    186        text, _, _ = self.text_after_cursor.partition("\n") 
    187        return text 
    188 
    189    @property 
    190    def lines(self) -> list[str]: 
    191        """ 
    192        Array of all the lines. 
    193        """ 
    194        # Cache, because this one is reused very often. 
    195        if self._cache.lines is None: 
    196            self._cache.lines = _ImmutableLineList(self.text.split("\n")) 
    197 
    198        return self._cache.lines 
    199 
    200    @property 
    201    def _line_start_indexes(self) -> list[int]: 
    202        """ 
    203        Array pointing to the start indexes of all the lines. 
    204        """ 
    205        # Cache, because this is often reused. (If it is used, it's often used 
    206        # many times. And this has to be fast for editing big documents!) 
    207        if self._cache.line_indexes is None: 
    208            # Create list of line lengths. 
    209            line_lengths = map(len, self.lines) 
    210 
    211            # Calculate cumulative sums. 
    212            indexes = [0] 
    213            append = indexes.append 
    214            pos = 0 
    215 
    216            for line_length in line_lengths: 
    217                pos += line_length + 1 
    218                append(pos) 
    219 
    220            # Remove the last item. (This is not a new line.) 
    221            if len(indexes) > 1: 
    222                indexes.pop() 
    223 
    224            self._cache.line_indexes = indexes 
    225 
    226        return self._cache.line_indexes 
    227 
    228    @property 
    229    def lines_from_current(self) -> list[str]: 
    230        """ 
    231        Array of the lines starting from the current line, until the last line. 
    232        """ 
    233        return self.lines[self.cursor_position_row :] 
    234 
    235    @property 
    236    def line_count(self) -> int: 
    237        r"""Return the number of lines in this document. If the document ends 
    238        with a trailing \n, that counts as the beginning of a new line.""" 
    239        return len(self.lines) 
    240 
    241    @property 
    242    def current_line(self) -> str: 
    243        """Return the text on the line where the cursor is. (when the input 
    244        consists of just one line, it equals `text`.""" 
    245        return self.current_line_before_cursor + self.current_line_after_cursor 
    246 
    247    @property 
    248    def leading_whitespace_in_current_line(self) -> str: 
    249        """The leading whitespace in the left margin of the current line.""" 
    250        current_line = self.current_line 
    251        length = len(current_line) - len(current_line.lstrip()) 
    252        return current_line[:length] 
    253 
    254    def _get_char_relative_to_cursor(self, offset: int = 0) -> str: 
    255        """ 
    256        Return character relative to cursor position, or empty string 
    257        """ 
    258        try: 
    259            return self.text[self.cursor_position + offset] 
    260        except IndexError: 
    261            return "" 
    262 
    263    @property 
    264    def on_first_line(self) -> bool: 
    265        """ 
    266        True when we are at the first line. 
    267        """ 
    268        return self.cursor_position_row == 0 
    269 
    270    @property 
    271    def on_last_line(self) -> bool: 
    272        """ 
    273        True when we are at the last line. 
    274        """ 
    275        return self.cursor_position_row == self.line_count - 1 
    276 
    277    @property 
    278    def cursor_position_row(self) -> int: 
    279        """ 
    280        Current row. (0-based.) 
    281        """ 
    282        row, _ = self._find_line_start_index(self.cursor_position) 
    283        return row 
    284 
    285    @property 
    286    def cursor_position_col(self) -> int: 
    287        """ 
    288        Current column. (0-based.) 
    289        """ 
    290        # (Don't use self.text_before_cursor to calculate this. Creating 
    291        # substrings and doing rsplit is too expensive for getting the cursor 
    292        # position.) 
    293        _, line_start_index = self._find_line_start_index(self.cursor_position) 
    294        return self.cursor_position - line_start_index 
    295 
    296    def _find_line_start_index(self, index: int) -> tuple[int, int]: 
    297        """ 
    298        For the index of a character at a certain line, calculate the index of 
    299        the first character on that line. 
    300 
    301        Return (row, index) tuple. 
    302        """ 
    303        indexes = self._line_start_indexes 
    304 
    305        pos = bisect.bisect_right(indexes, index) - 1 
    306        return pos, indexes[pos] 
    307 
    308    def translate_index_to_position(self, index: int) -> tuple[int, int]: 
    309        """ 
    310        Given an index for the text, return the corresponding (row, col) tuple. 
    311        (0-based. Returns (0, 0) for index=0.) 
    312        """ 
    313        # Find start of this line. 
    314        row, row_index = self._find_line_start_index(index) 
    315        col = index - row_index 
    316 
    317        return row, col 
    318 
    319    def translate_row_col_to_index(self, row: int, col: int) -> int: 
    320        """ 
    321        Given a (row, col) tuple, return the corresponding index. 
    322        (Row and col params are 0-based.) 
    323 
    324        Negative row/col values are turned into zero. 
    325        """ 
    326        try: 
    327            result = self._line_start_indexes[row] 
    328            line = self.lines[row] 
    329        except IndexError: 
    330            if row < 0: 
    331                result = self._line_start_indexes[0] 
    332                line = self.lines[0] 
    333            else: 
    334                result = self._line_start_indexes[-1] 
    335                line = self.lines[-1] 
    336 
    337        result += max(0, min(col, len(line))) 
    338 
    339        # Keep in range. (len(self.text) is included, because the cursor can be 
    340        # right after the end of the text as well.) 
    341        result = max(0, min(result, len(self.text))) 
    342        return result 
    343 
    344    @property 
    345    def is_cursor_at_the_end(self) -> bool: 
    346        """True when the cursor is at the end of the text.""" 
    347        return self.cursor_position == len(self.text) 
    348 
    349    @property 
    350    def is_cursor_at_the_end_of_line(self) -> bool: 
    351        """True when the cursor is at the end of this line.""" 
    352        return self.current_char in ("\n", "") 
    353 
    354    def has_match_at_current_position(self, sub: str) -> bool: 
    355        """ 
    356        `True` when this substring is found at the cursor position. 
    357        """ 
    358        return self.text.find(sub, self.cursor_position) == self.cursor_position 
    359 
    360    def find( 
    361        self, 
    362        sub: str, 
    363        in_current_line: bool = False, 
    364        include_current_position: bool = False, 
    365        ignore_case: bool = False, 
    366        count: int = 1, 
    367    ) -> int | None: 
    368        """ 
    369        Find `text` after the cursor, return position relative to the cursor 
    370        position. Return `None` if nothing was found. 
    371 
    372        :param count: Find the n-th occurrence. 
    373        """ 
    374        assert isinstance(ignore_case, bool) 
    375 
    376        if in_current_line: 
    377            text = self.current_line_after_cursor 
    378        else: 
    379            text = self.text_after_cursor 
    380 
    381        if not include_current_position: 
    382            if len(text) == 0: 
    383                return None  # (Otherwise, we always get a match for the empty string.) 
    384            else: 
    385                text = text[1:] 
    386 
    387        flags = re.IGNORECASE if ignore_case else 0 
    388        iterator = re.finditer(re.escape(sub), text, flags) 
    389 
    390        try: 
    391            for i, match in enumerate(iterator): 
    392                if i + 1 == count: 
    393                    if include_current_position: 
    394                        return match.start(0) 
    395                    else: 
    396                        return match.start(0) + 1 
    397        except StopIteration: 
    398            pass 
    399        return None 
    400 
    401    def find_all(self, sub: str, ignore_case: bool = False) -> list[int]: 
    402        """ 
    403        Find all occurrences of the substring. Return a list of absolute 
    404        positions in the document. 
    405        """ 
    406        flags = re.IGNORECASE if ignore_case else 0 
    407        return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)] 
    408 
    409    def find_backwards( 
    410        self, 
    411        sub: str, 
    412        in_current_line: bool = False, 
    413        ignore_case: bool = False, 
    414        count: int = 1, 
    415    ) -> int | None: 
    416        """ 
    417        Find `text` before the cursor, return position relative to the cursor 
    418        position. Return `None` if nothing was found. 
    419 
    420        :param count: Find the n-th occurrence. 
    421        """ 
    422        if in_current_line: 
    423            before_cursor = self.current_line_before_cursor[::-1] 
    424        else: 
    425            before_cursor = self.text_before_cursor[::-1] 
    426 
    427        flags = re.IGNORECASE if ignore_case else 0 
    428        iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags) 
    429 
    430        try: 
    431            for i, match in enumerate(iterator): 
    432                if i + 1 == count: 
    433                    return -match.start(0) - len(sub) 
    434        except StopIteration: 
    435            pass 
    436        return None 
    437 
    438    def get_word_before_cursor( 
    439        self, WORD: bool = False, pattern: Pattern[str] | None = None 
    440    ) -> str: 
    441        """ 
    442        Give the word before the cursor. 
    443        If we have whitespace before the cursor this returns an empty string. 
    444 
    445        :param pattern: (None or compiled regex). When given, use this regex 
    446            pattern. 
    447        """ 
    448        if self._is_word_before_cursor_complete(WORD=WORD, pattern=pattern): 
    449            # Space before the cursor or no text before cursor. 
    450            return "" 
    451 
    452        text_before_cursor = self.text_before_cursor 
    453        start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern) or 0 
    454 
    455        return text_before_cursor[len(text_before_cursor) + start :] 
    456 
    457    def _is_word_before_cursor_complete( 
    458        self, WORD: bool = False, pattern: Pattern[str] | None = None 
    459    ) -> bool: 
    460        if not self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace(): 
    461            return True 
    462        if pattern: 
    463            return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None 
    464 
    465    def find_start_of_previous_word( 
    466        self, count: int = 1, WORD: bool = False, pattern: Pattern[str] | None = None 
    467    ) -> int | None: 
    468        """ 
    469        Return an index relative to the cursor position pointing to the start 
    470        of the previous word. Return `None` if nothing was found. 
    471 
    472        :param pattern: (None or compiled regex). When given, use this regex 
    473            pattern. 
    474        """ 
    475        assert not (WORD and pattern) 
    476 
    477        # Reverse the text before the cursor, in order to do an efficient 
    478        # backwards search. 
    479        text_before_cursor = self.text_before_cursor[::-1] 
    480 
    481        if pattern: 
    482            regex = pattern 
    483        elif WORD: 
    484            regex = _FIND_BIG_WORD_RE 
    485        else: 
    486            regex = _FIND_WORD_RE 
    487 
    488        iterator = regex.finditer(text_before_cursor) 
    489 
    490        try: 
    491            for i, match in enumerate(iterator): 
    492                if i + 1 == count: 
    493                    return -match.end(0) 
    494        except StopIteration: 
    495            pass 
    496        return None 
    497 
    498    def find_boundaries_of_current_word( 
    499        self, 
    500        WORD: bool = False, 
    501        include_leading_whitespace: bool = False, 
    502        include_trailing_whitespace: bool = False, 
    503    ) -> tuple[int, int]: 
    504        """ 
    505        Return the relative boundaries (startpos, endpos) of the current word under the 
    506        cursor. (This is at the current line, because line boundaries obviously 
    507        don't belong to any word.) 
    508        If not on a word, this returns (0,0) 
    509        """ 
    510        text_before_cursor = self.current_line_before_cursor[::-1] 
    511        text_after_cursor = self.current_line_after_cursor 
    512 
    513        def get_regex(include_whitespace: bool) -> Pattern[str]: 
    514            return { 
    515                (False, False): _FIND_CURRENT_WORD_RE, 
    516                (False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE, 
    517                (True, False): _FIND_CURRENT_BIG_WORD_RE, 
    518                (True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE, 
    519            }[(WORD, include_whitespace)] 
    520 
    521        match_before = get_regex(include_leading_whitespace).search(text_before_cursor) 
    522        match_after = get_regex(include_trailing_whitespace).search(text_after_cursor) 
    523 
    524        # When there is a match before and after, and we're not looking for 
    525        # WORDs, make sure that both the part before and after the cursor are 
    526        # either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part 
    527        # before the cursor. 
    528        if not WORD and match_before and match_after: 
    529            c1 = self.text[self.cursor_position - 1] 
    530            c2 = self.text[self.cursor_position] 
    531            alphabet = string.ascii_letters + "0123456789_" 
    532 
    533            if (c1 in alphabet) != (c2 in alphabet): 
    534                match_before = None 
    535 
    536        return ( 
    537            -match_before.end(1) if match_before else 0, 
    538            match_after.end(1) if match_after else 0, 
    539        ) 
    540 
    541    def get_word_under_cursor(self, WORD: bool = False) -> str: 
    542        """ 
    543        Return the word, currently below the cursor. 
    544        This returns an empty string when the cursor is on a whitespace region. 
    545        """ 
    546        start, end = self.find_boundaries_of_current_word(WORD=WORD) 
    547        return self.text[self.cursor_position + start : self.cursor_position + end] 
    548 
    549    def find_next_word_beginning( 
    550        self, count: int = 1, WORD: bool = False 
    551    ) -> int | None: 
    552        """ 
    553        Return an index relative to the cursor position pointing to the start 
    554        of the next word. Return `None` if nothing was found. 
    555        """ 
    556        if count < 0: 
    557            return self.find_previous_word_beginning(count=-count, WORD=WORD) 
    558 
    559        regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE 
    560        iterator = regex.finditer(self.text_after_cursor) 
    561 
    562        try: 
    563            for i, match in enumerate(iterator): 
    564                # Take first match, unless it's the word on which we're right now. 
    565                if i == 0 and match.start(1) == 0: 
    566                    count += 1 
    567 
    568                if i + 1 == count: 
    569                    return match.start(1) 
    570        except StopIteration: 
    571            pass 
    572        return None 
    573 
    574    def find_next_word_ending( 
    575        self, include_current_position: bool = False, count: int = 1, WORD: bool = False 
    576    ) -> int | None: 
    577        """ 
    578        Return an index relative to the cursor position pointing to the end 
    579        of the next word. Return `None` if nothing was found. 
    580        """ 
    581        if count < 0: 
    582            return self.find_previous_word_ending(count=-count, WORD=WORD) 
    583 
    584        if include_current_position: 
    585            text = self.text_after_cursor 
    586        else: 
    587            text = self.text_after_cursor[1:] 
    588 
    589        regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE 
    590        iterable = regex.finditer(text) 
    591 
    592        try: 
    593            for i, match in enumerate(iterable): 
    594                if i + 1 == count: 
    595                    value = match.end(1) 
    596 
    597                    if include_current_position: 
    598                        return value 
    599                    else: 
    600                        return value + 1 
    601 
    602        except StopIteration: 
    603            pass 
    604        return None 
    605 
    606    def find_previous_word_beginning( 
    607        self, count: int = 1, WORD: bool = False 
    608    ) -> int | None: 
    609        """ 
    610        Return an index relative to the cursor position pointing to the start 
    611        of the previous word. Return `None` if nothing was found. 
    612        """ 
    613        if count < 0: 
    614            return self.find_next_word_beginning(count=-count, WORD=WORD) 
    615 
    616        regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE 
    617        iterator = regex.finditer(self.text_before_cursor[::-1]) 
    618 
    619        try: 
    620            for i, match in enumerate(iterator): 
    621                if i + 1 == count: 
    622                    return -match.end(1) 
    623        except StopIteration: 
    624            pass 
    625        return None 
    626 
    627    def find_previous_word_ending( 
    628        self, count: int = 1, WORD: bool = False 
    629    ) -> int | None: 
    630        """ 
    631        Return an index relative to the cursor position pointing to the end 
    632        of the previous word. Return `None` if nothing was found. 
    633        """ 
    634        if count < 0: 
    635            return self.find_next_word_ending(count=-count, WORD=WORD) 
    636 
    637        text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1] 
    638 
    639        regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE 
    640        iterator = regex.finditer(text_before_cursor) 
    641 
    642        try: 
    643            for i, match in enumerate(iterator): 
    644                # Take first match, unless it's the word on which we're right now. 
    645                if i == 0 and match.start(1) == 0: 
    646                    count += 1 
    647 
    648                if i + 1 == count: 
    649                    return -match.start(1) + 1 
    650        except StopIteration: 
    651            pass 
    652        return None 
    653 
    654    def find_next_matching_line( 
    655        self, match_func: Callable[[str], bool], count: int = 1 
    656    ) -> int | None: 
    657        """ 
    658        Look downwards for empty lines. 
    659        Return the line index, relative to the current line. 
    660        """ 
    661        result = None 
    662 
    663        for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]): 
    664            if match_func(line): 
    665                result = 1 + index 
    666                count -= 1 
    667 
    668            if count == 0: 
    669                break 
    670 
    671        return result 
    672 
    673    def find_previous_matching_line( 
    674        self, match_func: Callable[[str], bool], count: int = 1 
    675    ) -> int | None: 
    676        """ 
    677        Look upwards for empty lines. 
    678        Return the line index, relative to the current line. 
    679        """ 
    680        result = None 
    681 
    682        for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]): 
    683            if match_func(line): 
    684                result = -1 - index 
    685                count -= 1 
    686 
    687            if count == 0: 
    688                break 
    689 
    690        return result 
    691 
    692    def get_cursor_left_position(self, count: int = 1) -> int: 
    693        """ 
    694        Relative position for cursor left. 
    695        """ 
    696        if count < 0: 
    697            return self.get_cursor_right_position(-count) 
    698 
    699        return -min(self.cursor_position_col, count) 
    700 
    701    def get_cursor_right_position(self, count: int = 1) -> int: 
    702        """ 
    703        Relative position for cursor_right. 
    704        """ 
    705        if count < 0: 
    706            return self.get_cursor_left_position(-count) 
    707 
    708        return min(count, len(self.current_line_after_cursor)) 
    709 
    710    def get_cursor_up_position( 
    711        self, count: int = 1, preferred_column: int | None = None 
    712    ) -> int: 
    713        """ 
    714        Return the relative cursor position (character index) where we would be if the 
    715        user pressed the arrow-up button. 
    716 
    717        :param preferred_column: When given, go to this column instead of 
    718                                 staying at the current column. 
    719        """ 
    720        assert count >= 1 
    721        column = ( 
    722            self.cursor_position_col if preferred_column is None else preferred_column 
    723        ) 
    724 
    725        return ( 
    726            self.translate_row_col_to_index( 
    727                max(0, self.cursor_position_row - count), column 
    728            ) 
    729            - self.cursor_position 
    730        ) 
    731 
    732    def get_cursor_down_position( 
    733        self, count: int = 1, preferred_column: int | None = None 
    734    ) -> int: 
    735        """ 
    736        Return the relative cursor position (character index) where we would be if the 
    737        user pressed the arrow-down button. 
    738 
    739        :param preferred_column: When given, go to this column instead of 
    740                                 staying at the current column. 
    741        """ 
    742        assert count >= 1 
    743        column = ( 
    744            self.cursor_position_col if preferred_column is None else preferred_column 
    745        ) 
    746 
    747        return ( 
    748            self.translate_row_col_to_index(self.cursor_position_row + count, column) 
    749            - self.cursor_position 
    750        ) 
    751 
    752    def find_enclosing_bracket_right( 
    753        self, left_ch: str, right_ch: str, end_pos: int | None = None 
    754    ) -> int | None: 
    755        """ 
    756        Find the right bracket enclosing current position. Return the relative 
    757        position to the cursor position. 
    758 
    759        When `end_pos` is given, don't look past the position. 
    760        """ 
    761        if self.current_char == right_ch: 
    762            return 0 
    763 
    764        if end_pos is None: 
    765            end_pos = len(self.text) 
    766        else: 
    767            end_pos = min(len(self.text), end_pos) 
    768 
    769        stack = 1 
    770 
    771        # Look forward. 
    772        for i in range(self.cursor_position + 1, end_pos): 
    773            c = self.text[i] 
    774 
    775            if c == left_ch: 
    776                stack += 1 
    777            elif c == right_ch: 
    778                stack -= 1 
    779 
    780            if stack == 0: 
    781                return i - self.cursor_position 
    782 
    783        return None 
    784 
    785    def find_enclosing_bracket_left( 
    786        self, left_ch: str, right_ch: str, start_pos: int | None = None 
    787    ) -> int | None: 
    788        """ 
    789        Find the left bracket enclosing current position. Return the relative 
    790        position to the cursor position. 
    791 
    792        When `start_pos` is given, don't look past the position. 
    793        """ 
    794        if self.current_char == left_ch: 
    795            return 0 
    796 
    797        if start_pos is None: 
    798            start_pos = 0 
    799        else: 
    800            start_pos = max(0, start_pos) 
    801 
    802        stack = 1 
    803 
    804        # Look backward. 
    805        for i in range(self.cursor_position - 1, start_pos - 1, -1): 
    806            c = self.text[i] 
    807 
    808            if c == right_ch: 
    809                stack += 1 
    810            elif c == left_ch: 
    811                stack -= 1 
    812 
    813            if stack == 0: 
    814                return i - self.cursor_position 
    815 
    816        return None 
    817 
    818    def find_matching_bracket_position( 
    819        self, start_pos: int | None = None, end_pos: int | None = None 
    820    ) -> int: 
    821        """ 
    822        Return relative cursor position of matching [, (, { or < bracket. 
    823 
    824        When `start_pos` or `end_pos` are given. Don't look past the positions. 
    825        """ 
    826 
    827        # Look for a match. 
    828        for pair in "()", "[]", "{}", "<>": 
    829            A = pair[0] 
    830            B = pair[1] 
    831            if self.current_char == A: 
    832                return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0 
    833            elif self.current_char == B: 
    834                return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0 
    835 
    836        return 0 
    837 
    838    def get_start_of_document_position(self) -> int: 
    839        """Relative position for the start of the document.""" 
    840        return -self.cursor_position 
    841 
    842    def get_end_of_document_position(self) -> int: 
    843        """Relative position for the end of the document.""" 
    844        return len(self.text) - self.cursor_position 
    845 
    846    def get_start_of_line_position(self, after_whitespace: bool = False) -> int: 
    847        """Relative position for the start of this line.""" 
    848        if after_whitespace: 
    849            current_line = self.current_line 
    850            return ( 
    851                len(current_line) 
    852                - len(current_line.lstrip()) 
    853                - self.cursor_position_col 
    854            ) 
    855        else: 
    856            return -len(self.current_line_before_cursor) 
    857 
    858    def get_end_of_line_position(self) -> int: 
    859        """Relative position for the end of this line.""" 
    860        return len(self.current_line_after_cursor) 
    861 
    862    def last_non_blank_of_current_line_position(self) -> int: 
    863        """ 
    864        Relative position for the last non blank character of this line. 
    865        """ 
    866        return len(self.current_line.rstrip()) - self.cursor_position_col - 1 
    867 
    868    def get_column_cursor_position(self, column: int) -> int: 
    869        """ 
    870        Return the relative cursor position for this column at the current 
    871        line. (It will stay between the boundaries of the line in case of a 
    872        larger number.) 
    873        """ 
    874        line_length = len(self.current_line) 
    875        current_column = self.cursor_position_col 
    876        column = max(0, min(line_length, column)) 
    877 
    878        return column - current_column 
    879 
    880    def selection_range( 
    881        self, 
    882    ) -> tuple[ 
    883        int, int 
    884    ]:  # XXX: shouldn't this return `None` if there is no selection??? 
    885        """ 
    886        Return (from, to) tuple of the selection. 
    887        start and end position are included. 
    888 
    889        This doesn't take the selection type into account. Use 
    890        `selection_ranges` instead. 
    891        """ 
    892        if self.selection: 
    893            from_, to = sorted( 
    894                [self.cursor_position, self.selection.original_cursor_position] 
    895            ) 
    896        else: 
    897            from_, to = self.cursor_position, self.cursor_position 
    898 
    899        return from_, to 
    900 
    901    def selection_ranges(self) -> Iterable[tuple[int, int]]: 
    902        """ 
    903        Return a list of `(from, to)` tuples for the selection or none if 
    904        nothing was selected. The upper boundary is not included. 
    905 
    906        This will yield several (from, to) tuples in case of a BLOCK selection. 
    907        This will return zero ranges, like (8,8) for empty lines in a block 
    908        selection. 
    909        """ 
    910        if self.selection: 
    911            from_, to = sorted( 
    912                [self.cursor_position, self.selection.original_cursor_position] 
    913            ) 
    914 
    915            if self.selection.type == SelectionType.BLOCK: 
    916                from_line, from_column = self.translate_index_to_position(from_) 
    917                to_line, to_column = self.translate_index_to_position(to) 
    918                from_column, to_column = sorted([from_column, to_column]) 
    919                lines = self.lines 
    920 
    921                if vi_mode(): 
    922                    to_column += 1 
    923 
    924                for l in range(from_line, to_line + 1): 
    925                    line_length = len(lines[l]) 
    926 
    927                    if from_column <= line_length: 
    928                        yield ( 
    929                            self.translate_row_col_to_index(l, from_column), 
    930                            self.translate_row_col_to_index( 
    931                                l, min(line_length, to_column) 
    932                            ), 
    933                        ) 
    934            else: 
    935                # In case of a LINES selection, go to the start/end of the lines. 
    936                if self.selection.type == SelectionType.LINES: 
    937                    from_ = max(0, self.text.rfind("\n", 0, from_) + 1) 
    938 
    939                    if self.text.find("\n", to) >= 0: 
    940                        to = self.text.find("\n", to) 
    941                    else: 
    942                        to = len(self.text) - 1 
    943 
    944                # In Vi mode, the upper boundary is always included. For Emacs, 
    945                # that's not the case. 
    946                if vi_mode(): 
    947                    to += 1 
    948 
    949                yield from_, to 
    950 
    951    def selection_range_at_line(self, row: int) -> tuple[int, int] | None: 
    952        """ 
    953        If the selection spans a portion of the given line, return a (from, to) tuple. 
    954 
    955        The returned upper boundary is not included in the selection, so 
    956        `(0, 0)` is an empty selection.  `(0, 1)`, is a one character selection. 
    957 
    958        Returns None if the selection doesn't cover this line at all. 
    959        """ 
    960        if self.selection: 
    961            line = self.lines[row] 
    962 
    963            row_start = self.translate_row_col_to_index(row, 0) 
    964            row_end = self.translate_row_col_to_index(row, len(line)) 
    965 
    966            from_, to = sorted( 
    967                [self.cursor_position, self.selection.original_cursor_position] 
    968            ) 
    969 
    970            # Take the intersection of the current line and the selection. 
    971            intersection_start = max(row_start, from_) 
    972            intersection_end = min(row_end, to) 
    973 
    974            if intersection_start <= intersection_end: 
    975                if self.selection.type == SelectionType.LINES: 
    976                    intersection_start = row_start 
    977                    intersection_end = row_end 
    978 
    979                elif self.selection.type == SelectionType.BLOCK: 
    980                    _, col1 = self.translate_index_to_position(from_) 
    981                    _, col2 = self.translate_index_to_position(to) 
    982                    col1, col2 = sorted([col1, col2]) 
    983 
    984                    if col1 > len(line): 
    985                        return None  # Block selection doesn't cross this line. 
    986 
    987                    intersection_start = self.translate_row_col_to_index(row, col1) 
    988                    intersection_end = self.translate_row_col_to_index(row, col2) 
    989 
    990                _, from_column = self.translate_index_to_position(intersection_start) 
    991                _, to_column = self.translate_index_to_position(intersection_end) 
    992 
    993                # In Vi mode, the upper boundary is always included. For Emacs 
    994                # mode, that's not the case. 
    995                if vi_mode(): 
    996                    to_column += 1 
    997 
    998                return from_column, to_column 
    999        return None 
    1000 
    1001    def cut_selection(self) -> tuple[Document, ClipboardData]: 
    1002        """ 
    1003        Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the 
    1004        document represents the new document when the selection is cut, and the 
    1005        clipboard data, represents whatever has to be put on the clipboard. 
    1006        """ 
    1007        if self.selection: 
    1008            cut_parts = [] 
    1009            remaining_parts = [] 
    1010            new_cursor_position = self.cursor_position 
    1011 
    1012            last_to = 0 
    1013            for from_, to in self.selection_ranges(): 
    1014                if last_to == 0: 
    1015                    new_cursor_position = from_ 
    1016 
    1017                remaining_parts.append(self.text[last_to:from_]) 
    1018                cut_parts.append(self.text[from_:to]) 
    1019                last_to = to 
    1020 
    1021            remaining_parts.append(self.text[last_to:]) 
    1022 
    1023            cut_text = "\n".join(cut_parts) 
    1024            remaining_text = "".join(remaining_parts) 
    1025 
    1026            # In case of a LINES selection, don't include the trailing newline. 
    1027            if self.selection.type == SelectionType.LINES and cut_text.endswith("\n"): 
    1028                cut_text = cut_text[:-1] 
    1029 
    1030            return ( 
    1031                Document(text=remaining_text, cursor_position=new_cursor_position), 
    1032                ClipboardData(cut_text, self.selection.type), 
    1033            ) 
    1034        else: 
    1035            return self, ClipboardData("") 
    1036 
    1037    def paste_clipboard_data( 
    1038        self, 
    1039        data: ClipboardData, 
    1040        paste_mode: PasteMode = PasteMode.EMACS, 
    1041        count: int = 1, 
    1042    ) -> Document: 
    1043        """ 
    1044        Return a new :class:`.Document` instance which contains the result if 
    1045        we would paste this data at the current cursor position. 
    1046 
    1047        :param paste_mode: Where to paste. (Before/after/emacs.) 
    1048        :param count: When >1, Paste multiple times. 
    1049        """ 
    1050        before = paste_mode == PasteMode.VI_BEFORE 
    1051        after = paste_mode == PasteMode.VI_AFTER 
    1052 
    1053        if data.type == SelectionType.CHARACTERS: 
    1054            if after: 
    1055                new_text = ( 
    1056                    self.text[: self.cursor_position + 1] 
    1057                    + data.text * count 
    1058                    + self.text[self.cursor_position + 1 :] 
    1059                ) 
    1060            else: 
    1061                new_text = ( 
    1062                    self.text_before_cursor + data.text * count + self.text_after_cursor 
    1063                ) 
    1064 
    1065            new_cursor_position = self.cursor_position + len(data.text) * count 
    1066            if before: 
    1067                new_cursor_position -= 1 
    1068 
    1069        elif data.type == SelectionType.LINES: 
    1070            l = self.cursor_position_row 
    1071            if before: 
    1072                lines = self.lines[:l] + [data.text] * count + self.lines[l:] 
    1073                new_text = "\n".join(lines) 
    1074                new_cursor_position = len("".join(self.lines[:l])) + l 
    1075            else: 
    1076                lines = self.lines[: l + 1] + [data.text] * count + self.lines[l + 1 :] 
    1077                new_cursor_position = len("".join(self.lines[: l + 1])) + l + 1 
    1078                new_text = "\n".join(lines) 
    1079 
    1080        elif data.type == SelectionType.BLOCK: 
    1081            lines = self.lines[:] 
    1082            start_line = self.cursor_position_row 
    1083            start_column = self.cursor_position_col + (0 if before else 1) 
    1084 
    1085            for i, line in enumerate(data.text.split("\n")): 
    1086                index = i + start_line 
    1087                if index >= len(lines): 
    1088                    lines.append("") 
    1089 
    1090                lines[index] = lines[index].ljust(start_column) 
    1091                lines[index] = ( 
    1092                    lines[index][:start_column] 
    1093                    + line * count 
    1094                    + lines[index][start_column:] 
    1095                ) 
    1096 
    1097            new_text = "\n".join(lines) 
    1098            new_cursor_position = self.cursor_position + (0 if before else 1) 
    1099 
    1100        return Document(text=new_text, cursor_position=new_cursor_position) 
    1101 
    1102    def empty_line_count_at_the_end(self) -> int: 
    1103        """ 
    1104        Return number of empty lines at the end of the document. 
    1105        """ 
    1106        count = 0 
    1107        for line in self.lines[::-1]: 
    1108            if not line or line.isspace(): 
    1109                count += 1 
    1110            else: 
    1111                break 
    1112 
    1113        return count 
    1114 
    1115    def start_of_paragraph(self, count: int = 1, before: bool = False) -> int: 
    1116        """ 
    1117        Return the start of the current paragraph. (Relative cursor position.) 
    1118        """ 
    1119 
    1120        def match_func(text: str) -> bool: 
    1121            return not text or text.isspace() 
    1122 
    1123        line_index = self.find_previous_matching_line( 
    1124            match_func=match_func, count=count 
    1125        ) 
    1126 
    1127        if line_index: 
    1128            add = 0 if before else 1 
    1129            return min(0, self.get_cursor_up_position(count=-line_index) + add) 
    1130        else: 
    1131            return -self.cursor_position 
    1132 
    1133    def end_of_paragraph(self, count: int = 1, after: bool = False) -> int: 
    1134        """ 
    1135        Return the end of the current paragraph. (Relative cursor position.) 
    1136        """ 
    1137 
    1138        def match_func(text: str) -> bool: 
    1139            return not text or text.isspace() 
    1140 
    1141        line_index = self.find_next_matching_line(match_func=match_func, count=count) 
    1142 
    1143        if line_index: 
    1144            add = 0 if after else 1 
    1145            return max(0, self.get_cursor_down_position(count=line_index) - add) 
    1146        else: 
    1147            return len(self.text_after_cursor) 
    1148 
    1149    # Modifiers. 
    1150 
    1151    def insert_after(self, text: str) -> Document: 
    1152        """ 
    1153        Create a new document, with this text inserted after the buffer. 
    1154        It keeps selection ranges and cursor position in sync. 
    1155        """ 
    1156        return Document( 
    1157            text=self.text + text, 
    1158            cursor_position=self.cursor_position, 
    1159            selection=self.selection, 
    1160        ) 
    1161 
    1162    def insert_before(self, text: str) -> Document: 
    1163        """ 
    1164        Create a new document, with this text inserted before the buffer. 
    1165        It keeps selection ranges and cursor position in sync. 
    1166        """ 
    1167        selection_state = self.selection 
    1168 
    1169        if selection_state: 
    1170            selection_state = SelectionState( 
    1171                original_cursor_position=selection_state.original_cursor_position 
    1172                + len(text), 
    1173                type=selection_state.type, 
    1174            ) 
    1175 
    1176        return Document( 
    1177            text=text + self.text, 
    1178            cursor_position=self.cursor_position + len(text), 
    1179            selection=selection_state, 
    1180        )