Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/key_binding/bindings/vi.py: 5%
954 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
1# pylint: disable=function-redefined
2from __future__ import annotations
4import codecs
5import string
6from enum import Enum
7from itertools import accumulate
8from typing import Callable, Iterable, List, Optional, Tuple, TypeVar, Union
10from prompt_toolkit.application.current import get_app
11from prompt_toolkit.buffer import Buffer, 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 Filter,
18 has_arg,
19 is_read_only,
20 is_searching,
21)
22from prompt_toolkit.filters.app import (
23 in_paste_mode,
24 is_multiline,
25 vi_digraph_mode,
26 vi_insert_mode,
27 vi_insert_multiple_mode,
28 vi_mode,
29 vi_navigation_mode,
30 vi_recording_macro,
31 vi_replace_mode,
32 vi_replace_single_mode,
33 vi_search_direction_reversed,
34 vi_selection_mode,
35 vi_waiting_for_text_object_mode,
36)
37from prompt_toolkit.input.vt100_parser import Vt100Parser
38from prompt_toolkit.key_binding.digraphs import DIGRAPHS
39from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
40from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode
41from prompt_toolkit.keys import Keys
42from prompt_toolkit.search import SearchDirection
43from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType
45from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase
46from .named_commands import get_by_name
48__all__ = [
49 "load_vi_bindings",
50 "load_vi_search_bindings",
51]
53E = KeyPressEvent
55ascii_lowercase = string.ascii_lowercase
57vi_register_names = ascii_lowercase + "0123456789"
60class TextObjectType(Enum):
61 EXCLUSIVE = "EXCLUSIVE"
62 INCLUSIVE = "INCLUSIVE"
63 LINEWISE = "LINEWISE"
64 BLOCK = "BLOCK"
67class TextObject:
68 """
69 Return struct for functions wrapped in ``text_object``.
70 Both `start` and `end` are relative to the current cursor position.
71 """
73 def __init__(
74 self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE
75 ):
76 self.start = start
77 self.end = end
78 self.type = type
80 @property
81 def selection_type(self) -> SelectionType:
82 if self.type == TextObjectType.LINEWISE:
83 return SelectionType.LINES
84 if self.type == TextObjectType.BLOCK:
85 return SelectionType.BLOCK
86 else:
87 return SelectionType.CHARACTERS
89 def sorted(self) -> tuple[int, int]:
90 """
91 Return a (start, end) tuple where start <= end.
92 """
93 if self.start < self.end:
94 return self.start, self.end
95 else:
96 return self.end, self.start
98 def operator_range(self, document: Document) -> tuple[int, int]:
99 """
100 Return a (start, end) tuple with start <= end that indicates the range
101 operators should operate on.
102 `buffer` is used to get start and end of line positions.
104 This should return something that can be used in a slice, so the `end`
105 position is *not* included.
106 """
107 start, end = self.sorted()
108 doc = document
110 if (
111 self.type == TextObjectType.EXCLUSIVE
112 and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0
113 ):
114 # If the motion is exclusive and the end of motion is on the first
115 # column, the end position becomes end of previous line.
116 end -= 1
117 if self.type == TextObjectType.INCLUSIVE:
118 end += 1
119 if self.type == TextObjectType.LINEWISE:
120 # Select whole lines
121 row, col = doc.translate_index_to_position(start + doc.cursor_position)
122 start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position
123 row, col = doc.translate_index_to_position(end + doc.cursor_position)
124 end = (
125 doc.translate_row_col_to_index(row, len(doc.lines[row]))
126 - doc.cursor_position
127 )
128 return start, end
130 def get_line_numbers(self, buffer: Buffer) -> tuple[int, int]:
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
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)
143 return from_, to
145 def cut(self, buffer: Buffer) -> tuple[Document, ClipboardData]:
146 """
147 Turn text object into `ClipboardData` instance.
148 """
149 from_, to = self.operator_range(buffer.document)
151 from_ += buffer.cursor_position
152 to += buffer.cursor_position
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
161 document = Document(
162 buffer.text,
163 to,
164 SelectionState(original_cursor_position=from_, type=self.selection_type),
165 )
167 new_document, clipboard_data = document.cut_selection()
168 return new_document, clipboard_data
171# Typevar for any text object function:
172TextObjectFunction = Callable[[E], TextObject]
173_TOF = TypeVar("_TOF", bound=TextObjectFunction)
176def create_text_object_decorator(
177 key_bindings: KeyBindings,
178) -> Callable[..., Callable[[_TOF], _TOF]]:
179 """
180 Create a decorator that can be used to register Vi text object implementations.
181 """
183 def text_object_decorator(
184 *keys: Keys | str,
185 filter: Filter = Always(),
186 no_move_handler: bool = False,
187 no_selection_handler: bool = False,
188 eager: bool = False,
189 ) -> Callable[[_TOF], _TOF]:
190 """
191 Register a text object function.
193 Usage::
195 @text_object('w', filter=..., no_move_handler=False)
196 def handler(event):
197 # Return a text object for this key.
198 return TextObject(...)
200 :param no_move_handler: Disable the move handler in navigation mode.
201 (It's still active in selection mode.)
202 """
204 def decorator(text_object_func: _TOF) -> _TOF:
205 @key_bindings.add(
206 *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager
207 )
208 def _apply_operator_to_text_object(event: E) -> None:
209 # Arguments are multiplied.
210 vi_state = event.app.vi_state
211 event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1))
213 # Call the text object handler.
214 text_obj = text_object_func(event)
216 # Get the operator function.
217 # (Should never be None here, given the
218 # `vi_waiting_for_text_object_mode` filter state.)
219 operator_func = vi_state.operator_func
221 if text_obj is not None and operator_func is not None:
222 # Call the operator function with the text object.
223 operator_func(event, text_obj)
225 # Clear operator.
226 event.app.vi_state.operator_func = None
227 event.app.vi_state.operator_arg = None
229 # Register a move operation. (Doesn't need an operator.)
230 if not no_move_handler:
232 @key_bindings.add(
233 *keys,
234 filter=~vi_waiting_for_text_object_mode
235 & filter
236 & vi_navigation_mode,
237 eager=eager,
238 )
239 def _move_in_navigation_mode(event: E) -> None:
240 """
241 Move handler for navigation mode.
242 """
243 text_object = text_object_func(event)
244 event.current_buffer.cursor_position += text_object.start
246 # Register a move selection operation.
247 if not no_selection_handler:
249 @key_bindings.add(
250 *keys,
251 filter=~vi_waiting_for_text_object_mode
252 & filter
253 & vi_selection_mode,
254 eager=eager,
255 )
256 def _move_in_selection_mode(event: E) -> None:
257 """
258 Move handler for selection mode.
259 """
260 text_object = text_object_func(event)
261 buff = event.current_buffer
262 selection_state = buff.selection_state
264 if selection_state is None:
265 return # Should not happen, because of the `vi_selection_mode` filter.
267 # When the text object has both a start and end position, like 'i(' or 'iw',
268 # Turn this into a selection, otherwise the cursor.
269 if text_object.end:
270 # Take selection positions from text object.
271 start, end = text_object.operator_range(buff.document)
272 start += buff.cursor_position
273 end += buff.cursor_position
275 selection_state.original_cursor_position = start
276 buff.cursor_position = end
278 # Take selection type from text object.
279 if text_object.type == TextObjectType.LINEWISE:
280 selection_state.type = SelectionType.LINES
281 else:
282 selection_state.type = SelectionType.CHARACTERS
283 else:
284 event.current_buffer.cursor_position += text_object.start
286 # Make it possible to chain @text_object decorators.
287 return text_object_func
289 return decorator
291 return text_object_decorator
294# Typevar for any operator function:
295OperatorFunction = Callable[[E, TextObject], None]
296_OF = TypeVar("_OF", bound=OperatorFunction)
299def create_operator_decorator(
300 key_bindings: KeyBindings,
301) -> Callable[..., Callable[[_OF], _OF]]:
302 """
303 Create a decorator that can be used for registering Vi operators.
304 """
306 def operator_decorator(
307 *keys: Keys | str, filter: Filter = Always(), eager: bool = False
308 ) -> Callable[[_OF], _OF]:
309 """
310 Register a Vi operator.
312 Usage::
314 @operator('d', filter=...)
315 def handler(event, text_object):
316 # Do something with the text object here.
317 """
319 def decorator(operator_func: _OF) -> _OF:
320 @key_bindings.add(
321 *keys,
322 filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode,
323 eager=eager,
324 )
325 def _operator_in_navigation(event: E) -> None:
326 """
327 Handle operator in navigation mode.
328 """
329 # When this key binding is matched, only set the operator
330 # function in the ViState. We should execute it after a text
331 # object has been received.
332 event.app.vi_state.operator_func = operator_func
333 event.app.vi_state.operator_arg = event.arg
335 @key_bindings.add(
336 *keys,
337 filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode,
338 eager=eager,
339 )
340 def _operator_in_selection(event: E) -> None:
341 """
342 Handle operator in selection mode.
343 """
344 buff = event.current_buffer
345 selection_state = buff.selection_state
347 if selection_state is not None:
348 # Create text object from selection.
349 if selection_state.type == SelectionType.LINES:
350 text_obj_type = TextObjectType.LINEWISE
351 elif selection_state.type == SelectionType.BLOCK:
352 text_obj_type = TextObjectType.BLOCK
353 else:
354 text_obj_type = TextObjectType.INCLUSIVE
356 text_object = TextObject(
357 selection_state.original_cursor_position - buff.cursor_position,
358 type=text_obj_type,
359 )
361 # Execute operator.
362 operator_func(event, text_object)
364 # Quit selection mode.
365 buff.selection_state = None
367 return operator_func
369 return decorator
371 return operator_decorator
374def load_vi_bindings() -> KeyBindingsBase:
375 """
376 Vi extensions.
378 # Overview of Readline Vi commands:
379 # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf
380 """
381 # Note: Some key bindings have the "~IsReadOnly()" filter added. This
382 # prevents the handler to be executed when the focus is on a
383 # read-only buffer.
384 # This is however only required for those that change the ViState to
385 # INSERT mode. The `Buffer` class itself throws the
386 # `EditReadOnlyBuffer` exception for any text operations which is
387 # handled correctly. There is no need to add "~IsReadOnly" to all key
388 # bindings that do text manipulation.
390 key_bindings = KeyBindings()
391 handle = key_bindings.add
393 # (Note: Always take the navigation bindings in read-only mode, even when
394 # ViState says different.)
396 TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]]
398 vi_transform_functions: list[TransformFunction] = [
399 # Rot 13 transformation
400 (
401 ("g", "?"),
402 Always(),
403 lambda string: codecs.encode(string, "rot_13"),
404 ),
405 # To lowercase
406 (("g", "u"), Always(), lambda string: string.lower()),
407 # To uppercase.
408 (("g", "U"), Always(), lambda string: string.upper()),
409 # Swap case.
410 (("g", "~"), Always(), lambda string: string.swapcase()),
411 (
412 ("~",),
413 Condition(lambda: get_app().vi_state.tilde_operator),
414 lambda string: string.swapcase(),
415 ),
416 ]
418 # Insert a character literally (quoted insert).
419 handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert"))
421 @handle("escape")
422 def _back_to_navigation(event: E) -> None:
423 """
424 Escape goes to vi navigation mode.
425 """
426 buffer = event.current_buffer
427 vi_state = event.app.vi_state
429 if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE):
430 buffer.cursor_position += buffer.document.get_cursor_left_position()
432 vi_state.input_mode = InputMode.NAVIGATION
434 if bool(buffer.selection_state):
435 buffer.exit_selection()
437 @handle("k", filter=vi_selection_mode)
438 def _up_in_selection(event: E) -> None:
439 """
440 Arrow up in selection mode.
441 """
442 event.current_buffer.cursor_up(count=event.arg)
444 @handle("j", filter=vi_selection_mode)
445 def _down_in_selection(event: E) -> None:
446 """
447 Arrow down in selection mode.
448 """
449 event.current_buffer.cursor_down(count=event.arg)
451 @handle("up", filter=vi_navigation_mode)
452 @handle("c-p", filter=vi_navigation_mode)
453 def _up_in_navigation(event: E) -> None:
454 """
455 Arrow up and ControlP in navigation mode go up.
456 """
457 event.current_buffer.auto_up(count=event.arg)
459 @handle("k", filter=vi_navigation_mode)
460 def _go_up(event: E) -> None:
461 """
462 Go up, but if we enter a new history entry, move to the start of the
463 line.
464 """
465 event.current_buffer.auto_up(
466 count=event.arg, go_to_start_of_line_if_history_changes=True
467 )
469 @handle("down", filter=vi_navigation_mode)
470 @handle("c-n", filter=vi_navigation_mode)
471 def _go_down(event: E) -> None:
472 """
473 Arrow down and Control-N in navigation mode.
474 """
475 event.current_buffer.auto_down(count=event.arg)
477 @handle("j", filter=vi_navigation_mode)
478 def _go_down2(event: E) -> None:
479 """
480 Go down, but if we enter a new history entry, go to the start of the line.
481 """
482 event.current_buffer.auto_down(
483 count=event.arg, go_to_start_of_line_if_history_changes=True
484 )
486 @handle("backspace", filter=vi_navigation_mode)
487 def _go_left(event: E) -> None:
488 """
489 In navigation-mode, move cursor.
490 """
491 event.current_buffer.cursor_position += (
492 event.current_buffer.document.get_cursor_left_position(count=event.arg)
493 )
495 @handle("c-n", filter=vi_insert_mode)
496 def _complete_next(event: E) -> None:
497 b = event.current_buffer
499 if b.complete_state:
500 b.complete_next()
501 else:
502 b.start_completion(select_first=True)
504 @handle("c-p", filter=vi_insert_mode)
505 def _complete_prev(event: E) -> None:
506 """
507 Control-P: To previous completion.
508 """
509 b = event.current_buffer
511 if b.complete_state:
512 b.complete_previous()
513 else:
514 b.start_completion(select_last=True)
516 @handle("c-g", filter=vi_insert_mode)
517 @handle("c-y", filter=vi_insert_mode)
518 def _accept_completion(event: E) -> None:
519 """
520 Accept current completion.
521 """
522 event.current_buffer.complete_state = None
524 @handle("c-e", filter=vi_insert_mode)
525 def _cancel_completion(event: E) -> None:
526 """
527 Cancel completion. Go back to originally typed text.
528 """
529 event.current_buffer.cancel_completion()
531 @Condition
532 def is_returnable() -> bool:
533 return get_app().current_buffer.is_returnable
535 # In navigation mode, pressing enter will always return the input.
536 handle("enter", filter=vi_navigation_mode & is_returnable)(
537 get_by_name("accept-line")
538 )
540 # In insert mode, also accept input when enter is pressed, and the buffer
541 # has been marked as single line.
542 handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line"))
544 @handle("enter", filter=~is_returnable & vi_navigation_mode)
545 def _start_of_next_line(event: E) -> None:
546 """
547 Go to the beginning of next line.
548 """
549 b = event.current_buffer
550 b.cursor_down(count=event.arg)
551 b.cursor_position += b.document.get_start_of_line_position(
552 after_whitespace=True
553 )
555 # ** In navigation mode **
557 # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html
559 @handle("insert", filter=vi_navigation_mode)
560 def _insert_mode(event: E) -> None:
561 """
562 Pressing the Insert key.
563 """
564 event.app.vi_state.input_mode = InputMode.INSERT
566 @handle("insert", filter=vi_insert_mode)
567 def _navigation_mode(event: E) -> None:
568 """
569 Pressing the Insert key.
570 """
571 event.app.vi_state.input_mode = InputMode.NAVIGATION
573 @handle("a", filter=vi_navigation_mode & ~is_read_only)
574 # ~IsReadOnly, because we want to stay in navigation mode for
575 # read-only buffers.
576 def _a(event: E) -> None:
577 event.current_buffer.cursor_position += (
578 event.current_buffer.document.get_cursor_right_position()
579 )
580 event.app.vi_state.input_mode = InputMode.INSERT
582 @handle("A", filter=vi_navigation_mode & ~is_read_only)
583 def _A(event: E) -> None:
584 event.current_buffer.cursor_position += (
585 event.current_buffer.document.get_end_of_line_position()
586 )
587 event.app.vi_state.input_mode = InputMode.INSERT
589 @handle("C", filter=vi_navigation_mode & ~is_read_only)
590 def _change_until_end_of_line(event: E) -> None:
591 """
592 Change to end of line.
593 Same as 'c$' (which is implemented elsewhere.)
594 """
595 buffer = event.current_buffer
597 deleted = buffer.delete(count=buffer.document.get_end_of_line_position())
598 event.app.clipboard.set_text(deleted)
599 event.app.vi_state.input_mode = InputMode.INSERT
601 @handle("c", "c", filter=vi_navigation_mode & ~is_read_only)
602 @handle("S", filter=vi_navigation_mode & ~is_read_only)
603 def _change_current_line(event: E) -> None: # TODO: implement 'arg'
604 """
605 Change current line
606 """
607 buffer = event.current_buffer
609 # We copy the whole line.
610 data = ClipboardData(buffer.document.current_line, SelectionType.LINES)
611 event.app.clipboard.set_data(data)
613 # But we delete after the whitespace
614 buffer.cursor_position += buffer.document.get_start_of_line_position(
615 after_whitespace=True
616 )
617 buffer.delete(count=buffer.document.get_end_of_line_position())
618 event.app.vi_state.input_mode = InputMode.INSERT
620 @handle("D", filter=vi_navigation_mode)
621 def _delete_until_end_of_line(event: E) -> None:
622 """
623 Delete from cursor position until the end of the line.
624 """
625 buffer = event.current_buffer
626 deleted = buffer.delete(count=buffer.document.get_end_of_line_position())
627 event.app.clipboard.set_text(deleted)
629 @handle("d", "d", filter=vi_navigation_mode)
630 def _delete_line(event: E) -> None:
631 """
632 Delete line. (Or the following 'n' lines.)
633 """
634 buffer = event.current_buffer
636 # Split string in before/deleted/after text.
637 lines = buffer.document.lines
639 before = "\n".join(lines[: buffer.document.cursor_position_row])
640 deleted = "\n".join(
641 lines[
642 buffer.document.cursor_position_row : buffer.document.cursor_position_row
643 + event.arg
644 ]
645 )
646 after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :])
648 # Set new text.
649 if before and after:
650 before = before + "\n"
652 # Set text and cursor position.
653 buffer.document = Document(
654 text=before + after,
655 # Cursor At the start of the first 'after' line, after the leading whitespace.
656 cursor_position=len(before) + len(after) - len(after.lstrip(" ")),
657 )
659 # Set clipboard data
660 event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES))
662 @handle("x", filter=vi_selection_mode)
663 def _cut(event: E) -> None:
664 """
665 Cut selection.
666 ('x' is not an operator.)
667 """
668 clipboard_data = event.current_buffer.cut_selection()
669 event.app.clipboard.set_data(clipboard_data)
671 @handle("i", filter=vi_navigation_mode & ~is_read_only)
672 def _i(event: E) -> None:
673 event.app.vi_state.input_mode = InputMode.INSERT
675 @handle("I", filter=vi_navigation_mode & ~is_read_only)
676 def _I(event: E) -> None:
677 event.app.vi_state.input_mode = InputMode.INSERT
678 event.current_buffer.cursor_position += (
679 event.current_buffer.document.get_start_of_line_position(
680 after_whitespace=True
681 )
682 )
684 @Condition
685 def in_block_selection() -> bool:
686 buff = get_app().current_buffer
687 return bool(
688 buff.selection_state and buff.selection_state.type == SelectionType.BLOCK
689 )
691 @handle("I", filter=in_block_selection & ~is_read_only)
692 def insert_in_block_selection(event: E, after: bool = False) -> None:
693 """
694 Insert in block selection mode.
695 """
696 buff = event.current_buffer
698 # Store all cursor positions.
699 positions = []
701 if after:
703 def get_pos(from_to: tuple[int, int]) -> int:
704 return from_to[1]
706 else:
708 def get_pos(from_to: tuple[int, int]) -> int:
709 return from_to[0]
711 for i, from_to in enumerate(buff.document.selection_ranges()):
712 positions.append(get_pos(from_to))
713 if i == 0:
714 buff.cursor_position = get_pos(from_to)
716 buff.multiple_cursor_positions = positions
718 # Go to 'INSERT_MULTIPLE' mode.
719 event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE
720 buff.exit_selection()
722 @handle("A", filter=in_block_selection & ~is_read_only)
723 def _append_after_block(event: E) -> None:
724 insert_in_block_selection(event, after=True)
726 @handle("J", filter=vi_navigation_mode & ~is_read_only)
727 def _join(event: E) -> None:
728 """
729 Join lines.
730 """
731 for i in range(event.arg):
732 event.current_buffer.join_next_line()
734 @handle("g", "J", filter=vi_navigation_mode & ~is_read_only)
735 def _join_nospace(event: E) -> None:
736 """
737 Join lines without space.
738 """
739 for i in range(event.arg):
740 event.current_buffer.join_next_line(separator="")
742 @handle("J", filter=vi_selection_mode & ~is_read_only)
743 def _join_selection(event: E) -> None:
744 """
745 Join selected lines.
746 """
747 event.current_buffer.join_selected_lines()
749 @handle("g", "J", filter=vi_selection_mode & ~is_read_only)
750 def _join_selection_nospace(event: E) -> None:
751 """
752 Join selected lines without space.
753 """
754 event.current_buffer.join_selected_lines(separator="")
756 @handle("p", filter=vi_navigation_mode)
757 def _paste(event: E) -> None:
758 """
759 Paste after
760 """
761 event.current_buffer.paste_clipboard_data(
762 event.app.clipboard.get_data(),
763 count=event.arg,
764 paste_mode=PasteMode.VI_AFTER,
765 )
767 @handle("P", filter=vi_navigation_mode)
768 def _paste_before(event: E) -> None:
769 """
770 Paste before
771 """
772 event.current_buffer.paste_clipboard_data(
773 event.app.clipboard.get_data(),
774 count=event.arg,
775 paste_mode=PasteMode.VI_BEFORE,
776 )
778 @handle('"', Keys.Any, "p", filter=vi_navigation_mode)
779 def _paste_register(event: E) -> None:
780 """
781 Paste from named register.
782 """
783 c = event.key_sequence[1].data
784 if c in vi_register_names:
785 data = event.app.vi_state.named_registers.get(c)
786 if data:
787 event.current_buffer.paste_clipboard_data(
788 data, count=event.arg, paste_mode=PasteMode.VI_AFTER
789 )
791 @handle('"', Keys.Any, "P", filter=vi_navigation_mode)
792 def _paste_register_before(event: E) -> None:
793 """
794 Paste (before) from named register.
795 """
796 c = event.key_sequence[1].data
797 if c in vi_register_names:
798 data = event.app.vi_state.named_registers.get(c)
799 if data:
800 event.current_buffer.paste_clipboard_data(
801 data, count=event.arg, paste_mode=PasteMode.VI_BEFORE
802 )
804 @handle("r", filter=vi_navigation_mode)
805 def _replace(event: E) -> None:
806 """
807 Go to 'replace-single'-mode.
808 """
809 event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE
811 @handle("R", filter=vi_navigation_mode)
812 def _replace_mode(event: E) -> None:
813 """
814 Go to 'replace'-mode.
815 """
816 event.app.vi_state.input_mode = InputMode.REPLACE
818 @handle("s", filter=vi_navigation_mode & ~is_read_only)
819 def _substitute(event: E) -> None:
820 """
821 Substitute with new text
822 (Delete character(s) and go to insert mode.)
823 """
824 text = event.current_buffer.delete(count=event.arg)
825 event.app.clipboard.set_text(text)
826 event.app.vi_state.input_mode = InputMode.INSERT
828 @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False))
829 def _undo(event: E) -> None:
830 for i in range(event.arg):
831 event.current_buffer.undo()
833 @handle("V", filter=vi_navigation_mode)
834 def _visual_line(event: E) -> None:
835 """
836 Start lines selection.
837 """
838 event.current_buffer.start_selection(selection_type=SelectionType.LINES)
840 @handle("c-v", filter=vi_navigation_mode)
841 def _visual_block(event: E) -> None:
842 """
843 Enter block selection mode.
844 """
845 event.current_buffer.start_selection(selection_type=SelectionType.BLOCK)
847 @handle("V", filter=vi_selection_mode)
848 def _visual_line2(event: E) -> None:
849 """
850 Exit line selection mode, or go from non line selection mode to line
851 selection mode.
852 """
853 selection_state = event.current_buffer.selection_state
855 if selection_state is not None:
856 if selection_state.type != SelectionType.LINES:
857 selection_state.type = SelectionType.LINES
858 else:
859 event.current_buffer.exit_selection()
861 @handle("v", filter=vi_navigation_mode)
862 def _visual(event: E) -> None:
863 """
864 Enter character selection mode.
865 """
866 event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS)
868 @handle("v", filter=vi_selection_mode)
869 def _visual2(event: E) -> None:
870 """
871 Exit character selection mode, or go from non-character-selection mode
872 to character selection mode.
873 """
874 selection_state = event.current_buffer.selection_state
876 if selection_state is not None:
877 if selection_state.type != SelectionType.CHARACTERS:
878 selection_state.type = SelectionType.CHARACTERS
879 else:
880 event.current_buffer.exit_selection()
882 @handle("c-v", filter=vi_selection_mode)
883 def _visual_block2(event: E) -> None:
884 """
885 Exit block selection mode, or go from non block selection mode to block
886 selection mode.
887 """
888 selection_state = event.current_buffer.selection_state
890 if selection_state is not None:
891 if selection_state.type != SelectionType.BLOCK:
892 selection_state.type = SelectionType.BLOCK
893 else:
894 event.current_buffer.exit_selection()
896 @handle("a", "w", filter=vi_selection_mode)
897 @handle("a", "W", filter=vi_selection_mode)
898 def _visual_auto_word(event: E) -> None:
899 """
900 Switch from visual linewise mode to visual characterwise mode.
901 """
902 buffer = event.current_buffer
904 if (
905 buffer.selection_state
906 and buffer.selection_state.type == SelectionType.LINES
907 ):
908 buffer.selection_state.type = SelectionType.CHARACTERS
910 @handle("x", filter=vi_navigation_mode)
911 def _delete(event: E) -> None:
912 """
913 Delete character.
914 """
915 buff = event.current_buffer
916 count = min(event.arg, len(buff.document.current_line_after_cursor))
917 if count:
918 text = event.current_buffer.delete(count=count)
919 event.app.clipboard.set_text(text)
921 @handle("X", filter=vi_navigation_mode)
922 def _delete_before_cursor(event: E) -> None:
923 buff = event.current_buffer
924 count = min(event.arg, len(buff.document.current_line_before_cursor))
925 if count:
926 text = event.current_buffer.delete_before_cursor(count=count)
927 event.app.clipboard.set_text(text)
929 @handle("y", "y", filter=vi_navigation_mode)
930 @handle("Y", filter=vi_navigation_mode)
931 def _yank_line(event: E) -> None:
932 """
933 Yank the whole line.
934 """
935 text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg])
936 event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES))
938 @handle("+", filter=vi_navigation_mode)
939 def _next_line(event: E) -> None:
940 """
941 Move to first non whitespace of next line
942 """
943 buffer = event.current_buffer
944 buffer.cursor_position += buffer.document.get_cursor_down_position(
945 count=event.arg
946 )
947 buffer.cursor_position += buffer.document.get_start_of_line_position(
948 after_whitespace=True
949 )
951 @handle("-", filter=vi_navigation_mode)
952 def _prev_line(event: E) -> None:
953 """
954 Move to first non whitespace of previous line
955 """
956 buffer = event.current_buffer
957 buffer.cursor_position += buffer.document.get_cursor_up_position(
958 count=event.arg
959 )
960 buffer.cursor_position += buffer.document.get_start_of_line_position(
961 after_whitespace=True
962 )
964 @handle(">", ">", filter=vi_navigation_mode)
965 def _indent(event: E) -> None:
966 """
967 Indent lines.
968 """
969 buffer = event.current_buffer
970 current_row = buffer.document.cursor_position_row
971 indent(buffer, current_row, current_row + event.arg)
973 @handle("<", "<", filter=vi_navigation_mode)
974 def _unindent(event: E) -> None:
975 """
976 Unindent lines.
977 """
978 current_row = event.current_buffer.document.cursor_position_row
979 unindent(event.current_buffer, current_row, current_row + event.arg)
981 @handle("O", filter=vi_navigation_mode & ~is_read_only)
982 def _open_above(event: E) -> None:
983 """
984 Open line above and enter insertion mode
985 """
986 event.current_buffer.insert_line_above(copy_margin=not in_paste_mode())
987 event.app.vi_state.input_mode = InputMode.INSERT
989 @handle("o", filter=vi_navigation_mode & ~is_read_only)
990 def _open_below(event: E) -> None:
991 """
992 Open line below and enter insertion mode
993 """
994 event.current_buffer.insert_line_below(copy_margin=not in_paste_mode())
995 event.app.vi_state.input_mode = InputMode.INSERT
997 @handle("~", filter=vi_navigation_mode)
998 def _reverse_case(event: E) -> None:
999 """
1000 Reverse case of current character and move cursor forward.
1001 """
1002 buffer = event.current_buffer
1003 c = buffer.document.current_char
1005 if c is not None and c != "\n":
1006 buffer.insert_text(c.swapcase(), overwrite=True)
1008 @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only)
1009 def _lowercase_line(event: E) -> None:
1010 """
1011 Lowercase current line.
1012 """
1013 buff = event.current_buffer
1014 buff.transform_current_line(lambda s: s.lower())
1016 @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only)
1017 def _uppercase_line(event: E) -> None:
1018 """
1019 Uppercase current line.
1020 """
1021 buff = event.current_buffer
1022 buff.transform_current_line(lambda s: s.upper())
1024 @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only)
1025 def _swapcase_line(event: E) -> None:
1026 """
1027 Swap case of the current line.
1028 """
1029 buff = event.current_buffer
1030 buff.transform_current_line(lambda s: s.swapcase())
1032 @handle("#", filter=vi_navigation_mode)
1033 def _prev_occurence(event: E) -> None:
1034 """
1035 Go to previous occurrence of this word.
1036 """
1037 b = event.current_buffer
1038 search_state = event.app.current_search_state
1040 search_state.text = b.document.get_word_under_cursor()
1041 search_state.direction = SearchDirection.BACKWARD
1043 b.apply_search(search_state, count=event.arg, include_current_position=False)
1045 @handle("*", filter=vi_navigation_mode)
1046 def _next_occurance(event: E) -> None:
1047 """
1048 Go to next occurrence of this word.
1049 """
1050 b = event.current_buffer
1051 search_state = event.app.current_search_state
1053 search_state.text = b.document.get_word_under_cursor()
1054 search_state.direction = SearchDirection.FORWARD
1056 b.apply_search(search_state, count=event.arg, include_current_position=False)
1058 @handle("(", filter=vi_navigation_mode)
1059 def _begin_of_sentence(event: E) -> None:
1060 # TODO: go to begin of sentence.
1061 # XXX: should become text_object.
1062 pass
1064 @handle(")", filter=vi_navigation_mode)
1065 def _end_of_sentence(event: E) -> None:
1066 # TODO: go to end of sentence.
1067 # XXX: should become text_object.
1068 pass
1070 operator = create_operator_decorator(key_bindings)
1071 text_object = create_text_object_decorator(key_bindings)
1073 @handle(Keys.Any, filter=vi_waiting_for_text_object_mode)
1074 def _unknown_text_object(event: E) -> None:
1075 """
1076 Unknown key binding while waiting for a text object.
1077 """
1078 event.app.output.bell()
1080 #
1081 # *** Operators ***
1082 #
1084 def create_delete_and_change_operators(
1085 delete_only: bool, with_register: bool = False
1086 ) -> None:
1087 """
1088 Delete and change operators.
1090 :param delete_only: Create an operator that deletes, but doesn't go to insert mode.
1091 :param with_register: Copy the deleted text to this named register instead of the clipboard.
1092 """
1093 handler_keys: Iterable[str]
1094 if with_register:
1095 handler_keys = ('"', Keys.Any, "cd"[delete_only])
1096 else:
1097 handler_keys = "cd"[delete_only]
1099 @operator(*handler_keys, filter=~is_read_only)
1100 def delete_or_change_operator(event: E, text_object: TextObject) -> None:
1101 clipboard_data = None
1102 buff = event.current_buffer
1104 if text_object:
1105 new_document, clipboard_data = text_object.cut(buff)
1106 buff.document = new_document
1108 # Set deleted/changed text to clipboard or named register.
1109 if clipboard_data and clipboard_data.text:
1110 if with_register:
1111 reg_name = event.key_sequence[1].data
1112 if reg_name in vi_register_names:
1113 event.app.vi_state.named_registers[reg_name] = clipboard_data
1114 else:
1115 event.app.clipboard.set_data(clipboard_data)
1117 # Only go back to insert mode in case of 'change'.
1118 if not delete_only:
1119 event.app.vi_state.input_mode = InputMode.INSERT
1121 create_delete_and_change_operators(False, False)
1122 create_delete_and_change_operators(False, True)
1123 create_delete_and_change_operators(True, False)
1124 create_delete_and_change_operators(True, True)
1126 def create_transform_handler(
1127 filter: Filter, transform_func: Callable[[str], str], *a: str
1128 ) -> None:
1129 @operator(*a, filter=filter & ~is_read_only)
1130 def _(event: E, text_object: TextObject) -> None:
1131 """
1132 Apply transformation (uppercase, lowercase, rot13, swap case).
1133 """
1134 buff = event.current_buffer
1135 start, end = text_object.operator_range(buff.document)
1137 if start < end:
1138 # Transform.
1139 buff.transform_region(
1140 buff.cursor_position + start,
1141 buff.cursor_position + end,
1142 transform_func,
1143 )
1145 # Move cursor
1146 buff.cursor_position += text_object.end or text_object.start
1148 for k, f, func in vi_transform_functions:
1149 create_transform_handler(f, func, *k)
1151 @operator("y")
1152 def _yank(event: E, text_object: TextObject) -> None:
1153 """
1154 Yank operator. (Copy text.)
1155 """
1156 _, clipboard_data = text_object.cut(event.current_buffer)
1157 if clipboard_data.text:
1158 event.app.clipboard.set_data(clipboard_data)
1160 @operator('"', Keys.Any, "y")
1161 def _yank_to_register(event: E, text_object: TextObject) -> None:
1162 """
1163 Yank selection to named register.
1164 """
1165 c = event.key_sequence[1].data
1166 if c in vi_register_names:
1167 _, clipboard_data = text_object.cut(event.current_buffer)
1168 event.app.vi_state.named_registers[c] = clipboard_data
1170 @operator(">")
1171 def _indent_text_object(event: E, text_object: TextObject) -> None:
1172 """
1173 Indent.
1174 """
1175 buff = event.current_buffer
1176 from_, to = text_object.get_line_numbers(buff)
1177 indent(buff, from_, to + 1, count=event.arg)
1179 @operator("<")
1180 def _unindent_text_object(event: E, text_object: TextObject) -> None:
1181 """
1182 Unindent.
1183 """
1184 buff = event.current_buffer
1185 from_, to = text_object.get_line_numbers(buff)
1186 unindent(buff, from_, to + 1, count=event.arg)
1188 @operator("g", "q")
1189 def _reshape(event: E, text_object: TextObject) -> None:
1190 """
1191 Reshape text.
1192 """
1193 buff = event.current_buffer
1194 from_, to = text_object.get_line_numbers(buff)
1195 reshape_text(buff, from_, to)
1197 #
1198 # *** Text objects ***
1199 #
1201 @text_object("b")
1202 def _b(event: E) -> TextObject:
1203 """
1204 Move one word or token left.
1205 """
1206 return TextObject(
1207 event.current_buffer.document.find_start_of_previous_word(count=event.arg)
1208 or 0
1209 )
1211 @text_object("B")
1212 def _B(event: E) -> TextObject:
1213 """
1214 Move one non-blank word left
1215 """
1216 return TextObject(
1217 event.current_buffer.document.find_start_of_previous_word(
1218 count=event.arg, WORD=True
1219 )
1220 or 0
1221 )
1223 @text_object("$")
1224 def _dollar(event: E) -> TextObject:
1225 """
1226 'c$', 'd$' and '$': Delete/change/move until end of line.
1227 """
1228 return TextObject(event.current_buffer.document.get_end_of_line_position())
1230 @text_object("w")
1231 def _word_forward(event: E) -> TextObject:
1232 """
1233 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word.
1234 """
1235 return TextObject(
1236 event.current_buffer.document.find_next_word_beginning(count=event.arg)
1237 or event.current_buffer.document.get_end_of_document_position()
1238 )
1240 @text_object("W")
1241 def _WORD_forward(event: E) -> TextObject:
1242 """
1243 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD.
1244 """
1245 return TextObject(
1246 event.current_buffer.document.find_next_word_beginning(
1247 count=event.arg, WORD=True
1248 )
1249 or event.current_buffer.document.get_end_of_document_position()
1250 )
1252 @text_object("e")
1253 def _end_of_word(event: E) -> TextObject:
1254 """
1255 End of 'word': 'ce', 'de', 'e'
1256 """
1257 end = event.current_buffer.document.find_next_word_ending(count=event.arg)
1258 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE)
1260 @text_object("E")
1261 def _end_of_WORD(event: E) -> TextObject:
1262 """
1263 End of 'WORD': 'cE', 'dE', 'E'
1264 """
1265 end = event.current_buffer.document.find_next_word_ending(
1266 count=event.arg, WORD=True
1267 )
1268 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE)
1270 @text_object("i", "w", no_move_handler=True)
1271 def _inner_word(event: E) -> TextObject:
1272 """
1273 Inner 'word': ciw and diw
1274 """
1275 start, end = event.current_buffer.document.find_boundaries_of_current_word()
1276 return TextObject(start, end)
1278 @text_object("a", "w", no_move_handler=True)
1279 def _a_word(event: E) -> TextObject:
1280 """
1281 A 'word': caw and daw
1282 """
1283 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1284 include_trailing_whitespace=True
1285 )
1286 return TextObject(start, end)
1288 @text_object("i", "W", no_move_handler=True)
1289 def _inner_WORD(event: E) -> TextObject:
1290 """
1291 Inner 'WORD': ciW and diW
1292 """
1293 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1294 WORD=True
1295 )
1296 return TextObject(start, end)
1298 @text_object("a", "W", no_move_handler=True)
1299 def _a_WORD(event: E) -> TextObject:
1300 """
1301 A 'WORD': caw and daw
1302 """
1303 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1304 WORD=True, include_trailing_whitespace=True
1305 )
1306 return TextObject(start, end)
1308 @text_object("a", "p", no_move_handler=True)
1309 def _paragraph(event: E) -> TextObject:
1310 """
1311 Auto paragraph.
1312 """
1313 start = event.current_buffer.document.start_of_paragraph()
1314 end = event.current_buffer.document.end_of_paragraph(count=event.arg)
1315 return TextObject(start, end)
1317 @text_object("^")
1318 def _start_of_line(event: E) -> TextObject:
1319 """'c^', 'd^' and '^': Soft start of line, after whitespace."""
1320 return TextObject(
1321 event.current_buffer.document.get_start_of_line_position(
1322 after_whitespace=True
1323 )
1324 )
1326 @text_object("0")
1327 def _hard_start_of_line(event: E) -> TextObject:
1328 """
1329 'c0', 'd0': Hard start of line, before whitespace.
1330 (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.)
1331 """
1332 return TextObject(
1333 event.current_buffer.document.get_start_of_line_position(
1334 after_whitespace=False
1335 )
1336 )
1338 def create_ci_ca_handles(
1339 ci_start: str, ci_end: str, inner: bool, key: str | None = None
1340 ) -> None:
1341 # TODO: 'dat', 'dit', (tags (like xml)
1342 """
1343 Delete/Change string between this start and stop character. But keep these characters.
1344 This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations.
1345 """
1347 def handler(event: E) -> TextObject:
1348 if ci_start == ci_end:
1349 # Quotes
1350 start = event.current_buffer.document.find_backwards(
1351 ci_start, in_current_line=False
1352 )
1353 end = event.current_buffer.document.find(ci_end, in_current_line=False)
1354 else:
1355 # Brackets
1356 start = event.current_buffer.document.find_enclosing_bracket_left(
1357 ci_start, ci_end
1358 )
1359 end = event.current_buffer.document.find_enclosing_bracket_right(
1360 ci_start, ci_end
1361 )
1363 if start is not None and end is not None:
1364 offset = 0 if inner else 1
1365 return TextObject(start + 1 - offset, end + offset)
1366 else:
1367 # Nothing found.
1368 return TextObject(0)
1370 if key is None:
1371 text_object("ai"[inner], ci_start, no_move_handler=True)(handler)
1372 text_object("ai"[inner], ci_end, no_move_handler=True)(handler)
1373 else:
1374 text_object("ai"[inner], key, no_move_handler=True)(handler)
1376 for inner in (False, True):
1377 for ci_start, ci_end in [
1378 ('"', '"'),
1379 ("'", "'"),
1380 ("`", "`"),
1381 ("[", "]"),
1382 ("<", ">"),
1383 ("{", "}"),
1384 ("(", ")"),
1385 ]:
1386 create_ci_ca_handles(ci_start, ci_end, inner)
1388 create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib'
1389 create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB'
1391 @text_object("{")
1392 def _previous_section(event: E) -> TextObject:
1393 """
1394 Move to previous blank-line separated section.
1395 Implements '{', 'c{', 'd{', 'y{'
1396 """
1397 index = event.current_buffer.document.start_of_paragraph(
1398 count=event.arg, before=True
1399 )
1400 return TextObject(index)
1402 @text_object("}")
1403 def _next_section(event: E) -> TextObject:
1404 """
1405 Move to next blank-line separated section.
1406 Implements '}', 'c}', 'd}', 'y}'
1407 """
1408 index = event.current_buffer.document.end_of_paragraph(
1409 count=event.arg, after=True
1410 )
1411 return TextObject(index)
1413 @text_object("f", Keys.Any)
1414 def _next_occurence(event: E) -> TextObject:
1415 """
1416 Go to next occurrence of character. Typing 'fx' will move the
1417 cursor to the next occurrence of character. 'x'.
1418 """
1419 event.app.vi_state.last_character_find = CharacterFind(event.data, False)
1420 match = event.current_buffer.document.find(
1421 event.data, in_current_line=True, count=event.arg
1422 )
1423 if match:
1424 return TextObject(match, type=TextObjectType.INCLUSIVE)
1425 else:
1426 return TextObject(0)
1428 @text_object("F", Keys.Any)
1429 def _previous_occurance(event: E) -> TextObject:
1430 """
1431 Go to previous occurrence of character. Typing 'Fx' will move the
1432 cursor to the previous occurrence of character. 'x'.
1433 """
1434 event.app.vi_state.last_character_find = CharacterFind(event.data, True)
1435 return TextObject(
1436 event.current_buffer.document.find_backwards(
1437 event.data, in_current_line=True, count=event.arg
1438 )
1439 or 0
1440 )
1442 @text_object("t", Keys.Any)
1443 def _t(event: E) -> TextObject:
1444 """
1445 Move right to the next occurrence of c, then one char backward.
1446 """
1447 event.app.vi_state.last_character_find = CharacterFind(event.data, False)
1448 match = event.current_buffer.document.find(
1449 event.data, in_current_line=True, count=event.arg
1450 )
1451 if match:
1452 return TextObject(match - 1, type=TextObjectType.INCLUSIVE)
1453 else:
1454 return TextObject(0)
1456 @text_object("T", Keys.Any)
1457 def _T(event: E) -> TextObject:
1458 """
1459 Move left to the previous occurrence of c, then one char forward.
1460 """
1461 event.app.vi_state.last_character_find = CharacterFind(event.data, True)
1462 match = event.current_buffer.document.find_backwards(
1463 event.data, in_current_line=True, count=event.arg
1464 )
1465 return TextObject(match + 1 if match else 0)
1467 def repeat(reverse: bool) -> None:
1468 """
1469 Create ',' and ';' commands.
1470 """
1472 @text_object("," if reverse else ";")
1473 def _(event: E) -> TextObject:
1474 """
1475 Repeat the last 'f'/'F'/'t'/'T' command.
1476 """
1477 pos: int | None = 0
1478 vi_state = event.app.vi_state
1480 type = TextObjectType.EXCLUSIVE
1482 if vi_state.last_character_find:
1483 char = vi_state.last_character_find.character
1484 backwards = vi_state.last_character_find.backwards
1486 if reverse:
1487 backwards = not backwards
1489 if backwards:
1490 pos = event.current_buffer.document.find_backwards(
1491 char, in_current_line=True, count=event.arg
1492 )
1493 else:
1494 pos = event.current_buffer.document.find(
1495 char, in_current_line=True, count=event.arg
1496 )
1497 type = TextObjectType.INCLUSIVE
1498 if pos:
1499 return TextObject(pos, type=type)
1500 else:
1501 return TextObject(0)
1503 repeat(True)
1504 repeat(False)
1506 @text_object("h")
1507 @text_object("left")
1508 def _left(event: E) -> TextObject:
1509 """
1510 Implements 'ch', 'dh', 'h': Cursor left.
1511 """
1512 return TextObject(
1513 event.current_buffer.document.get_cursor_left_position(count=event.arg)
1514 )
1516 @text_object("j", no_move_handler=True, no_selection_handler=True)
1517 # Note: We also need `no_selection_handler`, because we in
1518 # selection mode, we prefer the other 'j' binding that keeps
1519 # `buffer.preferred_column`.
1520 def _down(event: E) -> TextObject:
1521 """
1522 Implements 'cj', 'dj', 'j', ... Cursor up.
1523 """
1524 return TextObject(
1525 event.current_buffer.document.get_cursor_down_position(count=event.arg),
1526 type=TextObjectType.LINEWISE,
1527 )
1529 @text_object("k", no_move_handler=True, no_selection_handler=True)
1530 def _up(event: E) -> TextObject:
1531 """
1532 Implements 'ck', 'dk', 'k', ... Cursor up.
1533 """
1534 return TextObject(
1535 event.current_buffer.document.get_cursor_up_position(count=event.arg),
1536 type=TextObjectType.LINEWISE,
1537 )
1539 @text_object("l")
1540 @text_object(" ")
1541 @text_object("right")
1542 def _right(event: E) -> TextObject:
1543 """
1544 Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right.
1545 """
1546 return TextObject(
1547 event.current_buffer.document.get_cursor_right_position(count=event.arg)
1548 )
1550 @text_object("H")
1551 def _top_of_screen(event: E) -> TextObject:
1552 """
1553 Moves to the start of the visible region. (Below the scroll offset.)
1554 Implements 'cH', 'dH', 'H'.
1555 """
1556 w = event.app.layout.current_window
1557 b = event.current_buffer
1559 if w and w.render_info:
1560 # When we find a Window that has BufferControl showing this window,
1561 # move to the start of the visible area.
1562 pos = (
1563 b.document.translate_row_col_to_index(
1564 w.render_info.first_visible_line(after_scroll_offset=True), 0
1565 )
1566 - b.cursor_position
1567 )
1569 else:
1570 # Otherwise, move to the start of the input.
1571 pos = -len(b.document.text_before_cursor)
1572 return TextObject(pos, type=TextObjectType.LINEWISE)
1574 @text_object("M")
1575 def _middle_of_screen(event: E) -> TextObject:
1576 """
1577 Moves cursor to the vertical center of the visible region.
1578 Implements 'cM', 'dM', 'M'.
1579 """
1580 w = event.app.layout.current_window
1581 b = event.current_buffer
1583 if w and w.render_info:
1584 # When we find a Window that has BufferControl showing this window,
1585 # move to the center of the visible area.
1586 pos = (
1587 b.document.translate_row_col_to_index(
1588 w.render_info.center_visible_line(), 0
1589 )
1590 - b.cursor_position
1591 )
1593 else:
1594 # Otherwise, move to the start of the input.
1595 pos = -len(b.document.text_before_cursor)
1596 return TextObject(pos, type=TextObjectType.LINEWISE)
1598 @text_object("L")
1599 def _end_of_screen(event: E) -> TextObject:
1600 """
1601 Moves to the end of the visible region. (Above the scroll offset.)
1602 """
1603 w = event.app.layout.current_window
1604 b = event.current_buffer
1606 if w and w.render_info:
1607 # When we find a Window that has BufferControl showing this window,
1608 # move to the end of the visible area.
1609 pos = (
1610 b.document.translate_row_col_to_index(
1611 w.render_info.last_visible_line(before_scroll_offset=True), 0
1612 )
1613 - b.cursor_position
1614 )
1616 else:
1617 # Otherwise, move to the end of the input.
1618 pos = len(b.document.text_after_cursor)
1619 return TextObject(pos, type=TextObjectType.LINEWISE)
1621 @text_object("n", no_move_handler=True)
1622 def _search_next(event: E) -> TextObject:
1623 """
1624 Search next.
1625 """
1626 buff = event.current_buffer
1627 search_state = event.app.current_search_state
1629 cursor_position = buff.get_search_position(
1630 search_state, include_current_position=False, count=event.arg
1631 )
1632 return TextObject(cursor_position - buff.cursor_position)
1634 @handle("n", filter=vi_navigation_mode)
1635 def _search_next2(event: E) -> None:
1636 """
1637 Search next in navigation mode. (This goes through the history.)
1638 """
1639 search_state = event.app.current_search_state
1641 event.current_buffer.apply_search(
1642 search_state, include_current_position=False, count=event.arg
1643 )
1645 @text_object("N", no_move_handler=True)
1646 def _search_previous(event: E) -> TextObject:
1647 """
1648 Search previous.
1649 """
1650 buff = event.current_buffer
1651 search_state = event.app.current_search_state
1653 cursor_position = buff.get_search_position(
1654 ~search_state, include_current_position=False, count=event.arg
1655 )
1656 return TextObject(cursor_position - buff.cursor_position)
1658 @handle("N", filter=vi_navigation_mode)
1659 def _search_previous2(event: E) -> None:
1660 """
1661 Search previous in navigation mode. (This goes through the history.)
1662 """
1663 search_state = event.app.current_search_state
1665 event.current_buffer.apply_search(
1666 ~search_state, include_current_position=False, count=event.arg
1667 )
1669 @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode)
1670 @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode)
1671 @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode)
1672 def _scroll_top(event: E) -> None:
1673 """
1674 Scrolls the window to makes the current line the first line in the visible region.
1675 """
1676 b = event.current_buffer
1677 event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row
1679 @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode)
1680 @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode)
1681 def _scroll_bottom(event: E) -> None:
1682 """
1683 Scrolls the window to makes the current line the last line in the visible region.
1684 """
1685 # We can safely set the scroll offset to zero; the Window will make
1686 # sure that it scrolls at least enough to make the cursor visible
1687 # again.
1688 event.app.layout.current_window.vertical_scroll = 0
1690 @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode)
1691 def _scroll_center(event: E) -> None:
1692 """
1693 Center Window vertically around cursor.
1694 """
1695 w = event.app.layout.current_window
1696 b = event.current_buffer
1698 if w and w.render_info:
1699 info = w.render_info
1701 # Calculate the offset that we need in order to position the row
1702 # containing the cursor in the center.
1703 scroll_height = info.window_height // 2
1705 y = max(0, b.document.cursor_position_row - 1)
1706 height = 0
1707 while y > 0:
1708 line_height = info.get_height_for_line(y)
1710 if height + line_height < scroll_height:
1711 height += line_height
1712 y -= 1
1713 else:
1714 break
1716 w.vertical_scroll = y
1718 @text_object("%")
1719 def _goto_corresponding_bracket(event: E) -> TextObject:
1720 """
1721 Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.)
1722 If an 'arg' has been given, go this this % position in the file.
1723 """
1724 buffer = event.current_buffer
1726 if event._arg:
1727 # If 'arg' has been given, the meaning of % is to go to the 'x%'
1728 # row in the file.
1729 if 0 < event.arg <= 100:
1730 absolute_index = buffer.document.translate_row_col_to_index(
1731 int((event.arg * buffer.document.line_count - 1) / 100), 0
1732 )
1733 return TextObject(
1734 absolute_index - buffer.document.cursor_position,
1735 type=TextObjectType.LINEWISE,
1736 )
1737 else:
1738 return TextObject(0) # Do nothing.
1740 else:
1741 # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s).
1742 match = buffer.document.find_matching_bracket_position()
1743 if match:
1744 return TextObject(match, type=TextObjectType.INCLUSIVE)
1745 else:
1746 return TextObject(0)
1748 @text_object("|")
1749 def _to_column(event: E) -> TextObject:
1750 """
1751 Move to the n-th column (you may specify the argument n by typing it on
1752 number keys, for example, 20|).
1753 """
1754 return TextObject(
1755 event.current_buffer.document.get_column_cursor_position(event.arg - 1)
1756 )
1758 @text_object("g", "g")
1759 def _goto_first_line(event: E) -> TextObject:
1760 """
1761 Go to the start of the very first line.
1762 Implements 'gg', 'cgg', 'ygg'
1763 """
1764 d = event.current_buffer.document
1766 if event._arg:
1767 # Move to the given line.
1768 return TextObject(
1769 d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position,
1770 type=TextObjectType.LINEWISE,
1771 )
1772 else:
1773 # Move to the top of the input.
1774 return TextObject(
1775 d.get_start_of_document_position(), type=TextObjectType.LINEWISE
1776 )
1778 @text_object("g", "_")
1779 def _goto_last_line(event: E) -> TextObject:
1780 """
1781 Go to last non-blank of line.
1782 'g_', 'cg_', 'yg_', etc..
1783 """
1784 return TextObject(
1785 event.current_buffer.document.last_non_blank_of_current_line_position(),
1786 type=TextObjectType.INCLUSIVE,
1787 )
1789 @text_object("g", "e")
1790 def _ge(event: E) -> TextObject:
1791 """
1792 Go to last character of previous word.
1793 'ge', 'cge', 'yge', etc..
1794 """
1795 prev_end = event.current_buffer.document.find_previous_word_ending(
1796 count=event.arg
1797 )
1798 return TextObject(
1799 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE
1800 )
1802 @text_object("g", "E")
1803 def _gE(event: E) -> TextObject:
1804 """
1805 Go to last character of previous WORD.
1806 'gE', 'cgE', 'ygE', etc..
1807 """
1808 prev_end = event.current_buffer.document.find_previous_word_ending(
1809 count=event.arg, WORD=True
1810 )
1811 return TextObject(
1812 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE
1813 )
1815 @text_object("g", "m")
1816 def _gm(event: E) -> TextObject:
1817 """
1818 Like g0, but half a screenwidth to the right. (Or as much as possible.)
1819 """
1820 w = event.app.layout.current_window
1821 buff = event.current_buffer
1823 if w and w.render_info:
1824 width = w.render_info.window_width
1825 start = buff.document.get_start_of_line_position(after_whitespace=False)
1826 start += int(min(width / 2, len(buff.document.current_line)))
1828 return TextObject(start, type=TextObjectType.INCLUSIVE)
1829 return TextObject(0)
1831 @text_object("G")
1832 def _last_line(event: E) -> TextObject:
1833 """
1834 Go to the end of the document. (If no arg has been given.)
1835 """
1836 buf = event.current_buffer
1837 return TextObject(
1838 buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0)
1839 - buf.cursor_position,
1840 type=TextObjectType.LINEWISE,
1841 )
1843 #
1844 # *** Other ***
1845 #
1847 @handle("G", filter=has_arg)
1848 def _to_nth_history_line(event: E) -> None:
1849 """
1850 If an argument is given, move to this line in the history. (for
1851 example, 15G)
1852 """
1853 event.current_buffer.go_to_history(event.arg - 1)
1855 for n in "123456789":
1857 @handle(
1858 n,
1859 filter=vi_navigation_mode
1860 | vi_selection_mode
1861 | vi_waiting_for_text_object_mode,
1862 )
1863 def _arg(event: E) -> None:
1864 """
1865 Always handle numerics in navigation mode as arg.
1866 """
1867 event.append_to_arg_count(event.data)
1869 @handle(
1870 "0",
1871 filter=(
1872 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode
1873 )
1874 & has_arg,
1875 )
1876 def _0_arg(event: E) -> None:
1877 """
1878 Zero when an argument was already give.
1879 """
1880 event.append_to_arg_count(event.data)
1882 @handle(Keys.Any, filter=vi_replace_mode)
1883 def _insert_text(event: E) -> None:
1884 """
1885 Insert data at cursor position.
1886 """
1887 event.current_buffer.insert_text(event.data, overwrite=True)
1889 @handle(Keys.Any, filter=vi_replace_single_mode)
1890 def _replace_single(event: E) -> None:
1891 """
1892 Replace single character at cursor position.
1893 """
1894 event.current_buffer.insert_text(event.data, overwrite=True)
1895 event.current_buffer.cursor_position -= 1
1896 event.app.vi_state.input_mode = InputMode.NAVIGATION
1898 @handle(
1899 Keys.Any,
1900 filter=vi_insert_multiple_mode,
1901 save_before=(lambda e: not e.is_repeat),
1902 )
1903 def _insert_text_multiple_cursors(event: E) -> None:
1904 """
1905 Insert data at multiple cursor positions at once.
1906 (Usually a result of pressing 'I' or 'A' in block-selection mode.)
1907 """
1908 buff = event.current_buffer
1909 original_text = buff.text
1911 # Construct new text.
1912 text = []
1913 p = 0
1915 for p2 in buff.multiple_cursor_positions:
1916 text.append(original_text[p:p2])
1917 text.append(event.data)
1918 p = p2
1920 text.append(original_text[p:])
1922 # Shift all cursor positions.
1923 new_cursor_positions = [
1924 pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions)
1925 ]
1927 # Set result.
1928 buff.text = "".join(text)
1929 buff.multiple_cursor_positions = new_cursor_positions
1930 buff.cursor_position += 1
1932 @handle("backspace", filter=vi_insert_multiple_mode)
1933 def _delete_before_multiple_cursors(event: E) -> None:
1934 """
1935 Backspace, using multiple cursors.
1936 """
1937 buff = event.current_buffer
1938 original_text = buff.text
1940 # Construct new text.
1941 deleted_something = False
1942 text = []
1943 p = 0
1945 for p2 in buff.multiple_cursor_positions:
1946 if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines.
1947 text.append(original_text[p : p2 - 1])
1948 deleted_something = True
1949 else:
1950 text.append(original_text[p:p2])
1951 p = p2
1953 text.append(original_text[p:])
1955 if deleted_something:
1956 # Shift all cursor positions.
1957 lengths = [len(part) for part in text[:-1]]
1958 new_cursor_positions = list(accumulate(lengths))
1960 # Set result.
1961 buff.text = "".join(text)
1962 buff.multiple_cursor_positions = new_cursor_positions
1963 buff.cursor_position -= 1
1964 else:
1965 event.app.output.bell()
1967 @handle("delete", filter=vi_insert_multiple_mode)
1968 def _delete_after_multiple_cursors(event: E) -> None:
1969 """
1970 Delete, using multiple cursors.
1971 """
1972 buff = event.current_buffer
1973 original_text = buff.text
1975 # Construct new text.
1976 deleted_something = False
1977 text = []
1978 new_cursor_positions = []
1979 p = 0
1981 for p2 in buff.multiple_cursor_positions:
1982 text.append(original_text[p:p2])
1983 if p2 >= len(original_text) or original_text[p2] == "\n":
1984 # Don't delete across lines.
1985 p = p2
1986 else:
1987 p = p2 + 1
1988 deleted_something = True
1990 text.append(original_text[p:])
1992 if deleted_something:
1993 # Shift all cursor positions.
1994 lengths = [len(part) for part in text[:-1]]
1995 new_cursor_positions = list(accumulate(lengths))
1997 # Set result.
1998 buff.text = "".join(text)
1999 buff.multiple_cursor_positions = new_cursor_positions
2000 else:
2001 event.app.output.bell()
2003 @handle("left", filter=vi_insert_multiple_mode)
2004 def _left_multiple(event: E) -> None:
2005 """
2006 Move all cursors to the left.
2007 (But keep all cursors on the same line.)
2008 """
2009 buff = event.current_buffer
2010 new_positions = []
2012 for p in buff.multiple_cursor_positions:
2013 if buff.document.translate_index_to_position(p)[1] > 0:
2014 p -= 1
2015 new_positions.append(p)
2017 buff.multiple_cursor_positions = new_positions
2019 if buff.document.cursor_position_col > 0:
2020 buff.cursor_position -= 1
2022 @handle("right", filter=vi_insert_multiple_mode)
2023 def _right_multiple(event: E) -> None:
2024 """
2025 Move all cursors to the right.
2026 (But keep all cursors on the same line.)
2027 """
2028 buff = event.current_buffer
2029 new_positions = []
2031 for p in buff.multiple_cursor_positions:
2032 row, column = buff.document.translate_index_to_position(p)
2033 if column < len(buff.document.lines[row]):
2034 p += 1
2035 new_positions.append(p)
2037 buff.multiple_cursor_positions = new_positions
2039 if not buff.document.is_cursor_at_the_end_of_line:
2040 buff.cursor_position += 1
2042 @handle("up", filter=vi_insert_multiple_mode)
2043 @handle("down", filter=vi_insert_multiple_mode)
2044 def _updown_multiple(event: E) -> None:
2045 """
2046 Ignore all up/down key presses when in multiple cursor mode.
2047 """
2049 @handle("c-x", "c-l", filter=vi_insert_mode)
2050 def _complete_line(event: E) -> None:
2051 """
2052 Pressing the ControlX - ControlL sequence in Vi mode does line
2053 completion based on the other lines in the document and the history.
2054 """
2055 event.current_buffer.start_history_lines_completion()
2057 @handle("c-x", "c-f", filter=vi_insert_mode)
2058 def _complete_filename(event: E) -> None:
2059 """
2060 Complete file names.
2061 """
2062 # TODO
2063 pass
2065 @handle("c-k", filter=vi_insert_mode | vi_replace_mode)
2066 def _digraph(event: E) -> None:
2067 """
2068 Go into digraph mode.
2069 """
2070 event.app.vi_state.waiting_for_digraph = True
2072 @Condition
2073 def digraph_symbol_1_given() -> bool:
2074 return get_app().vi_state.digraph_symbol1 is not None
2076 @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given)
2077 def _digraph1(event: E) -> None:
2078 """
2079 First digraph symbol.
2080 """
2081 event.app.vi_state.digraph_symbol1 = event.data
2083 @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given)
2084 def _create_digraph(event: E) -> None:
2085 """
2086 Insert digraph.
2087 """
2088 try:
2089 # Lookup.
2090 code: tuple[str, str] = (
2091 event.app.vi_state.digraph_symbol1 or "",
2092 event.data,
2093 )
2094 if code not in DIGRAPHS:
2095 code = code[::-1] # Try reversing.
2096 symbol = DIGRAPHS[code]
2097 except KeyError:
2098 # Unknown digraph.
2099 event.app.output.bell()
2100 else:
2101 # Insert digraph.
2102 overwrite = event.app.vi_state.input_mode == InputMode.REPLACE
2103 event.current_buffer.insert_text(chr(symbol), overwrite=overwrite)
2104 event.app.vi_state.waiting_for_digraph = False
2105 finally:
2106 event.app.vi_state.waiting_for_digraph = False
2107 event.app.vi_state.digraph_symbol1 = None
2109 @handle("c-o", filter=vi_insert_mode | vi_replace_mode)
2110 def _quick_normal_mode(event: E) -> None:
2111 """
2112 Go into normal mode for one single action.
2113 """
2114 event.app.vi_state.temporary_navigation_mode = True
2116 @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro)
2117 def _start_macro(event: E) -> None:
2118 """
2119 Start recording macro.
2120 """
2121 c = event.key_sequence[1].data
2122 if c in vi_register_names:
2123 vi_state = event.app.vi_state
2125 vi_state.recording_register = c
2126 vi_state.current_recording = ""
2128 @handle("q", filter=vi_navigation_mode & vi_recording_macro)
2129 def _stop_macro(event: E) -> None:
2130 """
2131 Stop recording macro.
2132 """
2133 vi_state = event.app.vi_state
2135 # Store and stop recording.
2136 if vi_state.recording_register:
2137 vi_state.named_registers[vi_state.recording_register] = ClipboardData(
2138 vi_state.current_recording
2139 )
2140 vi_state.recording_register = None
2141 vi_state.current_recording = ""
2143 @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False)
2144 def _execute_macro(event: E) -> None:
2145 """
2146 Execute macro.
2148 Notice that we pass `record_in_macro=False`. This ensures that the `@x`
2149 keys don't appear in the recording itself. This function inserts the
2150 body of the called macro back into the KeyProcessor, so these keys will
2151 be added later on to the macro of their handlers have
2152 `record_in_macro=True`.
2153 """
2154 # Retrieve macro.
2155 c = event.key_sequence[1].data
2156 try:
2157 macro = event.app.vi_state.named_registers[c]
2158 except KeyError:
2159 return
2161 # Expand macro (which is a string in the register), in individual keys.
2162 # Use vt100 parser for this.
2163 keys: list[KeyPress] = []
2165 parser = Vt100Parser(keys.append)
2166 parser.feed(macro.text)
2167 parser.flush()
2169 # Now feed keys back to the input processor.
2170 for _ in range(event.arg):
2171 event.app.key_processor.feed_multiple(keys, first=True)
2173 return ConditionalKeyBindings(key_bindings, vi_mode)
2176def load_vi_search_bindings() -> KeyBindingsBase:
2177 key_bindings = KeyBindings()
2178 handle = key_bindings.add
2179 from . import search
2181 @Condition
2182 def search_buffer_is_empty() -> bool:
2183 "Returns True when the search buffer is empty."
2184 return get_app().current_buffer.text == ""
2186 # Vi-style forward search.
2187 handle(
2188 "/",
2189 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed,
2190 )(search.start_forward_incremental_search)
2191 handle(
2192 "?",
2193 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed,
2194 )(search.start_forward_incremental_search)
2195 handle("c-s")(search.start_forward_incremental_search)
2197 # Vi-style backward search.
2198 handle(
2199 "?",
2200 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed,
2201 )(search.start_reverse_incremental_search)
2202 handle(
2203 "/",
2204 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed,
2205 )(search.start_reverse_incremental_search)
2206 handle("c-r")(search.start_reverse_incremental_search)
2208 # Apply the search. (At the / or ? prompt.)
2209 handle("enter", filter=is_searching)(search.accept_search)
2211 handle("c-r", filter=is_searching)(search.reverse_incremental_search)
2212 handle("c-s", filter=is_searching)(search.forward_incremental_search)
2214 handle("c-c")(search.abort_search)
2215 handle("c-g")(search.abort_search)
2216 handle("backspace", filter=search_buffer_is_empty)(search.abort_search)
2218 # Handle escape. This should accept the search, just like readline.
2219 # `abort_search` would be a meaningful alternative.
2220 handle("escape")(search.accept_search)
2222 return ConditionalKeyBindings(key_bindings, vi_mode)