import copy import logging import os import re from importlib.util import find_spec from deoplete.base.source import Base from deoplete.util import bytepos2charpos, getlines, load_external_module load_external_module(__file__, 'sources') from deoplete_jedi import profiler # isort:skip # noqa: E402 # Type mapping. Empty values will use the key value instead. # Keep them 5 characters max to minimize required space to display. _types = { 'import': 'imprt', 'class': '', 'function': 'def', 'globalstmt': 'var', 'instance': 'var', 'statement': 'var', 'keyword': 'keywd', 'module': 'mod', 'param': 'arg', 'property': 'prop', 'bool': '', 'bytes': 'byte', 'complex': 'cmplx', 'dict': '', 'list': '', 'float': '', 'int': '', 'object': 'obj', 'set': '', 'slice': '', 'str': '', 'tuple': '', 'mappingproxy': 'dict', # cls.__dict__ 'member_descriptor': 'cattr', 'getset_descriptor': 'cprop', 'method_descriptor': 'cdef', } def sort_key(item): w = item.get('name') z = len(w) - len(w.lstrip('_')) return (('z' * z) + w.lower()[z:], len(w)) class Source(Base): def __init__(self, vim): Base.__init__(self, vim) self.name = 'jedi' self.mark = '[jedi]' self.rank = 500 self.filetypes = ['python', 'cython', 'pyrex'] self.input_pattern = (r'[\w\)\]\}\'\"]+\.\w*$|' r'^\s*@\w*$|' r'^\s*from\s+[\w\.]*(?:\s+import\s+(?:\w*(?:,\s*)?)*)?|' r'^\s*import\s+(?:[\w\.]*(?:,\s*)?)*') self._async_keys = set() self.workers_started = False self._jedi = None def on_init(self, context): vars = context['vars'] self.statement_length = 50 if 'deoplete#sources#jedi#statement_length' in vars: self.statement_length = vars[ 'deoplete#sources#jedi#statement_length'] self.enable_typeinfo = True if 'deoplete#sources#jedi#enable_typeinfo' in vars: self.enable_typeinfo = vars[ 'deoplete#sources#jedi#enable_typeinfo'] self.enable_short_types = False if 'deoplete#sources#jedi#enable_short_types' in vars: self.enable_short_types = vars[ 'deoplete#sources#jedi#enable_short_types'] self.short_types_map = copy.copy(_types) if 'deoplete#sources#jedi#short_types_map' in vars: self.short_types_map.update(vars[ 'deoplete#sources#jedi#short_types_map']) self.show_docstring = False if 'deoplete#sources#jedi#show_docstring' in vars: self.show_docstring = vars[ 'deoplete#sources#jedi#show_docstring'] self.ignore_errors = False if 'deoplete#sources#jedi#ignore_errors' in vars: self.ignore_errors = vars[ 'deoplete#sources#jedi#ignore_errors'] self.ignore_private_members = False if 'deoplete#sources#jedi#ignore_private_members' in vars: self.ignore_private_members = vars[ 'deoplete#sources#jedi#ignore_private_members'] # TODO(blueyed) self.extra_path = '' if 'deoplete#sources#jedi#extra_path' in vars: self.extra_path = vars[ 'deoplete#sources#jedi#extra_path'] if not self.is_debug_enabled: root_log = logging.getLogger('deoplete') child_log = root_log.getChild('jedi') child_log.propagate = False self._python_path = None """Current Python executable.""" self._env = None """Current Jedi Environment.""" self._envs = {} """Cache for Jedi Environments.""" if find_spec('jedi'): import jedi # noqa: E402 self._jedi = jedi else: self.print_error( 'jedi module is not found. You need to install it.') @profiler.profile def set_env(self, python_path): if not python_path: import shutil python_path = shutil.which('python') self._python_path = python_path try: self._env = self._envs[python_path] except KeyError: self._env = self._jedi.api.environment.Environment( python_path, env_vars={'PYTHONPATH': str(self.extra_path)}) self.debug('Using Jedi environment: %r', self._env) @profiler.profile def get_script(self, source, filename, environment): return self._jedi.Script(code=source, path=filename, environment=self._env) @profiler.profile def get_completions(self, script, line, col): return script.complete(line, col) @profiler.profile def finalize_completions(self, completions): out = [] tmp_filecache = {} for c in completions: out.append(self.parse_completion(c, tmp_filecache)) if self.ignore_private_members: out = [x for x in out if not x['name'].startswith('__')] # partly from old finalized_cached out = [self.finalize(x) for x in sorted(out, key=sort_key)] return out @profiler.profile def gather_candidates(self, context): if not self._jedi: return [] python_path = None if 'deoplete#sources#jedi#python_path' in context['vars']: python_path = context['vars'][ 'deoplete#sources#jedi#python_path'] if python_path != self._python_path or self.extra_path: self.set_env(python_path) line = context['position'][1] col = bytepos2charpos( context['encoding'], context['input'], context['complete_position']) buf = self.vim.current.buffer filename = str(buf.name) # Only use source if buffer is modified, to skip transferring, joining, # and splitting the buffer lines unnecessarily. modified = buf.options['modified'] if not modified and os.path.exists(filename): source = None else: source = '\n'.join(getlines(self.vim)) if (line != self.vim.call('line', '.') or context['complete_position'] >= self.vim.call('col', '$')): return [] self.debug('Line: %r, Col: %r, Filename: %r, modified: %r', line, col, filename, modified) script = self.get_script(source, filename, environment=self._env) try: completions = self.get_completions(script, line, col) except BaseException: if not self.ignore_errors: raise return [] return self.finalize_completions(completions) def get_complete_position(self, context): if not self._jedi: return -1 pattern = r'\w*$' if context['input'].lstrip().startswith(('from ', 'import ')): m = re.search(r'[,\s]$', context['input']) if m: return m.end() m = re.search(pattern, context['input']) return m.start() if m else -1 def mix_boilerplate(self, completions): seen = set() for item in self.boilerplate + completions: if item['name'] in seen: continue seen.add(item['name']) yield item def finalize(self, item): abbr = item['name'] desc = item['doc'] if item['params']: sig = '{}({})'.format(item['name'], ', '.join(item['params'])) sig_len = len(sig) desc = sig + '\n\n' + desc if self.statement_length > 0 and sig_len > self.statement_length: params = [] length = len(item['name']) + 2 for p in item['params']: p = p.split('=', 1)[0] length += len(p) params.append(p) length += 2 * (len(params) - 1) # +5 for the ellipsis and separator while length + 5 > self.statement_length and len(params): length -= len(params[-1]) + 2 params = params[:-1] if len(item['params']) > len(params): params.append('...') sig = '{}({})'.format(item['name'], ', '.join(params)) abbr = sig if self.enable_short_types: kind = item['short_type'] or item['type'] else: kind = item['type'] return { 'word': item['name'], 'abbr': abbr, 'kind': kind, 'info': desc.strip(), 'dup': 1, } def completion_dict(self, name, type_, comp): """Final construction of the completion dict.""" doc = '' if self.show_docstring: try: doc = comp.docstring() except BaseException: if not self.ignore_errors: raise i = doc.find('\n\n') if i != -1: doc = doc[i:] params = None try: if type_ in ('function', 'class'): params = [] for i, p in enumerate(comp.params): desc = p.description.strip() if i == 0 and desc == 'self': continue if '\\n' in desc: desc = desc.replace('\\n', '\\x0A') # Note: Hack for jedi param bugs if desc.startswith('param ') or desc == 'param': desc = desc[5:].strip() if desc: params.append(desc) except Exception: params = None return { 'name': name, 'type': type_, 'short_type': self.short_types_map.get(type_), 'doc': doc.strip(), 'params': params, } def parse_completion(self, comp, cache): """Return a tuple describing the completion. Returns (name, type, description, abbreviated) """ name = comp.name if self.enable_typeinfo: type_ = comp.type else: type_ = '' if self.show_docstring: desc = comp.description else: desc = '' if type_ == 'instance' and desc.startswith(('builtins.', 'posix.')): # Simple description builtin_type = desc.rsplit('.', 1)[-1] if builtin_type in _types: return self.completion_dict(name, builtin_type, comp) return self.completion_dict(name, type_, comp)