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