Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/key_binding/key_bindings.py: 35%
227 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1"""
2Key bindings registry.
4A `KeyBindings` object is a container that holds a list of key bindings. It has a
5very efficient internal data structure for checking which key bindings apply
6for a pressed key.
8Typical usage::
10 kb = KeyBindings()
12 @kb.add(Keys.ControlX, Keys.ControlC, filter=INSERT)
13 def handler(event):
14 # Handle ControlX-ControlC key sequence.
15 pass
17It is also possible to combine multiple KeyBindings objects. We do this in the
18default key bindings. There are some KeyBindings objects that contain the Emacs
19bindings, while others contain the Vi bindings. They are merged together using
20`merge_key_bindings`.
22We also have a `ConditionalKeyBindings` object that can enable/disable a group of
23key bindings at once.
26It is also possible to add a filter to a function, before a key binding has
27been assigned, through the `key_binding` decorator.::
29 # First define a key handler with the `filter`.
30 @key_binding(filter=condition)
31 def my_key_binding(event):
32 ...
34 # Later, add it to the key bindings.
35 kb.add(Keys.A, my_key_binding)
36"""
37from __future__ import annotations
39from abc import ABCMeta, abstractmethod, abstractproperty
40from inspect import isawaitable
41from typing import (
42 TYPE_CHECKING,
43 Any,
44 Callable,
45 Coroutine,
46 Hashable,
47 Sequence,
48 Tuple,
49 TypeVar,
50 Union,
51 cast,
52)
54from prompt_toolkit.cache import SimpleCache
55from prompt_toolkit.filters import FilterOrBool, Never, to_filter
56from prompt_toolkit.keys import KEY_ALIASES, Keys
58if TYPE_CHECKING:
59 # Avoid circular imports.
60 from .key_processor import KeyPressEvent
62 # The only two return values for a mouse handler (and key bindings) are
63 # `None` and `NotImplemented`. For the type checker it's best to annotate
64 # this as `object`. (The consumer never expects a more specific instance:
65 # checking for NotImplemented can be done using `is NotImplemented`.)
66 NotImplementedOrNone = object
67 # Other non-working options are:
68 # * Optional[Literal[NotImplemented]]
69 # --> Doesn't work, Literal can't take an Any.
70 # * None
71 # --> Doesn't work. We can't assign the result of a function that
72 # returns `None` to a variable.
73 # * Any
74 # --> Works, but too broad.
77__all__ = [
78 "NotImplementedOrNone",
79 "Binding",
80 "KeyBindingsBase",
81 "KeyBindings",
82 "ConditionalKeyBindings",
83 "merge_key_bindings",
84 "DynamicKeyBindings",
85 "GlobalOnlyKeyBindings",
86]
88# Key bindings can be regular functions or coroutines.
89# In both cases, if they return `NotImplemented`, the UI won't be invalidated.
90# This is mainly used in case of mouse move events, to prevent excessive
91# repainting during mouse move events.
92KeyHandlerCallable = Callable[
93 ["KeyPressEvent"],
94 Union["NotImplementedOrNone", Coroutine[Any, Any, "NotImplementedOrNone"]],
95]
98class Binding:
99 """
100 Key binding: (key sequence + handler + filter).
101 (Immutable binding class.)
103 :param record_in_macro: When True, don't record this key binding when a
104 macro is recorded.
105 """
107 def __init__(
108 self,
109 keys: tuple[Keys | str, ...],
110 handler: KeyHandlerCallable,
111 filter: FilterOrBool = True,
112 eager: FilterOrBool = False,
113 is_global: FilterOrBool = False,
114 save_before: Callable[[KeyPressEvent], bool] = (lambda e: True),
115 record_in_macro: FilterOrBool = True,
116 ) -> None:
117 self.keys = keys
118 self.handler = handler
119 self.filter = to_filter(filter)
120 self.eager = to_filter(eager)
121 self.is_global = to_filter(is_global)
122 self.save_before = save_before
123 self.record_in_macro = to_filter(record_in_macro)
125 def call(self, event: KeyPressEvent) -> None:
126 result = self.handler(event)
128 # If the handler is a coroutine, create an asyncio task.
129 if isawaitable(result):
130 awaitable = cast(Coroutine[Any, Any, "NotImplementedOrNone"], result)
132 async def bg_task() -> None:
133 result = await awaitable
134 if result != NotImplemented:
135 event.app.invalidate()
137 event.app.create_background_task(bg_task())
139 elif result != NotImplemented:
140 event.app.invalidate()
142 def __repr__(self) -> str:
143 return "{}(keys={!r}, handler={!r})".format(
144 self.__class__.__name__,
145 self.keys,
146 self.handler,
147 )
150# Sequence of keys presses.
151KeysTuple = Tuple[Union[Keys, str], ...]
154class KeyBindingsBase(metaclass=ABCMeta):
155 """
156 Interface for a KeyBindings.
157 """
159 @abstractproperty
160 def _version(self) -> Hashable:
161 """
162 For cache invalidation. - This should increase every time that
163 something changes.
164 """
165 return 0
167 @abstractmethod
168 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
169 """
170 Return a list of key bindings that can handle these keys.
171 (This return also inactive bindings, so the `filter` still has to be
172 called, for checking it.)
174 :param keys: tuple of keys.
175 """
176 return []
178 @abstractmethod
179 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
180 """
181 Return a list of key bindings that handle a key sequence starting with
182 `keys`. (It does only return bindings for which the sequences are
183 longer than `keys`. And like `get_bindings_for_keys`, it also includes
184 inactive bindings.)
186 :param keys: tuple of keys.
187 """
188 return []
190 @abstractproperty
191 def bindings(self) -> list[Binding]:
192 """
193 List of `Binding` objects.
194 (These need to be exposed, so that `KeyBindings` objects can be merged
195 together.)
196 """
197 return []
199 # `add` and `remove` don't have to be part of this interface.
202T = TypeVar("T", bound=Union[KeyHandlerCallable, Binding])
205class KeyBindings(KeyBindingsBase):
206 """
207 A container for a set of key bindings.
209 Example usage::
211 kb = KeyBindings()
213 @kb.add('c-t')
214 def _(event):
215 print('Control-T pressed')
217 @kb.add('c-a', 'c-b')
218 def _(event):
219 print('Control-A pressed, followed by Control-B')
221 @kb.add('c-x', filter=is_searching)
222 def _(event):
223 print('Control-X pressed') # Works only if we are searching.
225 """
227 def __init__(self) -> None:
228 self._bindings: list[Binding] = []
229 self._get_bindings_for_keys_cache: SimpleCache[
230 KeysTuple, list[Binding]
231 ] = SimpleCache(maxsize=10000)
232 self._get_bindings_starting_with_keys_cache: SimpleCache[
233 KeysTuple, list[Binding]
234 ] = SimpleCache(maxsize=1000)
235 self.__version = 0 # For cache invalidation.
237 def _clear_cache(self) -> None:
238 self.__version += 1
239 self._get_bindings_for_keys_cache.clear()
240 self._get_bindings_starting_with_keys_cache.clear()
242 @property
243 def bindings(self) -> list[Binding]:
244 return self._bindings
246 @property
247 def _version(self) -> Hashable:
248 return self.__version
250 def add(
251 self,
252 *keys: Keys | str,
253 filter: FilterOrBool = True,
254 eager: FilterOrBool = False,
255 is_global: FilterOrBool = False,
256 save_before: Callable[[KeyPressEvent], bool] = (lambda e: True),
257 record_in_macro: FilterOrBool = True,
258 ) -> Callable[[T], T]:
259 """
260 Decorator for adding a key bindings.
262 :param filter: :class:`~prompt_toolkit.filters.Filter` to determine
263 when this key binding is active.
264 :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`.
265 When True, ignore potential longer matches when this key binding is
266 hit. E.g. when there is an active eager key binding for Ctrl-X,
267 execute the handler immediately and ignore the key binding for
268 Ctrl-X Ctrl-E of which it is a prefix.
269 :param is_global: When this key bindings is added to a `Container` or
270 `Control`, make it a global (always active) binding.
271 :param save_before: Callable that takes an `Event` and returns True if
272 we should save the current buffer, before handling the event.
273 (That's the default.)
274 :param record_in_macro: Record these key bindings when a macro is
275 being recorded. (True by default.)
276 """
277 assert keys
279 keys = tuple(_parse_key(k) for k in keys)
281 if isinstance(filter, Never):
282 # When a filter is Never, it will always stay disabled, so in that
283 # case don't bother putting it in the key bindings. It will slow
284 # down every key press otherwise.
285 def decorator(func: T) -> T:
286 return func
288 else:
290 def decorator(func: T) -> T:
291 if isinstance(func, Binding):
292 # We're adding an existing Binding object.
293 self.bindings.append(
294 Binding(
295 keys,
296 func.handler,
297 filter=func.filter & to_filter(filter),
298 eager=to_filter(eager) | func.eager,
299 is_global=to_filter(is_global) | func.is_global,
300 save_before=func.save_before,
301 record_in_macro=func.record_in_macro,
302 )
303 )
304 else:
305 self.bindings.append(
306 Binding(
307 keys,
308 cast(KeyHandlerCallable, func),
309 filter=filter,
310 eager=eager,
311 is_global=is_global,
312 save_before=save_before,
313 record_in_macro=record_in_macro,
314 )
315 )
316 self._clear_cache()
318 return func
320 return decorator
322 def remove(self, *args: Keys | str | KeyHandlerCallable) -> None:
323 """
324 Remove a key binding.
326 This expects either a function that was given to `add` method as
327 parameter or a sequence of key bindings.
329 Raises `ValueError` when no bindings was found.
331 Usage::
333 remove(handler) # Pass handler.
334 remove('c-x', 'c-a') # Or pass the key bindings.
335 """
336 found = False
338 if callable(args[0]):
339 assert len(args) == 1
340 function = args[0]
342 # Remove the given function.
343 for b in self.bindings:
344 if b.handler == function:
345 self.bindings.remove(b)
346 found = True
348 else:
349 assert len(args) > 0
350 args = cast(Tuple[Union[Keys, str]], args)
352 # Remove this sequence of key bindings.
353 keys = tuple(_parse_key(k) for k in args)
355 for b in self.bindings:
356 if b.keys == keys:
357 self.bindings.remove(b)
358 found = True
360 if found:
361 self._clear_cache()
362 else:
363 # No key binding found for this function. Raise ValueError.
364 raise ValueError(f"Binding not found: {function!r}")
366 # For backwards-compatibility.
367 add_binding = add
368 remove_binding = remove
370 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
371 """
372 Return a list of key bindings that can handle this key.
373 (This return also inactive bindings, so the `filter` still has to be
374 called, for checking it.)
376 :param keys: tuple of keys.
377 """
379 def get() -> list[Binding]:
380 result: list[tuple[int, Binding]] = []
382 for b in self.bindings:
383 if len(keys) == len(b.keys):
384 match = True
385 any_count = 0
387 for i, j in zip(b.keys, keys):
388 if i != j and i != Keys.Any:
389 match = False
390 break
392 if i == Keys.Any:
393 any_count += 1
395 if match:
396 result.append((any_count, b))
398 # Place bindings that have more 'Any' occurrences in them at the end.
399 result = sorted(result, key=lambda item: -item[0])
401 return [item[1] for item in result]
403 return self._get_bindings_for_keys_cache.get(keys, get)
405 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
406 """
407 Return a list of key bindings that handle a key sequence starting with
408 `keys`. (It does only return bindings for which the sequences are
409 longer than `keys`. And like `get_bindings_for_keys`, it also includes
410 inactive bindings.)
412 :param keys: tuple of keys.
413 """
415 def get() -> list[Binding]:
416 result = []
417 for b in self.bindings:
418 if len(keys) < len(b.keys):
419 match = True
420 for i, j in zip(b.keys, keys):
421 if i != j and i != Keys.Any:
422 match = False
423 break
424 if match:
425 result.append(b)
426 return result
428 return self._get_bindings_starting_with_keys_cache.get(keys, get)
431def _parse_key(key: Keys | str) -> str | Keys:
432 """
433 Replace key by alias and verify whether it's a valid one.
434 """
435 # Already a parse key? -> Return it.
436 if isinstance(key, Keys):
437 return key
439 # Lookup aliases.
440 key = KEY_ALIASES.get(key, key)
442 # Replace 'space' by ' '
443 if key == "space":
444 key = " "
446 # Return as `Key` object when it's a special key.
447 try:
448 return Keys(key)
449 except ValueError:
450 pass
452 # Final validation.
453 if len(key) != 1:
454 raise ValueError(f"Invalid key: {key}")
456 return key
459def key_binding(
460 filter: FilterOrBool = True,
461 eager: FilterOrBool = False,
462 is_global: FilterOrBool = False,
463 save_before: Callable[[KeyPressEvent], bool] = (lambda event: True),
464 record_in_macro: FilterOrBool = True,
465) -> Callable[[KeyHandlerCallable], Binding]:
466 """
467 Decorator that turn a function into a `Binding` object. This can be added
468 to a `KeyBindings` object when a key binding is assigned.
469 """
470 assert save_before is None or callable(save_before)
472 filter = to_filter(filter)
473 eager = to_filter(eager)
474 is_global = to_filter(is_global)
475 save_before = save_before
476 record_in_macro = to_filter(record_in_macro)
477 keys = ()
479 def decorator(function: KeyHandlerCallable) -> Binding:
480 return Binding(
481 keys,
482 function,
483 filter=filter,
484 eager=eager,
485 is_global=is_global,
486 save_before=save_before,
487 record_in_macro=record_in_macro,
488 )
490 return decorator
493class _Proxy(KeyBindingsBase):
494 """
495 Common part for ConditionalKeyBindings and _MergedKeyBindings.
496 """
498 def __init__(self) -> None:
499 # `KeyBindings` to be synchronized with all the others.
500 self._bindings2: KeyBindingsBase = KeyBindings()
501 self._last_version: Hashable = ()
503 def _update_cache(self) -> None:
504 """
505 If `self._last_version` is outdated, then this should update
506 the version and `self._bindings2`.
507 """
508 raise NotImplementedError
510 # Proxy methods to self._bindings2.
512 @property
513 def bindings(self) -> list[Binding]:
514 self._update_cache()
515 return self._bindings2.bindings
517 @property
518 def _version(self) -> Hashable:
519 self._update_cache()
520 return self._last_version
522 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
523 self._update_cache()
524 return self._bindings2.get_bindings_for_keys(keys)
526 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
527 self._update_cache()
528 return self._bindings2.get_bindings_starting_with_keys(keys)
531class ConditionalKeyBindings(_Proxy):
532 """
533 Wraps around a `KeyBindings`. Disable/enable all the key bindings according to
534 the given (additional) filter.::
536 @Condition
537 def setting_is_true():
538 return True # or False
540 registry = ConditionalKeyBindings(key_bindings, setting_is_true)
542 When new key bindings are added to this object. They are also
543 enable/disabled according to the given `filter`.
545 :param registries: List of :class:`.KeyBindings` objects.
546 :param filter: :class:`~prompt_toolkit.filters.Filter` object.
547 """
549 def __init__(
550 self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True
551 ) -> None:
552 _Proxy.__init__(self)
554 self.key_bindings = key_bindings
555 self.filter = to_filter(filter)
557 def _update_cache(self) -> None:
558 "If the original key bindings was changed. Update our copy version."
559 expected_version = self.key_bindings._version
561 if self._last_version != expected_version:
562 bindings2 = KeyBindings()
564 # Copy all bindings from `self.key_bindings`, adding our condition.
565 for b in self.key_bindings.bindings:
566 bindings2.bindings.append(
567 Binding(
568 keys=b.keys,
569 handler=b.handler,
570 filter=self.filter & b.filter,
571 eager=b.eager,
572 is_global=b.is_global,
573 save_before=b.save_before,
574 record_in_macro=b.record_in_macro,
575 )
576 )
578 self._bindings2 = bindings2
579 self._last_version = expected_version
582class _MergedKeyBindings(_Proxy):
583 """
584 Merge multiple registries of key bindings into one.
586 This class acts as a proxy to multiple :class:`.KeyBindings` objects, but
587 behaves as if this is just one bigger :class:`.KeyBindings`.
589 :param registries: List of :class:`.KeyBindings` objects.
590 """
592 def __init__(self, registries: Sequence[KeyBindingsBase]) -> None:
593 _Proxy.__init__(self)
594 self.registries = registries
596 def _update_cache(self) -> None:
597 """
598 If one of the original registries was changed. Update our merged
599 version.
600 """
601 expected_version = tuple(r._version for r in self.registries)
603 if self._last_version != expected_version:
604 bindings2 = KeyBindings()
606 for reg in self.registries:
607 bindings2.bindings.extend(reg.bindings)
609 self._bindings2 = bindings2
610 self._last_version = expected_version
613def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings:
614 """
615 Merge multiple :class:`.Keybinding` objects together.
617 Usage::
619 bindings = merge_key_bindings([bindings1, bindings2, ...])
620 """
621 return _MergedKeyBindings(bindings)
624class DynamicKeyBindings(_Proxy):
625 """
626 KeyBindings class that can dynamically returns any KeyBindings.
628 :param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance.
629 """
631 def __init__(self, get_key_bindings: Callable[[], KeyBindingsBase | None]) -> None:
632 self.get_key_bindings = get_key_bindings
633 self.__version = 0
634 self._last_child_version = None
635 self._dummy = KeyBindings() # Empty key bindings.
637 def _update_cache(self) -> None:
638 key_bindings = self.get_key_bindings() or self._dummy
639 assert isinstance(key_bindings, KeyBindingsBase)
640 version = id(key_bindings), key_bindings._version
642 self._bindings2 = key_bindings
643 self._last_version = version
646class GlobalOnlyKeyBindings(_Proxy):
647 """
648 Wrapper around a :class:`.KeyBindings` object that only exposes the global
649 key bindings.
650 """
652 def __init__(self, key_bindings: KeyBindingsBase) -> None:
653 _Proxy.__init__(self)
654 self.key_bindings = key_bindings
656 def _update_cache(self) -> None:
657 """
658 If one of the original registries was changed. Update our merged
659 version.
660 """
661 expected_version = self.key_bindings._version
663 if self._last_version != expected_version:
664 bindings2 = KeyBindings()
666 for b in self.key_bindings.bindings:
667 if b.is_global():
668 bindings2.bindings.append(b)
670 self._bindings2 = bindings2
671 self._last_version = expected_version