13
13
14
14
import rich .repr
15
15
16
+ from textual .cache import LRUCache
16
17
from textual .content import Content
17
18
from textual .visual import Style
18
19
@@ -43,8 +44,8 @@ def branch(self, offset: int) -> tuple[_Search, _Search]:
43
44
def groups (self ) -> int :
44
45
"""Number of groups in offsets."""
45
46
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 :
48
49
if offset != last_offset + 1 :
49
50
groups += 1
50
51
last_offset = offset
@@ -57,13 +58,17 @@ class FuzzySearch:
57
58
Unlike a regex solution, this will finds all possible matches.
58
59
"""
59
60
61
+ cache : LRUCache [tuple [str , str , bool ], tuple [float , tuple [int , ...]]] = LRUCache (
62
+ 1024 * 4
63
+ )
64
+
60
65
def __init__ (self , case_sensitive : bool = False ) -> None :
61
66
"""Initialize fuzzy search.
62
67
63
68
Args:
64
69
case_sensitive: Is the match case sensitive?
65
70
"""
66
- self . cache : dict [ tuple [ str , str , bool ], tuple [ float , tuple [ int , ...]]] = {}
71
+
67
72
self .case_sensitive = case_sensitive
68
73
69
74
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, ...]]:
76
81
Returns:
77
82
A pair of (score, tuple of offsets). `(0, ())` for no result.
78
83
"""
79
-
80
84
query_regex = ".*?" .join (f"({ escape (character )} )" for character in query )
81
85
if not search (
82
86
query_regex , candidate , flags = 0 if self .case_sensitive else IGNORECASE
@@ -124,13 +128,13 @@ def score(search: _Search) -> float:
124
128
"""
125
129
# This is a heuristic, and can be tweaked for better results
126
130
# Boost first letter matches
127
- score : float = sum (
128
- (2.0 if offset in first_letters else 1.0 ) for offset in search .offsets
131
+ offset_count = len (search .offsets )
132
+ score : float = offset_count + len (
133
+ first_letters .intersection (search .offsets )
129
134
)
130
135
# Boost to favor less groups
131
- offset_count = len (search .offsets )
132
136
normalized_groups = (offset_count - (search .groups - 1 )) / offset_count
133
- score *= 1 + (normalized_groups ** 2 )
137
+ score *= 1 + (normalized_groups * normalized_groups )
134
138
return score
135
139
136
140
stack : list [_Search ] = [_Search ()]
@@ -139,20 +143,24 @@ def score(search: _Search) -> float:
139
143
query_size = len (query )
140
144
find = candidate .find
141
145
# Limit the number of loops out of an abundance of caution.
142
- # This would be hard to reach without contrived data.
143
- remaining_loops = 200
144
-
146
+ # This should be hard to reach without contrived data.
147
+ remaining_loops = 10_000
145
148
while stack and (remaining_loops := remaining_loops - 1 ):
146
149
search = pop ()
147
150
offset = find (query [search .query_offset ], search .candidate_offset )
148
151
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
149
157
advance_branch , branch = search .branch (offset )
150
158
if advance_branch .query_offset == query_size :
151
159
yield score (advance_branch ), advance_branch .offsets
152
160
push (branch )
153
161
else :
154
- push (advance_branch )
155
162
push (branch )
163
+ push (advance_branch )
156
164
157
165
158
166
@rich .repr .auto
0 commit comments