Skip to content

[RFC] WIP: Signature help #1255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/_config.yml

This file was deleted.

14 changes: 7 additions & 7 deletions docs/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/bundle.js.map

Large diffs are not rendered by default.

375 changes: 374 additions & 1 deletion docs/index.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/main.css.map

Large diffs are not rendered by default.

127 changes: 126 additions & 1 deletion docs/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,131 @@ paths:
schema:
$ref: "#/definitions/ExceptionResponse"

/signature_help:
post:
summary: Get signature help (argument hints) for current file context.
description: |-
Returns a list of method signatures relevant to the current cursor
position, along with an indication of which signature and argument
are 'active'.

Only returned by semantic engines.

Signatures are actually queried immediately after a trigger character.
The result of this is that the client and server collude on remembering
the current "trigger" state for signature help. This trades off
some complexity in clients for significant performance benefits in the
general case.

The client is responsible for maintaining the state of any displayed
signature help popup, and is responsible for hiding it when this request
returns an empty set of signatures.

That is, when this request returns a non-empty list of signatures,
signature help is considered "triggered' and the client must record this
and return it in the 'signature_help_state' request field
(value `ACTIVE`). The client continues to request signature help on
new input and should update its state to `INACTIVE` whenever this
request returns an empty list of signatures.

If any errors are reported by the semantic engine, they are reported in
the `errors` key in the response.

The first signature and the first argument are both 0.

Identifying the position of an argument in the `label` for a signature
required matching the position of the text supplied in the `label` for
the active parameter of the active signature.
produces:
- application/json
parameters:
- name: request_data
in: body
description: |-
The context data, including the current cursor position, and details
of dirty buffers.
required: true
schema:
type: object
required:
- line_num
- column_num
- filepath
- file_data
properties:
line_num:
$ref: "#/definitions/LineNumber"
column_num:
$ref: "#/definitions/ColumnNumber"
filepath:
$ref: "#/definitions/FilePath"
file_data:
$ref: "#/definitions/FileDataMap"
completer_target:
$ref: "#/definitions/CompleterTarget"
extra_conf_data:
$ref: "#/definitions/ExtraConfData"
signature_help_state:
type: string
enum:
- ACTIVE
- INACTIVE
description: |-
The current state of the signature help triggering. After
signatures are returned for the first time (triggered), the
client sends further requests with `signature_help_state` set
to `ACTIVE` until the request returns no signatures.
responses:
200:
description: Signature info.
schema:
type: object
required:
- signature_help
- errors
properties:
errors:
type: array
description: errors reported by the semantic completion engine.
items:
$ref: "#/definitions/ExceptionResponse"
signature_help:
type: object
required:
- activeSignature
- activeParameter
- signatures
properties:
activeSignature:
type: number
description: The active signature. The first is 0.
activeParameter:
type: number
description: The active parameter. The first is 0.
signatures:
type: array
description: The list of signatures
items:
type: object
properties:
label:
description: |
The full signature text, including parameters
type: string
parameters:
description: List of parameters
type: array
items:
type: string
required:
- label
- parameters

500:
description: An error occurred.
schema:
$ref: "#/definitions/ExceptionResponse"

/filter_and_sort_candidates:
post:
summary: Filter and sort a set of candidates using ycmd's fuzzy matching.
Expand Down Expand Up @@ -1273,7 +1398,7 @@ paths:
method, but it is strongly recommended for certain languages to offer
the best user experience.
produces:
application/json
- application/json
parameters:
- name: request_data
in: body
Expand Down
56 changes: 53 additions & 3 deletions ycmd/completers/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ def __init__( self, user_options ):
user_trigger_map = user_options[ 'semantic_triggers' ],
filetype_set = set( self.SupportedFiletypes() ) )
if user_options[ 'auto_trigger' ] else None )

self.signature_triggers = (
completer_utils.PreparedTriggers(
user_trigger_map = {}, # user triggers not supported for signature help
filetype_set = set( self.SupportedFiletypes() ),
default_triggers = {} )
if not user_options[ 'disable_signature_help' ] else None )

self._completions_cache = CompletionsCache()
self._max_candidates = user_options[ 'max_num_candidates' ]

Expand Down Expand Up @@ -208,13 +216,44 @@ def ShouldUseNow( self, request_data ):
def ShouldUseNowInner( self, request_data ):
if not self.completion_triggers:
return False

current_line = request_data[ 'line_value' ]
start_codepoint = request_data[ 'start_codepoint' ] - 1
column_codepoint = request_data[ 'column_codepoint' ] - 1
filetype = self._CurrentFiletype( request_data[ 'filetypes' ] )

return self.completion_triggers.MatchesForFiletype(
current_line, start_codepoint, column_codepoint, filetype )
return self.completion_triggers.MatchesForFiletype( current_line,
start_codepoint,
column_codepoint,
filetype )


def ShouldUseSignatureHelpNow( self, request_data ):
if self.user_options[ 'disable_signature_help' ]:
return False

if not self.signature_triggers:
return False

state = request_data.get( 'signature_help_state', 'INACTIVE' )

current_line = request_data[ 'line_value' ]
# Note: We use the cursor column for all triggering of signature help, not
# the calculated "start" codepoint. This is because start_codepoint is based
# on the completion triggers, not the signature_triggers.
column_codepoint = request_data[ 'column_codepoint' ] - 1
filetype = self._CurrentFiletype( request_data[ 'filetypes' ] )

if state == 'ACTIVE':
# Signature help is already active (the menu is displayed), always
# re-trigger until we return no signatures (and the client thus closes
# the menu and returns state 'INACTIVE').
return True

return self.signature_triggers.MatchesForFiletype( current_line,
column_codepoint,
column_codepoint,
filetype )


def QueryLengthAboveMinThreshold( self, request_data ):
Expand Down Expand Up @@ -255,7 +294,18 @@ def DetailCandidates( self, request_data, candidates ):


def ComputeCandidatesInner( self, request_data ):
pass # pragma: no cover
return [] # pragma: no cover


def ComputeSignatures( self, request_data ):
if not self.ShouldUseSignatureHelpNow( request_data ):
return {}

return self.ComputeSignaturesInner( request_data )


def ComputeSignaturesInner( self, request_data ):
return {}


def DefinedSubcommands( self ):
Expand Down
11 changes: 9 additions & 2 deletions ycmd/completers/completer_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@


class PreparedTriggers( object ):
def __init__( self, user_trigger_map = None, filetype_set = None ):
def __init__( self,
user_trigger_map = None,
filetype_set = None,
default_triggers = None ):
self._user_trigger_map = user_trigger_map
self._server_trigger_map = None
self._filetype_set = filetype_set
self._default_triggers = (
default_triggers if default_triggers is not None
else PREPARED_DEFAULT_FILETYPE_TRIGGERS
)

self._CombineTriggers()

Expand All @@ -48,7 +55,7 @@ def _CombineTriggers( self ):
defaultdict( set ) )

# Combine all of the defaults, server-supplied and user-supplied triggers
final_triggers = _FiletypeDictUnion( PREPARED_DEFAULT_FILETYPE_TRIGGERS,
final_triggers = _FiletypeDictUnion( self._default_triggers,
server_prepared_triggers,
user_prepared_triggers )

Expand Down
4 changes: 4 additions & 0 deletions ycmd/completers/java/java_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ def SupportedFiletypes( self ):
return [ 'java' ]


def GetSignatureTriggerCharacters( self, server_trigger_characters ):
return server_trigger_characters + [ ',' ]


def GetCustomSubcommands( self ):
return {
'FixIt': (
Expand Down
44 changes: 44 additions & 0 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,25 @@ def _CandidatesFromCompletionItems( self, items, resolve, request_data ):
return completions


def ComputeSignaturesInner( self, request_data ):
if not self.ServerIsReady():
return {}

if not self._server_capabilities.get( 'signatureHelpProvider' ):
return {}

self._UpdateServerWithFileContents( request_data )

request_id = self.GetConnection().NextRequestId()
msg = lsp.SignatureHelp( request_id, request_data )

response = self.GetConnection().GetResponse( request_id,
msg,
REQUEST_TIMEOUT_COMPLETION )

return response[ 'result' ]


def GetCustomSubcommands( self ):
"""Return a list of subcommand definitions to be used in conjunction with
the subcommands detected by _DiscoverSubcommandSupport. The return is a dict
Expand Down Expand Up @@ -1596,6 +1615,11 @@ def GetTriggerCharacters( self, server_trigger_characters ):
return server_trigger_characters


def GetSignatureTriggerCharacters( self, server_trigger_characters ):
"""Same as _GetTriggerCharacters but for signature help."""
return server_trigger_characters


def _HandleInitializeInPollThread( self, response ):
"""Called within the context of the LanguageServerConnection's message pump
when the initialize request receives a response."""
Expand Down Expand Up @@ -1644,6 +1668,26 @@ def _HandleInitializeInPollThread( self, response ):
self.completion_triggers.SetServerSemanticTriggers(
trigger_characters )

if self.signature_triggers is not None:
server_trigger_characters = (
( self._server_capabilities.get( 'signatureHelpProvider' ) or {} )
.get( 'triggerCharacters' ) or []
)
LOGGER.debug( '%s: Server declares signature trigger characters: %s',
self.Language(),
server_trigger_characters )

trigger_characters = self.GetSignatureTriggerCharacters(
server_trigger_characters )

if trigger_characters:
LOGGER.info( '%s: Using characters for signature triggers: %s',
self.Language(),
','.join( trigger_characters ) )

self.signature_triggers.SetServerSemanticTriggers(
trigger_characters )

# We must notify the server that we received the initialize response (for
# no apparent reason, other than that's what the protocol says).
self.GetConnection().SendNotification( lsp.Initialized() )
Expand Down
21 changes: 19 additions & 2 deletions ycmd/completers/language_server/language_server_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,19 @@ def Initialize( request_id, project_directory, settings ):
'plaintext',
'markdown'
]
}
}
},
'signatureHelp': {
'signatureInformation': {
'parameterInformation': {
'labelOffsetSupport': False, # For now.
},
'documentationFormat': [
'plaintext',
'markdown'
],
},
},
},
},
} )

Expand Down Expand Up @@ -344,6 +355,12 @@ def ResolveCompletion( request_id, completion ):
return BuildRequest( request_id, 'completionItem/resolve', completion )


def SignatureHelp( request_id, request_data ):
return BuildRequest( request_id,
'textDocument/signatureHelp',
BuildTextDocumentPositionParams( request_data ) )


def Hover( request_id, request_data ):
return BuildRequest( request_id,
'textDocument/hover',
Expand Down
Loading