diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index 7dcf54a539..80bec4ae36 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -1441,7 +1441,7 @@ def _CandidatesFromCompletionItems( self, this_tem_is_resolved = True try: - insertion_text, extra_data, start_codepoint = ( + insertion_text, snippet, extra_data, start_codepoint = ( _InsertionTextForItem( request_data, item ) ) except IncompatibleCompletionException: LOGGER.exception( 'Ignoring incompatible completion suggestion %s', @@ -1459,6 +1459,13 @@ def _CandidatesFromCompletionItems( self, # We'll use this later to do the resolve. extra_data[ 'item' ] = item + if snippet: + extra_data = {} if extra_data is None else extra_data + extra_data[ 'snippet' ] = { + 'snippet': snippet, + 'trigger_string': insertion_text + } + min_start_codepoint = min( min_start_codepoint, start_codepoint ) # Build a ycmd-compatible completion for the text as we received it. Later @@ -2182,9 +2189,11 @@ def _SendInitialize( self, request_data ): # the settings on the Initialize request are somehow subtly different from # the settings supplied in didChangeConfiguration, though it's not exactly # clear how/where that is specified. + extra_capabilities = self._settings.get( 'capabilities' , {} ) + extra_capabilities.update( self.ExtraCapabilities() ) msg = lsp.Initialize( request_id, self._project_directory, - self.ExtraCapabilities(), + extra_capabilities, self._settings.get( 'ls', {} ) ) def response_handler( response, message ): @@ -2783,13 +2792,21 @@ def _InsertionTextForItem( request_data, item ): Returns a tuple ( - insertion_text = the text to insert + - snippet = optional snippet text - fixits = ycmd fixit which needs to be applied additionally when selecting this completion - start_codepoint = the start column at which the text should be inserted )""" start_codepoint = request_data[ 'start_codepoint' ] + label = item[ 'label' ] + insertion_text_is_snippet = False # We will always have one of insertText or label if 'insertText' in item and item[ 'insertText' ]: + # 1 = PlainText + # 2 = Snippet + if lsp.INSERT_TEXT_FORMAT[ item.get( 'insertTextFormat', 1 ) ] == 'Snippet': + insertion_text_is_snippet = True + insertion_text = item[ 'insertText' ] else: insertion_text = item[ 'label' ] @@ -2808,7 +2825,7 @@ def _InsertionTextForItem( request_data, item ): insertion_text = text_edit[ 'newText' ] - if '\n' in insertion_text: + if '\n' in insertion_text and not insertion_text_is_snippet: # FIXME: If this logic actually worked, then we should do it for # everything and not just hte multi-line completions ? Would that allow us # to avoid the _GetCompletionItemStartCodepointOrReject logic ? Possibly, @@ -2830,9 +2847,17 @@ def _InsertionTextForItem( request_data, item ): # insertion # - insert another textEdit in additionalTextEdits which applies this # textedit + # + # On the other hand, if the insertion text is a snippet, then the snippet + # system will handle the expansion. insertion_text = item[ 'label' ] start_codepoint = request_data[ 'start_codepoint' ] + # FIXME: + # So forced-completion breaks here, as the textEdit is formulated to + # remove the existing "prefix". E.g. typing getT, the edit + # attempts to replace getT with the new code. + # Add a fixit which removes the inserted label # # TODO: Perhaps we should actually supply a completion with an empty @@ -2864,7 +2889,7 @@ def _InsertionTextForItem( request_data, item ): contents = GetFileLines( request_data, filepath ) completion_fixit_chunks = [ responses.FixItChunk( - text_edit[ 'newText' ], + text_edit[ 'newText' ], # FIXME: This could also be a Snippet _BuildRange( contents, filepath, text_edit[ 'range' ] ) ) ] @@ -2898,7 +2923,11 @@ def _InsertionTextForItem( request_data, item ): fixits.append( responses.FixIt( chunks[ 0 ].range.start_, chunks ) ) extra_data = responses.BuildFixItResponse( fixits ) if fixits else None - return insertion_text, extra_data, start_codepoint + + if insertion_text_is_snippet: + return label, insertion_text, extra_data, start_codepoint + + return insertion_text, None, extra_data, start_codepoint def FindOverlapLength( line_value, insertion_text ): diff --git a/ycmd/responses.py b/ycmd/responses.py index c78c9f15c1..2cf784e84a 100644 --- a/ycmd/responses.py +++ b/ycmd/responses.py @@ -312,7 +312,6 @@ def BuildFixItData( fixit ): 'text': fixit.text, 'kind': fixit.kind, 'resolve': fixit.resolve, - 'is_completion': fixit.is_completion } else: result = { diff --git a/ycmd/utils.py b/ycmd/utils.py index 74c352b2b9..9d321a4bcc 100644 --- a/ycmd/utils.py +++ b/ycmd/utils.py @@ -25,6 +25,7 @@ import tempfile import time import threading +import collections.abc as collections_abc LOGGER = logging.getLogger( 'ycmd' ) ROOT_DIR = os.path.normpath( os.path.join( os.path.dirname( __file__ ), '..' ) )