diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim
index b09c4d38..f7c99c88 100644
--- a/autoload/youcompleteme.vim
+++ b/autoload/youcompleteme.vim
@@ -479,15 +479,13 @@ function! s:OnFileReadyToParse( ... )
" We only want to send a new FileReadyToParse event notification if the buffer
" has changed since the last time we sent one, or if forced.
- if force_parsing || b:changedtick != get( b:, 'ycm_changedtick', -1 )
+ if force_parsing || s:Pyeval( "ycm_state.NeedsReparse()" )
exec s:python_command "ycm_state.OnFileReadyToParse()"
call timer_stop( s:pollers.file_parse_response.id )
let s:pollers.file_parse_response.id = timer_start(
\ s:pollers.file_parse_response.wait_milliseconds,
\ function( 's:PollFileParseResponse' ) )
-
- let b:ycm_changedtick = b:changedtick
endif
endfunction
diff --git a/python/ycm/buffer.py b/python/ycm/buffer.py
new file mode 100644
index 00000000..3fdedbe9
--- /dev/null
+++ b/python/ycm/buffer.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2016, Davit Samvelyan
+#
+# This file is part of YouCompleteMe.
+#
+# YouCompleteMe is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# YouCompleteMe is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with YouCompleteMe. If not, see .
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import * # noqa
+
+from ycm import vimsupport
+from ycm.client.event_notification import EventNotification
+from ycm.diagnostic_interface import DiagnosticInterface
+
+
+class Buffer( object ):
+
+ def __init__( self, bufnr, user_options ):
+ self.number = bufnr
+ self._parse_tick = 0
+ self._handled_tick = 0
+ self._parse_request = None
+ self._diag_interface = DiagnosticInterface( bufnr, user_options )
+
+
+ def FileParseRequestReady( self, block = False ):
+ return bool( self._parse_request and
+ ( block or self._parse_request.Done() ) )
+
+
+ def SendParseRequest( self, extra_data ):
+ self._parse_request = EventNotification( 'FileReadyToParse',
+ extra_data = extra_data )
+ self._parse_request.Start()
+ # Decrement handled tick to ensure correct handling when we are forcing
+ # reparse on buffer visit and changed tick remains the same.
+ self._handled_tick -= 1
+ self._parse_tick = self._ChangedTick()
+
+
+ def NeedsReparse( self ):
+ return self._parse_tick != self._ChangedTick()
+
+
+ def UpdateDiagnostics( self ):
+ self._diag_interface.UpdateWithNewDiagnostics(
+ self._parse_request.Response() )
+
+
+ def PopulateLocationList( self ):
+ return self._diag_interface.PopulateLocationList()
+
+
+ def GetResponse( self ):
+ return self._parse_request.Response()
+
+
+ def IsResponseHandled( self ):
+ return self._handled_tick == self._parse_tick
+
+
+ def MarkResponseHandled( self ):
+ self._handled_tick = self._parse_tick
+
+
+ def OnCursorMoved( self ):
+ self._diag_interface.OnCursorMoved()
+
+
+ def GetErrorCount( self ):
+ return self._diag_interface.GetErrorCount()
+
+
+ def GetWarningCount( self ):
+ return self._diag_interface.GetWarningCount()
+
+
+ def _ChangedTick( self ):
+ return vimsupport.GetBufferChangedTick(self.number)
+
+
+class BufferDict( dict ):
+
+ def __init__( self, user_options ):
+ self._user_options = user_options
+
+
+ def __missing__( self, key ):
+ value = self[ key ] = Buffer( key, self._user_options )
+ return value
diff --git a/python/ycm/diagnostic_interface.py b/python/ycm/diagnostic_interface.py
index 89051635..7e5e59bf 100644
--- a/python/ycm/diagnostic_interface.py
+++ b/python/ycm/diagnostic_interface.py
@@ -30,16 +30,17 @@ 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 ):
@@ -53,54 +54,43 @@ 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, diags ):
- vimsupport.SetLocationList(
- vimsupport.ConvertDiagnosticsToQfList(
- self._ApplyDiagnosticFilter( diags ) ) )
+ def PopulateLocationList( self ):
+ # Do nothing if loc list is already populated by diag_interface
+ if not self._user_options[ 'always_populate_location_list' ]:
+ self._UpdateLocationList()
+ return bool( self._diagnostics )
def UpdateWithNewDiagnostics( self, diags ):
- normalized_diags = [ _NormalizeDiagnostic( x ) for x in
- self._ApplyDiagnosticFilter( diags ) ]
- self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
- normalized_diags )
+ self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
+ self._ApplyDiagnosticFilter( diags ) ]
+ 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.PopulateLocationList( normalized_diags )
+ 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
@@ -117,144 +107,105 @@ 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 _UpdateSquiggles( buffer_number_to_line_to_diags ):
- vimsupport.ClearYcmSyntaxMatches()
- line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ]
+ 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:
+ continue
+ line_number = location[ 'line_num' ]
+ self._line_to_diags[ line_number ].append( diag )
- 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:
- 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.
+ 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
+
+
+ def _UpdateSigns( self ):
+ new_signs, obsolete_signs = self._GetNewAndObsoleteSigns()
+
+ self._PlaceNewSigns( new_signs )
+
+ self._UnplaceObsoleteSigns( obsolete_signs )
+
+
+ def _GetNewAndObsoleteSigns( self ):
+ new_signs = []
+ obsolete_signs = 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 )
+ 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 _UpdateSquiggles( self ):
+ if self._bufnr != vim.current.buffer.number:
+ return
+
+ vimsupport.ClearYcmSyntaxMatches()
+
+ for diags in itervalues( self._line_to_diags ):
+ 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 _UpdateLocationList( self ):
+ vimsupport.SetLocationList(
+ vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )
_DiagnosticIsError = CompileLevel( 'error' )
@@ -271,12 +222,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 )
diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py
index fa4e082a..f7cf5dfc 100644
--- a/python/ycm/tests/event_notification_test.py
+++ b/python/ycm/tests/event_notification_test.py
@@ -105,7 +105,10 @@ def MockEventNotification( response_method, native_filetype_completer = True ):
'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = native_filetype_completer ):
- yield
+ with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache',
+ return_value = True ):
+
+ yield
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@@ -293,7 +296,6 @@ def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ):
# Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request.
vim_command.reset_mock()
- ok_( not ycm.FileParseRequestReady() )
ycm.HandleFileParseRequest()
vim_command.assert_not_called()
eq_( ycm.GetErrorCount(), 1 )
@@ -327,7 +329,6 @@ def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ):
# Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request.
vim_command.reset_mock()
- ok_( not ycm.FileParseRequestReady() )
ycm.HandleFileParseRequest()
vim_command.assert_not_called()
eq_( ycm.GetErrorCount(), 0 )
@@ -351,6 +352,8 @@ def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ):
@patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' )
+@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache',
+ return_value = True )
@YouCompleteMeInstance( { 'collect_identifiers_from_tags_files': 1 } )
def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
ycm, *args ):
@@ -495,6 +498,8 @@ def EventNotification_BufferUnload_BuildRequestForDeletedAndUnsavedBuffers_test(
@patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer',
return_value = [ 'foo', 'bar' ] )
+@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache',
+ return_value = True )
@YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } )
def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
ycm, *args ):
@@ -529,6 +534,8 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
@patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer',
return_value = [ 'foo', 'bar' ] )
+@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache',
+ return_value = True )
@YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } )
def EventNotification_FileReadyToParse_SyntaxKeywords_ClearCacheIfRestart_test(
ycm, *args ):
diff --git a/python/ycm/tests/test_utils.py b/python/ycm/tests/test_utils.py
index 36ce2478..30740188 100644
--- a/python/ycm/tests/test_utils.py
+++ b/python/ycm/tests/test_utils.py
@@ -40,7 +40,7 @@ BUFWINNR_REGEX = re.compile( '^bufwinnr\((?P[0-9]+)\)$' )
BWIPEOUT_REGEX = re.compile(
'^(?:silent! )bwipeout!? (?P[0-9]+)$' )
GETBUFVAR_REGEX = re.compile(
- '^getbufvar\((?P[0-9]+), "&(?P