Skip to content

Commit

Permalink
Support snippet completions
Browse files Browse the repository at this point in the history
In general this is terrible, compared to working normal completion, but
some servers (json, yaml) will only ever return snippet completions, and
they will not be convinved to change.

The approach is to put the snippet text in the extra_data and have the
client expand the snippet in-place on selecting the completion item.

As many servers will agressively over-use snippets when the client
claims they support it, we _dont'_ claim we support it, but provide a
mechanism for extra_conf files to override the capabilities that we
declare. This allows users to enable snippets where they make sense,
i.e. for the json LS.
  • Loading branch information
puremourning committed Nov 20, 2021
1 parent 3a9b933 commit 2578f9e
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 6 deletions.
39 changes: 34 additions & 5 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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 ):
Expand Down Expand Up @@ -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' ]
Expand All @@ -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,
Expand All @@ -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<ctrl-space>, 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
Expand Down Expand Up @@ -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' ] )
)
]
Expand Down Expand Up @@ -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 ):
Expand Down
1 change: 0 additions & 1 deletion ycmd/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ def BuildFixItData( fixit ):
'text': fixit.text,
'kind': fixit.kind,
'resolve': fixit.resolve,
'is_completion': fixit.is_completion
}
else:
result = {
Expand Down
1 change: 1 addition & 0 deletions ycmd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ ), '..' ) )
Expand Down

0 comments on commit 2578f9e

Please sign in to comment.