Skip to content

Commit

Permalink
Merge branches 'pm-master-commits' and 'snippets'
Browse files Browse the repository at this point in the history
  • Loading branch information
puremourning committed Apr 10, 2020
2 parents 7e20d1e + e7cf547 commit 59a4759
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 48 deletions.
2 changes: 2 additions & 0 deletions cpp/ycm/ClangCompleter/FixIt.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ struct FixIt {
/// multiple diagnostics offering different fixit options. The text is
/// displayed to the user, allowing them choose which diagnostic to apply.
std::string text;

bool is_completion{ false };
};

} // namespace YouCompleteMe
Expand Down
3 changes: 2 additions & 1 deletion cpp/ycm/ycm_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ PYBIND11_MODULE( ycm_core, mod )
.def( py::init<>() )
.def_readonly( "chunks", &FixIt::chunks )
.def_readonly( "location", &FixIt::location )
.def_readonly( "text", &FixIt::text );
.def_readonly( "text", &FixIt::text )
.def_readonly( "is_completion", &FixIt::is_completion );

py::bind_vector< std::vector< FixIt > >( mod, "FixItVector" );

Expand Down
148 changes: 108 additions & 40 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ def _CandidatesFromCompletionItems( self, items, resolve, request_data ):
item[ '_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 @@ -1180,6 +1180,13 @@ def _CandidatesFromCompletionItems( self, items, resolve, request_data ):
extra_data = {} if extra_data is None else extra_data
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 @@ -1887,7 +1894,8 @@ def _SendInitialize( self, request_data ):
# clear how/where that is specified.
msg = lsp.Initialize( request_id,
self._project_directory,
self._settings.get( 'ls', {} ) )
self._settings.get( 'ls', {} ),
self._settings.get( 'capabilities', {} ) )

def response_handler( response, message ):
if message is None:
Expand Down Expand Up @@ -2461,59 +2469,113 @@ 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
)"""
# We do not support completion types of "Snippet". This is implicit in that we
# don't say it is a "capability" in the initialize request.
# Abort this request if the server is buggy and ignores us.
assert lsp.INSERT_TEXT_FORMAT[
item.get( 'insertTextFormat' ) or 1 ] == 'PlainText'

fixits = None

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' ]

additional_text_edits = []
fixits = []
filepath = request_data[ 'filepath' ]
contents = None

# Per the protocol, textEdit takes precedence over insertText, and must be
# on the same line (and containing) the originally requested position. These
# are a pain, and require fixing up later in some cases, as most of our
# clients won't be able to apply arbitrary edits (only 'completion', as
# opposed to 'content assist').
# Per the protocol, textEdit takes precedence over insertText, and the initial
# range of the edit must be on the same line (and containing) the originally
# requested position.
if 'textEdit' in item and item[ 'textEdit' ]:
text_edit = item[ 'textEdit' ]
start_codepoint = _GetCompletionItemStartCodepointOrReject( text_edit,
request_data )

insertion_text = text_edit[ 'newText' ]

if '\n' in insertion_text:
# jdt.ls can return completions which generate code, such as
# getters/setters and entire anonymous classes.
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,
# but it would look strange in the UI cycling through items. In any case,
# doing both should be complete.

# servers can return completions which generate code, such as
# getters/setters and entire anonymous classes. These contain newlines in
# the generated textEdit. This is irksome because ycmd's clients don't
# necessarily support that. Certainly, Vim doesn't.
#
# In order to support this we would need to do something like:
# - invent some insertion_text based on label/insertText (or perhaps
# '<snippet>'
# - insert a textEdit in additionalTextEdits which deletes this
# insertion
# - or perhaps just modify this textEdit to undo that change?
# - or perhaps somehow support insertion_text of '' (this doesn't work
# because of filtering/sorting, etc.).
# - insert this textEdit in additionalTextEdits
# However, we do have 'fixits' in completion responses which we can lean
# on.
#
# These textEdits would need a lot of fixing up and is currently out of
# scope.
# In order to support this we:
# - use the item's label as the intial insertion text with the start
# codepoint set to the query codepoint
# - insert a textEdit in additionalTextEdits which deletes this
# insertion
# - insert another textEdit in additionalTextEdits which applies this
# textedit
#
# These sorts of completions aren't really in the spirit of ycmd at the
# moment anyway. So for now, we just ignore this candidate.
raise IncompatibleCompletionException( insertion_text )
# 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
# insertion_text, than have the _client_ deal with this. One key advantage
# to that is that the client can then decide where to put the cursor.
# Currently, the Vim client puts the cursor in the wrong place (i.e.
# before the text, rather than after it).
completion_fixit_chunks = [
responses.FixItChunk(
'',
responses.Range(
responses.Location( request_data[ 'line_num' ],
start_codepoint,
filepath ),
responses.Location( request_data[ 'line_num' ],
start_codepoint + len( insertion_text ),
filepath ),
)
)
]
# FIXME: The problem with this is that it _might_ break the offsets in any
# additionalTextEdits
fixits.append(
responses.FixIt( completion_fixit_chunks[ 0 ].range.start_,
completion_fixit_chunks,
item[ 'label' ] )
)
# Add a fixit which applies this textEdit
contents = GetFileLines( request_data, filepath )
completion_fixit_chunks = [
responses.FixItChunk(
text_edit[ 'newText' ], # FIXME: This could also be a Snippet
_BuildRange( contents, filepath, text_edit[ 'range' ] )
)
]
fixits.append(
responses.FixIt( completion_fixit_chunks[ 0 ].range.start_,
completion_fixit_chunks,
item[ 'label' ],
is_completion = True )
)
else:
# Calculate the start codepoint based on the overlapping text in the
# insertion text and the existing text. This is the behavior of Visual
Expand All @@ -2522,21 +2584,27 @@ def _InsertionTextForItem( request_data, item ):
start_codepoint -= FindOverlapLength( request_data[ 'prefix' ],
insertion_text )

additional_text_edits.extend( item.get( 'additionalTextEdits' ) or [] )

additional_text_edits = item.get( 'additionalTextEdits' ) or []
if additional_text_edits:
filepath = request_data[ 'filepath' ]
contents = GetFileLines( request_data, filepath )

# We might have already extracted the contents
if contents is None:
contents = GetFileLines( request_data, filepath )

chunks = [ responses.FixItChunk( e[ 'newText' ],
_BuildRange( contents,
filepath,
e[ 'range' ] ) )
for e in additional_text_edits ]

fixits = responses.BuildFixItResponse(
[ responses.FixIt( chunks[ 0 ].range.start_, chunks ) ] )
fixits.append( responses.FixIt( chunks[ 0 ].range.start_, chunks ) )

extra_data = responses.BuildFixItResponse( fixits ) if fixits else None

if insertion_text_is_snippet:
return label, insertion_text, extra_data, start_codepoint

return insertion_text, fixits, start_codepoint
return insertion_text, None, extra_data, start_codepoint


def FindOverlapLength( line_value, insertion_text ):
Expand Down
17 changes: 12 additions & 5 deletions ycmd/completers/language_server/language_server_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@

from ycmd.utils import ( ByteOffsetToCodepointOffset,
ToBytes,
ToUnicode )
ToUnicode,
UpdateDict )


Error = collections.namedtuple( 'RequestError', [ 'code', 'reason' ] )
Expand Down Expand Up @@ -223,16 +224,22 @@ 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', {
'processId': os.getpid(),
'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': {
Expand Down Expand Up @@ -278,7 +285,7 @@ def Initialize( request_id, project_directory, settings ):
},
},
},
},
}, capabilities ),
} )


Expand Down
6 changes: 4 additions & 2 deletions ycmd/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,12 @@ class FixIt:
must be byte offsets into the UTF-8 encoded version of the appropriate
buffer.
"""
def __init__( self, location, chunks, text = '' ):
def __init__( self, location, chunks, text = '', is_completion = False ):
"""location of type Location, chunks of type list<FixItChunk>"""
self.location = location
self.chunks = chunks
self.text = text
self.is_completion = is_completion


class FixItChunk:
Expand Down Expand Up @@ -297,7 +298,8 @@ def BuildFixItData( fixit ):
'location': BuildLocationData( fixit.location ),
'chunks' : [ BuildFixitChunkData( x ) for x in fixit.chunks ],
'text': fixit.text,
'resolve': False
'resolve': False,
'is_completion': fixit.is_completion,
}

return {
Expand Down
44 changes: 44 additions & 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 Expand Up @@ -535,3 +536,46 @@ def AbsoluatePath( path, relative_to ):
path = os.path.join( relative_to, path )

return os.path.normpath( path )


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 override.items():
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

0 comments on commit 59a4759

Please sign in to comment.