1# pylint: disable=function-redefined 2from __future__ import unicode_literals 3 4import codecs 5import string 6 7import six 8from six.moves import range 9 10from prompt_toolkit.application.current import get_app 11from prompt_toolkit.buffer import indent, reshape_text, unindent 12from prompt_toolkit.clipboard import ClipboardData 13from prompt_toolkit.document import Document 14from prompt_toolkit.filters import ( 15 Always, 16 Condition, 17 has_arg, 18 is_read_only, 19 is_searching, 20) 21from prompt_toolkit.filters.app import ( 22 in_paste_mode, 23 is_multiline, 24 vi_digraph_mode, 25 vi_insert_mode, 26 vi_insert_multiple_mode, 27 vi_mode, 28 vi_navigation_mode, 29 vi_recording_macro, 30 vi_replace_mode, 31 vi_search_direction_reversed, 32 vi_selection_mode, 33 vi_waiting_for_text_object_mode, 34) 35from prompt_toolkit.input.vt100_parser import Vt100Parser 36from prompt_toolkit.key_binding.digraphs import DIGRAPHS 37from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode 38from prompt_toolkit.keys import Keys 39from prompt_toolkit.search import SearchDirection 40from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType 41 42from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase 43from .named_commands import get_by_name 44 45try: 46 from itertools import accumulate 47except ImportError: # < Python 3.2 48 def accumulate(iterable): 49 " Super simple 'accumulate' implementation. " 50 total = 0 51 for item in iterable: 52 total += item 53 yield total 54 55__all__ = [ 56 'load_vi_bindings', 57 'load_vi_search_bindings', 58] 59 60if six.PY2: 61 ascii_lowercase = string.ascii_lowercase.decode('ascii') 62else: 63 ascii_lowercase = string.ascii_lowercase 64 65vi_register_names = ascii_lowercase + '0123456789' 66 67 68class TextObjectType(object): 69 EXCLUSIVE = 'EXCLUSIVE' 70 INCLUSIVE = 'INCLUSIVE' 71 LINEWISE = 'LINEWISE' 72 BLOCK = 'BLOCK' 73 74 75class TextObject(object): 76 """ 77 Return struct for functions wrapped in ``text_object``. 78 Both `start` and `end` are relative to the current cursor position. 79 """ 80 def __init__(self, start, end=0, type=TextObjectType.EXCLUSIVE): 81 self.start = start 82 self.end = end 83 self.type = type 84 85 @property 86 def selection_type(self): 87 if self.type == TextObjectType.LINEWISE: 88 return SelectionType.LINES 89 if self.type == TextObjectType.BLOCK: 90 return SelectionType.BLOCK 91 else: 92 return SelectionType.CHARACTERS 93 94 def sorted(self): 95 """ 96 Return a (start, end) tuple where start <= end. 97 """ 98 if self.start < self.end: 99 return self.start, self.end 100 else: 101 return self.end, self.start 102 103 def operator_range(self, document): 104 """ 105 Return a (start, end) tuple with start <= end that indicates the range 106 operators should operate on. 107 `buffer` is used to get start and end of line positions. 108 109 This should return something that can be used in a slice, so the `end` 110 position is *not* included. 111 """ 112 start, end = self.sorted() 113 doc = document 114 115 if (self.type == TextObjectType.EXCLUSIVE and 116 doc.translate_index_to_position(end + doc.cursor_position)[1] == 0): 117 # If the motion is exclusive and the end of motion is on the first 118 # column, the end position becomes end of previous line. 119 end -= 1 120 if self.type == TextObjectType.INCLUSIVE: 121 end += 1 122 if self.type == TextObjectType.LINEWISE: 123 # Select whole lines 124 row, col = doc.translate_index_to_position(start + doc.cursor_position) 125 start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position 126 row, col = doc.translate_index_to_position(end + doc.cursor_position) 127 end = doc.translate_row_col_to_index(row, len(doc.lines[row])) - doc.cursor_position 128 return start, end 129 130 def get_line_numbers(self, buffer): 131 """ 132 Return a (start_line, end_line) pair. 133 """ 134 # Get absolute cursor positions from the text object. 135 from_, to = self.operator_range(buffer.document) 136 from_ += buffer.cursor_position 137 to += buffer.cursor_position 138 139 # Take the start of the lines. 140 from_, _ = buffer.document.translate_index_to_position(from_) 141 to, _ = buffer.document.translate_index_to_position(to) 142 143 return from_, to 144 145 def cut(self, buffer): 146 """ 147 Turn text object into `ClipboardData` instance. 148 """ 149 from_, to = self.operator_range(buffer.document) 150 151 from_ += buffer.cursor_position 152 to += buffer.cursor_position 153 154 # For Vi mode, the SelectionState does include the upper position, 155 # while `self.operator_range` does not. So, go one to the left, unless 156 # we're in the line mode, then we don't want to risk going to the 157 # previous line, and missing one line in the selection. 158 if self.type != TextObjectType.LINEWISE: 159 to -= 1 160 161 document = Document(buffer.text, to, SelectionState( 162 original_cursor_position=from_, type=self.selection_type)) 163 164 new_document, clipboard_data = document.cut_selection() 165 return new_document, clipboard_data 166 167 168def create_text_object_decorator(key_bindings): 169 """ 170 Create a decorator that can be used to register Vi text object implementations. 171 """ 172 assert isinstance(key_bindings, KeyBindingsBase) 173 174 def text_object_decorator(*keys, **kw): 175 """ 176 Register a text object function. 177 178 Usage:: 179 180 @text_object('w', filter=..., no_move_handler=False) 181 def handler(event): 182 # Return a text object for this key. 183 return TextObject(...) 184 185 :param no_move_handler: Disable the move handler in navigation mode. 186 (It's still active in selection mode.) 187 """ 188 filter = kw.pop('filter', Always()) 189 no_move_handler = kw.pop('no_move_handler', False) 190 no_selection_handler = kw.pop('no_selection_handler', False) 191 eager = kw.pop('eager', False) 192 assert not kw 193 194 def decorator(text_object_func): 195 assert callable(text_object_func) 196 197 @key_bindings.add(*keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager) 198 def _(event): 199 # Arguments are multiplied. 200 vi_state = event.app.vi_state 201 event._arg = (vi_state.operator_arg or 1) * (event.arg or 1) 202 203 # Call the text object handler. 204 text_obj = text_object_func(event) 205 if text_obj is not None: 206 assert isinstance(text_obj, TextObject) 207 208 # Call the operator function with the text object. 209 vi_state.operator_func(event, text_obj) 210 211 # Clear operator. 212 event.app.vi_state.operator_func = None 213 event.app.vi_state.operator_arg = None 214 215 # Register a move operation. (Doesn't need an operator.) 216 if not no_move_handler: 217 @key_bindings.add(*keys, filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, eager=eager) 218 def _(event): 219 " Move handler for navigation mode. " 220 text_object = text_object_func(event) 221 event.current_buffer.cursor_position += text_object.start 222 223 # Register a move selection operation. 224 if not no_selection_handler: 225 @key_bindings.add(*keys, filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, eager=eager) 226 def _(event): 227 " Move handler for selection mode. " 228 text_object = text_object_func(event) 229 buff = event.current_buffer 230 231 # When the text object has both a start and end position, like 'i(' or 'iw', 232 # Turn this into a selection, otherwise the cursor. 233 if text_object.end: 234 # Take selection positions from text object. 235 start, end = text_object.operator_range(buff.document) 236 start += buff.cursor_position 237 end += buff.cursor_position 238 239 buff.selection_state.original_cursor_position = start 240 buff.cursor_position = end 241 242 # Take selection type from text object. 243 if text_object.type == TextObjectType.LINEWISE: 244 buff.selection_state.type = SelectionType.LINES 245 else: 246 buff.selection_state.type = SelectionType.CHARACTERS 247 else: 248 event.current_buffer.cursor_position += text_object.start 249 250 # Make it possible to chain @text_object decorators. 251 return text_object_func 252 253 return decorator 254 return text_object_decorator 255 256 257def create_operator_decorator(key_bindings): 258 """ 259 Create a decorator that can be used for registering Vi operators. 260 """ 261 assert isinstance(key_bindings, KeyBindingsBase) 262 263 def operator_decorator(*keys, **kw): 264 """ 265 Register a Vi operator. 266 267 Usage:: 268 269 @operator('d', filter=...) 270 def handler(event, text_object): 271 # Do something with the text object here. 272 """ 273 filter = kw.pop('filter', Always()) 274 eager = kw.pop('eager', False) 275 assert not kw 276 277 def decorator(operator_func): 278 @key_bindings.add(*keys, filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, eager=eager) 279 def _(event): 280 """ 281 Handle operator in navigation mode. 282 """ 283 # When this key binding is matched, only set the operator 284 # function in the ViState. We should execute it after a text 285 # object has been received. 286 event.app.vi_state.operator_func = operator_func 287 event.app.vi_state.operator_arg = event.arg 288 289 @key_bindings.add(*keys, filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, eager=eager) 290 def _(event): 291 """ 292 Handle operator in selection mode. 293 """ 294 buff = event.current_buffer 295 selection_state = buff.selection_state 296 297 # Create text object from selection. 298 if selection_state.type == SelectionType.LINES: 299 text_obj_type = TextObjectType.LINEWISE 300 elif selection_state.type == SelectionType.BLOCK: 301 text_obj_type = TextObjectType.BLOCK 302 else: 303 text_obj_type = TextObjectType.INCLUSIVE 304 305 text_object = TextObject( 306 selection_state.original_cursor_position - buff.cursor_position, 307 type=text_obj_type) 308 309 # Execute operator. 310 operator_func(event, text_object) 311 312 # Quit selection mode. 313 buff.selection_state = None 314 315 return operator_func 316 return decorator 317 return operator_decorator 318 319 320def load_vi_bindings(): 321 """ 322 Vi extensions. 323 324 # Overview of Readline Vi commands: 325 # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf 326 """ 327 # Note: Some key bindings have the "~IsReadOnly()" filter added. This 328 # prevents the handler to be executed when the focus is on a 329 # read-only buffer. 330 # This is however only required for those that change the ViState to 331 # INSERT mode. The `Buffer` class itself throws the 332 # `EditReadOnlyBuffer` exception for any text operations which is 333 # handled correctly. There is no need to add "~IsReadOnly" to all key 334 # bindings that do text manipulation. 335 336 key_bindings = KeyBindings() 337 handle = key_bindings.add 338 339 # (Note: Always take the navigation bindings in read-only mode, even when 340 # ViState says different.) 341 342 vi_transform_functions = [ 343 # Rot 13 transformation 344 (('g', '?'), Always(), lambda string: codecs.encode(string, 'rot_13')), 345 346 # To lowercase 347 (('g', 'u'), Always(), lambda string: string.lower()), 348 349 # To uppercase. 350 (('g', 'U'), Always(), lambda string: string.upper()), 351 352 # Swap case. 353 (('g', '~'), Always(), lambda string: string.swapcase()), 354 (('~', ), Condition(lambda: get_app().vi_state.tilde_operator), lambda string: string.swapcase()), 355 ] 356 357 # Insert a character literally (quoted insert). 358 handle('c-v', filter=vi_insert_mode)(get_by_name('quoted-insert')) 359 360 @handle('escape') 361 def _(event): 362 """ 363 Escape goes to vi navigation mode. 364 """ 365 buffer = event.current_buffer 366 vi_state = event.app.vi_state 367 368 if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): 369 buffer.cursor_position += buffer.document.get_cursor_left_position() 370 371 vi_state.input_mode = InputMode.NAVIGATION 372 373 if bool(buffer.selection_state): 374 buffer.exit_selection() 375 376 @handle('k', filter=vi_selection_mode) 377 def _(event): 378 """ 379 Arrow up in selection mode. 380 """ 381 event.current_buffer.cursor_up(count=event.arg) 382 383 @handle('j', filter=vi_selection_mode) 384 def _(event): 385 """ 386 Arrow down in selection mode. 387 """ 388 event.current_buffer.cursor_down(count=event.arg) 389 390 @handle('up', filter=vi_navigation_mode) 391 @handle('c-p', filter=vi_navigation_mode) 392 def _(event): 393 """ 394 Arrow up and ControlP in navigation mode go up. 395 """ 396 event.current_buffer.auto_up(count=event.arg) 397 398 @handle('k', filter=vi_navigation_mode) 399 def _(event): 400 """ 401 Go up, but if we enter a new history entry, move to the start of the 402 line. 403 """ 404 event.current_buffer.auto_up( 405 count=event.arg, go_to_start_of_line_if_history_changes=True) 406 407 @handle('down', filter=vi_navigation_mode) 408 @handle('c-n', filter=vi_navigation_mode) 409 def _(event): 410 """ 411 Arrow down and Control-N in navigation mode. 412 """ 413 event.current_buffer.auto_down(count=event.arg) 414 415 @handle('j', filter=vi_navigation_mode) 416 def _(event): 417 """ 418 Go down, but if we enter a new history entry, go to the start of the line. 419 """ 420 event.current_buffer.auto_down( 421 count=event.arg, go_to_start_of_line_if_history_changes=True) 422 423 @handle('backspace', filter=vi_navigation_mode) 424 def _(event): 425 """ 426 In navigation-mode, move cursor. 427 """ 428 event.current_buffer.cursor_position += \ 429 event.current_buffer.document.get_cursor_left_position(count=event.arg) 430 431 @handle('c-n', filter=vi_insert_mode) 432 def _(event): 433 b = event.current_buffer 434 435 if b.complete_state: 436 b.complete_next() 437 else: 438 b.start_completion(select_first=True) 439 440 @handle('c-p', filter=vi_insert_mode) 441 def _(event): 442 """ 443 Control-P: To previous completion. 444 """ 445 b = event.current_buffer 446 447 if b.complete_state: 448 b.complete_previous() 449 else: 450 b.start_completion(select_last=True) 451 452 @handle('c-g', filter=vi_insert_mode) 453 @handle('c-y', filter=vi_insert_mode) 454 def _(event): 455 """ 456 Accept current completion. 457 """ 458 event.current_buffer.complete_state = None 459 460 @handle('c-e', filter=vi_insert_mode) 461 def _(event): 462 """ 463 Cancel completion. Go back to originally typed text. 464 """ 465 event.current_buffer.cancel_completion() 466 467 @Condition 468 def is_returnable(): 469 return get_app().current_buffer.is_returnable 470 471 # In navigation mode, pressing enter will always return the input. 472 handle('enter', filter=vi_navigation_mode & is_returnable)( 473 get_by_name('accept-line')) 474 475 # In insert mode, also accept input when enter is pressed, and the buffer 476 # has been marked as single line. 477 handle('enter', filter=is_returnable & ~is_multiline)( 478 get_by_name('accept-line')) 479 480 @handle('enter', filter=~is_returnable & vi_navigation_mode) 481 def _(event): 482 " Go to the beginning of next line. " 483 b = event.current_buffer 484 b.cursor_down(count=event.arg) 485 b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True) 486 487 # ** In navigation mode ** 488 489 # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html 490 491 @handle('insert', filter=vi_navigation_mode) 492 def _(event): 493 " Pressing the Insert key. " 494 event.app.vi_state.input_mode = InputMode.INSERT 495 496 @handle('insert', filter=vi_insert_mode) 497 def _(event): 498 " Pressing the Insert key. " 499 event.app.vi_state.input_mode = InputMode.NAVIGATION 500 501 @handle('a', filter=vi_navigation_mode & ~is_read_only) 502 # ~IsReadOnly, because we want to stay in navigation mode for 503 # read-only buffers. 504 def _(event): 505 event.current_buffer.cursor_position += event.current_buffer.document.get_cursor_right_position() 506 event.app.vi_state.input_mode = InputMode.INSERT 507 508 @handle('A', filter=vi_navigation_mode & ~is_read_only) 509 def _(event): 510 event.current_buffer.cursor_position += event.current_buffer.document.get_end_of_line_position() 511 event.app.vi_state.input_mode = InputMode.INSERT 512 513 @handle('C', filter=vi_navigation_mode & ~is_read_only) 514 def _(event): 515 """ 516 # Change to end of line. 517 # Same as 'c$' (which is implemented elsewhere.) 518 """ 519 buffer = event.current_buffer 520 521 deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) 522 event.app.clipboard.set_text(deleted) 523 event.app.vi_state.input_mode = InputMode.INSERT 524 525 @handle('c', 'c', filter=vi_navigation_mode & ~is_read_only) 526 @handle('S', filter=vi_navigation_mode & ~is_read_only) 527 def _(event): # TODO: implement 'arg' 528 """ 529 Change current line 530 """ 531 buffer = event.current_buffer 532 533 # We copy the whole line. 534 data = ClipboardData(buffer.document.current_line, SelectionType.LINES) 535 event.app.clipboard.set_data(data) 536 537 # But we delete after the whitespace 538 buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) 539 buffer.delete(count=buffer.document.get_end_of_line_position()) 540 event.app.vi_state.input_mode = InputMode.INSERT 541 542 @handle('D', filter=vi_navigation_mode) 543 def _(event): 544 buffer = event.current_buffer 545 deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) 546 event.app.clipboard.set_text(deleted) 547 548 @handle('d', 'd', filter=vi_navigation_mode) 549 def _(event): 550 """ 551 Delete line. (Or the following 'n' lines.) 552 """ 553 buffer = event.current_buffer 554 555 # Split string in before/deleted/after text. 556 lines = buffer.document.lines 557 558 before = '\n'.join(lines[:buffer.document.cursor_position_row]) 559 deleted = '\n'.join(lines[buffer.document.cursor_position_row: 560 buffer.document.cursor_position_row + event.arg]) 561 after = '\n'.join(lines[buffer.document.cursor_position_row + event.arg:]) 562 563 # Set new text. 564 if before and after: 565 before = before + '\n' 566 567 # Set text and cursor position. 568 buffer.document = Document( 569 text=before + after, 570 # Cursor At the start of the first 'after' line, after the leading whitespace. 571 cursor_position = len(before) + len(after) - len(after.lstrip(' '))) 572 573 # Set clipboard data 574 event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) 575 576 @handle('x', filter=vi_selection_mode) 577 def _(event): 578 """ 579 Cut selection. 580 ('x' is not an operator.) 581 """ 582 clipboard_data = event.current_buffer.cut_selection() 583 event.app.clipboard.set_data(clipboard_data) 584 585 @handle('i', filter=vi_navigation_mode & ~is_read_only) 586 def _(event): 587 event.app.vi_state.input_mode = InputMode.INSERT 588 589 @handle('I', filter=vi_navigation_mode & ~is_read_only) 590 def _(event): 591 event.app.vi_state.input_mode = InputMode.INSERT 592 event.current_buffer.cursor_position += \ 593 event.current_buffer.document.get_start_of_line_position(after_whitespace=True) 594 595 @Condition 596 def in_block_selection(): 597 buff = get_app().current_buffer 598 return buff.selection_state and buff.selection_state.type == SelectionType.BLOCK 599 600 @handle('I', filter=in_block_selection & ~is_read_only) 601 def go_to_block_selection(event, after=False): 602 " Insert in block selection mode. " 603 buff = event.current_buffer 604 605 # Store all cursor positions. 606 positions = [] 607 608 if after: 609 def get_pos(from_to): 610 return from_to[1] 611 else: 612 def get_pos(from_to): 613 return from_to[0] 614 615 for i, from_to in enumerate(buff.document.selection_ranges()): 616 positions.append(get_pos(from_to)) 617 if i == 0: 618 buff.cursor_position = get_pos(from_to) 619 620 buff.multiple_cursor_positions = positions 621 622 # Go to 'INSERT_MULTIPLE' mode. 623 event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE 624 buff.exit_selection() 625 626 @handle('A', filter=in_block_selection & ~is_read_only) 627 def _(event): 628 go_to_block_selection(event, after=True) 629 630 @handle('J', filter=vi_navigation_mode & ~is_read_only) 631 def _(event): 632 " Join lines. " 633 for i in range(event.arg): 634 event.current_buffer.join_next_line() 635 636 @handle('g', 'J', filter=vi_navigation_mode & ~is_read_only) 637 def _(event): 638 " Join lines without space. " 639 for i in range(event.arg): 640 event.current_buffer.join_next_line(separator='') 641 642 @handle('J', filter=vi_selection_mode & ~is_read_only) 643 def _(event): 644 " Join selected lines. " 645 event.current_buffer.join_selected_lines() 646 647 @handle('g', 'J', filter=vi_selection_mode & ~is_read_only) 648 def _(event): 649 " Join selected lines without space. " 650 event.current_buffer.join_selected_lines(separator='') 651 652 @handle('p', filter=vi_navigation_mode) 653 def _(event): 654 """ 655 Paste after 656 """ 657 event.current_buffer.paste_clipboard_data( 658 event.app.clipboard.get_data(), 659 count=event.arg, 660 paste_mode=PasteMode.VI_AFTER) 661 662 @handle('P', filter=vi_navigation_mode) 663 def _(event): 664 """ 665 Paste before 666 """ 667 event.current_buffer.paste_clipboard_data( 668 event.app.clipboard.get_data(), 669 count=event.arg, 670 paste_mode=PasteMode.VI_BEFORE) 671 672 @handle('"', Keys.Any, 'p', filter=vi_navigation_mode) 673 def _(event): 674 " Paste from named register. " 675 c = event.key_sequence[1].data 676 if c in vi_register_names: 677 data = event.app.vi_state.named_registers.get(c) 678 if data: 679 event.current_buffer.paste_clipboard_data( 680 data, count=event.arg, paste_mode=PasteMode.VI_AFTER) 681 682 @handle('"', Keys.Any, 'P', filter=vi_navigation_mode) 683 def _(event): 684 " Paste (before) from named register. " 685 c = event.key_sequence[1].data 686 if c in vi_register_names: 687 data = event.app.vi_state.named_registers.get(c) 688 if data: 689 event.current_buffer.paste_clipboard_data( 690 data, count=event.arg, paste_mode=PasteMode.VI_BEFORE) 691 692 @handle('r', Keys.Any, filter=vi_navigation_mode) 693 def _(event): 694 """ 695 Replace single character under cursor 696 """ 697 event.current_buffer.insert_text(event.data * event.arg, overwrite=True) 698 event.current_buffer.cursor_position -= 1 699 700 @handle('R', filter=vi_navigation_mode) 701 def _(event): 702 """ 703 Go to 'replace'-mode. 704 """ 705 event.app.vi_state.input_mode = InputMode.REPLACE 706 707 @handle('s', filter=vi_navigation_mode & ~is_read_only) 708 def _(event): 709 """ 710 Substitute with new text 711 (Delete character(s) and go to insert mode.) 712 """ 713 text = event.current_buffer.delete(count=event.arg) 714 event.app.clipboard.set_text(text) 715 event.app.vi_state.input_mode = InputMode.INSERT 716 717 @handle('u', filter=vi_navigation_mode, save_before=(lambda e: False)) 718 def _(event): 719 for i in range(event.arg): 720 event.current_buffer.undo() 721 722 @handle('V', filter=vi_navigation_mode) 723 def _(event): 724 """ 725 Start lines selection. 726 """ 727 event.current_buffer.start_selection(selection_type=SelectionType.LINES) 728 729 @handle('c-v', filter=vi_navigation_mode) 730 def _(event): 731 " Enter block selection mode. " 732 event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) 733 734 @handle('V', filter=vi_selection_mode) 735 def _(event): 736 """ 737 Exit line selection mode, or go from non line selection mode to line 738 selection mode. 739 """ 740 selection_state = event.current_buffer.selection_state 741 742 if selection_state.type != SelectionType.LINES: 743 selection_state.type = SelectionType.LINES 744 else: 745 event.current_buffer.exit_selection() 746 747 @handle('v', filter=vi_navigation_mode) 748 def _(event): 749 " Enter character selection mode. " 750 event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) 751 752 @handle('v', filter=vi_selection_mode) 753 def _(event): 754 """ 755 Exit character selection mode, or go from non-character-selection mode 756 to character selection mode. 757 """ 758 selection_state = event.current_buffer.selection_state 759 760 if selection_state.type != SelectionType.CHARACTERS: 761 selection_state.type = SelectionType.CHARACTERS 762 else: 763 event.current_buffer.exit_selection() 764 765 @handle('c-v', filter=vi_selection_mode) 766 def _(event): 767 """ 768 Exit block selection mode, or go from non block selection mode to block 769 selection mode. 770 """ 771 selection_state = event.current_buffer.selection_state 772 773 if selection_state.type != SelectionType.BLOCK: 774 selection_state.type = SelectionType.BLOCK 775 else: 776 event.current_buffer.exit_selection() 777 778 @handle('a', 'w', filter=vi_selection_mode) 779 @handle('a', 'W', filter=vi_selection_mode) 780 def _(event): 781 """ 782 Switch from visual linewise mode to visual characterwise mode. 783 """ 784 buffer = event.current_buffer 785 786 if buffer.selection_state and buffer.selection_state.type == SelectionType.LINES: 787 buffer.selection_state.type = SelectionType.CHARACTERS 788 789 @handle('x', filter=vi_navigation_mode) 790 def _(event): 791 """ 792 Delete character. 793 """ 794 buff = event.current_buffer 795 count = min(event.arg, len(buff.document.current_line_after_cursor)) 796 if count: 797 text = event.current_buffer.delete(count=count) 798 event.app.clipboard.set_text(text) 799 800 @handle('X', filter=vi_navigation_mode) 801 def _(event): 802 buff = event.current_buffer 803 count = min(event.arg, len(buff.document.current_line_before_cursor)) 804 if count: 805 text = event.current_buffer.delete_before_cursor(count=count) 806 event.app.clipboard.set_text(text) 807 808 @handle('y', 'y', filter=vi_navigation_mode) 809 @handle('Y', filter=vi_navigation_mode) 810 def _(event): 811 """ 812 Yank the whole line. 813 """ 814 text = '\n'.join(event.current_buffer.document.lines_from_current[:event.arg]) 815 event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) 816 817 @handle('+', filter=vi_navigation_mode) 818 def _(event): 819 """ 820 Move to first non whitespace of next line 821 """ 822 buffer = event.current_buffer 823 buffer.cursor_position += buffer.document.get_cursor_down_position(count=event.arg) 824 buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) 825 826 @handle('-', filter=vi_navigation_mode) 827 def _(event): 828 """ 829 Move to first non whitespace of previous line 830 """ 831 buffer = event.current_buffer 832 buffer.cursor_position += buffer.document.get_cursor_up_position(count=event.arg) 833 buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) 834 835 @handle('>', '>', filter=vi_navigation_mode) 836 def _(event): 837 """ 838 Indent lines. 839 """ 840 buffer = event.current_buffer 841 current_row = buffer.document.cursor_position_row 842 indent(buffer, current_row, current_row + event.arg) 843 844 @handle('<', '<', filter=vi_navigation_mode) 845 def _(event): 846 """ 847 Unindent lines. 848 """ 849 current_row = event.current_buffer.document.cursor_position_row 850 unindent(event.current_buffer, current_row, current_row + event.arg) 851 852 @handle('O', filter=vi_navigation_mode & ~is_read_only) 853 def _(event): 854 """ 855 Open line above and enter insertion mode 856 """ 857 event.current_buffer.insert_line_above( 858 copy_margin=not in_paste_mode()) 859 event.app.vi_state.input_mode = InputMode.INSERT 860 861 @handle('o', filter=vi_navigation_mode & ~is_read_only) 862 def _(event): 863 """ 864 Open line below and enter insertion mode 865 """ 866 event.current_buffer.insert_line_below( 867 copy_margin=not in_paste_mode()) 868 event.app.vi_state.input_mode = InputMode.INSERT 869 870 @handle('~', filter=vi_navigation_mode) 871 def _(event): 872 """ 873 Reverse case of current character and move cursor forward. 874 """ 875 buffer = event.current_buffer 876 c = buffer.document.current_char 877 878 if c is not None and c != '\n': 879 buffer.insert_text(c.swapcase(), overwrite=True) 880 881 @handle('g', 'u', 'u', filter=vi_navigation_mode & ~is_read_only) 882 def _(event): 883 " Lowercase current line. " 884 buff = event.current_buffer 885 buff.transform_current_line(lambda s: s.lower()) 886 887 @handle('g', 'U', 'U', filter=vi_navigation_mode & ~is_read_only) 888 def _(event): 889 " Uppercase current line. " 890 buff = event.current_buffer 891 buff.transform_current_line(lambda s: s.upper()) 892 893 @handle('g', '~', '~', filter=vi_navigation_mode & ~is_read_only) 894 def _(event): 895 " Swap case of the current line. " 896 buff = event.current_buffer 897 buff.transform_current_line(lambda s: s.swapcase()) 898 899 @handle('#', filter=vi_navigation_mode) 900 def _(event): 901 """ 902 Go to previous occurrence of this word. 903 """ 904 b = event.current_buffer 905 search_state = event.app.current_search_state 906 907 search_state.text = b.document.get_word_under_cursor() 908 search_state.direction = SearchDirection.BACKWARD 909 910 b.apply_search(search_state, count=event.arg, 911 include_current_position=False) 912 913 @handle('*', filter=vi_navigation_mode) 914 def _(event): 915 """ 916 Go to next occurrence of this word. 917 """ 918 b = event.current_buffer 919 search_state = event.app.current_search_state 920 921 search_state.text = b.document.get_word_under_cursor() 922 search_state.direction = SearchDirection.FORWARD 923 924 b.apply_search(search_state, count=event.arg, 925 include_current_position=False) 926 927 @handle('(', filter=vi_navigation_mode) 928 def _(event): 929 # TODO: go to begin of sentence. 930 # XXX: should become text_object. 931 pass 932 933 @handle(')', filter=vi_navigation_mode) 934 def _(event): 935 # TODO: go to end of sentence. 936 # XXX: should become text_object. 937 pass 938 939 operator = create_operator_decorator(key_bindings) 940 text_object = create_text_object_decorator(key_bindings) 941 942 @text_object(Keys.Any, filter=vi_waiting_for_text_object_mode) 943 def _(event): 944 """ 945 Unknown key binding while waiting for a text object. 946 """ 947 event.app.output.bell() 948 949 # 950 # *** Operators *** 951 # 952 953 def create_delete_and_change_operators(delete_only, with_register=False): 954 """ 955 Delete and change operators. 956 957 :param delete_only: Create an operator that deletes, but doesn't go to insert mode. 958 :param with_register: Copy the deleted text to this named register instead of the clipboard. 959 """ 960 if with_register: 961 handler_keys = ('"', Keys.Any, 'cd'[delete_only]) 962 else: 963 handler_keys = 'cd'[delete_only] 964 965 @operator(*handler_keys, filter=~is_read_only) 966 def delete_or_change_operator(event, text_object): 967 clipboard_data = None 968 buff = event.current_buffer 969 970 if text_object: 971 new_document, clipboard_data = text_object.cut(buff) 972 buff.document = new_document 973 974 # Set deleted/changed text to clipboard or named register. 975 if clipboard_data and clipboard_data.text: 976 if with_register: 977 reg_name = event.key_sequence[1].data 978 if reg_name in vi_register_names: 979 event.app.vi_state.named_registers[reg_name] = clipboard_data 980 else: 981 event.app.clipboard.set_data(clipboard_data) 982 983 # Only go back to insert mode in case of 'change'. 984 if not delete_only: 985 event.app.vi_state.input_mode = InputMode.INSERT 986 987 create_delete_and_change_operators(False, False) 988 create_delete_and_change_operators(False, True) 989 create_delete_and_change_operators(True, False) 990 create_delete_and_change_operators(True, True) 991 992 def create_transform_handler(filter, transform_func, *a): 993 @operator(*a, filter=filter & ~is_read_only) 994 def _(event, text_object): 995 """ 996 Apply transformation (uppercase, lowercase, rot13, swap case). 997 """ 998 buff = event.current_buffer 999 start, end = text_object.operator_range(buff.document) 1000 1001 if start < end: 1002 # Transform. 1003 buff.transform_region( 1004 buff.cursor_position + start, 1005 buff.cursor_position + end, 1006 transform_func) 1007 1008 # Move cursor 1009 buff.cursor_position += (text_object.end or text_object.start) 1010 1011 for k, f, func in vi_transform_functions: 1012 create_transform_handler(f, func, *k) 1013 1014 @operator('y') 1015 def yank_handler(event, text_object): 1016 """ 1017 Yank operator. (Copy text.) 1018 """ 1019 _, clipboard_data = text_object.cut(event.current_buffer) 1020 if clipboard_data.text: 1021 event.app.clipboard.set_data(clipboard_data) 1022 1023 @operator('"', Keys.Any, 'y') 1024 def _(event, text_object): 1025 " Yank selection to named register. " 1026 c = event.key_sequence[1].data 1027 if c in vi_register_names: 1028 _, clipboard_data = text_object.cut(event.current_buffer) 1029 event.app.vi_state.named_registers[c] = clipboard_data 1030 1031 @operator('>') 1032 def _(event, text_object): 1033 """ 1034 Indent. 1035 """ 1036 buff = event.current_buffer 1037 from_, to = text_object.get_line_numbers(buff) 1038 indent(buff, from_, to + 1, count=event.arg) 1039 1040 @operator('<') 1041 def _(event, text_object): 1042 """ 1043 Unindent. 1044 """ 1045 buff = event.current_buffer 1046 from_, to = text_object.get_line_numbers(buff) 1047 unindent(buff, from_, to + 1, count=event.arg) 1048 1049 @operator('g', 'q') 1050 def _(event, text_object): 1051 """ 1052 Reshape text. 1053 """ 1054 buff = event.current_buffer 1055 from_, to = text_object.get_line_numbers(buff) 1056 reshape_text(buff, from_, to) 1057 1058 # 1059 # *** Text objects *** 1060 # 1061 1062 @text_object('b') 1063 def _(event): 1064 """ Move one word or token left. """ 1065 return TextObject(event.current_buffer.document.find_start_of_previous_word(count=event.arg) or 0) 1066 1067 @text_object('B') 1068 def _(event): 1069 """ Move one non-blank word left """ 1070 return TextObject(event.current_buffer.document.find_start_of_previous_word(count=event.arg, WORD=True) or 0) 1071 1072 @text_object('$') 1073 def key_dollar(event): 1074 """ 'c$', 'd$' and '$': Delete/change/move until end of line. """ 1075 return TextObject(event.current_buffer.document.get_end_of_line_position()) 1076 1077 @text_object('w') 1078 def _(event): 1079 """ 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. """ 1080 return TextObject(event.current_buffer.document.find_next_word_beginning(count=event.arg) or 1081 event.current_buffer.document.get_end_of_document_position()) 1082 1083 @text_object('W') 1084 def _(event): 1085 """ 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. """ 1086 return TextObject(event.current_buffer.document.find_next_word_beginning(count=event.arg, WORD=True) or 1087 event.current_buffer.document.get_end_of_document_position()) 1088 1089 @text_object('e') 1090 def _(event): 1091 """ End of 'word': 'ce', 'de', 'e' """ 1092 end = event.current_buffer.document.find_next_word_ending(count=event.arg) 1093 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) 1094 1095 @text_object('E') 1096 def _(event): 1097 """ End of 'WORD': 'cE', 'dE', 'E' """ 1098 end = event.current_buffer.document.find_next_word_ending(count=event.arg, WORD=True) 1099 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) 1100 1101 @text_object('i', 'w', no_move_handler=True) 1102 def _(event): 1103 """ Inner 'word': ciw and diw """ 1104 start, end = event.current_buffer.document.find_boundaries_of_current_word() 1105 return TextObject(start, end) 1106 1107 @text_object('a', 'w', no_move_handler=True) 1108 def _(event): 1109 """ A 'word': caw and daw """ 1110 start, end = event.current_buffer.document.find_boundaries_of_current_word(include_trailing_whitespace=True) 1111 return TextObject(start, end) 1112 1113 @text_object('i', 'W', no_move_handler=True) 1114 def _(event): 1115 """ Inner 'WORD': ciW and diW """ 1116 start, end = event.current_buffer.document.find_boundaries_of_current_word(WORD=True) 1117 return TextObject(start, end) 1118 1119 @text_object('a', 'W', no_move_handler=True) 1120 def _(event): 1121 """ A 'WORD': caw and daw """ 1122 start, end = event.current_buffer.document.find_boundaries_of_current_word(WORD=True, include_trailing_whitespace=True) 1123 return TextObject(start, end) 1124 1125 @text_object('a', 'p', no_move_handler=True) 1126 def _(event): 1127 """ 1128 Auto paragraph. 1129 """ 1130 start = event.current_buffer.document.start_of_paragraph() 1131 end = event.current_buffer.document.end_of_paragraph(count=event.arg) 1132 return TextObject(start, end) 1133 1134 @text_object('^') 1135 def key_circumflex(event): 1136 """ 'c^', 'd^' and '^': Soft start of line, after whitespace. """ 1137 return TextObject(event.current_buffer.document.get_start_of_line_position(after_whitespace=True)) 1138 1139 @text_object('0') 1140 def key_zero(event): 1141 """ 1142 'c0', 'd0': Hard start of line, before whitespace. 1143 (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) 1144 """ 1145 return TextObject(event.current_buffer.document.get_start_of_line_position(after_whitespace=False)) 1146 1147 def create_ci_ca_handles(ci_start, ci_end, inner, key=None): 1148 # TODO: 'dat', 'dit', (tags (like xml) 1149 """ 1150 Delete/Change string between this start and stop character. But keep these characters. 1151 This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. 1152 """ 1153 def handler(event): 1154 if ci_start == ci_end: 1155 # Quotes 1156 start = event.current_buffer.document.find_backwards(ci_start, in_current_line=False) 1157 end = event.current_buffer.document.find(ci_end, in_current_line=False) 1158 else: 1159 # Brackets 1160 start = event.current_buffer.document.find_enclosing_bracket_left(ci_start, ci_end) 1161 end = event.current_buffer.document.find_enclosing_bracket_right(ci_start, ci_end) 1162 1163 if start is not None and end is not None: 1164 offset = 0 if inner else 1 1165 return TextObject(start + 1 - offset, end + offset) 1166 else: 1167 # Nothing found. 1168 return TextObject(0) 1169 1170 if key is None: 1171 text_object('ai'[inner], ci_start, no_move_handler=True)(handler) 1172 text_object('ai'[inner], ci_end, no_move_handler=True)(handler) 1173 else: 1174 text_object('ai'[inner], key, no_move_handler=True)(handler) 1175 1176 for inner in (False, True): 1177 for ci_start, ci_end in [('"', '"'), ("'", "'"), ("`", "`"), 1178 ('[', ']'), ('<', '>'), ('{', '}'), ('(', ')')]: 1179 create_ci_ca_handles(ci_start, ci_end, inner) 1180 1181 create_ci_ca_handles('(', ')', inner, 'b') # 'dab', 'dib' 1182 create_ci_ca_handles('{', '}', inner, 'B') # 'daB', 'diB' 1183 1184 @text_object('{') 1185 def _(event): 1186 """ 1187 Move to previous blank-line separated section. 1188 Implements '{', 'c{', 'd{', 'y{' 1189 """ 1190 index = event.current_buffer.document.start_of_paragraph( 1191 count=event.arg, before=True) 1192 return TextObject(index) 1193 1194 @text_object('}') 1195 def _(event): 1196 """ 1197 Move to next blank-line separated section. 1198 Implements '}', 'c}', 'd}', 'y}' 1199 """ 1200 index = event.current_buffer.document.end_of_paragraph(count=event.arg, after=True) 1201 return TextObject(index) 1202 1203 @text_object('f', Keys.Any) 1204 def _(event): 1205 """ 1206 Go to next occurrence of character. Typing 'fx' will move the 1207 cursor to the next occurrence of character. 'x'. 1208 """ 1209 event.app.vi_state.last_character_find = CharacterFind(event.data, False) 1210 match = event.current_buffer.document.find( 1211 event.data, in_current_line=True, count=event.arg) 1212 if match: 1213 return TextObject(match, type=TextObjectType.INCLUSIVE) 1214 else: 1215 return TextObject(0) 1216 1217 @text_object('F', Keys.Any) 1218 def _(event): 1219 """ 1220 Go to previous occurrence of character. Typing 'Fx' will move the 1221 cursor to the previous occurrence of character. 'x'. 1222 """ 1223 event.app.vi_state.last_character_find = CharacterFind(event.data, True) 1224 return TextObject(event.current_buffer.document.find_backwards( 1225 event.data, in_current_line=True, count=event.arg) or 0) 1226 1227 @text_object('t', Keys.Any) 1228 def _(event): 1229 """ 1230 Move right to the next occurrence of c, then one char backward. 1231 """ 1232 event.app.vi_state.last_character_find = CharacterFind(event.data, False) 1233 match = event.current_buffer.document.find( 1234 event.data, in_current_line=True, count=event.arg) 1235 if match: 1236 return TextObject(match - 1, type=TextObjectType.INCLUSIVE) 1237 else: 1238 return TextObject(0) 1239 1240 @text_object('T', Keys.Any) 1241 def _(event): 1242 """ 1243 Move left to the previous occurrence of c, then one char forward. 1244 """ 1245 event.app.vi_state.last_character_find = CharacterFind(event.data, True) 1246 match = event.current_buffer.document.find_backwards( 1247 event.data, in_current_line=True, count=event.arg) 1248 return TextObject(match + 1 if match else 0) 1249 1250 def repeat(reverse): 1251 """ 1252 Create ',' and ';' commands. 1253 """ 1254 @text_object(',' if reverse else ';') 1255 def _(event): 1256 # Repeat the last 'f'/'F'/'t'/'T' command. 1257 pos = 0 1258 vi_state = event.app.vi_state 1259 1260 type = TextObjectType.EXCLUSIVE 1261 1262 if vi_state.last_character_find: 1263 char = vi_state.last_character_find.character 1264 backwards = vi_state.last_character_find.backwards 1265 1266 if reverse: 1267 backwards = not backwards 1268 1269 if backwards: 1270 pos = event.current_buffer.document.find_backwards(char, in_current_line=True, count=event.arg) 1271 else: 1272 pos = event.current_buffer.document.find(char, in_current_line=True, count=event.arg) 1273 type = TextObjectType.INCLUSIVE 1274 if pos: 1275 return TextObject(pos, type=type) 1276 else: 1277 return TextObject(0) 1278 repeat(True) 1279 repeat(False) 1280 1281 @text_object('h') 1282 @text_object('left') 1283 def _(event): 1284 """ Implements 'ch', 'dh', 'h': Cursor left. """ 1285 return TextObject(event.current_buffer.document.get_cursor_left_position(count=event.arg)) 1286 1287 @text_object('j', no_move_handler=True, no_selection_handler=True) 1288 # Note: We also need `no_selection_handler`, because we in 1289 # selection mode, we prefer the other 'j' binding that keeps 1290 # `buffer.preferred_column`. 1291 def _(event): 1292 """ Implements 'cj', 'dj', 'j', ... Cursor up. """ 1293 return TextObject(event.current_buffer.document.get_cursor_down_position(count=event.arg), 1294 type=TextObjectType.LINEWISE) 1295 1296 @text_object('k', no_move_handler=True, no_selection_handler=True) 1297 def _(event): 1298 """ Implements 'ck', 'dk', 'k', ... Cursor up. """ 1299 return TextObject(event.current_buffer.document.get_cursor_up_position(count=event.arg), 1300 type=TextObjectType.LINEWISE) 1301 1302 @text_object('l') 1303 @text_object(' ') 1304 @text_object('right') 1305 def _(event): 1306 """ Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. """ 1307 return TextObject(event.current_buffer.document.get_cursor_right_position(count=event.arg)) 1308 1309 @text_object('H') 1310 def _(event): 1311 """ 1312 Moves to the start of the visible region. (Below the scroll offset.) 1313 Implements 'cH', 'dH', 'H'. 1314 """ 1315 w = event.app.layout.current_window 1316 b = event.current_buffer 1317 1318 if w and w.render_info: 1319 # When we find a Window that has BufferControl showing this window, 1320 # move to the start of the visible area. 1321 pos = (b.document.translate_row_col_to_index( 1322 w.render_info.first_visible_line(after_scroll_offset=True), 0) - 1323 b.cursor_position) 1324 1325 else: 1326 # Otherwise, move to the start of the input. 1327 pos = -len(b.document.text_before_cursor) 1328 return TextObject(pos, type=TextObjectType.LINEWISE) 1329 1330 @text_object('M') 1331 def _(event): 1332 """ 1333 Moves cursor to the vertical center of the visible region. 1334 Implements 'cM', 'dM', 'M'. 1335 """ 1336 w = event.app.layout.current_window 1337 b = event.current_buffer 1338 1339 if w and w.render_info: 1340 # When we find a Window that has BufferControl showing this window, 1341 # move to the center of the visible area. 1342 pos = (b.document.translate_row_col_to_index( 1343 w.render_info.center_visible_line(), 0) - 1344 b.cursor_position) 1345 1346 else: 1347 # Otherwise, move to the start of the input. 1348 pos = -len(b.document.text_before_cursor) 1349 return TextObject(pos, type=TextObjectType.LINEWISE) 1350 1351 @text_object('L') 1352 def _(event): 1353 """ 1354 Moves to the end of the visible region. (Above the scroll offset.) 1355 """ 1356 w = event.app.layout.current_window 1357 b = event.current_buffer 1358 1359 if w and w.render_info: 1360 # When we find a Window that has BufferControl showing this window, 1361 # move to the end of the visible area. 1362 pos = (b.document.translate_row_col_to_index( 1363 w.render_info.last_visible_line(before_scroll_offset=True), 0) - 1364 b.cursor_position) 1365 1366 else: 1367 # Otherwise, move to the end of the input. 1368 pos = len(b.document.text_after_cursor) 1369 return TextObject(pos, type=TextObjectType.LINEWISE) 1370 1371 @text_object('n', no_move_handler=True) 1372 def _(event): 1373 " Search next. " 1374 buff = event.current_buffer 1375 search_state = event.app.current_search_state 1376 1377 cursor_position = buff.get_search_position( 1378 search_state, include_current_position=False, count=event.arg) 1379 return TextObject(cursor_position - buff.cursor_position) 1380 1381 @handle('n', filter=vi_navigation_mode) 1382 def _(event): 1383 " Search next in navigation mode. (This goes through the history.) " 1384 search_state = event.app.current_search_state 1385 1386 event.current_buffer.apply_search( 1387 search_state, include_current_position=False, count=event.arg) 1388 1389 @text_object('N', no_move_handler=True) 1390 def _(event): 1391 " Search previous. " 1392 buff = event.current_buffer 1393 search_state = event.app.current_search_state 1394 1395 cursor_position = buff.get_search_position( 1396 ~search_state, include_current_position=False, count=event.arg) 1397 return TextObject(cursor_position - buff.cursor_position) 1398 1399 @handle('N', filter=vi_navigation_mode) 1400 def _(event): 1401 " Search previous in navigation mode. (This goes through the history.) " 1402 search_state = event.app.current_search_state 1403 1404 event.current_buffer.apply_search( 1405 ~search_state, include_current_position=False, count=event.arg) 1406 1407 @handle('z', '+', filter=vi_navigation_mode|vi_selection_mode) 1408 @handle('z', 't', filter=vi_navigation_mode|vi_selection_mode) 1409 @handle('z', 'enter', filter=vi_navigation_mode|vi_selection_mode) 1410 def _(event): 1411 """ 1412 Scrolls the window to makes the current line the first line in the visible region. 1413 """ 1414 b = event.current_buffer 1415 event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row 1416 1417 @handle('z', '-', filter=vi_navigation_mode|vi_selection_mode) 1418 @handle('z', 'b', filter=vi_navigation_mode|vi_selection_mode) 1419 def _(event): 1420 """ 1421 Scrolls the window to makes the current line the last line in the visible region. 1422 """ 1423 # We can safely set the scroll offset to zero; the Window will make 1424 # sure that it scrolls at least enough to make the cursor visible 1425 # again. 1426 event.app.layout.current_window.vertical_scroll = 0 1427 1428 @handle('z', 'z', filter=vi_navigation_mode|vi_selection_mode) 1429 def _(event): 1430 """ 1431 Center Window vertically around cursor. 1432 """ 1433 w = event.app.layout.current_window 1434 b = event.current_buffer 1435 1436 if w and w.render_info: 1437 info = w.render_info 1438 1439 # Calculate the offset that we need in order to position the row 1440 # containing the cursor in the center. 1441 scroll_height = info.window_height // 2 1442 1443 y = max(0, b.document.cursor_position_row - 1) 1444 height = 0 1445 while y > 0: 1446 line_height = info.get_height_for_line(y) 1447 1448 if height + line_height < scroll_height: 1449 height += line_height 1450 y -= 1 1451 else: 1452 break 1453 1454 w.vertical_scroll = y 1455 1456 @text_object('%') 1457 def _(event): 1458 """ 1459 Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) 1460 If an 'arg' has been given, go this this % position in the file. 1461 """ 1462 buffer = event.current_buffer 1463 1464 if event._arg: 1465 # If 'arg' has been given, the meaning of % is to go to the 'x%' 1466 # row in the file. 1467 if 0 < event.arg <= 100: 1468 absolute_index = buffer.document.translate_row_col_to_index( 1469 int((event.arg * buffer.document.line_count - 1) / 100), 0) 1470 return TextObject(absolute_index - buffer.document.cursor_position, type=TextObjectType.LINEWISE) 1471 else: 1472 return TextObject(0) # Do nothing. 1473 1474 else: 1475 # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). 1476 match = buffer.document.find_matching_bracket_position() 1477 if match: 1478 return TextObject(match, type=TextObjectType.INCLUSIVE) 1479 else: 1480 return TextObject(0) 1481 1482 @text_object('|') 1483 def _(event): 1484 # Move to the n-th column (you may specify the argument n by typing 1485 # it on number keys, for example, 20|). 1486 return TextObject(event.current_buffer.document.get_column_cursor_position(event.arg - 1)) 1487 1488 @text_object('g', 'g') 1489 def _(event): 1490 """ 1491 Implements 'gg', 'cgg', 'ygg' 1492 """ 1493 d = event.current_buffer.document 1494 1495 if event._arg: 1496 # Move to the given line. 1497 return TextObject(d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, type=TextObjectType.LINEWISE) 1498 else: 1499 # Move to the top of the input. 1500 return TextObject(d.get_start_of_document_position(), type=TextObjectType.LINEWISE) 1501 1502 @text_object('g', '_') 1503 def _(event): 1504 """ 1505 Go to last non-blank of line. 1506 'g_', 'cg_', 'yg_', etc.. 1507 """ 1508 return TextObject( 1509 event.current_buffer.document.last_non_blank_of_current_line_position(), type=TextObjectType.INCLUSIVE) 1510 1511 @text_object('g', 'e') 1512 def _(event): 1513 """ 1514 Go to last character of previous word. 1515 'ge', 'cge', 'yge', etc.. 1516 """ 1517 prev_end = event.current_buffer.document.find_previous_word_ending(count=event.arg) 1518 return TextObject(prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE) 1519 1520 @text_object('g', 'E') 1521 def _(event): 1522 """ 1523 Go to last character of previous WORD. 1524 'gE', 'cgE', 'ygE', etc.. 1525 """ 1526 prev_end = event.current_buffer.document.find_previous_word_ending(count=event.arg, WORD=True) 1527 return TextObject(prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE) 1528 1529 @text_object('g', 'm') 1530 def _(event): 1531 """ 1532 Like g0, but half a screenwidth to the right. (Or as much as possible.) 1533 """ 1534 w = event.app.layout.current_window 1535 buff = event.current_buffer 1536 1537 if w and w.render_info: 1538 width = w.render_info.window_width 1539 start = buff.document.get_start_of_line_position(after_whitespace=False) 1540 start += int(min(width / 2, len(buff.document.current_line))) 1541 1542 return TextObject(start, type=TextObjectType.INCLUSIVE) 1543 return TextObject(0) 1544 1545 @text_object('G') 1546 def _(event): 1547 """ 1548 Go to the end of the document. (If no arg has been given.) 1549 """ 1550 buf = event.current_buffer 1551 return TextObject(buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) - 1552 buf.cursor_position, type=TextObjectType.LINEWISE) 1553 1554 # 1555 # *** Other *** 1556 # 1557 1558 @handle('G', filter=has_arg) 1559 def _(event): 1560 """ 1561 If an argument is given, move to this line in the history. (for 1562 example, 15G) 1563 """ 1564 event.current_buffer.go_to_history(event.arg - 1) 1565 1566 for n in '123456789': 1567 @handle(n, filter=vi_navigation_mode|vi_selection_mode|vi_waiting_for_text_object_mode) 1568 def _(event): 1569 """ 1570 Always handle numberics in navigation mode as arg. 1571 """ 1572 event.append_to_arg_count(event.data) 1573 1574 @handle('0', filter=(vi_navigation_mode|vi_selection_mode|vi_waiting_for_text_object_mode) & has_arg) 1575 def _(event): 1576 " Zero when an argument was already give. " 1577 event.append_to_arg_count(event.data) 1578 1579 @handle(Keys.Any, filter=vi_replace_mode) 1580 def _(event): 1581 """ 1582 Insert data at cursor position. 1583 """ 1584 event.current_buffer.insert_text(event.data, overwrite=True) 1585 1586 @handle(Keys.Any, filter=vi_insert_multiple_mode, 1587 save_before=(lambda e: not e.is_repeat)) 1588 def _(event): 1589 """ 1590 Insert data at multiple cursor positions at once. 1591 (Usually a result of pressing 'I' or 'A' in block-selection mode.) 1592 """ 1593 buff = event.current_buffer 1594 original_text = buff.text 1595 1596 # Construct new text. 1597 text = [] 1598 p = 0 1599 1600 for p2 in buff.multiple_cursor_positions: 1601 text.append(original_text[p:p2]) 1602 text.append(event.data) 1603 p = p2 1604 1605 text.append(original_text[p:]) 1606 1607 # Shift all cursor positions. 1608 new_cursor_positions = [ 1609 pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions)] 1610 1611 # Set result. 1612 buff.text = ''.join(text) 1613 buff.multiple_cursor_positions = new_cursor_positions 1614 buff.cursor_position += 1 1615 1616 @handle('backspace', filter=vi_insert_multiple_mode) 1617 def _(event): 1618 " Backspace, using multiple cursors. " 1619 buff = event.current_buffer 1620 original_text = buff.text 1621 1622 # Construct new text. 1623 deleted_something = False 1624 text = [] 1625 p = 0 1626 1627 for p2 in buff.multiple_cursor_positions: 1628 if p2 > 0 and original_text[p2 - 1] != '\n': # Don't delete across lines. 1629 text.append(original_text[p:p2 - 1]) 1630 deleted_something = True 1631 else: 1632 text.append(original_text[p:p2]) 1633 p = p2 1634 1635 text.append(original_text[p:]) 1636 1637 if deleted_something: 1638 # Shift all cursor positions. 1639 lengths = [len(part) for part in text[:-1]] 1640 new_cursor_positions = list(accumulate(lengths)) 1641 1642 # Set result. 1643 buff.text = ''.join(text) 1644 buff.multiple_cursor_positions = new_cursor_positions 1645 buff.cursor_position -= 1 1646 else: 1647 event.app.output.bell() 1648 1649 @handle('delete', filter=vi_insert_multiple_mode) 1650 def _(event): 1651 " Delete, using multiple cursors. " 1652 buff = event.current_buffer 1653 original_text = buff.text 1654 1655 # Construct new text. 1656 deleted_something = False 1657 text = [] 1658 new_cursor_positions = [] 1659 p = 0 1660 1661 for p2 in buff.multiple_cursor_positions: 1662 text.append(original_text[p:p2]) 1663 if p2 >= len(original_text) or original_text[p2] == '\n': 1664 # Don't delete across lines. 1665 p = p2 1666 else: 1667 p = p2 + 1 1668 deleted_something = True 1669 1670 text.append(original_text[p:]) 1671 1672 if deleted_something: 1673 # Shift all cursor positions. 1674 lengths = [len(part) for part in text[:-1]] 1675 new_cursor_positions = list(accumulate(lengths)) 1676 1677 # Set result. 1678 buff.text = ''.join(text) 1679 buff.multiple_cursor_positions = new_cursor_positions 1680 else: 1681 event.app.output.bell() 1682 1683 @handle('left', filter=vi_insert_multiple_mode) 1684 def _(event): 1685 """ 1686 Move all cursors to the left. 1687 (But keep all cursors on the same line.) 1688 """ 1689 buff = event.current_buffer 1690 new_positions = [] 1691 1692 for p in buff.multiple_cursor_positions: 1693 if buff.document.translate_index_to_position(p)[1] > 0: 1694 p -= 1 1695 new_positions.append(p) 1696 1697 buff.multiple_cursor_positions = new_positions 1698 1699 if buff.document.cursor_position_col > 0: 1700 buff.cursor_position -= 1 1701 1702 @handle('right', filter=vi_insert_multiple_mode) 1703 def _(event): 1704 """ 1705 Move all cursors to the right. 1706 (But keep all cursors on the same line.) 1707 """ 1708 buff = event.current_buffer 1709 new_positions = [] 1710 1711 for p in buff.multiple_cursor_positions: 1712 row, column = buff.document.translate_index_to_position(p) 1713 if column < len(buff.document.lines[row]): 1714 p += 1 1715 new_positions.append(p) 1716 1717 buff.multiple_cursor_positions = new_positions 1718 1719 if not buff.document.is_cursor_at_the_end_of_line: 1720 buff.cursor_position += 1 1721 1722 @handle('up', filter=vi_insert_multiple_mode) 1723 @handle('down', filter=vi_insert_multiple_mode) 1724 def _(event): 1725 " Ignore all up/down key presses when in multiple cursor mode. " 1726 1727 @handle('c-x', 'c-l', filter=vi_insert_mode) 1728 def _(event): 1729 """ 1730 Pressing the ControlX - ControlL sequence in Vi mode does line 1731 completion based on the other lines in the document and the history. 1732 """ 1733 event.current_buffer.start_history_lines_completion() 1734 1735 @handle('c-x', 'c-f', filter=vi_insert_mode) 1736 def _(event): 1737 """ 1738 Complete file names. 1739 """ 1740 # TODO 1741 pass 1742 1743 @handle('c-k', filter=vi_insert_mode|vi_replace_mode) 1744 def _(event): 1745 " Go into digraph mode. " 1746 event.app.vi_state.waiting_for_digraph = True 1747 1748 @Condition 1749 def digraph_symbol_1_given(): 1750 return get_app().vi_state.digraph_symbol1 is not None 1751 1752 @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) 1753 def _(event): 1754 event.app.vi_state.digraph_symbol1 = event.data 1755 1756 @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) 1757 def _(event): 1758 " Insert digraph. " 1759 try: 1760 # Lookup. 1761 code = (event.app.vi_state.digraph_symbol1, event.data) 1762 if code not in DIGRAPHS: 1763 code = code[::-1] # Try reversing. 1764 symbol = DIGRAPHS[code] 1765 except KeyError: 1766 # Unknown digraph. 1767 event.app.output.bell() 1768 else: 1769 # Insert digraph. 1770 overwrite = event.app.vi_state.input_mode == InputMode.REPLACE 1771 event.current_buffer.insert_text( 1772 six.unichr(symbol), overwrite=overwrite) 1773 event.app.vi_state.waiting_for_digraph = False 1774 finally: 1775 event.app.vi_state.waiting_for_digraph = False 1776 event.app.vi_state.digraph_symbol1 = None 1777 1778 @handle('c-o', filter=vi_insert_mode | vi_replace_mode) 1779 def _(event): 1780 " Go into normal mode for one single action. " 1781 event.app.vi_state.temporary_navigation_mode = True 1782 1783 @handle('q', Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) 1784 def _(event): 1785 " Start recording macro. " 1786 c = event.key_sequence[1].data 1787 if c in vi_register_names: 1788 vi_state = event.app.vi_state 1789 1790 vi_state.recording_register = c 1791 vi_state.current_recording = '' 1792 1793 @handle('q', filter=vi_navigation_mode & vi_recording_macro) 1794 def _(event): 1795 " Stop recording macro. " 1796 vi_state = event.app.vi_state 1797 1798 # Store and stop recording. 1799 vi_state.named_registers[vi_state.recording_register] = ClipboardData(vi_state.current_recording) 1800 vi_state.recording_register = None 1801 vi_state.current_recording = '' 1802 1803 @handle('@', Keys.Any, filter=vi_navigation_mode, record_in_macro=False) 1804 def _(event): 1805 """ 1806 Execute macro. 1807 1808 Notice that we pass `record_in_macro=False`. This ensures that the `@x` 1809 keys don't appear in the recording itself. This function inserts the 1810 body of the called macro back into the KeyProcessor, so these keys will 1811 be added later on to the macro of their handlers have 1812 `record_in_macro=True`. 1813 """ 1814 # Retrieve macro. 1815 c = event.key_sequence[1].data 1816 try: 1817 macro = event.app.vi_state.named_registers[c] 1818 except KeyError: 1819 return 1820 1821 # Expand macro (which is a string in the register), in individual keys. 1822 # Use vt100 parser for this. 1823 keys = [] 1824 1825 parser = Vt100Parser(keys.append) 1826 parser.feed(macro.text) 1827 parser.flush() 1828 1829 # Now feed keys back to the input processor. 1830 for _ in range(event.arg): 1831 event.app.key_processor.feed_multiple(keys, first=True) 1832 1833 return ConditionalKeyBindings(key_bindings, vi_mode) 1834 1835 1836def load_vi_search_bindings(): 1837 key_bindings = KeyBindings() 1838 handle = key_bindings.add 1839 from . import search 1840 1841 @Condition 1842 def search_buffer_is_empty(): 1843 " Returns True when the search buffer is empty. " 1844 return get_app().current_buffer.text == '' 1845 1846 # Vi-style forward search. 1847 handle('/', filter=(vi_navigation_mode|vi_selection_mode)&~vi_search_direction_reversed) \ 1848 (search.start_forward_incremental_search) 1849 handle('?', filter=(vi_navigation_mode|vi_selection_mode)&vi_search_direction_reversed) \ 1850 (search.start_forward_incremental_search) 1851 handle('c-s')(search.start_forward_incremental_search) 1852 1853 # Vi-style backward search. 1854 handle('?', filter=(vi_navigation_mode|vi_selection_mode)&~vi_search_direction_reversed) \ 1855 (search.start_reverse_incremental_search) 1856 handle('/', filter=(vi_navigation_mode|vi_selection_mode)&vi_search_direction_reversed) \ 1857 (search.start_reverse_incremental_search) 1858 handle('c-r')(search.start_reverse_incremental_search) 1859 1860 # Apply the search. (At the / or ? prompt.) 1861 handle('enter', filter=is_searching)(search.accept_search) 1862 1863 handle('c-r', filter=is_searching)(search.reverse_incremental_search) 1864 handle('c-s', filter=is_searching)(search.forward_incremental_search) 1865 1866 handle('c-c')(search.abort_search) 1867 handle('c-g')(search.abort_search) 1868 handle('backspace', filter=search_buffer_is_empty)(search.abort_search) 1869 1870 # Handle escape. This should accept the search, just like readline. 1871 # `abort_search` would be a meaningful alternative. 1872 handle('escape')(search.accept_search) 1873 1874 return ConditionalKeyBindings(key_bindings, vi_mode) 1875