Jelajahi Sumber

Auto merge of #2677 - davits:diag_interface, r=micbou

[Ready] Refactored diag interface, removed dummy sign.

# PR Prelude
- [x] I have read and understood YCM's [CONTRIBUTING][cont] document.
- [x] I have read and understood YCM's [CODE_OF_CONDUCT][code] document.
- [x] I have included tests for the changes in my PR. If not, I have included a
  rationale for why I haven't.
- [x] **I understand my PR may be closed if it becomes obvious I didn't
  actually perform all of these steps.**

# Why this change is necessary and useful

Removed dummy sign logic, now​ it places news signs, then removes obsolete ones. I did this in my fork long ago and never experienced any flicker.
Some source code refactoring, which I believe simplifies logic a little.

[cont]: https://github.com/Valloric/YouCompleteMe/blob/master/CONTRIBUTING.md
[code]: https://github.com/Valloric/YouCompleteMe/blob/master/CODE_OF_CONDUCT.md

<!-- 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/2677)
<!-- Reviewable:end -->
zzbot 7 tahun lalu
induk
melakukan
95ebf8666b

+ 0 - 5
autoload/youcompleteme.vim

@@ -105,9 +105,6 @@ function! youcompleteme#Enable()
     autocmd CompleteDone * call s:OnCompleteDone()
   augroup END
 
-  " The BufEnter event is not triggered for the first loaded file.
-  exec s:python_command "ycm_state.SetCurrentBuffer()"
-
   " The FileType event is not triggered for the first loaded file. We wait until
   " the server is ready to manually run the s:OnFileTypeSet function.
   let s:pollers.server_ready.id = timer_start(
@@ -434,8 +431,6 @@ endfunction
 
 
 function! s:OnBufferEnter()
-  exec s:python_command "ycm_state.SetCurrentBuffer()"
-
   if !s:VisitedBufferRequiresReparse()
     return
   endif

+ 1 - 1
python/ycm/buffer.py

@@ -38,7 +38,7 @@ class Buffer( object ):
     self._parse_tick = 0
     self._handled_tick = 0
     self._parse_request = None
-    self._diag_interface = DiagnosticInterface( user_options )
+    self._diag_interface = DiagnosticInterface( bufnr, user_options )
 
 
   def FileParseRequestReady( self, block = False ):

+ 101 - 163
python/ycm/diagnostic_interface.py

@@ -26,21 +26,20 @@ from future.utils import itervalues, iteritems
 from collections import defaultdict, namedtuple
 from ycm import vimsupport
 from ycm.diagnostic_filter import DiagnosticFilter, CompileLevel
-import vim
 
 
 class DiagnosticInterface( object ):
-  def __init__( self, user_options ):
+  def __init__( self, bufnr, user_options ):
+    self._bufnr = bufnr
     self._user_options = user_options
     self._diagnostics = []
     self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
     # Line and column numbers are 1-based
-    self._buffer_number_to_line_to_diags = defaultdict(
-      lambda: defaultdict( list ) )
+    self._line_to_diags = defaultdict( list )
+    self._placed_signs = []
     self._next_sign_id = 1
     self._previous_line_number = -1
     self._diag_message_needs_clearing = False
-    self._placed_signs = []
 
 
   def OnCursorMoved( self ):
@@ -54,11 +53,11 @@ class DiagnosticInterface( object ):
 
 
   def GetErrorCount( self ):
-    return len( self._FilterDiagnostics( _DiagnosticIsError ) )
+    return self._DiagnosticsCount( _DiagnosticIsError )
 
 
   def GetWarningCount( self ):
-    return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
+    return self._DiagnosticsCount( _DiagnosticIsWarning )
 
 
   def PopulateLocationList( self ):
@@ -71,38 +70,26 @@ class DiagnosticInterface( object ):
   def UpdateWithNewDiagnostics( self, diags ):
     self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
                             self._ApplyDiagnosticFilter( diags ) ]
-    self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
-        self._diagnostics )
+    self._ConvertDiagListToDict()
 
     if self._user_options[ 'enable_diagnostic_signs' ]:
-      self._placed_signs, self._next_sign_id = _UpdateSigns(
-        self._placed_signs,
-        self._buffer_number_to_line_to_diags,
-        self._next_sign_id )
+      self._UpdateSigns()
 
     if self._user_options[ 'enable_diagnostic_highlighting' ]:
-      _UpdateSquiggles( self._buffer_number_to_line_to_diags )
+      self._UpdateSquiggles()
 
     if self._user_options[ 'always_populate_location_list' ]:
       self._UpdateLocationList()
 
 
-  def _ApplyDiagnosticFilter( self, diags, extra_predicate = None ):
-    filetypes = vimsupport.CurrentFiletypes()
+  def _ApplyDiagnosticFilter( self, diags ):
+    filetypes = vimsupport.GetBufferFiletypes( self._bufnr )
     diag_filter = self._diag_filter.SubsetForTypes( filetypes )
-    predicate = diag_filter.IsAllowed
-    if extra_predicate is not None:
-      def Filter( diag ):
-        return extra_predicate( diag ) and diag_filter.IsAllowed( diag )
-
-      predicate = Filter
-
-    return filter( predicate, diags )
+    return filter( diag_filter.IsAllowed, diags )
 
 
   def _EchoDiagnosticForLine( self, line_num ):
-    buffer_num = vim.current.buffer.number
-    diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ]
+    diags = self._line_to_diags[ line_num ]
     if not diags:
       if self._diag_message_needs_clearing:
         # Clear any previous diag echo
@@ -119,15 +106,11 @@ class DiagnosticInterface( object ):
     self._diag_message_needs_clearing = True
 
 
-  def _FilterDiagnostics( self, predicate ):
-    matched_diags = []
-    line_to_diags = self._buffer_number_to_line_to_diags[
-      vim.current.buffer.number ]
-
-    for diags in itervalues( line_to_diags ):
-      matched_diags.extend( list(
-        self._ApplyDiagnosticFilter( diags, predicate ) ) )
-    return matched_diags
+  def _DiagnosticsCount( self, predicate ):
+    count = 0
+    for diags in itervalues( self._line_to_diags ):
+      count += sum( 1 for d in diags if predicate( d ) )
+    return count
 
 
   def _UpdateLocationList( self ):
@@ -135,133 +118,90 @@ class DiagnosticInterface( object ):
       vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )
 
 
-def _UpdateSquiggles( buffer_number_to_line_to_diags ):
-  vimsupport.ClearYcmSyntaxMatches()
-  line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ]
-
-  for diags in itervalues( line_to_diags ):
-    # Insert squiggles in reverse order so that errors overlap warnings.
-    for diag in reversed( diags ):
-      location_extent = diag[ 'location_extent' ]
-      is_error = _DiagnosticIsError( diag )
-
-      if location_extent[ 'start' ][ 'line_num' ] <= 0:
-        location = diag[ 'location' ]
-        vimsupport.AddDiagnosticSyntaxMatch(
-          location[ 'line_num' ],
-          location[ 'column_num' ],
-          is_error = is_error )
-      else:
-        vimsupport.AddDiagnosticSyntaxMatch(
-          location_extent[ 'start' ][ 'line_num' ],
-          location_extent[ 'start' ][ 'column_num' ],
-          location_extent[ 'end' ][ 'line_num' ],
-          location_extent[ 'end' ][ 'column_num' ],
-          is_error = is_error )
-
-      for diag_range in diag[ 'ranges' ]:
-        vimsupport.AddDiagnosticSyntaxMatch(
-          diag_range[ 'start' ][ 'line_num' ],
-          diag_range[ 'start' ][ 'column_num' ],
-          diag_range[ 'end' ][ 'line_num' ],
-          diag_range[ 'end' ][ 'column_num' ],
-          is_error = is_error )
-
-
-def _UpdateSigns( placed_signs, buffer_number_to_line_to_diags, next_sign_id ):
-  new_signs, kept_signs, next_sign_id = _GetKeptAndNewSigns(
-    placed_signs, buffer_number_to_line_to_diags, next_sign_id
-  )
-  # Dummy sign used to prevent "flickering" in Vim when last mark gets
-  # deleted from buffer. Dummy sign prevents Vim to collapsing the sign column
-  # in that case.
-  # There's also a vim bug which causes the whole window to redraw in some
-  # conditions (vim redraw logic is very complex). But, somehow, if we place a
-  # dummy sign before placing other "real" signs, it will not redraw the
-  # buffer (patch to vim pending).
-  dummy_sign_needed = not kept_signs and new_signs
-
-  if dummy_sign_needed:
-    vimsupport.PlaceDummySign( next_sign_id + 1,
-                               vim.current.buffer.number,
-                               new_signs[ 0 ].line )
-
-  # We place only those signs that haven't been placed yet.
-  new_placed_signs = _PlaceNewSigns( kept_signs, new_signs )
-
-  # We use incremental placement, so signs that already placed on the correct
-  # lines will not be deleted and placed again, which should improve performance
-  # in case of many diags. Signs which don't exist in the current diag should be
-  # deleted.
-  _UnplaceObsoleteSigns( kept_signs, placed_signs )
-
-  if dummy_sign_needed:
-    vimsupport.UnPlaceDummySign( next_sign_id + 1, vim.current.buffer.number )
-
-  return new_placed_signs, next_sign_id
-
-
-def _GetKeptAndNewSigns( placed_signs, buffer_number_to_line_to_diags,
-                         next_sign_id ):
-  new_signs = []
-  kept_signs = []
-  for buffer_number, line_to_diags in iteritems(
-                                            buffer_number_to_line_to_diags ):
-    if not vimsupport.BufferIsVisible( buffer_number ):
-      continue
-
-    for line, diags in iteritems( line_to_diags ):
-      # Only one sign is visible by line.
-      first_diag = diags[ 0 ]
-      sign = _DiagSignPlacement( next_sign_id,
-                                 line,
-                                 buffer_number,
-                                 _DiagnosticIsError( first_diag ) )
-      if sign not in placed_signs:
+  def _UpdateSquiggles( self ):
+
+    vimsupport.ClearYcmSyntaxMatches()
+
+    for diags in itervalues( self._line_to_diags ):
+      # Insert squiggles in reverse order so that errors overlap warnings.
+      for diag in reversed( diags ):
+        location_extent = diag[ 'location_extent' ]
+        is_error = _DiagnosticIsError( diag )
+
+        if location_extent[ 'start' ][ 'line_num' ] <= 0:
+          location = diag[ 'location' ]
+          vimsupport.AddDiagnosticSyntaxMatch(
+              location[ 'line_num' ],
+              location[ 'column_num' ],
+              is_error = is_error )
+        else:
+          vimsupport.AddDiagnosticSyntaxMatch(
+            location_extent[ 'start' ][ 'line_num' ],
+            location_extent[ 'start' ][ 'column_num' ],
+            location_extent[ 'end' ][ 'line_num' ],
+            location_extent[ 'end' ][ 'column_num' ],
+            is_error = is_error )
+
+        for diag_range in diag[ 'ranges' ]:
+          vimsupport.AddDiagnosticSyntaxMatch(
+            diag_range[ 'start' ][ 'line_num' ],
+            diag_range[ 'start' ][ 'column_num' ],
+            diag_range[ 'end' ][ 'line_num' ],
+            diag_range[ 'end' ][ 'column_num' ],
+            is_error = is_error )
+
+
+  def _UpdateSigns( self ):
+    new_signs, obsolete_signs = self._GetNewAndObsoleteSigns()
+
+    self._PlaceNewSigns( new_signs )
+
+    self._UnplaceObsoleteSigns( obsolete_signs )
+
+
+  def _GetNewAndObsoleteSigns( self ):
+    new_signs = []
+    obsolete_signs = list( self._placed_signs )
+    for line, diags in iteritems( self._line_to_diags ):
+      # We always go for the first diagnostic on line,
+      # because it is sorted giving priority to the Errors.
+      diag = diags[ 0 ]
+      sign = _DiagSignPlacement( self._next_sign_id,
+                                 line, _DiagnosticIsError( diag ) )
+      try:
+        obsolete_signs.remove( sign )
+      except ValueError:
         new_signs.append( sign )
-        next_sign_id += 1
-      else:
-        # We use .index here because `sign` contains a new id, but
-        # we need the sign with the old id to unplace it later on.
-        # We won't be placing the new sign.
-        kept_signs.append( placed_signs[ placed_signs.index( sign ) ] )
-  return new_signs, kept_signs, next_sign_id
-
-
-def _PlaceNewSigns( kept_signs, new_signs ):
-  placed_signs = kept_signs[:]
-  for sign in new_signs:
-    # Do not set two signs on the same line, it will screw up storing sign
-    # locations.
-    if sign in placed_signs:
-      continue
-    vimsupport.PlaceSign( sign.id, sign.line, sign.buffer, sign.is_error )
-    placed_signs.append( sign )
-  return placed_signs
-
-
-def _UnplaceObsoleteSigns( kept_signs, placed_signs ):
-  for sign in placed_signs:
-    if sign not in kept_signs:
-      vimsupport.UnplaceSignInBuffer( sign.buffer, sign.id )
-
-
-def _ConvertDiagListToDict( diag_list ):
-  buffer_to_line_to_diags = defaultdict( lambda: defaultdict( list ) )
-  for diag in diag_list:
-    location = diag[ 'location' ]
-    buffer_number = vimsupport.GetBufferNumberForFilename(
-      location[ 'filepath' ] )
-    line_number = location[ 'line_num' ]
-    buffer_to_line_to_diags[ buffer_number ][ line_number ].append( diag )
-
-  for line_to_diags in itervalues( buffer_to_line_to_diags ):
-    for diags in itervalues( line_to_diags ):
-      # We want errors to be listed before warnings so that errors aren't hidden
-      # by the warnings.
+        self._next_sign_id += 1
+    return new_signs, obsolete_signs
+
+
+  def _PlaceNewSigns( self, new_signs ):
+    for sign in new_signs:
+      vimsupport.PlaceSign( sign.id, sign.line, self._bufnr, sign.is_error )
+      self._placed_signs.append( sign )
+
+
+  def _UnplaceObsoleteSigns( self, obsolete_signs ):
+    for sign in obsolete_signs:
+      self._placed_signs.remove( sign )
+      vimsupport.UnplaceSignInBuffer( self._bufnr, sign.id )
+
+
+  def _ConvertDiagListToDict( self ):
+    self._line_to_diags = defaultdict( list )
+    for diag in self._diagnostics:
+      location = diag[ 'location' ]
+      bufnr = vimsupport.GetBufferNumberForFilename( location[ 'filepath' ] )
+      if bufnr == self._bufnr:
+        line_number = location[ 'line_num' ]
+        self._line_to_diags[ line_number ].append( diag )
+
+    for diags in itervalues( self._line_to_diags ):
+      # We also want errors to be listed before warnings so that errors aren't
+      # hidden by the warnings; Vim won't place a sign over an existing one.
       diags.sort( key = lambda diag: ( diag[ 'kind' ],
                                        diag[ 'location' ][ 'column_num' ] ) )
-  return buffer_to_line_to_diags
 
 
 _DiagnosticIsError = CompileLevel( 'error' )
@@ -278,12 +218,10 @@ def _NormalizeDiagnostic( diag ):
   return diag
 
 
-class _DiagSignPlacement(
-                    namedtuple( "_DiagSignPlacement",
-                                [ 'id', 'line', 'buffer', 'is_error' ] ) ):
+class _DiagSignPlacement( namedtuple( "_DiagSignPlacement",
+                                      [ 'id', 'line', 'is_error' ] ) ):
   # We want two signs that have different ids but the same location to compare
   # equal. ID doesn't matter.
   def __eq__( self, other ):
     return ( self.line == other.line and
-             self.buffer == other.buffer and
              self.is_error == other.is_error )

+ 11 - 11
python/ycm/tests/event_notification_test.py

@@ -59,7 +59,7 @@ def UnplaceSign_Call( sign_id, buffer_num ):
 
 
 @contextlib.contextmanager
-def MockArbitraryBuffer( filetype, ycm ):
+def MockArbitraryBuffer( filetype ):
   """Used via the with statement, set up a single buffer with an arbitrary name
   and no contents. Its filetype is set to the supplied filetype."""
 
@@ -68,7 +68,7 @@ def MockArbitraryBuffer( filetype, ycm ):
                               window = 1,
                               filetype = filetype )
 
-  with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+  with MockVimBuffers( [ current_buffer ], current_buffer ):
     yield
 
 
@@ -124,7 +124,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_Error_test(
   def ErrorResponse( *args ):
     raise ServerError( ERROR_TEXT )
 
-  with MockArbitraryBuffer( 'javascript', ycm ):
+  with MockArbitraryBuffer( 'javascript' ):
     with MockEventNotification( ErrorResponse ):
       ycm.OnFileReadyToParse()
       ok_( ycm.FileParseRequestReady() )
@@ -156,7 +156,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_Error_test(
 def EventNotification_FileReadyToParse_NonDiagnostic_Error_NonNative_test(
     ycm, vim_command ):
 
-  with MockArbitraryBuffer( 'javascript', ycm ):
+  with MockArbitraryBuffer( 'javascript' ):
     with MockEventNotification( None, False ):
       ycm.OnFileReadyToParse()
       ycm.HandleFileParseRequest()
@@ -182,7 +182,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test(
   def UnknownExtraConfResponse( *args ):
     raise UnknownExtraConf( FILE_NAME )
 
-  with MockArbitraryBuffer( 'javascript', ycm ):
+  with MockArbitraryBuffer( 'javascript' ):
     with MockEventNotification( UnknownExtraConfResponse ):
 
       # When the user accepts the extra conf, we load it
@@ -282,7 +282,7 @@ def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ):
     diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
     return [ BuildDiagnosticData( diagnostic ) ]
 
-  with MockArbitraryBuffer( 'cpp', ycm ):
+  with MockArbitraryBuffer( 'cpp' ):
     with MockEventNotification( DiagnosticResponse ):
       ycm.OnFileReadyToParse()
       ok_( ycm.FileParseRequestReady() )
@@ -314,7 +314,7 @@ def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ):
     diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
     return [ BuildDiagnosticData( diagnostic ) ]
 
-  with MockArbitraryBuffer( 'cpp', ycm ):
+  with MockArbitraryBuffer( 'cpp' ):
     with MockEventNotification( DiagnosticResponse ):
       ycm.OnFileReadyToParse()
       ok_( ycm.FileParseRequestReady() )
@@ -340,7 +340,7 @@ def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ):
   # Tests Vim sign unplacement and error/warning count python API
   # when there are no errors/warnings left.
   # Should be called after _Check_FileReadyToParse_Diagnostic_Warning
-  with MockArbitraryBuffer( 'cpp', ycm ):
+  with MockArbitraryBuffer( 'cpp' ):
     with MockEventNotification( MagicMock( return_value = [] ) ):
       ycm.OnFileReadyToParse()
       ycm.HandleFileParseRequest()
@@ -366,7 +366,7 @@ def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
   with patch( 'ycm.client.event_notification.EventNotification.'
               'PostDataToHandlerAsync' ) as post_data_to_handler_async:
     with CurrentWorkingDirectory( unicode_dir ):
-      with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ), ycm ):
+      with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ):
         ycm.OnFileReadyToParse()
 
     assert_that(
@@ -509,7 +509,7 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
 
   with patch( 'ycm.client.event_notification.EventNotification.'
               'PostDataToHandlerAsync' ) as post_data_to_handler_async:
-    with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+    with MockVimBuffers( [ current_buffer ], current_buffer ):
       ycm.OnFileReadyToParse()
       assert_that(
         # Positional arguments passed to PostDataToHandlerAsync.
@@ -545,7 +545,7 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_ClearCacheIfRestart_test(
 
   with patch( 'ycm.client.event_notification.EventNotification.'
               'PostDataToHandlerAsync' ) as post_data_to_handler_async:
-    with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+    with MockVimBuffers( [ current_buffer ], current_buffer ):
       ycm.OnFileReadyToParse()
       assert_that(
         # Positional arguments passed to PostDataToHandlerAsync.

+ 13 - 7
python/ycm/tests/test_utils.py

@@ -43,7 +43,7 @@ GETBUFVAR_REGEX = re.compile(
   '^getbufvar\((?P<buffer_number>[0-9]+), "(?P<option>.+)"\)$' )
 MATCHADD_REGEX = re.compile(
   '^matchadd\(\'(?P<group>.+)\', \'(?P<pattern>.+)\'\)$' )
-MATCHDELETE_REGEX = re.compile( '^matchdelete\((?P<id>)\)$' )
+MATCHDELETE_REGEX = re.compile( '^matchdelete\((?P<id>\d+)\)$' )
 
 # One-and only instance of mocked Vim object. The first 'import vim' that is
 # executed binds the vim module to the instance of MagicMock that is created,
@@ -139,7 +139,9 @@ def _MockVimOptionsEval( value ):
 
 def _MockVimMatchEval( value ):
   if value == 'getmatches()':
-    return VIM_MATCHES
+    # Returning a copy, because ClearYcmSyntaxMatches() gets the result of
+    # getmatches(), iterates over it and removes elements from VIM_MATCHES.
+    return list( VIM_MATCHES )
 
   match = MATCHADD_REGEX.search( value )
   if match:
@@ -151,7 +153,7 @@ def _MockVimMatchEval( value ):
 
   match = MATCHDELETE_REGEX.search( value )
   if match:
-    identity = match.group( 'id' )
+    identity = int( match.group( 'id' ) )
     for index, vim_match in enumerate( VIM_MATCHES ):
       if vim_match.id == identity:
         VIM_MATCHES.pop( index )
@@ -270,9 +272,15 @@ class VimMatch( object ):
                                                                 self.pattern )
 
 
+  def __getitem__( self, key ):
+    if key == 'group':
+      return self.group
+    elif key == 'id':
+      return self.id
+
+
 @contextlib.contextmanager
-def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ),
-                    ycm_state = None):
+def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ) ):
   """Simulates the Vim buffers list |buffers| where |current_buffer| is the
   buffer displayed in the current window and |cursor_position| is the current
   cursor position. All buffers are represented by a VimBuffer object."""
@@ -282,8 +290,6 @@ def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ),
   with patch( 'vim.buffers', buffers ):
     with patch( 'vim.current.buffer', current_buffer ):
       with patch( 'vim.current.window.cursor', cursor_position ):
-        if ycm_state is not None:
-          ycm_state.SetCurrentBuffer()
         yield
 
 

+ 23 - 8
python/ycm/tests/youcompleteme_test.py

@@ -330,7 +330,7 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
   ycm, set_location_list, post_vim_message, *args ):
 
   current_buffer = VimBuffer( 'buffer', filetype = 'cpp' )
-  with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+  with MockVimBuffers( [ current_buffer ], current_buffer ):
     with patch( 'ycm.client.event_notification.EventNotification.Response',
                 return_value = {} ):
       ycm.ShowDiagnostics()
@@ -367,7 +367,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
   }
 
   current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
-  with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+  with MockVimBuffers( [ current_buffer ], current_buffer ):
     with patch( 'ycm.client.event_notification.EventNotification.Response',
                 return_value = [ diagnostic ] ):
       ycm.ShowDiagnostics()
@@ -409,7 +409,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
   }
 
   current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
-  with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
+  with MockVimBuffers( [ current_buffer ], current_buffer ):
     with patch( 'ycm.client.event_notification.EventNotification.Response',
                 return_value = [ diagnostic ] ):
       ycm.ShowDiagnostics()
@@ -514,7 +514,7 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
 
   test_utils.VIM_MATCHES = []
 
-  with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ), ycm ):
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ) ):
     with patch( 'ycm.client.event_notification.EventNotification.Response',
                 return_value = diagnostics ):
       ycm.OnFileReadyToParse()
@@ -532,11 +532,7 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
 
     # Only the error sign is placed.
     vim_command.assert_has_exact_calls( [
-      call( 'sign define ycm_dummy_sign' ),
-      call( 'sign place 3 name=ycm_dummy_sign line=3 buffer=5' ),
       call( 'sign place 1 name=YcmError line=3 buffer=5' ),
-      call( 'sign undefine ycm_dummy_sign' ),
-      call( 'sign unplace 3 buffer=5' )
     ] )
 
     # When moving the cursor on the diagnostics, the error is displayed to the
@@ -546,3 +542,22 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
       call( "expected ';' after expression (FixIt)",
             truncate = True, warning = False )
     ] )
+
+    vim_command.reset_mock()
+    with patch( 'ycm.client.event_notification.EventNotification.Response',
+                return_value = diagnostics[ 1 : ] ):
+      ycm.OnFileReadyToParse()
+      ycm.HandleFileParseRequest( block = True )
+
+    assert_that(
+      test_utils.VIM_MATCHES,
+      contains(
+        VimMatch( 'YcmWarningSection', '\%3l\%5c\_.\{-}\%3l\%7c' ),
+        VimMatch( 'YcmWarningSection', '\%3l\%3c\_.\{-}\%3l\%9c' )
+      )
+    )
+
+    vim_command.assert_has_exact_calls( [
+      call( 'sign place 2 name=YcmWarning line=3 buffer=5' ),
+      call( 'try | exec "sign unplace 1 buffer=5" | catch /E158/ | endtry' )
+    ] )

+ 5 - 20
python/ycm/vimsupport.py

@@ -174,26 +174,6 @@ def PlaceSign( sign_id, line_num, buffer_num, is_error = True ):
     sign_id, sign_name, line_num, buffer_num ) )
 
 
-def PlaceDummySign( sign_id, buffer_num, line_num ):
-    if buffer_num < 0 or line_num < 0:
-      return
-    vim.command( 'sign define ycm_dummy_sign' )
-    vim.command(
-      'sign place {0} name=ycm_dummy_sign line={1} buffer={2}'.format(
-        sign_id,
-        line_num,
-        buffer_num,
-      )
-    )
-
-
-def UnPlaceDummySign( sign_id, buffer_num ):
-    if buffer_num < 0:
-      return
-    vim.command( 'sign undefine ycm_dummy_sign' )
-    vim.command( 'sign unplace {0} buffer={1}'.format( sign_id, buffer_num ) )
-
-
 def ClearYcmSyntaxMatches():
   matches = VimExpressionToPythonType( 'getmatches()' )
   for match in matches:
@@ -592,6 +572,11 @@ def CurrentFiletypes():
   return VimExpressionToPythonType( "&filetype" ).split( '.' )
 
 
+def GetBufferFiletypes( bufnr ):
+  command = 'getbufvar({0}, "&ft")'.format( bufnr )
+  return VimExpressionToPythonType( command ).split( '.' )
+
+
 def FiletypesForBuffer( buffer_object ):
   # NOTE: Getting &ft for other buffers only works when the buffer has been
   # visited by the user at least once, which is true for modified buffers

+ 10 - 11
python/ycm/youcompleteme.py

@@ -115,7 +115,6 @@ class YouCompleteMe( object ):
     self._user_notified_about_crash = False
     self._omnicomp = OmniCompleter( user_options )
     self._buffers = BufferDict( user_options )
-    self._current_buffer = None
     self._latest_completion_request = None
     self._logger = logging.getLogger( 'ycm' )
     self._client_logfile = None
@@ -357,7 +356,7 @@ class YouCompleteMe( object ):
 
 
   def NeedsReparse( self ):
-    return self._current_buffer.NeedsReparse()
+    return self.CurrentBuffer().NeedsReparse()
 
 
   def OnFileReadyToParse( self ):
@@ -375,7 +374,7 @@ class YouCompleteMe( object ):
     self._AddSyntaxDataIfNeeded( extra_data )
     self._AddExtraConfDataIfNeeded( extra_data )
 
-    self._current_buffer.SendParseRequest( extra_data )
+    self.CurrentBuffer().SendParseRequest( extra_data )
 
 
   def OnBufferUnload( self, deleted_buffer_file ):
@@ -388,8 +387,8 @@ class YouCompleteMe( object ):
     SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data )
 
 
-  def SetCurrentBuffer( self ):
-    self._current_buffer = self._buffers[ vimsupport.GetCurrentBufferNumber() ]
+  def CurrentBuffer( self ):
+    return self._buffers[ vimsupport.GetCurrentBufferNumber() ]
 
 
   def OnInsertLeave( self ):
@@ -397,7 +396,7 @@ class YouCompleteMe( object ):
 
 
   def OnCursorMoved( self ):
-    self._current_buffer.OnCursorMoved()
+    self.CurrentBuffer().OnCursorMoved()
 
 
   def _CleanLogfile( self ):
@@ -524,11 +523,11 @@ class YouCompleteMe( object ):
 
 
   def GetErrorCount( self ):
-    return self._current_buffer.GetErrorCount()
+    return self.CurrentBuffer().GetErrorCount()
 
 
   def GetWarningCount( self ):
-    return self._current_buffer.GetWarningCount()
+    return self.CurrentBuffer().GetWarningCount()
 
 
   def DiagnosticUiSupportedForCurrentFiletype( self ):
@@ -542,20 +541,20 @@ class YouCompleteMe( object ):
 
 
   def _PopulateLocationListWithLatestDiagnostics( self ):
-    return self._current_buffer.PopulateLocationList()
+    return self.CurrentBuffer().PopulateLocationList()
 
 
   def FileParseRequestReady( self ):
     # Return True if server is not ready yet, to stop repeating check timer.
     return ( not self.IsServerReady() or
-             self._current_buffer.FileParseRequestReady() )
+             self.CurrentBuffer().FileParseRequestReady() )
 
 
   def HandleFileParseRequest( self, block = False ):
     if not self.IsServerReady():
       return
 
-    current_buffer = self._current_buffer
+    current_buffer = self.CurrentBuffer()
     # Order is important here:
     # FileParseRequestReady has a low cost, while
     # NativeFiletypeCompletionUsable is a blocking server request