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