1313
1414import rich .repr
1515
16+ from textual .cache import LRUCache
1617from textual .content import Content
1718from textual .visual import Style
1819
@@ -43,8 +44,8 @@ def branch(self, offset: int) -> tuple[_Search, _Search]:
4344 def groups (self ) -> int :
4445 """Number of groups in offsets."""
4546 groups = 1
46- last_offset = self .offsets [ 0 ]
47- for offset in self . offsets [ 1 :] :
47+ last_offset , * offsets = self .offsets
48+ for offset in offsets :
4849 if offset != last_offset + 1 :
4950 groups += 1
5051 last_offset = offset
@@ -57,13 +58,17 @@ class FuzzySearch:
5758 Unlike a regex solution, this will finds all possible matches.
5859 """
5960
61+ cache : LRUCache [tuple [str , str , bool ], tuple [float , tuple [int , ...]]] = LRUCache (
62+ 1024 * 4
63+ )
64+
6065 def __init__ (self , case_sensitive : bool = False ) -> None :
6166 """Initialize fuzzy search.
6267
6368 Args:
6469 case_sensitive: Is the match case sensitive?
6570 """
66- self . cache : dict [ tuple [ str , str , bool ], tuple [ float , tuple [ int , ...]]] = {}
71+
6772 self .case_sensitive = case_sensitive
6873
6974 def match (self , query : str , candidate : str ) -> tuple [float , tuple [int , ...]]:
@@ -76,7 +81,6 @@ def match(self, query: str, candidate: str) -> tuple[float, tuple[int, ...]]:
7681 Returns:
7782 A pair of (score, tuple of offsets). `(0, ())` for no result.
7883 """
79-
8084 query_regex = ".*?" .join (f"({ escape (character )} )" for character in query )
8185 if not search (
8286 query_regex , candidate , flags = 0 if self .case_sensitive else IGNORECASE
@@ -124,11 +128,11 @@ def score(search: _Search) -> float:
124128 """
125129 # This is a heuristic, and can be tweaked for better results
126130 # Boost first letter matches
127- score : float = len (search .offsets ) + len (
131+ offset_count = len (search .offsets )
132+ score : float = offset_count + len (
128133 first_letters .intersection (search .offsets )
129134 )
130135 # Boost to favor less groups
131- offset_count = len (search .offsets )
132136 normalized_groups = (offset_count - (search .groups - 1 )) / offset_count
133137 score *= 1 + (normalized_groups * normalized_groups )
134138 return score
@@ -141,11 +145,15 @@ def score(search: _Search) -> float:
141145 # Limit the number of loops out of an abundance of caution.
142146 # This should be hard to reach without contrived data.
143147 remaining_loops = 10_000
144-
145148 while stack and (remaining_loops := remaining_loops - 1 ):
146149 search = pop ()
147150 offset = find (query [search .query_offset ], search .candidate_offset )
148151 if offset != - 1 :
152+ if not set (candidate [search .candidate_offset :]).issuperset (
153+ query [search .query_offset :]
154+ ):
155+ # Early out if there is not change of a match
156+ continue
149157 advance_branch , branch = search .branch (offset )
150158 if advance_branch .query_offset == query_size :
151159 yield score (advance_branch ), advance_branch .offsets
0 commit comments