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