diff --git a/README.md b/README.md index 22c11489..0b009c88 100644 --- a/README.md +++ b/README.md @@ -851,6 +851,31 @@ Default: `0` let g:ycm_seed_identifiers_with_syntax = 0 +### The `g:ycm_extra_conf_vim_data` option + +If you're using semantic completion for C-family files, this option might come +handy; it's a way of sending data from Vim to your `FlagsForFile` function in +your `.ycm_extra_conf.py` file. + +This option is supposed to be a list of VimScript expression strings that are +evaluated for every request to the `ycmd` server and then passed to your +`FlagsForFile` function as a `client_data` keyword argument. + +For instance, if you set this option to `['v:version']`, your `FlagsForFile` +function will be called like this: + +```python +# The '704' value is of course contingent on Vim 7.4; in 7.3 it would be '703' +FlagsForFile(filename, client_data = {'v:version': 704}) +``` + +So the `client_data` parameter is a dictionary mapping Vim expression strings to +their values at the time of the request. + +Default: `[]` + + let g:ycm_extra_conf_vim_data = [] + ### The `g:ycm_server_use_vim_stdout` option By default, the `ycmd` completion server writes logs to logfiles. When this @@ -1162,6 +1187,20 @@ Default: `1` FAQ --- +### I used to be able to `import vim` in `.ycm_extra_conf.py`, but now can't + +YCM was rewritten to use a client-server architecture where most of the logic is +in the `ycmd` server. So the magic `vim` module you could have previously +imported in your `.ycm_extra_conf.py` files doesn't exist anymore. + +To be fair, importing the magic `vim` module in extra conf files was never +supported in the first place; it only ever worked by accident and was never a +part of the extra conf API. + +But fear not, you should be able to tweak your extra conf files to continue +working by using the `g:ycm_extra_conf_vim_data` option. See the docs on that +option for details. + ### I get a linker warning regarding `libpython` on Mac when compiling YCM If the warning is `ld: warning: path '/usr/lib/libpython2.7.dylib' following -L diff --git a/cpp/ycm/.ycm_extra_conf.py b/cpp/ycm/.ycm_extra_conf.py index e2ec67b8..655d1279 100644 --- a/cpp/ycm/.ycm_extra_conf.py +++ b/cpp/ycm/.ycm_extra_conf.py @@ -43,6 +43,8 @@ flags = [ '-Wno-variadic-macros', '-fexceptions', '-DNDEBUG', +# You 100% do NOT need -DUSE_CLANG_COMPLETER in your flags; only the YCM +# source code needs it. '-DUSE_CLANG_COMPLETER', # THIS IS IMPORTANT! Without a "-std=" flag, clang won't know which # language to use when compiling headers. So it will guess. Badly. So C++ @@ -128,7 +130,7 @@ def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): return new_flags -def FlagsForFile( filename ): +def FlagsForFile( filename, **kwargs ): if database: # Bear in mind that compilation_info.compiler_flags_ does NOT return a # python list, but a "list-like" StringVec object diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index f8e9b3e9..60c22320 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -112,6 +112,9 @@ let g:ycm_server_keep_logfiles = let g:ycm_server_idle_suicide_seconds = \ get( g:, 'ycm_server_idle_suicide_seconds', 43200 ) +let g:ycm_extra_conf_vim_data = + \ get( g:, 'ycm_extra_conf_vim_data', [] ) + " On-demand loading. Let's use the autoload folder and not slow down vim's " startup procedure. diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index bb0001c2..3893e887 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -25,15 +25,15 @@ from ycm.client.base_request import ( BaseRequest, BuildRequestData, TIMEOUT_SECONDS = 0.5 class CompletionRequest( BaseRequest ): - def __init__( self, force_semantic = False ): + def __init__( self, extra_data = None ): super( CompletionRequest, self ).__init__() self._completion_start_column = base.CompletionStartColumn() # This field is also used by the omni_completion_request subclass self.request_data = BuildRequestData( self._completion_start_column ) - if force_semantic: - self.request_data[ 'force_semantic' ] = True + if extra_data: + self.request_data.update( extra_data ) def CompletionStartColumn( self ): diff --git a/python/ycm/completers/cpp/clang_completer.py b/python/ycm/completers/cpp/clang_completer.py index fb034cee..26b91d88 100644 --- a/python/ycm/completers/cpp/clang_completer.py +++ b/python/ycm/completers/cpp/clang_completer.py @@ -247,7 +247,8 @@ class ClangCompleter( Completer ): if 'compilation_flags' in request_data: return PrepareFlagsForClang( request_data[ 'compilation_flags' ], filename ) - return self._flags.FlagsForFile( filename ) + client_data = request_data.get( 'extra_conf_data', None ) + return self._flags.FlagsForFile( filename, client_data = client_data ) def ConvertCompletionData( completion_data ): diff --git a/python/ycm/completers/cpp/flags.py b/python/ycm/completers/cpp/flags.py index 385bdc03..39a830c4 100644 --- a/python/ycm/completers/cpp/flags.py +++ b/python/ycm/completers/cpp/flags.py @@ -19,6 +19,7 @@ import ycm_core import os +import inspect from ycm import extra_conf_store from ycm.utils import ToUtf8IfNeeded @@ -40,7 +41,10 @@ class Flags( object ): self.no_extra_conf_file_warning_posted = False - def FlagsForFile( self, filename, add_special_clang_flags = True ): + def FlagsForFile( self, + filename, + add_special_clang_flags = True, + client_data = None ): try: return self.flags_for_file[ filename ] except KeyError: @@ -51,7 +55,9 @@ class Flags( object ): raise RuntimeError( NO_EXTRA_CONF_FILENAME_MESSAGE ) return None - results = module.FlagsForFile( filename ) + results = _CallExtraConfFlagsForFile( module, + filename, + client_data ) if not results.get( 'flags_ready', True ): return None @@ -95,6 +101,15 @@ class Flags( object ): self.flags_for_file.clear() +def _CallExtraConfFlagsForFile( module, filename, client_data ): + # For the sake of backwards compatibility, we need to first check whether the + # FlagsForFile function in the extra conf module even allows keyword args. + if inspect.getargspec( module.FlagsForFile ).keywords: + return module.FlagsForFile( filename, client_data = client_data ) + else: + return module.FlagsForFile( filename ) + + def PrepareFlagsForClang( flags, filename ): flags = _RemoveUnusedFlags( flags, filename ) flags = _SanitizeFlags( flags ) diff --git a/python/ycm/completers/cpp/tests/flags_test.py b/python/ycm/completers/cpp/tests/flags_test.py index 90654152..5d7c6658 100644 --- a/python/ycm/completers/cpp/tests/flags_test.py +++ b/python/ycm/completers/cpp/tests/flags_test.py @@ -18,8 +18,6 @@ # along with YouCompleteMe. If not, see . from nose.tools import eq_ -from ycm.test_utils import MockVimModule -vim_mock = MockVimModule() from .. import flags diff --git a/python/ycm/server/tests/get_completions_test.py b/python/ycm/server/tests/get_completions_test.py index 62970f3a..3532d462 100644 --- a/python/ycm/server/tests/get_completions_test.py +++ b/python/ycm/server/tests/get_completions_test.py @@ -24,7 +24,7 @@ import httplib from .test_utils import Setup, BuildRequest, PathToTestFile from webtest import TestApp from nose.tools import eq_, with_setup -from hamcrest import ( assert_that, has_items, has_entry, +from hamcrest import ( assert_that, has_item, has_items, has_entry, contains_inanyorder ) from ..responses import BuildCompletionData, UnknownExtraConf from .. import handlers @@ -211,6 +211,28 @@ def GetCompletions_ForceSemantic_Works_test(): CompletionEntryMatcher( 'bool' ) ) ) +@with_setup( Setup ) +def GetCompletions_ClangCompleter_ClientDataGivenToExtraConf_test(): + app = TestApp( handlers.app ) + app.post_json( '/load_extra_conf_file', + { 'filepath': PathToTestFile( + 'client_data/.ycm_extra_conf.py' ) } ) + + filepath = PathToTestFile( 'client_data/main.cpp' ) + completion_data = BuildRequest( filepath = filepath, + filetype = 'cpp', + contents = open( filepath ).read(), + line_num = 8, + column_num = 6, + start_column = 6, + extra_conf_data = { + 'flags': ['-x', 'c++'] + }) + + results = app.post_json( '/completions', completion_data ).json + assert_that( results, has_item( CompletionEntryMatcher( 'x' ) ) ) + + @with_setup( Setup ) def GetCompletions_IdentifierCompleter_SyntaxKeywordsAdded_test(): app = TestApp( handlers.app ) diff --git a/python/ycm/server/tests/testdata/client_data/.ycm_extra_conf.py b/python/ycm/server/tests/testdata/client_data/.ycm_extra_conf.py new file mode 100644 index 00000000..1bc47380 --- /dev/null +++ b/python/ycm/server/tests/testdata/client_data/.ycm_extra_conf.py @@ -0,0 +1,5 @@ +def FlagsForFile( filename, **kwargs ): + return { + 'flags': kwargs['client_data']['flags'], + 'do_cache': True + } diff --git a/python/ycm/server/tests/testdata/client_data/main.cpp b/python/ycm/server/tests/testdata/client_data/main.cpp new file mode 100644 index 00000000..a56505df --- /dev/null +++ b/python/ycm/server/tests/testdata/client_data/main.cpp @@ -0,0 +1,11 @@ +struct Foo { + int x; +}; + +int main() +{ + Foo foo; + // The location after the dot is line 9, col 7 + foo. +} + diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 6ee36e3a..56bd9765 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -118,6 +118,16 @@ def GetReadOnlyVimGlobals( force_python_objects = False ): return vim.eval( 'g:' ) +def VimExpressionToPythonType( vim_expression ): + result = vim.eval( vim_expression ) + if not isinstance( result, basestring ): + return result + try: + return int( result ) + except ValueError: + return result + + # Both |line| and |column| need to be 1-based def JumpToLocation( filename, line, column ): # Add an entry to the jumplist diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index bf9d4d99..05704b63 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -132,7 +132,12 @@ class YouCompleteMe( object ): self._omnicomp.ShouldUseNow() ): self._latest_completion_request = OmniCompletionRequest( self._omnicomp ) else: - self._latest_completion_request = ( CompletionRequest( force_semantic ) + extra_data = {} + self._AddExtraConfDataIfNeeded( extra_data ) + if force_semantic: + extra_data[ 'force_semantic' ] = True + + self._latest_completion_request = ( CompletionRequest( extra_data ) if self._IsServerAlive() else None ) return self._latest_completion_request @@ -176,11 +181,9 @@ class YouCompleteMe( object ): self._NotifyUserIfServerCrashed() extra_data = {} - if self._user_options[ 'collect_identifiers_from_tags_files' ]: - extra_data[ 'tag_files' ] = _GetTagFiles() - - if self._user_options[ 'seed_identifiers_with_syntax' ]: - self._AddSyntaxDataIfNeeded( extra_data ) + self._AddTagsFilesIfNeeded( extra_data ) + self._AddSyntaxDataIfNeeded( extra_data ) + self._AddExtraConfDataIfNeeded( extra_data ) self._latest_file_parse_request = EventNotification( 'FileReadyToParse', extra_data ) @@ -280,6 +283,8 @@ class YouCompleteMe( object ): def _AddSyntaxDataIfNeeded( self, extra_data ): + if not self._user_options[ 'seed_identifiers_with_syntax' ]: + return filetype = vimsupport.CurrentFiletypes()[ 0 ] if filetype in self._filetypes_with_keywords_loaded: return @@ -289,10 +294,26 @@ class YouCompleteMe( object ): syntax_parse.SyntaxKeywordsForCurrentBuffer() ) -def _GetTagFiles(): - tag_files = vim.eval( 'tagfiles()' ) - current_working_directory = os.getcwd() - return [ os.path.join( current_working_directory, x ) for x in tag_files ] + def _AddTagsFilesIfNeeded( self, extra_data ): + def GetTagFiles(): + tag_files = vim.eval( 'tagfiles()' ) + current_working_directory = os.getcwd() + return [ os.path.join( current_working_directory, x ) for x in tag_files ] + + if not self._user_options[ 'collect_identifiers_from_tags_files' ]: + return + extra_data[ 'tag_files' ] = GetTagFiles() + + + def _AddExtraConfDataIfNeeded( self, extra_data ): + def BuildExtraConfData( extra_conf_vim_data ): + return dict( ( expr, vimsupport.VimExpressionToPythonType( expr ) ) + for expr in extra_conf_vim_data ) + + extra_conf_vim_data = self._user_options[ 'extra_conf_vim_data' ] + if extra_conf_vim_data: + extra_data[ 'extra_conf_data' ] = BuildExtraConfData( + extra_conf_vim_data ) def _PathToServerScript():