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
Ben Jackson committed Jan 10, 2020
2 parents 2d87993 + dc4b5d9 commit a7a387f
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 57 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
161 changes: 115 additions & 46 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,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 @@ -1077,6 +1077,7 @@ def _CandidatesFromCompletionItems( self, items, resolve, request_data ):
# we might modify insertion_text should we see a lower start codepoint.
completions.append( _CompletionItemToCompletionData(
insertion_text,
snippet,
item,
extra_data ) )
start_codepoints.append( start_codepoint )
Expand Down Expand Up @@ -1283,12 +1284,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 ):
Expand Down Expand Up @@ -1709,7 +1716,8 @@ def _SendInitialize( self, request_data ):
# 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:
Expand Down Expand Up @@ -1822,7 +1830,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
Expand Down Expand Up @@ -2222,7 +2230,7 @@ def _DistanceOfPointToRange( point, range ):
return 0


def _CompletionItemToCompletionData( insertion_text, item, fixits ):
def _CompletionItemToCompletionData( insertion_text, snippet, item, fixits ):
# Since we send completionItemKind capabilities, we guarantee to handle
# values outside our value set and fall back to a default.
try:
Expand All @@ -2240,7 +2248,8 @@ def _CompletionItemToCompletionData( insertion_text, item, fixits ):
detailed_info = item[ 'label' ] + '\n\n' + documentation,
menu_text = item[ 'label' ],
kind = kind,
extra_data = fixits )
extra_data = fixits,
snippet = snippet )


def _FixUpCompletionPrefixes( completions,
Expand Down Expand Up @@ -2278,59 +2287,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 @@ -2339,21 +2402,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 @@ -34,7 +34,8 @@
unquote,
url2pathname,
urlparse,
urljoin )
urljoin,
UpdateDict )


Error = collections.namedtuple( 'RequestError', [ 'code', 'reason' ] )
Expand Down Expand Up @@ -233,16 +234,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 @@ -288,7 +295,7 @@ def Initialize( request_id, project_directory, settings ):
},
},
},
},
}, capabilities ),
} )


Expand Down
14 changes: 10 additions & 4 deletions ycmd/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ def BuildCompletionData( insertion_text,
detailed_info = None,
menu_text = None,
kind = None,
extra_data = None ):
extra_data = None,
snippet = None ):
completion_data = {
'insertion_text': insertion_text
}
Expand All @@ -131,6 +132,8 @@ def BuildCompletionData( insertion_text,
completion_data[ 'kind' ] = kind
if extra_data:
completion_data[ 'extra_data' ] = extra_data
if snippet:
completion_data[ 'snippet' ] = snippet
return completion_data


Expand Down Expand Up @@ -202,11 +205,12 @@ class FixIt( object ):
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( object ):
Expand Down Expand Up @@ -298,14 +302,16 @@ def BuildFixItData( fixit ):
return {
'command': fixit.command,
'text': fixit.text,
'resolve': fixit.resolve
'resolve': fixit.resolve,
'is_completion': fixit.is_completion
}
else:
return {
'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
Loading

0 comments on commit a7a387f

Please sign in to comment.