Browse Source

YouCompleteMe client support for FixIt subcommands

Ben Jackson 9 years ago
parent
commit
98c4d712b4

+ 45 - 2
README.md

@@ -655,7 +655,15 @@ You may want to map this command to a key; try putting `nnoremap <F5>
 ### 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`

+ 62 - 10
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 )

+ 6 - 3
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
 
 

+ 523 - 0
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 <http://www.gnu.org/licenses/>.
+
+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,
+  }

+ 50 - 1
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 )