From 0846673aa49349de4ae63ea992b409f84ea39bf1 Mon Sep 17 00:00:00 2001 From: Davit Samvelyan Date: Sun, 21 May 2017 18:26:50 +0400 Subject: [PATCH] Latest upstream changes with buffer emulation. Contains diagnostic interface improvents as well. --- autoload/youcompleteme.vim | 4 +- python/ycm/buffer.py | 105 +++++++ python/ycm/diagnostic_interface.py | 287 ++++++++------------ python/ycm/tests/event_notification_test.py | 13 +- python/ycm/tests/test_utils.py | 13 +- python/ycm/tests/youcompleteme_test.py | 36 ++- python/ycm/vimsupport.py | 13 + python/ycm/youcompleteme.py | 72 ++--- 8 files changed, 327 insertions(+), 216 deletions(-) create mode 100644 python/ycm/buffer.py 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