diff --git a/README.md b/README.md index d441ffec..8205cb13 100644 --- a/README.md +++ b/README.md @@ -655,7 +655,15 @@ You may want to map this command to a key; try putting `nnoremap ### The `:YcmDiags` command Calling this command will fill Vim's `locationlist` with errors or warnings if -any were detected in your file and then open it. +any were detected in your file and then open it. If a given error or warning can +be fixed by a call to `:YcmCompleter FixIt`, then ` (FixIt available)` is +appended to the error or warning text. See the `FixIt` completer subcommand for +more information. + +NOTE: The absense of ` (FixIt available)` does not strictly imply a fix-it is +not available as not all completers are able to provide this indication. For +example, the c-sharp completer provides many fix-its but does not add this +additional indication. The `g:ycm_open_loclist_on_ycm_diags` option can be used to prevent the location list from opening, but still have it filled with new diagnostic data. See the @@ -817,6 +825,40 @@ NOTE: Causes reparsing of the current translation unit. Supported in filetypes: `c, cpp, objc, objcpp` +### The `FixIt` subcommand + +Where available, attempts to make changes to the buffer to correct the +diagnostic closest to the cursor position. + +Completers which provide diagnostics may also provide trivial modifications to +the source in order to correct the diagnostic. Examples include syntax errors +such as missing trailing semi-colons, spurious characters, or other errors which +the semantic engine can deterministically suggest corrections. + +If no fix-it is available for the current line, or there is no diagnostic on the +current line, this command has no effect on the current buffer. If any +modifications are made, the number of changes made to the buffer is echo'd and +the user may use the editor's undo command to revert. + +When a diagnostic is available, and `g:ycm_echo_current_diagnostic` is set to 1, +then the text ` (FixIt)` is appended to the echo'd diagnostic when the +completer is able to add this indication. The text ` (FixIt available)` is +also appended to the diagnostic text in the output of the `:YcmDiags` command +for any diagnostics with available fix-its (where the completer can provide this +indication). + +NOTE: Causes re-parsing of the current translation unit. + +NOTE: After applying a fix-it, the diagnostics UI is not immediately updated. +This is due to a technical restriction in vim, and moving the cursor, or issuing +the the `:YcmForceCompileAndDiagnostics` command will refresh the diagnostics. +Repeated invocations of the `FixIt` command on a given line, however, _do_ apply +all diagnostics as expected without requiring refreshing of the diagnostics UI. +This is particularly useful where there are multiple diagnostics on one line, or +where after fixing one diagnostic, another fix-it is available. + +Supported in filetypes: `c, cpp, objc, objcpp, cs` + ### The `StartServer` subcommand Starts the semantic-engine-as-localhost-server for those semantic engines that @@ -1075,7 +1117,8 @@ Default: `1` ### The `g:ycm_echo_current_diagnostic` option When this option is set, YCM will echo the text of the diagnostic present on the -current line when you move your cursor to that line. +current line when you move your cursor to that line. If a `FixIt` is available +for the current diagnostic, then ` (FixIt)` is appended. This option is part of the Syntastic compatibility layer; if the option is not set, YCM will fall back to the value of the `g:syntastic_echo_current_error` diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index 7aeb98af..6e766e99 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -36,6 +36,8 @@ class CommandRequest( BaseRequest ): else 'filetype_default' ) self._is_goto_command = ( self._arguments and self._arguments[ 0 ].startswith( 'GoTo' ) ) + self._is_fixit_command = ( + self._arguments and self._arguments[ 0 ].startswith( 'FixIt' ) ) self._response = None @@ -55,23 +57,73 @@ class CommandRequest( BaseRequest ): def Response( self ): return self._response - def RunPostCommandActionsIfNeeded( self ): if not self.Done() or not self._response: return if self._is_goto_command: - if isinstance( self._response, list ): - defs = [ _BuildQfListItem( x ) for x in self._response ] - vim.eval( 'setqflist( %s )' % repr( defs ) ) - vim.eval( 'youcompleteme#OpenGoToList()' ) - else: - vimsupport.JumpToLocation( self._response[ 'filepath' ], - self._response[ 'line_num' ], - self._response[ 'column_num' ] ) + self._HandleGotoResponse() + elif self._is_fixit_command: + self._HandleFixitResponse() elif 'message' in self._response: - vimsupport.EchoText( self._response['message'] ) + self._HandleMessageResponse() + def _HandleGotoResponse( self ): + if isinstance( self._response, list ): + defs = [ _BuildQfListItem( x ) for x in self._response ] + vim.eval( 'setqflist( %s )' % repr( defs ) ) + vim.eval( 'youcompleteme#OpenGoToList()' ) + else: + vimsupport.JumpToLocation( self._response[ 'filepath' ], + self._response[ 'line_num' ], + self._response[ 'column_num' ] ) + + def _HandleFixitResponse( self ): + if not len( self._response[ 'fixits' ] ): + vimsupport.EchoText( "No fixits found for current line" ) + else: + fixit = self._response[ 'fixits' ][ 0 ] + + # We need to track the difference in length, but ensuring we apply fixes + # in ascending order of insertion point. + fixit[ 'chunks' ].sort( key = lambda chunk: ( + str(chunk[ 'range' ][ 'start' ][ 'line_num' ]) + + ',' + + str(chunk[ 'range' ][ 'start' ][ 'column_num' ]) + )) + + # Remember the line number we're processing. Negative line number means we + # haven't processed any lines yet (by nature of being not equal to any + # real line number). + last_line = -1 + + # Counter of changes applied, so the user has a mental picture of the + # undo history this change is creating. + num_fixed = 0 + line_delta = 0 + for chunk in fixit[ 'chunks' ]: + if chunk[ 'range' ][ 'start' ][ 'line_num' ] != last_line: + # If this chunk is on a different line than the previous chunk, + # then ignore previous deltas (as offsets won't have changed). + last_line = chunk[ 'range' ][ 'end' ][ 'line_num' ] + char_delta = 0 + + (new_line_delta, new_char_delta) = vimsupport.ReplaceChunk( + chunk[ 'range' ][ 'start' ], + chunk[ 'range' ][ 'end' ], + chunk[ 'replacement_text' ], + line_delta, char_delta ) + line_delta += new_line_delta + char_delta += new_char_delta + + num_fixed = num_fixed + 1 + + vimsupport.EchoTextVimWidth("FixIt applied " + + str(num_fixed) + + " changes") + + def _HandleMessageResponse( self ): + vimsupport.EchoText( self._response[ 'message' ] ) def SendCommandRequest( arguments, completer ): request = CommandRequest( arguments, completer ) diff --git a/python/ycm/diagnostic_interface.py b/python/ycm/diagnostic_interface.py index 62c1db22..52afae28 100644 --- a/python/ycm/diagnostic_interface.py +++ b/python/ycm/diagnostic_interface.py @@ -43,7 +43,6 @@ class DiagnosticInterface( object ): if self._user_options[ 'echo_current_diagnostic' ]: self._EchoDiagnosticForLine( line ) - def UpdateWithNewDiagnostics( self, diags ): normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags ] self._buffer_number_to_line_to_diags = _ConvertDiagListToDict( @@ -62,7 +61,6 @@ class DiagnosticInterface( object ): vimsupport.SetLocationList( vimsupport.ConvertDiagnosticsToQfList( normalized_diags ) ) - def _EchoDiagnosticForLine( self, line_num ): buffer_num = vim.current.buffer.number diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ] @@ -72,7 +70,12 @@ class DiagnosticInterface( object ): vimsupport.EchoText( '', False ) self._diag_message_needs_clearing = False return - vimsupport.EchoTextVimWidth( diags[ 0 ][ 'text' ] ) + + text = diags[ 0 ][ 'text' ] + if diags[ 0 ].get( 'fixit_available', False ): + text += ' (FixIt)' + + vimsupport.EchoTextVimWidth( text ) self._diag_message_needs_clearing = True diff --git a/python/ycm/tests/vimsupport_test.py b/python/ycm/tests/vimsupport_test.py new file mode 100644 index 00000000..c0c4d5eb --- /dev/null +++ b/python/ycm/tests/vimsupport_test.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +# +# Copyright (C) 2015 YouCompleteMe contributors +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +from ycm import vimsupport +from nose.tools import eq_ + +def ReplaceChunk_SingleLine_Repl_1_test(): + # Replace with longer range + # 12345678901234567 + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 1, 1, 5 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'How long', + 0, + 0, + result_buffer ) + + eq_( [ "How long is a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 4 ) + + # and replace again, using delta + start, end = _BuildLocations( 1, 10, 1, 11 ) + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + ' piece of ', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( [ 'How long is a piece of string' ], result_buffer ) + eq_( new_line_offset, 0 ) + eq_( new_char_offset, 9 ) + eq_( line_offset, 0 ) + eq_( char_offset, 13 ) + + # and once more, for luck + start, end = _BuildLocations( 1, 11, 1, 17 ) + + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + 'pie', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( ['How long is a piece of pie' ], result_buffer ) + eq_( new_line_offset, 0 ) + eq_( new_char_offset, -3 ) + eq_( line_offset, 0 ) + eq_( char_offset, 10 ) + +def ReplaceChunk_SingleLine_Repl_2_test(): + # Replace with shorter range + # 12345678901234567 + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 11, 1, 17 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'test', + 0, + 0, + result_buffer ) + + eq_( [ "This is a test" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -2 ) + +def ReplaceChunk_SingleLine_Repl_3_test(): + # Replace with equal range + # 12345678901234567 + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 6, 1, 8 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'be', + 0, + 0, + result_buffer ) + + eq_( [ "This be a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 0 ) + +def ReplaceChunk_SingleLine_Add_1_test(): + # Insert at start + result_buffer = [ "is a string" ] + start, end = _BuildLocations( 1, 1, 1, 1 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'This ', + 0, + 0, + result_buffer ) + + eq_( [ "This is a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 5 ) + +def ReplaceChunk_SingleLine_Add_2_test(): + # Insert at end + result_buffer = [ "This is a " ] + start, end = _BuildLocations( 1, 11, 1, 11 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'string', + 0, + 0, + result_buffer ) + + eq_( [ "This is a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 6 ) + +def ReplaceChunk_SingleLine_Add_3_test(): + # Insert in the middle + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 8, 1, 8 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + ' not', + 0, + 0, + result_buffer ) + + eq_( [ "This is not a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 4 ) + +def ReplaceChunk_SingleLine_Del_1_test(): + # Delete from start + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 1, 1, 6 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + '', + 0, + 0, + result_buffer ) + + eq_( [ "is a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -5 ) + +def ReplaceChunk_SingleLine_Del_2_test(): + # Delete from end + result_buffer = [ "This is a string" ] + start, end = _BuildLocations( 1, 10, 1, 18 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + '', + 0, + 0, + result_buffer ) + + eq_( [ "This is a" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -8 ) + +def ReplaceChunk_SingleLine_Del_3_test(): + # Delete from middle + result_buffer = [ "This is not a string" ] + start, end = _BuildLocations( 1, 9, 1, 13 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + '', + 0, + 0, + result_buffer ) + + eq_( [ "This is a string" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -4 ) + +def ReplaceChunk_RemoveSingleLine_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 1, 3, 1 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, '', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, -1 ) + eq_( char_offset, 0 ) + + +def ReplaceChunk_SingleToMultipleLines_test(): + result_buffer = [ "aAa", + "aBa", + "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'Eb\nbF', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", + "aEb", + "bFBa", + "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 1 ) + eq_( char_offset, 1 ) + + # now make another change to the "2nd" line + start, end = _BuildLocations( 2, 3, 2, 4 ) + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + 'cccc', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( [ "aAa", "aEb", "bFBcccc", "aCa" ], result_buffer ) + eq_( line_offset, 1 ) + eq_( char_offset, 4 ) + + +def ReplaceChunk_SingleToMultipleLines2_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'Eb\nbFb\nG', + 0, + 0, + result_buffer ) + expected_buffer = [ "aAa", "aEb" ,"bFb", "GBa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 2 ) + eq_( char_offset, 0 ) + + +def ReplaceChunk_SingleToMultipleLines3_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'Eb\nbFb\nbGb', + 0, + 0, + result_buffer ) + expected_buffer = [ "aAa", "aEb" ,"bFb", "bGbBa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 2 ) + eq_( char_offset, 2 ) + +def ReplaceChunk_SingleToMultipleLinesReplace_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 1, 2, 1, 4 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'Eb\nbFb\nbGb', + 0, + 0, + result_buffer ) + expected_buffer = [ "aEb", "bFb", "bGb", "aBa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 2 ) + eq_( char_offset, 0 ) + +def ReplaceChunk_SingleToMultipleLinesReplace_2_test(): + result_buffer = [ "aAa", + "aBa", + "aCa" ] + start, end = _BuildLocations( 1, 2, 1, 4 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'Eb\nbFb\nbGb', + 0, + 0, + result_buffer ) + expected_buffer = [ "aEb", + "bFb", + "bGb", + "aBa", + "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 2 ) + eq_( char_offset, 0 ) + + # now do a subsequent change (insert at end of line "1") + start, end = _BuildLocations( 1, 4, 1, 4 ) + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + 'cccc', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( [ "aEb", + "bFb", + "bGbcccc", + "aBa", + "aCa" ], result_buffer ) + + eq_( line_offset, 2 ) + eq_( char_offset, 4 ) + + + +def ReplaceChunk_MultipleLinesToSingleLine_test(): + result_buffer = [ "aAa", "aBa", "aCaaaa" ] + start, end = _BuildLocations( 2, 2, 3, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'E', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "aECaaaa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, -1 ) + eq_( char_offset, 1 ) + + # make another modification applying offsets + start, end = _BuildLocations( 3, 3, 3, 4 ) + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + 'cccc', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( [ "aAa", "aECccccaaa" ], result_buffer ) + eq_( line_offset, -1 ) + eq_( char_offset, 4 ) + + # and another, for luck + start, end = _BuildLocations( 3, 4, 3, 5 ) + ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk( + start, + end, + 'dd\ndd', + line_offset, + char_offset, + result_buffer ) + + line_offset += new_line_offset + char_offset += new_char_offset + + eq_( [ "aAa", "aECccccdd", "ddaa" ], result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -2 ) + + +def ReplaceChunk_MultipleLinesToSameMultipleLines_test(): + result_buffer = [ "aAa", "aBa", "aCa", "aDe" ] + start, end = _BuildLocations( 2, 2, 3, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'Eb\nbF', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "aEb", "bFCa", "aDe" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 1 ) + + +def ReplaceChunk_MultipleLinesToMoreMultipleLines_test(): + result_buffer = [ "aAa", "aBa", "aCa", "aDe" ] + start, end = _BuildLocations( 2, 2, 3, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'Eb\nbFb\nbG', + 0, + 0, + result_buffer ) + expected_buffer = [ "aAa", "aEb", "bFb", "bGCa", "aDe" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 1 ) + eq_( char_offset, 1 ) + + +def ReplaceChunk_MultipleLinesToLessMultipleLines_test(): + result_buffer = [ "aAa", "aBa", "aCa", "aDe" ] + start, end = _BuildLocations( 1, 2, 3, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'Eb\nbF', + 0, 0, result_buffer ) + expected_buffer = [ "aEb", "bFCa", "aDe" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, -1 ) + eq_( char_offset, 1 ) + + +def ReplaceChunk_MultipleLinesToEvenLessMultipleLines_test(): + result_buffer = [ "aAa", "aBa", "aCa", "aDe" ] + start, end = _BuildLocations( 1, 2, 4, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'Eb\nbF', + 0, 0, result_buffer ) + expected_buffer = [ "aEb", "bFDe" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, -2 ) + eq_( char_offset, 1 ) + + +def ReplaceChunk_SpanBufferEdge_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 1, 1, 1, 3 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'bDb', + 0, 0, result_buffer ) + expected_buffer = [ "bDba", "aBa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 1 ) + + +def ReplaceChunk_DeleteTextInLine_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 3 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, '', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "aa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, -1 ) + + +def ReplaceChunk_AddTextInLine_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'bDb', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "abDbBa", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 3 ) + + +def ReplaceChunk_ReplaceTextInLine_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 2, 2, 2, 3 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'bDb', + 0, 0, result_buffer ) + expected_buffer = [ "aAa", "abDba", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 2 ) + + +def ReplaceChunk_SingleLineOffsetWorks_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 1, 1, 1, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'bDb', + 1, 1, result_buffer ) + expected_buffer = [ "aAa", "abDba", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 0 ) + eq_( char_offset, 2 ) + + +def ReplaceChunk_SingleLineToMultipleLinesOffsetWorks_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 1, 1, 1, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'Db\nE', + 1, 1, result_buffer ) + expected_buffer = [ "aAa", "aDb", "Ea", "aCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 1 ) + eq_( char_offset, -1 ) + + +def ReplaceChunk_MultipleLinesToSingleLineOffsetWorks_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 1, 1, 2, 2 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, 'bDb', + 1, 1, result_buffer ) + expected_buffer = [ "aAa", "abDbCa" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, -1 ) + eq_( char_offset, 3 ) + + +def ReplaceChunk_MultipleLineOffsetWorks_test(): + result_buffer = [ "aAa", "aBa", "aCa" ] + start, end = _BuildLocations( 3, 1, 4, 3 ) + ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, + end, + 'bDb\nbEb\nbFb', + -1, + 1, + result_buffer ) + expected_buffer = [ "aAa", "abDb", "bEb", "bFba" ] + eq_( expected_buffer, result_buffer ) + eq_( line_offset, 1 ) + eq_( char_offset, 1 ) + + +def _BuildLocations( start_line, start_column, end_line, end_column ): + return { + 'line_num' : start_line, + 'column_num': start_column, + }, { + 'line_num' : end_line, + 'column_num': end_column, + } diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 0de7ac85..62200003 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -245,11 +245,15 @@ def ConvertDiagnosticsToQfList( diagnostics ): if line_num < 1: line_num = 1 + text = diagnostic[ 'text' ] + if diagnostic.get( 'fixit_available', False ): + text += ' (FixIt available)' + return { 'bufnr' : GetBufferNumberForFilename( location[ 'filepath' ] ), 'lnum' : line_num, 'col' : location[ 'column_num' ], - 'text' : ToUtf8IfNeeded( diagnostic[ 'text' ] ), + 'text' : ToUtf8IfNeeded( text ), 'type' : diagnostic[ 'kind' ][ 0 ], 'valid' : 1 } @@ -454,3 +458,48 @@ def GetBoolValue( variable ): def GetIntValue( variable ): return int( vim.eval( variable ) ) + +# Replace the chunk of text specified by a contiguous range with the supplied +# text. +# * start and end are objects with line_num and column_num properties +# * the range is inclusive +# * indices are all 1-based +# * the returned character delta is the delta for the last line +# +# returns the delta (in lines and characters) that any position after the end +# needs to be adjusted by. +def ReplaceChunk( start, end, replacement_text, line_delta, char_delta, + vim_buffer = None ): + if vim_buffer is None: + vim_buffer = vim.current.buffer + + # 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) + start_line = start[ 'line_num' ] - 1 + line_delta + end_line = end[ 'line_num' ] - 1 + line_delta + source_lines_count = end_line - start_line + 1 + start_column = start[ 'column_num' ] - 1 + char_delta + end_column = end[ 'column_num' ] - 1 + if source_lines_count == 1: + end_column += char_delta + + replacement_lines = replacement_text.splitlines( False ) + if not replacement_lines: + replacement_lines = [ '' ] + replacement_lines_count = len( replacement_lines ) + + end_existing_text = vim_buffer[ end_line ][ end_column : ] + start_existing_text = vim_buffer[ start_line ][ : start_column ] + + new_char_delta = ( len( replacement_lines[ -1 ] ) + - ( end_column - start_column ) ) + if replacement_lines_count > 1: + new_char_delta -= start_column + + replacement_lines[ 0 ] = start_existing_text + replacement_lines[ 0 ] + replacement_lines[ -1 ] = replacement_lines[ -1 ] + end_existing_text + + vim_buffer[ start_line : end_line + 1 ] = replacement_lines[:] + + new_line_delta = replacement_lines_count - source_lines_count + return ( new_line_delta, new_char_delta )