Skip to content

Commit 5110867

Browse files
Improve FuzzyCompleter performance.
This should significantly improve the performance of `FuzzyCompleter` when there are many completions. Especially in the case when the "word" before the cursor is an empty string.
1 parent cb925b2 commit 5110867

File tree

1 file changed

+56
-44
lines changed

1 file changed

+56
-44
lines changed

src/prompt_toolkit/completion/fuzzy_completer.py

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(
4949
WORD: bool = False,
5050
pattern: Optional[str] = None,
5151
enable_fuzzy: FilterOrBool = True,
52-
):
52+
) -> None:
5353

5454
assert pattern is None or pattern.startswith("^")
5555

@@ -77,7 +77,6 @@ def _get_pattern(self) -> str:
7777
def _get_fuzzy_completions(
7878
self, document: Document, complete_event: CompleteEvent
7979
) -> Iterable[Completion]:
80-
8180
word_before_cursor = document.get_word_before_cursor(
8281
pattern=re.compile(self._get_pattern())
8382
)
@@ -88,27 +87,35 @@ def _get_fuzzy_completions(
8887
cursor_position=document.cursor_position - len(word_before_cursor),
8988
)
9089

91-
completions = list(self.completer.get_completions(document2, complete_event))
90+
inner_completions = list(
91+
self.completer.get_completions(document2, complete_event)
92+
)
9293

9394
fuzzy_matches: List[_FuzzyMatch] = []
9495

95-
pat = ".*?".join(map(re.escape, word_before_cursor))
96-
pat = f"(?=({pat}))" # lookahead regex to manage overlapping matches
97-
regex = re.compile(pat, re.IGNORECASE)
98-
for compl in completions:
99-
matches = list(regex.finditer(compl.text))
100-
if matches:
101-
# Prefer the match, closest to the left, then shortest.
102-
best = min(matches, key=lambda m: (m.start(), len(m.group(1))))
103-
fuzzy_matches.append(
104-
_FuzzyMatch(len(best.group(1)), best.start(), compl)
105-
)
106-
107-
def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]:
108-
"Sort by start position, then by the length of the match."
109-
return fuzzy_match.start_pos, fuzzy_match.match_length
110-
111-
fuzzy_matches = sorted(fuzzy_matches, key=sort_key)
96+
if word_before_cursor == "":
97+
# If word before the cursor is an empty string, consider all
98+
# completions, without filtering everything with an empty regex
99+
# pattern.
100+
fuzzy_matches = [_FuzzyMatch(0, 0, compl) for compl in inner_completions]
101+
else:
102+
pat = ".*?".join(map(re.escape, word_before_cursor))
103+
pat = f"(?=({pat}))" # lookahead regex to manage overlapping matches
104+
regex = re.compile(pat, re.IGNORECASE)
105+
for compl in inner_completions:
106+
matches = list(regex.finditer(compl.text))
107+
if matches:
108+
# Prefer the match, closest to the left, then shortest.
109+
best = min(matches, key=lambda m: (m.start(), len(m.group(1))))
110+
fuzzy_matches.append(
111+
_FuzzyMatch(len(best.group(1)), best.start(), compl)
112+
)
113+
114+
def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]:
115+
"Sort by start position, then by the length of the match."
116+
return fuzzy_match.start_pos, fuzzy_match.match_length
117+
118+
fuzzy_matches = sorted(fuzzy_matches, key=sort_key)
112119

113120
for match in fuzzy_matches:
114121
# Include these completions, but set the correct `display`
@@ -117,7 +124,8 @@ def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]:
117124
text=match.completion.text,
118125
start_position=match.completion.start_position
119126
- len(word_before_cursor),
120-
display_meta=match.completion.display_meta,
127+
# We access to private `_display_meta` attribute, because that one is lazy.
128+
display_meta=match.completion._display_meta,
121129
display=self._get_display(match, word_before_cursor),
122130
style=match.completion.style,
123131
)
@@ -128,37 +136,41 @@ def _get_display(
128136
"""
129137
Generate formatted text for the display label.
130138
"""
131-
m = fuzzy_match
132-
word = m.completion.text
133139

134-
if m.match_length == 0:
135-
# No highlighting when we have zero length matches (no input text).
136-
# In this case, use the original display text (which can include
137-
# additional styling or characters).
138-
return m.completion.display
140+
def get_display() -> AnyFormattedText:
141+
m = fuzzy_match
142+
word = m.completion.text
139143

140-
result: StyleAndTextTuples = []
144+
if m.match_length == 0:
145+
# No highlighting when we have zero length matches (no input text).
146+
# In this case, use the original display text (which can include
147+
# additional styling or characters).
148+
return m.completion.display
141149

142-
# Text before match.
143-
result.append(("class:fuzzymatch.outside", word[: m.start_pos]))
150+
result: StyleAndTextTuples = []
144151

145-
# The match itself.
146-
characters = list(word_before_cursor)
152+
# Text before match.
153+
result.append(("class:fuzzymatch.outside", word[: m.start_pos]))
147154

148-
for c in word[m.start_pos : m.start_pos + m.match_length]:
149-
classname = "class:fuzzymatch.inside"
150-
if characters and c.lower() == characters[0].lower():
151-
classname += ".character"
152-
del characters[0]
155+
# The match itself.
156+
characters = list(word_before_cursor)
153157

154-
result.append((classname, c))
158+
for c in word[m.start_pos : m.start_pos + m.match_length]:
159+
classname = "class:fuzzymatch.inside"
160+
if characters and c.lower() == characters[0].lower():
161+
classname += ".character"
162+
del characters[0]
155163

156-
# Text after match.
157-
result.append(
158-
("class:fuzzymatch.outside", word[m.start_pos + m.match_length :])
159-
)
164+
result.append((classname, c))
165+
166+
# Text after match.
167+
result.append(
168+
("class:fuzzymatch.outside", word[m.start_pos + m.match_length :])
169+
)
170+
171+
return result
160172

161-
return result
173+
return get_display()
162174

163175

164176
class FuzzyWordCompleter(Completer):

0 commit comments

Comments
 (0)