1""" 
    2Key bindings which are also known by GNU Readline by the given names. 
    3 
    4See: http://www.delorie.com/gnu/docs/readline/rlman_13.html 
    5""" 
    6 
    7from __future__ import annotations 
    8 
    9from typing import Callable, TypeVar, Union, cast 
    10 
    11from prompt_toolkit.document import Document 
    12from prompt_toolkit.enums import EditingMode 
    13from prompt_toolkit.key_binding.key_bindings import Binding, key_binding 
    14from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent 
    15from prompt_toolkit.keys import Keys 
    16from prompt_toolkit.layout.controls import BufferControl 
    17from prompt_toolkit.search import SearchDirection 
    18from prompt_toolkit.selection import PasteMode 
    19 
    20from .completion import display_completions_like_readline, generate_completions 
    21 
    22__all__ = [ 
    23    "get_by_name", 
    24] 
    25 
    26 
    27# Typing. 
    28_Handler = Callable[[KeyPressEvent], None] 
    29_HandlerOrBinding = Union[_Handler, Binding] 
    30_T = TypeVar("_T", bound=_HandlerOrBinding) 
    31E = KeyPressEvent 
    32 
    33 
    34# Registry that maps the Readline command names to their handlers. 
    35_readline_commands: dict[str, Binding] = {} 
    36 
    37 
    38def register(name: str) -> Callable[[_T], _T]: 
    39    """ 
    40    Store handler in the `_readline_commands` dictionary. 
    41    """ 
    42 
    43    def decorator(handler: _T) -> _T: 
    44        "`handler` is a callable or Binding." 
    45        if isinstance(handler, Binding): 
    46            _readline_commands[name] = handler 
    47        else: 
    48            _readline_commands[name] = key_binding()(cast(_Handler, handler)) 
    49 
    50        return handler 
    51 
    52    return decorator 
    53 
    54 
    55def get_by_name(name: str) -> Binding: 
    56    """ 
    57    Return the handler for the (Readline) command with the given name. 
    58    """ 
    59    try: 
    60        return _readline_commands[name] 
    61    except KeyError as e: 
    62        raise KeyError(f"Unknown Readline command: {name!r}") from e 
    63 
    64 
    65# 
    66# Commands for moving 
    67# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html 
    68# 
    69 
    70 
    71@register("beginning-of-buffer") 
    72def beginning_of_buffer(event: E) -> None: 
    73    """ 
    74    Move to the start of the buffer. 
    75    """ 
    76    buff = event.current_buffer 
    77    buff.cursor_position = 0 
    78 
    79 
    80@register("end-of-buffer") 
    81def end_of_buffer(event: E) -> None: 
    82    """ 
    83    Move to the end of the buffer. 
    84    """ 
    85    buff = event.current_buffer 
    86    buff.cursor_position = len(buff.text) 
    87 
    88 
    89@register("beginning-of-line") 
    90def beginning_of_line(event: E) -> None: 
    91    """ 
    92    Move to the start of the current line. 
    93    """ 
    94    buff = event.current_buffer 
    95    buff.cursor_position += buff.document.get_start_of_line_position( 
    96        after_whitespace=False 
    97    ) 
    98 
    99 
    100@register("end-of-line") 
    101def end_of_line(event: E) -> None: 
    102    """ 
    103    Move to the end of the line. 
    104    """ 
    105    buff = event.current_buffer 
    106    buff.cursor_position += buff.document.get_end_of_line_position() 
    107 
    108 
    109@register("forward-char") 
    110def forward_char(event: E) -> None: 
    111    """ 
    112    Move forward a character. 
    113    """ 
    114    buff = event.current_buffer 
    115    buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) 
    116 
    117 
    118@register("backward-char") 
    119def backward_char(event: E) -> None: 
    120    "Move back a character." 
    121    buff = event.current_buffer 
    122    buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) 
    123 
    124 
    125@register("forward-word") 
    126def forward_word(event: E) -> None: 
    127    """ 
    128    Move forward to the end of the next word. Words are composed of letters and 
    129    digits. 
    130    """ 
    131    buff = event.current_buffer 
    132    pos = buff.document.find_next_word_ending(count=event.arg) 
    133 
    134    if pos: 
    135        buff.cursor_position += pos 
    136 
    137 
    138@register("backward-word") 
    139def backward_word(event: E) -> None: 
    140    """ 
    141    Move back to the start of the current or previous word. Words are composed 
    142    of letters and digits. 
    143    """ 
    144    buff = event.current_buffer 
    145    pos = buff.document.find_previous_word_beginning(count=event.arg) 
    146 
    147    if pos: 
    148        buff.cursor_position += pos 
    149 
    150 
    151@register("clear-screen") 
    152def clear_screen(event: E) -> None: 
    153    """ 
    154    Clear the screen and redraw everything at the top of the screen. 
    155    """ 
    156    event.app.renderer.clear() 
    157 
    158 
    159@register("redraw-current-line") 
    160def redraw_current_line(event: E) -> None: 
    161    """ 
    162    Refresh the current line. 
    163    (Readline defines this command, but prompt-toolkit doesn't have it.) 
    164    """ 
    165    pass 
    166 
    167 
    168# 
    169# Commands for manipulating the history. 
    170# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html 
    171# 
    172 
    173 
    174@register("accept-line") 
    175def accept_line(event: E) -> None: 
    176    """ 
    177    Accept the line regardless of where the cursor is. 
    178    """ 
    179    event.current_buffer.validate_and_handle() 
    180 
    181 
    182@register("previous-history") 
    183def previous_history(event: E) -> None: 
    184    """ 
    185    Move `back` through the history list, fetching the previous command. 
    186    """ 
    187    event.current_buffer.history_backward(count=event.arg) 
    188 
    189 
    190@register("next-history") 
    191def next_history(event: E) -> None: 
    192    """ 
    193    Move `forward` through the history list, fetching the next command. 
    194    """ 
    195    event.current_buffer.history_forward(count=event.arg) 
    196 
    197 
    198@register("beginning-of-history") 
    199def beginning_of_history(event: E) -> None: 
    200    """ 
    201    Move to the first line in the history. 
    202    """ 
    203    event.current_buffer.go_to_history(0) 
    204 
    205 
    206@register("end-of-history") 
    207def end_of_history(event: E) -> None: 
    208    """ 
    209    Move to the end of the input history, i.e., the line currently being entered. 
    210    """ 
    211    event.current_buffer.history_forward(count=10**100) 
    212    buff = event.current_buffer 
    213    buff.go_to_history(len(buff._working_lines) - 1) 
    214 
    215 
    216@register("reverse-search-history") 
    217def reverse_search_history(event: E) -> None: 
    218    """ 
    219    Search backward starting at the current line and moving `up` through 
    220    the history as necessary. This is an incremental search. 
    221    """ 
    222    control = event.app.layout.current_control 
    223 
    224    if isinstance(control, BufferControl) and control.search_buffer_control: 
    225        event.app.current_search_state.direction = SearchDirection.BACKWARD 
    226        event.app.layout.current_control = control.search_buffer_control 
    227 
    228 
    229# 
    230# Commands for changing text 
    231# 
    232 
    233 
    234@register("end-of-file") 
    235def end_of_file(event: E) -> None: 
    236    """ 
    237    Exit. 
    238    """ 
    239    event.app.exit() 
    240 
    241 
    242@register("delete-char") 
    243def delete_char(event: E) -> None: 
    244    """ 
    245    Delete character before the cursor. 
    246    """ 
    247    deleted = event.current_buffer.delete(count=event.arg) 
    248    if not deleted: 
    249        event.app.output.bell() 
    250 
    251 
    252@register("backward-delete-char") 
    253def backward_delete_char(event: E) -> None: 
    254    """ 
    255    Delete the character behind the cursor. 
    256    """ 
    257    if event.arg < 0: 
    258        # When a negative argument has been given, this should delete in front 
    259        # of the cursor. 
    260        deleted = event.current_buffer.delete(count=-event.arg) 
    261    else: 
    262        deleted = event.current_buffer.delete_before_cursor(count=event.arg) 
    263 
    264    if not deleted: 
    265        event.app.output.bell() 
    266 
    267 
    268@register("self-insert") 
    269def self_insert(event: E) -> None: 
    270    """ 
    271    Insert yourself. 
    272    """ 
    273    event.current_buffer.insert_text(event.data * event.arg) 
    274 
    275 
    276@register("transpose-chars") 
    277def transpose_chars(event: E) -> None: 
    278    """ 
    279    Emulate Emacs transpose-char behavior: at the beginning of the buffer, 
    280    do nothing.  At the end of a line or buffer, swap the characters before 
    281    the cursor.  Otherwise, move the cursor right, and then swap the 
    282    characters before the cursor. 
    283    """ 
    284    b = event.current_buffer 
    285    p = b.cursor_position 
    286    if p == 0: 
    287        return 
    288    elif p == len(b.text) or b.text[p] == "\n": 
    289        b.swap_characters_before_cursor() 
    290    else: 
    291        b.cursor_position += b.document.get_cursor_right_position() 
    292        b.swap_characters_before_cursor() 
    293 
    294 
    295@register("uppercase-word") 
    296def uppercase_word(event: E) -> None: 
    297    """ 
    298    Uppercase the current (or following) word. 
    299    """ 
    300    buff = event.current_buffer 
    301 
    302    for i in range(event.arg): 
    303        pos = buff.document.find_next_word_ending() 
    304        words = buff.document.text_after_cursor[:pos] 
    305        buff.insert_text(words.upper(), overwrite=True) 
    306 
    307 
    308@register("downcase-word") 
    309def downcase_word(event: E) -> None: 
    310    """ 
    311    Lowercase the current (or following) word. 
    312    """ 
    313    buff = event.current_buffer 
    314 
    315    for i in range(event.arg):  # XXX: not DRY: see meta_c and meta_u!! 
    316        pos = buff.document.find_next_word_ending() 
    317        words = buff.document.text_after_cursor[:pos] 
    318        buff.insert_text(words.lower(), overwrite=True) 
    319 
    320 
    321@register("capitalize-word") 
    322def capitalize_word(event: E) -> None: 
    323    """ 
    324    Capitalize the current (or following) word. 
    325    """ 
    326    buff = event.current_buffer 
    327 
    328    for i in range(event.arg): 
    329        pos = buff.document.find_next_word_ending() 
    330        words = buff.document.text_after_cursor[:pos] 
    331        buff.insert_text(words.title(), overwrite=True) 
    332 
    333 
    334@register("quoted-insert") 
    335def quoted_insert(event: E) -> None: 
    336    """ 
    337    Add the next character typed to the line verbatim. This is how to insert 
    338    key sequences like C-q, for example. 
    339    """ 
    340    event.app.quoted_insert = True 
    341 
    342 
    343# 
    344# Killing and yanking. 
    345# 
    346 
    347 
    348@register("kill-line") 
    349def kill_line(event: E) -> None: 
    350    """ 
    351    Kill the text from the cursor to the end of the line. 
    352 
    353    If we are at the end of the line, this should remove the newline. 
    354    (That way, it is possible to delete multiple lines by executing this 
    355    command multiple times.) 
    356    """ 
    357    buff = event.current_buffer 
    358    if event.arg < 0: 
    359        deleted = buff.delete_before_cursor( 
    360            count=-buff.document.get_start_of_line_position() 
    361        ) 
    362    else: 
    363        if buff.document.current_char == "\n": 
    364            deleted = buff.delete(1) 
    365        else: 
    366            deleted = buff.delete(count=buff.document.get_end_of_line_position()) 
    367    event.app.clipboard.set_text(deleted) 
    368 
    369 
    370@register("kill-word") 
    371def kill_word(event: E) -> None: 
    372    """ 
    373    Kill from point to the end of the current word, or if between words, to the 
    374    end of the next word. Word boundaries are the same as forward-word. 
    375    """ 
    376    buff = event.current_buffer 
    377    pos = buff.document.find_next_word_ending(count=event.arg) 
    378 
    379    if pos: 
    380        deleted = buff.delete(count=pos) 
    381 
    382        if event.is_repeat: 
    383            deleted = event.app.clipboard.get_data().text + deleted 
    384 
    385        event.app.clipboard.set_text(deleted) 
    386 
    387 
    388@register("unix-word-rubout") 
    389def unix_word_rubout(event: E, WORD: bool = True) -> None: 
    390    """ 
    391    Kill the word behind point, using whitespace as a word boundary. 
    392    Usually bound to ControlW. 
    393    """ 
    394    buff = event.current_buffer 
    395    pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) 
    396 
    397    if pos is None: 
    398        # Nothing found? delete until the start of the document.  (The 
    399        # input starts with whitespace and no words were found before the 
    400        # cursor.) 
    401        pos = -buff.cursor_position 
    402 
    403    if pos: 
    404        deleted = buff.delete_before_cursor(count=-pos) 
    405 
    406        # If the previous key press was also Control-W, concatenate deleted 
    407        # text. 
    408        if event.is_repeat: 
    409            deleted += event.app.clipboard.get_data().text 
    410 
    411        event.app.clipboard.set_text(deleted) 
    412    else: 
    413        # Nothing to delete. Bell. 
    414        event.app.output.bell() 
    415 
    416 
    417@register("backward-kill-word") 
    418def backward_kill_word(event: E) -> None: 
    419    """ 
    420    Kills the word before point, using "not a letter nor a digit" as a word boundary. 
    421    Usually bound to M-Del or M-Backspace. 
    422    """ 
    423    unix_word_rubout(event, WORD=False) 
    424 
    425 
    426@register("delete-horizontal-space") 
    427def delete_horizontal_space(event: E) -> None: 
    428    """ 
    429    Delete all spaces and tabs around point. 
    430    """ 
    431    buff = event.current_buffer 
    432    text_before_cursor = buff.document.text_before_cursor 
    433    text_after_cursor = buff.document.text_after_cursor 
    434 
    435    delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) 
    436    delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) 
    437 
    438    buff.delete_before_cursor(count=delete_before) 
    439    buff.delete(count=delete_after) 
    440 
    441 
    442@register("unix-line-discard") 
    443def unix_line_discard(event: E) -> None: 
    444    """ 
    445    Kill backward from the cursor to the beginning of the current line. 
    446    """ 
    447    buff = event.current_buffer 
    448 
    449    if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: 
    450        buff.delete_before_cursor(count=1) 
    451    else: 
    452        deleted = buff.delete_before_cursor( 
    453            count=-buff.document.get_start_of_line_position() 
    454        ) 
    455        event.app.clipboard.set_text(deleted) 
    456 
    457 
    458@register("yank") 
    459def yank(event: E) -> None: 
    460    """ 
    461    Paste before cursor. 
    462    """ 
    463    event.current_buffer.paste_clipboard_data( 
    464        event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS 
    465    ) 
    466 
    467 
    468@register("yank-nth-arg") 
    469def yank_nth_arg(event: E) -> None: 
    470    """ 
    471    Insert the first argument of the previous command. With an argument, insert 
    472    the nth word from the previous command (start counting at 0). 
    473    """ 
    474    n = event.arg if event.arg_present else None 
    475    event.current_buffer.yank_nth_arg(n) 
    476 
    477 
    478@register("yank-last-arg") 
    479def yank_last_arg(event: E) -> None: 
    480    """ 
    481    Like `yank_nth_arg`, but if no argument has been given, yank the last word 
    482    of each line. 
    483    """ 
    484    n = event.arg if event.arg_present else None 
    485    event.current_buffer.yank_last_arg(n) 
    486 
    487 
    488@register("yank-pop") 
    489def yank_pop(event: E) -> None: 
    490    """ 
    491    Rotate the kill ring, and yank the new top. Only works following yank or 
    492    yank-pop. 
    493    """ 
    494    buff = event.current_buffer 
    495    doc_before_paste = buff.document_before_paste 
    496    clipboard = event.app.clipboard 
    497 
    498    if doc_before_paste is not None: 
    499        buff.document = doc_before_paste 
    500        clipboard.rotate() 
    501        buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) 
    502 
    503 
    504# 
    505# Completion. 
    506# 
    507 
    508 
    509@register("complete") 
    510def complete(event: E) -> None: 
    511    """ 
    512    Attempt to perform completion. 
    513    """ 
    514    display_completions_like_readline(event) 
    515 
    516 
    517@register("menu-complete") 
    518def menu_complete(event: E) -> None: 
    519    """ 
    520    Generate completions, or go to the next completion. (This is the default 
    521    way of completing input in prompt_toolkit.) 
    522    """ 
    523    generate_completions(event) 
    524 
    525 
    526@register("menu-complete-backward") 
    527def menu_complete_backward(event: E) -> None: 
    528    """ 
    529    Move backward through the list of possible completions. 
    530    """ 
    531    event.current_buffer.complete_previous() 
    532 
    533 
    534# 
    535# Keyboard macros. 
    536# 
    537 
    538 
    539@register("start-kbd-macro") 
    540def start_kbd_macro(event: E) -> None: 
    541    """ 
    542    Begin saving the characters typed into the current keyboard macro. 
    543    """ 
    544    event.app.emacs_state.start_macro() 
    545 
    546 
    547@register("end-kbd-macro") 
    548def end_kbd_macro(event: E) -> None: 
    549    """ 
    550    Stop saving the characters typed into the current keyboard macro and save 
    551    the definition. 
    552    """ 
    553    event.app.emacs_state.end_macro() 
    554 
    555 
    556@register("call-last-kbd-macro") 
    557@key_binding(record_in_macro=False) 
    558def call_last_kbd_macro(event: E) -> None: 
    559    """ 
    560    Re-execute the last keyboard macro defined, by making the characters in the 
    561    macro appear as if typed at the keyboard. 
    562 
    563    Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' 
    564    key sequence doesn't appear in the recording itself. This function inserts 
    565    the body of the called macro back into the KeyProcessor, so these keys will 
    566    be added later on to the macro of their handlers have `record_in_macro=True`. 
    567    """ 
    568    # Insert the macro. 
    569    macro = event.app.emacs_state.macro 
    570 
    571    if macro: 
    572        event.app.key_processor.feed_multiple(macro, first=True) 
    573 
    574 
    575@register("print-last-kbd-macro") 
    576def print_last_kbd_macro(event: E) -> None: 
    577    """ 
    578    Print the last keyboard macro. 
    579    """ 
    580 
    581    # TODO: Make the format suitable for the inputrc file. 
    582    def print_macro() -> None: 
    583        macro = event.app.emacs_state.macro 
    584        if macro: 
    585            for k in macro: 
    586                print(k) 
    587 
    588    from prompt_toolkit.application.run_in_terminal import run_in_terminal 
    589 
    590    run_in_terminal(print_macro) 
    591 
    592 
    593# 
    594# Miscellaneous Commands. 
    595# 
    596 
    597 
    598@register("undo") 
    599def undo(event: E) -> None: 
    600    """ 
    601    Incremental undo. 
    602    """ 
    603    event.current_buffer.undo() 
    604 
    605 
    606@register("insert-comment") 
    607def insert_comment(event: E) -> None: 
    608    """ 
    609    Without numeric argument, comment all lines. 
    610    With numeric argument, uncomment all lines. 
    611    In any case accept the input. 
    612    """ 
    613    buff = event.current_buffer 
    614 
    615    # Transform all lines. 
    616    if event.arg != 1: 
    617 
    618        def change(line: str) -> str: 
    619            return line[1:] if line.startswith("#") else line 
    620 
    621    else: 
    622 
    623        def change(line: str) -> str: 
    624            return "#" + line 
    625 
    626    buff.document = Document( 
    627        text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 
    628    ) 
    629 
    630    # Accept input. 
    631    buff.validate_and_handle() 
    632 
    633 
    634@register("vi-editing-mode") 
    635def vi_editing_mode(event: E) -> None: 
    636    """ 
    637    Switch to Vi editing mode. 
    638    """ 
    639    event.app.editing_mode = EditingMode.VI 
    640 
    641 
    642@register("emacs-editing-mode") 
    643def emacs_editing_mode(event: E) -> None: 
    644    """ 
    645    Switch to Emacs editing mode. 
    646    """ 
    647    event.app.editing_mode = EditingMode.EMACS 
    648 
    649 
    650@register("prefix-meta") 
    651def prefix_meta(event: E) -> None: 
    652    """ 
    653    Metafy the next character typed. This is for keyboards without a meta key. 
    654 
    655    Sometimes people also want to bind other keys to Meta, e.g. 'jj':: 
    656 
    657        key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) 
    658    """ 
    659    # ('first' should be true, because we want to insert it at the current 
    660    # position in the queue.) 
    661    event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) 
    662 
    663 
    664@register("operate-and-get-next") 
    665def operate_and_get_next(event: E) -> None: 
    666    """ 
    667    Accept the current line for execution and fetch the next line relative to 
    668    the current line from the history for editing. 
    669    """ 
    670    buff = event.current_buffer 
    671    new_index = buff.working_index + 1 
    672 
    673    # Accept the current input. (This will also redraw the interface in the 
    674    # 'done' state.) 
    675    buff.validate_and_handle() 
    676 
    677    # Set the new index at the start of the next run. 
    678    def set_working_index() -> None: 
    679        if new_index < len(buff._working_lines): 
    680            buff.working_index = new_index 
    681 
    682    event.app.pre_run_callables.append(set_working_index) 
    683 
    684 
    685@register("edit-and-execute-command") 
    686def edit_and_execute(event: E) -> None: 
    687    """ 
    688    Invoke an editor on the current command line, and accept the result. 
    689    """ 
    690    buff = event.current_buffer 
    691    buff.open_in_editor(validate_and_handle=True)