diff --git a/src/textual/suggester.py b/src/textual/suggester.py index d0be59507..adf61ea60 100644 --- a/src/textual/suggester.py +++ b/src/textual/suggester.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Iterable -from ._cache import FIFOCache +from ._cache import LRUCache from .dom import DOMNode from .message import Message @@ -13,7 +13,7 @@ from .message import Message class SuggestionReady(Message): """Sent when a completion suggestion is ready.""" - initial_value: str + value: str """The value to which the suggestion is for.""" suggestion: str """The string suggestion.""" @@ -27,16 +27,20 @@ class Suggester(ABC): See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example. """ - cache: FIFOCache[str, str | None] | None + cache: LRUCache[str, str | None] | None """Suggestion cache, if used.""" - def __init__(self, use_cache: bool = True) -> None: + def __init__(self, *, use_cache: bool = True, case_sensitive: bool = True) -> None: """Create a suggester object. Args: use_cache: Whether to cache suggestion results. + case_sensitive: Whether suggestions are case sensitive or not. + If they are not, incoming values are casefolded before generating + the suggestion. """ - self.cache = FIFOCache(1024) if use_cache else None + self.cache = LRUCache(1024) if use_cache else None + self.case_sensitive = case_sensitive async def _get_suggestion(self, requester: DOMNode, value: str) -> None: """Used by widgets to get completion suggestions. @@ -47,15 +51,16 @@ class Suggester(ABC): Args: requester: The message target that requested a suggestion. - value: The current input value to complete. + value: The current value to complete. """ - if self.cache is None or value not in self.cache: - suggestion = await self.get_suggestion(value) + normalized_value = value if self.case_sensitive else value.casefold() + if self.cache is None or normalized_value not in self.cache: + suggestion = await self.get_suggestion(normalized_value) if self.cache is not None: - self.cache[value] = suggestion + self.cache[normalized_value] = suggestion else: - suggestion = self.cache[value] + suggestion = self.cache[normalized_value] if suggestion is None: return @@ -67,6 +72,9 @@ class Suggester(ABC): Custom suggesters should implement this method. + Note: + The value argument will be casefolded if `self.case_sensitive` is `False`. + Note: If your implementation is not deterministic, you may need to disable caching. @@ -88,18 +96,32 @@ class SuggestFromList(Suggester): class MyApp(App[None]): def compose(self) -> ComposeResult: - yield Input(suggester=SuggestFromList(countries)) + yield Input(suggester=SuggestFromList(countries, case_sensitive=False)) ``` + + If the user types ++p++ inside the input widget, a completion suggestion + for `"Portugal"` appears. """ - def __init__(self, suggestions: Iterable[str]) -> None: + def __init__( + self, suggestions: Iterable[str], *, case_sensitive: bool = True + ) -> None: """Creates a suggester based off of a given iterable of possibilities. Args: suggestions: Valid suggestions sorted by decreasing priority. + case_sensitive: Whether suggestions are computed in a case sensitive manner + or not. The values provided in the argument `suggestions` represent the + canonical representation of the completions and they will be suggested + with that same casing. """ - super().__init__() + super().__init__(case_sensitive=case_sensitive) self._suggestions = list(suggestions) + self._for_comparison = ( + self._suggestions + if self.case_sensitive + else [suggestion.casefold() for suggestion in self._suggestions] + ) async def get_suggestion(self, value: str) -> str | None: """Gets a completion from the given possibilities. @@ -110,7 +132,7 @@ class SuggestFromList(Suggester): Returns: A valid completion suggestion or `None`. """ - for suggestion in self._suggestions: + for idx, suggestion in enumerate(self._for_comparison): if suggestion.startswith(value): - return suggestion + return self._suggestions[idx] return None diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index c5be91da6..3581ccda8 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -38,9 +38,7 @@ class _InputRenderable: value = input.value value_length = len(value) suggestion = input._suggestion - show_suggestion = suggestion.startswith(value) and ( - len(suggestion) > value_length - ) + show_suggestion = len(suggestion) > value_length if show_suggestion: result += Text( suggestion[value_length:], @@ -388,7 +386,7 @@ class Input(Widget, can_focus=True): async def _on_suggestion_ready(self, event: SuggestionReady) -> None: """Handle suggestion messages and set the suggestion when relevant.""" - if event.initial_value == self.value: + if event.value == self.value: self._suggestion = event.suggestion def insert_text_at_cursor(self, text: str) -> None: