44from  collections  import  namedtuple 
55from  six  import  string_types 
66
7- from  prompt_toolkit .completion  import  Completer , Completion 
7+ from  prompt_toolkit .document  import  Document 
8+ from  prompt_toolkit .filters  import  to_filter 
9+ 
10+ from  .base  import  Completer , Completion 
11+ from  .word_completer  import  WordCompleter 
812
913__all__  =  [
14+  'FuzzyCompleter' ,
1015 'FuzzyWordCompleter' ,
1116]
1217
1318
14- class  FuzzyWordCompleter (Completer ):
19+ class  FuzzyCompleter (Completer ):
1520 """ 
16-  Fuzzy completion on a list of words. 
21+  Fuzzy completion. 
22+  This wraps any other completer and turns it into a fuzzy completer. 
1723
1824 If the list of words is: ["leopard" , "gorilla", "dinosaur", "cat", "bee"] 
1925 Then trying to complete "oar" would yield "leopard" and "dinosaur", but not 
2026 the others, because they match the regular expression 'o.*a.*r'. 
27+  Similar, in another application "djm" could expand to "django_migrations". 
2128
2229 The results are sorted by relevance, which is defined as the start position 
2330 and the length of the match. 
2431
25-  See: https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python 
32+  Notice that this is not really a tool to work around spelling mistakes, 
33+  like what would be possible with difflib. The purpose is rather to have a 
34+  quicker or more intuitive way to filter the given completions, especially 
35+  when many completions have a common prefix. 
2636
27-  :param words: List of words or callable that returns a list of words. 
28-  :param meta_dict: Optional dict mapping words to their meta-information. 
29-  :param WORD: When True, use WORD characters. 
30-  :param sort_results: Boolean to determine whether to sort the results (default: True). 
37+  Fuzzy algorithm is based on this post: 
38+  https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python 
3139
32-  Fuzzy algorithm is based on this post: https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python 
40+  :param completer: A :class:`~.Completer` instance. 
41+  :param WORD: When True, use WORD characters. 
42+  :param pattern: Regex pattern which selects the characters before the 
43+  cursor that are considered for the fuzzy matching. 
44+  :param enable_fuzzy: (bool or `Filter`) Enabled the fuzzy behavior. For 
45+  easily turning fuzzyness on or off according to a certain condition. 
3346 """ 
34-  def  __init__ (self , words , meta_dict = None , WORD = False , sort_results = True ):
35-  assert  callable (words ) or  all (isinstance (w , string_types ) for  w  in  words )
47+  def  __init__ (self , completer , WORD = False , pattern = None , enable_fuzzy = True ):
48+  assert  isinstance (completer , Completer )
49+  assert  pattern  is  None  or  pattern .startswith ('^' )
3650
37-  self .words  =  words 
38-  self .meta_dict  =  meta_dict  or  {}
39-  self .sort_results  =  sort_results 
51+  self .completer  =  completer 
52+  self .pattern  =  pattern 
4053 self .WORD  =  WORD 
54+  self .pattern  =  pattern 
55+  self .enable_fuzzy  =  to_filter (enable_fuzzy )
4156
4257 def  get_completions (self , document , complete_event ):
43-  # Get list of words. 
44-  words  =  self .words 
45-  if  callable (words ):
46-  words  =  words ()
58+  if  self .enable_fuzzy ():
59+  return  self ._get_fuzzy_completions (document , complete_event )
60+  else :
61+  return  self .completer .get_completions (document , complete_event )
62+ 
63+  def  _get_pattern (self ):
64+  if  self .pattern :
65+  return  self .pattern 
66+  if  self .WORD :
67+  return  r'[^\s]+' 
68+  return  '^[a-zA-Z0-9_]*' 
69+ 
70+  def  _get_fuzzy_completions (self , document , complete_event ):
71+  word_before_cursor  =  document .get_word_before_cursor (
72+  pattern = re .compile (self ._get_pattern ()))
73+ 
74+  # Get completions 
75+  document2  =  Document (
76+  text = document .text [:document .cursor_position  -  len (word_before_cursor )],
77+  cursor_position = document .cursor_position  -  len (word_before_cursor ))
4778
48-  word_before_cursor  =  document . get_word_before_cursor ( WORD = self .WORD )
79+  completions  =  list ( self .completer . get_completions ( document2 ,  complete_event ) )
4980
5081 fuzzy_matches  =  []
5182 pat  =  '.*?' .join (map (re .escape , word_before_cursor ))
5283 pat  =  '(?=({0}))' .format (pat ) # lookahead regex to manage overlapping matches 
5384 regex  =  re .compile (pat , re .IGNORECASE )
54-  for  word  in  words :
55-  matches  =  list (regex .finditer (word ))
85+  for  compl  in  completions :
86+  matches  =  list (regex .finditer (compl . text ))
5687 if  matches :
5788 # Prefer the match, closest to the left, then shortest. 
5889 best  =  min (matches , key = lambda  m : (m .start (), len (m .group (1 ))))
59-  fuzzy_matches .append (_FuzzyMatch (len (best .group (1 )), best .start (), word ))
90+  fuzzy_matches .append (_FuzzyMatch (len (best .group (1 )), best .start (), compl ))
6091
6192 def  sort_key (fuzzy_match ):
6293 " Sort by start position, then by the length of the match. " 
@@ -65,45 +96,76 @@ def sort_key(fuzzy_match):
6596 fuzzy_matches  =  sorted (fuzzy_matches , key = sort_key )
6697
6798 for  match  in  fuzzy_matches :
68-  display_meta   =   self . meta_dict . get ( match . word ,  '' ) 
69- 
99+  # Include these completions, but set the correct `display` 
100+   # attribute and `start_position`. 
70101 yield  Completion (
71-  match .word ,
72-  - len (word_before_cursor ),
73-  display_meta = display_meta ,
74-  display = self ._get_display (match , word_before_cursor ))
102+  match .completion .text ,
103+  start_position = match .completion .start_position  -  len (word_before_cursor ),
104+  display_meta = match .completion .display_meta ,
105+  display = self ._get_display (match , word_before_cursor ),
106+  style = match .completion .style )
75107
76108 def  _get_display (self , fuzzy_match , word_before_cursor ):
77109 """ 
78110 Generate formatted text for the display label. 
79111 """ 
80112 m  =  fuzzy_match 
113+  word  =  m .completion .text 
81114
82115 if  m .match_length  ==  0 :
83116 # No highlighting when we have zero length matches (no input text). 
84-  return  m . word 
117+  return  word 
85118
86119 result  =  []
87120
88121 # Text before match. 
89-  result .append (('class:fuzzymatch.outside' , m . word [:m .start_pos ]))
122+  result .append (('class:fuzzymatch.outside' , word [:m .start_pos ]))
90123
91124 # The match itself. 
92125 characters  =  list (word_before_cursor )
93126
94-  for  c  in  m . word [m .start_pos :m .start_pos  +  m .match_length ]:
127+  for  c  in  word [m .start_pos :m .start_pos  +  m .match_length ]:
95128 classname  =  'class:fuzzymatch.inside' 
96-  if  characters  and  c  ==  characters [0 ]:
129+  if  characters  and  c . lower ()  ==  characters [0 ]. lower () :
97130 classname  +=  '.character' 
98131 del  characters [0 ]
99132
100133 result .append ((classname , c ))
101134
102135 # Text after match. 
103136 result .append (
104-  ('class:fuzzymatch.outside' , m . word [m .start_pos  +  m .match_length :]))
137+  ('class:fuzzymatch.outside' , word [m .start_pos  +  m .match_length :]))
105138
106139 return  result 
107140
108141
109- _FuzzyMatch  =  namedtuple ('_FuzzyMatch' , 'match_length start_pos word' )
142+ class  FuzzyWordCompleter (Completer ):
143+  """ 
144+  Fuzzy completion on a list of words. 
145+ 
146+  (This is basically a `WordCompleter` wrapped in a `FuzzyCompleter`.) 
147+ 
148+  :param words: List of words or callable that returns a list of words. 
149+  :param meta_dict: Optional dict mapping words to their meta-information. 
150+  :param WORD: When True, use WORD characters. 
151+  """ 
152+  def  __init__ (self , words , meta_dict = None , WORD = False ):
153+  assert  callable (words ) or  all (isinstance (w , string_types ) for  w  in  words )
154+ 
155+  self .words  =  words 
156+  self .meta_dict  =  meta_dict  or  {}
157+  self .WORD  =  WORD 
158+ 
159+  self .word_completer  =  WordCompleter (
160+  words = lambda : self .words ,
161+  WORD = self .WORD )
162+ 
163+  self .fuzzy_completer  =  FuzzyCompleter (
164+  self .word_completer ,
165+  WORD = self .WORD )
166+ 
167+  def  get_completions (self , document , complete_event ):
168+  return  self .fuzzy_completer .get_completions (document , complete_event )
169+ 
170+ 
171+ _FuzzyMatch  =  namedtuple ('_FuzzyMatch' , 'match_length start_pos completion' )
0 commit comments