From e7861a10e1505500d6cfda08d68314ba30875f2f 2023-09-12 07:54:21
From: Matthias Bussonnier <bussonniermatthias@gmail.com>
Date: 2023-09-12 07:54:21
Subject: [PATCH] Backport PR #14080: Add pass-through filter for shortcuts

---

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,
 }
 
 
diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py
index 387c105..23b4711 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_}")