Source code for xonsh.shells.ptk_shell.key_bindings

"""Key bindings for prompt_toolkit xonsh shell."""

from prompt_toolkit import search
from prompt_toolkit.application.current import get_app
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.filters import (
    Condition,
    EmacsInsertMode,
    HasSelection,
    IsMultiline,
    IsSearching,
    ViInsertMode,
)
from prompt_toolkit.input import ansi_escape_sequences
from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase
from prompt_toolkit.keys import Keys

from xonsh.aliases import xonsh_exit
from xonsh.built_ins import XSH
from xonsh.platform import ON_WINDOWS
from xonsh.shell import transform_command
from xonsh.tools import (
    check_for_partial_string,
    ends_with_colon_token,
    get_line_continuation,
)

DEDENT_TOKENS = frozenset(["raise", "return", "pass", "break", "continue"])


[docs] def carriage_return(b, cli, *, autoindent=True): """Preliminary parser to determine if 'Enter' key should send command to the xonsh parser for execution or should insert a newline for continued input. Current 'triggers' for inserting a newline are: - Not on first line of buffer and line is non-empty - Previous character is a colon (covers if, for, etc...) - User is in an open paren-block - Line ends with backslash - Any text exists below cursor position (relevant when editing previous multiline blocks) """ doc = b.document at_end_of_line = _is_blank(doc.current_line_after_cursor) current_line_blank = _is_blank(doc.current_line) env = XSH.env indent = env.get("INDENT") if autoindent else "" partial_string_info = check_for_partial_string(doc.text) in_partial_string = ( partial_string_info[0] is not None and partial_string_info[1] is None ) # indent after a colon if ends_with_colon_token(doc.current_line_before_cursor) and at_end_of_line: b.newline(copy_margin=autoindent) b.insert_text(indent, fire_event=False) # if current line isn't blank, check dedent tokens elif ( not current_line_blank and doc.current_line.split(maxsplit=1)[0] in DEDENT_TOKENS and doc.line_count > 1 ): b.newline(copy_margin=autoindent) b.delete_before_cursor(count=len(indent)) elif not doc.on_first_line and not current_line_blank: b.newline(copy_margin=autoindent) elif doc.current_line.endswith(get_line_continuation()): b.newline(copy_margin=autoindent) elif doc.find_next_word_beginning() is not None and ( any(not _is_blank(i) for i in doc.lines_from_current[1:]) ): b.newline(copy_margin=autoindent) elif not current_line_blank and not can_compile(doc.text): b.newline(copy_margin=autoindent) elif current_line_blank and in_partial_string: b.newline(copy_margin=autoindent) else: b.validate_and_handle()
def _is_blank(line): return len(line.strip()) == 0
[docs] def can_compile(src): """Returns whether the code can be compiled, i.e. it is valid xonsh.""" src = src if src.endswith("\n") else src + "\n" src = transform_command(src, show_diff=False) src = src.lstrip() try: XSH.execer.compile(src, mode="single", glbs=None, locs=XSH.ctx) rtn = True except SyntaxError: rtn = False except Exception: rtn = True return rtn
@Condition def tab_insert_indent(): """Check if <Tab> should insert indent instead of starting autocompletion. Checks if there are only whitespaces before the cursor - if so indent should be inserted, otherwise autocompletion. """ before_cursor = get_app().current_buffer.document.current_line_before_cursor return bool(before_cursor.isspace()) @Condition def tab_menu_complete(): """Checks whether completion mode is `menu-complete`""" return XSH.env.get("COMPLETION_MODE") == "menu-complete" @Condition def beginning_of_line(): """Check if cursor is at beginning of a line other than the first line in a multiline document """ app = get_app() before_cursor = app.current_buffer.document.current_line_before_cursor return bool( len(before_cursor) == 0 and not app.current_buffer.document.on_first_line ) @Condition def end_of_line(): """Check if cursor is at the end of a line other than the last line in a multiline document """ d = get_app().current_buffer.document at_end = d.is_cursor_at_the_end_of_line last_line = d.is_cursor_at_the_end return bool(at_end and not last_line) @Condition def should_confirm_completion(): """Check if completion needs confirmation""" return ( XSH.env.get("COMPLETIONS_CONFIRM") and get_app().current_buffer.complete_state ) # Copied from prompt-toolkit's key_binding/bindings/basic.py @Condition def ctrl_d_condition(): """Ctrl-D binding is only active when the default buffer is selected and empty. """ if XSH.env.get("IGNOREEOF"): return False else: app = get_app() buffer_name = app.current_buffer.name return buffer_name == DEFAULT_BUFFER and not app.current_buffer.text @Condition def autopair_condition(): """Check if XONSH_AUTOPAIR is set""" return XSH.env.get("XONSH_AUTOPAIR", False) @Condition def whitespace_or_bracket_before(): """Check if there is whitespace or an opening bracket to the left of the cursor""" d = get_app().current_buffer.document return bool( d.cursor_position == 0 or d.char_before_cursor.isspace() or d.char_before_cursor in "([{" ) @Condition def whitespace_or_bracket_after(): """Check if there is whitespace or a closing bracket to the right of the cursor""" d = get_app().current_buffer.document return bool( d.is_cursor_at_the_end_of_line or d.current_char.isspace() or d.current_char in ")]}" )
[docs] def wrap_selection(buffer, left, right=None): selection_state = buffer.selection_state for start, end in buffer.document.selection_ranges(): buffer.transform_region(start, end, lambda s: f"{left}{s}{right}") # keep the selection of the inner expression # e.g. `echo |Hello World|` -> `echo "|Hello World|"` buffer.cursor_position += 1 selection_state.original_cursor_position += 1 buffer.selection_state = selection_state
[docs] def load_xonsh_bindings(ptk_bindings: KeyBindingsBase) -> KeyBindingsBase: """ Load custom key bindings. Parameters ---------- ptk_bindings : The default prompt toolkit bindings. We need these to add aliases to them. """ key_bindings = KeyBindings() handle = key_bindings.add has_selection = HasSelection() insert_mode = ViInsertMode() | EmacsInsertMode() if XSH.env["XONSH_CTRL_BKSP_DELETION"]: # Not all terminal emulators emit the same keys for backspace, therefore # ptk always maps backspace ("\x7f") to ^H ("\x08"), and all the backspace bindings are registered for ^H. # This means we can't re-map backspace and instead we register a new "real-ctrl-bksp" key. # See https://github.com/xonsh/xonsh/issues/4407 if ON_WINDOWS: # On windows BKSP is "\x08" and CTRL-BKSP is "\x7f" REAL_CTRL_BKSP = "\x7f" # PTK uses a second mapping from prompt_toolkit.input import win32 as ptk_win32 ptk_win32.ConsoleInputReader.mappings[b"\x7f"] = REAL_CTRL_BKSP # type: ignore else: REAL_CTRL_BKSP = "\x08" # Prompt-toolkit allows using single-character keys that aren't in the `Keys` enum. ansi_escape_sequences.ANSI_SEQUENCES[REAL_CTRL_BKSP] = REAL_CTRL_BKSP # type: ignore ansi_escape_sequences.REVERSE_ANSI_SEQUENCES[REAL_CTRL_BKSP] = REAL_CTRL_BKSP # type: ignore @handle(REAL_CTRL_BKSP, filter=insert_mode) def delete_word(event): """Delete a single word (like ALT-backspace)""" get_by_name("backward-kill-word").call(event) @handle(Keys.Tab, filter=tab_insert_indent) def insert_indent(event): """ If there are only whitespaces before current cursor position insert indent instead of autocompleting. """ env = XSH.env event.cli.current_buffer.insert_text(env.get("INDENT")) @handle(Keys.Tab, filter=~tab_insert_indent & tab_menu_complete) def menu_complete_select(event): """Start completion in menu-complete mode, or tab to next completion""" b = event.current_buffer if b.complete_state: b.complete_next() else: b.start_completion(select_first=True) @handle(Keys.ControlX, Keys.ControlE, filter=~has_selection) def open_editor(event): """Open current buffer in editor""" event.current_buffer.open_in_editor(event.cli) @handle(Keys.BackTab, filter=insert_mode) def insert_literal_tab(event): """Insert literal tab on Shift+Tab instead of autocompleting""" b = event.current_buffer if b.complete_state: b.complete_previous() else: env = XSH.env event.cli.current_buffer.insert_text(env.get("INDENT")) def generate_parens_handlers(left, right): @handle(left, filter=autopair_condition) def insert_left_paren(event): buffer = event.cli.current_buffer if has_selection(): wrap_selection(buffer, left, right) elif whitespace_or_bracket_after(): buffer.insert_text(left) buffer.insert_text(right, move_cursor=False) else: buffer.insert_text(left) @handle(right, filter=autopair_condition) def overwrite_right_paren(event): buffer = event.cli.current_buffer if buffer.document.current_char == right: buffer.cursor_position += 1 else: buffer.insert_text(right) generate_parens_handlers("(", ")") generate_parens_handlers("[", "]") generate_parens_handlers("{", "}") def generate_quote_handler(quote): @handle(quote, filter=autopair_condition) def insert_quote(event): buffer = event.cli.current_buffer if has_selection(): wrap_selection(buffer, quote, quote) elif buffer.document.current_char == quote: buffer.cursor_position += 1 elif whitespace_or_bracket_before() and whitespace_or_bracket_after(): buffer.insert_text(quote) buffer.insert_text(quote, move_cursor=False) else: buffer.insert_text(quote) generate_quote_handler("'") generate_quote_handler('"') @handle(Keys.Backspace, filter=autopair_condition) def delete_brackets_or_quotes(event): """Delete empty pair of brackets or quotes""" buffer = event.cli.current_buffer before = buffer.document.char_before_cursor after = buffer.document.current_char if any( [before == b and after == a for (b, a) in ["()", "[]", "{}", "''", '""']] ): buffer.delete(1) buffer.delete_before_cursor(1) @handle(Keys.ControlD, filter=ctrl_d_condition) def call_exit_alias(event): """Use xonsh exit function""" b = event.cli.current_buffer b.validate_and_handle() xonsh_exit([]) @handle(Keys.ControlJ, filter=IsMultiline() & insert_mode) @handle(Keys.ControlM, filter=IsMultiline() & insert_mode) def multiline_carriage_return(event): """Wrapper around carriage_return multiline parser""" b = event.cli.current_buffer carriage_return(b, event.cli) @handle(Keys.ControlJ, filter=should_confirm_completion) @handle(Keys.ControlM, filter=should_confirm_completion) def enter_confirm_completion(event): """Ignore <enter> (confirm completion)""" event.current_buffer.complete_state = None @handle(Keys.Escape, filter=should_confirm_completion) def esc_cancel_completion(event): """Use <ESC> to cancel completion""" event.cli.current_buffer.cancel_completion() @handle(Keys.Escape, Keys.ControlJ) def execute_block_now(event): """Execute a block of text irrespective of cursor position""" b = event.cli.current_buffer b.validate_and_handle() @handle(Keys.Left, filter=beginning_of_line) def wrap_cursor_back(event): """Move cursor to end of previous line unless at beginning of document """ b = event.cli.current_buffer b.cursor_up(count=1) relative_end_index = b.document.get_end_of_line_position() b.cursor_right(count=relative_end_index) @handle(Keys.Right, filter=end_of_line) def wrap_cursor_forward(event): """Move cursor to beginning of next line unless at end of document""" b = event.cli.current_buffer relative_begin_index = b.document.get_start_of_line_position() b.cursor_left(count=abs(relative_begin_index)) b.cursor_down(count=1) @handle(Keys.ControlM, filter=IsSearching()) @handle(Keys.ControlJ, filter=IsSearching()) def accept_search(event): search.accept_search() @handle(Keys.ControlZ) def skip_control_z(event): """Prevents the writing of ^Z to the prompt, if Ctrl+Z was pressed during the previous command. """ pass @handle(Keys.ControlX, Keys.ControlX, filter=has_selection) def _cut(event): """Cut selected text.""" data = event.current_buffer.cut_selection() event.app.clipboard.set_data(data) @handle(Keys.ControlX, Keys.ControlC, filter=has_selection) def _copy(event): """Copy selected text.""" data = event.current_buffer.copy_selection() event.app.clipboard.set_data(data) @handle(Keys.ControlV, filter=insert_mode | has_selection) def _yank(event): """Paste selected text.""" buff = event.current_buffer if buff.selection_state: buff.cut_selection() get_by_name("yank").call(event) def create_alias(new_keys, original_keys): bindings = ptk_bindings.get_bindings_for_keys(tuple(original_keys)) for original_binding in bindings: handle(*new_keys, filter=original_binding.filter)(original_binding.handler) # Complete a single auto-suggestion word create_alias([Keys.ControlRight], ["escape", "f"]) # since macOS uses Control as reserved, then we use the alt/option key instead # which is mapped as the "escape" key create_alias(["escape", "right"], ["escape", "f"]) return key_bindings