| | """ |
| | Module to define and register Terminal IPython shortcuts with |
| | :mod:`prompt_toolkit` |
| | """ |
| |
|
| | |
| | |
| |
|
| | import os |
| | import signal |
| | import sys |
| | import warnings |
| | from dataclasses import dataclass |
| | from typing import Callable, Any, Optional, List |
| |
|
| | from prompt_toolkit.application.current import get_app |
| | from prompt_toolkit.key_binding import KeyBindings |
| | from prompt_toolkit.key_binding.key_processor import KeyPressEvent |
| | from prompt_toolkit.key_binding.bindings import named_commands as nc |
| | from prompt_toolkit.key_binding.bindings.completion import ( |
| | display_completions_like_readline, |
| | ) |
| | from prompt_toolkit.key_binding.vi_state import InputMode, ViState |
| | from prompt_toolkit.filters import Condition |
| |
|
| | from IPython.core.getipython import get_ipython |
| | from IPython.terminal.shortcuts import auto_match as match |
| | from IPython.terminal.shortcuts import auto_suggest |
| | from IPython.terminal.shortcuts.filters import filter_from_string |
| | from IPython.utils.decorators import undoc |
| |
|
| | from prompt_toolkit.enums import DEFAULT_BUFFER |
| |
|
| | __all__ = ["create_ipython_shortcuts"] |
| |
|
| |
|
| | @dataclass |
| | class BaseBinding: |
| | command: Callable[[KeyPressEvent], Any] |
| | keys: List[str] |
| |
|
| |
|
| | @dataclass |
| | class RuntimeBinding(BaseBinding): |
| | filter: Condition |
| |
|
| |
|
| | @dataclass |
| | class Binding(BaseBinding): |
| | |
| | |
| | |
| | |
| | condition: Optional[str] = None |
| |
|
| | def __post_init__(self): |
| | if self.condition: |
| | self.filter = filter_from_string(self.condition) |
| | else: |
| | self.filter = None |
| |
|
| |
|
| | def create_identifier(handler: Callable): |
| | parts = handler.__module__.split(".") |
| | name = handler.__name__ |
| | package = parts[0] |
| | if len(parts) > 1: |
| | final_module = parts[-1] |
| | return f"{package}:{final_module}.{name}" |
| | else: |
| | return f"{package}:{name}" |
| |
|
| |
|
| | AUTO_MATCH_BINDINGS = [ |
| | *[ |
| | Binding( |
| | cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end" |
| | ) |
| | for key, cmd in match.auto_match_parens.items() |
| | ], |
| | *[ |
| | |
| | Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix") |
| | for key, cmd in match.auto_match_parens_raw_string.items() |
| | ], |
| | Binding( |
| | match.double_quote, |
| | ['"'], |
| | "focused_insert" |
| | " & auto_match" |
| | " & not_inside_unclosed_string" |
| | " & preceded_by_paired_double_quotes" |
| | " & followed_by_closing_paren_or_end", |
| | ), |
| | Binding( |
| | match.single_quote, |
| | ["'"], |
| | "focused_insert" |
| | " & auto_match" |
| | " & not_inside_unclosed_string" |
| | " & preceded_by_paired_single_quotes" |
| | " & followed_by_closing_paren_or_end", |
| | ), |
| | Binding( |
| | match.docstring_double_quotes, |
| | ['"'], |
| | "focused_insert" |
| | " & auto_match" |
| | " & not_inside_unclosed_string" |
| | " & preceded_by_two_double_quotes", |
| | ), |
| | Binding( |
| | match.docstring_single_quotes, |
| | ["'"], |
| | "focused_insert" |
| | " & auto_match" |
| | " & not_inside_unclosed_string" |
| | " & preceded_by_two_single_quotes", |
| | ), |
| | Binding( |
| | match.skip_over, |
| | [")"], |
| | "focused_insert & auto_match & followed_by_closing_round_paren", |
| | ), |
| | Binding( |
| | match.skip_over, |
| | ["]"], |
| | "focused_insert & auto_match & followed_by_closing_bracket", |
| | ), |
| | Binding( |
| | match.skip_over, |
| | ["}"], |
| | "focused_insert & auto_match & followed_by_closing_brace", |
| | ), |
| | Binding( |
| | match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote" |
| | ), |
| | Binding( |
| | match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote" |
| | ), |
| | Binding( |
| | match.delete_pair, |
| | ["backspace"], |
| | "focused_insert" |
| | " & preceded_by_opening_round_paren" |
| | " & auto_match" |
| | " & followed_by_closing_round_paren", |
| | ), |
| | Binding( |
| | match.delete_pair, |
| | ["backspace"], |
| | "focused_insert" |
| | " & preceded_by_opening_bracket" |
| | " & auto_match" |
| | " & followed_by_closing_bracket", |
| | ), |
| | Binding( |
| | match.delete_pair, |
| | ["backspace"], |
| | "focused_insert" |
| | " & preceded_by_opening_brace" |
| | " & auto_match" |
| | " & followed_by_closing_brace", |
| | ), |
| | Binding( |
| | match.delete_pair, |
| | ["backspace"], |
| | "focused_insert" |
| | " & preceded_by_double_quote" |
| | " & auto_match" |
| | " & followed_by_double_quote", |
| | ), |
| | Binding( |
| | match.delete_pair, |
| | ["backspace"], |
| | "focused_insert" |
| | " & preceded_by_single_quote" |
| | " & auto_match" |
| | " & followed_by_single_quote", |
| | ), |
| | ] |
| |
|
| | AUTO_SUGGEST_BINDINGS = [ |
| | |
| | |
| | |
| | |
| | Binding( |
| | auto_suggest.accept_or_jump_to_end, |
| | ["end"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept_or_jump_to_end, |
| | ["c-e"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept, |
| | ["c-f"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept, |
| | ["right"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept_word, |
| | ["escape", "f"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept_token, |
| | ["c-right"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.discard, |
| | ["escape"], |
| | |
| | |
| | "has_suggestion & default_buffer_focused & emacs_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.discard, |
| | ["delete"], |
| | "has_suggestion & default_buffer_focused & emacs_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.swap_autosuggestion_up, |
| | ["c-up"], |
| | "navigable_suggestions" |
| | " & ~has_line_above" |
| | " & has_suggestion" |
| | " & default_buffer_focused", |
| | ), |
| | Binding( |
| | auto_suggest.swap_autosuggestion_down, |
| | ["c-down"], |
| | "navigable_suggestions" |
| | " & ~has_line_below" |
| | " & has_suggestion" |
| | " & default_buffer_focused", |
| | ), |
| | Binding( |
| | auto_suggest.up_and_update_hint, |
| | ["c-up"], |
| | "has_line_above & navigable_suggestions & default_buffer_focused", |
| | ), |
| | Binding( |
| | auto_suggest.down_and_update_hint, |
| | ["c-down"], |
| | "has_line_below & navigable_suggestions & default_buffer_focused", |
| | ), |
| | Binding( |
| | auto_suggest.accept_character, |
| | ["escape", "right"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept_and_move_cursor_left, |
| | ["c-left"], |
| | "has_suggestion & default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.accept_and_keep_cursor, |
| | ["escape", "down"], |
| | "has_suggestion & default_buffer_focused & emacs_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.backspace_and_resume_hint, |
| | ["backspace"], |
| | |
| | "default_buffer_focused & emacs_like_insert_mode", |
| | ), |
| | Binding( |
| | auto_suggest.resume_hinting, |
| | ["right"], |
| | "is_cursor_at_the_end_of_line" |
| | " & default_buffer_focused" |
| | " & emacs_like_insert_mode" |
| | " & pass_through", |
| | ), |
| | ] |
| |
|
| |
|
| | SIMPLE_CONTROL_BINDINGS = [ |
| | Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim") |
| | for key, cmd in { |
| | "c-a": nc.beginning_of_line, |
| | "c-b": nc.backward_char, |
| | "c-k": nc.kill_line, |
| | "c-w": nc.backward_kill_word, |
| | "c-y": nc.yank, |
| | "c-_": nc.undo, |
| | }.items() |
| | ] |
| |
|
| |
|
| | ALT_AND_COMOBO_CONTROL_BINDINGS = [ |
| | Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim") |
| | for keys, cmd in { |
| | |
| | ("c-x", "c-e"): nc.edit_and_execute, |
| | ("c-x", "e"): nc.edit_and_execute, |
| | |
| | ("escape", "b"): nc.backward_word, |
| | ("escape", "c"): nc.capitalize_word, |
| | ("escape", "d"): nc.kill_word, |
| | ("escape", "h"): nc.backward_kill_word, |
| | ("escape", "l"): nc.downcase_word, |
| | ("escape", "u"): nc.uppercase_word, |
| | ("escape", "y"): nc.yank_pop, |
| | ("escape", "."): nc.yank_last_arg, |
| | }.items() |
| | ] |
| |
|
| |
|
| | def add_binding(bindings: KeyBindings, binding: Binding): |
| | bindings.add( |
| | *binding.keys, |
| | **({"filter": binding.filter} if binding.filter is not None else {}), |
| | )(binding.command) |
| |
|
| |
|
| | def create_ipython_shortcuts(shell, skip=None) -> KeyBindings: |
| | """Set up the prompt_toolkit keyboard shortcuts for IPython. |
| | |
| | Parameters |
| | ---------- |
| | shell: InteractiveShell |
| | The current IPython shell Instance |
| | skip: List[Binding] |
| | Bindings to skip. |
| | |
| | Returns |
| | ------- |
| | KeyBindings |
| | the keybinding instance for prompt toolkit. |
| | |
| | """ |
| | kb = KeyBindings() |
| | skip = skip or [] |
| | for binding in KEY_BINDINGS: |
| | skip_this_one = False |
| | for to_skip in skip: |
| | if ( |
| | to_skip.command == binding.command |
| | and to_skip.filter == binding.filter |
| | and to_skip.keys == binding.keys |
| | ): |
| | skip_this_one = True |
| | break |
| | if skip_this_one: |
| | continue |
| | add_binding(kb, binding) |
| |
|
| | def get_input_mode(self): |
| | app = get_app() |
| | app.ttimeoutlen = shell.ttimeoutlen |
| | app.timeoutlen = shell.timeoutlen |
| |
|
| | return self._input_mode |
| |
|
| | def set_input_mode(self, mode): |
| | shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) |
| | cursor = "\x1b[{} q".format(shape) |
| |
|
| | sys.stdout.write(cursor) |
| | sys.stdout.flush() |
| |
|
| | self._input_mode = mode |
| |
|
| | if shell.editing_mode == "vi" and shell.modal_cursor: |
| | ViState._input_mode = InputMode.INSERT |
| | ViState.input_mode = property(get_input_mode, set_input_mode) |
| |
|
| | return kb |
| |
|
| |
|
| | def reformat_and_execute(event): |
| | """Reformat code and execute it""" |
| | shell = get_ipython() |
| | reformat_text_before_cursor( |
| | event.current_buffer, event.current_buffer.document, shell |
| | ) |
| | event.current_buffer.validate_and_handle() |
| |
|
| |
|
| | def reformat_text_before_cursor(buffer, document, shell): |
| | text = buffer.delete_before_cursor(len(document.text[: document.cursor_position])) |
| | try: |
| | formatted_text = shell.reformat_handler(text) |
| | buffer.insert_text(formatted_text) |
| | except Exception as e: |
| | buffer.insert_text(text) |
| |
|
| |
|
| | def handle_return_or_newline_or_execute(event): |
| | shell = get_ipython() |
| | if getattr(shell, "handle_return", None): |
| | return shell.handle_return(shell)(event) |
| | else: |
| | return newline_or_execute_outer(shell)(event) |
| |
|
| |
|
| | def newline_or_execute_outer(shell): |
| | def newline_or_execute(event): |
| | """When the user presses return, insert a newline or execute the code.""" |
| | b = event.current_buffer |
| | d = b.document |
| |
|
| | if b.complete_state: |
| | cc = b.complete_state.current_completion |
| | if cc: |
| | b.apply_completion(cc) |
| | else: |
| | b.cancel_completion() |
| | return |
| |
|
| | |
| | |
| | if d.line_count == 1: |
| | check_text = d.text |
| | else: |
| | check_text = d.text[: d.cursor_position] |
| | status, indent = shell.check_complete(check_text) |
| |
|
| | |
| | |
| | after_cursor = d.text[d.cursor_position :] |
| | reformatted = False |
| | if not after_cursor.strip(): |
| | reformat_text_before_cursor(b, d, shell) |
| | reformatted = True |
| | if not ( |
| | d.on_last_line |
| | or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() |
| | ): |
| | if shell.autoindent: |
| | b.insert_text("\n" + indent) |
| | else: |
| | b.insert_text("\n") |
| | return |
| |
|
| | if (status != "incomplete") and b.accept_handler: |
| | if not reformatted: |
| | reformat_text_before_cursor(b, d, shell) |
| | b.validate_and_handle() |
| | else: |
| | if shell.autoindent: |
| | b.insert_text("\n" + indent) |
| | else: |
| | b.insert_text("\n") |
| |
|
| | return newline_or_execute |
| |
|
| |
|
| | def previous_history_or_previous_completion(event): |
| | """ |
| | Control-P in vi edit mode on readline is history next, unlike default prompt toolkit. |
| | |
| | If completer is open this still select previous completion. |
| | """ |
| | event.current_buffer.auto_up() |
| |
|
| |
|
| | def next_history_or_next_completion(event): |
| | """ |
| | Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit. |
| | |
| | If completer is open this still select next completion. |
| | """ |
| | event.current_buffer.auto_down() |
| |
|
| |
|
| | def dismiss_completion(event): |
| | """Dismiss completion""" |
| | b = event.current_buffer |
| | if b.complete_state: |
| | b.cancel_completion() |
| |
|
| |
|
| | def reset_buffer(event): |
| | """Reset buffer""" |
| | b = event.current_buffer |
| | if b.complete_state: |
| | b.cancel_completion() |
| | else: |
| | b.reset() |
| |
|
| |
|
| | def reset_search_buffer(event): |
| | """Reset search buffer""" |
| | if event.current_buffer.document.text: |
| | event.current_buffer.reset() |
| | else: |
| | event.app.layout.focus(DEFAULT_BUFFER) |
| |
|
| |
|
| | def suspend_to_bg(event): |
| | """Suspend to background""" |
| | event.app.suspend_to_background() |
| |
|
| |
|
| | def quit(event): |
| | """ |
| | Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise. |
| | |
| | On platforms that support SIGQUIT, send SIGQUIT to the current process. |
| | On other platforms, just exit the process with a message. |
| | """ |
| | sigquit = getattr(signal, "SIGQUIT", None) |
| | if sigquit is not None: |
| | os.kill(0, signal.SIGQUIT) |
| | else: |
| | sys.exit("Quit") |
| |
|
| |
|
| | def indent_buffer(event): |
| | """Indent buffer""" |
| | event.current_buffer.insert_text(" " * 4) |
| |
|
| |
|
| | def newline_autoindent(event): |
| | """Insert a newline after the cursor indented appropriately. |
| | |
| | Fancier version of former ``newline_with_copy_margin`` which should |
| | compute the correct indentation of the inserted line. That is to say, indent |
| | by 4 extra space after a function definition, class definition, context |
| | manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``. |
| | """ |
| | shell = get_ipython() |
| | inputsplitter = shell.input_transformer_manager |
| | b = event.current_buffer |
| | d = b.document |
| |
|
| | if b.complete_state: |
| | b.cancel_completion() |
| | text = d.text[: d.cursor_position] + "\n" |
| | _, indent = inputsplitter.check_complete(text) |
| | b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False) |
| |
|
| |
|
| | def open_input_in_editor(event): |
| | """Open code from input in external editor""" |
| | event.app.current_buffer.open_in_editor() |
| |
|
| |
|
| | if sys.platform == "win32": |
| | from IPython.core.error import TryNext |
| | from IPython.lib.clipboard import ( |
| | ClipboardEmpty, |
| | tkinter_clipboard_get, |
| | win32_clipboard_get, |
| | ) |
| |
|
| | @undoc |
| | def win_paste(event): |
| | try: |
| | text = win32_clipboard_get() |
| | except TryNext: |
| | try: |
| | text = tkinter_clipboard_get() |
| | except (TryNext, ClipboardEmpty): |
| | return |
| | except ClipboardEmpty: |
| | return |
| | event.current_buffer.insert_text(text.replace("\t", " " * 4)) |
| |
|
| | else: |
| |
|
| | @undoc |
| | def win_paste(event): |
| | """Stub used on other platforms""" |
| | pass |
| |
|
| |
|
| | KEY_BINDINGS = [ |
| | Binding( |
| | handle_return_or_newline_or_execute, |
| | ["enter"], |
| | "default_buffer_focused & ~has_selection & insert_mode", |
| | ), |
| | Binding( |
| | reformat_and_execute, |
| | ["escape", "enter"], |
| | "default_buffer_focused & ~has_selection & insert_mode & ebivim", |
| | ), |
| | Binding(quit, ["c-\\"]), |
| | Binding( |
| | previous_history_or_previous_completion, |
| | ["c-p"], |
| | "vi_insert_mode & default_buffer_focused", |
| | ), |
| | Binding( |
| | next_history_or_next_completion, |
| | ["c-n"], |
| | "vi_insert_mode & default_buffer_focused", |
| | ), |
| | Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"), |
| | Binding(reset_buffer, ["c-c"], "default_buffer_focused"), |
| | Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"), |
| | Binding(suspend_to_bg, ["c-z"], "supports_suspend"), |
| | Binding( |
| | indent_buffer, |
| | ["tab"], |
| | "default_buffer_focused" |
| | " & ~has_selection" |
| | " & insert_mode" |
| | " & cursor_in_leading_ws", |
| | ), |
| | Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"), |
| | Binding(open_input_in_editor, ["f2"], "default_buffer_focused"), |
| | *AUTO_MATCH_BINDINGS, |
| | *AUTO_SUGGEST_BINDINGS, |
| | Binding( |
| | display_completions_like_readline, |
| | ["c-i"], |
| | "readline_like_completions" |
| | " & default_buffer_focused" |
| | " & ~has_selection" |
| | " & insert_mode" |
| | " & ~cursor_in_leading_ws", |
| | ), |
| | Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"), |
| | *SIMPLE_CONTROL_BINDINGS, |
| | *ALT_AND_COMOBO_CONTROL_BINDINGS, |
| | ] |
| |
|