From fe0c0a1607f91127ad44134f54a4ac2e795fecd7 Mon Sep 17 00:00:00 2001 From: Strahinja Val Markovic Date: Fri, 27 Sep 2013 13:52:04 -0700 Subject: [PATCH] GoTo commands work again --- autoload/youcompleteme.vim | 22 +++--- python/ycm/client/base_request.py | 10 +++ python/ycm/client/command_request.py | 76 +++++++++++-------- python/ycm/client/completion_request.py | 1 + python/ycm/completers/completer.py | 2 +- python/ycm/completers/cpp/clang_completer.py | 28 ++----- python/ycm/completers/cs/cs_completer.py | 1 + .../general/general_completer_store.py | 4 + .../ycm/completers/python/jedi_completer.py | 11 +-- python/ycm/server/responses.py | 8 ++ python/ycm/server/server_state.py | 10 ++- python/ycm/server/tests/basic_test.py | 35 +++++++++ python/ycm/server/ycmd.py | 18 +++-- python/ycm/vimsupport.py | 2 +- 14 files changed, 149 insertions(+), 79 deletions(-) diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 3ce1fbd2..ed1577a3 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -585,33 +585,27 @@ endfunction command! YcmDebugInfo call s:DebugInfo() + function! s:CompleterCommand(...) " CompleterCommand will call the OnUserCommand function of a completer. " If the first arguments is of the form "ft=..." it can be used to specify the " completer to use (for example "ft=cpp"). Else the native filetype completer " of the current buffer is used. If no native filetype completer is found and - " no completer was specified this throws an error. You can use "ft=ycm:omni" - " to select the omni completer or "ft=ycm:ident" to select the identifier - " completer. The remaining arguments will passed to the completer. + " no completer was specified this throws an error. You can use + " "ft=ycm:ident" to select the identifier completer. + " The remaining arguments will be passed to the completer. let arguments = copy(a:000) let completer = '' if a:0 > 0 && strpart(a:1, 0, 3) == 'ft=' - if a:1 == 'ft=ycm:omni' - let completer = 'omni' - elseif a:1 == 'ft=ycm:ident' + if a:1 == 'ft=ycm:ident' let completer = 'identifier' endif let arguments = arguments[1:] endif -py << EOF -response = ycm_state.SendCommandRequest( vim.eval( 'l:arguments' ), - vim.eval( 'l:completer' ) ) -if not response.Valid(): - vimsupport.PostVimMessage( 'No native completer found for current buffer. ' + - 'Use ft=... as the first argument to specify a completer.') -EOF + py ycm_state.SendCommandRequest( vim.eval( 'l:arguments' ), + \ vim.eval( 'l:completer' ) ) endfunction @@ -625,6 +619,8 @@ function! youcompleteme#OpenGoToList() endfunction +command! -nargs=* YcmCompleter call s:CompleterCommand() + " TODO: Make this work again " command! -nargs=* -complete=custom,youcompleteme#SubCommandsComplete " \ YcmCompleter call s:CompleterCommand() diff --git a/python/ycm/client/base_request.py b/python/ycm/client/base_request.py index 1b922973..84b95e81 100644 --- a/python/ycm/client/base_request.py +++ b/python/ycm/client/base_request.py @@ -24,6 +24,11 @@ from ycm import vimsupport HEADERS = {'content-type': 'application/json'} +class ServerError( Exception ): + def __init__( self, message ): + super( ServerError, self ).__init__( message ) + + class BaseRequest( object ): def __init__( self ): pass @@ -46,8 +51,13 @@ class BaseRequest( object ): response = requests.post( _BuildUri( handler ), data = json.dumps( data ), headers = HEADERS ) + if response.status_code == requests.codes.server_error: + raise ServerError( response.json()[ 'message' ] ) + # We let Requests handle the other status types, we only handle the 500 + # error code. response.raise_for_status() + if response.text: return response.json() return None diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index 7e830c68..4c2630fc 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -17,26 +17,23 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +import vim import time -from ycm.client.base_request import BaseRequest, BuildRequestData +from ycm.client.base_request import BaseRequest, BuildRequestData, ServerError +from ycm import vimsupport +from ycm.utils import ToUtf8IfNeeded class CommandRequest( BaseRequest ): - class ServerResponse( object ): - def __init__( self ): - pass - - def Valid( self ): - return True - def __init__( self, arguments, completer_target = None ): super( CommandRequest, self ).__init__() self._arguments = arguments self._completer_target = ( completer_target if completer_target else 'filetype_default' ) - # TODO: Handle this case. - # if completer_target == 'omni': - # completer = SERVER_STATE.GetOmniCompleter() + self._is_goto_command = ( + True if arguments and arguments[ 0 ].startswith( 'GoTo' ) else False ) + self._response = None + def Start( self ): request_data = BuildRequestData() @@ -44,34 +41,51 @@ class CommandRequest( BaseRequest ): 'completer_target': self._completer_target, 'command_arguments': self._arguments } ) - self._response = self.PostDataToHandler( request_data, - 'run_completer_command' ) + try: + self._response = self.PostDataToHandler( request_data, + 'run_completer_command' ) + except ServerError as e: + vimsupport.PostVimMessage( e ) def Response( self ): - # TODO: Call vimsupport.JumpToLocation if the user called a GoTo command... - # we may want to have specific subclasses of CommandRequest so that a - # GoToRequest knows it needs to jump after the data comes back. - # - # Also need to run the following on GoTo data: - # - # CAREFUL about line/column number 0-based/1-based confusion! - # - # defs = [] - # defs.append( {'filename': definition.module_path.encode( 'utf-8' ), - # 'lnum': definition.line, - # 'col': definition.column + 1, - # 'text': definition.description.encode( 'utf-8' ) } ) - # vim.eval( 'setqflist( %s )' % repr( defs ) ) - # vim.eval( 'youcompleteme#OpenGoToList()' ) - return self.ServerResponse() + return self._response -def SendCommandRequest( self, arguments, completer ): - request = CommandRequest( self, arguments, completer ) + def RunPostCommandActionsIfNeeded( self ): + if not self._is_goto_command or not self.Done() or not self._response: + return + + if isinstance( self._response, list ): + defs = [ _BuildQfListItem( x ) for x in self._response ] + vim.eval( 'setqflist( %s )' % repr( defs ) ) + vim.eval( 'youcompleteme#OpenGoToList()' ) + else: + vimsupport.JumpToLocation( self._response[ 'filepath' ], + self._response[ 'line_num' ] + 1, + self._response[ 'column_num' ] ) + + + + +def SendCommandRequest( arguments, completer ): + request = CommandRequest( arguments, completer ) request.Start() while not request.Done(): time.sleep( 0.1 ) + request.RunPostCommandActionsIfNeeded() return request.Response() + +def _BuildQfListItem( goto_data_item ): + qf_item = {} + if 'filepath' in goto_data_item: + qf_item[ 'filename' ] = ToUtf8IfNeeded( goto_data_item[ 'filepath' ] ) + if 'description' in goto_data_item: + qf_item[ 'text' ] = ToUtf8IfNeeded( goto_data_item[ 'description' ] ) + if 'line_num' in goto_data_item: + qf_item[ 'lnum' ] = goto_data_item[ 'line_num' ] + 1 + if 'column_num' in goto_data_item: + qf_item[ 'col' ] = goto_data_item[ 'column_num' ] + return qf_item diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index b3ff943f..076f26ac 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -30,6 +30,7 @@ class CompletionRequest( BaseRequest ): self._request_data = BuildRequestData( self._completion_start_column ) + # TODO: Do we need this anymore? # def ShouldComplete( self ): # return ( self._do_filetype_completion or # self._ycm_state.ShouldUseGeneralCompleter( self._request_data ) ) diff --git a/python/ycm/completers/completer.py b/python/ycm/completers/completer.py index 84411d95..c66b7564 100644 --- a/python/ycm/completers/completer.py +++ b/python/ycm/completers/completer.py @@ -198,7 +198,7 @@ class Completer( object ): '\n'.join( subcommands ) + '\nSee the docs for information on what they do.' ) else: - return 'No supported subcommands' + return 'This Completer has no supported subcommands.' def FilterAndSortCandidates( self, candidates, query ): diff --git a/python/ycm/completers/cpp/clang_completer.py b/python/ycm/completers/cpp/clang_completer.py index d2fd8b17..a4028f5a 100644 --- a/python/ycm/completers/cpp/clang_completer.py +++ b/python/ycm/completers/cpp/clang_completer.py @@ -152,7 +152,8 @@ class ClangCompleter( Completer ): elif command == 'GoToDefinitionElseDeclaration': return self._GoToDefinitionElseDeclaration( request_data ) elif command == 'ClearCompilationFlagCache': - self._ClearCompilationFlagCache( request_data ) + return self._ClearCompilationFlagCache( request_data ) + raise ValueError( self.UserCommandsHelpMessage() ) def _LocationForGoTo( self, goto_function, request_data ): @@ -170,7 +171,7 @@ class ClangCompleter( Completer ): files = self.GetUnsavedFilesVector( request_data ) line = request_data[ 'line_num' ] + 1 - column = request_data[ 'start_column' ] + 1 + column = request_data[ 'column_num' ] + 1 return getattr( self.completer, goto_function )( ToUtf8IfNeeded( filename ), line, @@ -180,7 +181,7 @@ class ClangCompleter( Completer ): def _GoToDefinition( self, request_data ): - location = self._LocationForGoTo( 'GetDefinitionLocation' ) + location = self._LocationForGoTo( 'GetDefinitionLocation', request_data ) if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to definition.' ) @@ -190,7 +191,7 @@ class ClangCompleter( Completer ): def _GoToDeclaration( self, request_data ): - location = self._LocationForGoTo( 'GetDeclarationLocation' ) + location = self._LocationForGoTo( 'GetDeclarationLocation', request_data ) if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to declaration.' ) @@ -200,9 +201,9 @@ class ClangCompleter( Completer ): def _GoToDefinitionElseDeclaration( self, request_data ): - location = self._LocationForGoTo( 'GetDefinitionLocation' ) + location = self._LocationForGoTo( 'GetDefinitionLocation', request_data ) if not location or not location.IsValid(): - location = self._LocationForGoTo( 'GetDeclarationLocation' ) + location = self._LocationForGoTo( 'GetDeclarationLocation', request_data ) if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to definition or declaration.' ) @@ -325,20 +326,7 @@ class ClangCompleter( Completer ): source, list( flags ) ) ) - -# TODO: make these functions module-local -# def CompletionDataToDict( completion_data ): -# # see :h complete-items for a description of the dictionary fields -# return { -# 'word' : completion_data.TextToInsertInBuffer(), -# 'abbr' : completion_data.MainCompletionText(), -# 'menu' : completion_data.ExtraMenuInfo(), -# 'kind' : completion_data.kind_, -# 'info' : completion_data.DetailedInfoForPreviewWindow(), -# 'dup' : 1, -# } - - +# TODO: Make this work again # def DiagnosticToDict( diagnostic ): # # see :h getqflist for a description of the dictionary fields # return { diff --git a/python/ycm/completers/cs/cs_completer.py b/python/ycm/completers/cs/cs_completer.py index 0f902c27..4943ccae 100755 --- a/python/ycm/completers/cs/cs_completer.py +++ b/python/ycm/completers/cs/cs_completer.py @@ -95,6 +95,7 @@ class CsharpCompleter( ThreadedCompleter ): 'GoToDeclaration', 'GoToDefinitionElseDeclaration' ]: return self._GoToDefinition( request_data ) + raise ValueError( self.UserCommandsHelpMessage() ) def DebugInfo( self ): diff --git a/python/ycm/completers/general/general_completer_store.py b/python/ycm/completers/general/general_completer_store.py index 3c80a7b7..850b39ed 100644 --- a/python/ycm/completers/general/general_completer_store.py +++ b/python/ycm/completers/general/general_completer_store.py @@ -51,6 +51,10 @@ class GeneralCompleterStore( Completer ): return set() + def GetIdentifierCompleter( self ): + return self._identifier_completer + + def ShouldUseNow( self, request_data ): self._current_query_completers = [] diff --git a/python/ycm/completers/python/jedi_completer.py b/python/ycm/completers/python/jedi_completer.py index b64a95e4..ea9113a0 100644 --- a/python/ycm/completers/python/jedi_completer.py +++ b/python/ycm/completers/python/jedi_completer.py @@ -88,6 +88,7 @@ class JediCompleter( ThreadedCompleter ): return self._GoToDeclaration( request_data ) elif command == 'GoToDefinitionElseDeclaration': return self._GoToDefinitionElseDeclaration( request_data ) + raise ValueError( self.UserCommandsHelpMessage() ) def _GoToDefinition( self, request_data ): @@ -107,8 +108,8 @@ class JediCompleter( ThreadedCompleter ): def _GoToDefinitionElseDeclaration( self, request_data ): - definitions = self._GetDefinitionsList() or \ - self._GetDefinitionsList( request_data, declaration = True ) + definitions = ( self._GetDefinitionsList( request_data ) or + self._GetDefinitionsList( request_data, declaration = True ) ) if definitions: return self._BuildGoToResponse( definitions ) else: @@ -141,7 +142,7 @@ class JediCompleter( ThreadedCompleter ): raise RuntimeError( 'Builtin modules cannot be displayed.' ) else: return responses.BuildGoToResponse( definition.module_path, - definition.line -1, + definition.line - 1, definition.column ) else: # multiple definitions @@ -149,11 +150,11 @@ class JediCompleter( ThreadedCompleter ): for definition in definition_list: if definition.in_builtin_module(): defs.append( responses.BuildDescriptionOnlyGoToResponse( - 'Builting ' + definition.description ) ) + 'Builtin ' + definition.description ) ) else: defs.append( responses.BuildGoToResponse( definition.module_path, - definition.line -1, + definition.line - 1, definition.column, definition.description ) ) return defs diff --git a/python/ycm/server/responses.py b/python/ycm/server/responses.py index a6544d4b..6be83ce5 100644 --- a/python/ycm/server/responses.py +++ b/python/ycm/server/responses.py @@ -76,3 +76,11 @@ def BuildDiagnosticData( filepath, 'text': text, 'kind': kind } + + +def BuildExceptionResponse( error_message, traceback ): + return { + 'message': error_message, + 'traceback': traceback + } + diff --git a/python/ycm/server/server_state.py b/python/ycm/server/server_state.py index 1d1c1572..afcf6c32 100644 --- a/python/ycm/server/server_state.py +++ b/python/ycm/server/server_state.py @@ -74,12 +74,16 @@ class ServerState( object ): if completer: return completer - return None + raise ValueError( 'No semantic completer exists for filetypes: {}'.format( + current_filetypes ) ) def FiletypeCompletionAvailable( self, filetypes ): - return bool( self.GetFiletypeCompleter( filetypes ) ) - + try: + self.GetFiletypeCompleter( filetypes ) + return True + except: + return False def FiletypeCompletionUsable( self, filetypes ): return ( self.CurrentFiletypeCompletionEnabled( filetypes ) and diff --git a/python/ycm/server/tests/basic_test.py b/python/ycm/server/tests/basic_test.py index db0bbfed..3f2b6214 100644 --- a/python/ycm/server/tests/basic_test.py +++ b/python/ycm/server/tests/basic_test.py @@ -120,6 +120,41 @@ def GetCompletions_UltiSnipsCompleter_Works_test(): app.post_json( '/get_completions', completion_data ).json ) +@with_setup( Setup ) +def RunCompleterCommand_GoTo_Jedi_ZeroBasedLineAndColumn_test(): + app = TestApp( ycmd.app ) + contents = """ +def foo(): + pass + +foo() +""" + + goto_data = { + 'completer_target': 'filetype_default', + 'command_arguments': ['GoToDefinition'], + 'line_num': 4, + 'column_num': 0, + 'filetypes': ['python'], + 'filepath': '/foo.py', + 'line_value': contents, + 'file_data': { + '/foo.py': { + 'contents': contents, + 'filetypes': ['python'] + } + } + } + + # 0-based line and column! + eq_( { + 'filepath': '/foo.py', + 'line_num': 1, + 'column_num': 4 + }, + app.post_json( '/run_completer_command', goto_data ).json ) + + @with_setup( Setup ) def FiletypeCompletionAvailable_Works_test(): app = TestApp( ycmd.app ) diff --git a/python/ycm/server/ycmd.py b/python/ycm/server/ycmd.py index 2b6df431..c0284c15 100755 --- a/python/ycm/server/ycmd.py +++ b/python/ycm/server/ycmd.py @@ -37,6 +37,7 @@ import bottle from bottle import run, request, response import server_state from ycm import user_options_store +from ycm.server.responses import BuildExceptionResponse import argparse # num bytes for the request body buffer; request.json only works if the request @@ -71,13 +72,13 @@ def RunCompleterCommand(): completer_target = request_data[ 'completer_target' ] if completer_target == 'identifier': - completer = SERVER_STATE.GetGeneralCompleter() + completer = SERVER_STATE.GetGeneralCompleter().GetIdentifierCompleter() else: - completer = SERVER_STATE.GetFiletypeCompleter() + completer = SERVER_STATE.GetFiletypeCompleter( request_data[ 'filetypes' ] ) - return _JsonResponse( - completer.OnUserCommand( request_data[ 'command_arguments' ], - request_data ) ) + return _JsonResponse( completer.OnUserCommand( + request_data[ 'command_arguments' ], + request_data ) ) @app.post( '/get_completions' ) @@ -123,6 +124,13 @@ def FiletypeCompletionAvailable(): request.json[ 'filetypes' ] ) ) +# The type of the param is Bottle.HTTPError +@app.error( 500 ) +def ErrorHandler( httperror ): + return _JsonResponse( BuildExceptionResponse( str( httperror.exception ), + httperror.traceback ) ) + + def _JsonResponse( data ): response.set_header( 'Content-Type', 'application/json' ) return json.dumps( data ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 2314be72..bb2e20ec 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -105,7 +105,7 @@ def PostVimMessage( message ): # non-GUI thread which *sometimes* crashes Vim because Vim is not thread-safe. # A consistent crash should force us to notice the error. vim.command( "echohl WarningMsg | echomsg '{0}' | echohl None" - .format( EscapeForVim( message ) ) ) + .format( EscapeForVim( str( message ) ) ) ) def PresentDialog( message, choices, default_choice_index = 0 ):