Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NestedSeparator improvements and additions #1815

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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