Skip to content

Commit 19416ee

Browse files
Improved autocompletion:
- Added fuzzy completion. - Added dictionary key completion (for keys which are strings). - Highlighting of Python keywords in completion drop down.
1 parent 284b38f commit 19416ee

File tree

5 files changed

+157
-6
lines changed

5 files changed

+157
-6
lines changed

examples/ptpython_config/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def configure(repl):
6161
# completion menu is shown.)
6262
repl.complete_while_typing = True
6363

64+
# Fuzzy and dictionary completion.
65+
self.enable_fuzzy_completion = False
66+
self.enable_dictionary_completion = False
67+
6468
# Vi mode.
6569
repl.vi_mode = False
6670

ptpython/completer.py

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
from ptpython.utils import get_jedi_script_from_document
88

9+
import keyword
10+
import ast
911
import re
12+
import six
1013

1114
__all__ = (
1215
'PythonCompleter',
@@ -17,11 +20,14 @@ class PythonCompleter(Completer):
1720
"""
1821
Completer for Python code.
1922
"""
20-
def __init__(self, get_globals, get_locals):
23+
def __init__(self, get_globals, get_locals, get_enable_dictionary_completion):
2124
super(PythonCompleter, self).__init__()
2225

2326
self.get_globals = get_globals
2427
self.get_locals = get_locals
28+
self.get_enable_dictionary_completion = get_enable_dictionary_completion
29+
30+
self.dictionary_completer = DictionaryCompleter(get_globals, get_locals)
2531

2632
self._path_completer_cache = None
2733
self._path_completer_grammar_cache = None
@@ -108,7 +114,16 @@ def get_completions(self, document, complete_event):
108114
"""
109115
Get Python completions.
110116
"""
111-
# Do Path completions
117+
# Do dictionary key completions.
118+
if self.get_enable_dictionary_completion():
119+
has_dict_completions = False
120+
for c in self.dictionary_completer.get_completions(document, complete_event):
121+
has_dict_completions = True
122+
yield c
123+
if has_dict_completions:
124+
return
125+
126+
# Do Path completions (if there were no dictionary completions).
112127
if complete_event.completion_requested or self._complete_path_while_typing(document):
113128
for c in self._path_completer.get_completions(document, complete_event):
114129
yield c
@@ -162,5 +177,107 @@ def get_completions(self, document, complete_event):
162177
pass
163178
else:
164179
for c in completions:
165-
yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols),
166-
display=c.name_with_symbols)
180+
yield Completion(
181+
c.name_with_symbols, len(c.complete) - len(c.name_with_symbols),
182+
display=c.name_with_symbols,
183+
style=_get_style_for_name(c.name_with_symbols))
184+
185+
186+
class DictionaryCompleter(Completer):
187+
"""
188+
Experimental completer for Python dictionary keys.
189+
190+
Warning: This does an `eval` on the Python object before the open square
191+
bracket, which is potentially dangerous. It doesn't match on
192+
function calls, so it only triggers attribute access.
193+
"""
194+
def __init__(self, get_globals, get_locals):
195+
super(DictionaryCompleter, self).__init__()
196+
197+
self.get_globals = get_globals
198+
self.get_locals = get_locals
199+
200+
self.pattern = re.compile(
201+
r'''
202+
# Any expression safe enough to eval while typing.
203+
# No operators, except dot, and only other dict lookups.
204+
# Technically, this can be unsafe of course, if bad code runs
205+
# in `__getattr__` or ``__getitem__``.
206+
(
207+
# Variable name
208+
[a-zA-Z0-9_]+
209+
210+
\s*
211+
212+
(?:
213+
# Attribute access.
214+
\s* \. \s* [a-zA-Z0-9_]+ \s*
215+
216+
|
217+
218+
# Item lookup.
219+
# (We match the square brackets. We don't care about
220+
# matching quotes here in the regex. Nested square
221+
# brackets are not supported.)
222+
\s* \[ [a-zA-Z0-9_'"\s]+ \] \s*
223+
)*
224+
)
225+
226+
# Dict loopup to complete (square bracket open + start of
227+
# string).
228+
\[
229+
\s* ([a-zA-Z0-9_'"]*)$
230+
''',
231+
re.VERBOSE
232+
)
233+
234+
def get_completions(self, document, complete_event):
235+
match = self.pattern.search(document.text_before_cursor)
236+
if match is not None:
237+
object_var, key = match.groups()
238+
object_var = object_var.strip()
239+
240+
# Do lookup of `object_var` in the context.
241+
try:
242+
result = eval(object_var, self.get_globals(), self.get_locals())
243+
except BaseException as e:
244+
return # Many exception, like NameError can be thrown here.
245+
246+
# If this object is a dictionary, complete the keys.
247+
if isinstance(result, dict):
248+
# Try to evaluate the key.
249+
key_obj = key
250+
for k in [key, key + '"', key + "'"]:
251+
try:
252+
key_obj = ast.literal_eval(k)
253+
except (SyntaxError, ValueError):
254+
continue
255+
else:
256+
break
257+
258+
for k in result:
259+
if six.text_type(k).startswith(key_obj):
260+
yield Completion(
261+
six.text_type(repr(k)),
262+
- len(key),
263+
display=six.text_type(repr(k))
264+
)
265+
266+
try:
267+
import builtins
268+
_builtin_names = dir(builtins)
269+
except ImportError: # Python 2.
270+
_builtin_names = []
271+
272+
273+
def _get_style_for_name(name):
274+
"""
275+
Return completion style to use for this name.
276+
"""
277+
if name in _builtin_names:
278+
return 'class:completion.builtin'
279+
280+
if keyword.iskeyword(name):
281+
return 'class:completion.keyword'
282+
283+
return ''

ptpython/python_input.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from prompt_toolkit.styles import DynamicStyle, SwapLightAndDarkStyleTransformation, ConditionalStyleTransformation, AdjustBrightnessStyleTransformation, merge_style_transformations
2626
from prompt_toolkit.utils import is_windows
2727
from prompt_toolkit.validation import ConditionalValidator
28+
from prompt_toolkit.completion import FuzzyCompleter
2829

2930
from .completer import PythonCompleter
3031
from .history_browser import History
@@ -151,7 +152,10 @@ def __init__(self,
151152
self.get_globals = get_globals or (lambda: {})
152153
self.get_locals = get_locals or self.get_globals
153154

154-
self._completer = _completer or PythonCompleter(self.get_globals, self.get_locals)
155+
self._completer = _completer or FuzzyCompleter(
156+
PythonCompleter(self.get_globals, self.get_locals,
157+
lambda: self.enable_dictionary_completion),
158+
enable_fuzzy=Condition(lambda: self.enable_fuzzy_completion))
155159
self._validator = _validator or PythonValidator(self.get_compiler_flags)
156160
self._lexer = _lexer or PygmentsLexer(PythonLexer)
157161

@@ -193,6 +197,8 @@ def __init__(self,
193197
# with the current input.
194198

195199
self.enable_syntax_highlighting = True
200+
self.enable_fuzzy_completion = False
201+
self.enable_dictionary_completion = False
196202
self.swap_light_and_dark = False
197203
self.highlight_matching_parenthesis = False
198204
self.show_sidebar = False # Currently show the sidebar.
@@ -433,6 +439,23 @@ def get_values():
433439
'on': lambda: enable('complete_while_typing') and disable('enable_history_search'),
434440
'off': lambda: disable('complete_while_typing'),
435441
}),
442+
Option(title='Enable fuzzy completion',
443+
description="Enable fuzzy completion.",
444+
get_current_value=lambda: ['off', 'on'][self.enable_fuzzy_completion],
445+
get_values=lambda: {
446+
'on': lambda: enable('enable_fuzzy_completion'),
447+
'off': lambda: disable('enable_fuzzy_completion'),
448+
}),
449+
Option(title='Dictionary completion',
450+
description='Enable experimental dictionary completion.\n'
451+
'WARNING: this does "eval" on fragments of\n'
452+
' your Python input and is\n'
453+
' potentially unsafe.',
454+
get_current_value=lambda: ['off', 'on'][self.enable_dictionary_completion],
455+
get_values=lambda: {
456+
'on': lambda: enable('enable_dictionary_completion'),
457+
'off': lambda: disable('enable_dictionary_completion'),
458+
}),
436459
Option(title='History search',
437460
description='When pressing the up-arrow, filter the history on input starting '
438461
'with the current text. (Not compatible with "Complete while typing".)',

ptpython/style.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def generate_style(python_style, ui_style):
8383
'out': '#ff0000',
8484
'out.number': '#ff0000',
8585

86+
# Completions.
87+
'completion.builtin': '',
88+
'completion.keyword': 'fg:#008800',
89+
90+
'completion.keyword fuzzymatch.inside': 'fg:#008800',
91+
'completion.keyword fuzzymatch.outside': 'fg:#44aa44',
92+
8693
# Separator between windows. (Used above docstring.)
8794
'separator': '#bbbbbb',
8895

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
install_requires = [
1919
'docopt',
2020
'jedi>=0.9.0',
21-
'prompt_toolkit>=2.0.6,<2.1.0',
21+
'prompt_toolkit>=2.0.8,<2.1.0',
2222
'pygments',
2323
],
2424
classifiers=[

0 commit comments

Comments
 (0)