From 2f522cc78572d2eea29442093e14d8a1a0dcd849 Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:44:44 -0700 Subject: [PATCH] Route nested completion after any whitespace Recognize whitespace consistently with the existing splitter and trimmer, so tabs and Unicode separators enter the nested completion branch. AI assistance: OpenAI Codex. --- src/prompt_toolkit/completion/nested.py | 6 +-- tests/test_completion.py | 49 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/prompt_toolkit/completion/nested.py b/src/prompt_toolkit/completion/nested.py index 5c18114ef..8487cbd6c 100644 --- a/src/prompt_toolkit/completion/nested.py +++ b/src/prompt_toolkit/completion/nested.py @@ -84,9 +84,9 @@ def get_completions( text = document.text_before_cursor.lstrip() 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 whitespace, check for the first term, and use a # subcompleter. - if " " in text: + if any(c.isspace() for c in text): first_term = text.split()[0] completer = self.options.get(first_term) @@ -102,7 +102,7 @@ def get_completions( yield from completer.get_completions(new_document, complete_event) - # No space in the input: behave exactly like `WordCompleter`. + # No whitespace in the input: behave exactly like `WordCompleter`. else: completer = WordCompleter( list(self.options.keys()), ignore_case=self.ignore_case diff --git a/tests/test_completion.py b/tests/test_completion.py index 8b3541af0..3c8871d8a 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -6,6 +6,8 @@ import tempfile from contextlib import contextmanager +import pytest + from prompt_toolkit.completion import ( CompleteEvent, FuzzyWordCompleter, @@ -444,6 +446,53 @@ def test_nested_completer(): assert {c.text for c in completions} == {"brief"} +@pytest.mark.parametrize( + "separator", [" ", "\t", "\n", "\r", "\v", "\f", "\u00a0", "\u2003"] +) +@pytest.mark.parametrize("word", ["", "ve"]) +def test_nested_completer_whitespace_separators(separator, word): + completer = NestedCompleter.from_nested_dict({"show": {"version": None}}) + before_cursor = " \tshow" + separator + word + document = Document(before_cursor + " suffix", cursor_position=len(before_cursor)) + + completions = completer.get_completions(document, CompleteEvent()) + + assert [(c.text, c.start_position) for c in completions] == [ + ("version", -len(word)) + ] + + +@pytest.mark.parametrize("command", ["exit", "unknown"]) +@pytest.mark.parametrize("separator", [" ", "\t", "\n", "\u2003"]) +def test_nested_completer_whitespace_terminal_and_unknown(command, separator): + completer = NestedCompleter.from_nested_dict( + {"show": {"version": None}, "exit": None} + ) + + completions = completer.get_completions( + Document(command + separator), CompleteEvent() + ) + + assert list(completions) == [] + + +def test_nested_completer_mixed_whitespace_multiple_levels(): + completer = NestedCompleter.from_nested_dict({"show": {"ip": {"route": None}}}) + + completions = completer.get_completions(Document("show\tip\nro"), CompleteEvent()) + + assert [(c.text, c.start_position) for c in completions] == [("route", -2)] + + +@pytest.mark.parametrize("text", ["", " \t\n", "sh", "\tsh"]) +def test_nested_completer_whitespace_root_controls(text): + completer = NestedCompleter.from_nested_dict({"show": {"version": None}}) + + completions = completer.get_completions(Document(text), CompleteEvent()) + + assert [c.text for c in completions] == ["show"] + + def test_deduplicate_completer(): def create_completer(deduplicate: bool): return merge_completers(