Update suggester implementation.

This commit is contained in:
Rodrigo Girão Serrão
2023-05-25 17:32:40 +01:00
parent fc86682dfa
commit ae266551a1
2 changed files with 39 additions and 19 deletions

View File

@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable from typing import Iterable
from ._cache import FIFOCache from ._cache import LRUCache
from .dom import DOMNode from .dom import DOMNode
from .message import Message from .message import Message
@@ -13,7 +13,7 @@ from .message import Message
class SuggestionReady(Message): class SuggestionReady(Message):
"""Sent when a completion suggestion is ready.""" """Sent when a completion suggestion is ready."""
initial_value: str value: str
"""The value to which the suggestion is for.""" """The value to which the suggestion is for."""
suggestion: str suggestion: str
"""The string suggestion.""" """The string suggestion."""
@@ -27,16 +27,20 @@ class Suggester(ABC):
See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example. See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example.
""" """
cache: FIFOCache[str, str | None] | None cache: LRUCache[str, str | None] | None
"""Suggestion cache, if used.""" """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. """Create a suggester object.
Args: Args:
use_cache: Whether to cache suggestion results. 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: async def _get_suggestion(self, requester: DOMNode, value: str) -> None:
"""Used by widgets to get completion suggestions. """Used by widgets to get completion suggestions.
@@ -47,15 +51,16 @@ class Suggester(ABC):
Args: Args:
requester: The message target that requested a suggestion. 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: normalized_value = value if self.case_sensitive else value.casefold()
suggestion = await self.get_suggestion(value) 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: if self.cache is not None:
self.cache[value] = suggestion self.cache[normalized_value] = suggestion
else: else:
suggestion = self.cache[value] suggestion = self.cache[normalized_value]
if suggestion is None: if suggestion is None:
return return
@@ -67,6 +72,9 @@ class Suggester(ABC):
Custom suggesters should implement this method. Custom suggesters should implement this method.
Note:
The value argument will be casefolded if `self.case_sensitive` is `False`.
Note: Note:
If your implementation is not deterministic, you may need to disable caching. If your implementation is not deterministic, you may need to disable caching.
@@ -88,18 +96,32 @@ class SuggestFromList(Suggester):
class MyApp(App[None]): class MyApp(App[None]):
def compose(self) -> ComposeResult: 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. """Creates a suggester based off of a given iterable of possibilities.
Args: Args:
suggestions: Valid suggestions sorted by decreasing priority. 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._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: async def get_suggestion(self, value: str) -> str | None:
"""Gets a completion from the given possibilities. """Gets a completion from the given possibilities.
@@ -110,7 +132,7 @@ class SuggestFromList(Suggester):
Returns: Returns:
A valid completion suggestion or `None`. A valid completion suggestion or `None`.
""" """
for suggestion in self._suggestions: for idx, suggestion in enumerate(self._for_comparison):
if suggestion.startswith(value): if suggestion.startswith(value):
return suggestion return self._suggestions[idx]
return None return None

View File

@@ -38,9 +38,7 @@ class _InputRenderable:
value = input.value value = input.value
value_length = len(value) value_length = len(value)
suggestion = input._suggestion suggestion = input._suggestion
show_suggestion = suggestion.startswith(value) and ( show_suggestion = len(suggestion) > value_length
len(suggestion) > value_length
)
if show_suggestion: if show_suggestion:
result += Text( result += Text(
suggestion[value_length:], suggestion[value_length:],
@@ -388,7 +386,7 @@ class Input(Widget, can_focus=True):
async def _on_suggestion_ready(self, event: SuggestionReady) -> None: async def _on_suggestion_ready(self, event: SuggestionReady) -> None:
"""Handle suggestion messages and set the suggestion when relevant.""" """Handle suggestion messages and set the suggestion when relevant."""
if event.initial_value == self.value: if event.value == self.value:
self._suggestion = event.suggestion self._suggestion = event.suggestion
def insert_text_at_cursor(self, text: str) -> None: def insert_text_at_cursor(self, text: str) -> None: