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