فهرست منبع

Auto merge of #2898 - micbou:line-overlapping-chunks, r=micbou

[READY] Apply chunks from bottom to top

Without the proposed fix, the test included in that PR fails as follows
```
FAIL: ycm.tests.vimsupport_test.ReplaceChunksInBuffer_LineOverlappingChunks_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\nose\case.py", line 198, in runTest
    self.test(*self.arg)
  File "C:\Users\micbou\projects\YouCompleteMe\python\ycm\tests\vimsupport_test.py", line 768, in ReplaceChunksInBuffer_LineOverlappingChunks_test
    AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
  File "C:\Users\micbou\projects\YouCompleteMe\python\ycm\tests\vimsupport_test.py", line 86, in AssertBuffersAreEqualAsBytes
    eq_( ToBytes( result_line ), ToBytes( expected_line ) )
AssertionError: b'    third line' != b'    third '
```
Found the issue while trying to add code formatting support to the language server completer. The test is based on a real-world scenario when formatting with jdt.ls (for some reason, jdt.ls is including the newline of the previous line when fixing indentation).

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/2898)
<!-- Reviewable:end -->
zzbot 7 سال پیش
والد
کامیت
39fe6d1f86
2فایلهای تغییر یافته به همراه335 افزوده شده و 545 حذف شده
  1. 273 471
      python/ycm/tests/vimsupport_test.py
  2. 62 74
      python/ycm/vimsupport.py

+ 273 - 471
python/ycm/tests/vimsupport_test.py

@@ -183,628 +183,364 @@ def AssertBuffersAreEqualAsBytes( result_buffer, expected_buffer ):
 
 def ReplaceChunk_SingleLine_Repl_1_test():
   # Replace with longer range
-  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 )
-
-  AssertBuffersAreEqualAsBytes( [ '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
-
-  AssertBuffersAreEqualAsBytes( [ '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
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 11, 1, 17 )
+  vimsupport.ReplaceChunk( start, end, 'pie', result_buffer )
 
-  ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
-                                                          start,
-                                                          end,
-                                                          'pie',
-                                                          line_offset,
-                                                          char_offset,
-                                                          result_buffer )
+  AssertBuffersAreEqualAsBytes( [ 'This is a pie' ], result_buffer )
 
-  line_offset += new_line_offset
-  char_offset += new_char_offset
+  # and replace again
+  start, end = _BuildLocations( 1, 10, 1, 11 )
+  vimsupport.ReplaceChunk( start, end, ' piece of ', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'This is a piece of pie' ], result_buffer )
+
+  # and once more, for luck
+  start, end = _BuildLocations( 1, 1, 1, 5 )
+  vimsupport.ReplaceChunk( start, end, 'How long', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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 )
+                                result_buffer )
 
 
 def ReplaceChunk_SingleLine_Repl_2_test():
   # Replace with shorter range
-  result_buffer = [ 'This is a string' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 11, 1, 17 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'test',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'test', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'This is a test' ], result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -2 )
 
 
 def ReplaceChunk_SingleLine_Repl_3_test():
   # Replace with equal range
-  result_buffer = [ 'This is a string' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 6, 1, 8 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'be',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'be', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'is a string' ] )
   start, end = _BuildLocations( 1, 1, 1, 1 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'This ',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'This ', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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 ' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a ' ] )
   start, end = _BuildLocations( 1, 11, 1, 11 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'string',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'string', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 8, 1, 8 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          ' not',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, ' not', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 1, 1, 6 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          '',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, '', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is a string' ] )
   start, end = _BuildLocations( 1, 10, 1, 18 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          '',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, '', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ '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' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'This is not a string' ] )
   start, end = _BuildLocations( 1, 9, 1, 13 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          '',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, '', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'This is a string' ], result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -4 )
 
 
 def ReplaceChunk_SingleLine_Unicode_ReplaceUnicodeChars_test():
   # Replace Unicode characters.
-  result_buffer = [ 'This Uniçø∂‰ string is in the middle' ]
+  result_buffer = VimBuffer(
+    'buffer', contents = [ 'This Uniçø∂‰ string is in the middle' ] )
   start, end = _BuildLocations( 1, 6, 1, 20 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'Unicode ',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'Unicode ', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'This Unicode string is in the middle' ],
                                  result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -6 )
 
 
 def ReplaceChunk_SingleLine_Unicode_ReplaceAfterUnicode_test():
   # Replace ASCII characters after Unicode characters in the line.
-  result_buffer = [ 'This Uniçø∂‰ string is in the middle' ]
+  result_buffer = VimBuffer(
+    'buffer', contents = [ 'This Uniçø∂‰ string is in the middle' ] )
   start, end = _BuildLocations( 1, 30, 1, 43 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'fåke',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'fåke', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'This Uniçø∂‰ string is fåke' ],
                                  result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -8 )
 
 
 def ReplaceChunk_SingleLine_Unicode_Grown_test():
   # Replace ASCII characters after Unicode characters in the line.
-  result_buffer = [ 'a' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'a' ] )
   start, end = _BuildLocations( 1, 1, 1, 2 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start,
-                                                          end,
-                                                          'å',
-                                                          0,
-                                                          0,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'å', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'å' ], result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, 1 ) # Note: byte difference (a = 1 byte, å = 2 bytes)
 
 
 def ReplaceChunk_RemoveSingleLine_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ 'aAa',
+                                                    'aBa',
+                                                    'aCa' ] )
   start, end = _BuildLocations( 2, 1, 3, 1 )
-  ( line_offset, char_offset ) = vimsupport.ReplaceChunk( start, end, '',
-                                                          0, 0, result_buffer )
+  vimsupport.ReplaceChunk( start, end, '', result_buffer )
   # First line is not affected.
-  expected_buffer = [ 'aAa',
-                      'aCa' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, -1 )
-  eq_( char_offset, 0 )
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aCa' ], result_buffer )
 
 
 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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 1 )
-  eq_( char_offset, 1 )
-
-  # now make another change to the "2nd" line
+  result_buffer = VimBuffer( 'buffer', contents = [ 'aAa',
+                                                    'aBa',
+                                                    'aCa' ] )
   start, end = _BuildLocations( 2, 3, 2, 4 )
-  ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
-                                                           start,
-                                                           end,
-                                                           'cccc',
-                                                           line_offset,
-                                                           char_offset,
-                                                           result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'cccc', result_buffer )
 
-  line_offset += new_line_offset
-  char_offset += new_char_offset
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aBcccc',
+                                  'aCa' ], result_buffer )
+
+  # now make another change to the second line
+  start, end = _BuildLocations( 2, 2, 2, 2 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbF', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'aAa',
-                                   'aEb',
-                                   'bFBcccc',
-                                   'aCa' ], result_buffer )
-  eq_( line_offset, 1 )
-  eq_( char_offset, 4 )
+                                  'aEb',
+                                  'bFBcccc',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_SingleToMultipleLines2_test():
-  result_buffer = [ 'aAa', 'aBa', 'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 2 )
-  eq_( char_offset, 0 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbFb\nG', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aEb',
+                                  'bFb',
+                                  'GBa',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_SingleToMultipleLines3_test():
-  result_buffer = [ 'aAa', 'aBa', 'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 2 )
-  eq_( char_offset, 2 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbFb\nbGb', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aEb',
+                                  'bFb',
+                                  'bGbBa',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_SingleToMultipleLinesReplace_test():
-  result_buffer = [ 'aAa', 'aBa', 'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 2 )
-  eq_( char_offset, 0 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbFb\nbGb', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aEb',
+                                  'bFb',
+                                  'bGb',
+                                  'aBa',
+                                  'aCa' ], result_buffer )
 
 
 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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 2 )
-  eq_( char_offset, 0 )
-
-  # now do a subsequent change (insert at end of line "1")
+  result_buffer = VimBuffer( 'buffer', contents = [ 'aAa',
+                                                    'aBa',
+                                                    'aCa' ] )
   start, end = _BuildLocations( 1, 4, 1, 4 )
-  ( new_line_offset, new_char_offset ) = vimsupport.ReplaceChunk(
-                                                          start,
-                                                          end,
-                                                          'cccc',
-                                                          line_offset,
-                                                          char_offset,
-                                                          result_buffer )
+  vimsupport.ReplaceChunk( start, end, 'cccc', result_buffer )
 
-  line_offset += new_line_offset
-  char_offset += new_char_offset
+  AssertBuffersAreEqualAsBytes( [ 'aAacccc',
+                                  'aBa',
+                                  'aCa', ], result_buffer )
 
-  AssertBuffersAreEqualAsBytes( [ 'aEb',
-                                   'bFb',
-                                   'bGbcccc',
-                                   'aBa',
-                                   'aCa' ], result_buffer )
+  # now do a subsequent change (insert in the middle of the first line)
+  start, end = _BuildLocations( 1, 2, 1, 4 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbFb\nbGb', result_buffer )
 
-  eq_( line_offset, 2 )
-  eq_( char_offset, 4 )
+  AssertBuffersAreEqualAsBytes( [ 'aEb',
+                                  'bFb',
+                                  'bGbcccc',
+                                  'aBa',
+                                  'aCa' ], result_buffer )
 
 
 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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, -1 )
-  eq_( char_offset, 1 )
+  result_buffer = VimBuffer( 'buffer', contents = [ 'aAa',
+                                                    'aBa',
+                                                    'aCaaaa' ] )
+  start, end = _BuildLocations( 3, 4, 3, 5 )
+  vimsupport.ReplaceChunk( start, end, 'dd\ndd', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aBa',
+                                  'aCadd',
+                                  'ddaa' ], result_buffer )
 
   # 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
+  vimsupport.ReplaceChunk( start, end, 'cccc', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'aAa',
-                                   'aECccccaaa' ], result_buffer )
-  eq_( line_offset, -1 )
-  eq_( char_offset, 4 )
+                                  'aBa',
+                                  'aCccccdd',
+                                  'ddaa' ], result_buffer )
 
   # 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
+  start, end = _BuildLocations( 2, 2, 3, 2 )
+  vimsupport.ReplaceChunk( start, end, 'E', result_buffer )
 
   AssertBuffersAreEqualAsBytes( [ 'aAa',
-                                   'aECccccdd',
-                                   'ddaa' ], result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -2 )
+                                  'aECccccdd',
+                                  'ddaa' ], result_buffer )
 
 
 def ReplaceChunk_MultipleLinesToSameMultipleLines_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa',
-                    'aDe' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbF', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aEb',
+                                  'bFCa',
+                                  'aDe' ], result_buffer )
 
 
 def ReplaceChunk_MultipleLinesToMoreMultipleLines_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa',
-                    'aDe' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 1 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbFb\nbG', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aEb',
+                                  'bFb',
+                                  'bGCa',
+                                  'aDe' ], result_buffer )
 
 
 def ReplaceChunk_MultipleLinesToLessMultipleLines_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa',
-                    'aDe' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, -1 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbF', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aEb',
+                                  'bFCa',
+                                  'aDe' ], result_buffer )
 
 
 def ReplaceChunk_MultipleLinesToEvenLessMultipleLines_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa',
-                    'aDe' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, -2 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'Eb\nbF', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aEb',
+                                  'bFDe' ], result_buffer )
 
 
 def ReplaceChunk_SpanBufferEdge_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'bDb', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'bDba',
+                                  'aBa',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_DeleteTextInLine_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, -1 )
+  vimsupport.ReplaceChunk( start, end, '', result_buffer )
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'aa',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_AddTextInLine_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 0 )
-  eq_( char_offset, 3 )
+  vimsupport.ReplaceChunk( start, end, 'bDb', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'abDbBa',
+                                  'aCa' ], result_buffer )
 
 
 def ReplaceChunk_ReplaceTextInLine_test():
-  result_buffer = [ 'aAa',
-                    'aBa',
-                    'aCa' ]
+  result_buffer = VimBuffer( 'buffer', contents = [ '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' ]
-  AssertBuffersAreEqualAsBytes( 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' ]
-  AssertBuffersAreEqualAsBytes( 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' ]
-  AssertBuffersAreEqualAsBytes( 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' ]
-  AssertBuffersAreEqualAsBytes( 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' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
-  eq_( line_offset, 1 )
-  eq_( char_offset, 1 )
+  vimsupport.ReplaceChunk( start, end, 'bDb', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'aAa',
+                                  'abDba',
+                                  'aCa' ], result_buffer )
+
+
+def ReplaceChunk_NewlineChunk_test():
+  result_buffer = VimBuffer( 'buffer', contents = [ 'first line',
+                                                    'second line' ] )
+  start, end = _BuildLocations( 1, 11, 2, 1 )
+  vimsupport.ReplaceChunk( start, end, '\n', result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'first line',
+                                  'second line' ], result_buffer )
 
 
 def _BuildLocations( start_line, start_column, end_line, end_column ):
@@ -823,11 +559,10 @@ def ReplaceChunksInBuffer_SortedChunks_test():
     _BuildChunk( 1, 11, 1, 11, ')' )
   ]
 
-  result_buffer = [ 'CT<10 >> 2> ct' ]
-  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
+  result_buffer = VimBuffer( 'buffer', contents = [ 'CT<10 >> 2> ct' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
 
-  expected_buffer = [ 'CT<(10 >> 2)> ct' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
+  AssertBuffersAreEqualAsBytes( [ 'CT<(10 >> 2)> ct' ], result_buffer )
 
 
 def ReplaceChunksInBuffer_UnsortedChunks_test():
@@ -836,11 +571,78 @@ def ReplaceChunksInBuffer_UnsortedChunks_test():
     _BuildChunk( 1, 4, 1, 4, '(' )
   ]
 
-  result_buffer = [ 'CT<10 >> 2> ct' ]
-  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer, None )
+  result_buffer = VimBuffer( 'buffer', contents = [ 'CT<10 >> 2> ct' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'CT<(10 >> 2)> ct' ], result_buffer )
+
+
+def ReplaceChunksInBuffer_LineOverlappingChunks_test():
+  chunks = [
+    _BuildChunk( 1, 11, 2, 1, '\n    ' ),
+    _BuildChunk( 2, 12, 3, 1, '\n    ' ),
+    _BuildChunk( 3, 11, 4, 1, '\n    ' )
+  ]
+
+  result_buffer = VimBuffer( 'buffer', contents = [ 'first line',
+                                                    'second line',
+                                                    'third line',
+                                                    'fourth line' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'first line',
+                                  '    second line',
+                                  '    third line',
+                                  '    fourth line' ], result_buffer )
+
+
+def ReplaceChunksInBuffer_OutdentChunks_test():
+  chunks = [
+    _BuildChunk( 1,  1, 1, 5, '  ' ),
+    _BuildChunk( 1, 15, 2, 9, '\n    ' ),
+    _BuildChunk( 2, 20, 3, 3, '\n' )
+  ]
+
+  result_buffer = VimBuffer( 'buffer', contents = [ '    first line',
+                                                    '        second line',
+                                                    '    third line' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ '  first line',
+                                  '    second line',
+                                  '  third line' ], result_buffer )
+
+
+def ReplaceChunksInBuffer_OneLineIndentingChunks_test():
+  chunks = [
+    _BuildChunk( 1,  8, 2,  1, '\n ' ),
+    _BuildChunk( 2,  9, 2, 10, '\n  ' ),
+    _BuildChunk( 2, 19, 2, 20, '\n ' )
+  ]
+
+  result_buffer = VimBuffer( 'buffer', contents = [ 'class {',
+                                                    'method { statement }',
+                                                    '}' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
+
+  AssertBuffersAreEqualAsBytes( [ 'class {',
+                                  ' method {',
+                                  '  statement',
+                                  ' }',
+                                  '}' ], result_buffer )
+
+
+def ReplaceChunksInBuffer_SameLocation_test():
+  chunks = [
+    _BuildChunk( 1, 1, 1, 1, 'this ' ),
+    _BuildChunk( 1, 1, 1, 1, 'is ' ),
+    _BuildChunk( 1, 1, 1, 1, 'pure ' )
+  ]
+
+  result_buffer = VimBuffer( 'buffer', contents = [ 'folly' ] )
+  vimsupport.ReplaceChunksInBuffer( chunks, result_buffer )
 
-  expected_buffer = [ 'CT<(10 >> 2)> ct' ]
-  AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
+  AssertBuffersAreEqualAsBytes( [ 'this is pure folly' ], result_buffer )
 
 
 @patch( 'ycm.vimsupport.VariableExists', return_value = False )

+ 62 - 74
python/ycm/vimsupport.py

@@ -749,15 +749,14 @@ def ReplaceChunks( chunks ):
   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).
+  # We apply the edits file-wise for efficiency.
   chunks_by_file = _SortChunksByFile( chunks )
 
-  # We sort the file list simply to enable repeatable testing
+  # We sort the file list simply to enable repeatable testing.
   sorted_file_list = sorted( iterkeys( chunks_by_file ) )
 
   # Make sure the user is prepared to have her screen mutilated by the new
-  # buffers
+  # buffers.
   num_files_to_open = _GetNumNonVisibleFiles( sorted_file_list )
 
   if num_files_to_open > 0:
@@ -770,11 +769,10 @@ def ReplaceChunks( chunks ):
   locations = []
 
   for filepath in sorted_file_list:
-    ( buffer_num, close_window ) = _OpenFileInSplitIfNeeded( filepath )
+    buffer_num, close_window = _OpenFileInSplitIfNeeded( filepath )
 
-    ReplaceChunksInBuffer( chunks_by_file[ filepath ],
-                           vim.buffers[ buffer_num ],
-                           locations )
+    locations.extend( ReplaceChunksInBuffer( chunks_by_file[ filepath ],
+                                             vim.buffers[ buffer_num ] ) )
 
     # 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
@@ -798,101 +796,91 @@ def ReplaceChunks( chunks ):
                   warning = False )
 
 
-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
-  # in ascending order of insertion point.
+def ReplaceChunksInBuffer( chunks, vim_buffer ):
+  """Apply changes in |chunks| to the buffer-like object |buffer| and return the
+  locations for that buffer."""
+
+  # We apply the chunks from the bottom to the top of the buffer so that we
+  # don't need to adjust the position of the remaining chunks due to text
+  # changes. This assumes that chunks are not overlapping. However, we still
+  # allow multiple chunks to share the same starting position (because of the
+  # language server protocol specs). These chunks must be applied in their order
+  # of appareance. Since Python sorting is stable, if we sort the whole list in
+  # reverse order of location, these chunks will be reversed. Therefore, we
+  # need to fully reverse the list then sort it on the starting position in
+  # reverse order.
+  chunks.reverse()
   chunks.sort( key = lambda chunk: (
     chunk[ 'range' ][ 'start' ][ 'line_num' ],
     chunk[ 'range' ][ 'start' ][ 'column_num' ]
-  ) )
+  ), reverse = True )
 
-  # 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
+  # However, we still want to display the locations from the top of the buffer
+  # to its bottom.
+  return reversed( [ ReplaceChunk( chunk[ 'range' ][ 'start' ],
+                                   chunk[ 'range' ][ 'end' ],
+                                   chunk[ 'replacement_text' ],
+                                   vim_buffer )
+                     for chunk in chunks ] )
 
-  line_delta = 0
-  for chunk in 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 ) = ReplaceChunk(
-      chunk[ 'range' ][ 'start' ],
-      chunk[ 'range' ][ 'end' ],
-      chunk[ 'replacement_text' ],
-      line_delta, char_delta,
-      vim_buffer,
-      locations )
-    line_delta += new_line_delta
-    char_delta += new_char_delta
+
+def SplitLines( contents ):
+  """Return a list of each of the lines in the byte string |contents|.
+  Behavior is equivalent to str.splitlines with the following exceptions:
+   - empty strings are returned as [ '' ];
+   - a trailing newline is not ignored (i.e. SplitLines( '\n' )
+     returns [ '', '' ], not [ '' ] )."""
+  if contents == b'':
+    return [ b'' ]
+
+  lines = contents.splitlines()
+
+  if contents.endswith( b'\r' ) or contents.endswith( b'\n' ):
+    lines.append( b'' )
+
+  return lines
 
 
 # Replace the chunk of text specified by a contiguous range with the supplied
-# text.
+# text and return the location.
 # * 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.
 #
 # NOTE: Works exclusively with bytes() instances and byte offsets as returned
 # by ycmd and used within the Vim buffers
-def ReplaceChunk( start, end, replacement_text, line_delta, char_delta,
-                  vim_buffer, locations = None ):
+def ReplaceChunk( start, end, replacement_text, vim_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
+  start_line = start[ 'line_num' ] - 1
+  end_line = end[ 'line_num' ] - 1
 
-  source_lines_count = end_line - start_line + 1
-  start_column = start[ 'column_num' ] - 1 + char_delta
+  start_column = start[ 'column_num' ] - 1
   end_column = end[ 'column_num' ] - 1
-  if source_lines_count == 1:
-    end_column += char_delta
 
   # NOTE: replacement_text is unicode, but all our offsets are byte offsets,
   # so we convert to bytes
-  replacement_lines = ToBytes( replacement_text ).splitlines( False )
-  if not replacement_lines:
-    replacement_lines = [ bytes( b'' ) ]
-  replacement_lines_count = len( replacement_lines )
+  replacement_lines = SplitLines( ToBytes( replacement_text ) )
 
   # NOTE: Vim buffers are a list of byte objects on Python 2 but unicode
   # objects on Python 3.
-  end_existing_text = ToBytes( vim_buffer[ end_line ] )[ end_column : ]
   start_existing_text = ToBytes( 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
+  end_existing_text = ToBytes( vim_buffer[ end_line ] )[ end_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[:]
 
-  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
-  return ( new_line_delta, new_char_delta )
+  return {
+    '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',
+  }
 
 
 def InsertNamespace( namespace ):
@@ -909,9 +897,9 @@ def InsertNamespace( namespace ):
   if line:
     existing_line = LineTextInCurrentBuffer( line )
     existing_indent = re.sub( r"\S.*", "", existing_line )
-  new_line = "{0}using {1};\n\n".format( existing_indent, namespace )
+  new_line = "{0}using {1};\n".format( existing_indent, namespace )
   replace_pos = { 'line_num': line + 1, 'column_num': 1 }
-  ReplaceChunk( replace_pos, replace_pos, new_line, 0, 0, vim.current.buffer )
+  ReplaceChunk( replace_pos, replace_pos, new_line, vim.current.buffer )
   PostVimMessage( 'Add namespace: {0}'.format( namespace ), warning = False )