diff --git a/README.md b/README.md index ff897136..e3d9a1d2 100644 --- a/README.md +++ b/README.md @@ -826,7 +826,7 @@ Quick Feature Summary **NOTE**: Java support is currently experimental. Please let us know your [feedback](#contact). -* Semantic auto-completion +* Semantic auto-completion with automatic import insertion * Go to definition (`GoTo`, `GoToDefinition`, and `GoToDeclaration` are identical) * Reference finding (`GoToReferences`) @@ -834,7 +834,7 @@ Quick Feature Summary * Renaming symbols (`RefactorRename `) * View documentation comments for identifiers (`GetDoc`) * Type information for identifiers (`GetType`) -* Automatically fix certain errors (`FixIt`) +* Automatically fix certain errors including code generation (`FixIt`) * Detection of java projects * Management of `jdt.ls` server instance @@ -1185,6 +1185,9 @@ package you have in the virtual environment. 4. Edit a Java file from your project. +For the best experience, we highly recommend at least Vim 8.0.1493 when using +Java support with YouCompleteMe. + #### Java Project Files In order to provide semantic analysis, the Java completion engine requires diff --git a/doc/youcompleteme.txt b/doc/youcompleteme.txt index d09392d0..6614911d 100644 --- a/doc/youcompleteme.txt +++ b/doc/youcompleteme.txt @@ -1074,7 +1074,7 @@ Java ~ **NOTE**: Java support is currently experimental. Please let us know your feedback. -- Semantic auto-completion +- Semantic auto-completion with automatic import insertion - Go to definition (|GoTo|, |GoToDefinition|, and |GoToDeclaration| are identical) - Reference finding (|GoToReferences|) @@ -1082,7 +1082,7 @@ feedback. - Renaming symbols ('RefactorRename ') - View documentation comments for identifiers (|GetDoc|) - Type information for identifiers (|GetType|) -- Automatically fix certain errors (|FixIt|) +- Automatically fix certain errors including code generation (|FixIt|) - Detection of java projects - Management of 'jdt.ls' server instance @@ -1454,6 +1454,9 @@ Java quick Start ~ 4. Edit a Java file from your project. +For the best experience, we highly recommend at least Vim 8.0.1493 when using +Java support with YouCompleteMe. + ------------------------------------------------------------------------------- *youcompleteme-java-project-files* Java Project Files ~ diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index 1b530823..7eb53067 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -69,7 +69,7 @@ class CompletionRequest( BaseRequest ): return response -def ConvertCompletionDataToVimData( completion_data ): +def ConvertCompletionDataToVimData( completion_identifier, completion_data ): # see :h complete-items for a description of the dictionary fields vim_data = { 'word' : '', @@ -100,9 +100,19 @@ def ConvertCompletionDataToVimData( completion_data ): elif doc_string: vim_data[ 'info' ] = doc_string + # We store the completion item index as a string in the completion user_data. + # This allows us to identify the _exact_ item that was completed in the + # CompleteDone handler, by inspecting this item from v:completed_item + # + # We convert to string because completion user data items must be strings. + # + # Note: Not all versions of Vim support this (added in 8.0.1483), but adding + # the item to the dictionary is harmless in earlier Vims. + vim_data[ 'user_data' ] = str( completion_identifier ) + return vim_data def _ConvertCompletionDatasToVimDatas( response_data ): - return [ ConvertCompletionDataToVimData( x ) - for x in response_data ] + return [ ConvertCompletionDataToVimData( i, x ) + for i, x in enumerate( response_data ) ] diff --git a/python/ycm/tests/client/completion_request_test.py b/python/ycm/tests/client/completion_request_test.py index 45881cf1..c2f0e870 100644 --- a/python/ycm/tests/client/completion_request_test.py +++ b/python/ycm/tests/client/completion_request_test.py @@ -33,8 +33,9 @@ class ConvertCompletionResponseToVimDatas_test( object ): """ This class tests the completion_request._ConvertCompletionResponseToVimDatas method """ - def _Check( self, completion_data, expected_vim_data ): + def _Check( self, completion_id, completion_data, expected_vim_data ): vim_data = completion_request.ConvertCompletionDataToVimData( + completion_id, completion_data ) try: @@ -48,7 +49,7 @@ class ConvertCompletionResponseToVimDatas_test( object ): def All_Fields_test( self ): - self._Check( { + self._Check( 0, { 'insertion_text': 'INSERTION TEXT', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', @@ -58,36 +59,38 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'doc_string': 'DOC STRING', }, }, { - 'word' : 'INSERTION TEXT', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DETAILED INFO\nDOC STRING', - 'dup' : 1, - 'empty': 1, + 'word' : 'INSERTION TEXT', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DETAILED INFO\nDOC STRING', + 'dup' : 1, + 'empty' : 1, + 'user_data': '0', } ) def Just_Detailed_Info_test( self ): - self._Check( { + self._Check( 9999999999, { 'insertion_text': 'INSERTION TEXT', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', 'kind': 'K', 'detailed_info': 'DETAILED INFO', }, { - 'word' : 'INSERTION TEXT', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DETAILED INFO', - 'dup' : 1, - 'empty': 1, + 'word' : 'INSERTION TEXT', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DETAILED INFO', + 'dup' : 1, + 'empty' : 1, + 'user_data': '9999999999', } ) def Just_Doc_String_test( self ): - self._Check( { + self._Check( 'not_an_int', { 'insertion_text': 'INSERTION TEXT', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', @@ -96,18 +99,19 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'doc_string': 'DOC STRING', }, }, { - 'word' : 'INSERTION TEXT', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DOC STRING', - 'dup' : 1, - 'empty': 1, + 'word' : 'INSERTION TEXT', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DOC STRING', + 'dup' : 1, + 'empty' : 1, + 'user_data': 'not_an_int', } ) def Extra_Info_No_Doc_String_test( self ): - self._Check( { + self._Check( 0, { 'insertion_text': 'INSERTION TEXT', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', @@ -115,17 +119,18 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'extra_data': { }, }, { - 'word' : 'INSERTION TEXT', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'dup' : 1, - 'empty': 1, + 'word' : 'INSERTION TEXT', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'dup' : 1, + 'empty' : 1, + 'user_data': '0', } ) def Extra_Info_No_Doc_String_With_Detailed_Info_test( self ): - self._Check( { + self._Check( '0', { 'insertion_text': 'INSERTION TEXT', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', @@ -134,18 +139,19 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'extra_data': { }, }, { - 'word' : 'INSERTION TEXT', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DETAILED INFO', - 'dup' : 1, - 'empty': 1, + 'word' : 'INSERTION TEXT', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DETAILED INFO', + 'dup' : 1, + 'empty' : 1, + 'user_data': '0', } ) def Empty_Insertion_Text_test( self ): - self._Check( { + self._Check( 0, { 'insertion_text': '', 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', @@ -155,18 +161,19 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'doc_string': 'DOC STRING', }, }, { - 'word' : '', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DETAILED INFO\nDOC STRING', - 'dup' : 1, - 'empty': 1, + 'word' : '', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DETAILED INFO\nDOC STRING', + 'dup' : 1, + 'empty' : 1, + 'user_data': '0', } ) def No_Insertion_Text_test( self ): - self._Check( { + self._Check( 0, { 'menu_text': 'MENU TEXT', 'extra_menu_info': 'EXTRA MENU INFO', 'kind': 'K', @@ -175,11 +182,12 @@ class ConvertCompletionResponseToVimDatas_test( object ): 'doc_string': 'DOC STRING', }, }, { - 'word' : '', - 'abbr' : 'MENU TEXT', - 'menu' : 'EXTRA MENU INFO', - 'kind' : 'k', - 'info' : 'DETAILED INFO\nDOC STRING', - 'dup' : 1, - 'empty': 1, + 'word' : '', + 'abbr' : 'MENU TEXT', + 'menu' : 'EXTRA MENU INFO', + 'kind' : 'k', + 'info' : 'DETAILED INFO\nDOC STRING', + 'dup' : 1, + 'empty' : 1, + 'user_data': '0' } ) diff --git a/python/ycm/tests/postcomplete_test.py b/python/ycm/tests/postcomplete_test.py index c953ca7e..c1e9883e 100644 --- a/python/ycm/tests/postcomplete_test.py +++ b/python/ycm/tests/postcomplete_test.py @@ -36,27 +36,43 @@ from ycm import vimsupport from ycm.tests import YouCompleteMeInstance from ycmd.utils import ToBytes +from ycm.youcompleteme import _CompleteDoneHook_CSharp +from ycm.youcompleteme import _CompleteDoneHook_Java + + +def CompleteItemIs( word, abbr = None, menu = None, + info = None, kind = None, **kwargs ): + item = { + 'word': ToBytes( word ), + 'abbr': ToBytes( abbr ), + 'menu': ToBytes( menu ), + 'info': ToBytes( info ), + 'kind': ToBytes( kind ), + } + item.update( **kwargs ) + return item + def GetVariableValue_CompleteItemIs( word, abbr = None, menu = None, - info = None, kind = None ): + info = None, kind = None, **kwargs ): def Result( variable ): if variable == 'v:completed_item': - return { - 'word': ToBytes( word ), - 'abbr': ToBytes( abbr ), - 'menu': ToBytes( menu ), - 'info': ToBytes( info ), - 'kind': ToBytes( kind ), - } + return CompleteItemIs( word, abbr, menu, info, kind, **kwargs ) return DEFAULT return MagicMock( side_effect = Result ) -def BuildCompletion( namespace = None, insertion_text = 'Test', - menu_text = None, extra_menu_info = None, - detailed_info = None, kind = None ): +def BuildCompletion( insertion_text = 'Test', + menu_text = None, + extra_menu_info = None, + detailed_info = None, + kind = None, + extra_data = None ): + if extra_data is None: + extra_data = {} + return { - 'extra_data': { 'required_namespace_import': namespace }, + 'extra_data': extra_data, 'insertion_text': insertion_text, 'menu_text': menu_text, 'extra_menu_info': extra_menu_info, @@ -65,141 +81,191 @@ def BuildCompletion( namespace = None, insertion_text = 'Test', } +def BuildCompletionNamespace( namespace = None, + insertion_text = 'Test', + menu_text = None, + extra_menu_info = None, + detailed_info = None, + kind = None ): + return BuildCompletion( insertion_text = insertion_text, + menu_text = menu_text, + extra_menu_info = extra_menu_info, + detailed_info = detailed_info, + kind = kind, + extra_data = { + 'required_namespace_import': namespace + } ) + + +def BuildCompletionFixIt( fixits, + insertion_text = 'Test', + menu_text = None, + extra_menu_info = None, + detailed_info = None, + kind = None ): + return BuildCompletion( insertion_text = insertion_text, + menu_text = menu_text, + extra_menu_info = extra_menu_info, + detailed_info = detailed_info, + kind = kind, + extra_data = { + 'fixits': fixits, + } ) + + @contextlib.contextmanager def _SetupForCsharpCompletionDone( ycm, completions ): with patch( 'ycm.vimsupport.InsertNamespace' ): - with patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Test' ): - request = MagicMock() - request.Done = MagicMock( return_value = True ) - request.RawResponse = MagicMock( return_value = completions ) - ycm._latest_completion_request = request + with _SetUpCompleteDone( ycm, completions ): yield +@contextlib.contextmanager +def _SetUpCompleteDone( ycm, completions ): + with patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Test' ): + request = MagicMock() + request.Done = MagicMock( return_value = True ) + request.RawResponse = MagicMock( return_value = { + 'completions': completions + } ) + ycm._latest_completion_request = request + yield + + @patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'cs' ] ) @YouCompleteMeInstance() def GetCompleteDoneHooks_ResultOnCsharp_test( ycm, *args ): - result = ycm.GetCompleteDoneHooks() - eq_( 1, len( list( result ) ) ) + result = list( ycm.GetCompleteDoneHooks() ) + eq_( [ _CompleteDoneHook_CSharp ], result ) -@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'txt' ] ) +@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'java' ] ) +@YouCompleteMeInstance() +def GetCompleteDoneHooks_ResultOnJava_test( ycm, *args ): + result = list( ycm.GetCompleteDoneHooks() ) + eq_( [ _CompleteDoneHook_Java ], result ) + + +@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'ycmtest' ] ) @YouCompleteMeInstance() def GetCompleteDoneHooks_EmptyOnOtherFiletype_test( ycm, *args ): result = ycm.GetCompleteDoneHooks() eq_( 0, len( list( result ) ) ) -@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'txt' ] ) +@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'ycmtest' ] ) @YouCompleteMeInstance() def OnCompleteDone_WithActionCallsIt_test( ycm, *args ): action = MagicMock() - ycm._complete_done_hooks[ 'txt' ] = action + ycm._complete_done_hooks[ 'ycmtest' ] = action ycm.OnCompleteDone() ok_( action.called ) -@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'txt' ] ) +@patch( 'ycm.vimsupport.CurrentFiletypes', return_value = [ 'ycmtest' ] ) @YouCompleteMeInstance() def OnCompleteDone_NoActionNoError_test( ycm, *args ): - ycm.OnCompleteDone() + with patch.object( ycm, '_OnCompleteDone_Csharp' ) as csharp: + with patch.object( ycm, '_OnCompleteDone_Java' ) as java: + ycm.OnCompleteDone() + csharp.assert_not_called() + java.assert_not_called() -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'Test' ) ) @YouCompleteMeInstance() def FilterToCompletedCompletions_MatchIsReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'Test' ) ] - result = ycm._FilterToMatchingCompletions( completions, False ) + result = ycm._FilterToMatchingCompletions( CompleteItemIs( 'Test' ), + completions, + False ) eq_( list( result ), completions ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'A' ) ) @YouCompleteMeInstance() def FilterToCompletedCompletions_ShortTextDoesntRaise_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'AAA' ) ] - ycm._FilterToMatchingCompletions( completions, False ) + ycm._FilterToMatchingCompletions( CompleteItemIs( 'A' ), + completions, + False ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'Test' ) ) @YouCompleteMeInstance() def FilterToCompletedCompletions_ExactMatchIsReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'Test' ) ] - result = ycm._FilterToMatchingCompletions( completions, False ) + result = ycm._FilterToMatchingCompletions( CompleteItemIs( 'Test' ), + completions, + False ) eq_( list( result ), completions ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( ' Quote' ) ) @YouCompleteMeInstance() def FilterToCompletedCompletions_NonMatchIsntReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'A' ) ] - result = ycm._FilterToMatchingCompletions( completions, False ) + result = ycm._FilterToMatchingCompletions( CompleteItemIs( ' Quote' ), + completions, + False ) assert_that( list( result ), empty() ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( '†es†' ) ) @YouCompleteMeInstance() def FilterToCompletedCompletions_Unicode_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = '†es†' ) ] - result = ycm._FilterToMatchingCompletions( completions, False ) + result = ycm._FilterToMatchingCompletions( CompleteItemIs( '†es†' ), + completions, + False ) eq_( list( result ), completions ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'Te' ) ) @patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Quote' ) @YouCompleteMeInstance() def HasCompletionsThatCouldBeCompletedWithMoreText_MatchIsReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'Test' ) ] - result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( completions ) + result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( + CompleteItemIs( 'Te' ), + completions ) eq_( result, True ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'X' ) ) @patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Quote' ) @YouCompleteMeInstance() def HasCompletionsThatCouldBeCompletedWithMoreText_ShortTextDoesntRaise_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'AAA' ) ] - ycm._HasCompletionsThatCouldBeCompletedWithMoreText( completions ) + ycm._HasCompletionsThatCouldBeCompletedWithMoreText( CompleteItemIs( 'X' ), + completions ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'Test' ) ) @patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Quote' ) @YouCompleteMeInstance() def HasCompletionsThatCouldBeCompletedWithMoreText_ExactMatchIsntReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'Test' ) ] - result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( completions ) + result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( + CompleteItemIs( 'Test' ), + completions ) eq_( result, False ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( ' Quote' ) ) @patch( 'ycm.vimsupport.TextBeforeCursor', return_value = ' Quote' ) @YouCompleteMeInstance() def HasCompletionsThatCouldBeCompletedWithMoreText_NonMatchIsntReturned_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = "A" ) ] - result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( completions ) + result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( + CompleteItemIs( ' Quote' ), + completions ) eq_( result, False ) -@patch( 'ycm.vimsupport.GetVariableValue', - GetVariableValue_CompleteItemIs( 'Uniç' ) ) @patch( 'ycm.vimsupport.TextBeforeCursor', return_value = 'Uniç' ) @YouCompleteMeInstance() def HasCompletionsThatCouldBeCompletedWithMoreText_Unicode_test( ycm, *args ): completions = [ BuildCompletion( insertion_text = 'Uniçø∂¢' ) ] - result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( completions ) + result = ycm._HasCompletionsThatCouldBeCompletedWithMoreText( + CompleteItemIs( 'Uniç' ), + completions ) eq_( result, True ) @@ -212,7 +278,7 @@ def GetRequiredNamespaceImport_ReturnNoneForNoExtraData_test( ycm ): def GetRequiredNamespaceImport_ReturnNamespaceFromExtraData_test( ycm ): namespace = 'A_NAMESPACE' eq_( namespace, ycm._GetRequiredNamespaceImport( - BuildCompletion( namespace ) + BuildCompletionNamespace( namespace ) ) ) @@ -228,7 +294,7 @@ def GetCompletionsUserMayHaveCompleted_ReturnEmptyIfNotDone_test( ycm ): @YouCompleteMeInstance() def GetCompletionsUserMayHaveCompleted_ReturnEmptyIfPendingMatches_test( ycm, *args ): - completions = [ BuildCompletion( None ) ] + completions = [ BuildCompletionNamespace( None ) ] with _SetupForCsharpCompletionDone( ycm, completions ): eq_( [], ycm.GetCompletionsUserMayHaveCompleted() ) @@ -237,7 +303,7 @@ def GetCompletionsUserMayHaveCompleted_ReturnEmptyIfPendingMatches_test( def GetCompletionsUserMayHaveCompleted_ReturnMatchIfExactMatches_test( ycm, *args ): info = [ 'NS', 'Test', 'Abbr', 'Menu', 'Info', 'Kind' ] - completions = [ BuildCompletion( *info ) ] + completions = [ BuildCompletionNamespace( *info ) ] with _SetupForCsharpCompletionDone( ycm, completions ): with patch( 'ycm.vimsupport.GetVariableValue', GetVariableValue_CompleteItemIs( *info[ 1: ] ) ): @@ -248,7 +314,7 @@ def GetCompletionsUserMayHaveCompleted_ReturnMatchIfExactMatches_test( def GetCompletionsUserMayHaveCompleted_ReturnMatchIfExactMatchesEvenIfPartial_test( # noqa ycm, *args ): info = [ 'NS', 'Test', 'Abbr', 'Menu', 'Info', 'Kind' ] - completions = [ BuildCompletion( *info ), + completions = [ BuildCompletionNamespace( *info ), BuildCompletion( insertion_text = 'TestTest' ) ] with _SetupForCsharpCompletionDone( ycm, completions ): with patch( 'ycm.vimsupport.GetVariableValue', @@ -272,11 +338,41 @@ def GetCompletionsUserMayHaveCompleted_DontReturnMatchIfNoExactMatchesAndPartial GetVariableValue_CompleteItemIs( 'Test' ) ) @YouCompleteMeInstance() def GetCompletionsUserMayHaveCompleted_ReturnMatchIfMatches_test( ycm, *args ): - completions = [ BuildCompletion( None ) ] + completions = [ BuildCompletionNamespace( None ) ] with _SetupForCsharpCompletionDone( ycm, completions ): eq_( completions, ycm.GetCompletionsUserMayHaveCompleted() ) +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test', user_data='0' ) ) +@YouCompleteMeInstance() +def GetCompletionsUserMayHaveCompleted_UseUserData0_test( ycm, *args ): + # identical completions but we specify the first one via user_data + completions = [ + BuildCompletionNamespace( 'namespace1' ), + BuildCompletionNamespace( 'namespace2' ) + ] + + with _SetupForCsharpCompletionDone( ycm, completions ): + eq_( [ BuildCompletionNamespace( 'namespace1' ) ], + ycm.GetCompletionsUserMayHaveCompleted() ) + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test', user_data='1' ) ) +@YouCompleteMeInstance() +def GetCompletionsUserMayHaveCompleted_UseUserData1_test( ycm, *args ): + # identical completions but we specify the second one via user_data + completions = [ + BuildCompletionNamespace( 'namespace1' ), + BuildCompletionNamespace( 'namespace2' ) + ] + + with _SetupForCsharpCompletionDone( ycm, completions ): + eq_( [ BuildCompletionNamespace( 'namespace2' ) ], + ycm.GetCompletionsUserMayHaveCompleted() ) + + @patch( 'ycm.vimsupport.GetVariableValue', GetVariableValue_CompleteItemIs( 'Test' ) ) @YouCompleteMeInstance() @@ -291,7 +387,7 @@ def PostCompleteCsharp_EmptyDoesntInsertNamespace_test( ycm, *args ): @YouCompleteMeInstance() def PostCompleteCsharp_ExistingWithoutNamespaceDoesntInsertNamespace_test( ycm, *args ): - completions = [ BuildCompletion( None ) ] + completions = [ BuildCompletionNamespace( None ) ] with _SetupForCsharpCompletionDone( ycm, completions ): ycm._OnCompleteDone_Csharp() ok_( not vimsupport.InsertNamespace.called ) @@ -302,7 +398,7 @@ def PostCompleteCsharp_ExistingWithoutNamespaceDoesntInsertNamespace_test( @YouCompleteMeInstance() def PostCompleteCsharp_ValueDoesInsertNamespace_test( ycm, *args ): namespace = 'A_NAMESPACE' - completions = [ BuildCompletion( namespace ) ] + completions = [ BuildCompletionNamespace( namespace ) ] with _SetupForCsharpCompletionDone( ycm, completions ): ycm._OnCompleteDone_Csharp() vimsupport.InsertNamespace.assert_called_once_with( namespace ) @@ -316,9 +412,92 @@ def PostCompleteCsharp_InsertSecondNamespaceIfSelected_test( ycm, *args ): namespace = 'A_NAMESPACE' namespace2 = 'ANOTHER_NAMESPACE' completions = [ - BuildCompletion( namespace ), - BuildCompletion( namespace2 ), + BuildCompletionNamespace( namespace ), + BuildCompletionNamespace( namespace2 ), ] with _SetupForCsharpCompletionDone( ycm, completions ): ycm._OnCompleteDone_Csharp() vimsupport.InsertNamespace.assert_called_once_with( namespace2 ) + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_NoFixIts_test( ycm, replace_chunks, *args ): + completions = [ + BuildCompletionFixIt( [] ) + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_not_called() + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_EmptyFixIt_test( ycm, replace_chunks, *args ): + completions = [ + BuildCompletionFixIt( [ { 'chunks': [] } ] ) + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_called_once_with( [], silent=True ) + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_NoFixIt_test( ycm, replace_chunks, *args ): + completions = [ + BuildCompletion( ) + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_not_called() + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_PickFirst_test( ycm, replace_chunks, *args ): + completions = [ + BuildCompletionFixIt( [ { 'chunks': 'one' } ] ), + BuildCompletionFixIt( [ { 'chunks': 'two' } ] ), + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_called_once_with( 'one', silent=True ) + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test', user_data='0' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_PickFirstUserData_test( ycm, + replace_chunks, + *args ): + completions = [ + BuildCompletionFixIt( [ { 'chunks': 'one' } ] ), + BuildCompletionFixIt( [ { 'chunks': 'two' } ] ), + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_called_once_with( 'one', silent=True ) + + +@patch( 'ycm.vimsupport.GetVariableValue', + GetVariableValue_CompleteItemIs( 'Test', user_data='1' ) ) +@patch( 'ycm.vimsupport.ReplaceChunks' ) +@YouCompleteMeInstance() +def PostCompleteJava_ApplyFixIt_PickSecond_test( ycm, replace_chunks, *args ): + completions = [ + BuildCompletionFixIt( [ { 'chunks': 'one' } ] ), + BuildCompletionFixIt( [ { 'chunks': 'two' } ] ), + ] + with _SetUpCompleteDone( ycm, completions ): + ycm._OnCompleteDone_Java() + replace_chunks.assert_called_once_with( 'two', silent=True ) diff --git a/python/ycm/tests/vimsupport_test.py b/python/ycm/tests/vimsupport_test.py index 046a5a31..d00e36ef 100644 --- a/python/ycm/tests/vimsupport_test.py +++ b/python/ycm/tests/vimsupport_test.py @@ -841,6 +841,103 @@ def ReplaceChunks_SingleFile_NotOpen_test( vim_command, ] ) +@patch( 'ycm.vimsupport.VariableExists', return_value = False ) +@patch( 'ycm.vimsupport.SetFittingHeightForCurrentWindow' ) +@patch( 'ycm.vimsupport.GetBufferNumberForFilename', + side_effect = [ -1, 1 ], + new_callable = ExtendedMock ) +@patch( 'ycm.vimsupport.BufferIsVisible', + side_effect = [ False, True ], + new_callable = ExtendedMock ) +@patch( 'ycm.vimsupport.OpenFilename', + new_callable = ExtendedMock ) +@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) +@patch( 'ycm.vimsupport.Confirm', + return_value = True, + new_callable = ExtendedMock ) +@patch( 'vim.eval', return_value = 10, new_callable = ExtendedMock ) +@patch( 'vim.command', new_callable = ExtendedMock ) +def ReplaceChunks_SingleFile_NotOpen_Silent_test( + vim_command, + vim_eval, + confirm, + post_vim_message, + open_filename, + buffer_is_visible, + get_buffer_number_for_filename, + set_fitting_height, + variable_exists ): + + # This test is the same as ReplaceChunks_SingleFile_NotOpen_test, but we pass + # the silent flag, as used by post-complete actions, and shows the stuff we + # _don't_ call in that case. + + single_buffer_name = os.path.realpath( 'single_file' ) + + chunks = [ + _BuildChunk( 1, 1, 2, 1, 'replacement', single_buffer_name ) + ] + + result_buffer = VimBuffer( + single_buffer_name, + contents = [ + 'line1', + 'line2', + 'line3' + ] + ) + + with patch( 'vim.buffers', [ None, result_buffer, None ] ): + vimsupport.ReplaceChunks( chunks, silent=True ) + + # We didn't check if it was OK to open the file (silent) + confirm.assert_not_called() + + # Ensure that we applied the replacement correctly + eq_( result_buffer.GetLines(), [ + 'replacementline2', + 'line3', + ] ) + + # GetBufferNumberForFilename is called 2 times. The return values are set in + # the @patch call above: + # - once whilst applying the changes (-1 return) + # - finally after calling OpenFilename (1 return) + get_buffer_number_for_filename.assert_has_exact_calls( [ + call( single_buffer_name ), + call( single_buffer_name ), + ] ) + + # BufferIsVisible is called 2 times for the same reasons as above, with the + # return of each one + buffer_is_visible.assert_has_exact_calls( [ + call( -1 ), + call( 1 ), + ] ) + + # We open 'single_file' as expected. + open_filename.assert_called_with( single_buffer_name, { + 'focus': True, + 'fix': True, + 'size': 10 + } ) + + # And close it again, but don't show the quickfix window + vim_command.assert_has_exact_calls( [ + call( 'lclose' ), + call( 'hide' ), + ] ) + set_fitting_height.assert_not_called() + + # But we _don't_ update the QuickFix list + vim_eval.assert_has_exact_calls( [ + call( '&previewheight' ), + ] ) + + # And we don't print a message either + post_vim_message.assert_not_called() + + @patch( 'ycm.vimsupport.GetBufferNumberForFilename', side_effect = [ -1, -1, 1 ], new_callable = ExtendedMock ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index ff2395c1..a888e9f7 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -735,7 +735,7 @@ def _OpenFileInSplitIfNeeded( filepath ): return ( buffer_num, True ) -def ReplaceChunks( chunks ): +def ReplaceChunks( chunks, silent=False ): """Apply the source file deltas supplied in |chunks| to arbitrary files. |chunks| is a list of changes defined by ycmd.responses.FixItChunk, which may apply arbitrary modifications to arbitrary files. @@ -755,14 +755,15 @@ def ReplaceChunks( chunks ): # We sort the file list simply to enable repeatable testing. sorted_file_list = sorted( iterkeys( chunks_by_file ) ) - # Make sure the user is prepared to have her screen mutilated by the new - # buffers. - num_files_to_open = _GetNumNonVisibleFiles( sorted_file_list ) + if not silent: + # Make sure the user is prepared to have her screen mutilated by the new + # buffers. + num_files_to_open = _GetNumNonVisibleFiles( sorted_file_list ) - if num_files_to_open > 0: - if not Confirm( + if num_files_to_open > 0: + if not Confirm( FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( num_files_to_open ) ): - return + return # Store the list of locations where we applied changes. We use this to display # the quickfix window showing the user where we applied changes. @@ -788,12 +789,13 @@ def ReplaceChunks( chunks ): vim.command( 'hide' ) # Open the quickfix list, populated with entries for each location we changed. - if locations: - SetQuickFixList( locations ) - OpenQuickFixList() + if not silent: + if locations: + SetQuickFixList( locations ) + OpenQuickFixList() - PostVimMessage( 'Applied {0} changes'.format( len( chunks ) ), - warning = False ) + PostVimMessage( 'Applied {0} changes'.format( len( chunks ) ), + warning = False ) def ReplaceChunksInBuffer( chunks, vim_buffer ): diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 82d56765..e91c34f3 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -108,6 +108,15 @@ SERVER_LOGFILE_FORMAT = 'ycmd_{port}_{std}_' HANDLE_FLAG_INHERIT = 0x00000001 +# The following two methods exist for testability only +def _CompleteDoneHook_CSharp( ycm ): + ycm._OnCompleteDone_Csharp() + + +def _CompleteDoneHook_Java( ycm ): + ycm._OnCompleteDone_Java() + + class YouCompleteMe( object ): def __init__( self, user_options ): self._available_completers = {} @@ -128,7 +137,8 @@ class YouCompleteMe( object ): self._SetUpServer() self._ycmd_keepalive.Start() self._complete_done_hooks = { - 'cs': lambda self: self._OnCompleteDone_Csharp() + 'cs': _CompleteDoneHook_CSharp, + 'java': _CompleteDoneHook_Java, } @@ -491,42 +501,63 @@ class YouCompleteMe( object ): if not latest_completion_request or not latest_completion_request.Done(): return [] - completions = latest_completion_request.RawResponse() + completed_item = vimsupport.GetVariableValue( 'v:completed_item' ) + completions = latest_completion_request.RawResponse()[ 'completions' ] - result = self._FilterToMatchingCompletions( completions, True ) + if 'user_data' in completed_item and completed_item[ 'user_data' ] != '': + # Vim supports user_data (8.0.1493) or later, so we actually know the + # _exact_ element that was selected, having put its index in the user_data + # field. + return [ completions[ int( completed_item[ 'user_data' ] ) ] ] + + # Otherwise, we have to guess by matching the values in the completed item + # and the list of completions. Sometimes this returns multiple + # possibilities, which is essentially unresolvable. + + result = self._FilterToMatchingCompletions( completed_item, + completions, + True ) result = list( result ) + if result: return result - if self._HasCompletionsThatCouldBeCompletedWithMoreText( completions ): + if self._HasCompletionsThatCouldBeCompletedWithMoreText( completed_item, + completions ): # Since the way that YCM works leads to CompleteDone called on every # character, return blank if the completion might not be done. This won't # match if the completion is ended with typing a non-keyword character. return [] - result = self._FilterToMatchingCompletions( completions, False ) + result = self._FilterToMatchingCompletions( completed_item, + completions, + False ) return list( result ) - def _FilterToMatchingCompletions( self, completions, full_match_only ): + def _FilterToMatchingCompletions( self, + completed_item, + completions, + full_match_only ): """Filter to completions matching the item Vim said was completed""" - completed = vimsupport.GetVariableValue( 'v:completed_item' ) - for completion in completions: - item = ConvertCompletionDataToVimData( completion ) - match_keys = ( [ "word", "abbr", "menu", "info" ] if full_match_only - else [ 'word' ] ) + match_keys = ( [ "word", "abbr", "menu", "info" ] if full_match_only + else [ 'word' ] ) + + for index, completion in enumerate( completions ): + item = ConvertCompletionDataToVimData( index, completion ) def matcher( key ): - return ( utils.ToUnicode( completed.get( key, "" ) ) == + return ( utils.ToUnicode( completed_item.get( key, "" ) ) == utils.ToUnicode( item.get( key, "" ) ) ) if all( [ matcher( i ) for i in match_keys ] ): yield completion - def _HasCompletionsThatCouldBeCompletedWithMoreText( self, completions ): - completed_item = vimsupport.GetVariableValue( 'v:completed_item' ) + def _HasCompletionsThatCouldBeCompletedWithMoreText( self, + completed_item, + completions ): if not completed_item: return False @@ -542,9 +573,9 @@ class YouCompleteMe( object ): reject_exact_match = False completed_word += text[ -1 ] - for completion in completions: + for index, completion in enumerate( completions ): word = utils.ToUnicode( - ConvertCompletionDataToVimData( completion )[ 'word' ] ) + ConvertCompletionDataToVimData( index, completion )[ 'word' ] ) if reject_exact_match and word == completed_word: continue if word.startswith( completed_word ): @@ -580,6 +611,33 @@ class YouCompleteMe( object ): return completion[ "extra_data" ][ "required_namespace_import" ] + def _OnCompleteDone_Java( self ): + completions = self.GetCompletionsUserMayHaveCompleted() + fixit_completions = [ self._GetFixItCompletion( c ) for c in completions ] + fixit_completions = [ f for f in fixit_completions if f ] + if not fixit_completions: + return + + # If we have user_data in completions (8.0.1493 or later), then we would + # only ever return max. 1 completion here. However, if we had to guess, it + # is possible that we matched multiple completion items (e.g. for overloads, + # or similar classes in multiple packages). In any case, rather than + # prompting the user and disturbing her workflow, we just apply the first + # one. This might be wrong, but the solution is to use a (very) new version + # of Vim which supports user_data on completion items + fixit_completion = fixit_completions[ 0 ] + + for fixit in fixit_completion: + vimsupport.ReplaceChunks( fixit[ 'chunks' ], silent=True ) + + + def _GetFixItCompletion( self, completion ): + if ( "extra_data" not in completion + or "fixits" not in completion[ "extra_data" ] ): + return None + + return completion[ "extra_data" ][ "fixits" ] + def GetErrorCount( self ): return self.CurrentBuffer().GetErrorCount()