Skip to content

Commit

Permalink
NestedSeparator improvements and additions
Browse files Browse the repository at this point in the history
1. Allow passing an optional custom separator.
2. Allow passing all arguments of the default constructor to
   the `from_nested_dict` constructor as well.
  • Loading branch information
robamu committed Nov 17, 2023
1 parent 3e71b1e commit bdd9746
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 30 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
CHANGELOG
=========

New features:

- `NestedCompleter` now accepts an optional separator argument which allows to specify custom
separators. Space will still be the default separator.
- `NestedCompleter.from_nested_dict` now accepts optional `ignore_case` and `separator` arguments
of the default constructor and passes them on recursively to all contained nested separator
instances.

3.0.41: 2023-11-14
------------------

Expand Down
41 changes: 29 additions & 12 deletions src/prompt_toolkit/completion/nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,33 @@ class NestedCompleter(Completer):
By combining multiple `NestedCompleter` instances, we can achieve multiple
hierarchical levels of autocompletion. This is useful when `WordCompleter`
is not sufficient.
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
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})"
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) -> NestedCompleter:
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:
Expand Down Expand Up @@ -66,31 +77,37 @@ def from_nested_dict(cls, data: NestedDict) -> NestedCompleter:
if isinstance(value, Completer):
options[key] = value
elif isinstance(value, dict):
options[key] = cls.from_nested_dict(value)
options[key] = cls.from_nested_dict(
data=value, ignore_case=ignore_case, separator=separator
)
elif isinstance(value, set):
options[key] = cls.from_nested_dict({item: None for item in value})
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)
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()
text = document.text_before_cursor.lstrip(self.separator)
stripped_len = len(document.text_before_cursor) - len(text)

# If there is a space, check for the first term, and use a
# If there is a separator character, check for the first term, and use a
# subcompleter.
if " " in text:
first_term = text.split()[0]
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()
remaining_text = text[len(first_term) :].lstrip(self.separator)
move_cursor = len(text) - len(remaining_text) + stripped_len

new_document = Document(
Expand Down
4 changes: 3 additions & 1 deletion src/prompt_toolkit/layout/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,9 @@ def create_content(self, width: int, height: int) -> UIContent:
def get_line(i: int) -> StyleAndTextTuples:
return []

return UIContent(get_line=get_line, line_count=100**100) # Something very big.
return UIContent(
get_line=get_line, line_count=100**100
) # Something very big.

def is_focusable(self) -> bool:
return False
Expand Down
62 changes: 45 additions & 17 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,19 +400,7 @@ def test_fuzzy_completer():
assert [c.text for c in completions] == ["users.txt", "accounts.txt"]


def test_nested_completer():
completer = NestedCompleter.from_nested_dict(
{
"show": {
"version": None,
"clock": None,
"interfaces": None,
"ip": {"interface": {"brief"}},
},
"exit": None,
}
)

def _generic_test_nested_completer(completer: NestedCompleter):
# Empty input.
completions = completer.get_completions(Document(""), CompleteEvent())
assert {c.text for c in completions} == {"show", "exit"}
Expand All @@ -426,24 +414,64 @@ def test_nested_completer():
assert {c.text for c in completions} == {"show"}

# One word + space.
completions = completer.get_completions(Document("show "), CompleteEvent())
completions = completer.get_completions(
Document(f"show{completer.separator}"), CompleteEvent()
)
assert {c.text for c in completions} == {"version", "clock", "interfaces", "ip"}

# One word + space + one character.
completions = completer.get_completions(Document("show i"), CompleteEvent())
completions = completer.get_completions(
Document(f"show{completer.separator}i"), CompleteEvent()
)
assert {c.text for c in completions} == {"ip", "interfaces"}

# One space + one word + space + one character.
completions = completer.get_completions(Document(" show i"), CompleteEvent())
completions = completer.get_completions(
Document(f"{completer.separator}show{completer.separator}i"), CompleteEvent()
)
assert {c.text for c in completions} == {"ip", "interfaces"}

# Test nested set.
completions = completer.get_completions(
Document("show ip interface br"), CompleteEvent()
Document(
f"show{completer.separator}ip{completer.separator}interface{completer.separator}br"
),
CompleteEvent(),
)
assert {c.text for c in completions} == {"brief"}


def test_nested_completer():
completer = NestedCompleter.from_nested_dict(
{
"show": {
"version": None,
"clock": None,
"interfaces": None,
"ip": {"interface": {"brief"}},
},
"exit": None,
}
)
_generic_test_nested_completer(completer)


def test_nested_completer_different_separator():
completer = NestedCompleter.from_nested_dict(
data={
"show": {
"version": None,
"clock": None,
"interfaces": None,
"ip": {"interface": {"brief"}},
},
"exit": None,
},
separator="/",
)
_generic_test_nested_completer(completer)


def test_deduplicate_completer():
def create_completer(deduplicate: bool):
return merge_completers(
Expand Down

0 comments on commit bdd9746

Please sign in to comment.