Support selecting from the list of FixIts when multiple are supplied

This commit is contained in:
Ben Jackson 2016-06-19 20:38:07 +01:00
parent c44489af16
commit cfd4bbd531
7 changed files with 155 additions and 21 deletions

View File

@ -1359,8 +1359,11 @@ undone, and never saves or writes files to the disk.
#### The `FixIt` subcommand #### The `FixIt` subcommand
Where available, attempts to make changes to the buffer to correct the Where available, attempts to make changes to the buffer to correct diagnostics
diagnostic closest to the cursor position. on the current line. Where multiple suggestions are available (such as when
there are multiple ways to resolve a given warning, or where multiple
diagnostics are reported for the current line), the options are presented
and one can be selected.
Completers which provide diagnostics may also provide trivial modifications to Completers which provide diagnostics may also provide trivial modifications to
the source in order to correct the diagnostic. Examples include syntax errors the source in order to correct the diagnostic. Examples include syntax errors

View File

@ -1637,8 +1637,11 @@ undone, and never saves or writes files to the disk.
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
The *FixIt* subcommand The *FixIt* subcommand
Where available, attempts to make changes to the buffer to correct the Where available, attempts to make changes to the buffer to correct diagnostics
diagnostic closest to the cursor position. on the current line. Where multiple suggestions are available (such as when
there are multiple ways to resolve a given warning, or where multiple
diagnostics are reported for the current line), the options are presented and
one can be selected.
Completers which provide diagnostics may also provide trivial modifications to Completers which provide diagnostics may also provide trivial modifications to
the source in order to correct the diagnostic. Examples include syntax errors the source in order to correct the diagnostic. Examples include syntax errors

View File

@ -105,9 +105,19 @@ class CommandRequest( BaseRequest ):
if not len( self._response[ 'fixits' ] ): if not len( self._response[ 'fixits' ] ):
vimsupport.EchoText( "No fixits found for current line" ) vimsupport.EchoText( "No fixits found for current line" )
else: else:
chunks = self._response[ 'fixits' ][ 0 ][ 'chunks' ]
try: try:
vimsupport.ReplaceChunks( chunks ) fixit_index = 0
# When there are multiple fixit suggestions, present them as a list to
# the user hand have her choose which one to apply.
if len( self._response[ 'fixits' ] ) > 1:
fixit_index = vimsupport.SelectFromList(
"Multiple FixIt suggestions are available at this location. "
"Which one would you like to apply?",
[ fixit[ 'text' ] for fixit in self._response[ 'fixits' ] ] )
vimsupport.ReplaceChunks(
self._response[ 'fixits' ][ fixit_index ][ 'chunks' ] )
except RuntimeError as e: except RuntimeError as e:
vimsupport.PostMultiLineNotice( str( e ) ) vimsupport.PostMultiLineNotice( str( e ) )

View File

@ -156,9 +156,11 @@ class Response_Detection_test( object ):
def FixIt_Response_test( self ): def FixIt_Response_test( self ):
# Ensures we recognise and handle fixit responses with some dummy chunk data # Ensures we recognise and handle fixit responses with some dummy chunk data
def FixItTest( command, response, chunks ): def FixItTest( command, response, chunks, selection ):
with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks: with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks:
with patch( 'ycm.vimsupport.EchoText' ) as echo_text: with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
with patch( 'ycm.vimsupport.SelectFromList',
return_value = selection ):
request = CommandRequest( [ command ] ) request = CommandRequest( [ command ] )
request._response = response request._response = response
request.RunPostCommandActionsIfNeeded() request.RunPostCommandActionsIfNeeded()
@ -177,25 +179,31 @@ class Response_Detection_test( object ):
multi_fixit = { multi_fixit = {
'fixits': [ { 'fixits': [ {
'text': 'first',
'chunks': [ { 'chunks': [ {
'dummy chunk contents': True 'dummy chunk contents': True
} ] } ]
}, { }, {
'additional fixits are ignored currently': True 'text': 'second',
'chunks': [ {
'dummy chunk contents': False
}]
} ] } ]
} }
multi_fixit_first_chunks = multi_fixit[ 'fixits' ][ 0 ][ 'chunks' ] multi_fixit_first_chunks = multi_fixit[ 'fixits' ][ 0 ][ 'chunks' ]
multi_fixit_second_chunks = multi_fixit[ 'fixits' ][ 1 ][ 'chunks' ]
tests = [ tests = [
[ 'AnythingYouLike', basic_fixit, basic_fixit_chunks ], [ 'AnythingYouLike', basic_fixit, basic_fixit_chunks, 0 ],
[ 'GoToEvenWorks', basic_fixit, basic_fixit_chunks ], [ 'GoToEvenWorks', basic_fixit, basic_fixit_chunks, 0 ],
[ 'FixItWorks', basic_fixit, basic_fixit_chunks ], [ 'FixItWorks', basic_fixit, basic_fixit_chunks, 0 ],
[ 'and8434fd andy garbag!', basic_fixit, basic_fixit_chunks ], [ 'and8434fd andy garbag!', basic_fixit, basic_fixit_chunks, 0 ],
[ 'additional fixits ignored', multi_fixit, multi_fixit_first_chunks ], [ 'select from multiple 1', multi_fixit, multi_fixit_first_chunks, 0 ],
[ 'select from multiple 2', multi_fixit, multi_fixit_second_chunks, 1 ],
] ]
for test in tests: for test in tests:
yield FixItTest, test[ 0 ], test[ 1 ], test[ 2 ] yield FixItTest, test[ 0 ], test[ 1 ], test[ 2 ], test[ 3 ]
def Message_Response_test( self ): def Message_Response_test( self ):

View File

@ -49,7 +49,7 @@ def PostVimMessage_Call( message ):
def PostMultiLineNotice_Call( message ): def PostMultiLineNotice_Call( message ):
"""Return a mock.call object for a call to vimsupport.PostMultiLineNotice with """Return a mock.call object for a call to vimsupport.PostMultiLineNotice with
the supplied message""" the supplied message"""
return call( 'echohl WarningMsg | echo \'' return call( 'redraw | echohl WarningMsg | echo \''
+ message + + message +
'\' | echohl None' ) '\' | echohl None' )

View File

@ -1432,3 +1432,52 @@ def VimExpressionToPythonType_ObjectPassthrough_test( *args ):
def VimExpressionToPythonType_GeneratorPassthrough_test( *args ): def VimExpressionToPythonType_GeneratorPassthrough_test( *args ):
gen = ( x**2 for x in [ 1, 2, 3 ] ) gen = ( x**2 for x in [ 1, 2, 3 ] )
eq_( vimsupport.VimExpressionToPythonType( gen ), gen ) eq_( vimsupport.VimExpressionToPythonType( gen ), gen )
@patch( 'vim.eval',
new_callable = ExtendedMock,
side_effect = [ None, 2, None ] )
def SelectFromList_LastItem_test( vim_eval ):
eq_( vimsupport.SelectFromList( 'test', [ 'a', 'b' ] ),
1 )
vim_eval.assert_has_exact_calls( [
call( 'inputsave()' ),
call( 'inputlist( ["test", "1: a", "2: b"] )' ),
call( 'inputrestore()' )
] )
@patch( 'vim.eval',
new_callable = ExtendedMock,
side_effect = [ None, 1, None ] )
def SelectFromList_FirstItem_test( vim_eval ):
eq_( vimsupport.SelectFromList( 'test', [ 'a', 'b' ] ),
0 )
vim_eval.assert_has_exact_calls( [
call( 'inputsave()' ),
call( 'inputlist( ["test", "1: a", "2: b"] )' ),
call( 'inputrestore()' )
] )
@patch( 'vim.eval', side_effect = [ None, 3, None ] )
def SelectFromList_OutOfRange_test( vim_eval ):
assert_that( calling( vimsupport.SelectFromList).with_args( 'test',
[ 'a', 'b' ] ),
raises( RuntimeError, vimsupport.NO_SELECTION_MADE_MSG ) )
@patch( 'vim.eval', side_effect = [ None, 0, None ] )
def SelectFromList_SelectPrompt_test( vim_eval ):
assert_that( calling( vimsupport.SelectFromList ).with_args( 'test',
[ 'a', 'b' ] ),
raises( RuntimeError, vimsupport.NO_SELECTION_MADE_MSG ) )
@patch( 'vim.eval', side_effect = [ None, -199, None ] )
def SelectFromList_Negative_test( vim_eval ):
assert_that( calling( vimsupport.SelectFromList ).with_args( 'test',
[ 'a', 'b' ] ),
raises( RuntimeError, vimsupport.NO_SELECTION_MADE_MSG ) )

View File

@ -44,6 +44,8 @@ FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT = (
'buffers. The quickfix list can then be used to review the changes. No ' 'buffers. The quickfix list can then be used to review the changes. No '
'files will be written to disk. Do you wish to continue?' ) 'files will be written to disk. Do you wish to continue?' )
NO_SELECTION_MADE_MSG = "No valid selection was made; aborting."
def CurrentLineAndColumn(): def CurrentLineAndColumn():
"""Returns the 0-based current line and 0-based current column.""" """Returns the 0-based current line and 0-based current column."""
@ -441,8 +443,11 @@ def PostVimMessage( message ):
# Unlike PostVimMesasge, this supports messages with newlines in them because it # Unlike PostVimMesasge, this supports messages with newlines in them because it
# uses 'echo' instead of 'echomsg'. This also means that the message will NOT # uses 'echo' instead of 'echomsg'. This also means that the message will NOT
# appear in Vim's message log. # appear in Vim's message log.
# Similarly to PostVimMesasge, we do a redraw first to clear any previous
# messages, which might lead to this message appearing without a newline and/or
# requring the "Press ENTER or type command to continue".
def PostMultiLineNotice( message ): def PostMultiLineNotice( message ):
vim.command( "echohl WarningMsg | echo '{0}' | echohl None" vim.command( "redraw | echohl WarningMsg | echo '{0}' | echohl None"
.format( EscapeForVim( ToUnicode( message ) ) ) ) .format( EscapeForVim( ToUnicode( message ) ) ) )
@ -458,6 +463,10 @@ def PresentDialog( message, choices, default_choice_index = 0 ):
PresentDialog will return a 0-based index into the list PresentDialog will return a 0-based index into the list
or -1 if the dialog was dismissed by using <Esc>, Ctrl-C, etc. or -1 if the dialog was dismissed by using <Esc>, Ctrl-C, etc.
If you are presenting a list of options for the user to choose from, such as
a list of imports, or lines to insert (etc.), SelectFromList is a better
option.
See also: See also:
:help confirm() in vim (Note that vim uses 1-based indexes) :help confirm() in vim (Note that vim uses 1-based indexes)
@ -478,6 +487,58 @@ def Confirm( message ):
return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 ) return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 )
def SelectFromList( prompt, items ):
"""Ask the user to select an item from the list |items|.
Presents the user with |prompt| followed by a numbered list of |items|,
from which they select one. The user is asked to enter the number of an
item or click it.
|items| should not contain leading ordinals: they are added automatically.
Returns the 0-based index in the list |items| that the user selected, or a
negative number if no valid item was selected.
See also :help inputlist()."""
vim_items = [ prompt ]
vim_items.extend( [ "{0}: {1}".format( i + 1, item )
for i, item in enumerate( items ) ] )
# The vim documentation warns not to present lists larger than the number of
# lines of display. This is sound advice, but there really isn't any sensible
# thing we can do in that scenario. Testing shows that Vim just pages the
# message; that behaviour is as good as any, so we don't manipulate the list,
# or attempt to page it.
# For an explanation of the purpose of inputsave() / inputrestore(),
# see :help input(). Briefly, it makes inputlist() work as part of a mapping.
vim.eval( 'inputsave()' )
try:
# Vim returns the number the user entered, or the line number the user
# clicked. This may be wildly out of range for our list. It might even be
# negative.
#
# The first item is index 0, and this maps to our "prompt", so we subtract 1
# from the result and return that, assuming it is within the range of the
# supplied list. If not, we return negative.
#
# See :help input() for explanation of the use of inputsave() and inpput
# restore(). It is done in try/finally in case vim.eval ever throws an
# exception (such as KeyboardInterrupt)
selected = int( vim.eval( "inputlist( "
+ json.dumps( vim_items )
+ " )" ) ) - 1
finally:
vim.eval( 'inputrestore()' )
if selected < 0 or selected >= len( items ):
# User selected something outside of the range
raise RuntimeError( NO_SELECTION_MADE_MSG )
return selected
def EchoText( text, log_as_message = True ): def EchoText( text, log_as_message = True ):
def EchoLine( text ): def EchoLine( text ):
command = 'echom' if log_as_message else 'echo' command = 'echom' if log_as_message else 'echo'