diff --git a/CHANGELOG b/CHANGELOG index e8afc07cb..24985a5cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,13 @@ CHANGELOG ========= +Unreleased +---------- + +Fixes: +- Preserve preceding text when case-insensitive word completion lowercases + characters into multiple code points. + 3.0.53: 2026-07-26 ------------------ diff --git a/src/prompt_toolkit/completion/word_completer.py b/src/prompt_toolkit/completion/word_completer.py index 563fdadcb..e44f218c8 100644 --- a/src/prompt_toolkit/completion/word_completer.py +++ b/src/prompt_toolkit/completion/word_completer.py @@ -70,6 +70,8 @@ def get_completions( WORD=self.WORD, pattern=self.pattern ) + # Lowercasing can change the number of characters in the prefix. + start_position = -len(word_before_cursor) if self.ignore_case: word_before_cursor = word_before_cursor.lower() @@ -89,7 +91,7 @@ def word_matches(word: str) -> bool: display_meta = self.meta_dict.get(a, "") yield Completion( text=a, - start_position=-len(word_before_cursor), + start_position=start_position, display=display, display_meta=display_meta, ) diff --git a/tests/test_completion.py b/tests/test_completion.py index 8b3541af0..e8cf8c9dd 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -6,6 +6,9 @@ import tempfile from contextlib import contextmanager +import pytest + +from prompt_toolkit.buffer import Buffer from prompt_toolkit.completion import ( CompleteEvent, FuzzyWordCompleter, @@ -295,6 +298,31 @@ def test_word_completer_match_middle(): assert [c.text for c in completions] == ["abc", "abca"] +@pytest.mark.parametrize( + "word, candidate", + [("İ", "İstanbul"), ("İİ", "İİstanbul"), ("I", "Istanbul"), ("i", "istanbul")], +) +@pytest.mark.parametrize("ignore_case", [False, True]) +@pytest.mark.parametrize("match_middle", [False, True]) +@pytest.mark.parametrize("WORD", [False, True]) +def test_word_completer_preserves_text_before_case_expanding_word( + word, candidate, ignore_case, match_middle, WORD +): + if match_middle: + candidate = "city" + candidate + completer = WordCompleter( + [candidate], ignore_case=ignore_case, match_middle=match_middle, WORD=WORD + ) + buffer = Buffer(document=Document("before " + word)) + (completion,) = completer.get_completions(buffer.document, CompleteEvent()) + + buffer.apply_completion(completion) + + assert buffer.text == "before " + candidate + assert buffer.cursor_position == len(buffer.text) + assert completion.start_position == -len(word) + + def test_word_completer_sentence(): # With sentence=True completer = WordCompleter(