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