Skip to content
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

[READY] Remap GoTo commands in Python completer #1275

Merged
Merged
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
138 changes: 88 additions & 50 deletions ycmd/completers/python/python_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from ycmd import extra_conf_store, responses
from ycmd.completers.completer import Completer
from ycmd.completers.completer_utils import GetFileContents
from ycmd.utils import ExpandVariablesInPath, FindExecutable, LOGGER

import os
Expand Down Expand Up @@ -162,12 +163,27 @@ def _GetJediScript( self, request_data ):
line = request_data[ 'line_num' ]
# Jedi expects columns to start at 0, not 1, and for them to be Unicode
# codepoint offsets.
col = request_data[ 'start_codepoint' ] - 1
column = request_data[ 'start_codepoint' ] - 1
environment = self._EnvironmentForRequest( request_data )
sys_path = self._SysPathForFile( request_data, environment )
return jedi.Script( source,
line,
col,
column,
path,
sys_path = sys_path,
environment = environment )


def _GetJediScriptForDefinition( self, request_data, definition ):
path = definition.module_path
source = GetFileContents( request_data, path )
line = definition.line
column = definition.column
environment = self._EnvironmentForRequest( request_data )
sys_path = self._SysPathForFile( request_data, environment )
return jedi.Script( source,
line,
column,
path,
sys_path = sys_path,
environment = environment )
Expand Down Expand Up @@ -212,12 +228,14 @@ def DetailCandidates( self, request_data, candidates ):

def GetSubcommandsMap( self ):
return {
'GoTo' : ( lambda self, request_data, args:
self._GoToDefinition( request_data ) ),
'GoToDefinition' : ( lambda self, request_data, args:
self._GoToDefinition( request_data ) ),
'GoToDeclaration': ( lambda self, request_data, args:
self._GoToDeclaration( request_data ) ),
'GoTo' : ( lambda self, request_data, args:
self._GoTo( request_data ) ),
self._GoToDefinition( request_data ) ),
'GoToType' : ( lambda self, request_data, args:
self._GoToType( request_data ) ),
'GoToReferences' : ( lambda self, request_data, args:
self._GoToReferences( request_data ) ),
'GetType' : ( lambda self, request_data, args:
Expand All @@ -227,33 +245,83 @@ def GetSubcommandsMap( self ):
}


def _BuildGoToResponse( self, definitions ):
if len( definitions ) == 1:
definition = definitions[ 0 ]
if definition.in_builtin_module():
raise RuntimeError( 'Can\'t jump to builtin module.' )
return responses.BuildGoToResponse( definition.module_path,
definition.line,
definition.column + 1 )

gotos = []
for definition in definitions:
if definition.in_builtin_module():
gotos.append( responses.BuildDescriptionOnlyGoToResponse(
'Builtin {}'.format( definition.description ) ) )
else:
gotos.append( responses.BuildGoToResponse( definition.module_path,
definition.line,
definition.column + 1,
definition.description ) )
return gotos


def _GoToDefinition( self, request_data ):
def _GoToDefinitionsWithSameName( definition ):
module_path = definition.module_path
if not module_path:
return []
name = definition.name
line = definition.line
column = definition.column

definitions = self._GetJediScriptForDefinition(
request_data, definition ).goto_assignments()
return [ d for d in definitions if d.name == name and
( d.module_path != module_path or
d.line != line or
d.column != column ) ]

with self._jedi_lock:
definitions = self._GetJediScript( request_data ).goto_definitions()
# Jedi's goto_assignments() does not jump to a different file unless the
# cursor is on an import statement or if the follow_imports parameter is
# set to True. Unfortunately, we can't use that option because it also
# jumps to the original name of aliases which is unexpected as an alias is
# a kind of definition. So, we need to traverse definitions by repeatedly
# calling goto_assignments() until we can't jump anymore.
definitions = self._GetJediScript( request_data ).goto_assignments()
keep_traversing = True
while keep_traversing:
keep_traversing = False
previous_definitions = definitions
definitions = []
for definition in previous_definitions:
goto_definitions = _GoToDefinitionsWithSameName( definition )
if goto_definitions:
definitions.extend( goto_definitions )
keep_traversing = True
else:
definitions.append( definition )
if definitions:
return self._BuildGoToResponse( definitions )
raise RuntimeError( 'Can\'t jump to definition.' )


def _GoToDeclaration( self, request_data ):
def _GoToType( self, request_data ):
with self._jedi_lock:
definitions = self._GetJediScript( request_data ).goto_assignments()
definitions = self._GetJediScript( request_data ).goto_definitions()
if definitions:
return self._BuildGoToResponse( definitions )
raise RuntimeError( 'Can\'t jump to declaration.' )
raise RuntimeError( 'Can\'t jump to type definition.' )


def _GoTo( self, request_data ):
try:
return self._GoToDefinition( request_data )
except Exception:
LOGGER.exception( 'Failed to jump to definition' )

try:
return self._GoToDeclaration( request_data )
except Exception:
LOGGER.exception( 'Failed to jump to declaration' )
raise RuntimeError( 'Can\'t jump to definition or declaration.' )
def _GoToReferences( self, request_data ):
with self._jedi_lock:
definitions = self._GetJediScript( request_data ).usages()
if definitions:
return self._BuildGoToResponse( definitions )
raise RuntimeError( 'Can\'t find references.' )


# This method must be called under Jedi's lock.
Expand Down Expand Up @@ -291,36 +359,6 @@ def _GetDoc( self, request_data ):
raise RuntimeError( 'No documentation available.' )


def _GoToReferences( self, request_data ):
with self._jedi_lock:
definitions = self._GetJediScript( request_data ).usages()
if definitions:
return self._BuildGoToResponse( definitions )
raise RuntimeError( 'Can\'t find references.' )


def _BuildGoToResponse( self, definitions ):
if len( definitions ) == 1:
definition = definitions[ 0 ]
if definition.in_builtin_module():
raise RuntimeError( 'Can\'t jump to builtin module.' )
return responses.BuildGoToResponse( definition.module_path,
definition.line,
definition.column + 1 )

gotos = []
for definition in definitions:
if definition.in_builtin_module():
gotos.append( responses.BuildDescriptionOnlyGoToResponse(
'Builtin {}'.format( definition.description ) ) )
else:
gotos.append( responses.BuildGoToResponse( definition.module_path,
definition.line,
definition.column + 1,
definition.description ) )
return gotos


def DebugInfo( self, request_data ):
environment = self._EnvironmentForRequest( request_data )

Expand Down
Loading