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