diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py index 2f7effb..12890f4 100644 --- a/IPython/terminal/shortcuts/__init__.py +++ b/IPython/terminal/shortcuts/__init__.py @@ -279,7 +279,8 @@ AUTO_SUGGEST_BINDINGS = [ ["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 9b03370..65f9157 100644 --- a/IPython/terminal/shortcuts/auto_suggest.py +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -20,6 +20,8 @@ from prompt_toolkit.layout.processors import ( 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 5a582af..7c9d6a9 100644 --- a/IPython/terminal/shortcuts/filters.py +++ b/IPython/terminal/shortcuts/filters.py @@ -13,7 +13,8 @@ from typing import Callable, Dict, Union 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 @@ KEYBINDING_FILTERS = { "followed_by_single_quote": following_text("^'"), "navigable_suggestions": navigable_suggestions, "cursor_in_leading_ws": cursor_in_leading_ws, + "pass_through": pass_through, }