diff --git a/CHANGELOG b/CHANGELOG index c61430778..9537a4a35 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 ------------------ diff --git a/src/prompt_toolkit/completion/nested.py b/src/prompt_toolkit/completion/nested.py index a1d211ab0..26a7c8bc8 100644 --- a/src/prompt_toolkit/completion/nested.py +++ b/src/prompt_toolkit/completion/nested.py @@ -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: @@ -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( diff --git a/tests/test_completion.py b/tests/test_completion.py index 8b3541af0..24fc82986 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -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"} @@ -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(