| """ |
| 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, |
| ] |
|
|