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
MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10
MIN_NUM_CHARS = int( vimsupport.GetVariableValue(
"g:ycm_min_num_of_chars_for_completion" ) )
class IdentifierCompleter( GeneralCompleter ):

View File

@ -22,7 +22,6 @@ import vim
import vimsupport
import ycm_core
from collections import defaultdict
from threading import Event
NO_USER_COMMANDS = 'This completer does not define any commands.'
@ -161,7 +160,6 @@ class Completer( object ):
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 ):
@ -186,13 +184,10 @@ class Completer( object ):
candidates = candidates.words
items_are_objects = 'word' in candidates[ 0 ]
try:
matches = ycm_core.FilterAndSortCandidates(
candidates,
'word' if items_are_objects else '',
query )
except:
matches = []
return matches
@ -310,23 +305,16 @@ class Completer( object ):
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
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 ):
super( GeneralCompleter, self ).__init__()
self._should_start = Event()
self._should_use = False
self._finished = Event()
self._results = []
def SupportedFiletypes( self ):

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# 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
# 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
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from completers.completer import GeneralCompleter, CompletionsCache
import vimsupport
import ycm_core
from completers.completer import GeneralCompleter
import vim
import os
import re
@ -27,68 +25,60 @@ 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
self._path_regex = re.compile("""
# 1 or more 'D:/'-like token or '/' or '~' or './' or '../'
(?:[A-z]+:/|[/~]|\./|\.+/)+
# any alphanumeric symbal and space literal
(?:[ /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 ):
token = vim.current.line[ start_column - 1 ]
if token == '/' or self._should_use:
self._should_use = True
return True
else:
return False
return vim.current.line[ start_column - 1 ] == '/'
def CandidatesForQueryAsyncInner( self, query, start_column ):
self._candidates = []
self._query = query
self._completions_ready = False
self.line = str( vim.current.line.strip() )
self.SetCandidates()
self.ComputePaths( start_column )
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
return True
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 ''
def ComputePaths( self, start_column ):
def GenerateCandidateForPath( path, path_dir ):
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:
paths = os.listdir( self._working_dir )
paths = os.listdir( path_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._candidates = [ GenerateCandidateForPath( path, path_dir ) for path
in paths ]
self._completions_ready = True

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
#
# Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com>
# Strahinja Val Markovic <val@markovic.io>
#
# This file is part of YouCompleteMe.
#
@ -19,76 +20,38 @@
from completers.completer import Completer
from completers.all.identifier_completer import IdentifierCompleter
from threading import Thread
import vimsupport
import inspect
import fnmatch
import os
from filename_completer import FilenameCompleter
try:
from ultisnips_completer import UltiSnipsCompleter
USE_ULTISNIPS_COMPLETER = True
except ImportError:
USE_ULTISNIPS_COMPLETER = False
class GeneralCompleterStore( Completer ):
"""
Main class that 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.
Holds a list of completers that can be used in all filetypes.
It overrides all Competer API methods so that specific calls to
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 ):
super( GeneralCompleterStore, self ).__init__()
self.completers = self.InitCompleters()
self.query = None
self._candidates = []
self.threads = []
self.StartThreads()
def _start_completion_thread( self, completer ):
thread = Thread( target=self.SetCandidates, args=(completer,) )
thread.daemon = True
thread.start()
self.threads.append( thread )
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
self._identifier_completer = IdentifierCompleter()
self._filename_completer = FilenameCompleter()
self._ultisnips_completer = ( UltiSnipsCompleter()
if USE_ULTISNIPS_COMPLETER else None )
self._non_filename_completers = filter( lambda x: x,
[ self._ultisnips_completer,
self._identifier_completer ] )
self._all_completers = filter( lambda x: x,
[ self._identifier_completer,
self._filename_completer,
self._ultisnips_completer ] )
self._current_query_completers = []
def SupportedFiletypes( self ):
@ -96,107 +59,73 @@ class GeneralCompleterStore( Completer ):
def ShouldUseNow( self, start_column ):
# Query all completers and set flag to True if any of completers returns
# 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
self._current_query_completers = []
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 ):
self.query = query
self._candidates = []
# 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()
for completer in self._current_query_completers:
completer.CandidatesForQueryAsync( query, start_column )
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._should_use:
return False
return True
return all( x.AsyncCandidateRequestReady() for x in
self._current_query_completers )
def CandidatesFromStoredRequest( self ):
for completer in self.completers:
if completer._should_use and completer._finished.is_set():
self._candidates += completer._results.pop()
candidates = []
for completer in self._current_query_completers:
candidates += completer.CandidatesFromStoredRequest()
return self._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 )
return candidates
def OnFileReadyToParse( self ):
# Process all parsing methods of completers. Needed by identifier completer
for completer in self.completers:
# clear all stored completion results
completer._results = []
for completer in self._all_completers:
completer.OnFileReadyToParse()
def OnCursorMovedInsertMode( self ):
for completer in self.completers:
for completer in self._all_completers:
completer.OnCursorMovedInsertMode()
def OnCursorMovedNormalMode( self ):
for completer in self.completers:
for completer in self._all_completers:
completer.OnCursorMovedNormalMode()
def OnBufferVisit( self ):
for completer in self.completers:
for completer in self._all_completers:
completer.OnBufferVisit()
def OnBufferDelete( self, deleted_buffer_file ):
for completer in self.completers:
for completer in self._all_completers:
completer.OnBufferDelete( deleted_buffer_file )
def OnCursorHold( self ):
for completer in self.completers:
for completer in self._all_completers:
completer.OnCursorHold()
def OnInsertLeave( self ):
for completer in self.completers:
for completer in self._all_completers:
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
#
# Copyright (C) 2013 Stanislav Golovanov <stgolovanov@gmail.com>
# Strahinja Val Markovic <val@markovic.io>
#
# This file is part of YouCompleteMe.
#
@ -17,81 +18,51 @@
# You should have received a copy of the GNU General Public License
# 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
import vimsupport
class UltiSnipsCompleter( GeneralCompleter ):
"""
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 ):
super( UltiSnipsCompleter, self ).__init__()
self._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
self._filtered_candidates = None
def ShouldUseNowInner( self, start_column ):
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
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 CandidatesForQueryAsync( self, query, unused_start_column ):
self._filtered_candidates = self.FilterAndSortCandidates( self._candidates,
query )
def CandidatesForQueryAsyncInner( self, query, start_column ):
self._query = query
def AsyncCandidateRequestReady( self ):
return bool( self._candidates )
def AsyncCandidateRequestReadyInner( self ):
return self.flag
def CandidatesFromStoredRequest( self ):
return self._filtered_candidates
def CandidatesFromStoredRequestInner( self ):
return self._candidates
def OnFileReadyToParse( self ):
self._candidates = _GetCandidates()
def SetCandidates( self ):
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
self._candidates = [ { 'word': str( snip.trigger ),
return [ { 'word': str( snip.trigger ),
'menu': str( '<snip> ' + snip.description ) }
for snip in rawsnips ]
except:
self._candidates = []
self.flag = True
def OnFileReadyToParse( self ):
# Invalidate cache on buffer switch
self.completions_cache = CompletionsCache()
self.SetCandidates()
self.completions_cache.raw_completions = self._candidates
return []