Support FixIt commands across buffers
We simply apply the changes to each file in turn. The existing replacement logic is unchanged, except that it now no longer implicitly assumes we are talking about the current buffer. If a buffer is not visible for the requested file name, we open it in a horizontal split, make the edits, then hide the window. Because this can cause UI flickering, and leave hidden, modified buffers around, we issue a warning to the user stating the number of files for which we are going to do this. We pop up the quickfix list at the end of applying the edits to allow the user to see what we changed. If the user opts to abort due to, say, the file being open in another window, we simply raise an error and give up, as undoing the changes is too complex to do programatically, but trivial to do manually in such a rare case.
This commit is contained in:
parent
d9fef6be14
commit
24f1f9c900
@ -80,8 +80,8 @@ class CommandRequest( BaseRequest ):
|
|||||||
|
|
||||||
def _HandleGotoResponse( self ):
|
def _HandleGotoResponse( self ):
|
||||||
if isinstance( self._response, list ):
|
if isinstance( self._response, list ):
|
||||||
defs = [ _BuildQfListItem( x ) for x in self._response ]
|
vimsupport.SetQuickFixList(
|
||||||
vim.eval( 'setqflist( %s )' % repr( defs ) )
|
[ _BuildQfListItem( x ) for x in self._response ] )
|
||||||
vim.eval( 'youcompleteme#OpenGoToList()' )
|
vim.eval( 'youcompleteme#OpenGoToList()' )
|
||||||
else:
|
else:
|
||||||
vimsupport.JumpToLocation( self._response[ 'filepath' ],
|
vimsupport.JumpToLocation( self._response[ 'filepath' ],
|
||||||
@ -94,12 +94,10 @@ class CommandRequest( BaseRequest ):
|
|||||||
vimsupport.EchoText( "No fixits found for current line" )
|
vimsupport.EchoText( "No fixits found for current line" )
|
||||||
else:
|
else:
|
||||||
chunks = self._response[ 'fixits' ][ 0 ][ 'chunks' ]
|
chunks = self._response[ 'fixits' ][ 0 ][ 'chunks' ]
|
||||||
|
try:
|
||||||
vimsupport.ReplaceChunksList( chunks )
|
vimsupport.ReplaceChunks( chunks )
|
||||||
|
except RuntimeError as e:
|
||||||
vimsupport.EchoTextVimWidth( "FixIt applied "
|
vimsupport.PostMultiLineNotice( e.message )
|
||||||
+ str( len( chunks ) )
|
|
||||||
+ " changes" )
|
|
||||||
|
|
||||||
|
|
||||||
def _HandleBasicResponse( self ):
|
def _HandleBasicResponse( self ):
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
from ycm.test_utils import MockVimModule
|
from ycm.test_utils import MockVimModule
|
||||||
MockVimModule()
|
MockVimModule()
|
||||||
|
|
||||||
|
import json
|
||||||
from mock import patch, call
|
from mock import patch, call
|
||||||
|
from nose.tools import ok_
|
||||||
from ycm.client.command_request import CommandRequest
|
from ycm.client.command_request import CommandRequest
|
||||||
|
|
||||||
|
|
||||||
@ -85,6 +87,177 @@ class GoToResponse_QuickFix_test:
|
|||||||
self._request.RunPostCommandActionsIfNeeded()
|
self._request.RunPostCommandActionsIfNeeded()
|
||||||
|
|
||||||
vim_eval.assert_has_calls( [
|
vim_eval.assert_has_calls( [
|
||||||
call( 'setqflist( {0} )'.format( repr( expected_qf_list ) ) ),
|
call( 'setqflist( {0} )'.format( json.dumps( expected_qf_list ) ) ),
|
||||||
call( 'youcompleteme#OpenGoToList()' ),
|
call( 'youcompleteme#OpenGoToList()' ),
|
||||||
] )
|
] )
|
||||||
|
|
||||||
|
|
||||||
|
class Response_Detection_test:
|
||||||
|
|
||||||
|
def BasicResponse_test( self ):
|
||||||
|
def _BasicResponseTest( command, response ):
|
||||||
|
with patch( 'vim.command' ) as vim_command:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = response
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
vim_command.assert_called_with( "echom '{0}'".format( response ) )
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[ 'AnythingYouLike', True ],
|
||||||
|
[ 'GoToEvenWorks', 10 ],
|
||||||
|
[ 'FixItWorks', 'String!' ],
|
||||||
|
[ 'and8434fd andy garbag!', 10.3 ],
|
||||||
|
]
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
yield _BasicResponseTest, test[ 0 ], test[ 1 ]
|
||||||
|
|
||||||
|
|
||||||
|
def FixIt_Response_Empty_test( self ):
|
||||||
|
# Ensures we recognise and handle fixit responses which indicate that there
|
||||||
|
# are no fixits available
|
||||||
|
def EmptyFixItTest( command ):
|
||||||
|
with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks:
|
||||||
|
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = {
|
||||||
|
'fixits': []
|
||||||
|
}
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
|
||||||
|
echo_text.assert_called_with( 'No fixits found for current line' )
|
||||||
|
replace_chunks.assert_not_called()
|
||||||
|
|
||||||
|
for test in [ 'FixIt', 'Refactor', 'GoToHell', 'any_old_garbade!!!21' ]:
|
||||||
|
yield EmptyFixItTest, test
|
||||||
|
|
||||||
|
|
||||||
|
def FixIt_Response_test( self ):
|
||||||
|
# Ensures we recognise and handle fixit responses with some dummy chunk data
|
||||||
|
def FixItTest( command, response, chunks ):
|
||||||
|
with patch( 'ycm.vimsupport.ReplaceChunks' ) as replace_chunks:
|
||||||
|
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = response
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
|
||||||
|
replace_chunks.assert_called_with( chunks )
|
||||||
|
echo_text.assert_not_called()
|
||||||
|
|
||||||
|
basic_fixit = {
|
||||||
|
'fixits': [ {
|
||||||
|
'chunks': [ {
|
||||||
|
'dummy chunk contents': True
|
||||||
|
} ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
basic_fixit_chunks = basic_fixit[ 'fixits' ][ 0 ][ 'chunks' ]
|
||||||
|
|
||||||
|
multi_fixit = {
|
||||||
|
'fixits': [ {
|
||||||
|
'chunks': [ {
|
||||||
|
'dummy chunk contents': True
|
||||||
|
} ]
|
||||||
|
}, {
|
||||||
|
'additional fixits are ignored currently': True
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
multi_fixit_first_chunks = multi_fixit[ 'fixits' ][ 0 ][ 'chunks' ]
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[ 'AnythingYouLike', basic_fixit, basic_fixit_chunks ],
|
||||||
|
[ 'GoToEvenWorks', basic_fixit, basic_fixit_chunks ],
|
||||||
|
[ 'FixItWorks', basic_fixit, basic_fixit_chunks ],
|
||||||
|
[ 'and8434fd andy garbag!', basic_fixit, basic_fixit_chunks ],
|
||||||
|
[ 'additional fixits ignored', multi_fixit, multi_fixit_first_chunks ],
|
||||||
|
]
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
yield FixItTest, test[ 0 ], test[ 1 ], test[ 2 ]
|
||||||
|
|
||||||
|
|
||||||
|
def Message_Response_test( self ):
|
||||||
|
# Ensures we correctly recognise and handle responses with a message to show
|
||||||
|
# to the user
|
||||||
|
|
||||||
|
def MessageTest( command, message ):
|
||||||
|
with patch( 'ycm.vimsupport.EchoText' ) as echo_text:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = { 'message': message }
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
echo_text.assert_called_with( message )
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[ '___________', 'This is a message' ],
|
||||||
|
[ '', 'this is also a message' ],
|
||||||
|
[ 'GetType', 'std::string' ],
|
||||||
|
]
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
yield MessageTest, test[ 0 ], test[ 1 ]
|
||||||
|
|
||||||
|
|
||||||
|
def Detailed_Info_test( self ):
|
||||||
|
# Ensures we correctly detect and handle detailed_info responses which are
|
||||||
|
# used to display information in the preview window
|
||||||
|
|
||||||
|
def DetailedInfoTest( command, info ):
|
||||||
|
with patch( 'ycm.vimsupport.WriteToPreviewWindow' ) as write_to_preview:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = { 'detailed_info': info }
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
write_to_preview.assert_called_with( info )
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[ '___________', 'This is a message' ],
|
||||||
|
[ '', 'this is also a message' ],
|
||||||
|
[ 'GetDoc', 'std::string\netc\netc' ],
|
||||||
|
]
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
yield DetailedInfoTest, test[ 0 ], test[ 1 ]
|
||||||
|
|
||||||
|
|
||||||
|
def GoTo_Single_test( self ):
|
||||||
|
# Ensures we handle any unknown type of response as a GoTo response
|
||||||
|
|
||||||
|
def GoToTest( command, response ):
|
||||||
|
with patch( 'ycm.vimsupport.JumpToLocation' ) as jump_to_location:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = response
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
jump_to_location.assert_called_with(
|
||||||
|
response[ 'filepath' ],
|
||||||
|
response[ 'line_num' ],
|
||||||
|
response[ 'column_num' ] )
|
||||||
|
|
||||||
|
def GoToListTest( command, response ):
|
||||||
|
# Note: the detail of these called are tested by
|
||||||
|
# GoToResponse_QuickFix_test, so here we just check that the right call is
|
||||||
|
# made
|
||||||
|
with patch( 'ycm.vimsupport.SetQuickFixList' ) as set_qf_list:
|
||||||
|
with patch( 'vim.eval' ) as vim_eval:
|
||||||
|
request = CommandRequest( [ command ] )
|
||||||
|
request._response = response
|
||||||
|
request.RunPostCommandActionsIfNeeded()
|
||||||
|
ok_( set_qf_list.called )
|
||||||
|
ok_( vim_eval.called )
|
||||||
|
|
||||||
|
basic_goto = {
|
||||||
|
'filepath': 'test',
|
||||||
|
'line_num': 10,
|
||||||
|
'column_num': 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
[ GoToTest, 'AnythingYouLike', basic_goto ],
|
||||||
|
[ GoToTest, 'GoTo', basic_goto ],
|
||||||
|
[ GoToTest, 'FindAThing', basic_goto ],
|
||||||
|
[ GoToTest, 'FixItGoto', basic_goto ],
|
||||||
|
[ GoToListTest, 'AnythingYouLike', [ basic_goto ] ],
|
||||||
|
[ GoToListTest, 'GoTo', [] ],
|
||||||
|
[ GoToListTest, 'FixItGoto', [ basic_goto, basic_goto ] ],
|
||||||
|
]
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
yield test[ 0 ], test[ 1 ], test[ 2 ]
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
|
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from ycm.test_utils import MockVimModule, MockVimCommand
|
from ycm.test_utils import ExtendedMock, MockVimModule, MockVimCommand
|
||||||
MockVimModule()
|
MockVimModule()
|
||||||
|
|
||||||
from ycm import vimsupport
|
from ycm import vimsupport
|
||||||
@ -23,6 +23,7 @@ from nose.tools import eq_
|
|||||||
from hamcrest import assert_that, calling, raises, none
|
from hamcrest import assert_that, calling, raises, none
|
||||||
from mock import MagicMock, call, patch
|
from mock import MagicMock, call, patch
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
def ReplaceChunk_SingleLine_Repl_1_test():
|
def ReplaceChunk_SingleLine_Repl_1_test():
|
||||||
@ -239,9 +240,9 @@ def ReplaceChunk_SingleToMultipleLines_test():
|
|||||||
|
|
||||||
# now make another change to the "2nd" line
|
# now make another change to the "2nd" line
|
||||||
start, end = _BuildLocations( 2, 3, 2, 4 )
|
start, end = _BuildLocations( 2, 3, 2, 4 )
|
||||||
( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
|
( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
'cccc',
|
'cccc',
|
||||||
line_offset,
|
line_offset,
|
||||||
char_offset,
|
char_offset,
|
||||||
@ -343,7 +344,6 @@ def ReplaceChunk_SingleToMultipleLinesReplace_2_test():
|
|||||||
eq_( char_offset, 4 )
|
eq_( char_offset, 4 )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def ReplaceChunk_MultipleLinesToSingleLine_test():
|
def ReplaceChunk_MultipleLinesToSingleLine_test():
|
||||||
result_buffer = [ "aAa", "aBa", "aCaaaa" ]
|
result_buffer = [ "aAa", "aBa", "aCaaaa" ]
|
||||||
start, end = _BuildLocations( 2, 2, 3, 2 )
|
start, end = _BuildLocations( 2, 2, 3, 2 )
|
||||||
@ -539,41 +539,517 @@ def _BuildLocations( start_line, start_column, end_line, end_column ):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def ReplaceChunksList_SortedChunks_test():
|
def ReplaceChunksInBuffer_SortedChunks_test():
|
||||||
chunks = [
|
chunks = [
|
||||||
_BuildChunk( 1, 4, 1, 4, '('),
|
_BuildChunk( 1, 4, 1, 4, '('),
|
||||||
_BuildChunk( 1, 11, 1, 11, ')' )
|
_BuildChunk( 1, 11, 1, 11, ')' )
|
||||||
]
|
]
|
||||||
|
|
||||||
result_buffer = [ "CT<10 >> 2> ct" ]
|
result_buffer = [ "CT<10 >> 2> ct" ]
|
||||||
vimsupport.ReplaceChunksList( chunks, result_buffer )
|
vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
|
||||||
|
|
||||||
expected_buffer = [ "CT<(10 >> 2)> ct" ]
|
expected_buffer = [ "CT<(10 >> 2)> ct" ]
|
||||||
eq_( expected_buffer, result_buffer )
|
eq_( expected_buffer, result_buffer )
|
||||||
|
|
||||||
|
|
||||||
def ReplaceChunksList_UnsortedChunks_test():
|
def ReplaceChunksInBuffer_UnsortedChunks_test():
|
||||||
chunks = [
|
chunks = [
|
||||||
_BuildChunk( 1, 11, 1, 11, ')'),
|
_BuildChunk( 1, 11, 1, 11, ')'),
|
||||||
_BuildChunk( 1, 4, 1, 4, '(' )
|
_BuildChunk( 1, 4, 1, 4, '(' )
|
||||||
]
|
]
|
||||||
|
|
||||||
result_buffer = [ "CT<10 >> 2> ct" ]
|
result_buffer = [ "CT<10 >> 2> ct" ]
|
||||||
vimsupport.ReplaceChunksList( chunks, result_buffer )
|
vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
|
||||||
|
|
||||||
expected_buffer = [ "CT<(10 >> 2)> ct" ]
|
expected_buffer = [ "CT<(10 >> 2)> ct" ]
|
||||||
eq_( expected_buffer, result_buffer )
|
eq_( expected_buffer, result_buffer )
|
||||||
|
|
||||||
|
|
||||||
def _BuildChunk( start_line, start_column, end_line, end_column,
|
class MockBuffer( ):
|
||||||
replacement_text ):
|
"""An object that looks like a vim.buffer object, enough for ReplaceChunk to
|
||||||
|
generate a location list"""
|
||||||
|
|
||||||
|
def __init__( self, lines, name, number ):
|
||||||
|
self.lines = lines
|
||||||
|
self.name = name
|
||||||
|
self.number = number
|
||||||
|
|
||||||
|
|
||||||
|
def __getitem__( self, index ):
|
||||||
|
return self.lines[ index ]
|
||||||
|
|
||||||
|
|
||||||
|
def __len__( self ):
|
||||||
|
return len( self.lines )
|
||||||
|
|
||||||
|
|
||||||
|
def __setitem__( self, key, value ):
|
||||||
|
return self.lines.__setitem__( key, value )
|
||||||
|
|
||||||
|
|
||||||
|
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
|
||||||
|
return_value=1,
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.BufferIsVisible',
|
||||||
|
return_value=True,
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.OpenFilename' )
|
||||||
|
@patch( 'ycm.vimsupport.EchoTextVimWidth', new_callable=ExtendedMock )
|
||||||
|
@patch( 'vim.eval', new_callable=ExtendedMock )
|
||||||
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
|
def ReplaceChunks_SingleFile_Open_test( vim_command,
|
||||||
|
vim_eval,
|
||||||
|
echo_text_vim_width,
|
||||||
|
open_filename,
|
||||||
|
buffer_is_visible,
|
||||||
|
get_buffer_number_for_filename ):
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
|
||||||
|
]
|
||||||
|
|
||||||
|
result_buffer = MockBuffer( [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
], 'single_file', 1 )
|
||||||
|
|
||||||
|
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
|
||||||
|
vimsupport.ReplaceChunks( chunks )
|
||||||
|
|
||||||
|
# Ensure that we applied the replacement correctly
|
||||||
|
eq_( result_buffer.lines, [
|
||||||
|
'replacementline2',
|
||||||
|
'line3',
|
||||||
|
] )
|
||||||
|
|
||||||
|
# GetBufferNumberForFilename is called twice:
|
||||||
|
# - once to the check if we would require opening the file (so that we can
|
||||||
|
# raise a warning)
|
||||||
|
# - once whilst applying the changes
|
||||||
|
get_buffer_number_for_filename.assert_has_exact_calls( [
|
||||||
|
call( 'single_file', False ),
|
||||||
|
call( 'single_file', False ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# BufferIsVisible is called twice for the same reasons as above
|
||||||
|
buffer_is_visible.assert_has_exact_calls( [
|
||||||
|
call( 1 ),
|
||||||
|
call( 1 ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# we don't attempt to open any files
|
||||||
|
open_filename.assert_not_called()
|
||||||
|
|
||||||
|
# But we do set the quickfix list
|
||||||
|
vim_eval.assert_has_exact_calls( [
|
||||||
|
call( 'setqflist( {0} )'.format( json.dumps( [ {
|
||||||
|
'bufnr': 1,
|
||||||
|
'filename': 'single_file',
|
||||||
|
'lnum': 1,
|
||||||
|
'col': 1,
|
||||||
|
'text': 'replacement',
|
||||||
|
'type': 'F'
|
||||||
|
} ] ) ) ),
|
||||||
|
] )
|
||||||
|
vim_command.assert_has_calls( [
|
||||||
|
call( 'copen 1' )
|
||||||
|
] )
|
||||||
|
|
||||||
|
# And it is ReplaceChunks that prints the message showing the number of
|
||||||
|
# changes
|
||||||
|
echo_text_vim_width.assert_has_exact_calls( [
|
||||||
|
call( 'Applied 1 changes' ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
|
||||||
|
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
|
||||||
|
side_effect=[ -1, -1, 1 ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.BufferIsVisible',
|
||||||
|
side_effect=[ False, False, True ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.OpenFilename',
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.EchoTextVimWidth', 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_test( vim_command,
|
||||||
|
vim_eval,
|
||||||
|
confirm,
|
||||||
|
echo_text_vim_width,
|
||||||
|
open_filename,
|
||||||
|
buffer_is_visible,
|
||||||
|
get_buffer_number_for_filename ):
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
|
||||||
|
]
|
||||||
|
|
||||||
|
result_buffer = MockBuffer( [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
], 'single_file', 1 )
|
||||||
|
|
||||||
|
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
|
||||||
|
vimsupport.ReplaceChunks( chunks )
|
||||||
|
|
||||||
|
# We checked if it was OK to open the file
|
||||||
|
confirm.assert_has_exact_calls( [
|
||||||
|
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
|
||||||
|
] )
|
||||||
|
|
||||||
|
# Ensure that we applied the replacement correctly
|
||||||
|
eq_( result_buffer.lines, [
|
||||||
|
'replacementline2',
|
||||||
|
'line3',
|
||||||
|
] )
|
||||||
|
|
||||||
|
# GetBufferNumberForFilename is called 3 times. The return values are set in
|
||||||
|
# the @patch call above:
|
||||||
|
# - once to the check if we would require opening the file (so that we can
|
||||||
|
# raise a warning) (-1 return)
|
||||||
|
# - once whilst applying the changes (-1 return)
|
||||||
|
# - finally after calling OpenFilename (1 return)
|
||||||
|
get_buffer_number_for_filename.assert_has_exact_calls( [
|
||||||
|
call( 'single_file', False ),
|
||||||
|
call( 'single_file', False ),
|
||||||
|
call( 'single_file', False ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# BufferIsVisible is called 3 times for the same reasons as above, with the
|
||||||
|
# return of each one
|
||||||
|
buffer_is_visible.assert_has_exact_calls( [
|
||||||
|
call( -1 ),
|
||||||
|
call( -1 ),
|
||||||
|
call( 1 ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# We open 'single_file' as expected.
|
||||||
|
open_filename.assert_called_with( 'single_file', {
|
||||||
|
'focus': True,
|
||||||
|
'fix': True,
|
||||||
|
'size': 10
|
||||||
|
} )
|
||||||
|
|
||||||
|
# And close it again, then show the preview window (note, we don't check exact
|
||||||
|
# calls because there are other calls which are checked elsewhere)
|
||||||
|
vim_command.assert_has_calls( [
|
||||||
|
call( 'lclose' ),
|
||||||
|
call( 'hide' ),
|
||||||
|
call( 'copen 1' ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# And update the quickfix list
|
||||||
|
vim_eval.assert_has_exact_calls( [
|
||||||
|
call( '&previewheight' ),
|
||||||
|
call( 'setqflist( {0} )'.format( json.dumps( [ {
|
||||||
|
'bufnr': 1,
|
||||||
|
'filename': 'single_file',
|
||||||
|
'lnum': 1,
|
||||||
|
'col': 1,
|
||||||
|
'text': 'replacement',
|
||||||
|
'type': 'F'
|
||||||
|
} ] ) ) ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# And it is ReplaceChunks that prints the message showing the number of
|
||||||
|
# changes
|
||||||
|
echo_text_vim_width.assert_has_exact_calls( [
|
||||||
|
call( 'Applied 1 changes' ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
|
||||||
|
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
|
||||||
|
side_effect=[ -1, -1, 1 ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.BufferIsVisible',
|
||||||
|
side_effect=[ False, False, True ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.OpenFilename',
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.EchoTextVimWidth',
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.Confirm',
|
||||||
|
return_value=False,
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'vim.eval',
|
||||||
|
return_value=10,
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
|
def ReplaceChunks_User_Declines_To_Open_File_test(
|
||||||
|
vim_command,
|
||||||
|
vim_eval,
|
||||||
|
confirm,
|
||||||
|
echo_text_vim_width,
|
||||||
|
open_filename,
|
||||||
|
buffer_is_visible,
|
||||||
|
get_buffer_number_for_filename ):
|
||||||
|
|
||||||
|
# Same as above, except the user selects Cancel when asked if they should
|
||||||
|
# allow us to open lots of (ahem, 1) file.
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
|
||||||
|
]
|
||||||
|
|
||||||
|
result_buffer = MockBuffer( [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
], 'single_file', 1 )
|
||||||
|
|
||||||
|
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
|
||||||
|
vimsupport.ReplaceChunks( chunks )
|
||||||
|
|
||||||
|
# We checked if it was OK to open the file
|
||||||
|
confirm.assert_has_exact_calls( [
|
||||||
|
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
|
||||||
|
] )
|
||||||
|
|
||||||
|
# Ensure that buffer is not changed
|
||||||
|
eq_( result_buffer.lines, [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
] )
|
||||||
|
|
||||||
|
# GetBufferNumberForFilename is called once. The return values are set in
|
||||||
|
# the @patch call above:
|
||||||
|
# - once to the check if we would require opening the file (so that we can
|
||||||
|
# raise a warning) (-1 return)
|
||||||
|
get_buffer_number_for_filename.assert_has_exact_calls( [
|
||||||
|
call( 'single_file', False ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# BufferIsVisible is called once for the above file, which wasn't visible.
|
||||||
|
buffer_is_visible.assert_has_exact_calls( [
|
||||||
|
call( -1 ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# We don't attempt to open any files or update any quickfix list or anything
|
||||||
|
# like that
|
||||||
|
open_filename.assert_not_called()
|
||||||
|
vim_eval.assert_not_called()
|
||||||
|
vim_command.assert_not_called()
|
||||||
|
echo_text_vim_width.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
|
||||||
|
side_effect=[ -1, -1, 1 ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
# Key difference is here: In the final check, BufferIsVisible returns False
|
||||||
|
@patch( 'ycm.vimsupport.BufferIsVisible',
|
||||||
|
side_effect=[ False, False, False ],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.OpenFilename',
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.EchoTextVimWidth',
|
||||||
|
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_User_Aborts_Opening_File_test(
|
||||||
|
vim_command,
|
||||||
|
vim_eval,
|
||||||
|
confirm,
|
||||||
|
echo_text_vim_width,
|
||||||
|
open_filename,
|
||||||
|
buffer_is_visible,
|
||||||
|
get_buffer_number_for_filename ):
|
||||||
|
|
||||||
|
# Same as above, except the user selects Abort or Quick during the
|
||||||
|
# "swap-file-found" dialog
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
|
||||||
|
]
|
||||||
|
|
||||||
|
result_buffer = MockBuffer( [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
], 'single_file', 1 )
|
||||||
|
|
||||||
|
with patch( 'vim.buffers', [ None, result_buffer, None ] ):
|
||||||
|
assert_that( calling( vimsupport.ReplaceChunks ).with_args( chunks ),
|
||||||
|
raises( RuntimeError,
|
||||||
|
'Unable to open file: single_file\nFixIt/Refactor operation '
|
||||||
|
'aborted prior to completion. Your files have not been '
|
||||||
|
'fully updated. Please use undo commands to revert the '
|
||||||
|
'applied changes.' ) )
|
||||||
|
|
||||||
|
# We checked if it was OK to open the file
|
||||||
|
confirm.assert_has_exact_calls( [
|
||||||
|
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
|
||||||
|
] )
|
||||||
|
|
||||||
|
# Ensure that buffer is not changed
|
||||||
|
eq_( result_buffer.lines, [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
] )
|
||||||
|
|
||||||
|
# We tried to open this file
|
||||||
|
open_filename.assert_called_with( "single_file", {
|
||||||
|
'focus': True,
|
||||||
|
'fix': True,
|
||||||
|
'size': 10
|
||||||
|
} )
|
||||||
|
vim_eval.assert_called_with( "&previewheight" )
|
||||||
|
|
||||||
|
# But raised an exception before issuing the message at the end
|
||||||
|
echo_text_vim_width.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch( 'ycm.vimsupport.GetBufferNumberForFilename', side_effect=[
|
||||||
|
22, # first_file (check)
|
||||||
|
-1, # another_file (check)
|
||||||
|
22, # first_file (apply)
|
||||||
|
-1, # another_file (apply)
|
||||||
|
19, # another_file (check after open)
|
||||||
|
],
|
||||||
|
new_callable=ExtendedMock )
|
||||||
|
@patch( 'ycm.vimsupport.BufferIsVisible', side_effect=[
|
||||||
|
True, # first_file (check)
|
||||||
|
False, # second_file (check)
|
||||||
|
True, # first_file (apply)
|
||||||
|
False, # second_file (apply)
|
||||||
|
True, # side_effect (check after open)
|
||||||
|
],
|
||||||
|
new_callable=ExtendedMock)
|
||||||
|
@patch( 'ycm.vimsupport.OpenFilename',
|
||||||
|
new_callable=ExtendedMock)
|
||||||
|
@patch( 'ycm.vimsupport.EchoTextVimWidth',
|
||||||
|
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_MultiFile_Open_test( vim_command,
|
||||||
|
vim_eval,
|
||||||
|
confirm,
|
||||||
|
echo_text_vim_width,
|
||||||
|
open_filename,
|
||||||
|
buffer_is_visible,
|
||||||
|
get_buffer_number_for_filename ):
|
||||||
|
|
||||||
|
# Chunks are split across 2 files, one is already open, one isn't
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
_BuildChunk( 1, 1, 2, 1, 'first_file_replacement ', '1_first_file' ),
|
||||||
|
_BuildChunk( 2, 1, 2, 1, 'second_file_replacement ', '2_another_file' ),
|
||||||
|
]
|
||||||
|
|
||||||
|
first_file = MockBuffer( [
|
||||||
|
'line1',
|
||||||
|
'line2',
|
||||||
|
'line3',
|
||||||
|
], '1_first_file', 22 )
|
||||||
|
another_file = MockBuffer( [
|
||||||
|
'another line1',
|
||||||
|
'ACME line2',
|
||||||
|
], '2_another_file', 19 )
|
||||||
|
|
||||||
|
vim_buffers = [ None ] * 23
|
||||||
|
vim_buffers[ 22 ] = first_file
|
||||||
|
vim_buffers[ 19 ] = another_file
|
||||||
|
|
||||||
|
with patch( 'vim.buffers', vim_buffers ):
|
||||||
|
vimsupport.ReplaceChunks( chunks )
|
||||||
|
|
||||||
|
# We checked for the right file names
|
||||||
|
get_buffer_number_for_filename.assert_has_exact_calls( [
|
||||||
|
call( '1_first_file', False ),
|
||||||
|
call( '2_another_file', False ),
|
||||||
|
call( '1_first_file', False ),
|
||||||
|
call( '2_another_file', False ),
|
||||||
|
call( '2_another_file', False ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# We checked if it was OK to open the file
|
||||||
|
confirm.assert_has_exact_calls( [
|
||||||
|
call( vimsupport.FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( 1 ) )
|
||||||
|
] )
|
||||||
|
|
||||||
|
# Ensure that buffers are updated
|
||||||
|
eq_( another_file.lines, [
|
||||||
|
'another line1',
|
||||||
|
'second_file_replacement ACME line2',
|
||||||
|
] )
|
||||||
|
eq_( first_file.lines, [
|
||||||
|
'first_file_replacement line2',
|
||||||
|
'line3',
|
||||||
|
] )
|
||||||
|
|
||||||
|
# We open '2_another_file' as expected.
|
||||||
|
open_filename.assert_called_with( '2_another_file', {
|
||||||
|
'focus': True,
|
||||||
|
'fix': True,
|
||||||
|
'size': 10
|
||||||
|
} )
|
||||||
|
|
||||||
|
# And close it again, then show the preview window (note, we don't check exact
|
||||||
|
# calls because there are other calls which are checked elsewhere)
|
||||||
|
vim_command.assert_has_calls( [
|
||||||
|
call( 'lclose' ),
|
||||||
|
call( 'hide' ),
|
||||||
|
call( 'copen 2' ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# And update the quickfix list with each entry
|
||||||
|
vim_eval.assert_has_exact_calls( [
|
||||||
|
call( '&previewheight' ),
|
||||||
|
call( 'setqflist( {0} )'.format( json.dumps( [ {
|
||||||
|
'bufnr': 22,
|
||||||
|
'filename': '1_first_file',
|
||||||
|
'lnum': 1,
|
||||||
|
'col': 1,
|
||||||
|
'text': 'first_file_replacement ',
|
||||||
|
'type': 'F'
|
||||||
|
}, {
|
||||||
|
'bufnr': 19,
|
||||||
|
'filename': '2_another_file',
|
||||||
|
'lnum': 2,
|
||||||
|
'col': 1,
|
||||||
|
'text': 'second_file_replacement ',
|
||||||
|
'type': 'F'
|
||||||
|
} ] ) ) ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
# And it is ReplaceChunks that prints the message showing the number of
|
||||||
|
# changes
|
||||||
|
echo_text_vim_width.assert_has_exact_calls( [
|
||||||
|
call( 'Applied 2 changes' ),
|
||||||
|
] )
|
||||||
|
|
||||||
|
|
||||||
|
def _BuildChunk( start_line,
|
||||||
|
start_column,
|
||||||
|
end_line,
|
||||||
|
end_column,
|
||||||
|
replacement_text, filepath='test_file_name' ):
|
||||||
return {
|
return {
|
||||||
'range': {
|
'range': {
|
||||||
'start': {
|
'start': {
|
||||||
|
'filepath': filepath,
|
||||||
'line_num': start_line,
|
'line_num': start_line,
|
||||||
'column_num': start_column,
|
'column_num': start_column,
|
||||||
},
|
},
|
||||||
'end': {
|
'end': {
|
||||||
|
'filepath': filepath,
|
||||||
'line_num': end_line,
|
'line_num': end_line,
|
||||||
'column_num': end_column,
|
'column_num': end_column,
|
||||||
},
|
},
|
||||||
@ -582,14 +1058,14 @@ def _BuildChunk( start_line, start_column, end_line, end_column,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@patch( 'vim.command' )
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
@patch( 'vim.current' )
|
@patch( 'vim.current', new_callable=ExtendedMock)
|
||||||
def WriteToPreviewWindow_test( vim_current, vim_command ):
|
def WriteToPreviewWindow_test( vim_current, vim_command ):
|
||||||
vim_current.window.options.__getitem__ = MagicMock( return_value = True )
|
vim_current.window.options.__getitem__ = MagicMock( return_value = True )
|
||||||
|
|
||||||
vimsupport.WriteToPreviewWindow( "test" )
|
vimsupport.WriteToPreviewWindow( "test" )
|
||||||
|
|
||||||
vim_command.assert_has_calls( [
|
vim_command.assert_has_exact_calls( [
|
||||||
call( 'silent! pclose!' ),
|
call( 'silent! pclose!' ),
|
||||||
call( 'silent! pedit! _TEMP_FILE_' ),
|
call( 'silent! pedit! _TEMP_FILE_' ),
|
||||||
call( 'silent! wincmd P' ),
|
call( 'silent! wincmd P' ),
|
||||||
@ -598,7 +1074,9 @@ def WriteToPreviewWindow_test( vim_current, vim_command ):
|
|||||||
vim_current.buffer.__setitem__.assert_called_with(
|
vim_current.buffer.__setitem__.assert_called_with(
|
||||||
slice( None, None, None ), [ 'test' ] )
|
slice( None, None, None ), [ 'test' ] )
|
||||||
|
|
||||||
vim_current.buffer.options.__setitem__.assert_has_calls( [
|
vim_current.buffer.options.__setitem__.assert_has_exact_calls( [
|
||||||
|
call( 'modifiable', True ),
|
||||||
|
call( 'readonly', False ),
|
||||||
call( 'buftype', 'nofile' ),
|
call( 'buftype', 'nofile' ),
|
||||||
call( 'swapfile', False ),
|
call( 'swapfile', False ),
|
||||||
call( 'modifiable', False ),
|
call( 'modifiable', False ),
|
||||||
@ -616,14 +1094,14 @@ def WriteToPreviewWindow_MultiLine_test( vim_current ):
|
|||||||
slice( None, None, None ), [ 'test', 'test2' ] )
|
slice( None, None, None ), [ 'test', 'test2' ] )
|
||||||
|
|
||||||
|
|
||||||
@patch( 'vim.command' )
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
@patch( 'vim.current' )
|
@patch( 'vim.current', new_callable=ExtendedMock )
|
||||||
def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command ):
|
def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command ):
|
||||||
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
|
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
|
||||||
|
|
||||||
vimsupport.WriteToPreviewWindow( "test" )
|
vimsupport.WriteToPreviewWindow( "test" )
|
||||||
|
|
||||||
vim_command.assert_has_calls( [
|
vim_command.assert_has_exact_calls( [
|
||||||
call( 'silent! pclose!' ),
|
call( 'silent! pclose!' ),
|
||||||
call( 'silent! pedit! _TEMP_FILE_' ),
|
call( 'silent! pedit! _TEMP_FILE_' ),
|
||||||
call( 'silent! wincmd P' ),
|
call( 'silent! wincmd P' ),
|
||||||
@ -634,15 +1112,15 @@ def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command ):
|
|||||||
vim_current.buffer.options.__setitem__.assert_not_called()
|
vim_current.buffer.options.__setitem__.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@patch( 'vim.command' )
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
@patch( 'vim.current' )
|
@patch( 'vim.current', new_callable=ExtendedMock )
|
||||||
def WriteToPreviewWindow_JumpFail_MultiLine_test( vim_current, vim_command ):
|
def WriteToPreviewWindow_JumpFail_MultiLine_test( vim_current, vim_command ):
|
||||||
|
|
||||||
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
|
vim_current.window.options.__getitem__ = MagicMock( return_value = False )
|
||||||
|
|
||||||
vimsupport.WriteToPreviewWindow( "test\ntest2" )
|
vimsupport.WriteToPreviewWindow( "test\ntest2" )
|
||||||
|
|
||||||
vim_command.assert_has_calls( [
|
vim_command.assert_has_exact_calls( [
|
||||||
call( 'silent! pclose!' ),
|
call( 'silent! pclose!' ),
|
||||||
call( 'silent! pedit! _TEMP_FILE_' ),
|
call( 'silent! pedit! _TEMP_FILE_' ),
|
||||||
call( 'silent! wincmd P' ),
|
call( 'silent! wincmd P' ),
|
||||||
@ -689,7 +1167,9 @@ def BufferIsVisibleForFilename_test():
|
|||||||
eq_( vimsupport.BufferIsVisibleForFilename( 'another_filename' ), False )
|
eq_( vimsupport.BufferIsVisibleForFilename( 'another_filename' ), False )
|
||||||
|
|
||||||
|
|
||||||
@patch( 'vim.command', side_effect = MockVimCommand )
|
@patch( 'vim.command',
|
||||||
|
side_effect = MockVimCommand,
|
||||||
|
new_callable=ExtendedMock )
|
||||||
def CloseBuffersForFilename_test( vim_command ):
|
def CloseBuffersForFilename_test( vim_command ):
|
||||||
buffers = [
|
buffers = [
|
||||||
{
|
{
|
||||||
@ -709,14 +1189,14 @@ def CloseBuffersForFilename_test( vim_command ):
|
|||||||
with patch( 'vim.buffers', buffers ):
|
with patch( 'vim.buffers', buffers ):
|
||||||
vimsupport.CloseBuffersForFilename( 'some_filename' )
|
vimsupport.CloseBuffersForFilename( 'some_filename' )
|
||||||
|
|
||||||
vim_command.assert_has_calls( [
|
vim_command.assert_has_exact_calls( [
|
||||||
call( 'silent! bwipeout! 2' ),
|
call( 'silent! bwipeout! 2' ),
|
||||||
call( 'silent! bwipeout! 5' )
|
call( 'silent! bwipeout! 5' )
|
||||||
], any_order = True )
|
], any_order = True )
|
||||||
|
|
||||||
|
|
||||||
@patch( 'vim.command' )
|
@patch( 'vim.command', new_callable=ExtendedMock )
|
||||||
@patch( 'vim.current' )
|
@patch( 'vim.current', new_callable=ExtendedMock )
|
||||||
def OpenFilename_test( vim_current, vim_command ):
|
def OpenFilename_test( vim_current, vim_command ):
|
||||||
# Options used to open a logfile
|
# Options used to open a logfile
|
||||||
options = {
|
options = {
|
||||||
@ -728,18 +1208,18 @@ def OpenFilename_test( vim_current, vim_command ):
|
|||||||
|
|
||||||
vimsupport.OpenFilename( __file__, options )
|
vimsupport.OpenFilename( __file__, options )
|
||||||
|
|
||||||
vim_command.assert_has_calls( [
|
vim_command.assert_has_exact_calls( [
|
||||||
call( 'silent! 12split {0}'.format( __file__ ) ),
|
call( '12split {0}'.format( __file__ ) ),
|
||||||
call( "exec "
|
call( "exec "
|
||||||
"'au BufEnter <buffer> :silent! checktime {0}'".format( __file__ ) ),
|
"'au BufEnter <buffer> :silent! checktime {0}'".format( __file__ ) ),
|
||||||
call( 'silent! normal G zz' ),
|
call( 'silent! normal G zz' ),
|
||||||
call( 'silent! wincmd p' )
|
call( 'silent! wincmd p' )
|
||||||
] )
|
] )
|
||||||
|
|
||||||
vim_current.buffer.options.__setitem__.assert_has_calls( [
|
vim_current.buffer.options.__setitem__.assert_has_exact_calls( [
|
||||||
call( 'autoread', True ),
|
call( 'autoread', True ),
|
||||||
] )
|
] )
|
||||||
|
|
||||||
vim_current.window.options.__setitem__.assert_has_calls( [
|
vim_current.window.options.__setitem__.assert_has_exact_calls( [
|
||||||
call( 'winfixheight', True )
|
call( 'winfixheight', True )
|
||||||
] )
|
] )
|
||||||
|
@ -20,6 +20,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
from ycmd.utils import ToUtf8IfNeeded
|
from ycmd.utils import ToUtf8IfNeeded
|
||||||
from ycmd import user_options_store
|
from ycmd import user_options_store
|
||||||
|
|
||||||
@ -28,6 +29,13 @@ BUFFER_COMMAND_MAP = { 'same-buffer' : 'edit',
|
|||||||
'vertical-split' : 'vsplit',
|
'vertical-split' : 'vsplit',
|
||||||
'new-tab' : 'tabedit' }
|
'new-tab' : 'tabedit' }
|
||||||
|
|
||||||
|
FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT = (
|
||||||
|
'The requested operation will apply changes to {0} files which are not '
|
||||||
|
'currently open. This will therefore open {0} new files in the hidden '
|
||||||
|
'buffers. The quickfix list can then be used to review the changes. No '
|
||||||
|
'files will be written to disk. Do you wish to continue?' )
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
# See the comment in CurrentColumn about the calculation for the line and
|
# See the comment in CurrentColumn about the calculation for the line and
|
||||||
@ -236,6 +244,15 @@ def SetLocationList( diagnostics ):
|
|||||||
vim.eval( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
|
vim.eval( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
|
||||||
|
|
||||||
|
|
||||||
|
def SetQuickFixList( quickfix_list, display=False ):
|
||||||
|
"""list should be in qflist format: see ":h setqflist" for details"""
|
||||||
|
vim.eval( 'setqflist( {0} )'.format( json.dumps( quickfix_list ) ) )
|
||||||
|
|
||||||
|
if display:
|
||||||
|
vim.command( 'copen {0}'.format( len( quickfix_list ) ) )
|
||||||
|
JumpToPreviousWindow()
|
||||||
|
|
||||||
|
|
||||||
def ConvertDiagnosticsToQfList( diagnostics ):
|
def ConvertDiagnosticsToQfList( diagnostics ):
|
||||||
def ConvertDiagnosticToQfFormat( diagnostic ):
|
def ConvertDiagnosticToQfFormat( diagnostic ):
|
||||||
# See :h getqflist for a description of the dictionary fields.
|
# See :h getqflist for a description of the dictionary fields.
|
||||||
@ -428,6 +445,8 @@ def PresentDialog( message, choices, default_choice_index = 0 ):
|
|||||||
|
|
||||||
|
|
||||||
def Confirm( message ):
|
def Confirm( message ):
|
||||||
|
"""Display |message| with Ok/Cancel operations. Returns True if the user
|
||||||
|
selects Ok"""
|
||||||
return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 )
|
return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 )
|
||||||
|
|
||||||
|
|
||||||
@ -449,6 +468,7 @@ def EchoTextVimWidth( text ):
|
|||||||
old_ruler = GetIntValue( '&ruler' )
|
old_ruler = GetIntValue( '&ruler' )
|
||||||
old_showcmd = GetIntValue( '&showcmd' )
|
old_showcmd = GetIntValue( '&showcmd' )
|
||||||
vim.command( 'set noruler noshowcmd' )
|
vim.command( 'set noruler noshowcmd' )
|
||||||
|
vim.command( 'redraw' )
|
||||||
|
|
||||||
EchoText( truncated_text, False )
|
EchoText( truncated_text, False )
|
||||||
|
|
||||||
@ -490,9 +510,145 @@ def GetIntValue( variable ):
|
|||||||
return int( vim.eval( variable ) )
|
return int( vim.eval( variable ) )
|
||||||
|
|
||||||
|
|
||||||
def ReplaceChunksList( chunks, vim_buffer = None ):
|
def _SortChunksByFile( chunks ):
|
||||||
if vim_buffer is None:
|
"""Sort the members of the list |chunks| (which must be a list of dictionaries
|
||||||
vim_buffer = vim.current.buffer
|
conforming to ycmd.responses.FixItChunk) by their filepath. Returns a new
|
||||||
|
list in arbitrary order."""
|
||||||
|
|
||||||
|
chunks_by_file = defaultdict( list )
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
filepath = chunk[ 'range' ][ 'start' ][ 'filepath' ]
|
||||||
|
chunks_by_file[ filepath ].append( chunk )
|
||||||
|
|
||||||
|
return chunks_by_file
|
||||||
|
|
||||||
|
|
||||||
|
def _GetNumNonVisibleFiles( file_list ):
|
||||||
|
"""Returns the number of file in the iterable list of files |file_list| which
|
||||||
|
are not curerntly open in visible windows"""
|
||||||
|
return len(
|
||||||
|
[ f for f in file_list
|
||||||
|
if not BufferIsVisible( GetBufferNumberForFilename( f, False ) ) ] )
|
||||||
|
|
||||||
|
|
||||||
|
def _OpenFileInSplitIfNeeded( filepath ):
|
||||||
|
"""Ensure that the supplied filepath is open in a visible window, opening a
|
||||||
|
new split if required. Returns the buffer number of the file and an indication
|
||||||
|
of whether or not a new split was opened.
|
||||||
|
|
||||||
|
If the supplied filename is already open in a visible window, return just
|
||||||
|
return its buffer number. If the supplied file is not visible in a window
|
||||||
|
in the current tab, opens it in a new vertical split.
|
||||||
|
|
||||||
|
Returns a tuple of ( buffer_num, split_was_opened ) indicating the buffer
|
||||||
|
number and whether or not this method created a new split. If the user opts
|
||||||
|
not to open a file, or if opening fails, this method raises RuntimeError,
|
||||||
|
otherwise, guarantees to return a visible buffer number in buffer_num."""
|
||||||
|
|
||||||
|
buffer_num = GetBufferNumberForFilename( filepath, False )
|
||||||
|
|
||||||
|
# We only apply changes in the current tab page (i.e. "visible" windows).
|
||||||
|
# Applying changes in tabs does not lead to a better user experience, as the
|
||||||
|
# quickfix list no longer works as you might expect (doesn't jump into other
|
||||||
|
# tabs), and the complexity of choosing where to apply edits is significant.
|
||||||
|
if BufferIsVisible( buffer_num ):
|
||||||
|
# file is already open and visible, just return that buffer number (and an
|
||||||
|
# idicator that we *didn't* open a split)
|
||||||
|
return ( buffer_num, False )
|
||||||
|
|
||||||
|
# The file is not open in a visible window, so we open it in a split.
|
||||||
|
# We open the file with a small, fixed height. This means that we don't
|
||||||
|
# make the current buffer the smallest after a series of splits.
|
||||||
|
OpenFilename( filepath, {
|
||||||
|
'focus': True,
|
||||||
|
'fix': True,
|
||||||
|
'size': GetIntValue( '&previewheight' ),
|
||||||
|
} )
|
||||||
|
|
||||||
|
# OpenFilename returns us to the original cursor location. This is what we
|
||||||
|
# want, because we don't want to disorientate the user, but we do need to
|
||||||
|
# know the (now open) buffer number for the filename
|
||||||
|
buffer_num = GetBufferNumberForFilename( filepath, False )
|
||||||
|
if not BufferIsVisible( buffer_num ):
|
||||||
|
# This happens, for example, if there is a swap file and the user
|
||||||
|
# selects the "Quit" or "Abort" options. We just raise an exception to
|
||||||
|
# make it clear to the user that the abort has left potentially
|
||||||
|
# partially-applied changes.
|
||||||
|
raise RuntimeError(
|
||||||
|
'Unable to open file: {0}\nFixIt/Refactor operation '
|
||||||
|
'aborted prior to completion. Your files have not been '
|
||||||
|
'fully updated. Please use undo commands to revert the '
|
||||||
|
'applied changes.'.format( filepath ) )
|
||||||
|
|
||||||
|
# We opened this file in a split
|
||||||
|
return ( buffer_num, True )
|
||||||
|
|
||||||
|
|
||||||
|
def ReplaceChunks( chunks ):
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
If a file specified in a particular chunk is not currently open in a visible
|
||||||
|
buffer (i.e., one in a window visible in the current tab), we:
|
||||||
|
- issue a warning to the user that we're going to open new files (and offer
|
||||||
|
her the option to abort cleanly)
|
||||||
|
- open the file in a new split, make the changes, then hide the buffer.
|
||||||
|
|
||||||
|
If for some reason a file could not be opened or changed, raises RuntimeError.
|
||||||
|
Otherwise, returns no meaningful value."""
|
||||||
|
|
||||||
|
# We apply the edits file-wise for efficiency, and because we must track the
|
||||||
|
# file-wise offset deltas (caused by the modifications to the text).
|
||||||
|
chunks_by_file = _SortChunksByFile( chunks )
|
||||||
|
|
||||||
|
# We sort the file list simply to enable repeatable testing
|
||||||
|
sorted_file_list = sorted( chunks_by_file.iterkeys() )
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
FIXIT_OPENING_BUFFERS_MESSAGE_FORMAT.format( num_files_to_open ) ):
|
||||||
|
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.
|
||||||
|
locations = []
|
||||||
|
|
||||||
|
for filepath in sorted_file_list:
|
||||||
|
( buffer_num, close_window ) = _OpenFileInSplitIfNeeded( filepath )
|
||||||
|
|
||||||
|
ReplaceChunksInBuffer( chunks_by_file[ filepath ],
|
||||||
|
vim.buffers[ buffer_num ],
|
||||||
|
locations )
|
||||||
|
|
||||||
|
# When opening tons of files, we don't want to have a split for each new
|
||||||
|
# file, as this simply does not scale, so we open the window, make the
|
||||||
|
# edits, then hide the window.
|
||||||
|
if close_window:
|
||||||
|
# Some plugins (I'm looking at you, syntastic) might open a location list
|
||||||
|
# for the window we just opened. We don't want that location list hanging
|
||||||
|
# around, so we close it. lclose is a no-op if there is no location list.
|
||||||
|
vim.command( 'lclose' )
|
||||||
|
|
||||||
|
# Note that this doesn't lose our changes. It simply "hides" the buffer,
|
||||||
|
# which can later be re-accessed via the quickfix list or `:ls`
|
||||||
|
vim.command( 'hide' )
|
||||||
|
|
||||||
|
# Open the quickfix list, populated with entries for each location we changed.
|
||||||
|
if locations:
|
||||||
|
SetQuickFixList( locations, True )
|
||||||
|
|
||||||
|
EchoTextVimWidth( "Applied " + str( len( chunks ) ) + " changes" )
|
||||||
|
|
||||||
|
|
||||||
|
def ReplaceChunksInBuffer( chunks, vim_buffer, locations ):
|
||||||
|
"""Apply changes in |chunks| to the buffer-like object |buffer|. Append each
|
||||||
|
chunk's start to the list |locations|"""
|
||||||
|
|
||||||
# We need to track the difference in length, but ensuring we apply fixes
|
# We need to track the difference in length, but ensuring we apply fixes
|
||||||
# in ascending order of insertion point.
|
# in ascending order of insertion point.
|
||||||
@ -519,7 +675,8 @@ def ReplaceChunksList( chunks, vim_buffer = None ):
|
|||||||
chunk[ 'range' ][ 'end' ],
|
chunk[ 'range' ][ 'end' ],
|
||||||
chunk[ 'replacement_text' ],
|
chunk[ 'replacement_text' ],
|
||||||
line_delta, char_delta,
|
line_delta, char_delta,
|
||||||
vim_buffer )
|
vim_buffer,
|
||||||
|
locations )
|
||||||
line_delta += new_line_delta
|
line_delta += new_line_delta
|
||||||
char_delta += new_char_delta
|
char_delta += new_char_delta
|
||||||
|
|
||||||
@ -534,11 +691,12 @@ def ReplaceChunksList( chunks, vim_buffer = None ):
|
|||||||
# returns the delta (in lines and characters) that any position after the end
|
# returns the delta (in lines and characters) that any position after the end
|
||||||
# needs to be adjusted by.
|
# needs to be adjusted by.
|
||||||
def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
|
def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
|
||||||
vim_buffer ):
|
vim_buffer, locations = None ):
|
||||||
# ycmd's results are all 1-based, but vim's/python's are all 0-based
|
# ycmd's results are all 1-based, but vim's/python's are all 0-based
|
||||||
# (so we do -1 on all of the values)
|
# (so we do -1 on all of the values)
|
||||||
start_line = start[ 'line_num' ] - 1 + line_delta
|
start_line = start[ 'line_num' ] - 1 + line_delta
|
||||||
end_line = end[ 'line_num' ] - 1 + line_delta
|
end_line = end[ 'line_num' ] - 1 + line_delta
|
||||||
|
|
||||||
source_lines_count = end_line - start_line + 1
|
source_lines_count = end_line - start_line + 1
|
||||||
start_column = start[ 'column_num' ] - 1 + char_delta
|
start_column = start[ 'column_num' ] - 1 + char_delta
|
||||||
end_column = end[ 'column_num' ] - 1
|
end_column = end[ 'column_num' ] - 1
|
||||||
@ -563,6 +721,17 @@ def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
|
|||||||
|
|
||||||
vim_buffer[ start_line : end_line + 1 ] = replacement_lines[:]
|
vim_buffer[ start_line : end_line + 1 ] = replacement_lines[:]
|
||||||
|
|
||||||
|
if locations is not None:
|
||||||
|
locations.append( {
|
||||||
|
'bufnr': vim_buffer.number,
|
||||||
|
'filename': vim_buffer.name,
|
||||||
|
# line and column numbers are 1-based in qflist
|
||||||
|
'lnum': start_line + 1,
|
||||||
|
'col': start_column + 1,
|
||||||
|
'text': replacement_text,
|
||||||
|
'type': 'F',
|
||||||
|
} )
|
||||||
|
|
||||||
new_line_delta = replacement_lines_count - source_lines_count
|
new_line_delta = replacement_lines_count - source_lines_count
|
||||||
return ( new_line_delta, new_char_delta )
|
return ( new_line_delta, new_char_delta )
|
||||||
|
|
||||||
@ -710,30 +879,39 @@ def OpenFilename( filename, options = {} ):
|
|||||||
size = ( options.get( 'size', '' ) if command in [ 'split', 'vsplit' ] else
|
size = ( options.get( 'size', '' ) if command in [ 'split', 'vsplit' ] else
|
||||||
'' )
|
'' )
|
||||||
focus = options.get( 'focus', False )
|
focus = options.get( 'focus', False )
|
||||||
watch = options.get( 'watch', False )
|
|
||||||
position = options.get( 'position', 'start' )
|
|
||||||
|
|
||||||
# There is no command in Vim to return to the previous tab so we need to
|
# There is no command in Vim to return to the previous tab so we need to
|
||||||
# remember the current tab if needed.
|
# remember the current tab if needed.
|
||||||
if not focus and command is 'tabedit':
|
if not focus and command is 'tabedit':
|
||||||
previous_tab = GetIntValue( 'tabpagenr()' )
|
previous_tab = GetIntValue( 'tabpagenr()' )
|
||||||
|
else:
|
||||||
|
previous_tab = None
|
||||||
|
|
||||||
# Open the file
|
# Open the file
|
||||||
CheckFilename( filename )
|
CheckFilename( filename )
|
||||||
vim.command( 'silent! {0}{1} {2}'.format( size, command, filename ) )
|
try:
|
||||||
|
vim.command( '{0}{1} {2}'.format( size, command, filename ) )
|
||||||
|
# When the file we are trying to jump to has a swap file,
|
||||||
|
# Vim opens swap-exists-choices dialog and throws vim.error with E325 error,
|
||||||
|
# or KeyboardInterrupt after user selects one of the options which actually
|
||||||
|
# opens the file (Open read-only/Edit anyway).
|
||||||
|
except vim.error as e:
|
||||||
|
if 'E325' not in str( e ):
|
||||||
|
raise
|
||||||
|
|
||||||
if command is 'split':
|
# Otherwise, the user might have chosen Quit. This is detectable by the
|
||||||
vim.current.window.options[ 'winfixheight' ] = options.get( 'fix', False )
|
# current file not being the target file
|
||||||
if command is 'vsplit':
|
if filename != GetCurrentBufferFilepath():
|
||||||
vim.current.window.options[ 'winfixwidth' ] = options.get( 'fix', False )
|
return
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Raised when the user selects "Abort" after swap-exists-choices
|
||||||
|
return
|
||||||
|
|
||||||
if watch:
|
_SetUpLoadedBuffer( command,
|
||||||
vim.current.buffer.options[ 'autoread' ] = True
|
filename,
|
||||||
vim.command( "exec 'au BufEnter <buffer> :silent! checktime {0}'"
|
options.get( 'fix', False ),
|
||||||
.format( filename ) )
|
options.get( 'position', 'start' ),
|
||||||
|
options.get( 'watch', False ) )
|
||||||
if position is 'end':
|
|
||||||
vim.command( 'silent! normal G zz' )
|
|
||||||
|
|
||||||
# Vim automatically set the focus to the opened file so we need to get the
|
# Vim automatically set the focus to the opened file so we need to get the
|
||||||
# focus back (if the focus option is disabled) when opening a new tab or
|
# focus back (if the focus option is disabled) when opening a new tab or
|
||||||
@ -743,3 +921,22 @@ def OpenFilename( filename, options = {} ):
|
|||||||
JumpToTab( previous_tab )
|
JumpToTab( previous_tab )
|
||||||
if command in [ 'split', 'vsplit' ]:
|
if command in [ 'split', 'vsplit' ]:
|
||||||
JumpToPreviousWindow()
|
JumpToPreviousWindow()
|
||||||
|
|
||||||
|
|
||||||
|
def _SetUpLoadedBuffer( command, filename, fix, position, watch ):
|
||||||
|
"""After opening a buffer, configure it according to the supplied options,
|
||||||
|
which are as defined by the OpenFilename method."""
|
||||||
|
|
||||||
|
if command is 'split':
|
||||||
|
vim.current.window.options[ 'winfixheight' ] = fix
|
||||||
|
if command is 'vsplit':
|
||||||
|
vim.current.window.options[ 'winfixwidth' ] = fix
|
||||||
|
|
||||||
|
if watch:
|
||||||
|
vim.current.buffer.options[ 'autoread' ] = True
|
||||||
|
vim.command( "exec 'au BufEnter <buffer> :silent! checktime {0}'"
|
||||||
|
.format( filename ) )
|
||||||
|
|
||||||
|
if position is 'end':
|
||||||
|
vim.command( 'silent! normal G zz' )
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user