class Deoplete(logger.LoggingMixin): def __init__(self, vim: Nvim): self.name = 'core' self._vim = vim self._runtimepath = '' self._runtimepath_list: typing.List[str] = [] self._custom: typing.Dict[str, typing.Dict[str, typing.Any]] = {} self._loaded_paths: typing.Set[str] = set() self._prev_results: typing.Dict[int, Candidates] = {} self._prev_input = '' self._prev_next_input = '' self._context: typing.Optional[Context] = None self._parents: typing.List[Parent] = [] self._parent_count = 0 self._max_parents = self._vim.call('deoplete#custom#_get_option', 'num_processes') self._hier_mark_ignore = self._vim.call('deoplete#custom#_get_option', 'hier_mark_ignore') self._hier_mark_ignore = set(self._hier_mark_ignore) if self._max_parents != 1 and not hasattr(self._vim, 'loop'): msg = ('pynvim 0.3.0+ is required for %d parents. ' 'Using single process.' % self._max_parents) error(self._vim, msg) self._max_parents = 1 # Enable logging for more information, and e.g. # deoplete-jedi picks up the log filename from deoplete's handler in # its on_init. if self._vim.vars['deoplete#_logging']: self.enable_logging() if hasattr(self._vim, 'channel_id'): self._vim.vars['deoplete#_channel_id'] = self._vim.channel_id self._vim.vars['deoplete#_initialized'] = True def enable_logging(self) -> None: logging = self._vim.vars['deoplete#_logging'] logger.setup(self._vim, logging['level'], logging['logfile']) self.is_debug_enabled = True def init_context(self) -> None: self._context = Context(self._vim) # Initialization context = self._context.get('Init') context['rpc'] = 'deoplete_on_event' self.on_event(context) def completion_begin(self, user_context: UserContext) -> None: if not self._context: self.init_context() else: self._context._init_cached() context = self._context.get(user_context['event']) # type: ignore context.update(user_context) self.debug( 'completion_begin (%s): %r', # type: ignore context['event'], context['input']) if self._vim.call('deoplete#handler#_check_omnifunc', context): return self._check_recache(context) try: (is_async, needs_poll, position, candidates) = self._merge_results(context) except Exception: error_tb(self._vim, 'Error while gathering completions') is_async = False needs_poll = False position = -1 candidates = [] if needs_poll: self._vim.call('deoplete#handler#_async_timer_start') prev_completion = self._vim.vars['deoplete#_prev_completion'] # Skip if async update is same. # Note: If needs_poll, it cannot be skipped. prev_candidates = prev_completion['candidates'] event = context['event'] same_candidates = prev_candidates and candidates == prev_candidates if not needs_poll and same_candidates and (event == 'Async' or event == 'Update'): return # Skip if old completion. if context['time'] < prev_completion['time']: return # error(self._vim, candidates) self._vim.vars['deoplete#_context'] = { 'complete_position': position, 'complete_str': context['input'][position:], 'candidates': candidates, 'event': context['event'], 'input': context['input'], 'time': context['time'], 'is_async': needs_poll, } if candidates or self._vim.call('deoplete#util#check_popup'): self.debug( 'do_complete (%s): ' # type: ignore + '%d candidates, input=%s, complete_position=%d, ' + 'is_async=%d', context['event'], len(candidates), context['input'], position, is_async) self._vim.call('deoplete#handler#_do_complete') def on_event(self, user_context: UserContext) -> None: self._vim.call('deoplete#custom#_update_cache') if not self._context: self.init_context() else: self._context._init_cached() context = self._context.get(user_context['event']) # type: ignore context.update(user_context) self.debug('initialized context: %s', context) # type: ignore self.debug('on_event: %s', context['event']) # type: ignore self._check_recache(context) for parent in self._parents: parent.on_event(context) def _get_results(self, context: UserContext) -> typing.List[typing.Any]: is_async = False needs_poll = False results: typing.List[Candidates] = [] for cnt, parent in enumerate(self._parents): if cnt in self._prev_results: # Use previous result results += copy.deepcopy( self._prev_results[cnt]) # type: ignore else: result = parent.merge_results(context) is_async = is_async or result[0] needs_poll = needs_poll or result[1] if not result[0]: self._prev_results[cnt] = result[2] results += result[2] return [is_async, needs_poll, results] def _merge_results( self, context: UserContext ) -> typing.Tuple[bool, bool, int, typing.List[typing.Any]]: # If parallel feature is enabled, it is updated frequently. # But if it is single process, it cannot be updated. # So it must be updated. async_check = len(self._parents) > 1 or ( context['event'] != 'Async' and context['event'] != 'Update') use_prev = (context['input'] == self._prev_input and context['next_input'] == self._prev_next_input and context['event'] != 'Manual' and async_check) if not use_prev: self._prev_results = {} self._prev_input = context['input'] self._prev_next_input = context['next_input'] [is_async, needs_poll, results] = self._get_results(context) if not results: return (is_async, needs_poll, -1, []) complete_position = min(x['complete_position'] for x in results) all_candidates: typing.List[Candidates] = [] results = sorted(results, key=lambda x: int(x['rank']), reverse=True) results = self._littlebird_update_markers(results) for result in results: candidates = result['candidates'] prefix = context['input'][ complete_position:result['complete_position']] if prefix != '': for candidate in candidates: # Add prefix candidate['word'] = prefix + candidate['word'] all_candidates += candidates # self.debug(candidates) max_list = self._vim.call('deoplete#custom#_get_option', 'max_list') if max_list > 0: all_candidates = all_candidates[:max_list] candidate_marks = self._vim.call('deoplete#custom#_get_option', 'candidate_marks') if candidate_marks: all_candidates = copy.deepcopy(all_candidates) for i, candidate in enumerate(all_candidates): mark = (candidate_marks[i] if i < len(candidate_marks) and candidate_marks[i] else ' ') candidate['menu'] = mark + ' ' + candidate.get('menu', '') return (is_async, needs_poll, complete_position, all_candidates) def _add_parent(self, parent_cls: typing.Callable[[Nvim], Parent]) -> None: parent = parent_cls(self._vim) if self._vim.vars['deoplete#_logging']: parent.enable_logging() self._parents.append(parent) def _find_rplugins(self, source: str) -> typing.Generator[str, None, None]: """Search for base.py or *.py Searches $VIMRUNTIME/*/rplugin/python3/deoplete/$source[s]/ """ if not self._runtimepath_list: return sources = ( Path('rplugin').joinpath('python3', 'deoplete', source, '*.py'), Path('rplugin').joinpath('python3', 'deoplete', source + 's', '*.py'), Path('rplugin').joinpath('python3', 'deoplete', source, '*', '*.py'), ) for src in sources: for path in self._runtimepath_list: yield from glob.iglob(str(Path(path).joinpath(src))) def _load_sources(self, context: UserContext) -> None: if not self._parents and self._max_parents == 1: self._add_parent(deoplete.parent.SyncParent) for path in self._find_rplugins('source'): if path in self._loaded_paths or Path(path).name == 'base.py': continue self._loaded_paths.add(path) if len(self._parents) <= self._parent_count: # Add parent automatically self._add_parent(deoplete.parent.AsyncParent) self._parents[self._parent_count].add_source(path) self.debug( # type: ignore f'Process {self._parent_count}: {path}') self._parent_count += 1 if self._max_parents > 0: self._parent_count %= self._max_parents self._set_source_attributes(context) def _load_filters(self, context: UserContext) -> None: for path in self._find_rplugins('filter'): for parent in self._parents: parent.add_filter(path) def _set_source_attributes(self, context: UserContext) -> None: for parent in self._parents: parent.set_source_attributes(context) def _check_recache(self, context: UserContext) -> None: runtimepath = self._vim.options['runtimepath'] if runtimepath != self._runtimepath: self._runtimepath = runtimepath self._runtimepath_list = runtimepath.split(',') self._load_sources(context) self._load_filters(context) if context['rpc'] != 'deoplete_on_event': self.on_event(context) elif context['custom'] != self._custom: self._set_source_attributes(context) self._custom = context['custom'] def _littlebird_update_markers(self, results): ignore_sources = self._hier_mark_ignore word2menu = dict() for result in reversed(results): updated = set() candidates = result['candidates'] for candidate in candidates: source = candidate['source'] if source in ignore_sources: continue word = candidate['word'] menu = candidate['menu'] if word in updated: continue updated.add(word) if word not in word2menu: word2menu[word] = (menu, menu) else: word2menu[word] = (menu, word2menu[word][0]) for i in range(len(results)): for j in range(len(results[i]['candidates'])): word = results[i]['candidates'][j]['word'] if word in word2menu: results[i]['candidates'][j]['menu'] = word2menu[word][1] return results
class Deoplete(logger.LoggingMixin): def __init__(self, vim): self.name = 'core' self._vim = vim self._runtimepath = '' self._runtimepath_list = [] self._custom = [] self._loaded_paths = set() self._prev_results = {} self._prev_input = '' self._prev_next_input = '' self._context = None self._parents = [] self._parent_count = 0 self._max_parents = self._vim.call('deoplete#custom#_get_option', 'num_processes') if self._max_parents != 1 and not hasattr(self._vim, 'loop'): msg = ('pynvim 0.3.0+ is required for %d parents. ' 'Using single process.' % self._max_parents) error(self._vim, msg) self._max_parents = 1 # Enable logging for more information, and e.g. # deoplete-jedi picks up the log filename from deoplete's handler in # its on_init. if self._vim.vars['deoplete#_logging']: self.enable_logging() if hasattr(self._vim, 'channel_id'): self._vim.vars['deoplete#_channel_id'] = self._vim.channel_id self._vim.vars['deoplete#_initialized'] = True def enable_logging(self): logging = self._vim.vars['deoplete#_logging'] logger.setup(self._vim, logging['level'], logging['logfile']) self.is_debug_enabled = True def init_context(self): self._context = Context(self._vim) # Initialization context = self._context.get('Init') context['rpc'] = 'deoplete_on_event' self.on_event(context) def completion_begin(self, user_context): if not self._context: self.init_context() context = self._context.get(user_context['event']) context.update(user_context) self.debug('completion_begin (%s): %r', context['event'], context['input']) if self._vim.call('deoplete#handler#_check_omnifunc', context): return self._check_recache(context) try: (is_async, needs_poll, position, candidates) = self._merge_results(context) except Exception: error_tb(self._vim, 'Error while gathering completions') is_async = False needs_poll = False position = -1 candidates = [] if needs_poll: self._vim.call('deoplete#handler#_async_timer_start') if not candidates and ('deoplete#_saved_completeopt' in self._vim.vars): self._vim.call('deoplete#mapping#_restore_completeopt') # Async update is skipped if same. prev_completion = self._vim.vars['deoplete#_prev_completion'] prev_candidates = prev_completion['candidates'] if (context['event'] == 'Async' and prev_candidates and len(candidates) <= len(prev_candidates)): return # error(self._vim, candidates) self._vim.vars['deoplete#_context'] = { 'complete_position': position, 'candidates': candidates, 'event': context['event'], 'input': context['input'], 'is_async': is_async, } self.debug('do_complete (%s): ' + '%d candidates, input=%s, complete_position=%d, ' + 'is_async=%d', context['event'], len(candidates), context['input'], position, is_async) self._vim.call('deoplete#handler#_do_complete') def on_event(self, user_context): self._vim.call('deoplete#custom#_update_cache') if not self._context: self.init_context() else: self._context._init_cached() context = self._context.get(user_context['event']) context.update(user_context) self.debug('initialized context: %s', context) self.debug('on_event: %s', context['event']) self._check_recache(context) for parent in self._parents: parent.on_event(context) def _get_results(self, context): is_async = False needs_poll = False results = [] for cnt, parent in enumerate(self._parents): if cnt in self._prev_results: # Use previous result results += copy.deepcopy(self._prev_results[cnt]) else: result = parent.merge_results(context) is_async = is_async or result[0] needs_poll = needs_poll or result[1] if not result[0]: self._prev_results[cnt] = result[2] results += result[2] return [is_async, needs_poll, results] def _merge_results(self, context): use_prev = (context['input'] == self._prev_input and context['next_input'] == self._prev_next_input and context['event'] != 'Manual') if not use_prev: self._prev_results = {} self._prev_input = context['input'] self._prev_next_input = context['next_input'] [is_async, needs_poll, results] = self._get_results(context) if not results: return (is_async, needs_poll, -1, []) complete_position = min(x['complete_position'] for x in results) all_candidates = [] for result in sorted(results, key=lambda x: x['rank'], reverse=True): candidates = result['candidates'] prefix = context['input'][ complete_position:result['complete_position']] if prefix != '': for candidate in candidates: # Add prefix candidate['word'] = prefix + candidate['word'] all_candidates += candidates # self.debug(candidates) max_list = self._vim.call( 'deoplete#custom#_get_option', 'max_list') if max_list > 0: all_candidates = all_candidates[: max_list] candidate_marks = self._vim.call( 'deoplete#custom#_get_option', 'candidate_marks') if candidate_marks: all_candidates = copy.deepcopy(all_candidates) for i, candidate in enumerate(all_candidates): mark = (candidate_marks[i] if i < len(candidate_marks) and candidate_marks[i] else ' ') candidate['menu'] = mark + ' ' + candidate.get('menu', '') return (is_async, needs_poll, complete_position, all_candidates) def _add_parent(self, parent_cls): parent = parent_cls(self._vim) if self._vim.vars['deoplete#_logging']: parent.enable_logging() self._parents.append(parent) def _find_rplugins(self, source): """Search for base.py or *.py Searches $VIMRUNTIME/*/rplugin/python3/deoplete/$source[s]/ """ if not self._runtimepath_list: return sources = ( os.path.join('rplugin', 'python3', 'deoplete', source, '*.py'), os.path.join('rplugin', 'python3', 'deoplete', source + 's', '*.py'), os.path.join('rplugin', 'python3', 'deoplete', source, '*', '*.py'), ) for src in sources: for path in self._runtimepath_list: yield from glob.iglob(os.path.join(path, src)) def _load_sources(self, context): if not self._parents and self._max_parents == 1: self._add_parent(deoplete.parent.SyncParent) for path in self._find_rplugins('source'): if (path in self._loaded_paths or os.path.basename(path) == 'base.py'): continue self._loaded_paths.add(path) if len(self._parents) <= self._parent_count: # Add parent automatically self._add_parent(deoplete.parent.AsyncParent) self._parents[self._parent_count].add_source(path) self.debug('Process %d: %s', self._parent_count, path) self._parent_count += 1 if self._max_parents > 0: self._parent_count %= self._max_parents self._set_source_attributes(context) def _load_filters(self, context): for path in self._find_rplugins('filter'): for parent in self._parents: parent.add_filter(path) def _set_source_attributes(self, context): for parent in self._parents: parent.set_source_attributes(context) def _check_recache(self, context): runtimepath = self._vim.options['runtimepath'] if runtimepath != self._runtimepath: self._runtimepath = runtimepath self._runtimepath_list = runtimepath.split(',') self._load_sources(context) self._load_filters(context) if context['rpc'] != 'deoplete_on_event': self.on_event(context) elif context['custom'] != self._custom: self._set_source_attributes(context) self._custom = context['custom']