Auto merge of #2907 - puremourning:java-additional-fixits, r=micbou

[READY] Support for additional FixIts on java completions, e.g. for automatic imports

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.

## Use SplitLines in ReplaceChunk to allow newlines to be inserted.

The Java completer frequently inserts newlines as part of its FixIts. We
previously used the base python splitlines implementation which consumed
terminating newlines and also consumed empty strings.

We can therefore now remove the duplicate newline in InsertNamespace, as
this was only to work around the splitlines behaviour.

In the tests, be clear that replacement_text in ReplaceChunks is Unicode

# PR Prelude

Thank you for working on YCM! :)

**Please complete these steps and check these boxes (by putting an `x` inside
the brackets) _before_ filing your PR:**

- [X] I have read and understood YCM's [CONTRIBUTING][cont] document.
- [X] I have read and understood YCM's [CODE_OF_CONDUCT][code] document.
- [X] I have included tests for the changes in my PR. If not, I have included a
  rationale for why I haven't.
- [X] **I understand my PR may be closed if it becomes obvious I didn't
  actually perform all of these steps.**

# Why this change is necessary and useful

Automatic addition of import statements is a highly desirable feature when working with languages like java where it is idiomatic to literally import everything by name. Users expect this function: it was the first question I got when i put java live at work.

I was at first reticent to include it due to the irksome complete_done interface, but since Bram merged my PR: 9b56a57cda we can now identify the exact completion selected which makes this a _lot_ more robust.

We can't just remove the old code as nobody will actually have that version yet, but I have tested before and after and it is fully backwardly compatible.

# Test case

A simple way to verify this is with the ycmd tests:

* open `third_party/ycmd/ycmd/tests/java/testdata/simple_eclipse_project/src/com/test/TestFactory.java`
* Invoke completion for `CUTHBERT` on line 19 (replace to end of line)
* select one of the enum values

You should get the enum value automatically imported at the top of the file.

[cont]: https://github.com/Valloric/YouCompleteMe/blob/master/CONTRIBUTING.md
[code]: https://github.com/Valloric/YouCompleteMe/blob/master/CODE_OF_CONDUCT.md

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/2907)
<!-- Reviewable:end -->
This commit is contained in:
zzbot 2018-02-11 12:09:42 -08:00 committed by GitHub
commit ce1764cf40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 514 additions and 154 deletions

View File

@ -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 <new name>`)
* 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

View File

@ -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 <new name>')
- 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 ~

View File

@ -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 ) ]

View File

@ -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',
@ -64,12 +65,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DETAILED INFO\nDOC STRING',
'dup' : 1,
'empty': 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',
@ -82,12 +84,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DETAILED INFO',
'dup' : 1,
'empty': 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',
@ -102,12 +105,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DOC STRING',
'dup' : 1,
'empty': 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',
@ -120,12 +124,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'menu' : 'EXTRA MENU INFO',
'kind' : 'k',
'dup' : 1,
'empty': 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',
@ -140,12 +145,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DETAILED INFO',
'dup' : 1,
'empty': 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',
@ -161,12 +167,13 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DETAILED INFO\nDOC STRING',
'dup' : 1,
'empty': 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',
@ -181,5 +188,6 @@ class ConvertCompletionResponseToVimDatas_test( object ):
'kind' : 'k',
'info' : 'DETAILED INFO\nDOC STRING',
'dup' : 1,
'empty': 1,
'empty' : 1,
'user_data': '0'
} )

View File

@ -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 GetVariableValue_CompleteItemIs( word, abbr = None, menu = None,
info = None, kind = None ):
def Result( variable ):
if variable == 'v:completed_item':
return {
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, **kwargs ):
def Result( variable ):
if variable == 'v:completed_item':
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,13 +81,53 @@ 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 _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 )
request.RawResponse = MagicMock( return_value = {
'completions': completions
} )
ycm._latest_completion_request = request
yield
@ -79,127 +135,137 @@ def _SetupForCsharpCompletionDone( ycm, completions ):
@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 ):
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 )

View File

@ -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 )

View File

@ -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,6 +755,7 @@ def ReplaceChunks( chunks ):
# We sort the file list simply to enable repeatable testing.
sorted_file_list = sorted( iterkeys( chunks_by_file ) )
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 )
@ -788,6 +789,7 @@ def ReplaceChunks( chunks ):
vim.command( 'hide' )
# Open the quickfix list, populated with entries for each location we changed.
if not silent:
if locations:
SetQuickFixList( locations )
OpenQuickFixList()

View File

@ -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' ] )
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()