| | |
| | from __future__ import annotations |
| |
|
| | from prompt_toolkit.application.current import get_app |
| | from prompt_toolkit.buffer import Buffer, indent, unindent |
| | from prompt_toolkit.completion import CompleteEvent |
| | from prompt_toolkit.filters import ( |
| | Condition, |
| | emacs_insert_mode, |
| | emacs_mode, |
| | has_arg, |
| | has_selection, |
| | in_paste_mode, |
| | is_multiline, |
| | is_read_only, |
| | shift_selection_mode, |
| | vi_search_direction_reversed, |
| | ) |
| | from prompt_toolkit.key_binding.key_bindings import Binding |
| | from prompt_toolkit.key_binding.key_processor import KeyPressEvent |
| | from prompt_toolkit.keys import Keys |
| | from prompt_toolkit.selection import SelectionType |
| |
|
| | from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase |
| | from .named_commands import get_by_name |
| |
|
| | __all__ = [ |
| | "load_emacs_bindings", |
| | "load_emacs_search_bindings", |
| | "load_emacs_shift_selection_bindings", |
| | ] |
| |
|
| | E = KeyPressEvent |
| |
|
| |
|
| | @Condition |
| | def is_returnable() -> bool: |
| | return get_app().current_buffer.is_returnable |
| |
|
| |
|
| | @Condition |
| | def is_arg() -> bool: |
| | return get_app().key_processor.arg == "-" |
| |
|
| |
|
| | def load_emacs_bindings() -> KeyBindingsBase: |
| | """ |
| | Some e-macs extensions. |
| | """ |
| | |
| | |
| | key_bindings = KeyBindings() |
| | handle = key_bindings.add |
| |
|
| | insert_mode = emacs_insert_mode |
| |
|
| | @handle("escape") |
| | def _esc(event: E) -> None: |
| | """ |
| | By default, ignore escape key. |
| | |
| | (If we don't put this here, and Esc is followed by a key which sequence |
| | is not handled, we'll insert an Escape character in the input stream. |
| | Something we don't want and happens to easily in emacs mode. |
| | Further, people can always use ControlQ to do a quoted insert.) |
| | """ |
| | pass |
| |
|
| | handle("c-a")(get_by_name("beginning-of-line")) |
| | handle("c-b")(get_by_name("backward-char")) |
| | handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) |
| | handle("c-e")(get_by_name("end-of-line")) |
| | handle("c-f")(get_by_name("forward-char")) |
| | handle("c-left")(get_by_name("backward-word")) |
| | handle("c-right")(get_by_name("forward-word")) |
| | handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) |
| | handle("c-y", filter=insert_mode)(get_by_name("yank")) |
| | handle("escape", "b")(get_by_name("backward-word")) |
| | handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) |
| | handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) |
| | handle("escape", "f")(get_by_name("forward-word")) |
| | handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) |
| | handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) |
| | handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) |
| | handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) |
| | handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) |
| |
|
| | handle("c-home")(get_by_name("beginning-of-buffer")) |
| | handle("c-end")(get_by_name("end-of-buffer")) |
| |
|
| | handle("c-_", save_before=(lambda e: False), filter=insert_mode)( |
| | get_by_name("undo") |
| | ) |
| |
|
| | handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( |
| | get_by_name("undo") |
| | ) |
| |
|
| | handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) |
| | handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) |
| |
|
| | handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) |
| | handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) |
| | handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) |
| | handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) |
| | handle("c-o")(get_by_name("operate-and-get-next")) |
| |
|
| | |
| | |
| | |
| | handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) |
| |
|
| | handle("c-x", "(")(get_by_name("start-kbd-macro")) |
| | handle("c-x", ")")(get_by_name("end-kbd-macro")) |
| | handle("c-x", "e")(get_by_name("call-last-kbd-macro")) |
| |
|
| | @handle("c-n") |
| | def _next(event: E) -> None: |
| | "Next line." |
| | event.current_buffer.auto_down() |
| |
|
| | @handle("c-p") |
| | def _prev(event: E) -> None: |
| | "Previous line." |
| | event.current_buffer.auto_up(count=event.arg) |
| |
|
| | def handle_digit(c: str) -> None: |
| | """ |
| | Handle input of arguments. |
| | The first number needs to be preceded by escape. |
| | """ |
| |
|
| | @handle(c, filter=has_arg) |
| | @handle("escape", c) |
| | def _(event: E) -> None: |
| | event.append_to_arg_count(c) |
| |
|
| | for c in "0123456789": |
| | handle_digit(c) |
| |
|
| | @handle("escape", "-", filter=~has_arg) |
| | def _meta_dash(event: E) -> None: |
| | """""" |
| | if event._arg is None: |
| | event.append_to_arg_count("-") |
| |
|
| | @handle("-", filter=is_arg) |
| | def _dash(event: E) -> None: |
| | """ |
| | When '-' is typed again, after exactly '-' has been given as an |
| | argument, ignore this. |
| | """ |
| | event.app.key_processor.arg = "-" |
| |
|
| | |
| | handle("escape", "enter", filter=insert_mode & is_returnable)( |
| | get_by_name("accept-line") |
| | ) |
| |
|
| | |
| | handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( |
| | get_by_name("accept-line") |
| | ) |
| |
|
| | def character_search(buff: Buffer, char: str, count: int) -> None: |
| | if count < 0: |
| | match = buff.document.find_backwards( |
| | char, in_current_line=True, count=-count |
| | ) |
| | else: |
| | match = buff.document.find(char, in_current_line=True, count=count) |
| |
|
| | if match is not None: |
| | buff.cursor_position += match |
| |
|
| | @handle("c-]", Keys.Any) |
| | def _goto_char(event: E) -> None: |
| | "When Ctl-] + a character is pressed. go to that character." |
| | |
| | character_search(event.current_buffer, event.data, event.arg) |
| |
|
| | @handle("escape", "c-]", Keys.Any) |
| | def _goto_char_backwards(event: E) -> None: |
| | "Like Ctl-], but backwards." |
| | |
| | character_search(event.current_buffer, event.data, -event.arg) |
| |
|
| | @handle("escape", "a") |
| | def _prev_sentence(event: E) -> None: |
| | "Previous sentence." |
| | |
| |
|
| | @handle("escape", "e") |
| | def _end_of_sentence(event: E) -> None: |
| | "Move to end of sentence." |
| | |
| |
|
| | @handle("escape", "t", filter=insert_mode) |
| | def _swap_characters(event: E) -> None: |
| | """ |
| | Swap the last two words before the cursor. |
| | """ |
| | |
| |
|
| | @handle("escape", "*", filter=insert_mode) |
| | def _insert_all_completions(event: E) -> None: |
| | """ |
| | `meta-*`: Insert all possible completions of the preceding text. |
| | """ |
| | buff = event.current_buffer |
| |
|
| | |
| | complete_event = CompleteEvent(text_inserted=False, completion_requested=True) |
| | completions = list( |
| | buff.completer.get_completions(buff.document, complete_event) |
| | ) |
| |
|
| | |
| | text_to_insert = " ".join(c.text for c in completions) |
| | buff.insert_text(text_to_insert) |
| |
|
| | @handle("c-x", "c-x") |
| | def _toggle_start_end(event: E) -> None: |
| | """ |
| | Move cursor back and forth between the start and end of the current |
| | line. |
| | """ |
| | buffer = event.current_buffer |
| |
|
| | if buffer.document.is_cursor_at_the_end_of_line: |
| | buffer.cursor_position += buffer.document.get_start_of_line_position( |
| | after_whitespace=False |
| | ) |
| | else: |
| | buffer.cursor_position += buffer.document.get_end_of_line_position() |
| |
|
| | @handle("c-@") |
| | def _start_selection(event: E) -> None: |
| | """ |
| | Start of the selection (if the current buffer is not empty). |
| | """ |
| | |
| | buff = event.current_buffer |
| | if buff.text: |
| | buff.start_selection(selection_type=SelectionType.CHARACTERS) |
| |
|
| | @handle("c-g", filter=~has_selection) |
| | def _cancel(event: E) -> None: |
| | """ |
| | Control + G: Cancel completion menu and validation state. |
| | """ |
| | event.current_buffer.complete_state = None |
| | event.current_buffer.validation_error = None |
| |
|
| | @handle("c-g", filter=has_selection) |
| | def _cancel_selection(event: E) -> None: |
| | """ |
| | Cancel selection. |
| | """ |
| | event.current_buffer.exit_selection() |
| |
|
| | @handle("c-w", filter=has_selection) |
| | @handle("c-x", "r", "k", filter=has_selection) |
| | def _cut(event: E) -> None: |
| | """ |
| | Cut selected text. |
| | """ |
| | data = event.current_buffer.cut_selection() |
| | event.app.clipboard.set_data(data) |
| |
|
| | @handle("escape", "w", filter=has_selection) |
| | def _copy(event: E) -> None: |
| | """ |
| | Copy selected text. |
| | """ |
| | data = event.current_buffer.copy_selection() |
| | event.app.clipboard.set_data(data) |
| |
|
| | @handle("escape", "left") |
| | def _start_of_word(event: E) -> None: |
| | """ |
| | Cursor to start of previous word. |
| | """ |
| | buffer = event.current_buffer |
| | buffer.cursor_position += ( |
| | buffer.document.find_previous_word_beginning(count=event.arg) or 0 |
| | ) |
| |
|
| | @handle("escape", "right") |
| | def _start_next_word(event: E) -> None: |
| | """ |
| | Cursor to start of next word. |
| | """ |
| | buffer = event.current_buffer |
| | buffer.cursor_position += ( |
| | buffer.document.find_next_word_beginning(count=event.arg) |
| | or buffer.document.get_end_of_document_position() |
| | ) |
| |
|
| | @handle("escape", "/", filter=insert_mode) |
| | def _complete(event: E) -> None: |
| | """ |
| | M-/: Complete. |
| | """ |
| | b = event.current_buffer |
| | if b.complete_state: |
| | b.complete_next() |
| | else: |
| | b.start_completion(select_first=True) |
| |
|
| | @handle("c-c", ">", filter=has_selection) |
| | def _indent(event: E) -> None: |
| | """ |
| | Indent selected text. |
| | """ |
| | buffer = event.current_buffer |
| |
|
| | buffer.cursor_position += buffer.document.get_start_of_line_position( |
| | after_whitespace=True |
| | ) |
| |
|
| | from_, to = buffer.document.selection_range() |
| | from_, _ = buffer.document.translate_index_to_position(from_) |
| | to, _ = buffer.document.translate_index_to_position(to) |
| |
|
| | indent(buffer, from_, to + 1, count=event.arg) |
| |
|
| | @handle("c-c", "<", filter=has_selection) |
| | def _unindent(event: E) -> None: |
| | """ |
| | Unindent selected text. |
| | """ |
| | buffer = event.current_buffer |
| |
|
| | from_, to = buffer.document.selection_range() |
| | from_, _ = buffer.document.translate_index_to_position(from_) |
| | to, _ = buffer.document.translate_index_to_position(to) |
| |
|
| | unindent(buffer, from_, to + 1, count=event.arg) |
| |
|
| | return ConditionalKeyBindings(key_bindings, emacs_mode) |
| |
|
| |
|
| | def load_emacs_search_bindings() -> KeyBindingsBase: |
| | key_bindings = KeyBindings() |
| | handle = key_bindings.add |
| | from . import search |
| |
|
| | |
| | |
| | |
| |
|
| | handle("c-r")(search.start_reverse_incremental_search) |
| | handle("c-s")(search.start_forward_incremental_search) |
| |
|
| | handle("c-c")(search.abort_search) |
| | handle("c-g")(search.abort_search) |
| | handle("c-r")(search.reverse_incremental_search) |
| | handle("c-s")(search.forward_incremental_search) |
| | handle("up")(search.reverse_incremental_search) |
| | handle("down")(search.forward_incremental_search) |
| | handle("enter")(search.accept_search) |
| |
|
| | |
| | handle("escape", eager=True)(search.accept_search) |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| |
|
| | |
| | handle("?", filter=is_read_only & ~vi_search_direction_reversed)( |
| | search.start_reverse_incremental_search |
| | ) |
| | handle("/", filter=is_read_only & ~vi_search_direction_reversed)( |
| | search.start_forward_incremental_search |
| | ) |
| | handle("?", filter=is_read_only & vi_search_direction_reversed)( |
| | search.start_forward_incremental_search |
| | ) |
| | handle("/", filter=is_read_only & vi_search_direction_reversed)( |
| | search.start_reverse_incremental_search |
| | ) |
| |
|
| | @handle("n", filter=is_read_only) |
| | def _jump_next(event: E) -> None: |
| | "Jump to next match." |
| | event.current_buffer.apply_search( |
| | event.app.current_search_state, |
| | include_current_position=False, |
| | count=event.arg, |
| | ) |
| |
|
| | @handle("N", filter=is_read_only) |
| | def _jump_prev(event: E) -> None: |
| | "Jump to previous match." |
| | event.current_buffer.apply_search( |
| | ~event.app.current_search_state, |
| | include_current_position=False, |
| | count=event.arg, |
| | ) |
| |
|
| | return ConditionalKeyBindings(key_bindings, emacs_mode) |
| |
|
| |
|
| | def load_emacs_shift_selection_bindings() -> KeyBindingsBase: |
| | """ |
| | Bindings to select text with shift + cursor movements |
| | """ |
| |
|
| | key_bindings = KeyBindings() |
| | handle = key_bindings.add |
| |
|
| | def unshift_move(event: E) -> None: |
| | """ |
| | Used for the shift selection mode. When called with |
| | a shift + movement key press event, moves the cursor |
| | as if shift is not pressed. |
| | """ |
| | key = event.key_sequence[0].key |
| |
|
| | if key == Keys.ShiftUp: |
| | event.current_buffer.auto_up(count=event.arg) |
| | return |
| | if key == Keys.ShiftDown: |
| | event.current_buffer.auto_down(count=event.arg) |
| | return |
| |
|
| | |
| | key_to_command: dict[Keys | str, str] = { |
| | Keys.ShiftLeft: "backward-char", |
| | Keys.ShiftRight: "forward-char", |
| | Keys.ShiftHome: "beginning-of-line", |
| | Keys.ShiftEnd: "end-of-line", |
| | Keys.ControlShiftLeft: "backward-word", |
| | Keys.ControlShiftRight: "forward-word", |
| | Keys.ControlShiftHome: "beginning-of-buffer", |
| | Keys.ControlShiftEnd: "end-of-buffer", |
| | } |
| |
|
| | try: |
| | |
| | binding = get_by_name(key_to_command[key]) |
| | except KeyError: |
| | pass |
| | else: |
| | if isinstance(binding, Binding): |
| | |
| | binding.call(event) |
| |
|
| | @handle("s-left", filter=~has_selection) |
| | @handle("s-right", filter=~has_selection) |
| | @handle("s-up", filter=~has_selection) |
| | @handle("s-down", filter=~has_selection) |
| | @handle("s-home", filter=~has_selection) |
| | @handle("s-end", filter=~has_selection) |
| | @handle("c-s-left", filter=~has_selection) |
| | @handle("c-s-right", filter=~has_selection) |
| | @handle("c-s-home", filter=~has_selection) |
| | @handle("c-s-end", filter=~has_selection) |
| | def _start_selection(event: E) -> None: |
| | """ |
| | Start selection with shift + movement. |
| | """ |
| | |
| | buff = event.current_buffer |
| | if buff.text: |
| | buff.start_selection(selection_type=SelectionType.CHARACTERS) |
| |
|
| | if buff.selection_state is not None: |
| | |
| | |
| | buff.selection_state.enter_shift_mode() |
| |
|
| | |
| | original_position = buff.cursor_position |
| | unshift_move(event) |
| | if buff.cursor_position == original_position: |
| | |
| | |
| | buff.exit_selection() |
| |
|
| | @handle("s-left", filter=shift_selection_mode) |
| | @handle("s-right", filter=shift_selection_mode) |
| | @handle("s-up", filter=shift_selection_mode) |
| | @handle("s-down", filter=shift_selection_mode) |
| | @handle("s-home", filter=shift_selection_mode) |
| | @handle("s-end", filter=shift_selection_mode) |
| | @handle("c-s-left", filter=shift_selection_mode) |
| | @handle("c-s-right", filter=shift_selection_mode) |
| | @handle("c-s-home", filter=shift_selection_mode) |
| | @handle("c-s-end", filter=shift_selection_mode) |
| | def _extend_selection(event: E) -> None: |
| | """ |
| | Extend the selection |
| | """ |
| | |
| | unshift_move(event) |
| | buff = event.current_buffer |
| |
|
| | if buff.selection_state is not None: |
| | if buff.cursor_position == buff.selection_state.original_cursor_position: |
| | |
| | buff.exit_selection() |
| |
|
| | @handle(Keys.Any, filter=shift_selection_mode) |
| | def _replace_selection(event: E) -> None: |
| | """ |
| | Replace selection by what is typed |
| | """ |
| | event.current_buffer.cut_selection() |
| | get_by_name("self-insert").call(event) |
| |
|
| | @handle("enter", filter=shift_selection_mode & is_multiline) |
| | def _newline(event: E) -> None: |
| | """ |
| | A newline replaces the selection |
| | """ |
| | event.current_buffer.cut_selection() |
| | event.current_buffer.newline(copy_margin=not in_paste_mode()) |
| |
|
| | @handle("backspace", filter=shift_selection_mode) |
| | def _delete(event: E) -> None: |
| | """ |
| | Delete selection. |
| | """ |
| | event.current_buffer.cut_selection() |
| |
|
| | @handle("c-y", filter=shift_selection_mode) |
| | def _yank(event: E) -> None: |
| | """ |
| | In shift selection mode, yanking (pasting) replace the selection. |
| | """ |
| | buff = event.current_buffer |
| | if buff.selection_state: |
| | buff.cut_selection() |
| | get_by_name("yank").call(event) |
| |
|
| | |
| | @handle("left", filter=shift_selection_mode) |
| | @handle("right", filter=shift_selection_mode) |
| | @handle("up", filter=shift_selection_mode) |
| | @handle("down", filter=shift_selection_mode) |
| | @handle("home", filter=shift_selection_mode) |
| | @handle("end", filter=shift_selection_mode) |
| | @handle("c-left", filter=shift_selection_mode) |
| | @handle("c-right", filter=shift_selection_mode) |
| | @handle("c-home", filter=shift_selection_mode) |
| | @handle("c-end", filter=shift_selection_mode) |
| | def _cancel(event: E) -> None: |
| | """ |
| | Cancel selection. |
| | """ |
| | event.current_buffer.exit_selection() |
| | |
| | key_press = event.key_sequence[0] |
| | event.key_processor.feed(key_press, first=True) |
| |
|
| | return ConditionalKeyBindings(key_bindings, emacs_mode) |
| |
|