Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/key_binding/bindings/vi.py: 5%
956 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +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, Tuple, TypeVar
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 @handle("c-t", filter=vi_insert_mode)
966 def _indent(event: E) -> None:
967 """
968 Indent lines.
969 """
970 buffer = event.current_buffer
971 current_row = buffer.document.cursor_position_row
972 indent(buffer, current_row, current_row + event.arg)
974 @handle("<", "<", filter=vi_navigation_mode)
975 @handle("c-d", filter=vi_insert_mode)
976 def _unindent(event: E) -> None:
977 """
978 Unindent lines.
979 """
980 current_row = event.current_buffer.document.cursor_position_row
981 unindent(event.current_buffer, current_row, current_row + event.arg)
983 @handle("O", filter=vi_navigation_mode & ~is_read_only)
984 def _open_above(event: E) -> None:
985 """
986 Open line above and enter insertion mode
987 """
988 event.current_buffer.insert_line_above(copy_margin=not in_paste_mode())
989 event.app.vi_state.input_mode = InputMode.INSERT
991 @handle("o", filter=vi_navigation_mode & ~is_read_only)
992 def _open_below(event: E) -> None:
993 """
994 Open line below and enter insertion mode
995 """
996 event.current_buffer.insert_line_below(copy_margin=not in_paste_mode())
997 event.app.vi_state.input_mode = InputMode.INSERT
999 @handle("~", filter=vi_navigation_mode)
1000 def _reverse_case(event: E) -> None:
1001 """
1002 Reverse case of current character and move cursor forward.
1003 """
1004 buffer = event.current_buffer
1005 c = buffer.document.current_char
1007 if c is not None and c != "\n":
1008 buffer.insert_text(c.swapcase(), overwrite=True)
1010 @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only)
1011 def _lowercase_line(event: E) -> None:
1012 """
1013 Lowercase current line.
1014 """
1015 buff = event.current_buffer
1016 buff.transform_current_line(lambda s: s.lower())
1018 @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only)
1019 def _uppercase_line(event: E) -> None:
1020 """
1021 Uppercase current line.
1022 """
1023 buff = event.current_buffer
1024 buff.transform_current_line(lambda s: s.upper())
1026 @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only)
1027 def _swapcase_line(event: E) -> None:
1028 """
1029 Swap case of the current line.
1030 """
1031 buff = event.current_buffer
1032 buff.transform_current_line(lambda s: s.swapcase())
1034 @handle("#", filter=vi_navigation_mode)
1035 def _prev_occurrence(event: E) -> None:
1036 """
1037 Go to previous occurrence of this word.
1038 """
1039 b = event.current_buffer
1040 search_state = event.app.current_search_state
1042 search_state.text = b.document.get_word_under_cursor()
1043 search_state.direction = SearchDirection.BACKWARD
1045 b.apply_search(search_state, count=event.arg, include_current_position=False)
1047 @handle("*", filter=vi_navigation_mode)
1048 def _next_occurrence(event: E) -> None:
1049 """
1050 Go to next occurrence of this word.
1051 """
1052 b = event.current_buffer
1053 search_state = event.app.current_search_state
1055 search_state.text = b.document.get_word_under_cursor()
1056 search_state.direction = SearchDirection.FORWARD
1058 b.apply_search(search_state, count=event.arg, include_current_position=False)
1060 @handle("(", filter=vi_navigation_mode)
1061 def _begin_of_sentence(event: E) -> None:
1062 # TODO: go to begin of sentence.
1063 # XXX: should become text_object.
1064 pass
1066 @handle(")", filter=vi_navigation_mode)
1067 def _end_of_sentence(event: E) -> None:
1068 # TODO: go to end of sentence.
1069 # XXX: should become text_object.
1070 pass
1072 operator = create_operator_decorator(key_bindings)
1073 text_object = create_text_object_decorator(key_bindings)
1075 @handle(Keys.Any, filter=vi_waiting_for_text_object_mode)
1076 def _unknown_text_object(event: E) -> None:
1077 """
1078 Unknown key binding while waiting for a text object.
1079 """
1080 event.app.output.bell()
1082 #
1083 # *** Operators ***
1084 #
1086 def create_delete_and_change_operators(
1087 delete_only: bool, with_register: bool = False
1088 ) -> None:
1089 """
1090 Delete and change operators.
1092 :param delete_only: Create an operator that deletes, but doesn't go to insert mode.
1093 :param with_register: Copy the deleted text to this named register instead of the clipboard.
1094 """
1095 handler_keys: Iterable[str]
1096 if with_register:
1097 handler_keys = ('"', Keys.Any, "cd"[delete_only])
1098 else:
1099 handler_keys = "cd"[delete_only]
1101 @operator(*handler_keys, filter=~is_read_only)
1102 def delete_or_change_operator(event: E, text_object: TextObject) -> None:
1103 clipboard_data = None
1104 buff = event.current_buffer
1106 if text_object:
1107 new_document, clipboard_data = text_object.cut(buff)
1108 buff.document = new_document
1110 # Set deleted/changed text to clipboard or named register.
1111 if clipboard_data and clipboard_data.text:
1112 if with_register:
1113 reg_name = event.key_sequence[1].data
1114 if reg_name in vi_register_names:
1115 event.app.vi_state.named_registers[reg_name] = clipboard_data
1116 else:
1117 event.app.clipboard.set_data(clipboard_data)
1119 # Only go back to insert mode in case of 'change'.
1120 if not delete_only:
1121 event.app.vi_state.input_mode = InputMode.INSERT
1123 create_delete_and_change_operators(False, False)
1124 create_delete_and_change_operators(False, True)
1125 create_delete_and_change_operators(True, False)
1126 create_delete_and_change_operators(True, True)
1128 def create_transform_handler(
1129 filter: Filter, transform_func: Callable[[str], str], *a: str
1130 ) -> None:
1131 @operator(*a, filter=filter & ~is_read_only)
1132 def _(event: E, text_object: TextObject) -> None:
1133 """
1134 Apply transformation (uppercase, lowercase, rot13, swap case).
1135 """
1136 buff = event.current_buffer
1137 start, end = text_object.operator_range(buff.document)
1139 if start < end:
1140 # Transform.
1141 buff.transform_region(
1142 buff.cursor_position + start,
1143 buff.cursor_position + end,
1144 transform_func,
1145 )
1147 # Move cursor
1148 buff.cursor_position += text_object.end or text_object.start
1150 for k, f, func in vi_transform_functions:
1151 create_transform_handler(f, func, *k)
1153 @operator("y")
1154 def _yank(event: E, text_object: TextObject) -> None:
1155 """
1156 Yank operator. (Copy text.)
1157 """
1158 _, clipboard_data = text_object.cut(event.current_buffer)
1159 if clipboard_data.text:
1160 event.app.clipboard.set_data(clipboard_data)
1162 @operator('"', Keys.Any, "y")
1163 def _yank_to_register(event: E, text_object: TextObject) -> None:
1164 """
1165 Yank selection to named register.
1166 """
1167 c = event.key_sequence[1].data
1168 if c in vi_register_names:
1169 _, clipboard_data = text_object.cut(event.current_buffer)
1170 event.app.vi_state.named_registers[c] = clipboard_data
1172 @operator(">")
1173 def _indent_text_object(event: E, text_object: TextObject) -> None:
1174 """
1175 Indent.
1176 """
1177 buff = event.current_buffer
1178 from_, to = text_object.get_line_numbers(buff)
1179 indent(buff, from_, to + 1, count=event.arg)
1181 @operator("<")
1182 def _unindent_text_object(event: E, text_object: TextObject) -> None:
1183 """
1184 Unindent.
1185 """
1186 buff = event.current_buffer
1187 from_, to = text_object.get_line_numbers(buff)
1188 unindent(buff, from_, to + 1, count=event.arg)
1190 @operator("g", "q")
1191 def _reshape(event: E, text_object: TextObject) -> None:
1192 """
1193 Reshape text.
1194 """
1195 buff = event.current_buffer
1196 from_, to = text_object.get_line_numbers(buff)
1197 reshape_text(buff, from_, to)
1199 #
1200 # *** Text objects ***
1201 #
1203 @text_object("b")
1204 def _b(event: E) -> TextObject:
1205 """
1206 Move one word or token left.
1207 """
1208 return TextObject(
1209 event.current_buffer.document.find_start_of_previous_word(count=event.arg)
1210 or 0
1211 )
1213 @text_object("B")
1214 def _B(event: E) -> TextObject:
1215 """
1216 Move one non-blank word left
1217 """
1218 return TextObject(
1219 event.current_buffer.document.find_start_of_previous_word(
1220 count=event.arg, WORD=True
1221 )
1222 or 0
1223 )
1225 @text_object("$")
1226 def _dollar(event: E) -> TextObject:
1227 """
1228 'c$', 'd$' and '$': Delete/change/move until end of line.
1229 """
1230 return TextObject(event.current_buffer.document.get_end_of_line_position())
1232 @text_object("w")
1233 def _word_forward(event: E) -> TextObject:
1234 """
1235 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word.
1236 """
1237 return TextObject(
1238 event.current_buffer.document.find_next_word_beginning(count=event.arg)
1239 or event.current_buffer.document.get_end_of_document_position()
1240 )
1242 @text_object("W")
1243 def _WORD_forward(event: E) -> TextObject:
1244 """
1245 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD.
1246 """
1247 return TextObject(
1248 event.current_buffer.document.find_next_word_beginning(
1249 count=event.arg, WORD=True
1250 )
1251 or event.current_buffer.document.get_end_of_document_position()
1252 )
1254 @text_object("e")
1255 def _end_of_word(event: E) -> TextObject:
1256 """
1257 End of 'word': 'ce', 'de', 'e'
1258 """
1259 end = event.current_buffer.document.find_next_word_ending(count=event.arg)
1260 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE)
1262 @text_object("E")
1263 def _end_of_WORD(event: E) -> TextObject:
1264 """
1265 End of 'WORD': 'cE', 'dE', 'E'
1266 """
1267 end = event.current_buffer.document.find_next_word_ending(
1268 count=event.arg, WORD=True
1269 )
1270 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE)
1272 @text_object("i", "w", no_move_handler=True)
1273 def _inner_word(event: E) -> TextObject:
1274 """
1275 Inner 'word': ciw and diw
1276 """
1277 start, end = event.current_buffer.document.find_boundaries_of_current_word()
1278 return TextObject(start, end)
1280 @text_object("a", "w", no_move_handler=True)
1281 def _a_word(event: E) -> TextObject:
1282 """
1283 A 'word': caw and daw
1284 """
1285 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1286 include_trailing_whitespace=True
1287 )
1288 return TextObject(start, end)
1290 @text_object("i", "W", no_move_handler=True)
1291 def _inner_WORD(event: E) -> TextObject:
1292 """
1293 Inner 'WORD': ciW and diW
1294 """
1295 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1296 WORD=True
1297 )
1298 return TextObject(start, end)
1300 @text_object("a", "W", no_move_handler=True)
1301 def _a_WORD(event: E) -> TextObject:
1302 """
1303 A 'WORD': caw and daw
1304 """
1305 start, end = event.current_buffer.document.find_boundaries_of_current_word(
1306 WORD=True, include_trailing_whitespace=True
1307 )
1308 return TextObject(start, end)
1310 @text_object("a", "p", no_move_handler=True)
1311 def _paragraph(event: E) -> TextObject:
1312 """
1313 Auto paragraph.
1314 """
1315 start = event.current_buffer.document.start_of_paragraph()
1316 end = event.current_buffer.document.end_of_paragraph(count=event.arg)
1317 return TextObject(start, end)
1319 @text_object("^")
1320 def _start_of_line(event: E) -> TextObject:
1321 """'c^', 'd^' and '^': Soft start of line, after whitespace."""
1322 return TextObject(
1323 event.current_buffer.document.get_start_of_line_position(
1324 after_whitespace=True
1325 )
1326 )
1328 @text_object("0")
1329 def _hard_start_of_line(event: E) -> TextObject:
1330 """
1331 'c0', 'd0': Hard start of line, before whitespace.
1332 (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.)
1333 """
1334 return TextObject(
1335 event.current_buffer.document.get_start_of_line_position(
1336 after_whitespace=False
1337 )
1338 )
1340 def create_ci_ca_handles(
1341 ci_start: str, ci_end: str, inner: bool, key: str | None = None
1342 ) -> None:
1343 # TODO: 'dat', 'dit', (tags (like xml)
1344 """
1345 Delete/Change string between this start and stop character. But keep these characters.
1346 This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations.
1347 """
1349 def handler(event: E) -> TextObject:
1350 if ci_start == ci_end:
1351 # Quotes
1352 start = event.current_buffer.document.find_backwards(
1353 ci_start, in_current_line=False
1354 )
1355 end = event.current_buffer.document.find(ci_end, in_current_line=False)
1356 else:
1357 # Brackets
1358 start = event.current_buffer.document.find_enclosing_bracket_left(
1359 ci_start, ci_end
1360 )
1361 end = event.current_buffer.document.find_enclosing_bracket_right(
1362 ci_start, ci_end
1363 )
1365 if start is not None and end is not None:
1366 offset = 0 if inner else 1
1367 return TextObject(start + 1 - offset, end + offset)
1368 else:
1369 # Nothing found.
1370 return TextObject(0)
1372 if key is None:
1373 text_object("ai"[inner], ci_start, no_move_handler=True)(handler)
1374 text_object("ai"[inner], ci_end, no_move_handler=True)(handler)
1375 else:
1376 text_object("ai"[inner], key, no_move_handler=True)(handler)
1378 for inner in (False, True):
1379 for ci_start, ci_end in [
1380 ('"', '"'),
1381 ("'", "'"),
1382 ("`", "`"),
1383 ("[", "]"),
1384 ("<", ">"),
1385 ("{", "}"),
1386 ("(", ")"),
1387 ]:
1388 create_ci_ca_handles(ci_start, ci_end, inner)
1390 create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib'
1391 create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB'
1393 @text_object("{")
1394 def _previous_section(event: E) -> TextObject:
1395 """
1396 Move to previous blank-line separated section.
1397 Implements '{', 'c{', 'd{', 'y{'
1398 """
1399 index = event.current_buffer.document.start_of_paragraph(
1400 count=event.arg, before=True
1401 )
1402 return TextObject(index)
1404 @text_object("}")
1405 def _next_section(event: E) -> TextObject:
1406 """
1407 Move to next blank-line separated section.
1408 Implements '}', 'c}', 'd}', 'y}'
1409 """
1410 index = event.current_buffer.document.end_of_paragraph(
1411 count=event.arg, after=True
1412 )
1413 return TextObject(index)
1415 @text_object("f", Keys.Any)
1416 def _find_next_occurrence(event: E) -> TextObject:
1417 """
1418 Go to next occurrence of character. Typing 'fx' will move the
1419 cursor to the next occurrence of character. 'x'.
1420 """
1421 event.app.vi_state.last_character_find = CharacterFind(event.data, False)
1422 match = event.current_buffer.document.find(
1423 event.data, in_current_line=True, count=event.arg
1424 )
1425 if match:
1426 return TextObject(match, type=TextObjectType.INCLUSIVE)
1427 else:
1428 return TextObject(0)
1430 @text_object("F", Keys.Any)
1431 def _find_previous_occurrence(event: E) -> TextObject:
1432 """
1433 Go to previous occurrence of character. Typing 'Fx' will move the
1434 cursor to the previous occurrence of character. 'x'.
1435 """
1436 event.app.vi_state.last_character_find = CharacterFind(event.data, True)
1437 return TextObject(
1438 event.current_buffer.document.find_backwards(
1439 event.data, in_current_line=True, count=event.arg
1440 )
1441 or 0
1442 )
1444 @text_object("t", Keys.Any)
1445 def _t(event: E) -> TextObject:
1446 """
1447 Move right to the next occurrence of c, then one char backward.
1448 """
1449 event.app.vi_state.last_character_find = CharacterFind(event.data, False)
1450 match = event.current_buffer.document.find(
1451 event.data, in_current_line=True, count=event.arg
1452 )
1453 if match:
1454 return TextObject(match - 1, type=TextObjectType.INCLUSIVE)
1455 else:
1456 return TextObject(0)
1458 @text_object("T", Keys.Any)
1459 def _T(event: E) -> TextObject:
1460 """
1461 Move left to the previous occurrence of c, then one char forward.
1462 """
1463 event.app.vi_state.last_character_find = CharacterFind(event.data, True)
1464 match = event.current_buffer.document.find_backwards(
1465 event.data, in_current_line=True, count=event.arg
1466 )
1467 return TextObject(match + 1 if match else 0)
1469 def repeat(reverse: bool) -> None:
1470 """
1471 Create ',' and ';' commands.
1472 """
1474 @text_object("," if reverse else ";")
1475 def _(event: E) -> TextObject:
1476 """
1477 Repeat the last 'f'/'F'/'t'/'T' command.
1478 """
1479 pos: int | None = 0
1480 vi_state = event.app.vi_state
1482 type = TextObjectType.EXCLUSIVE
1484 if vi_state.last_character_find:
1485 char = vi_state.last_character_find.character
1486 backwards = vi_state.last_character_find.backwards
1488 if reverse:
1489 backwards = not backwards
1491 if backwards:
1492 pos = event.current_buffer.document.find_backwards(
1493 char, in_current_line=True, count=event.arg
1494 )
1495 else:
1496 pos = event.current_buffer.document.find(
1497 char, in_current_line=True, count=event.arg
1498 )
1499 type = TextObjectType.INCLUSIVE
1500 if pos:
1501 return TextObject(pos, type=type)
1502 else:
1503 return TextObject(0)
1505 repeat(True)
1506 repeat(False)
1508 @text_object("h")
1509 @text_object("left")
1510 def _left(event: E) -> TextObject:
1511 """
1512 Implements 'ch', 'dh', 'h': Cursor left.
1513 """
1514 return TextObject(
1515 event.current_buffer.document.get_cursor_left_position(count=event.arg)
1516 )
1518 @text_object("j", no_move_handler=True, no_selection_handler=True)
1519 # Note: We also need `no_selection_handler`, because we in
1520 # selection mode, we prefer the other 'j' binding that keeps
1521 # `buffer.preferred_column`.
1522 def _down(event: E) -> TextObject:
1523 """
1524 Implements 'cj', 'dj', 'j', ... Cursor up.
1525 """
1526 return TextObject(
1527 event.current_buffer.document.get_cursor_down_position(count=event.arg),
1528 type=TextObjectType.LINEWISE,
1529 )
1531 @text_object("k", no_move_handler=True, no_selection_handler=True)
1532 def _up(event: E) -> TextObject:
1533 """
1534 Implements 'ck', 'dk', 'k', ... Cursor up.
1535 """
1536 return TextObject(
1537 event.current_buffer.document.get_cursor_up_position(count=event.arg),
1538 type=TextObjectType.LINEWISE,
1539 )
1541 @text_object("l")
1542 @text_object(" ")
1543 @text_object("right")
1544 def _right(event: E) -> TextObject:
1545 """
1546 Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right.
1547 """
1548 return TextObject(
1549 event.current_buffer.document.get_cursor_right_position(count=event.arg)
1550 )
1552 @text_object("H")
1553 def _top_of_screen(event: E) -> TextObject:
1554 """
1555 Moves to the start of the visible region. (Below the scroll offset.)
1556 Implements 'cH', 'dH', 'H'.
1557 """
1558 w = event.app.layout.current_window
1559 b = event.current_buffer
1561 if w and w.render_info:
1562 # When we find a Window that has BufferControl showing this window,
1563 # move to the start of the visible area.
1564 pos = (
1565 b.document.translate_row_col_to_index(
1566 w.render_info.first_visible_line(after_scroll_offset=True), 0
1567 )
1568 - b.cursor_position
1569 )
1571 else:
1572 # Otherwise, move to the start of the input.
1573 pos = -len(b.document.text_before_cursor)
1574 return TextObject(pos, type=TextObjectType.LINEWISE)
1576 @text_object("M")
1577 def _middle_of_screen(event: E) -> TextObject:
1578 """
1579 Moves cursor to the vertical center of the visible region.
1580 Implements 'cM', 'dM', 'M'.
1581 """
1582 w = event.app.layout.current_window
1583 b = event.current_buffer
1585 if w and w.render_info:
1586 # When we find a Window that has BufferControl showing this window,
1587 # move to the center of the visible area.
1588 pos = (
1589 b.document.translate_row_col_to_index(
1590 w.render_info.center_visible_line(), 0
1591 )
1592 - b.cursor_position
1593 )
1595 else:
1596 # Otherwise, move to the start of the input.
1597 pos = -len(b.document.text_before_cursor)
1598 return TextObject(pos, type=TextObjectType.LINEWISE)
1600 @text_object("L")
1601 def _end_of_screen(event: E) -> TextObject:
1602 """
1603 Moves to the end of the visible region. (Above the scroll offset.)
1604 """
1605 w = event.app.layout.current_window
1606 b = event.current_buffer
1608 if w and w.render_info:
1609 # When we find a Window that has BufferControl showing this window,
1610 # move to the end of the visible area.
1611 pos = (
1612 b.document.translate_row_col_to_index(
1613 w.render_info.last_visible_line(before_scroll_offset=True), 0
1614 )
1615 - b.cursor_position
1616 )
1618 else:
1619 # Otherwise, move to the end of the input.
1620 pos = len(b.document.text_after_cursor)
1621 return TextObject(pos, type=TextObjectType.LINEWISE)
1623 @text_object("n", no_move_handler=True)
1624 def _search_next(event: E) -> TextObject:
1625 """
1626 Search next.
1627 """
1628 buff = event.current_buffer
1629 search_state = event.app.current_search_state
1631 cursor_position = buff.get_search_position(
1632 search_state, include_current_position=False, count=event.arg
1633 )
1634 return TextObject(cursor_position - buff.cursor_position)
1636 @handle("n", filter=vi_navigation_mode)
1637 def _search_next2(event: E) -> None:
1638 """
1639 Search next in navigation mode. (This goes through the history.)
1640 """
1641 search_state = event.app.current_search_state
1643 event.current_buffer.apply_search(
1644 search_state, include_current_position=False, count=event.arg
1645 )
1647 @text_object("N", no_move_handler=True)
1648 def _search_previous(event: E) -> TextObject:
1649 """
1650 Search previous.
1651 """
1652 buff = event.current_buffer
1653 search_state = event.app.current_search_state
1655 cursor_position = buff.get_search_position(
1656 ~search_state, include_current_position=False, count=event.arg
1657 )
1658 return TextObject(cursor_position - buff.cursor_position)
1660 @handle("N", filter=vi_navigation_mode)
1661 def _search_previous2(event: E) -> None:
1662 """
1663 Search previous in navigation mode. (This goes through the history.)
1664 """
1665 search_state = event.app.current_search_state
1667 event.current_buffer.apply_search(
1668 ~search_state, include_current_position=False, count=event.arg
1669 )
1671 @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode)
1672 @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode)
1673 @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode)
1674 def _scroll_top(event: E) -> None:
1675 """
1676 Scrolls the window to makes the current line the first line in the visible region.
1677 """
1678 b = event.current_buffer
1679 event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row
1681 @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode)
1682 @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode)
1683 def _scroll_bottom(event: E) -> None:
1684 """
1685 Scrolls the window to makes the current line the last line in the visible region.
1686 """
1687 # We can safely set the scroll offset to zero; the Window will make
1688 # sure that it scrolls at least enough to make the cursor visible
1689 # again.
1690 event.app.layout.current_window.vertical_scroll = 0
1692 @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode)
1693 def _scroll_center(event: E) -> None:
1694 """
1695 Center Window vertically around cursor.
1696 """
1697 w = event.app.layout.current_window
1698 b = event.current_buffer
1700 if w and w.render_info:
1701 info = w.render_info
1703 # Calculate the offset that we need in order to position the row
1704 # containing the cursor in the center.
1705 scroll_height = info.window_height // 2
1707 y = max(0, b.document.cursor_position_row - 1)
1708 height = 0
1709 while y > 0:
1710 line_height = info.get_height_for_line(y)
1712 if height + line_height < scroll_height:
1713 height += line_height
1714 y -= 1
1715 else:
1716 break
1718 w.vertical_scroll = y
1720 @text_object("%")
1721 def _goto_corresponding_bracket(event: E) -> TextObject:
1722 """
1723 Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.)
1724 If an 'arg' has been given, go this this % position in the file.
1725 """
1726 buffer = event.current_buffer
1728 if event._arg:
1729 # If 'arg' has been given, the meaning of % is to go to the 'x%'
1730 # row in the file.
1731 if 0 < event.arg <= 100:
1732 absolute_index = buffer.document.translate_row_col_to_index(
1733 int((event.arg * buffer.document.line_count - 1) / 100), 0
1734 )
1735 return TextObject(
1736 absolute_index - buffer.document.cursor_position,
1737 type=TextObjectType.LINEWISE,
1738 )
1739 else:
1740 return TextObject(0) # Do nothing.
1742 else:
1743 # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s).
1744 match = buffer.document.find_matching_bracket_position()
1745 if match:
1746 return TextObject(match, type=TextObjectType.INCLUSIVE)
1747 else:
1748 return TextObject(0)
1750 @text_object("|")
1751 def _to_column(event: E) -> TextObject:
1752 """
1753 Move to the n-th column (you may specify the argument n by typing it on
1754 number keys, for example, 20|).
1755 """
1756 return TextObject(
1757 event.current_buffer.document.get_column_cursor_position(event.arg - 1)
1758 )
1760 @text_object("g", "g")
1761 def _goto_first_line(event: E) -> TextObject:
1762 """
1763 Go to the start of the very first line.
1764 Implements 'gg', 'cgg', 'ygg'
1765 """
1766 d = event.current_buffer.document
1768 if event._arg:
1769 # Move to the given line.
1770 return TextObject(
1771 d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position,
1772 type=TextObjectType.LINEWISE,
1773 )
1774 else:
1775 # Move to the top of the input.
1776 return TextObject(
1777 d.get_start_of_document_position(), type=TextObjectType.LINEWISE
1778 )
1780 @text_object("g", "_")
1781 def _goto_last_line(event: E) -> TextObject:
1782 """
1783 Go to last non-blank of line.
1784 'g_', 'cg_', 'yg_', etc..
1785 """
1786 return TextObject(
1787 event.current_buffer.document.last_non_blank_of_current_line_position(),
1788 type=TextObjectType.INCLUSIVE,
1789 )
1791 @text_object("g", "e")
1792 def _ge(event: E) -> TextObject:
1793 """
1794 Go to last character of previous word.
1795 'ge', 'cge', 'yge', etc..
1796 """
1797 prev_end = event.current_buffer.document.find_previous_word_ending(
1798 count=event.arg
1799 )
1800 return TextObject(
1801 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE
1802 )
1804 @text_object("g", "E")
1805 def _gE(event: E) -> TextObject:
1806 """
1807 Go to last character of previous WORD.
1808 'gE', 'cgE', 'ygE', etc..
1809 """
1810 prev_end = event.current_buffer.document.find_previous_word_ending(
1811 count=event.arg, WORD=True
1812 )
1813 return TextObject(
1814 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE
1815 )
1817 @text_object("g", "m")
1818 def _gm(event: E) -> TextObject:
1819 """
1820 Like g0, but half a screenwidth to the right. (Or as much as possible.)
1821 """
1822 w = event.app.layout.current_window
1823 buff = event.current_buffer
1825 if w and w.render_info:
1826 width = w.render_info.window_width
1827 start = buff.document.get_start_of_line_position(after_whitespace=False)
1828 start += int(min(width / 2, len(buff.document.current_line)))
1830 return TextObject(start, type=TextObjectType.INCLUSIVE)
1831 return TextObject(0)
1833 @text_object("G")
1834 def _last_line(event: E) -> TextObject:
1835 """
1836 Go to the end of the document. (If no arg has been given.)
1837 """
1838 buf = event.current_buffer
1839 return TextObject(
1840 buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0)
1841 - buf.cursor_position,
1842 type=TextObjectType.LINEWISE,
1843 )
1845 #
1846 # *** Other ***
1847 #
1849 @handle("G", filter=has_arg)
1850 def _to_nth_history_line(event: E) -> None:
1851 """
1852 If an argument is given, move to this line in the history. (for
1853 example, 15G)
1854 """
1855 event.current_buffer.go_to_history(event.arg - 1)
1857 for n in "123456789":
1859 @handle(
1860 n,
1861 filter=vi_navigation_mode
1862 | vi_selection_mode
1863 | vi_waiting_for_text_object_mode,
1864 )
1865 def _arg(event: E) -> None:
1866 """
1867 Always handle numerics in navigation mode as arg.
1868 """
1869 event.append_to_arg_count(event.data)
1871 @handle(
1872 "0",
1873 filter=(
1874 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode
1875 )
1876 & has_arg,
1877 )
1878 def _0_arg(event: E) -> None:
1879 """
1880 Zero when an argument was already give.
1881 """
1882 event.append_to_arg_count(event.data)
1884 @handle(Keys.Any, filter=vi_replace_mode)
1885 def _insert_text(event: E) -> None:
1886 """
1887 Insert data at cursor position.
1888 """
1889 event.current_buffer.insert_text(event.data, overwrite=True)
1891 @handle(Keys.Any, filter=vi_replace_single_mode)
1892 def _replace_single(event: E) -> None:
1893 """
1894 Replace single character at cursor position.
1895 """
1896 event.current_buffer.insert_text(event.data, overwrite=True)
1897 event.current_buffer.cursor_position -= 1
1898 event.app.vi_state.input_mode = InputMode.NAVIGATION
1900 @handle(
1901 Keys.Any,
1902 filter=vi_insert_multiple_mode,
1903 save_before=(lambda e: not e.is_repeat),
1904 )
1905 def _insert_text_multiple_cursors(event: E) -> None:
1906 """
1907 Insert data at multiple cursor positions at once.
1908 (Usually a result of pressing 'I' or 'A' in block-selection mode.)
1909 """
1910 buff = event.current_buffer
1911 original_text = buff.text
1913 # Construct new text.
1914 text = []
1915 p = 0
1917 for p2 in buff.multiple_cursor_positions:
1918 text.append(original_text[p:p2])
1919 text.append(event.data)
1920 p = p2
1922 text.append(original_text[p:])
1924 # Shift all cursor positions.
1925 new_cursor_positions = [
1926 pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions)
1927 ]
1929 # Set result.
1930 buff.text = "".join(text)
1931 buff.multiple_cursor_positions = new_cursor_positions
1932 buff.cursor_position += 1
1934 @handle("backspace", filter=vi_insert_multiple_mode)
1935 def _delete_before_multiple_cursors(event: E) -> None:
1936 """
1937 Backspace, using multiple cursors.
1938 """
1939 buff = event.current_buffer
1940 original_text = buff.text
1942 # Construct new text.
1943 deleted_something = False
1944 text = []
1945 p = 0
1947 for p2 in buff.multiple_cursor_positions:
1948 if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines.
1949 text.append(original_text[p : p2 - 1])
1950 deleted_something = True
1951 else:
1952 text.append(original_text[p:p2])
1953 p = p2
1955 text.append(original_text[p:])
1957 if deleted_something:
1958 # Shift all cursor positions.
1959 lengths = [len(part) for part in text[:-1]]
1960 new_cursor_positions = list(accumulate(lengths))
1962 # Set result.
1963 buff.text = "".join(text)
1964 buff.multiple_cursor_positions = new_cursor_positions
1965 buff.cursor_position -= 1
1966 else:
1967 event.app.output.bell()
1969 @handle("delete", filter=vi_insert_multiple_mode)
1970 def _delete_after_multiple_cursors(event: E) -> None:
1971 """
1972 Delete, using multiple cursors.
1973 """
1974 buff = event.current_buffer
1975 original_text = buff.text
1977 # Construct new text.
1978 deleted_something = False
1979 text = []
1980 new_cursor_positions = []
1981 p = 0
1983 for p2 in buff.multiple_cursor_positions:
1984 text.append(original_text[p:p2])
1985 if p2 >= len(original_text) or original_text[p2] == "\n":
1986 # Don't delete across lines.
1987 p = p2
1988 else:
1989 p = p2 + 1
1990 deleted_something = True
1992 text.append(original_text[p:])
1994 if deleted_something:
1995 # Shift all cursor positions.
1996 lengths = [len(part) for part in text[:-1]]
1997 new_cursor_positions = list(accumulate(lengths))
1999 # Set result.
2000 buff.text = "".join(text)
2001 buff.multiple_cursor_positions = new_cursor_positions
2002 else:
2003 event.app.output.bell()
2005 @handle("left", filter=vi_insert_multiple_mode)
2006 def _left_multiple(event: E) -> None:
2007 """
2008 Move all cursors to the left.
2009 (But keep all cursors on the same line.)
2010 """
2011 buff = event.current_buffer
2012 new_positions = []
2014 for p in buff.multiple_cursor_positions:
2015 if buff.document.translate_index_to_position(p)[1] > 0:
2016 p -= 1
2017 new_positions.append(p)
2019 buff.multiple_cursor_positions = new_positions
2021 if buff.document.cursor_position_col > 0:
2022 buff.cursor_position -= 1
2024 @handle("right", filter=vi_insert_multiple_mode)
2025 def _right_multiple(event: E) -> None:
2026 """
2027 Move all cursors to the right.
2028 (But keep all cursors on the same line.)
2029 """
2030 buff = event.current_buffer
2031 new_positions = []
2033 for p in buff.multiple_cursor_positions:
2034 row, column = buff.document.translate_index_to_position(p)
2035 if column < len(buff.document.lines[row]):
2036 p += 1
2037 new_positions.append(p)
2039 buff.multiple_cursor_positions = new_positions
2041 if not buff.document.is_cursor_at_the_end_of_line:
2042 buff.cursor_position += 1
2044 @handle("up", filter=vi_insert_multiple_mode)
2045 @handle("down", filter=vi_insert_multiple_mode)
2046 def _updown_multiple(event: E) -> None:
2047 """
2048 Ignore all up/down key presses when in multiple cursor mode.
2049 """
2051 @handle("c-x", "c-l", filter=vi_insert_mode)
2052 def _complete_line(event: E) -> None:
2053 """
2054 Pressing the ControlX - ControlL sequence in Vi mode does line
2055 completion based on the other lines in the document and the history.
2056 """
2057 event.current_buffer.start_history_lines_completion()
2059 @handle("c-x", "c-f", filter=vi_insert_mode)
2060 def _complete_filename(event: E) -> None:
2061 """
2062 Complete file names.
2063 """
2064 # TODO
2065 pass
2067 @handle("c-k", filter=vi_insert_mode | vi_replace_mode)
2068 def _digraph(event: E) -> None:
2069 """
2070 Go into digraph mode.
2071 """
2072 event.app.vi_state.waiting_for_digraph = True
2074 @Condition
2075 def digraph_symbol_1_given() -> bool:
2076 return get_app().vi_state.digraph_symbol1 is not None
2078 @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given)
2079 def _digraph1(event: E) -> None:
2080 """
2081 First digraph symbol.
2082 """
2083 event.app.vi_state.digraph_symbol1 = event.data
2085 @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given)
2086 def _create_digraph(event: E) -> None:
2087 """
2088 Insert digraph.
2089 """
2090 try:
2091 # Lookup.
2092 code: tuple[str, str] = (
2093 event.app.vi_state.digraph_symbol1 or "",
2094 event.data,
2095 )
2096 if code not in DIGRAPHS:
2097 code = code[::-1] # Try reversing.
2098 symbol = DIGRAPHS[code]
2099 except KeyError:
2100 # Unknown digraph.
2101 event.app.output.bell()
2102 else:
2103 # Insert digraph.
2104 overwrite = event.app.vi_state.input_mode == InputMode.REPLACE
2105 event.current_buffer.insert_text(chr(symbol), overwrite=overwrite)
2106 event.app.vi_state.waiting_for_digraph = False
2107 finally:
2108 event.app.vi_state.waiting_for_digraph = False
2109 event.app.vi_state.digraph_symbol1 = None
2111 @handle("c-o", filter=vi_insert_mode | vi_replace_mode)
2112 def _quick_normal_mode(event: E) -> None:
2113 """
2114 Go into normal mode for one single action.
2115 """
2116 event.app.vi_state.temporary_navigation_mode = True
2118 @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro)
2119 def _start_macro(event: E) -> None:
2120 """
2121 Start recording macro.
2122 """
2123 c = event.key_sequence[1].data
2124 if c in vi_register_names:
2125 vi_state = event.app.vi_state
2127 vi_state.recording_register = c
2128 vi_state.current_recording = ""
2130 @handle("q", filter=vi_navigation_mode & vi_recording_macro)
2131 def _stop_macro(event: E) -> None:
2132 """
2133 Stop recording macro.
2134 """
2135 vi_state = event.app.vi_state
2137 # Store and stop recording.
2138 if vi_state.recording_register:
2139 vi_state.named_registers[vi_state.recording_register] = ClipboardData(
2140 vi_state.current_recording
2141 )
2142 vi_state.recording_register = None
2143 vi_state.current_recording = ""
2145 @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False)
2146 def _execute_macro(event: E) -> None:
2147 """
2148 Execute macro.
2150 Notice that we pass `record_in_macro=False`. This ensures that the `@x`
2151 keys don't appear in the recording itself. This function inserts the
2152 body of the called macro back into the KeyProcessor, so these keys will
2153 be added later on to the macro of their handlers have
2154 `record_in_macro=True`.
2155 """
2156 # Retrieve macro.
2157 c = event.key_sequence[1].data
2158 try:
2159 macro = event.app.vi_state.named_registers[c]
2160 except KeyError:
2161 return
2163 # Expand macro (which is a string in the register), in individual keys.
2164 # Use vt100 parser for this.
2165 keys: list[KeyPress] = []
2167 parser = Vt100Parser(keys.append)
2168 parser.feed(macro.text)
2169 parser.flush()
2171 # Now feed keys back to the input processor.
2172 for _ in range(event.arg):
2173 event.app.key_processor.feed_multiple(keys, first=True)
2175 return ConditionalKeyBindings(key_bindings, vi_mode)
2178def load_vi_search_bindings() -> KeyBindingsBase:
2179 key_bindings = KeyBindings()
2180 handle = key_bindings.add
2181 from . import search
2183 @Condition
2184 def search_buffer_is_empty() -> bool:
2185 "Returns True when the search buffer is empty."
2186 return get_app().current_buffer.text == ""
2188 # Vi-style forward search.
2189 handle(
2190 "/",
2191 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed,
2192 )(search.start_forward_incremental_search)
2193 handle(
2194 "?",
2195 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed,
2196 )(search.start_forward_incremental_search)
2197 handle("c-s")(search.start_forward_incremental_search)
2199 # Vi-style backward search.
2200 handle(
2201 "?",
2202 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed,
2203 )(search.start_reverse_incremental_search)
2204 handle(
2205 "/",
2206 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed,
2207 )(search.start_reverse_incremental_search)
2208 handle("c-r")(search.start_reverse_incremental_search)
2210 # Apply the search. (At the / or ? prompt.)
2211 handle("enter", filter=is_searching)(search.accept_search)
2213 handle("c-r", filter=is_searching)(search.reverse_incremental_search)
2214 handle("c-s", filter=is_searching)(search.forward_incremental_search)
2216 handle("c-c")(search.abort_search)
2217 handle("c-g")(search.abort_search)
2218 handle("backspace", filter=search_buffer_is_empty)(search.abort_search)
2220 # Handle escape. This should accept the search, just like readline.
2221 # `abort_search` would be a meaningful alternative.
2222 handle("escape")(search.accept_search)
2224 return ConditionalKeyBindings(key_bindings, vi_mode)