nested.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """
  2. Nestedcompleter for completion of hierarchical data structures.
  3. """
  4. from __future__ import annotations
  5. from typing import Any, Iterable, Mapping, Set, Union
  6. from prompt_toolkit.completion import CompleteEvent, Completer, Completion
  7. from prompt_toolkit.completion.word_completer import WordCompleter
  8. from prompt_toolkit.document import Document
  9. __all__ = ["NestedCompleter"]
  10. # NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]]
  11. NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]]
  12. class NestedCompleter(Completer):
  13. """
  14. Completer which wraps around several other completers, and calls any the
  15. one that corresponds with the first word of the input.
  16. By combining multiple `NestedCompleter` instances, we can achieve multiple
  17. hierarchical levels of autocompletion. This is useful when `WordCompleter`
  18. is not sufficient.
  19. If you need multiple levels, check out the `from_nested_dict` classmethod.
  20. """
  21. def __init__(
  22. self, options: dict[str, Completer | None], ignore_case: bool = True
  23. ) -> None:
  24. self.options = options
  25. self.ignore_case = ignore_case
  26. def __repr__(self) -> str:
  27. return f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
  28. @classmethod
  29. def from_nested_dict(cls, data: NestedDict) -> NestedCompleter:
  30. """
  31. Create a `NestedCompleter`, starting from a nested dictionary data
  32. structure, like this:
  33. .. code::
  34. data = {
  35. 'show': {
  36. 'version': None,
  37. 'interfaces': None,
  38. 'clock': None,
  39. 'ip': {'interface': {'brief'}}
  40. },
  41. 'exit': None
  42. 'enable': None
  43. }
  44. The value should be `None` if there is no further completion at some
  45. point. If all values in the dictionary are None, it is also possible to
  46. use a set instead.
  47. Values in this data structure can be a completers as well.
  48. """
  49. options: dict[str, Completer | None] = {}
  50. for key, value in data.items():
  51. if isinstance(value, Completer):
  52. options[key] = value
  53. elif isinstance(value, dict):
  54. options[key] = cls.from_nested_dict(value)
  55. elif isinstance(value, set):
  56. options[key] = cls.from_nested_dict({item: None for item in value})
  57. else:
  58. assert value is None
  59. options[key] = None
  60. return cls(options)
  61. def get_completions(
  62. self, document: Document, complete_event: CompleteEvent
  63. ) -> Iterable[Completion]:
  64. # Split document.
  65. text = document.text_before_cursor.lstrip()
  66. stripped_len = len(document.text_before_cursor) - len(text)
  67. # If there is a space, check for the first term, and use a
  68. # subcompleter.
  69. if " " in text:
  70. first_term = text.split()[0]
  71. completer = self.options.get(first_term)
  72. # If we have a sub completer, use this for the completions.
  73. if completer is not None:
  74. remaining_text = text[len(first_term) :].lstrip()
  75. move_cursor = len(text) - len(remaining_text) + stripped_len
  76. new_document = Document(
  77. remaining_text,
  78. cursor_position=document.cursor_position - move_cursor,
  79. )
  80. yield from completer.get_completions(new_document, complete_event)
  81. # No space in the input: behave exactly like `WordCompleter`.
  82. else:
  83. completer = WordCompleter(
  84. list(self.options.keys()), ignore_case=self.ignore_case
  85. )
  86. yield from completer.get_completions(document, complete_event)