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