From fb6b454e2f4d51378e46e5d9e67507be91494aea Mon Sep 17 00:00:00 2001 From: micbou Date: Thu, 28 Mar 2019 21:55:50 +0100 Subject: [PATCH] Add extra conf support to Clangd completer --- README.md | 59 ++-- ycmd/completers/cpp/clangd_completer.py | 82 ++++- ycmd/completers/cpp/flags.py | 20 +- .../language_server_completer.py | 36 ++- .../simple_language_server_completer.py | 18 +- ycmd/completers/python/python_completer.py | 7 +- ycmd/tests/clang/flags_test.py | 304 +++++++++--------- ycmd/tests/clangd/debug_info_test.py | 54 +++- ycmd/tests/clangd/get_completions_test.py | 60 +++- ycmd/tests/clangd/server_management_test.py | 4 +- .../testdata/extra_conf/.ycm_extra_conf.py | 19 ++ ycmd/tests/clangd/testdata/extra_conf/bar.cpp | 6 + ycmd/tests/clangd/testdata/extra_conf/foo.cpp | 6 + .../clangd/testdata/extra_conf/include/foo.h | 5 + .../testdata/extra_conf/subdir/include/bar.h | 5 + 15 files changed, 463 insertions(+), 222 deletions(-) create mode 100644 ycmd/tests/clangd/testdata/extra_conf/.ycm_extra_conf.py create mode 100644 ycmd/tests/clangd/testdata/extra_conf/bar.cpp create mode 100644 ycmd/tests/clangd/testdata/extra_conf/foo.cpp create mode 100644 ycmd/tests/clangd/testdata/extra_conf/include/foo.h create mode 100644 ycmd/tests/clangd/testdata/extra_conf/subdir/include/bar.h diff --git a/README.md b/README.md index 060c1eccec..465b0f98f7 100644 --- a/README.md +++ b/README.md @@ -97,14 +97,13 @@ non-semantic. There are also several semantic engines in YCM. There's a libclang-based completer and [clangd][clangd]-based completer that both provide semantic -completion for C-family languages. The [clangd][clangd]-based completer doesn't -support extra conf; you must have a compilation database. [clangd][clangd] -support is currently **experimental** and changes in the near future might break -backwards compatibility. There's also a Jedi-based completer for semantic -completion for Python, an OmniSharp-based completer for C#, a -[Gocode][gocode]-based completer for Go (using [Godef][godef] for jumping to -definitions), a TSServer-based completer for JavaScript and TypeScript, and a -[jdt.ls][jdtls]-based server for Java. More will be added with time. +completion for C-family languages. [clangd][clangd] support is currently +**experimental** and changes in the near future might break backwards +compatibility. There's also a Jedi-based completer for semantic completion for +Python, an OmniSharp-based completer for C#, a [Gocode][gocode]-based completer +for Go (using [Godef][godef] for jumping to definitions), a TSServer-based +completer for JavaScript and TypeScript, and a [jdt.ls][jdtls]-based server for +Java. More will be added with time. There are also other completion engines, like the filepath completer (part of the identifier completer). @@ -217,8 +216,8 @@ The `.ycm_extra_conf.py` module may define the following functions: #### `Settings( **kwargs )` This function allows users to configure the language completers on a per project -basis or globally. Currently, it is required by the C-family completer and -optional for the Python completer. The following arguments can be retrieved from +basis or globally. Currently, it is required by the libclang-based completer and +optional for other completers. The following arguments can be retrieved from the `kwargs` dictionary and are common to all completers: - `language`: an identifier of the completer that called the function. Its value @@ -231,7 +230,7 @@ the `kwargs` dictionary and are common to all completers: language = kwargs[ 'language' ] if language == 'cfamily': return { - # Settings for the C-family completer. + # Settings for the libclang and clangd-based completer. } if language == 'python': return { @@ -240,6 +239,8 @@ the `kwargs` dictionary and are common to all completers: return {} ``` +- `filename`: absolute path of the file currently edited. + - `client_data`: any additional data supplied by the client application. See the [YouCompleteMe documentation][extra-conf-vim-data-doc] for an example. @@ -248,32 +249,32 @@ The return value is a dictionary whose content depends on the completer. ##### C-family settings -The `Settings` function is called by the C-family completer to get the compiler -flags to use when compiling the current file. The absolute path of this file is -accessible under the `filename` key of the `kwargs` dictionary. -[clangd][clangd]-based completer doesn't support extra conf files. If you are -using [clangd][clangd]-based completer, you must have a compilation database in -your project's root or in one of the parent directories to provide compiler -flags. +The `Settings` function is called by the libclang and clangd-based completers to +get the compiler flags to use when compiling the current file. The absolute path +of this file is accessible under the `filename` key of the `kwargs` dictionary. -The return value expected by the completer is a dictionary containing the +The return value expected by both completers is a dictionary containing the following items: -- `flags`: (mandatory) a list of compiler flags. +- `flags`: (mandatory for libclang, optional for clangd) a list of compiler + flags. -- `include_paths_relative_to_dir`: (optional) the directory to which the - include paths in the list of flags are relative. Defaults to ycmd working - directory. - -- `override_filename`: (optional) a string indicating the name of the file to - parse as the translation unit for the supplied file name. This fairly - advanced feature allows for projects that use a 'unity'-style build, or - for header files which depend on other includes in other files. +- `include_paths_relative_to_dir`: (optional) the directory to which the include + paths in the list of flags are relative. Defaults to ycmd working directory + for the libclang completer and `.ycm_extra_conf.py`'s directory for the + clangd completer. - `do_cache`: (optional) a boolean indicating whether or not the result of this call (i.e. the list of flags) should be cached for this file name. Defaults to `True`. If unsure, the default is almost always correct. +The libclang-based completer also supports the following items: + +- `override_filename`: (optional) a string indicating the name of the file to + parse as the translation unit for the supplied file name. This fairly advanced + feature allows for projects that use a 'unity'-style build, or for header + files which depend on other includes in other files. + - `flags_ready`: (optional) a boolean indicating that the flags should be used. Defaults to `True`. If unsure, the default is almost always correct. @@ -393,7 +394,7 @@ License ------- This software is licensed under the [GPL v3 license][gpl]. -© 2015-2018 ycmd contributors +© 2015-2019 ycmd contributors [ycmd-users]: https://groups.google.com/forum/?hl=en#!forum/ycmd-users [ycm]: http://valloric.github.io/YouCompleteMe/ diff --git a/ycmd/completers/cpp/clangd_completer.py b/ycmd/completers/cpp/clangd_completer.py index df65146055..574e66aae6 100644 --- a/ycmd/completers/cpp/clangd_completer.py +++ b/ycmd/completers/cpp/clangd_completer.py @@ -26,8 +26,10 @@ import os import subprocess -from ycmd import responses +from ycmd import extra_conf_store, responses from ycmd.completers.completer_utils import GetFileLines +from ycmd.completers.cpp.flags import ( RemoveUnusedFlags, + ShouldAllowWinStyleFlags ) from ycmd.completers.language_server import simple_language_server_completer from ycmd.completers.language_server import language_server_completer from ycmd.completers.language_server import language_server_protocol as lsp @@ -200,6 +202,27 @@ def ShouldEnableClangdCompleter( user_options ): return True +def AddCompilerToFlags( flags, enable_windows_style_flags ): + """Removes everything before the first flag and returns the remaining flags + prepended with clangd.""" + for index, flag in enumerate( flags ): + if ( flag.startswith( '-' ) or + ( enable_windows_style_flags and + flag.startswith( '/' ) and + not os.path.exists( flag ) ) ): + flags = flags[ index: ] + break + return [ 'clangd' ] + flags + + +def BuildCompilationCommand( flags, filepath ): + """Returns a compilation command from a list of flags and a file.""" + enable_windows_style_flags = ShouldAllowWinStyleFlags( flags ) + flags = AddCompilerToFlags( flags, enable_windows_style_flags ) + flags = RemoveUnusedFlags( flags, filepath, enable_windows_style_flags ) + return flags + [ filepath ] + + class ClangdCompleter( simple_language_server_completer.SimpleLSPCompleter ): """A LSP-based completer for C-family languages, powered by Clangd. @@ -214,6 +237,17 @@ def __init__( self, user_options ): self._clangd_command = GetClangdCommand( user_options ) self._use_ycmd_caching = user_options[ 'clangd_uses_ycmd_caching' ] + self._flags_for_file = {} + + + def _Reset( self ): + with self._server_state_mutex: + super( ClangdCompleter, self )._Reset() + self._flags_for_file = {} + + + def GetCompleterName( self ): + return 'C-family' def GetServerName( self ): @@ -224,6 +258,10 @@ def GetCommandLine( self ): return self._clangd_command + def Language( self ): + return 'cfamily' + + def SupportedFiletypes( self ): return ( 'c', 'cpp', 'objc', 'objcpp', 'cuda' ) @@ -361,3 +399,45 @@ def GetDetailedDiagnostic( self, request_data ): minimum_distance = distance return responses.BuildDisplayMessageResponse( message ) + + + def _SendFlagsFromExtraConf( self, request_data ): + """Reads the flags from the extra conf of the given request and sends them + to Clangd as an entry of a compilation database using the + 'compilationDatabaseChanges' configuration.""" + filepath = request_data[ 'filepath' ] + + with self._server_info_mutex: + module = extra_conf_store.ModuleForSourceFile( filepath ) + if not module: + return + + settings = self.GetSettings( module, request_data ) + if settings.get( 'do_cache', True ) and filepath in self._flags_for_file: + return + + flags = settings.get( 'flags', [] ) + + self.GetConnection().SendNotification( lsp.DidChangeConfiguration( { + 'compilationDatabaseChanges': { + filepath: { + 'compilationCommand': BuildCompilationCommand( flags, filepath ), + 'workingDirectory': settings.get( 'include_paths_relative_to_dir', + self._project_directory ) + } + } + } ) ) + + self._flags_for_file[ filepath ] = flags + + + def OnFileReadyToParse( self, request_data ): + return super( ClangdCompleter, self ).OnFileReadyToParse( + request_data, + handlers = [ lambda self: self._SendFlagsFromExtraConf( request_data ) ] ) + + + def ExtraDebugItems( self, request_data ): + return [ responses.DebugInfoItem( + 'using flags from extra configuration file', + self._flags_for_file.get( request_data[ 'filepath' ], False ) ) ] diff --git a/ycmd/completers/cpp/flags.py b/ycmd/completers/cpp/flags.py index cfcb7e6eef..768baf86e0 100644 --- a/ycmd/completers/cpp/flags.py +++ b/ycmd/completers/cpp/flags.py @@ -161,7 +161,7 @@ def _ParseFlagsFromExtraConfOrDatabase( self, sanitized_flags = PrepareFlagsForClang( flags, filename, add_extra_clang_flags, - _ShouldAllowWinStyleFlags( flags ) ) + ShouldAllowWinStyleFlags( flags ) ) if results.get( 'do_cache', True ): self.flags_for_file[ filename, client_data ] = sanitized_flags, filename @@ -248,7 +248,7 @@ def _ExtractFlagsList( flags_for_file_output ): return [ ToUnicode( x ) for x in flags_for_file_output[ 'flags' ] ] -def _ShouldAllowWinStyleFlags( flags ): +def ShouldAllowWinStyleFlags( flags ): if OnWindows(): # Iterate in reverse because we only care # about the last occurrence of --driver-mode flag. @@ -302,7 +302,7 @@ def PrepareFlagsForClang( flags, enable_windows_style_flags = False ): flags = _AddLanguageFlagWhenAppropriate( flags, enable_windows_style_flags ) flags = _RemoveXclangFlags( flags ) - flags = _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ) + flags = RemoveUnusedFlags( flags, filename, enable_windows_style_flags ) if add_extra_clang_flags: # This flag tells libclang where to find the builtin includes. flags.append( '-resource-dir=' + CLANG_RESOURCE_DIR ) @@ -407,7 +407,7 @@ def _AddLanguageFlagWhenAppropriate( flags, enable_windows_style_flags ): return flags -def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ): +def RemoveUnusedFlags( flags, filename, enable_windows_style_flags ): """Given an iterable object that produces strings (flags for Clang), removes the '-c' and '-o' options that Clang does not like to see when it's producing completions for a file. Same for '-MD' etc. @@ -455,8 +455,8 @@ def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ): # flags for headers. The returned flags include "foo.cpp" and we need to # remove that. if _SkipStrayFilenameFlag( current_flag, - previous_flag, - enable_windows_style_flags ): + previous_flag, + enable_windows_style_flags ): continue new_flags.append( flag ) @@ -465,8 +465,8 @@ def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ): def _SkipStrayFilenameFlag( current_flag, - previous_flag, - enable_windows_style_flags ): + previous_flag, + enable_windows_style_flags ): current_flag_starts_with_slash = current_flag.startswith( '/' ) previous_flag_starts_with_slash = previous_flag.startswith( '/' ) @@ -622,7 +622,7 @@ def _MakeRelativePathsInFlagsAbsolute( flags, working_directory ): new_flags = [] make_next_absolute = False path_flags = ( PATH_FLAGS + INCLUDE_FLAGS_WIN_STYLE - if _ShouldAllowWinStyleFlags( flags ) + if ShouldAllowWinStyleFlags( flags ) else PATH_FLAGS ) for flag in flags: new_flag = flag @@ -685,7 +685,7 @@ def UserIncludePaths( user_flags, filename ): '-isystem': include_paths, '-F': framework_paths, '-iframework': framework_paths } - if _ShouldAllowWinStyleFlags( user_flags ): + if ShouldAllowWinStyleFlags( user_flags ): include_flags[ '/I' ] = include_paths try: diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index 5435992568..b513f93ccd 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2018 ycmd contributors +# Copyright (C) 2017-2019 ycmd contributors # # This file is part of ycmd. # @@ -1087,10 +1087,12 @@ def _DiscoverSubcommandSupport( self, commands ): return subcommands_map - def _GetSettings( self, module, client_data ): + def GetSettings( self, module, request_data ): if hasattr( module, 'Settings' ): - settings = module.Settings( language = self.Language(), - client_data = client_data ) + settings = module.Settings( + language = self.Language(), + filename = request_data[ 'filepath' ], + client_data = request_data[ 'extra_conf_data' ] ) if settings is not None: return settings @@ -1102,7 +1104,7 @@ def _GetSettings( self, module, client_data ): def _GetSettingsFromExtraConf( self, request_data ): module = extra_conf_store.ModuleForSourceFile( request_data[ 'filepath' ] ) if module: - settings = self._GetSettings( module, request_data[ 'extra_conf_data' ] ) + settings = self.GetSettings( module, request_data ) self._settings = settings.get( 'ls' ) or {} # Only return the dir if it was found in the paths; we don't want to use # the path of the global extra conf as a project root dir. @@ -1130,7 +1132,7 @@ def _StartAndInitializeServer( self, request_data, *args, **kwargs ): self._SendInitialize( request_data, extra_conf_dir ) - def OnFileReadyToParse( self, request_data ): + def OnFileReadyToParse( self, request_data, handlers = None ): if not self.ServerIsHealthy() and not self._server_started: # We have to get the settings before starting the server, as this call # might throw UnknownExtraConf. @@ -1139,17 +1141,23 @@ def OnFileReadyToParse( self, request_data ): if not self.ServerIsHealthy(): return - # If we haven't finished initializing yet, we need to queue up a call to - # _UpdateServerWithFileContents. This ensures that the server is up to date - # as soon as we are able to send more messages. This is important because - # server start up can be quite slow and we must not block the user, while we - # must keep the server synchronized. + # If we haven't finished initializing yet, we need to queue up + # all functions from |handlers| and _UpdateServerWithFileContents. This + # ensures that the server is up to date as soon as we are able to send more + # messages. This is important because server start up can be quite slow and + # we must not block the user, while we must keep the server synchronized. + if handlers is None: + handlers = [] + handlers.append( + lambda self: self._UpdateServerWithFileContents( request_data ) ) + if not self._initialize_event.is_set(): - self._OnInitializeComplete( - lambda self: self._UpdateServerWithFileContents( request_data ) ) + for handler in handlers: + self._OnInitializeComplete( handler ) return - self._UpdateServerWithFileContents( request_data ) + for handler in handlers: + handler( self ) # Return the latest diagnostics that we have received. # diff --git a/ycmd/completers/language_server/simple_language_server_completer.py b/ycmd/completers/language_server/simple_language_server_completer.py index 20be4aef11..6e0758e93c 100644 --- a/ycmd/completers/language_server/simple_language_server_completer.py +++ b/ycmd/completers/language_server/simple_language_server_completer.py @@ -33,6 +33,11 @@ class SimpleLSPCompleter( lsc.LanguageServerCompleter ): + @abc.abstractmethod + def GetCompleterName( self ): + pass + + @abc.abstractmethod def GetServerName( self ): pass @@ -72,22 +77,23 @@ def GetConnection( self ): return self._connection + def ExtraDebugItems( self, request_data ): + return [] + + def DebugInfo( self, request_data ): with self._server_state_mutex: + extras = self.CommonDebugItems() + self.ExtraDebugItems( request_data ) server = responses.DebugInfoServer( name = self.GetServerName(), handle = self._server_handle, executable = self.GetCommandLine(), logfiles = [ self._stderr_file ], - extras = self.CommonDebugItems() ) + extras = extras ) - return responses.BuildDebugInfoResponse( name = self.Language(), + return responses.BuildDebugInfoResponse( name = self.GetCompleterName(), servers = [ server ] ) - def Language( self ): - return self.GetServerName() - - def ServerIsHealthy( self ): with self._server_state_mutex: return utils.ProcessIsRunning( self._server_handle ) diff --git a/ycmd/completers/python/python_completer.py b/ycmd/completers/python/python_completer.py index bf6d9b59e7..94c3a732e6 100644 --- a/ycmd/completers/python/python_completer.py +++ b/ycmd/completers/python/python_completer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2018 ycmd contributors +# Copyright (C) 2011-2019 ycmd contributors # # This file is part of ycmd. # @@ -67,16 +67,17 @@ def _SettingsForRequest( self, request_data ): pass module = extra_conf_store.ModuleForSourceFile( filepath ) - settings = self._GetSettings( module, client_data ) + settings = self._GetSettings( module, filepath, client_data ) self._settings_for_file[ filepath, client_data ] = settings return settings - def _GetSettings( self, module, client_data ): + def _GetSettings( self, module, filepath, client_data ): # We don't warn the user if no extra conf file is found. if module: if hasattr( module, 'Settings' ): settings = module.Settings( language = 'python', + filename = filepath, client_data = client_data ) if settings is not None: return settings diff --git a/ycmd/tests/clang/flags_test.py b/ycmd/tests/clang/flags_test.py index 8db9c80919..f3dfde7b8e 100644 --- a/ycmd/tests/clang/flags_test.py +++ b/ycmd/tests/clang/flags_test.py @@ -35,7 +35,7 @@ from types import ModuleType from ycmd.completers.cpp import flags -from ycmd.completers.cpp.flags import _ShouldAllowWinStyleFlags, INCLUDE_FLAGS +from ycmd.completers.cpp.flags import ShouldAllowWinStyleFlags, INCLUDE_FLAGS from ycmd.tests.test_utils import ( MacOnly, TemporaryTestDir, WindowsOnly, TemporaryClangProject ) from ycmd.utils import CLANG_RESOURCE_DIR @@ -647,10 +647,10 @@ def FlagsForFile( filename ): def RemoveUnusedFlags_Passthrough_test(): eq_( [ '-foo', '-bar' ], - flags._RemoveUnusedFlags( [ '-foo', '-bar' ], - 'file', - _ShouldAllowWinStyleFlags( - [ '-foo', '-bar' ] ) ) ) + flags.RemoveUnusedFlags( [ '-foo', '-bar' ], + 'file', + ShouldAllowWinStyleFlags( + [ '-foo', '-bar' ] ) ) ) def RemoveUnusedFlags_RemoveDashC_test(): @@ -659,22 +659,22 @@ def RemoveUnusedFlags_RemoveDashC_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( to_remove + expected, - filename, - _ShouldAllowWinStyleFlags( - to_remove + expected ) ) ) + flags.RemoveUnusedFlags( to_remove + expected, + filename, + ShouldAllowWinStyleFlags( + to_remove + expected ) ) ) eq_( expected, - flags._RemoveUnusedFlags( + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ -1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ -1: ] ) ) ) @@ -684,22 +684,22 @@ def RemoveUnusedFlags_RemoveColor_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( to_remove + expected, - filename, - _ShouldAllowWinStyleFlags( - to_remove + expected ) ) ) + flags.RemoveUnusedFlags( to_remove + expected, + filename, + ShouldAllowWinStyleFlags( + to_remove + expected ) ) ) eq_( expected, - flags._RemoveUnusedFlags( + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ -1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ -1: ] ) ) ) @@ -709,22 +709,22 @@ def RemoveUnusedFlags_RemoveDashO_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( to_remove + expected, - filename, - _ShouldAllowWinStyleFlags( - to_remove + expected ) ) ) + flags.RemoveUnusedFlags( to_remove + expected, + filename, + ShouldAllowWinStyleFlags( + to_remove + expected ) ) ) eq_( expected, - flags._RemoveUnusedFlags( + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ -1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ -1: ] ) ) ) @@ -734,22 +734,22 @@ def RemoveUnusedFlags_RemoveMP_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( to_remove + expected, - filename, - _ShouldAllowWinStyleFlags( - to_remove + expected ) ) ) + flags.RemoveUnusedFlags( to_remove + expected, + filename, + ShouldAllowWinStyleFlags( + to_remove + expected ) ) ) eq_( expected, - flags._RemoveUnusedFlags( + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ -1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ -1: ] ) ) ) @@ -759,23 +759,23 @@ def RemoveUnusedFlags_RemoveFilename_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) eq_( expected, - flags._RemoveUnusedFlags( + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ -1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ -1: ] ) ) ) @@ -785,17 +785,17 @@ def RemoveUnusedFlags_RemoveFlagWithoutPrecedingDashFlag_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) @WindowsOnly @@ -807,17 +807,17 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) # clang-cl and --driver-mode=cl expected = [ 'clang-cl.exe', '-foo', '--driver-mode=cl', '-xc++', '-bar', @@ -826,18 +826,18 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove - ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove + ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) # clang-cl only expected = [ 'clang-cl.exe', '-foo', '-xc++', '-bar', @@ -846,15 +846,15 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], filename, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( expected[ :1 ] + to_remove + expected[ 1: ] ) ) ) @@ -865,16 +865,16 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) # cl only with extension @@ -883,16 +883,16 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) # cl path with Windows separators expected = [ 'path\\to\\cl', '-foo', '-xc++', '/I', 'path\\to\\include\\dir' ] @@ -900,16 +900,16 @@ def RemoveUnusedFlags_RemoveStrayFilenames_CLDriver_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) @@ -926,16 +926,16 @@ def RemoveUnusedFlags_MultipleDriverModeFlagsWindows_test(): filename = 'file' eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + to_remove + expected[ 1: ] + ) ) ) flags_expected = [ '/usr/bin/g++', '--driver-mode=cl', '--driver-mode=gcc' ] flags_all = [ '/usr/bin/g++', @@ -945,10 +945,10 @@ def RemoveUnusedFlags_MultipleDriverModeFlagsWindows_test(): '--driver-mode=gcc' ] filename = 'file' - eq_( flags_expected, flags._RemoveUnusedFlags( flags_all, - filename, - _ShouldAllowWinStyleFlags( - flags_all ) ) ) + eq_( flags_expected, flags.RemoveUnusedFlags( flags_all, + filename, + ShouldAllowWinStyleFlags( + flags_all ) ) ) def RemoveUnusedFlags_Depfiles_test(): @@ -968,10 +968,10 @@ def RemoveUnusedFlags_Depfiles_test(): '-arch', 'armv7', ] - assert_that( flags._RemoveUnusedFlags( full_flags, - 'test.m', - _ShouldAllowWinStyleFlags( - full_flags ) ), + assert_that( flags.RemoveUnusedFlags( full_flags, + 'test.m', + ShouldAllowWinStyleFlags( + full_flags ) ), contains( *expected ) ) @@ -999,25 +999,25 @@ def tester( flag ): expected = [ 'clang', flag, '/foo/bar', '-isystem/zoo/goo' ] eq_( expected, - flags._RemoveUnusedFlags( expected + to_remove, - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove ) ) ) + flags.RemoveUnusedFlags( expected + to_remove, + filename, + ShouldAllowWinStyleFlags( + expected + to_remove ) ) ) eq_( expected, - flags._RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected[ :1 ] + - to_remove + - expected[ 1: ] ) ) ) + flags.RemoveUnusedFlags( expected[ :1 ] + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected[ :1 ] + + to_remove + + expected[ 1: ] ) ) ) eq_( expected + expected[ 1: ], - flags._RemoveUnusedFlags( expected + to_remove + expected[ 1: ], - filename, - _ShouldAllowWinStyleFlags( - expected + to_remove + expected[ 1: ] - ) ) ) + flags.RemoveUnusedFlags( expected + to_remove + expected[ 1: ], + filename, + ShouldAllowWinStyleFlags( + expected + to_remove + expected[ 1: ] + ) ) ) to_remove = [ '/moo/boo' ] filename = 'file' @@ -1044,7 +1044,7 @@ def RemoveXclangFlags_test(): def AddLanguageFlagWhenAppropriate_Passthrough_test(): eq_( [ '-foo', '-bar' ], flags._AddLanguageFlagWhenAppropriate( [ '-foo', '-bar' ], - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( [ '-foo', '-bar' ] ) ) ) @@ -1054,7 +1054,7 @@ def AddLanguageFlagWhenAppropriate_CLDriver_Passthrough_test(): flags._AddLanguageFlagWhenAppropriate( [ '-foo', '-bar', '--driver-mode=cl' ], - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( [ '-foo', '-bar', '--driver-mode=cl' ] ) ) ) @@ -1072,7 +1072,7 @@ def _AddLanguageFlagWhenAppropriateTester( compiler, language_flag = [] ): eq_( [ compiler ] + language_flag + expected, flags._AddLanguageFlagWhenAppropriate( to_remove + [ compiler ] + expected, - _ShouldAllowWinStyleFlags( + ShouldAllowWinStyleFlags( to_remove + [ compiler ] + expected ) ) ) diff --git a/ycmd/tests/clangd/debug_info_test.py b/ycmd/tests/clangd/debug_info_test.py index f18bb880ed..ec6e413a84 100644 --- a/ycmd/tests/clangd/debug_info_test.py +++ b/ycmd/tests/clangd/debug_info_test.py @@ -1,5 +1,4 @@ -# Copyright (C) 2011-2012 Google Inc. -# 2018 ycmd contributors +# Copyright (C) 2011-2019 ycmd contributors # # This file is part of ycmd. # @@ -37,7 +36,7 @@ def DebugInfo_NotInitialized_test( app ): assert_that( app.post_json( '/debug_info', request_data ).json, has_entry( 'completer', has_entries( { - 'name': 'clangd', + 'name': 'C-family', 'servers': contains( has_entries( { 'name': 'clangd', 'pid': None, @@ -55,6 +54,10 @@ def DebugInfo_NotInitialized_test( app ): 'key': 'Settings', 'value': '{}', } ), + has_entries( { + 'key': 'using flags from extra configuration file', + 'value': False, + } ), ), } ) ), 'items': empty(), @@ -71,7 +74,7 @@ def DebugInfo_Initialized_test( app ): assert_that( app.post_json( '/debug_info', request_data ).json, has_entry( 'completer', has_entries( { - 'name': 'clangd', + 'name': 'C-family', 'servers': contains( has_entries( { 'name': 'clangd', 'is_running': True, @@ -88,6 +91,49 @@ def DebugInfo_Initialized_test( app ): 'key': 'Settings', 'value': '{}', } ), + has_entries( { + 'key': 'using flags from extra configuration file', + 'value': False, + } ), + ), + } ) ), + 'items': empty() + } ) ) + ) + + +@IsolatedYcmd( { 'extra_conf_globlist': [ + PathToTestFile( 'extra_conf', '.ycm_extra_conf.py' ) ] } ) +def DebugInfo_ExtraConf_test( app ): + request_data = BuildRequest( filepath = PathToTestFile( 'extra_conf', + 'foo.cpp' ), + filetype = 'cpp' ) + test = { 'request': request_data } + RunAfterInitialized( app, test ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + has_entry( 'completer', has_entries( { + 'name': 'C-family', + 'servers': contains( has_entries( { + 'name': 'clangd', + 'is_running': True, + 'extras': contains( + has_entries( { + 'key': 'Server State', + 'value': 'Initialized', + } ), + has_entries( { + 'key': 'Project Directory', + 'value': PathToTestFile( 'extra_conf' ), + } ), + has_entries( { + 'key': 'Settings', + 'value': '{}', + } ), + has_entries( { + 'key': 'using flags from extra configuration file', + 'value': contains( '-I', 'include', '-DFOO' ), + } ), ), } ) ), 'items': empty() diff --git a/ycmd/tests/clangd/get_completions_test.py b/ycmd/tests/clangd/get_completions_test.py index c9d08f9bf0..063851f765 100644 --- a/ycmd/tests/clangd/get_completions_test.py +++ b/ycmd/tests/clangd/get_completions_test.py @@ -1,6 +1,6 @@ # encoding: utf-8 # -# Copyright (C) 2015-2018 ycmd contributors +# Copyright (C) 2015-2019 ycmd contributors # # This file is part of ycmd. # @@ -805,3 +805,61 @@ def GetCompletions_ServerTriggers_Ignored_test( app ): } ) } } ) + + +@IsolatedYcmd( { 'extra_conf_globlist': [ + PathToTestFile( 'extra_conf', '.ycm_extra_conf.py' ) ] } ) +def GetCompletions_SupportExtraConf_test( app ): + RunTest( app, { + 'description': 'Flags for foo.cpp from extra conf file are used', + 'request': { + 'filetype' : 'cpp', + 'filepath' : PathToTestFile( 'extra_conf', 'foo.cpp' ), + 'line_num' : 5, + 'column_num': 15 + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completion_start_column': 15, + 'completions': contains( CompletionEntryMatcher( 'member_foo' ) ), + 'errors': empty(), + } ) + } + } ) + + RunTest( app, { + 'description': 'Same flags are used again for foo.cpp', + 'request': { + 'filetype' : 'cpp', + 'filepath' : PathToTestFile( 'extra_conf', 'foo.cpp' ), + 'line_num' : 5, + 'column_num': 15 + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completion_start_column': 15, + 'completions': contains( CompletionEntryMatcher( 'member_foo' ) ), + 'errors': empty(), + } ) + } + } ) + + RunTest( app, { + 'description': 'Flags for bar.cpp from extra conf file are used', + 'request': { + 'filetype' : 'cpp', + 'filepath' : PathToTestFile( 'extra_conf', 'bar.cpp' ), + 'line_num' : 5, + 'column_num': 15 + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completion_start_column': 15, + 'completions': contains( CompletionEntryMatcher( 'member_bar' ) ), + 'errors': empty(), + } ) + } + } ) diff --git a/ycmd/tests/clangd/server_management_test.py b/ycmd/tests/clangd/server_management_test.py index 729f05b28d..6257cf2d24 100644 --- a/ycmd/tests/clangd/server_management_test.py +++ b/ycmd/tests/clangd/server_management_test.py @@ -62,7 +62,7 @@ def CheckStopped( app ): assert_that( GetDebugInfo( app ), has_entry( 'completer', has_entries( { - 'name': 'clangd', + 'name': 'C-family', 'servers': contains( has_entries( { 'name': 'clangd', 'pid': None, @@ -167,7 +167,7 @@ def ServerManagement_RestartServer_test( app ): assert_that( GetDebugInfo( app ), has_entry( 'completer', has_entries( { - 'name': 'clangd', + 'name': 'C-family', 'servers': contains( has_entries( { 'name': 'clangd', 'is_running': True, diff --git a/ycmd/tests/clangd/testdata/extra_conf/.ycm_extra_conf.py b/ycmd/tests/clangd/testdata/extra_conf/.ycm_extra_conf.py new file mode 100644 index 0000000000..be8ebb0b3a --- /dev/null +++ b/ycmd/tests/clangd/testdata/extra_conf/.ycm_extra_conf.py @@ -0,0 +1,19 @@ +import os + +DIR_OF_THIS_SCRIPT = os.path.abspath( os.path.dirname( __file__ ) ) + + +def Settings( **kwargs ): + if kwargs[ 'language' ] == 'cfamily': + basename = os.path.basename( kwargs[ 'filename' ] ) + + if basename == 'foo.cpp': + return { + 'flags': ['-I', 'include', '-DFOO'] + } + if basename == 'bar.cpp': + return { + 'flags': ['g++', '-I', 'include', '-DBAR'], + 'include_paths_relative_to_dir': os.path.join( DIR_OF_THIS_SCRIPT, + 'subdir' ) + } diff --git a/ycmd/tests/clangd/testdata/extra_conf/bar.cpp b/ycmd/tests/clangd/testdata/extra_conf/bar.cpp new file mode 100644 index 0000000000..3ea7a98cef --- /dev/null +++ b/ycmd/tests/clangd/testdata/extra_conf/bar.cpp @@ -0,0 +1,6 @@ +#include "bar.h" + +int main() { + Structure structure; + structure. +} diff --git a/ycmd/tests/clangd/testdata/extra_conf/foo.cpp b/ycmd/tests/clangd/testdata/extra_conf/foo.cpp new file mode 100644 index 0000000000..90983440ca --- /dev/null +++ b/ycmd/tests/clangd/testdata/extra_conf/foo.cpp @@ -0,0 +1,6 @@ +#include "foo.h" + +int main() { + Structure structure; + structure. +} diff --git a/ycmd/tests/clangd/testdata/extra_conf/include/foo.h b/ycmd/tests/clangd/testdata/extra_conf/include/foo.h new file mode 100644 index 0000000000..797ecfe408 --- /dev/null +++ b/ycmd/tests/clangd/testdata/extra_conf/include/foo.h @@ -0,0 +1,5 @@ +struct Structure { +#ifdef FOO + int member_foo; +#endif +}; diff --git a/ycmd/tests/clangd/testdata/extra_conf/subdir/include/bar.h b/ycmd/tests/clangd/testdata/extra_conf/subdir/include/bar.h new file mode 100644 index 0000000000..b88384bb3b --- /dev/null +++ b/ycmd/tests/clangd/testdata/extra_conf/subdir/include/bar.h @@ -0,0 +1,5 @@ +struct Structure { +#ifdef BAR + int member_bar; +#endif +};