diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index 8f3e30e552..8ccf37c73c 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -1283,12 +1283,18 @@ def GetSettings( self, module, request_data ): def _GetSettingsFromExtraConf( self, request_data ): - self._settings = self.DefaultSettings( request_data ) + ls = self.DefaultSettings( request_data ) + # In case no module is found + self._settings = { + 'ls': ls + } module = extra_conf_store.ModuleForSourceFile( request_data[ 'filepath' ] ) if module: - settings = self.GetSettings( module, request_data ) - self._settings.update( settings.get( 'ls', {} ) ) + self._settings = self.GetSettings( module, request_data ) + ls.update( self._settings.get( 'ls', {} ) ) + self._settings[ 'ls' ] = ls + # 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. if not extra_conf_store.IsGlobalExtraConfModule( module ): @@ -1706,7 +1712,8 @@ def _SendInitialize( self, request_data, extra_conf_dir ): # clear how/where that is specified. msg = lsp.Initialize( request_id, self._project_directory, - self._settings ) + self._settings.get( 'ls', {} ), + self._settings.get( 'capabilities', {} ) ) def response_handler( response, message ): if message is None: @@ -1819,7 +1826,7 @@ def _HandleInitializeInPollThread( self, response ): # configuration should be send in response to a workspace/configuration # request? self.GetConnection().SendNotification( - lsp.DidChangeConfiguration( self._settings ) ) + lsp.DidChangeConfiguration( self._settings.get( 'ls', {} ) ) ) # Notify the other threads that we have completed the initialize exchange. self._initialize_response = None diff --git a/ycmd/completers/language_server/language_server_protocol.py b/ycmd/completers/language_server/language_server_protocol.py index 601bef3e55..8710f0b200 100644 --- a/ycmd/completers/language_server/language_server_protocol.py +++ b/ycmd/completers/language_server/language_server_protocol.py @@ -34,7 +34,8 @@ unquote, url2pathname, urlparse, - urljoin ) + urljoin, + UpdateDict ) Error = collections.namedtuple( 'RequestError', [ 'code', 'reason' ] ) @@ -233,7 +234,10 @@ def BuildResponse( request, parameters ): return _BuildMessageData( message ) -def Initialize( request_id, project_directory, settings ): +def Initialize( request_id, + project_directory, + settings, + capabilities ): """Build the Language Server initialize request""" return BuildRequest( request_id, 'initialize', { @@ -241,8 +245,11 @@ def Initialize( request_id, project_directory, settings ): 'rootPath': project_directory, 'rootUri': FilePathToUri( project_directory ), 'initializationOptions': settings, - 'capabilities': { - 'workspace': { 'applyEdit': True, 'documentChanges': True }, + 'capabilities': UpdateDict( { + 'workspace': { + 'applyEdit': True, + 'documentChanges': True, + }, 'textDocument': { 'codeAction': { 'codeActionLiteralSupport': { @@ -288,7 +295,7 @@ def Initialize( request_id, project_directory, settings ): }, }, }, - }, + }, capabilities ), } ) diff --git a/ycmd/utils.py b/ycmd/utils.py index fca525fbb1..90aed0564b 100644 --- a/ycmd/utils.py +++ b/ycmd/utils.py @@ -24,7 +24,12 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -from future.utils import PY2, native +try: + import collections.abc as collections_abc +except ImportError: + import collections as collections_abc + +from future.utils import PY2, native, iteritems import copy import json import logging @@ -673,3 +678,46 @@ def GetClangResourceDir(): CLANG_RESOURCE_DIR = GetClangResourceDir() + + +def UpdateDict( target, override ): + """Apply the updates in |override| to the dict |target|. This is like + dict.update, but recursive. i.e. if the existing element is a dict, then + override elements of the sub-dict rather than wholesale replacing. + + e.g. + + UpdateDict( + { + 'outer': { 'inner': { 'key': 'oldValue', 'existingKey': True } } + }, + { + 'outer': { 'inner': { 'key': 'newValue' } }, + 'newKey': { 'newDict': True }, + } + ) + + yields: + + { + outer: { + inner: { + key: 'newValue', + exitingKey: True + } + }, + newKey: { newDict: True } + } + + """ + + for key, value in iteritems( override ): + current_value = target.get( key ) + if not isinstance( current_value, collections_abc.Mapping ): + target[ key ] = value + elif isinstance( value, collections_abc.Mapping ): + target[ key ] = UpdateDict( current_value, value ) + else: + target[ key ] = value + + return target