Skip to content

Commit 6c8b6ef

Browse files
Added DeduplicateCompleter and ConditionalCompleter.
Also added a `deduplicate` argument to merge_completers.
1 parent eced76b commit 6c8b6ef

File tree

4 files changed

+126
-2
lines changed

4 files changed

+126
-2
lines changed

prompt_toolkit/completion/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
CompleteEvent,
33
Completer,
44
Completion,
5+
ConditionalCompleter,
56
DummyCompleter,
67
DynamicCompleter,
78
ThreadedCompleter,
89
get_common_complete_suffix,
910
merge_completers,
1011
)
12+
from .deduplicate import DeduplicateCompleter
1113
from .filesystem import ExecutableCompleter, PathCompleter
1214
from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter
1315
from .nested import NestedCompleter
@@ -21,6 +23,7 @@
2123
"DummyCompleter",
2224
"DynamicCompleter",
2325
"CompleteEvent",
26+
"ConditionalCompleter",
2427
"merge_completers",
2528
"get_common_complete_suffix",
2629
# Filesystem.
@@ -33,4 +36,6 @@
3336
"NestedCompleter",
3437
# Word completer.
3538
"WordCompleter",
39+
# Deduplicate
40+
"DeduplicateCompleter",
3641
]

prompt_toolkit/completion/base.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from prompt_toolkit.document import Document
77
from prompt_toolkit.eventloop import generator_to_async_generator
8+
from prompt_toolkit.filters import FilterOrBool, to_filter
89
from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
910

1011
__all__ = [
@@ -14,6 +15,7 @@
1415
"DummyCompleter",
1516
"DynamicCompleter",
1617
"CompleteEvent",
18+
"ConditionalCompleter",
1719
"merge_completers",
1820
"get_common_complete_suffix",
1921
]
@@ -275,6 +277,42 @@ def __repr__(self) -> str:
275277
return "DynamicCompleter(%r -> %r)" % (self.get_completer, self.get_completer())
276278

277279

280+
class ConditionalCompleter(Completer):
281+
"""
282+
Wrapper around any other completer that will enable/disable the completions
283+
depending on whether the received condition is satisfied.
284+
285+
:param completer: :class:`.Completer` instance.
286+
:param filter: :class:`.Filter` instance.
287+
"""
288+
289+
def __init__(self, completer: Completer, filter: FilterOrBool) -> None:
290+
self.completer = completer
291+
self.filter = to_filter(filter)
292+
293+
def __repr__(self) -> str:
294+
return "ConditionalCompleter(%r, filter=%r)" % (self.completer, self.filter)
295+
296+
def get_completions(
297+
self, document: Document, complete_event: CompleteEvent
298+
) -> Iterable[Completion]:
299+
# Get all completions in a blocking way.
300+
if self.filter():
301+
for c in self.completer.get_completions(document, complete_event):
302+
yield c
303+
304+
async def get_completions_async(
305+
self, document: Document, complete_event: CompleteEvent
306+
) -> AsyncGenerator[Completion, None]:
307+
308+
# Get all completions in a non-blocking way.
309+
if self.filter():
310+
async for item in self.completer.get_completions_async(
311+
document, complete_event
312+
):
313+
yield item
314+
315+
278316
class _MergedCompleter(Completer):
279317
"""
280318
Combine several completers into one.
@@ -295,16 +333,27 @@ async def get_completions_async(
295333
self, document: Document, complete_event: CompleteEvent
296334
) -> AsyncGenerator[Completion, None]:
297335

298-
# Get all completions from the other completers in a blocking way.
336+
# Get all completions from the other completers in a non-blocking way.
299337
for completer in self.completers:
300338
async for item in completer.get_completions_async(document, complete_event):
301339
yield item
302340

303341

304-
def merge_completers(completers: Sequence[Completer]) -> _MergedCompleter:
342+
def merge_completers(
343+
completers: Sequence[Completer], deduplicate: bool = False
344+
) -> Completer:
305345
"""
306346
Combine several completers into one.
347+
348+
:param deduplicate: If `True`, wrap the result in a `DeduplicateCompleter`
349+
so that completions that would result in the same text will be
350+
deduplicated.
307351
"""
352+
if deduplicate:
353+
from .deduplicate import DeduplicateCompleter
354+
355+
return DeduplicateCompleter(_MergedCompleter(completers))
356+
308357
return _MergedCompleter(completers)
309358

310359

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import Iterable, Set
2+
3+
from prompt_toolkit.document import Document
4+
5+
from .base import CompleteEvent, Completer, Completion
6+
7+
__all__ = ["DeduplicateCompleter"]
8+
9+
10+
class DeduplicateCompleter(Completer):
11+
"""
12+
Wrapper around a completer that removes duplicates. Only the first unique
13+
completions are kept.
14+
15+
Completions are considered to be a duplicate if they result in the same
16+
document text when they would be applied.
17+
"""
18+
19+
def __init__(self, completer: Completer) -> None:
20+
self.completer = completer
21+
22+
def get_completions(
23+
self, document: Document, complete_event: CompleteEvent
24+
) -> Iterable[Completion]:
25+
# Keep track of the document strings we'd get after applying any completion.
26+
found_so_far: Set[str] = set()
27+
28+
for completion in self.completer.get_completions(document, complete_event):
29+
text_if_applied = (
30+
document.text[: document.cursor_position + completion.start_position]
31+
+ completion.text
32+
+ document.text[document.cursor_position :]
33+
)
34+
35+
if text_if_applied == document.text:
36+
# Don't include completions that don't have any effect at all.
37+
continue
38+
39+
if text_if_applied in found_so_far:
40+
continue
41+
42+
found_so_far.add(text_if_applied)
43+
yield completion

tests/test_completion.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
from prompt_toolkit.completion import (
88
CompleteEvent,
9+
DeduplicateCompleter,
910
FuzzyWordCompleter,
1011
NestedCompleter,
1112
PathCompleter,
1213
WordCompleter,
14+
merge_completers,
1315
)
1416
from prompt_toolkit.document import Document
1517

@@ -439,3 +441,28 @@ def test_nested_completer():
439441
Document("show ip interface br"), CompleteEvent()
440442
)
441443
assert {c.text for c in completions} == {"brief"}
444+
445+
446+
def test_deduplicate_completer():
447+
def create_completer(deduplicate: bool):
448+
return merge_completers(
449+
[
450+
WordCompleter(["hello", "world", "abc", "def"]),
451+
WordCompleter(["xyz", "xyz", "abc", "def"]),
452+
],
453+
deduplicate=deduplicate,
454+
)
455+
456+
completions = list(
457+
create_completer(deduplicate=False).get_completions(
458+
Document(""), CompleteEvent()
459+
)
460+
)
461+
assert len(completions) == 8
462+
463+
completions = list(
464+
create_completer(deduplicate=True).get_completions(
465+
Document(""), CompleteEvent()
466+
)
467+
)
468+
assert len(completions) == 5

0 commit comments

Comments
 (0)