diff --git a/README.md b/README.md index e93696ac..a39df03e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ YCM also provides semantic go-to-definition/declaration commands for C-family languages. Expect more IDE features powered by the various YCM semantic engines in the future. +You'll also find that YCM has filepath completers (try typing `./` in a file) +and a completer that integrates with [UltiSnips][]. + + Mac OS X super-quick installation --------------------------------- @@ -861,6 +865,17 @@ Default: `[]` let g:ycm_extra_conf_globlist = [] +### The `g:ycm_filepath_completion_use_working_dir` option + +By default, YCM's filepath completion will interpret relative paths like `../` +as being relative to the folder of the file of the currently active buffer. +Setting this option will force YCM to always interpret relative paths as being +relative to Vim's current working directory. + +Default: `0` + + let g:ycm_filepath_completion_use_working_dir = 0 + ### The `g:ycm_semantic_triggers` option This option controls the character-based triggers for the various semantic @@ -1146,3 +1161,4 @@ This software is licensed under the [GPL v3 license][gpl]. [win-wiki]: https://github.com/Valloric/YouCompleteMe/wiki/Windows-Installation-Guide [eclim]: http://eclim.org/ [jedi]: https://github.com/davidhalter/jedi +[ultisnips]: https://github.com/SirVer/ultisnips/blob/master/doc/UltiSnips.txt diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index a0e6955d..68e35d3b 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -438,7 +438,7 @@ function! s:CompletionsForQuery( query, use_filetype_completer, if a:use_filetype_completer py completer = ycm_state.GetFiletypeCompleter() else - py completer = ycm_state.GetIdentifierCompleter() + py completer = ycm_state.GetGeneralCompleter() endif py completer.CandidatesForQueryAsync( vim.eval( 'a:query' ), @@ -487,7 +487,7 @@ function! youcompleteme#Complete( findstart, base ) \ s:completion_start_column . ')' ) if !s:should_use_filetype_completion && - \ !pyeval( 'ycm_state.ShouldUseIdentifierCompleter(' . + \ !pyeval( 'ycm_state.ShouldUseGeneralCompleter(' . \ s:completion_start_column . ')' ) " for vim, -2 means not found but don't trigger an error message " see :h complete-functions @@ -551,7 +551,7 @@ function! s:CompleterCommand(...) if a:1 == 'ft=ycm:omni' py completer = ycm_state.GetOmniCompleter() elseif a:1 == 'ft=ycm:ident' - py completer = ycm_state.GetIdentifierCompleter() + py completer = ycm_state.GetGeneralCompleter() else py completer = ycm_state.GetFiletypeCompleterForFiletype( \ vim.eval('a:1').lstrip('ft=') ) diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index 134708cc..196f1271 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -124,6 +124,9 @@ let g:ycm_confirm_extra_conf = let g:ycm_extra_conf_globlist = \ get( g:, 'ycm_extra_conf_globlist', [] ) +let g:ycm_filepath_completion_use_working_dir = + \ get( g:, 'ycm_filepath_completion_use_working_dir', 0 ) + let g:ycm_semantic_triggers = \ get( g:, 'ycm_semantic_triggers', { \ 'c' : ['->', '.'], diff --git a/python/completers/all/identifier_completer.py b/python/completers/all/identifier_completer.py index fcd3f07f..858fd34e 100644 --- a/python/completers/all/identifier_completer.py +++ b/python/completers/all/identifier_completer.py @@ -17,7 +17,7 @@ # 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.general_completer import GeneralCompleter import vim import vimsupport import ycm_core @@ -28,21 +28,15 @@ 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 d991e774..7ebffb24 100644 --- a/python/completers/completer.py +++ b/python/completers/completer.py @@ -25,6 +25,9 @@ from collections import defaultdict 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. @@ -153,6 +156,10 @@ 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 ): @@ -177,11 +184,13 @@ class Completer( object ): candidates = candidates.words items_are_objects = 'word' in candidates[ 0 ] - return ycm_core.FilterAndSortCandidates( + matches = ycm_core.FilterAndSortCandidates( candidates, 'word' if items_are_objects else '', query ) + return matches + def CandidatesForQueryAsyncInner( self, query, start_column ): pass diff --git a/python/completers/general/__init__.py b/python/completers/general/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/completers/general/filename_completer.py b/python/completers/general/filename_completer.py new file mode 100644 index 00000000..f3b1a1c4 --- /dev/null +++ b/python/completers/general/filename_completer.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Stanislav Golovanov +# Strahinja Val Markovic +# +# 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.threaded_completer import ThreadedCompleter +import vim +import vimsupport +import os +import re + +USE_WORKING_DIR = vimsupport.GetBoolValue( + 'g:ycm_filepath_completion_use_working_dir' ) + +class FilenameCompleter( ThreadedCompleter ): + """ + General completer that provides filename and filepath completions. + """ + + def __init__(self): + super( FilenameCompleter, self ).__init__() + + 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 ): + return vim.current.line[ start_column - 1 ] == '/' + + + def SupportedFiletypes( self ): + return [] + + + def ComputeCandidates( self, unused_query, 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 '' + + if not USE_WORKING_DIR and not path_dir.startswith( '/' ): + path_dir = os.path.join( os.path.dirname( vim.current.buffer.name ), + path_dir ) + + try: + paths = os.listdir( path_dir ) + except: + paths = [] + + return [ GenerateCandidateForPath( path, path_dir ) for path in paths ] diff --git a/python/completers/general/general_completer_store.py b/python/completers/general/general_completer_store.py new file mode 100644 index 00000000..0817055d --- /dev/null +++ b/python/completers/general/general_completer_store.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Stanislav Golovanov +# Strahinja Val Markovic +# +# 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 completers.completer import Completer +from completers.all.identifier_completer import IdentifierCompleter +from filename_completer import FilenameCompleter + +try: + from ultisnips_completer import UltiSnipsCompleter + USE_ULTISNIPS_COMPLETER = True +except ImportError: + USE_ULTISNIPS_COMPLETER = False + + + +class GeneralCompleterStore( 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. + """ + + def __init__( self ): + super( GeneralCompleterStore, self ).__init__() + 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 ): + return set() + + + def ShouldUseNow( self, start_column ): + self._current_query_completers = [] + + 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 ): + for completer in self._current_query_completers: + completer.CandidatesForQueryAsync( query, start_column ) + + + def AsyncCandidateRequestReady( self ): + return all( x.AsyncCandidateRequestReady() for x in + self._current_query_completers ) + + + def CandidatesFromStoredRequest( self ): + candidates = [] + for completer in self._current_query_completers: + candidates += completer.CandidatesFromStoredRequest() + + return candidates + + + def OnFileReadyToParse( self ): + for completer in self._all_completers: + completer.OnFileReadyToParse() + + + def OnCursorMovedInsertMode( self ): + for completer in self._all_completers: + completer.OnCursorMovedInsertMode() + + + def OnCursorMovedNormalMode( self ): + for completer in self._all_completers: + completer.OnCursorMovedNormalMode() + + + def OnBufferVisit( self ): + for completer in self._all_completers: + completer.OnBufferVisit() + + + def OnBufferDelete( self, deleted_buffer_file ): + for completer in self._all_completers: + completer.OnBufferDelete( deleted_buffer_file ) + + + def OnCursorHold( self ): + for completer in self._all_completers: + completer.OnCursorHold() + + + def OnInsertLeave( self ): + for completer in self._all_completers: + completer.OnInsertLeave() + diff --git a/python/completers/general/ultisnips_completer.py b/python/completers/general/ultisnips_completer.py new file mode 100644 index 00000000..84192bdc --- /dev/null +++ b/python/completers/general/ultisnips_completer.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Stanislav Golovanov +# Strahinja Val Markovic +# +# 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 completers.general_completer import GeneralCompleter +from UltiSnips import UltiSnips_Manager + + +class UltiSnipsCompleter( GeneralCompleter ): + """ + General completer that provides UltiSnips snippet names in completions. + """ + + def __init__( self ): + super( UltiSnipsCompleter, self ).__init__() + self._candidates = None + self._filtered_candidates = None + + + def ShouldUseNowInner( self, start_column ): + return self.QueryLengthAboveMinThreshold( start_column ) + + + def CandidatesForQueryAsync( self, query, unused_start_column ): + self._filtered_candidates = self.FilterAndSortCandidates( self._candidates, + query ) + + + def AsyncCandidateRequestReady( self ): + return bool( self._candidates ) + + + def CandidatesFromStoredRequest( self ): + return self._filtered_candidates + + + def OnFileReadyToParse( self ): + self._candidates = _GetCandidates() + + +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.description ) } + for snip in rawsnips ] + except: + return [] diff --git a/python/completers/general_completer.py b/python/completers/general_completer.py new file mode 100644 index 00000000..ba8de867 --- /dev/null +++ b/python/completers/general_completer.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Strahinja Val Markovic +# +# 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 completer import Completer + +class GeneralCompleter( Completer ): + """ + 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 + Completer class documentation. Do NOT use this class for semantic completers! + Subclass Completer directly. + + """ + def __init__( self ): + super( GeneralCompleter, self ).__init__() + + + def SupportedFiletypes( self ): + return set() diff --git a/python/completers/python/jedi_completer.py b/python/completers/python/jedi_completer.py index be80268b..cec39408 100644 --- a/python/completers/python/jedi_completer.py +++ b/python/completers/python/jedi_completer.py @@ -19,8 +19,7 @@ # along with YouCompleteMe. If not, see . import vim -from threading import Thread, Event -from completers.completer import Completer +from completers.threaded_completer import ThreadedCompleter import vimsupport import sys @@ -39,7 +38,7 @@ except ImportError: sys.path.pop( 0 ) -class JediCompleter( Completer ): +class JediCompleter( ThreadedCompleter ): """ A Completer that uses the Jedi completion engine. https://jedi.readthedocs.org/en/latest/ @@ -47,16 +46,6 @@ class JediCompleter( Completer ): def __init__( self ): super( JediCompleter, self ).__init__() - self._query_ready = Event() - self._candidates_ready = Event() - self._candidates = None - self._start_completion_thread() - - - def _start_completion_thread( self ): - self._completion_thread = Thread( target=self.SetCandidates ) - self._completion_thread.daemon = True - self._completion_thread.start() def SupportedFiletypes( self ): @@ -64,47 +53,17 @@ class JediCompleter( Completer ): return [ 'python' ] - def CandidatesForQueryAsyncInner( self, unused_query, unused_start_column ): - self._candidates = None - self._candidates_ready.clear() - self._query_ready.set() + def ComputeCandidates( self, unused_query, unused_start_column ): + filename = vim.current.buffer.name + line, column = vimsupport.CurrentLineAndColumn() + # Jedi expects lines to start at 1, not 0 + line += 1 + contents = '\n'.join( vim.current.buffer ) + script = Script( contents, line, column, filename ) + + return [ { 'word': str( completion.word ), + 'menu': str( completion.description ), + 'info': str( completion.doc ) } + for completion in script.complete() ] - def AsyncCandidateRequestReadyInner( self ): - return WaitAndClear( self._candidates_ready, timeout=0.005 ) - - - def CandidatesFromStoredRequestInner( self ): - return self._candidates or [] - - - def SetCandidates( self ): - while True: - try: - WaitAndClear( self._query_ready ) - - filename = vim.current.buffer.name - line, column = vimsupport.CurrentLineAndColumn() - # Jedi expects lines to start at 1, not 0 - line += 1 - contents = '\n'.join( vim.current.buffer ) - script = Script( contents, line, column, filename ) - - self._candidates = [ { 'word': str( completion.word ), - 'menu': str( completion.description ), - 'info': str( completion.doc ) } - for completion in script.complete() ] - except: - self._query_ready.clear() - self._candidates = [] - self._candidates_ready.set() - - -def WaitAndClear( event, timeout=None ): - # We can't just do flag_is_set = event.wait( timeout ) because that breaks on - # Python 2.6 - event.wait( timeout ) - flag_is_set = event.is_set() - if flag_is_set: - event.clear() - return flag_is_set diff --git a/python/completers/threaded_completer.py b/python/completers/threaded_completer.py new file mode 100644 index 00000000..81d39ef7 --- /dev/null +++ b/python/completers/threaded_completer.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Strahinja Val Markovic +# +# 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 . + +import abc +from threading import Thread, Event +from completer import Completer + +class ThreadedCompleter( Completer ): + def __init__( self ): + super( ThreadedCompleter, self ).__init__() + self._query_ready = Event() + self._candidates_ready = Event() + self._candidates = None + self._start_completion_thread() + + + def _start_completion_thread( self ): + self._completion_thread = Thread( target=self.SetCandidates ) + self._completion_thread.daemon = True + self._completion_thread.start() + + + def CandidatesForQueryAsyncInner( self, query, start_column ): + self._candidates = None + self._candidates_ready.clear() + self._query = query + self._start_column = start_column + self._query_ready.set() + + + def AsyncCandidateRequestReadyInner( self ): + return WaitAndClearIfSet( self._candidates_ready, timeout=0.005 ) + + + def CandidatesFromStoredRequestInner( self ): + return self._candidates or [] + + + @abc.abstractmethod + def ComputeCandidates( self, query, start_column ): + pass + + + def SetCandidates( self ): + while True: + try: + WaitAndClearIfSet( self._query_ready ) + self._candidates = self.ComputeCandidates( self._query, + self._start_column ) + except: + self._query_ready.clear() + self._candidates = [] + self._candidates_ready.set() + + +def WaitAndClearIfSet( event, timeout=None ): + """Given an |event| and a |timeout|, waits for the event a maximum of timeout + seconds. After waiting, clears the event if it's set and returns the state of + the event before it was cleared.""" + + # We can't just do flag_is_set = event.wait( timeout ) because that breaks on + # Python 2.6 + event.wait( timeout ) + flag_is_set = event.is_set() + if flag_is_set: + event.clear() + return flag_is_set diff --git a/python/ycm.py b/python/ycm.py index f8c68056..7f65e2f2 100644 --- a/python/ycm.py +++ b/python/ycm.py @@ -33,9 +33,9 @@ except ImportError as e: 'the docs. Full error: {1}'.format( os.path.dirname( os.path.abspath( __file__ ) ), str( e ) ) ) -from completers.all.identifier_completer import IdentifierCompleter -from completers.all.omni_completer import OmniCompleter +from completers.all.omni_completer import OmniCompleter +from completers.general.general_completer_store import GeneralCompleterStore FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE = vim.eval( 'g:ycm_filetype_specific_completion_to_disable' ) @@ -43,13 +43,13 @@ FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE = vim.eval( class YouCompleteMe( object ): def __init__( self ): - self.identcomp = IdentifierCompleter() + self.gencomp = GeneralCompleterStore() self.omnicomp = OmniCompleter() self.filetype_completers = {} - def GetIdentifierCompleter( self ): - return self.identcomp + def GetGeneralCompleter( self ): + return self.gencomp def GetOmniCompleter( self ): @@ -101,8 +101,8 @@ class YouCompleteMe( object ): return completer - def ShouldUseIdentifierCompleter( self, start_column ): - return self.identcomp.ShouldUseNow( start_column ) + def ShouldUseGeneralCompleter( self, start_column ): + return self.gencomp.ShouldUseNow( start_column ) def ShouldUseFiletypeCompleter( self, start_column ): @@ -132,21 +132,21 @@ class YouCompleteMe( object ): def OnFileReadyToParse( self ): - self.identcomp.OnFileReadyToParse() + self.gencomp.OnFileReadyToParse() if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnFileReadyToParse() def OnBufferDelete( self, deleted_buffer_file ): - self.identcomp.OnBufferDelete( deleted_buffer_file ) + self.gencomp.OnBufferDelete( deleted_buffer_file ) if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnBufferDelete( deleted_buffer_file ) def OnInsertLeave( self ): - self.identcomp.OnInsertLeave() + self.gencomp.OnInsertLeave() if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnInsertLeave() @@ -176,7 +176,7 @@ class YouCompleteMe( object ): def OnCurrentIdentifierFinished( self ): - self.identcomp.OnCurrentIdentifierFinished() + self.gencomp.OnCurrentIdentifierFinished() if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnCurrentIdentifierFinished() @@ -184,7 +184,7 @@ class YouCompleteMe( object ): def DebugInfo( self ): completers = set( self.filetype_completers.values() ) - completers.add( self.identcomp ) + completers.add( self.gencomp ) output = [] for completer in completers: if not completer: diff --git a/python/ycm_utils.py b/python/ycm_utils.py index 68744e50..d1fe5035 100644 --- a/python/ycm_utils.py +++ b/python/ycm_utils.py @@ -23,3 +23,4 @@ def IsIdentifierChar( char ): def SanitizeQuery( query ): return query.strip() +