From bb5839dd74c76e502572e54a50b48eb56c785b2b Mon Sep 17 00:00:00 2001 From: Stanislav Golovanov Date: Thu, 11 Apr 2013 15:42:36 +0400 Subject: [PATCH] Add general filename completer & other stuff --- python/completers/all/identifier_completer.py | 14 +-- python/completers/completer.py | 48 +++++++++- .../completers/general/filename_completer.py | 94 +++++++++++++++++++ .../completers/general/general_completer.py | 78 +++++++-------- python/completers/general/hook.py | 21 ----- .../completers/general/ultisnips_completer.py | 37 +++----- 6 files changed, 184 insertions(+), 108 deletions(-) create mode 100644 python/completers/general/filename_completer.py delete mode 100644 python/completers/general/hook.py diff --git a/python/completers/all/identifier_completer.py b/python/completers/all/identifier_completer.py index fcd3f07f..ba0e82e0 100644 --- a/python/completers/all/identifier_completer.py +++ b/python/completers/all/identifier_completer.py @@ -17,32 +17,24 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -from completers.completer import Completer +from completers.completer import GeneralCompleter import vim import vimsupport import ycm_core import ycm_utils as utils MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10 -MIN_NUM_CHARS = int( vimsupport.GetVariableValue( - "g:ycm_min_num_of_chars_for_completion" ) ) -class IdentifierCompleter( Completer ): +class IdentifierCompleter( GeneralCompleter ): def __init__( self ): super( IdentifierCompleter, self ).__init__() self.completer = ycm_core.IdentifierCompleter() self.completer.EnableThreading() - def SupportedFiletypes( self ): - # magic token meaning all filetypes - return set( [ 'ycm_all' ] ) - - def ShouldUseNow( self, start_column ): - query_length = vimsupport.CurrentColumn() - start_column - return query_length >= MIN_NUM_CHARS + return self.QueryLengthAboveMinThreshold( start_column ) def CandidatesForQueryAsync( self, query, unused_start_column ): diff --git a/python/completers/completer.py b/python/completers/completer.py index e5e18675..9a3868de 100644 --- a/python/completers/completer.py +++ b/python/completers/completer.py @@ -21,11 +21,14 @@ import abc import vim import vimsupport import ycm_core -import ycm from collections import defaultdict +from threading import Event NO_USER_COMMANDS = 'This completer does not define any commands.' +MIN_NUM_CHARS = int( vimsupport.GetVariableValue( + "g:ycm_min_num_of_chars_for_completion" ) ) + class Completer( object ): """A base class for all Completers in YCM. @@ -154,6 +157,11 @@ class Completer( object ): return False + def QueryLengthAboveMinThreshold( self, start_column ): + query_length = vimsupport.CurrentColumn() - start_column + return query_length >= MIN_NUM_CHARS + + # It's highly likely you DON'T want to override this function but the *Inner # version of it. def CandidatesForQueryAsync( self, query, start_column ): @@ -178,10 +186,15 @@ class Completer( object ): candidates = candidates.words items_are_objects = 'word' in candidates[ 0 ] - return ycm_core.FilterAndSortCandidates( - candidates, - 'word' if items_are_objects else '', - query ) + try: + matches = ycm_core.FilterAndSortCandidates( + candidates, + 'word' if items_are_objects else '', + query ) + except: + matches = [] + + return matches def CandidatesForQueryAsyncInner( self, query, start_column ): @@ -295,6 +308,31 @@ class Completer( object ): return '' +class GeneralCompleter( Completer ): + """ + A base class for General completers in YCM. + + Because this is a subclass of Completer class, you should refer to the + dpcumentation of Completer API. + + Only exception is that GeneralCompleterStore class that collects and controls + all general completers already adds threading for completers, so there + is no need to add a threading to new general completers. + + added __init__ fields are for GeneralCompleterStore internal use only. + """ + def __init__( self ): + super( GeneralCompleter, self ).__init__() + self._should_start = Event() + self._should_use = False + self._finished = Event() + self._results = [] + + + def SupportedFiletypes( self ): + return set() + + class CompletionsCache( object ): def __init__( self ): self.line = -1 diff --git a/python/completers/general/filename_completer.py b/python/completers/general/filename_completer.py new file mode 100644 index 00000000..f1d1d158 --- /dev/null +++ b/python/completers/general/filename_completer.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Stanislav Golovanov +# +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +from completers.completer import GeneralCompleter, CompletionsCache +import vimsupport +import ycm_core +import vim +import os +import re + + +class FilenameCompleter( GeneralCompleter ): + """ + General completer that provides filename and filepath completions. + + It maintains a cache of completions which is invalidated on each '/' symbol. + """ + def __init__(self): + super( FilenameCompleter, self ).__init__() + self._candidates = [] + self._query = None + self._should_use = False + + # TODO look into vim-style path globbing, NCC has a nice implementation + self._path_regex = re.compile( """(?:[A-z]+:/|[/~]|\./|\.+/)+ # 1 or more 'D:/'-like token or '/' or '~' or './' or '../' + (?:[ /a-zA-Z0-9()$+_~.\x80-\xff-\[\]]| # any alphanumeric symbal and space literal + [^\x20-\x7E]| # skip any special symbols + \\.)* # backslash and 1 char after it. + matches 1 or more of whole group + """, re.X ) + + + def ShouldUseNowInner( self, start_column ): + token = vim.current.line[ start_column - 1 ] + if token == '/' or self._should_use: + self._should_use = True + return True + else: + return False + + + def CandidatesForQueryAsyncInner( self, query, start_column ): + self._candidates = [] + self._query = query + self._completions_ready = False + self.line = str( vim.current.line.strip() ) + self.SetCandidates() + + + def AsyncCandidateRequestReadyInner( self ): + return self._completions_ready + + + def OnInsertLeave( self ): + # TODO this a hackish way to keep results when typing 2-3rd char after slash + # because identifier completer will kick in and replace results for 1 char. + # Need to do something better + self._should_use = False + + + def CandidatesFromStoredRequestInner( self ): + return self._candidates + + + def SetCandidates( self ): + path = self._path_regex.search( self.line ) + self._working_dir = os.path.expanduser( path.group() ) if path else '' + + try: + paths = os.listdir( self._working_dir ) + except: + paths = [] + + self._candidates = [ {'word': path, + 'dup': 1, + 'menu': '[Dir]' if os.path.isdir( self._working_dir + \ + '/' + path ) else '[File]' + } for path in paths ] + + self._completions_ready = True diff --git a/python/completers/general/general_completer.py b/python/completers/general/general_completer.py index 25a0c243..d625c8fe 100644 --- a/python/completers/general/general_completer.py +++ b/python/completers/general/general_completer.py @@ -19,7 +19,7 @@ from completers.completer import Completer from completers.all.identifier_completer import IdentifierCompleter -from threading import Thread, Event +from threading import Thread import vimsupport import inspect import fnmatch @@ -58,11 +58,10 @@ class GeneralCompleterStore( Completer ): def InitCompleters( self ): # This method creates objects of main completers class. completers = [] - modules = [ module for module in os.listdir( '.' ) + modules = [ module for module in os.listdir( os.path.dirname(__file__) ) if fnmatch.fnmatch(module, '*.py') and not 'general_completer' in module - and not '__init__' in module - and not 'hook' in module ] + and not '__init__' in module ] for module in modules: @@ -79,20 +78,21 @@ class GeneralCompleterStore( Completer ): for _, ClassObject in inspect.getmembers( module, inspect.isclass ): # Iterate over all classes in a module and select main class - if hasattr( ClassObject, 'CandidatesForQueryAsyncInner' ): + if not __name__ in str(ClassObject) and 'general' in str(ClassObject): + classInstance = ClassObject - # Init selected class and store class object - completers.append( GeneralCompleterInstance( classInstance() ) ) - # append IdentifierCompleter - completers.append( GeneralCompleterInstance( IdentifierCompleter() ) ) + # Init selected class and store class object + completers.append( classInstance() ) + + completers.append( IdentifierCompleter() ) + return completers def SupportedFiletypes( self ): - # magic token meaning all filetypes - return set( [ 'ycm_all' ] ) + return set() def ShouldUseNow( self, start_column ): @@ -100,9 +100,9 @@ class GeneralCompleterStore( Completer ): # True. Also update flags in completers classes flag = False for completer in self.completers: - ShouldUse = completer.completer.ShouldUseNow( start_column ) - completer.ShouldUse = ShouldUse - if ShouldUse: + _should_use = completer.ShouldUseNow( start_column ) + completer._should_use = _should_use + if _should_use: flag = True return flag @@ -114,23 +114,23 @@ class GeneralCompleterStore( Completer ): # if completer should be used start thread by setting Event flag for completer in self.completers: - completer.finished.clear() - if completer.ShouldUse and not completer.should_start.is_set(): - completer.should_start.set() + completer._finished.clear() + if completer._should_use and not completer._should_start.is_set(): + completer._should_start.set() def AsyncCandidateRequestReady( self ): # Return True when all completers that should be used are finished their work. for completer in self.completers: - if not completer.finished.is_set() and completer.ShouldUse: + if not completer._finished.is_set() and completer._should_use: return False return True def CandidatesFromStoredRequest( self ): for completer in self.completers: - if completer.ShouldUse and completer.finished.is_set(): - self._candidates += completer.results.pop() + if completer._should_use and completer._finished.is_set(): + self._candidates += completer._results.pop() return self._candidates @@ -139,17 +139,17 @@ class GeneralCompleterStore( Completer ): while True: # sleep until ShouldUseNow returns True - WaitAndClear( completer.should_start ) + WaitAndClear( completer._should_start ) - completer.completer.CandidatesForQueryAsync( self.query, + completer.CandidatesForQueryAsync( self.query, self.completion_start_column ) - while not completer.completer.AsyncCandidateRequestReady(): + while not completer.AsyncCandidateRequestReady(): continue - completer.results.append( completer.completer.CandidatesFromStoredRequest() ) + completer._results.append( completer.CandidatesFromStoredRequest() ) - completer.finished.set() + completer._finished.set() def StartThreads( self ): @@ -161,50 +161,38 @@ class GeneralCompleterStore( Completer ): # Process all parsing methods of completers. Needed by identifier completer for completer in self.completers: # clear all stored completion results - completer.results = [] - completer.completer.OnFileReadyToParse() + completer._results = [] + completer.OnFileReadyToParse() def OnCursorMovedInsertMode( self ): for completer in self.completers: - completer.completer.OnCursorMovedInsertMode() + completer.OnCursorMovedInsertMode() def OnCursorMovedNormalMode( self ): for completer in self.completers: - completer.completer.OnCursorMovedNormalMode() + completer.OnCursorMovedNormalMode() def OnBufferVisit( self ): for completer in self.completers: - completer.completer.OnBufferVisit() + completer.OnBufferVisit() def OnBufferDelete( self, deleted_buffer_file ): for completer in self.completers: - completer.completer.OnBufferDelete( deleted_buffer_file ) + completer.OnBufferDelete( deleted_buffer_file ) def OnCursorHold( self ): for completer in self.completers: - completer.completer.OnCursorHold() + completer.OnCursorHold() def OnInsertLeave( self ): for completer in self.completers: - completer.completer.OnInsertLeave() - - -class GeneralCompleterInstance( object ): - """ - Class that holds all meta information about specific general completer - """ - def __init__( self, completer ): - self.completer = completer - self.should_start = Event() - self.ShouldUse = False - self.finished = Event() - self.results = [] + completer.OnInsertLeave() def WaitAndClear( event, timeout=None ): diff --git a/python/completers/general/hook.py b/python/completers/general/hook.py deleted file mode 100644 index e84cf950..00000000 --- a/python/completers/general/hook.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2013 Stanislav Golovanov -# -# This file is part of YouCompleteMe. -# -# YouCompleteMe is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# YouCompleteMe is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with YouCompleteMe. If not, see . - -from general_completer import GeneralCompleterStore - -def GetCompleter(): - return GeneralCompleterStore() diff --git a/python/completers/general/ultisnips_completer.py b/python/completers/general/ultisnips_completer.py index 367e4b08..1c31f7de 100644 --- a/python/completers/general/ultisnips_completer.py +++ b/python/completers/general/ultisnips_completer.py @@ -17,14 +17,12 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -from completers.completer import Completer, CompletionsCache +from completers.completer import GeneralCompleter, CompletionsCache from UltiSnips import UltiSnips_Manager import vimsupport -MIN_NUM_CHARS = int( vimsupport.GetVariableValue( - "g:ycm_min_num_of_chars_for_completion" ) ) -class UltiSnipsCompleter( Completer ): +class UltiSnipsCompleter( GeneralCompleter ): """ General completer that provides UltiSnips snippet names in completions. @@ -47,15 +45,11 @@ class UltiSnipsCompleter( Completer ): def ShouldUseNowInner( self, start_column ): - query_length = vimsupport.CurrentColumn() - start_column - return query_length >= MIN_NUM_CHARS - - - def SupportedFiletypes( self ): - # magic token meaning all filetypes - return set( [ 'ycm_all' ] ) + return self.QueryLengthAboveMinThreshold( start_column ) + # We need to override this because Completer version invalidates cache on + # empty query and we want to invalidate cache only on buffer switch. def CandidatesForQueryAsync( self, query, start_column ): self.completion_start_column = start_column @@ -77,27 +71,17 @@ class UltiSnipsCompleter( Completer ): return self.flag - # We need to override this because we need to store all snippets but return - # filtered results on first call. - def CandidatesFromStoredRequest( self ): - if self.completions_cache: - return self.completions_cache.filtered_completions - else: - self.completions_cache = CompletionsCache() - self.completions_cache.raw_completions = self.CandidatesFromStoredRequestInner() - self.completions_cache.line, _ = vimsupport.CurrentLineAndColumn() - self.completions_cache.column = self.completion_start_column - return self.FilterAndSortCandidates( self._candidates, self._query ) - - def CandidatesFromStoredRequestInner( self ): return self._candidates def SetCandidates( self ): try: - # get all snippets for filetype rawsnips = UltiSnips_Manager._snips( '', 1 ) + + # UltiSnips_Manager._snips() returns a class instance where: + # class.trigger - name of snippet trigger word ( e.g. defn or testcase ) + # class.description - description of the snippet self._candidates = [ { 'word': str( snip.trigger ), 'menu': str( ' ' + snip.description ) } for snip in rawsnips ] @@ -108,5 +92,6 @@ class UltiSnipsCompleter( Completer ): def OnFileReadyToParse( self ): # Invalidate cache on buffer switch - self.completions_cache = None + self.completions_cache = CompletionsCache() self.SetCandidates() + self.completions_cache.raw_completions = self._candidates