You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
6.8 KiB
167 lines
6.8 KiB
from __future__ import unicode_literals
|
|
|
|
from prompt_toolkit.completion import Completer, Completion, PathCompleter
|
|
from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar
|
|
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
|
|
|
|
from ptpython.utils import get_jedi_script_from_document
|
|
|
|
import re
|
|
|
|
__all__ = (
|
|
'PythonCompleter',
|
|
)
|
|
|
|
|
|
class PythonCompleter(Completer):
|
|
"""
|
|
Completer for Python code.
|
|
"""
|
|
def __init__(self, get_globals, get_locals):
|
|
super(PythonCompleter, self).__init__()
|
|
|
|
self.get_globals = get_globals
|
|
self.get_locals = get_locals
|
|
|
|
self._path_completer_cache = None
|
|
self._path_completer_grammar_cache = None
|
|
|
|
@property
|
|
def _path_completer(self):
|
|
if self._path_completer_cache is None:
|
|
self._path_completer_cache = GrammarCompleter(
|
|
self._path_completer_grammar, {
|
|
'var1': PathCompleter(expanduser=True),
|
|
'var2': PathCompleter(expanduser=True),
|
|
})
|
|
return self._path_completer_cache
|
|
|
|
@property
|
|
def _path_completer_grammar(self):
|
|
"""
|
|
Return the grammar for matching paths inside strings inside Python
|
|
code.
|
|
"""
|
|
# We make this lazy, because it delays startup time a little bit.
|
|
# This way, the grammar is build during the first completion.
|
|
if self._path_completer_grammar_cache is None:
|
|
self._path_completer_grammar_cache = self._create_path_completer_grammar()
|
|
return self._path_completer_grammar_cache
|
|
|
|
def _create_path_completer_grammar(self):
|
|
def unwrapper(text):
|
|
return re.sub(r'\\(.)', r'\1', text)
|
|
|
|
def single_quoted_wrapper(text):
|
|
return text.replace('\\', '\\\\').replace("'", "\\'")
|
|
|
|
def double_quoted_wrapper(text):
|
|
return text.replace('\\', '\\\\').replace('"', '\\"')
|
|
|
|
grammar = r"""
|
|
# Text before the current string.
|
|
(
|
|
[^'"#] | # Not quoted characters.
|
|
''' ([^'\\]|'(?!')|''(?!')|\\.])* ''' | # Inside single quoted triple strings
|
|
"" " ([^"\\]|"(?!")|""(?!^)|\\.])* "" " | # Inside double quoted triple strings
|
|
|
|
\#[^\n]*(\n|$) | # Comment.
|
|
"(?!"") ([^"\\]|\\.)*" | # Inside double quoted strings.
|
|
'(?!'') ([^'\\]|\\.)*' # Inside single quoted strings.
|
|
|
|
# Warning: The negative lookahead in the above two
|
|
# statements is important. If we drop that,
|
|
# then the regex will try to interpret every
|
|
# triple quoted string also as a single quoted
|
|
# string, making this exponentially expensive to
|
|
# execute!
|
|
)*
|
|
# The current string that we're completing.
|
|
(
|
|
' (?P<var1>([^\n'\\]|\\.)*) | # Inside a single quoted string.
|
|
" (?P<var2>([^\n"\\]|\\.)*) # Inside a double quoted string.
|
|
)
|
|
"""
|
|
|
|
return compile_grammar(
|
|
grammar,
|
|
escape_funcs={
|
|
'var1': single_quoted_wrapper,
|
|
'var2': double_quoted_wrapper,
|
|
},
|
|
unescape_funcs={
|
|
'var1': unwrapper,
|
|
'var2': unwrapper,
|
|
})
|
|
|
|
def _complete_path_while_typing(self, document):
|
|
char_before_cursor = document.char_before_cursor
|
|
return document.text and (
|
|
char_before_cursor.isalnum() or char_before_cursor in '/.~')
|
|
|
|
def _complete_python_while_typing(self, document):
|
|
char_before_cursor = document.char_before_cursor
|
|
return document.text and (
|
|
char_before_cursor.isalnum() or char_before_cursor in '_.')
|
|
|
|
def get_completions(self, document, complete_event):
|
|
"""
|
|
Get Python completions.
|
|
"""
|
|
# Do Path completions
|
|
if complete_event.completion_requested or self._complete_path_while_typing(document):
|
|
for c in self._path_completer.get_completions(document, complete_event):
|
|
yield c
|
|
|
|
# If we are inside a string, Don't do Jedi completion.
|
|
if self._path_completer_grammar.match(document.text_before_cursor):
|
|
return
|
|
|
|
# Do Jedi Python completions.
|
|
if complete_event.completion_requested or self._complete_python_while_typing(document):
|
|
script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals())
|
|
|
|
if script:
|
|
try:
|
|
completions = script.completions()
|
|
except TypeError:
|
|
# Issue #9: bad syntax causes completions() to fail in jedi.
|
|
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/9
|
|
pass
|
|
except UnicodeDecodeError:
|
|
# Issue #43: UnicodeDecodeError on OpenBSD
|
|
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/43
|
|
pass
|
|
except AttributeError:
|
|
# Jedi issue #513: https://github.com/davidhalter/jedi/issues/513
|
|
pass
|
|
except ValueError:
|
|
# Jedi issue: "ValueError: invalid \x escape"
|
|
pass
|
|
except KeyError:
|
|
# Jedi issue: "KeyError: u'a_lambda'."
|
|
# https://github.com/jonathanslenders/ptpython/issues/89
|
|
pass
|
|
except IOError:
|
|
# Jedi issue: "IOError: No such file or directory."
|
|
# https://github.com/jonathanslenders/ptpython/issues/71
|
|
pass
|
|
except AssertionError:
|
|
# In jedi.parser.__init__.py: 227, in remove_last_newline,
|
|
# the assertion "newline.value.endswith('\n')" can fail.
|
|
pass
|
|
except SystemError:
|
|
# In jedi.api.helpers.py: 144, in get_stack_at_position
|
|
# raise SystemError("This really shouldn't happen. There's a bug in Jedi.")
|
|
pass
|
|
except NotImplementedError:
|
|
# See: https://github.com/jonathanslenders/ptpython/issues/223
|
|
pass
|
|
except Exception:
|
|
# Supress all other Jedi exceptions.
|
|
pass
|
|
else:
|
|
for c in completions:
|
|
yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols),
|
|
display=c.name_with_symbols)
|