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