Rewriting the code from the pull request

This implements the filename completer and introduces integration with
UltiSnips. The user will now see snippets in the completion menu. After
selecting a snippet, the user should invoke the UltiSnips trigger key
(which should be changed from the default of TAB) to trigger the snippet
expansion.

Fixes #77, Fixes #36
This commit is contained in:
Strahinja Val Markovic 2013-04-22 21:23:49 -07:00
parent bb5839dd74
commit aa9127e3dc
5 changed files with 120 additions and 240 deletions

View File

@ -24,6 +24,8 @@ import ycm_core
import ycm_utils as utils import ycm_utils as utils
MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10 MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10
MIN_NUM_CHARS = int( vimsupport.GetVariableValue(
"g:ycm_min_num_of_chars_for_completion" ) )
class IdentifierCompleter( GeneralCompleter ): class IdentifierCompleter( GeneralCompleter ):

View File

@ -22,7 +22,6 @@ import vim
import vimsupport import vimsupport
import ycm_core import ycm_core
from collections import defaultdict from collections import defaultdict
from threading import Event
NO_USER_COMMANDS = 'This completer does not define any commands.' NO_USER_COMMANDS = 'This completer does not define any commands.'
@ -161,7 +160,6 @@ class Completer( object ):
query_length = vimsupport.CurrentColumn() - start_column query_length = vimsupport.CurrentColumn() - start_column
return query_length >= MIN_NUM_CHARS return query_length >= MIN_NUM_CHARS
# It's highly likely you DON'T want to override this function but the *Inner # It's highly likely you DON'T want to override this function but the *Inner
# version of it. # version of it.
def CandidatesForQueryAsync( self, query, start_column ): def CandidatesForQueryAsync( self, query, start_column ):
@ -186,13 +184,10 @@ class Completer( object ):
candidates = candidates.words candidates = candidates.words
items_are_objects = 'word' in candidates[ 0 ] items_are_objects = 'word' in candidates[ 0 ]
try: matches = ycm_core.FilterAndSortCandidates(
matches = ycm_core.FilterAndSortCandidates( candidates,
candidates, 'word' if items_are_objects else '',
'word' if items_are_objects else '', query )
query )
except:
matches = []
return matches return matches
@ -310,23 +305,16 @@ class Completer( object ):
class GeneralCompleter( Completer ): class GeneralCompleter( Completer ):
""" """
A base class for General completers in YCM. A base class for General completers in YCM. A general completer is used in all
filetypes.
Because this is a subclass of Completer class, you should refer to the Because this is a subclass of Completer class, you should refer to the
dpcumentation of Completer API. Completer class documentation. Do NOT use this class for semantic completers!
Subclass Completer directly.
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 ): def __init__( self ):
super( GeneralCompleter, self ).__init__() super( GeneralCompleter, self ).__init__()
self._should_start = Event()
self._should_use = False
self._finished = Event()
self._results = []
def SupportedFiletypes( self ): def SupportedFiletypes( self ):

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com> # Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com>
# # Strahinja Val Markovic <val@markovic.io>
# #
# YouCompleteMe is free software: you can redistribute it and/or modify # YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -16,9 +16,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from completers.completer import GeneralCompleter, CompletionsCache from completers.completer import GeneralCompleter
import vimsupport
import ycm_core
import vim import vim
import os import os
import re import re
@ -27,68 +25,60 @@ import re
class FilenameCompleter( GeneralCompleter ): class FilenameCompleter( GeneralCompleter ):
""" """
General completer that provides filename and filepath completions. General completer that provides filename and filepath completions.
It maintains a cache of completions which is invalidated on each '/' symbol.
""" """
def __init__(self): def __init__(self):
super( FilenameCompleter, self ).__init__() super( FilenameCompleter, self ).__init__()
self._candidates = [] 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("""
self._path_regex = re.compile( """(?:[A-z]+:/|[/~]|\./|\.+/)+ # 1 or more 'D:/'-like token or '/' or '~' or './' or '../' # 1 or more 'D:/'-like token or '/' or '~' or './' or '../'
(?:[ /a-zA-Z0-9()$+_~.\x80-\xff-\[\]]| # any alphanumeric symbal and space literal (?:[A-z]+:/|[/~]|\./|\.+/)+
[^\x20-\x7E]| # skip any special symbols
\\.)* # backslash and 1 char after it. + matches 1 or more of whole group # any alphanumeric symbal and space literal
""", re.X ) (?:[ /a-zA-Z0-9()$+_~.\x80-\xff-\[\]]|
# skip any special symbols
[^\x20-\x7E]|
# backslash and 1 char after it. + matches 1 or more of whole group
\\.)*$
""", re.X )
def ShouldUseNowInner( self, start_column ): def ShouldUseNowInner( self, start_column ):
token = vim.current.line[ start_column - 1 ] return 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 ): def CandidatesForQueryAsyncInner( self, query, start_column ):
self._candidates = [] self._candidates = []
self._query = query self.ComputePaths( start_column )
self._completions_ready = False
self.line = str( vim.current.line.strip() )
self.SetCandidates()
def AsyncCandidateRequestReadyInner( self ): def AsyncCandidateRequestReadyInner( self ):
return self._completions_ready return True
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 ): def CandidatesFromStoredRequestInner( self ):
return self._candidates return self._candidates
def SetCandidates( self ): def ComputePaths( self, start_column ):
path = self._path_regex.search( self.line ) def GenerateCandidateForPath( path, path_dir ):
self._working_dir = os.path.expanduser( path.group() ) if path else '' is_dir = os.path.isdir( os.path.join( path_dir, path ) )
return { 'word': path,
'dup': 1,
'menu': '[Dir]' if is_dir else '[File]' }
line = vim.current.line[ :start_column ]
match = self._path_regex.search( line )
path_dir = os.path.expanduser( match.group() ) if match else ''
try: try:
paths = os.listdir( self._working_dir ) paths = os.listdir( path_dir )
except: except:
paths = [] paths = []
self._candidates = [ {'word': path, self._candidates = [ GenerateCandidateForPath( path, path_dir ) for path
'dup': 1, in paths ]
'menu': '[Dir]' if os.path.isdir( self._working_dir + \
'/' + path ) else '[File]'
} for path in paths ]
self._completions_ready = True self._completions_ready = True

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com> # Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com>
# Strahinja Val Markovic <val@markovic.io>
# #
# This file is part of YouCompleteMe. # This file is part of YouCompleteMe.
# #
@ -19,76 +20,38 @@
from completers.completer import Completer from completers.completer import Completer
from completers.all.identifier_completer import IdentifierCompleter from completers.all.identifier_completer import IdentifierCompleter
from threading import Thread from filename_completer import FilenameCompleter
import vimsupport
import inspect try:
import fnmatch from ultisnips_completer import UltiSnipsCompleter
import os USE_ULTISNIPS_COMPLETER = True
except ImportError:
USE_ULTISNIPS_COMPLETER = False
class GeneralCompleterStore( Completer ): class GeneralCompleterStore( Completer ):
""" """
Main class that holds a list of completers that can be used in all filetypes. Holds a list of completers that can be used in all filetypes.
This class creates a single GeneralCompleterInstance() class instance
for each general completer and makes a separate thread for each completer.
It overrides all Competer API methods so that specific calls to It overrides all Competer API methods so that specific calls to
GeneralCompleterStore are passed to all general completers. GeneralCompleterStore are passed to all general completers.
This class doesnt maintain a cache because it will make a problems for
some completers like identifier completer. Caching is done in a general
completers itself.
""" """
def __init__( self ): def __init__( self ):
super( GeneralCompleterStore, self ).__init__() super( GeneralCompleterStore, self ).__init__()
self.completers = self.InitCompleters() self._identifier_completer = IdentifierCompleter()
self.query = None self._filename_completer = FilenameCompleter()
self._candidates = [] self._ultisnips_completer = ( UltiSnipsCompleter()
self.threads = [] if USE_ULTISNIPS_COMPLETER else None )
self.StartThreads() self._non_filename_completers = filter( lambda x: x,
[ self._ultisnips_completer,
self._identifier_completer ] )
def _start_completion_thread( self, completer ): self._all_completers = filter( lambda x: x,
thread = Thread( target=self.SetCandidates, args=(completer,) ) [ self._identifier_completer,
thread.daemon = True self._filename_completer,
thread.start() self._ultisnips_completer ] )
self.threads.append( thread ) self._current_query_completers = []
def InitCompleters( self ):
# This method creates objects of main completers class.
completers = []
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 ]
for module in modules:
# We need to specify full path to the module
fullpath = 'completers.general.' + module[:-3]
try:
module = __import__( fullpath, fromlist=[''] )
except ImportError as error:
vimsupport.PostVimMessage( 'Import of general completer "{0}" has '
'failed, skipping. Full error: {1}'.format(
module, str( error ) ) )
continue
for _, ClassObject in inspect.getmembers( module, inspect.isclass ):
# Iterate over all classes in a module and select main class
if not __name__ in str(ClassObject) and 'general' in str(ClassObject):
classInstance = ClassObject
# Init selected class and store class object
completers.append( classInstance() )
completers.append( IdentifierCompleter() )
return completers
def SupportedFiletypes( self ): def SupportedFiletypes( self ):
@ -96,107 +59,73 @@ class GeneralCompleterStore( Completer ):
def ShouldUseNow( self, start_column ): def ShouldUseNow( self, start_column ):
# Query all completers and set flag to True if any of completers returns self._current_query_completers = []
# True. Also update flags in completers classes
flag = False
for completer in self.completers:
_should_use = completer.ShouldUseNow( start_column )
completer._should_use = _should_use
if _should_use:
flag = True
return flag if self._filename_completer.ShouldUseNow( start_column ):
self._current_query_completers = [ self._filename_completer ]
return True
should_use_now = False
for completer in self._non_filename_completers:
should_use_this_completer = completer.ShouldUseNow( start_column )
should_use_now = should_use_now or should_use_this_completer
if should_use_this_completer:
self._current_query_completers.append( completer )
return should_use_now
def CandidatesForQueryAsync( self, query, start_column ): def CandidatesForQueryAsync( self, query, start_column ):
self.query = query for completer in self._current_query_completers:
self._candidates = [] completer.CandidatesForQueryAsync( query, start_column )
# if completer should be used start thread by setting Event flag
for completer in self.completers:
completer._finished.clear()
if completer._should_use and not completer._should_start.is_set():
completer._should_start.set()
def AsyncCandidateRequestReady( self ): def AsyncCandidateRequestReady( self ):
# Return True when all completers that should be used are finished their work. return all( x.AsyncCandidateRequestReady() for x in
for completer in self.completers: self._current_query_completers )
if not completer._finished.is_set() and completer._should_use:
return False
return True
def CandidatesFromStoredRequest( self ): def CandidatesFromStoredRequest( self ):
for completer in self.completers: candidates = []
if completer._should_use and completer._finished.is_set(): for completer in self._current_query_completers:
self._candidates += completer._results.pop() candidates += completer.CandidatesFromStoredRequest()
return self._candidates return candidates
def SetCandidates( self, completer ):
while True:
# sleep until ShouldUseNow returns True
WaitAndClear( completer._should_start )
completer.CandidatesForQueryAsync( self.query,
self.completion_start_column )
while not completer.AsyncCandidateRequestReady():
continue
completer._results.append( completer.CandidatesFromStoredRequest() )
completer._finished.set()
def StartThreads( self ):
for completer in self.completers:
self._start_completion_thread( completer )
def OnFileReadyToParse( self ): def OnFileReadyToParse( self ):
# Process all parsing methods of completers. Needed by identifier completer for completer in self._all_completers:
for completer in self.completers:
# clear all stored completion results
completer._results = []
completer.OnFileReadyToParse() completer.OnFileReadyToParse()
def OnCursorMovedInsertMode( self ): def OnCursorMovedInsertMode( self ):
for completer in self.completers: for completer in self._all_completers:
completer.OnCursorMovedInsertMode() completer.OnCursorMovedInsertMode()
def OnCursorMovedNormalMode( self ): def OnCursorMovedNormalMode( self ):
for completer in self.completers: for completer in self._all_completers:
completer.OnCursorMovedNormalMode() completer.OnCursorMovedNormalMode()
def OnBufferVisit( self ): def OnBufferVisit( self ):
for completer in self.completers: for completer in self._all_completers:
completer.OnBufferVisit() completer.OnBufferVisit()
def OnBufferDelete( self, deleted_buffer_file ): def OnBufferDelete( self, deleted_buffer_file ):
for completer in self.completers: for completer in self._all_completers:
completer.OnBufferDelete( deleted_buffer_file ) completer.OnBufferDelete( deleted_buffer_file )
def OnCursorHold( self ): def OnCursorHold( self ):
for completer in self.completers: for completer in self._all_completers:
completer.OnCursorHold() completer.OnCursorHold()
def OnInsertLeave( self ): def OnInsertLeave( self ):
for completer in self.completers: for completer in self._all_completers:
completer.OnInsertLeave() completer.OnInsertLeave()
def WaitAndClear( event, timeout=None ):
flag_is_set = event.wait( timeout )
if flag_is_set:
event.clear()
return flag_is_set

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com> # Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com>
# Strahinja Val Markovic <val@markovic.io>
# #
# This file is part of YouCompleteMe. # This file is part of YouCompleteMe.
# #
@ -17,81 +18,51 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from completers.completer import GeneralCompleter, CompletionsCache from completers.completer import GeneralCompleter
from UltiSnips import UltiSnips_Manager from UltiSnips import UltiSnips_Manager
import vimsupport
class UltiSnipsCompleter( GeneralCompleter ): class UltiSnipsCompleter( GeneralCompleter ):
""" """
General completer that provides UltiSnips snippet names in completions. General completer that provides UltiSnips snippet names in completions.
This completer makes a cache of all snippets for filetype because each
call to _snips() is quite long and it is much faster to cache all snippets
once and then filter them. Cache is invalidated on buffer switching.
""" """
def __init__( self ): def __init__( self ):
super( UltiSnipsCompleter, self ).__init__() super( UltiSnipsCompleter, self ).__init__()
self._candidates = None self._candidates = None
self._filtered_candidates = None
def ShouldUseNow( self, start_column ):
inner_says_yes = self.ShouldUseNowInner( start_column )
previous_results_were_empty = ( self.completions_cache and
not self.completions_cache.raw_completions )
return inner_says_yes and not previous_results_were_empty
def ShouldUseNowInner( self, start_column ): def ShouldUseNowInner( self, start_column ):
return self.QueryLengthAboveMinThreshold( start_column ) return self.QueryLengthAboveMinThreshold( start_column )
# We need to override this because Completer version invalidates cache on def CandidatesForQueryAsync( self, query, unused_start_column ):
# empty query and we want to invalidate cache only on buffer switch. self._filtered_candidates = self.FilterAndSortCandidates( self._candidates,
def CandidatesForQueryAsync( self, query, start_column ): query )
self.completion_start_column = start_column
if self.completions_cache:
self.completions_cache.filtered_completions = (
self.FilterAndSortCandidates(
self.completions_cache.raw_completions,
query ) )
else:
self.completions_cache = None
self.CandidatesForQueryAsyncInner( query, start_column )
def CandidatesForQueryAsyncInner( self, query, start_column ): def AsyncCandidateRequestReady( self ):
self._query = query return bool( self._candidates )
def AsyncCandidateRequestReadyInner( self ): def CandidatesFromStoredRequest( self ):
return self.flag return self._filtered_candidates
def CandidatesFromStoredRequestInner( self ):
return self._candidates
def SetCandidates( self ):
try:
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> ' + snip.description ) }
for snip in rawsnips ]
except:
self._candidates = []
self.flag = True
def OnFileReadyToParse( self ): def OnFileReadyToParse( self ):
# Invalidate cache on buffer switch self._candidates = _GetCandidates()
self.completions_cache = CompletionsCache()
self.SetCandidates()
self.completions_cache.raw_completions = self._candidates def _GetCandidates():
try:
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
return [ { 'word': str( snip.trigger ),
'menu': str( '<snip> ' + snip.description ) }
for snip in rawsnips ]
except:
return []