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)