Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/key_binding/bindings/emacs.py: 6%
271 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1# pylint: disable=function-redefined
2from __future__ import annotations
4from prompt_toolkit.application.current import get_app
5from prompt_toolkit.buffer import Buffer, indent, unindent
6from prompt_toolkit.completion import CompleteEvent
7from prompt_toolkit.filters import (
8 Condition,
9 emacs_insert_mode,
10 emacs_mode,
11 has_arg,
12 has_selection,
13 in_paste_mode,
14 is_multiline,
15 is_read_only,
16 shift_selection_mode,
17 vi_search_direction_reversed,
18)
19from prompt_toolkit.key_binding.key_bindings import Binding
20from prompt_toolkit.key_binding.key_processor import KeyPressEvent
21from prompt_toolkit.keys import Keys
22from prompt_toolkit.selection import SelectionType
24from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase
25from .named_commands import get_by_name
27__all__ = [
28 "load_emacs_bindings",
29 "load_emacs_search_bindings",
30 "load_emacs_shift_selection_bindings",
31]
33E = KeyPressEvent
36def load_emacs_bindings() -> KeyBindingsBase:
37 """
38 Some e-macs extensions.
39 """
40 # Overview of Readline emacs commands:
41 # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
42 key_bindings = KeyBindings()
43 handle = key_bindings.add
45 insert_mode = emacs_insert_mode
47 @handle("escape")
48 def _esc(event: E) -> None:
49 """
50 By default, ignore escape key.
52 (If we don't put this here, and Esc is followed by a key which sequence
53 is not handled, we'll insert an Escape character in the input stream.
54 Something we don't want and happens to easily in emacs mode.
55 Further, people can always use ControlQ to do a quoted insert.)
56 """
57 pass
59 handle("c-a")(get_by_name("beginning-of-line"))
60 handle("c-b")(get_by_name("backward-char"))
61 handle("c-delete", filter=insert_mode)(get_by_name("kill-word"))
62 handle("c-e")(get_by_name("end-of-line"))
63 handle("c-f")(get_by_name("forward-char"))
64 handle("c-left")(get_by_name("backward-word"))
65 handle("c-right")(get_by_name("forward-word"))
66 handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank"))
67 handle("c-y", filter=insert_mode)(get_by_name("yank"))
68 handle("escape", "b")(get_by_name("backward-word"))
69 handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word"))
70 handle("escape", "d", filter=insert_mode)(get_by_name("kill-word"))
71 handle("escape", "f")(get_by_name("forward-word"))
72 handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word"))
73 handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word"))
74 handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop"))
75 handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word"))
76 handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space"))
78 handle("c-home")(get_by_name("beginning-of-buffer"))
79 handle("c-end")(get_by_name("end-of-buffer"))
81 handle("c-_", save_before=(lambda e: False), filter=insert_mode)(
82 get_by_name("undo")
83 )
85 handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)(
86 get_by_name("undo")
87 )
89 handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history"))
90 handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history"))
92 handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg"))
93 handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg"))
94 handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg"))
95 handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment"))
96 handle("c-o")(get_by_name("operate-and-get-next"))
98 # ControlQ does a quoted insert. Not that for vt100 terminals, you have to
99 # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and
100 # Ctrl-S are captured by the terminal.
101 handle("c-q", filter=~has_selection)(get_by_name("quoted-insert"))
103 handle("c-x", "(")(get_by_name("start-kbd-macro"))
104 handle("c-x", ")")(get_by_name("end-kbd-macro"))
105 handle("c-x", "e")(get_by_name("call-last-kbd-macro"))
107 @handle("c-n")
108 def _next(event: E) -> None:
109 "Next line."
110 event.current_buffer.auto_down()
112 @handle("c-p")
113 def _prev(event: E) -> None:
114 "Previous line."
115 event.current_buffer.auto_up(count=event.arg)
117 def handle_digit(c: str) -> None:
118 """
119 Handle input of arguments.
120 The first number needs to be preceded by escape.
121 """
123 @handle(c, filter=has_arg)
124 @handle("escape", c)
125 def _(event: E) -> None:
126 event.append_to_arg_count(c)
128 for c in "0123456789":
129 handle_digit(c)
131 @handle("escape", "-", filter=~has_arg)
132 def _meta_dash(event: E) -> None:
133 """"""
134 if event._arg is None:
135 event.append_to_arg_count("-")
137 @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-"))
138 def _dash(event: E) -> None:
139 """
140 When '-' is typed again, after exactly '-' has been given as an
141 argument, ignore this.
142 """
143 event.app.key_processor.arg = "-"
145 @Condition
146 def is_returnable() -> bool:
147 return get_app().current_buffer.is_returnable
149 # Meta + Enter: always accept input.
150 handle("escape", "enter", filter=insert_mode & is_returnable)(
151 get_by_name("accept-line")
152 )
154 # Enter: accept input in single line mode.
155 handle("enter", filter=insert_mode & is_returnable & ~is_multiline)(
156 get_by_name("accept-line")
157 )
159 def character_search(buff: Buffer, char: str, count: int) -> None:
160 if count < 0:
161 match = buff.document.find_backwards(
162 char, in_current_line=True, count=-count
163 )
164 else:
165 match = buff.document.find(char, in_current_line=True, count=count)
167 if match is not None:
168 buff.cursor_position += match
170 @handle("c-]", Keys.Any)
171 def _goto_char(event: E) -> None:
172 "When Ctl-] + a character is pressed. go to that character."
173 # Also named 'character-search'
174 character_search(event.current_buffer, event.data, event.arg)
176 @handle("escape", "c-]", Keys.Any)
177 def _goto_char_backwards(event: E) -> None:
178 "Like Ctl-], but backwards."
179 # Also named 'character-search-backward'
180 character_search(event.current_buffer, event.data, -event.arg)
182 @handle("escape", "a")
183 def _prev_sentence(event: E) -> None:
184 "Previous sentence."
185 # TODO:
187 @handle("escape", "e")
188 def _end_of_sentence(event: E) -> None:
189 "Move to end of sentence."
190 # TODO:
192 @handle("escape", "t", filter=insert_mode)
193 def _swap_characters(event: E) -> None:
194 """
195 Swap the last two words before the cursor.
196 """
197 # TODO
199 @handle("escape", "*", filter=insert_mode)
200 def _insert_all_completions(event: E) -> None:
201 """
202 `meta-*`: Insert all possible completions of the preceding text.
203 """
204 buff = event.current_buffer
206 # List all completions.
207 complete_event = CompleteEvent(text_inserted=False, completion_requested=True)
208 completions = list(
209 buff.completer.get_completions(buff.document, complete_event)
210 )
212 # Insert them.
213 text_to_insert = " ".join(c.text for c in completions)
214 buff.insert_text(text_to_insert)
216 @handle("c-x", "c-x")
217 def _toggle_start_end(event: E) -> None:
218 """
219 Move cursor back and forth between the start and end of the current
220 line.
221 """
222 buffer = event.current_buffer
224 if buffer.document.is_cursor_at_the_end_of_line:
225 buffer.cursor_position += buffer.document.get_start_of_line_position(
226 after_whitespace=False
227 )
228 else:
229 buffer.cursor_position += buffer.document.get_end_of_line_position()
231 @handle("c-@") # Control-space or Control-@
232 def _start_selection(event: E) -> None:
233 """
234 Start of the selection (if the current buffer is not empty).
235 """
236 # Take the current cursor position as the start of this selection.
237 buff = event.current_buffer
238 if buff.text:
239 buff.start_selection(selection_type=SelectionType.CHARACTERS)
241 @handle("c-g", filter=~has_selection)
242 def _cancel(event: E) -> None:
243 """
244 Control + G: Cancel completion menu and validation state.
245 """
246 event.current_buffer.complete_state = None
247 event.current_buffer.validation_error = None
249 @handle("c-g", filter=has_selection)
250 def _cancel_selection(event: E) -> None:
251 """
252 Cancel selection.
253 """
254 event.current_buffer.exit_selection()
256 @handle("c-w", filter=has_selection)
257 @handle("c-x", "r", "k", filter=has_selection)
258 def _cut(event: E) -> None:
259 """
260 Cut selected text.
261 """
262 data = event.current_buffer.cut_selection()
263 event.app.clipboard.set_data(data)
265 @handle("escape", "w", filter=has_selection)
266 def _copy(event: E) -> None:
267 """
268 Copy selected text.
269 """
270 data = event.current_buffer.copy_selection()
271 event.app.clipboard.set_data(data)
273 @handle("escape", "left")
274 def _start_of_word(event: E) -> None:
275 """
276 Cursor to start of previous word.
277 """
278 buffer = event.current_buffer
279 buffer.cursor_position += (
280 buffer.document.find_previous_word_beginning(count=event.arg) or 0
281 )
283 @handle("escape", "right")
284 def _start_next_word(event: E) -> None:
285 """
286 Cursor to start of next word.
287 """
288 buffer = event.current_buffer
289 buffer.cursor_position += (
290 buffer.document.find_next_word_beginning(count=event.arg)
291 or buffer.document.get_end_of_document_position()
292 )
294 @handle("escape", "/", filter=insert_mode)
295 def _complete(event: E) -> None:
296 """
297 M-/: Complete.
298 """
299 b = event.current_buffer
300 if b.complete_state:
301 b.complete_next()
302 else:
303 b.start_completion(select_first=True)
305 @handle("c-c", ">", filter=has_selection)
306 def _indent(event: E) -> None:
307 """
308 Indent selected text.
309 """
310 buffer = event.current_buffer
312 buffer.cursor_position += buffer.document.get_start_of_line_position(
313 after_whitespace=True
314 )
316 from_, to = buffer.document.selection_range()
317 from_, _ = buffer.document.translate_index_to_position(from_)
318 to, _ = buffer.document.translate_index_to_position(to)
320 indent(buffer, from_, to + 1, count=event.arg)
322 @handle("c-c", "<", filter=has_selection)
323 def _unindent(event: E) -> None:
324 """
325 Unindent selected text.
326 """
327 buffer = event.current_buffer
329 from_, to = buffer.document.selection_range()
330 from_, _ = buffer.document.translate_index_to_position(from_)
331 to, _ = buffer.document.translate_index_to_position(to)
333 unindent(buffer, from_, to + 1, count=event.arg)
335 return ConditionalKeyBindings(key_bindings, emacs_mode)
338def load_emacs_search_bindings() -> KeyBindingsBase:
339 key_bindings = KeyBindings()
340 handle = key_bindings.add
341 from . import search
343 # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we
344 # want Alt+Enter to accept input directly in incremental search mode.
345 # Instead, we have double escape.
347 handle("c-r")(search.start_reverse_incremental_search)
348 handle("c-s")(search.start_forward_incremental_search)
350 handle("c-c")(search.abort_search)
351 handle("c-g")(search.abort_search)
352 handle("c-r")(search.reverse_incremental_search)
353 handle("c-s")(search.forward_incremental_search)
354 handle("up")(search.reverse_incremental_search)
355 handle("down")(search.forward_incremental_search)
356 handle("enter")(search.accept_search)
358 # Handling of escape.
359 handle("escape", eager=True)(search.accept_search)
361 # Like Readline, it's more natural to accept the search when escape has
362 # been pressed, however instead the following two bindings could be used
363 # instead.
364 # #handle('escape', 'escape', eager=True)(search.abort_search)
365 # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input)
367 # If Read-only: also include the following key bindings:
369 # '/' and '?' key bindings for searching, just like Vi mode.
370 handle("?", filter=is_read_only & ~vi_search_direction_reversed)(
371 search.start_reverse_incremental_search
372 )
373 handle("/", filter=is_read_only & ~vi_search_direction_reversed)(
374 search.start_forward_incremental_search
375 )
376 handle("?", filter=is_read_only & vi_search_direction_reversed)(
377 search.start_forward_incremental_search
378 )
379 handle("/", filter=is_read_only & vi_search_direction_reversed)(
380 search.start_reverse_incremental_search
381 )
383 @handle("n", filter=is_read_only)
384 def _jump_next(event: E) -> None:
385 "Jump to next match."
386 event.current_buffer.apply_search(
387 event.app.current_search_state,
388 include_current_position=False,
389 count=event.arg,
390 )
392 @handle("N", filter=is_read_only)
393 def _jump_prev(event: E) -> None:
394 "Jump to previous match."
395 event.current_buffer.apply_search(
396 ~event.app.current_search_state,
397 include_current_position=False,
398 count=event.arg,
399 )
401 return ConditionalKeyBindings(key_bindings, emacs_mode)
404def load_emacs_shift_selection_bindings() -> KeyBindingsBase:
405 """
406 Bindings to select text with shift + cursor movements
407 """
409 key_bindings = KeyBindings()
410 handle = key_bindings.add
412 def unshift_move(event: E) -> None:
413 """
414 Used for the shift selection mode. When called with
415 a shift + movement key press event, moves the cursor
416 as if shift is not pressed.
417 """
418 key = event.key_sequence[0].key
420 if key == Keys.ShiftUp:
421 event.current_buffer.auto_up(count=event.arg)
422 return
423 if key == Keys.ShiftDown:
424 event.current_buffer.auto_down(count=event.arg)
425 return
427 # the other keys are handled through their readline command
428 key_to_command: dict[Keys | str, str] = {
429 Keys.ShiftLeft: "backward-char",
430 Keys.ShiftRight: "forward-char",
431 Keys.ShiftHome: "beginning-of-line",
432 Keys.ShiftEnd: "end-of-line",
433 Keys.ControlShiftLeft: "backward-word",
434 Keys.ControlShiftRight: "forward-word",
435 Keys.ControlShiftHome: "beginning-of-buffer",
436 Keys.ControlShiftEnd: "end-of-buffer",
437 }
439 try:
440 # Both the dict lookup and `get_by_name` can raise KeyError.
441 binding = get_by_name(key_to_command[key])
442 except KeyError:
443 pass
444 else: # (`else` is not really needed here.)
445 if isinstance(binding, Binding):
446 # (It should always be a binding here)
447 binding.call(event)
449 @handle("s-left", filter=~has_selection)
450 @handle("s-right", filter=~has_selection)
451 @handle("s-up", filter=~has_selection)
452 @handle("s-down", filter=~has_selection)
453 @handle("s-home", filter=~has_selection)
454 @handle("s-end", filter=~has_selection)
455 @handle("c-s-left", filter=~has_selection)
456 @handle("c-s-right", filter=~has_selection)
457 @handle("c-s-home", filter=~has_selection)
458 @handle("c-s-end", filter=~has_selection)
459 def _start_selection(event: E) -> None:
460 """
461 Start selection with shift + movement.
462 """
463 # Take the current cursor position as the start of this selection.
464 buff = event.current_buffer
465 if buff.text:
466 buff.start_selection(selection_type=SelectionType.CHARACTERS)
468 if buff.selection_state is not None:
469 # (`selection_state` should never be `None`, it is created by
470 # `start_selection`.)
471 buff.selection_state.enter_shift_mode()
473 # Then move the cursor
474 original_position = buff.cursor_position
475 unshift_move(event)
476 if buff.cursor_position == original_position:
477 # Cursor didn't actually move - so cancel selection
478 # to avoid having an empty selection
479 buff.exit_selection()
481 @handle("s-left", filter=shift_selection_mode)
482 @handle("s-right", filter=shift_selection_mode)
483 @handle("s-up", filter=shift_selection_mode)
484 @handle("s-down", filter=shift_selection_mode)
485 @handle("s-home", filter=shift_selection_mode)
486 @handle("s-end", filter=shift_selection_mode)
487 @handle("c-s-left", filter=shift_selection_mode)
488 @handle("c-s-right", filter=shift_selection_mode)
489 @handle("c-s-home", filter=shift_selection_mode)
490 @handle("c-s-end", filter=shift_selection_mode)
491 def _extend_selection(event: E) -> None:
492 """
493 Extend the selection
494 """
495 # Just move the cursor, like shift was not pressed
496 unshift_move(event)
497 buff = event.current_buffer
499 if buff.selection_state is not None:
500 if buff.cursor_position == buff.selection_state.original_cursor_position:
501 # selection is now empty, so cancel selection
502 buff.exit_selection()
504 @handle(Keys.Any, filter=shift_selection_mode)
505 def _replace_selection(event: E) -> None:
506 """
507 Replace selection by what is typed
508 """
509 event.current_buffer.cut_selection()
510 get_by_name("self-insert").call(event)
512 @handle("enter", filter=shift_selection_mode & is_multiline)
513 def _newline(event: E) -> None:
514 """
515 A newline replaces the selection
516 """
517 event.current_buffer.cut_selection()
518 event.current_buffer.newline(copy_margin=not in_paste_mode())
520 @handle("backspace", filter=shift_selection_mode)
521 def _delete(event: E) -> None:
522 """
523 Delete selection.
524 """
525 event.current_buffer.cut_selection()
527 @handle("c-y", filter=shift_selection_mode)
528 def _yank(event: E) -> None:
529 """
530 In shift selection mode, yanking (pasting) replace the selection.
531 """
532 buff = event.current_buffer
533 if buff.selection_state:
534 buff.cut_selection()
535 get_by_name("yank").call(event)
537 # moving the cursor in shift selection mode cancels the selection
538 @handle("left", filter=shift_selection_mode)
539 @handle("right", filter=shift_selection_mode)
540 @handle("up", filter=shift_selection_mode)
541 @handle("down", filter=shift_selection_mode)
542 @handle("home", filter=shift_selection_mode)
543 @handle("end", filter=shift_selection_mode)
544 @handle("c-left", filter=shift_selection_mode)
545 @handle("c-right", filter=shift_selection_mode)
546 @handle("c-home", filter=shift_selection_mode)
547 @handle("c-end", filter=shift_selection_mode)
548 def _cancel(event: E) -> None:
549 """
550 Cancel selection.
551 """
552 event.current_buffer.exit_selection()
553 # we then process the cursor movement
554 key_press = event.key_sequence[0]
555 event.key_processor.feed(key_press, first=True)
557 return ConditionalKeyBindings(key_bindings, emacs_mode)