prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.11k stars 717 forks source link

Call handler of keybinding even if there are multiple matches #1754

Open krassowski opened 1 year ago

krassowski commented 1 year ago

Currently if multiple matching shortcut handlers are found (after filtering) only one handler is called (the one that was defined last). This is implemented by this logic of key processor:

https://github.com/prompt-toolkit/python-prompt-toolkit/blob/07412a37b38dd70a3d63f2966a21f0a645133264/src/prompt_toolkit/key_binding/key_processor.py#L185-L187

In other words currently the bindings (after filtering) are always exclusive.

There are some limitations with this approach:

Would it be in scope to have a new option for bindings that allows the handler to be called even if another binding matches? This could have public interface similar to eager option, for example:

@bindings.add('a', exclusive=False)     # default would be `exclusive=True`
def binding_2(event):
    ...

and the logic could be adapted to:

 # Exact matches found, call handler.
 if not is_prefix_of_longer_match and matches:
     exclusive_matches = [match for match in matches if match.exclusive]
     non_exclusive_matches = [match for match in matches if not match.exclusive]
     # fire only one exclusive match (this guarantees backward-compatibility)
     if exclusive_matches:
         self._call_handler(exclusive_matches[-1], key_sequence=buffer[:])
     # call all non-exclusive matches (regardless of the exclusive matches)
     for match in non_exclusive_matches:
         self._call_handler(match, key_sequence=buffer[:])

The proposed logic does not stop after the exclusive match is found as stopping there would still lead to the problem that motivates this proposal: newly added handler would suppress a built-in handler. This suggests that a different name could be more appropriate, for example unconditional=True.

For context, here is the downstream issue: https://github.com/ipython/ipython/issues/14070 and workaround: https://github.com/ipython/ipython/pull/14080