From 7989b7b0fd3cd1c8ecb1e29a3fcd6337ebf28d29 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 10 Feb 2018 23:48:22 +0000 Subject: [PATCH] Support for additional FixIts on java completions Java completer can include FixIts which are applied when a completion entry is selected. We use the existing mechanism implemented for c-sharp to perform these edits using the CompleteDone autocommand. However, the existing mechanism relies on pattern matching the source to work out which item was completed. Vim patch 8.0.1493 introduces support for user_data on completion items, so when available we populate it with the completion array index of the item and use that to get the exact element that was selected. This is both a lot faster and a lot more accirate. Of course when applying these 'FixIts' we don't interrupt the user with confirmation or the quickfix list as this would just be annoying. If the server reports that an edit must be made, we just make the edit. This is achieved by adding a silent flag to ReplaceChunks. --- README.md | 7 +- doc/youcompleteme.txt | 7 +- python/ycm/client/completion_request.py | 16 +- .../tests/client/completion_request_test.py | 120 +++---- python/ycm/tests/postcomplete_test.py | 305 ++++++++++++++---- python/ycm/tests/vimsupport_test.py | 97 ++++++ python/ycm/vimsupport.py | 26 +- python/ycm/youcompleteme.py | 90 +++++- 8 files changed, 514 insertions(+), 154 deletions(-) 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()