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 index 8d931a7d..636506ae 100644 --- a/python/ycm/buffer.py +++ b/python/ycm/buffer.py @@ -38,13 +38,17 @@ class Buffer( object ): def FileParseRequestReady( self, block = False ): - return self._parse_tick == 0 or block or self._parse_request.Done() + 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() @@ -53,8 +57,8 @@ class Buffer( object ): def UpdateDiagnostics( self ): - diagnostics = self._parse_request.Response() - self._diag_interface.UpdateWithNewDiagnostics( diagnostics ) + self._diag_interface.UpdateWithNewDiagnostics( + self._parse_request.Response() ) def PopulateLocationList( self ): diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py index 463cd060..f7cf5dfc 100644 --- a/python/ycm/tests/event_notification_test.py +++ b/python/ycm/tests/event_notification_test.py @@ -25,7 +25,6 @@ from __future__ import absolute_import from builtins import * # noqa from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock, - EmulateCurrentBufferChange, MockVimBuffers, MockVimModule, VimBuffer ) MockVimModule() @@ -106,7 +105,7 @@ def MockEventNotification( response_method, native_filetype_completer = True ): 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', return_value = native_filetype_completer ): - with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady', + with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache', return_value = True ): yield @@ -142,7 +141,6 @@ def EventNotification_FileReadyToParse_NonDiagnostic_Error_test( call( ERROR_TEXT, truncate = True ) ] ) - EmulateCurrentBufferChange() # But it does if a subsequent event raises again ycm.OnFileReadyToParse() ok_( ycm.FileParseRequestReady() ) @@ -212,7 +210,6 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test( call( FILE_NAME ), ] ) - EmulateCurrentBufferChange() # But it does if a subsequent event raises again ycm.OnFileReadyToParse() ok_( ycm.FileParseRequestReady() ) @@ -227,7 +224,6 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test( call( FILE_NAME ), ] ) - EmulateCurrentBufferChange() # When the user rejects the extra conf, we reject it with patch( 'ycm.vimsupport.PresentDialog', return_value = 1, @@ -253,7 +249,6 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test( call( FILE_NAME ), ] ) - EmulateCurrentBufferChange() # But it does if a subsequent event raises again ycm.OnFileReadyToParse() ok_( ycm.FileParseRequestReady() ) @@ -271,12 +266,9 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test( @YouCompleteMeInstance() def EventNotification_FileReadyToParse_Diagnostic_Error_Native_test( ycm ): - with MockArbitraryBuffer( 'cpp' ): - _Check_FileReadyToParse_Diagnostic_Error( ycm ) - EmulateCurrentBufferChange() - _Check_FileReadyToParse_Diagnostic_Warning( ycm ) - EmulateCurrentBufferChange() - _Check_FileReadyToParse_Diagnostic_Clean( ycm ) + _Check_FileReadyToParse_Diagnostic_Error( ycm ) + _Check_FileReadyToParse_Diagnostic_Warning( ycm ) + _Check_FileReadyToParse_Diagnostic_Clean( ycm ) @patch( 'vim.command' ) @@ -290,24 +282,24 @@ def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ): diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' ) return [ BuildDiagnosticData( diagnostic ) ] - with MockEventNotification( DiagnosticResponse ): - ycm.OnFileReadyToParse() - ok_( ycm.FileParseRequestReady() ) - ycm.HandleFileParseRequest() - vim_command.assert_has_calls( [ - PlaceSign_Call( 1, 1, 1, True ) - ] ) - eq_( ycm.GetErrorCount(), 1 ) - eq_( ycm.GetWarningCount(), 0 ) + with MockArbitraryBuffer( 'cpp' ): + with MockEventNotification( DiagnosticResponse ): + ycm.OnFileReadyToParse() + ok_( ycm.FileParseRequestReady() ) + ycm.HandleFileParseRequest() + vim_command.assert_has_calls( [ + PlaceSign_Call( 1, 1, 1, True ) + ] ) + eq_( ycm.GetErrorCount(), 1 ) + eq_( ycm.GetWarningCount(), 0 ) - # Consequent calls to HandleFileParseRequest shouldn't mess with - # existing diagnostics, when there is no new parse request. - vim_command.reset_mock() - ok_( ycm.FileParseRequestReady() ) - ycm.HandleFileParseRequest() - vim_command.assert_not_called() - eq_( ycm.GetErrorCount(), 1 ) - eq_( ycm.GetWarningCount(), 0 ) + # Consequent calls to HandleFileParseRequest shouldn't mess with + # existing diagnostics, when there is no new parse request. + vim_command.reset_mock() + ycm.HandleFileParseRequest() + vim_command.assert_not_called() + eq_( ycm.GetErrorCount(), 1 ) + eq_( ycm.GetWarningCount(), 0 ) @patch( 'vim.command' ) @@ -322,25 +314,25 @@ def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ): diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' ) return [ BuildDiagnosticData( diagnostic ) ] - with MockEventNotification( DiagnosticResponse ): - ycm.OnFileReadyToParse() - ok_( ycm.FileParseRequestReady() ) - ycm.HandleFileParseRequest() - vim_command.assert_has_calls( [ - PlaceSign_Call( 2, 2, 1, False ), - UnplaceSign_Call( 1, 1 ) - ] ) - eq_( ycm.GetErrorCount(), 0 ) - eq_( ycm.GetWarningCount(), 1 ) + with MockArbitraryBuffer( 'cpp' ): + with MockEventNotification( DiagnosticResponse ): + ycm.OnFileReadyToParse() + ok_( ycm.FileParseRequestReady() ) + ycm.HandleFileParseRequest() + vim_command.assert_has_calls( [ + PlaceSign_Call( 2, 2, 1, False ), + UnplaceSign_Call( 1, 1 ) + ] ) + eq_( ycm.GetErrorCount(), 0 ) + eq_( ycm.GetWarningCount(), 1 ) - # Consequent calls to HandleFileParseRequest shouldn't mess with - # existing diagnostics, when there is no new parse request. - vim_command.reset_mock() - ok_( ycm.FileParseRequestReady() ) - ycm.HandleFileParseRequest() - vim_command.assert_not_called() - eq_( ycm.GetErrorCount(), 0 ) - eq_( ycm.GetWarningCount(), 1 ) + # Consequent calls to HandleFileParseRequest shouldn't mess with + # existing diagnostics, when there is no new parse request. + vim_command.reset_mock() + ycm.HandleFileParseRequest() + vim_command.assert_not_called() + eq_( ycm.GetErrorCount(), 0 ) + eq_( ycm.GetWarningCount(), 1 ) @patch( 'vim.command' ) @@ -348,17 +340,20 @@ 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 MockEventNotification( MagicMock( return_value = [] ) ): - ycm.OnFileReadyToParse() - ycm.HandleFileParseRequest() - vim_command.assert_has_calls( [ - UnplaceSign_Call( 2, 1 ) - ] ) - eq_( ycm.GetErrorCount(), 0 ) - eq_( ycm.GetWarningCount(), 0 ) + with MockArbitraryBuffer( 'cpp' ): + with MockEventNotification( MagicMock( return_value = [] ) ): + ycm.OnFileReadyToParse() + ycm.HandleFileParseRequest() + vim_command.assert_has_calls( [ + UnplaceSign_Call( 2, 1 ) + ] ) + eq_( ycm.GetErrorCount(), 0 ) + eq_( ycm.GetWarningCount(), 0 ) @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 ): @@ -372,9 +367,7 @@ def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test( 'PostDataToHandlerAsync' ) as post_data_to_handler_async: with CurrentWorkingDirectory( unicode_dir ): with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ): - with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady', - return_value = True ): - ycm.OnFileReadyToParse() + ycm.OnFileReadyToParse() assert_that( # Positional arguments passed to PostDataToHandlerAsync. @@ -505,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 ): @@ -539,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/youcompleteme_test.py b/python/ycm/tests/youcompleteme_test.py index 6722e69d..a5570c7c 100644 --- a/python/ycm/tests/youcompleteme_test.py +++ b/python/ycm/tests/youcompleteme_test.py @@ -349,7 +349,8 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test( 'open_loclist_on_ycm_diags': 0 } ) @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', return_value = True ) -@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady', return_value = True ) +@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache', + return_value = True ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock ) def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test( @@ -389,7 +390,8 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test( @YouCompleteMeInstance( { 'open_loclist_on_ycm_diags': 1 } ) @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', return_value = True ) -@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady', return_value = True ) +@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache', + return_value = True ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.OpenLocationList', new_callable = ExtendedMock ) @@ -433,7 +435,8 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test( 'enable_diagnostic_highlighting': 1 } ) @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', return_value = True ) -@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady', return_value = True ) +@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache', + return_value = True ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'vim.command', new_callable = ExtendedMock ) def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test( @@ -514,7 +517,8 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test( with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ) ): with patch( 'ycm.client.event_notification.EventNotification.Response', return_value = diagnostics ): - ycm.OnFileReadyToParse( block = True ) + ycm.OnFileReadyToParse() + ycm.HandleFileParseRequest( block = True ) # Error match is added after warning matches. assert_that( diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index f2d0f0a7..a37538de 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -592,6 +592,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 diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 289468bb..e064266e 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -232,6 +232,10 @@ class YouCompleteMe( object ): return self._server_is_ready_with_cache + def IsServerReadyWithCache( self ): + return self._server_is_ready_with_cache + + def _NotifyUserIfServerCrashed( self ): if self._user_notified_about_crash or self.IsServerAlive(): return @@ -355,28 +359,26 @@ class YouCompleteMe( object ): self.NativeFiletypeCompletionAvailable() ) - def OnFileReadyToParse( self, block = False, force = False ): + def NeedsReparse( self ): + return self._GetCurrentBuffer().NeedsReparse() + + + def OnFileReadyToParse( self ): if not self.IsServerAlive(): self._NotifyUserIfServerCrashed() return - if not self.IsServerReady(): + if not self.IsServerReadyWithCache(): return self._omnicomp.OnFileReadyToParse( None ) - self.HandleFileParseRequest() + extra_data = {} + self._AddTagsFilesIfNeeded( extra_data ) + self._AddSyntaxDataIfNeeded( extra_data ) + self._AddExtraConfDataIfNeeded( extra_data ) - current_buffer = self._GetCurrentBuffer() - if force_parsing or current_buffer.NeedsReparse(): - extra_data = {} - self._AddTagsFilesIfNeeded( extra_data ) - self._AddSyntaxDataIfNeeded( extra_data ) - self._AddExtraConfDataIfNeeded( extra_data ) - - current_buffer.SendParseRequest( extra_data ) - if block: - self.HandleFileParseRequest() + self._GetCurrentBuffer().SendParseRequest( extra_data ) def OnBufferUnload( self, deleted_buffer_file ): @@ -543,21 +545,44 @@ class YouCompleteMe( object ): def FileParseRequestReady( self ): - return self._GetCurrentBuffer().FileParseRequestReady() + # Return True if server is not ready yet, to stop repeating check timer. + return ( not self.IsServerReadyWithCache() or + self._GetCurrentBuffer().FileParseRequestReady() ) - def HandleFileParseRequest( self ): - current_buffer = self._GetCurrentBuffer() - if current_buffer.IsResponseHandled(): + def HandleFileParseRequest( self, block = False ): + if not self.IsServerReadyWithCache(): return + current_buffer = self._GetCurrentBuffer() + # Order is important here: + # FileParseRequestReady has a low cost, while # NativeFiletypeCompletionUsable is a blocking server request - if self.NativeFiletypeCompletionUsable(): - current_buffer.GetResponse() + if ( not current_buffer.IsResponseHandled() and + current_buffer.FileParseRequestReady( block ) and + self.NativeFiletypeCompletionUsable() ): + if self.ShouldDisplayDiagnostics(): current_buffer.UpdateDiagnostics() + else: + # YCM client has a hard-coded list of filetypes which are known + # to support diagnostics, self.DiagnosticUiSupportedForCurrentFiletype() + # + # For filetypes which don't support diagnostics, we just want to check + # the _latest_file_parse_request for any exception or UnknownExtraConf + # response, to allow the server to raise configuration warnings, etc. + # to the user. We ignore any other supplied data. + current_buffer.GetResponse() - current_buffer.MarkResponseHandled() + # We set the file parse request as handled because we want to prevent + # repeated issuing of the same warnings/errors/prompts. Setting this + # makes IsRequestHandled return True until the next request is created. + # + # Note: it is the server's responsibility to determine the frequency of + # error/warning/prompts when receiving a FileReadyToParse event, but + # it our responsibility to ensure that we only apply the + # warning/error/prompt received once (for each event). + current_buffer.MarkResponseHandled() def DebugInfo( self ): @@ -666,7 +691,8 @@ class YouCompleteMe( object ): vimsupport.PostVimMessage( 'Forcing compilation, this will block Vim until done.', warning = False ) - self.OnFileReadyToParse( block = True ) + self.OnFileReadyToParse() + self.HandleFileParseRequest( block = True ) vimsupport.PostVimMessage( 'Diagnostics refreshed', warning = False ) return True @@ -691,7 +717,7 @@ class YouCompleteMe( object ): if filetype in self._filetypes_with_keywords_loaded: return - if self.IsServerReady(): + if self.IsServerReadyWithCache(): self._filetypes_with_keywords_loaded.add( filetype ) extra_data[ 'syntax_keywords' ] = list( syntax_parse.SyntaxKeywordsForCurrentBuffer() )