Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/prompt_toolkit/key_binding/key_bindings.py: 35%
227 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +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 Awaitable,
44 Callable,
45 Hashable,
46 Sequence,
47 Tuple,
48 TypeVar,
49 Union,
50 cast,
51)
53from prompt_toolkit.cache import SimpleCache
54from prompt_toolkit.filters import FilterOrBool, Never, to_filter
55from prompt_toolkit.keys import KEY_ALIASES, Keys
57if TYPE_CHECKING:
58 # Avoid circular imports.
59 from .key_processor import KeyPressEvent
61 # The only two return values for a mouse handler (and key bindings) are
62 # `None` and `NotImplemented`. For the type checker it's best to annotate
63 # this as `object`. (The consumer never expects a more specific instance:
64 # checking for NotImplemented can be done using `is NotImplemented`.)
65 NotImplementedOrNone = object
66 # Other non-working options are:
67 # * Optional[Literal[NotImplemented]]
68 # --> Doesn't work, Literal can't take an Any.
69 # * None
70 # --> Doesn't work. We can't assign the result of a function that
71 # returns `None` to a variable.
72 # * Any
73 # --> Works, but too broad.
76__all__ = [
77 "NotImplementedOrNone",
78 "Binding",
79 "KeyBindingsBase",
80 "KeyBindings",
81 "ConditionalKeyBindings",
82 "merge_key_bindings",
83 "DynamicKeyBindings",
84 "GlobalOnlyKeyBindings",
85]
87# Key bindings can be regular functions or coroutines.
88# In both cases, if they return `NotImplemented`, the UI won't be invalidated.
89# This is mainly used in case of mouse move events, to prevent excessive
90# repainting during mouse move events.
91KeyHandlerCallable = Callable[
92 ["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]]
93]
96class Binding:
97 """
98 Key binding: (key sequence + handler + filter).
99 (Immutable binding class.)
101 :param record_in_macro: When True, don't record this key binding when a
102 macro is recorded.
103 """
105 def __init__(
106 self,
107 keys: tuple[Keys | str, ...],
108 handler: KeyHandlerCallable,
109 filter: FilterOrBool = True,
110 eager: FilterOrBool = False,
111 is_global: FilterOrBool = False,
112 save_before: Callable[[KeyPressEvent], bool] = (lambda e: True),
113 record_in_macro: FilterOrBool = True,
114 ) -> None:
115 self.keys = keys
116 self.handler = handler
117 self.filter = to_filter(filter)
118 self.eager = to_filter(eager)
119 self.is_global = to_filter(is_global)
120 self.save_before = save_before
121 self.record_in_macro = to_filter(record_in_macro)
123 def call(self, event: KeyPressEvent) -> None:
124 result = self.handler(event)
126 # If the handler is a coroutine, create an asyncio task.
127 if isawaitable(result):
128 awaitable = cast(Awaitable["NotImplementedOrNone"], result)
130 async def bg_task() -> None:
131 result = await awaitable
132 if result != NotImplemented:
133 event.app.invalidate()
135 event.app.create_background_task(bg_task())
137 elif result != NotImplemented:
138 event.app.invalidate()
140 def __repr__(self) -> str:
141 return "{}(keys={!r}, handler={!r})".format(
142 self.__class__.__name__,
143 self.keys,
144 self.handler,
145 )
148# Sequence of keys presses.
149KeysTuple = Tuple[Union[Keys, str], ...]
152class KeyBindingsBase(metaclass=ABCMeta):
153 """
154 Interface for a KeyBindings.
155 """
157 @abstractproperty
158 def _version(self) -> Hashable:
159 """
160 For cache invalidation. - This should increase every time that
161 something changes.
162 """
163 return 0
165 @abstractmethod
166 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
167 """
168 Return a list of key bindings that can handle these keys.
169 (This return also inactive bindings, so the `filter` still has to be
170 called, for checking it.)
172 :param keys: tuple of keys.
173 """
174 return []
176 @abstractmethod
177 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
178 """
179 Return a list of key bindings that handle a key sequence starting with
180 `keys`. (It does only return bindings for which the sequences are
181 longer than `keys`. And like `get_bindings_for_keys`, it also includes
182 inactive bindings.)
184 :param keys: tuple of keys.
185 """
186 return []
188 @abstractproperty
189 def bindings(self) -> list[Binding]:
190 """
191 List of `Binding` objects.
192 (These need to be exposed, so that `KeyBindings` objects can be merged
193 together.)
194 """
195 return []
197 # `add` and `remove` don't have to be part of this interface.
200T = TypeVar("T", bound=Union[KeyHandlerCallable, Binding])
203class KeyBindings(KeyBindingsBase):
204 """
205 A container for a set of key bindings.
207 Example usage::
209 kb = KeyBindings()
211 @kb.add('c-t')
212 def _(event):
213 print('Control-T pressed')
215 @kb.add('c-a', 'c-b')
216 def _(event):
217 print('Control-A pressed, followed by Control-B')
219 @kb.add('c-x', filter=is_searching)
220 def _(event):
221 print('Control-X pressed') # Works only if we are searching.
223 """
225 def __init__(self) -> None:
226 self._bindings: list[Binding] = []
227 self._get_bindings_for_keys_cache: SimpleCache[
228 KeysTuple, list[Binding]
229 ] = SimpleCache(maxsize=10000)
230 self._get_bindings_starting_with_keys_cache: SimpleCache[
231 KeysTuple, list[Binding]
232 ] = SimpleCache(maxsize=1000)
233 self.__version = 0 # For cache invalidation.
235 def _clear_cache(self) -> None:
236 self.__version += 1
237 self._get_bindings_for_keys_cache.clear()
238 self._get_bindings_starting_with_keys_cache.clear()
240 @property
241 def bindings(self) -> list[Binding]:
242 return self._bindings
244 @property
245 def _version(self) -> Hashable:
246 return self.__version
248 def add(
249 self,
250 *keys: Keys | str,
251 filter: FilterOrBool = True,
252 eager: FilterOrBool = False,
253 is_global: FilterOrBool = False,
254 save_before: Callable[[KeyPressEvent], bool] = (lambda e: True),
255 record_in_macro: FilterOrBool = True,
256 ) -> Callable[[T], T]:
257 """
258 Decorator for adding a key bindings.
260 :param filter: :class:`~prompt_toolkit.filters.Filter` to determine
261 when this key binding is active.
262 :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`.
263 When True, ignore potential longer matches when this key binding is
264 hit. E.g. when there is an active eager key binding for Ctrl-X,
265 execute the handler immediately and ignore the key binding for
266 Ctrl-X Ctrl-E of which it is a prefix.
267 :param is_global: When this key bindings is added to a `Container` or
268 `Control`, make it a global (always active) binding.
269 :param save_before: Callable that takes an `Event` and returns True if
270 we should save the current buffer, before handling the event.
271 (That's the default.)
272 :param record_in_macro: Record these key bindings when a macro is
273 being recorded. (True by default.)
274 """
275 assert keys
277 keys = tuple(_parse_key(k) for k in keys)
279 if isinstance(filter, Never):
280 # When a filter is Never, it will always stay disabled, so in that
281 # case don't bother putting it in the key bindings. It will slow
282 # down every key press otherwise.
283 def decorator(func: T) -> T:
284 return func
286 else:
288 def decorator(func: T) -> T:
289 if isinstance(func, Binding):
290 # We're adding an existing Binding object.
291 self.bindings.append(
292 Binding(
293 keys,
294 func.handler,
295 filter=func.filter & to_filter(filter),
296 eager=to_filter(eager) | func.eager,
297 is_global=to_filter(is_global) | func.is_global,
298 save_before=func.save_before,
299 record_in_macro=func.record_in_macro,
300 )
301 )
302 else:
303 self.bindings.append(
304 Binding(
305 keys,
306 cast(KeyHandlerCallable, func),
307 filter=filter,
308 eager=eager,
309 is_global=is_global,
310 save_before=save_before,
311 record_in_macro=record_in_macro,
312 )
313 )
314 self._clear_cache()
316 return func
318 return decorator
320 def remove(self, *args: Keys | str | KeyHandlerCallable) -> None:
321 """
322 Remove a key binding.
324 This expects either a function that was given to `add` method as
325 parameter or a sequence of key bindings.
327 Raises `ValueError` when no bindings was found.
329 Usage::
331 remove(handler) # Pass handler.
332 remove('c-x', 'c-a') # Or pass the key bindings.
333 """
334 found = False
336 if callable(args[0]):
337 assert len(args) == 1
338 function = args[0]
340 # Remove the given function.
341 for b in self.bindings:
342 if b.handler == function:
343 self.bindings.remove(b)
344 found = True
346 else:
347 assert len(args) > 0
348 args = cast(Tuple[Union[Keys, str]], args)
350 # Remove this sequence of key bindings.
351 keys = tuple(_parse_key(k) for k in args)
353 for b in self.bindings:
354 if b.keys == keys:
355 self.bindings.remove(b)
356 found = True
358 if found:
359 self._clear_cache()
360 else:
361 # No key binding found for this function. Raise ValueError.
362 raise ValueError(f"Binding not found: {function!r}")
364 # For backwards-compatibility.
365 add_binding = add
366 remove_binding = remove
368 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
369 """
370 Return a list of key bindings that can handle this key.
371 (This return also inactive bindings, so the `filter` still has to be
372 called, for checking it.)
374 :param keys: tuple of keys.
375 """
377 def get() -> list[Binding]:
378 result: list[tuple[int, Binding]] = []
380 for b in self.bindings:
381 if len(keys) == len(b.keys):
382 match = True
383 any_count = 0
385 for i, j in zip(b.keys, keys):
386 if i != j and i != Keys.Any:
387 match = False
388 break
390 if i == Keys.Any:
391 any_count += 1
393 if match:
394 result.append((any_count, b))
396 # Place bindings that have more 'Any' occurrences in them at the end.
397 result = sorted(result, key=lambda item: -item[0])
399 return [item[1] for item in result]
401 return self._get_bindings_for_keys_cache.get(keys, get)
403 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
404 """
405 Return a list of key bindings that handle a key sequence starting with
406 `keys`. (It does only return bindings for which the sequences are
407 longer than `keys`. And like `get_bindings_for_keys`, it also includes
408 inactive bindings.)
410 :param keys: tuple of keys.
411 """
413 def get() -> list[Binding]:
414 result = []
415 for b in self.bindings:
416 if len(keys) < len(b.keys):
417 match = True
418 for i, j in zip(b.keys, keys):
419 if i != j and i != Keys.Any:
420 match = False
421 break
422 if match:
423 result.append(b)
424 return result
426 return self._get_bindings_starting_with_keys_cache.get(keys, get)
429def _parse_key(key: Keys | str) -> str | Keys:
430 """
431 Replace key by alias and verify whether it's a valid one.
432 """
433 # Already a parse key? -> Return it.
434 if isinstance(key, Keys):
435 return key
437 # Lookup aliases.
438 key = KEY_ALIASES.get(key, key)
440 # Replace 'space' by ' '
441 if key == "space":
442 key = " "
444 # Return as `Key` object when it's a special key.
445 try:
446 return Keys(key)
447 except ValueError:
448 pass
450 # Final validation.
451 if len(key) != 1:
452 raise ValueError(f"Invalid key: {key}")
454 return key
457def key_binding(
458 filter: FilterOrBool = True,
459 eager: FilterOrBool = False,
460 is_global: FilterOrBool = False,
461 save_before: Callable[[KeyPressEvent], bool] = (lambda event: True),
462 record_in_macro: FilterOrBool = True,
463) -> Callable[[KeyHandlerCallable], Binding]:
464 """
465 Decorator that turn a function into a `Binding` object. This can be added
466 to a `KeyBindings` object when a key binding is assigned.
467 """
468 assert save_before is None or callable(save_before)
470 filter = to_filter(filter)
471 eager = to_filter(eager)
472 is_global = to_filter(is_global)
473 save_before = save_before
474 record_in_macro = to_filter(record_in_macro)
475 keys = ()
477 def decorator(function: KeyHandlerCallable) -> Binding:
478 return Binding(
479 keys,
480 function,
481 filter=filter,
482 eager=eager,
483 is_global=is_global,
484 save_before=save_before,
485 record_in_macro=record_in_macro,
486 )
488 return decorator
491class _Proxy(KeyBindingsBase):
492 """
493 Common part for ConditionalKeyBindings and _MergedKeyBindings.
494 """
496 def __init__(self) -> None:
497 # `KeyBindings` to be synchronized with all the others.
498 self._bindings2: KeyBindingsBase = KeyBindings()
499 self._last_version: Hashable = ()
501 def _update_cache(self) -> None:
502 """
503 If `self._last_version` is outdated, then this should update
504 the version and `self._bindings2`.
505 """
506 raise NotImplementedError
508 # Proxy methods to self._bindings2.
510 @property
511 def bindings(self) -> list[Binding]:
512 self._update_cache()
513 return self._bindings2.bindings
515 @property
516 def _version(self) -> Hashable:
517 self._update_cache()
518 return self._last_version
520 def get_bindings_for_keys(self, keys: KeysTuple) -> list[Binding]:
521 self._update_cache()
522 return self._bindings2.get_bindings_for_keys(keys)
524 def get_bindings_starting_with_keys(self, keys: KeysTuple) -> list[Binding]:
525 self._update_cache()
526 return self._bindings2.get_bindings_starting_with_keys(keys)
529class ConditionalKeyBindings(_Proxy):
530 """
531 Wraps around a `KeyBindings`. Disable/enable all the key bindings according to
532 the given (additional) filter.::
534 @Condition
535 def setting_is_true():
536 return True # or False
538 registry = ConditionalKeyBindings(key_bindings, setting_is_true)
540 When new key bindings are added to this object. They are also
541 enable/disabled according to the given `filter`.
543 :param registries: List of :class:`.KeyBindings` objects.
544 :param filter: :class:`~prompt_toolkit.filters.Filter` object.
545 """
547 def __init__(
548 self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True
549 ) -> None:
550 _Proxy.__init__(self)
552 self.key_bindings = key_bindings
553 self.filter = to_filter(filter)
555 def _update_cache(self) -> None:
556 "If the original key bindings was changed. Update our copy version."
557 expected_version = self.key_bindings._version
559 if self._last_version != expected_version:
560 bindings2 = KeyBindings()
562 # Copy all bindings from `self.key_bindings`, adding our condition.
563 for b in self.key_bindings.bindings:
564 bindings2.bindings.append(
565 Binding(
566 keys=b.keys,
567 handler=b.handler,
568 filter=self.filter & b.filter,
569 eager=b.eager,
570 is_global=b.is_global,
571 save_before=b.save_before,
572 record_in_macro=b.record_in_macro,
573 )
574 )
576 self._bindings2 = bindings2
577 self._last_version = expected_version
580class _MergedKeyBindings(_Proxy):
581 """
582 Merge multiple registries of key bindings into one.
584 This class acts as a proxy to multiple :class:`.KeyBindings` objects, but
585 behaves as if this is just one bigger :class:`.KeyBindings`.
587 :param registries: List of :class:`.KeyBindings` objects.
588 """
590 def __init__(self, registries: Sequence[KeyBindingsBase]) -> None:
591 _Proxy.__init__(self)
592 self.registries = registries
594 def _update_cache(self) -> None:
595 """
596 If one of the original registries was changed. Update our merged
597 version.
598 """
599 expected_version = tuple(r._version for r in self.registries)
601 if self._last_version != expected_version:
602 bindings2 = KeyBindings()
604 for reg in self.registries:
605 bindings2.bindings.extend(reg.bindings)
607 self._bindings2 = bindings2
608 self._last_version = expected_version
611def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings:
612 """
613 Merge multiple :class:`.Keybinding` objects together.
615 Usage::
617 bindings = merge_key_bindings([bindings1, bindings2, ...])
618 """
619 return _MergedKeyBindings(bindings)
622class DynamicKeyBindings(_Proxy):
623 """
624 KeyBindings class that can dynamically returns any KeyBindings.
626 :param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance.
627 """
629 def __init__(self, get_key_bindings: Callable[[], KeyBindingsBase | None]) -> None:
630 self.get_key_bindings = get_key_bindings
631 self.__version = 0
632 self._last_child_version = None
633 self._dummy = KeyBindings() # Empty key bindings.
635 def _update_cache(self) -> None:
636 key_bindings = self.get_key_bindings() or self._dummy
637 assert isinstance(key_bindings, KeyBindingsBase)
638 version = id(key_bindings), key_bindings._version
640 self._bindings2 = key_bindings
641 self._last_version = version
644class GlobalOnlyKeyBindings(_Proxy):
645 """
646 Wrapper around a :class:`.KeyBindings` object that only exposes the global
647 key bindings.
648 """
650 def __init__(self, key_bindings: KeyBindingsBase) -> None:
651 _Proxy.__init__(self)
652 self.key_bindings = key_bindings
654 def _update_cache(self) -> None:
655 """
656 If one of the original registries was changed. Update our merged
657 version.
658 """
659 expected_version = self.key_bindings._version
661 if self._last_version != expected_version:
662 bindings2 = KeyBindings()
664 for b in self.key_bindings.bindings:
665 if b.is_global():
666 bindings2.bindings.append(b)
668 self._bindings2 = bindings2
669 self._last_version = expected_version