From e7861a10e1505500d6cfda08d68314ba30875f2f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 2 Jun 2023 10:35:50 +0200 Subject: [PATCH] Backport PR #14080: Add pass-through filter for shortcuts --- IPython/terminal/shortcuts/__init__.py | 3 +- IPython/terminal/shortcuts/auto_suggest.py | 7 ++++- IPython/terminal/shortcuts/filters.py | 34 +++++++++++++++++++++- docs/autogen_shortcuts.py | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 2f7effbe78c..12890f4ab6e 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -279,7 +279,8 @@ def create_identifier(handler: Callable): ["right"], "is_cursor_at_the_end_of_line" " & default_buffer_focused" - " & emacs_like_insert_mode", + " & emacs_like_insert_mode" + " & pass_through", ), ] diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py index 9b03370e9e3..65f91577ce9 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -20,6 +20,8 @@ from IPython.core.getipython import get_ipython from IPython.utils.tokenutil import generate_tokens +from .filters import pass_through + def _get_query(document: Document): return document.lines[document.cursor_position_row] @@ -267,7 +269,10 @@ def backspace_and_resume_hint(event: KeyPressEvent): def resume_hinting(event: KeyPressEvent): """Resume autosuggestions""" - return _update_hint(event.current_buffer) + pass_through.reply(event) + # Order matters: if update happened first and event reply second, the + # suggestion would be auto-accepted if both actions are bound to same key. + _update_hint(event.current_buffer) def up_and_update_hint(event: KeyPressEvent): diff --git a/IPython/terminal/shortcuts/filters.py b/IPython/terminal/shortcuts/filters.py index 5a582afedcc..7c9d6a9c41d 100644 --- a/IPython/terminal/shortcuts/filters.py +++ b/IPython/terminal/shortcuts/filters.py @@ -13,7 +13,8 @@ from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions +from prompt_toolkit.key_binding import KeyPressEvent +from prompt_toolkit.filters import Condition, Filter, emacs_insert_mode, has_completions from prompt_toolkit.filters import has_focus as has_focus_impl from prompt_toolkit.filters import ( Always, @@ -175,6 +176,36 @@ def is_windows_os(): return sys.platform == "win32" +class PassThrough(Filter): + """A filter allowing to implement pass-through behaviour of keybindings. + + Prompt toolkit key processor dispatches only one event per binding match, + which means that adding a new shortcut will suppress the old shortcut + if the keybindings are the same (unless one is filtered out). + + To stop a shortcut binding from suppressing other shortcuts: + - add the `pass_through` filter to list of filter, and + - call `pass_through.reply(event)` in the shortcut handler. + """ + + def __init__(self): + self._is_replying = False + + def reply(self, event: KeyPressEvent): + self._is_replying = True + try: + event.key_processor.reset() + event.key_processor.feed_multiple(event.key_sequence) + event.key_processor.process_keys() + finally: + self._is_replying = False + + def __call__(self): + return not self._is_replying + + +pass_through = PassThrough() + # these one is callable and re-used multiple times hence needs to be # only defined once beforhand so that transforming back to human-readable # names works well in the documentation. @@ -248,6 +279,7 @@ def is_windows_os(): "followed_by_single_quote": following_text("^'"), "navigable_suggestions": navigable_suggestions, "cursor_in_leading_ws": cursor_in_leading_ws, + "pass_through": pass_through, } diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index 387c105f6f2..23b47111665 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -88,6 +88,8 @@ def format_filter( return result elif s in ["Never", "Always"]: return s.lower() + elif s == "PassThrough": + return "pass_through" else: raise ValueError(f"Unknown filter type: {filter_}")