Auto merge of #2631 - davits:changedtick, r=bstaletic

[READY] Moved change tracking to python. Per buffer diagnostics

Thank you for working on YCM! :)

- [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.**

Adds an emulation of Vim buffer object and moves change tracking into python.
Now diagnostics are stored per buffer, which effectively solves #2165 .

<!-- 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/2631)
<!-- Reviewable:end -->
This commit is contained in:
zzbot 2017-06-05 03:21:41 -07:00 committed by GitHub
commit c2906b6fef
8 changed files with 223 additions and 68 deletions

View File

@ -105,6 +105,9 @@ function! youcompleteme#Enable()
autocmd CompleteDone * call s:OnCompleteDone() autocmd CompleteDone * call s:OnCompleteDone()
augroup END 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 FileType event is not triggered for the first loaded file. We wait until
" the server is ready to manually run the s:OnFileTypeSet function. " the server is ready to manually run the s:OnFileTypeSet function.
let s:pollers.server_ready.id = timer_start( let s:pollers.server_ready.id = timer_start(
@ -431,6 +434,8 @@ endfunction
function! s:OnBufferEnter() function! s:OnBufferEnter()
exec s:python_command "ycm_state.SetCurrentBuffer()"
if !s:VisitedBufferRequiresReparse() if !s:VisitedBufferRequiresReparse()
return return
endif endif
@ -460,7 +465,7 @@ endfunction
function! s:PollServerReady( timer_id ) function! s:PollServerReady( timer_id )
if !s:Pyeval( 'ycm_state.IsServerReady()' ) if !s:Pyeval( 'ycm_state.CheckIfServerIsReady()' )
let s:pollers.server_ready.id = timer_start( let s:pollers.server_ready.id = timer_start(
\ s:pollers.server_ready.wait_milliseconds, \ s:pollers.server_ready.wait_milliseconds,
\ function( 's:PollServerReady' ) ) \ function( 's:PollServerReady' ) )
@ -479,15 +484,13 @@ function! s:OnFileReadyToParse( ... )
" We only want to send a new FileReadyToParse event notification if the buffer " 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. " 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()" exec s:python_command "ycm_state.OnFileReadyToParse()"
call timer_stop( s:pollers.file_parse_response.id ) call timer_stop( s:pollers.file_parse_response.id )
let s:pollers.file_parse_response.id = timer_start( let s:pollers.file_parse_response.id = timer_start(
\ s:pollers.file_parse_response.wait_milliseconds, \ s:pollers.file_parse_response.wait_milliseconds,
\ function( 's:PollFileParseResponse' ) ) \ function( 's:PollFileParseResponse' ) )
let b:ycm_changedtick = b:changedtick
endif endif
endfunction endfunction

109
python/ycm/buffer.py Normal file
View File

@ -0,0 +1,109 @@
# 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 <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
# Not installing aliases from python-future; it's unreliable and slow.
from builtins import * # noqa
from ycm import vimsupport
from ycm.client.event_notification import EventNotification
from ycm.diagnostic_interface import DiagnosticInterface
# Emulates Vim buffer
# Used to store buffer related information like diagnostics, latest parse
# request. Stores buffer change tick at the parse request moment, allowing
# to effectively determine whether reparse is needed for the buffer.
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( 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 ):
# Python does not allow to return assignment operation result directly
new_value = self[ key ] = Buffer( key, self._user_options )
return new_value

View File

@ -32,6 +32,7 @@ import vim
class DiagnosticInterface( object ): class DiagnosticInterface( object ):
def __init__( self, user_options ): def __init__( self, user_options ):
self._user_options = user_options self._user_options = user_options
self._diagnostics = []
self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options ) self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
# Line and column numbers are 1-based # Line and column numbers are 1-based
self._buffer_number_to_line_to_diags = defaultdict( self._buffer_number_to_line_to_diags = defaultdict(
@ -60,17 +61,18 @@ class DiagnosticInterface( object ):
return len( self._FilterDiagnostics( _DiagnosticIsWarning ) ) return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
def PopulateLocationList( self, diags ): def PopulateLocationList( self ):
vimsupport.SetLocationList( # Do nothing if loc list is already populated by diag_interface
vimsupport.ConvertDiagnosticsToQfList( if not self._user_options[ 'always_populate_location_list' ]:
self._ApplyDiagnosticFilter( diags ) ) ) self._UpdateLocationList()
return bool( self._diagnostics )
def UpdateWithNewDiagnostics( self, diags ): def UpdateWithNewDiagnostics( self, diags ):
normalized_diags = [ _NormalizeDiagnostic( x ) for x in self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
self._ApplyDiagnosticFilter( diags ) ] self._ApplyDiagnosticFilter( diags ) ]
self._buffer_number_to_line_to_diags = _ConvertDiagListToDict( self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
normalized_diags ) self._diagnostics )
if self._user_options[ 'enable_diagnostic_signs' ]: if self._user_options[ 'enable_diagnostic_signs' ]:
self._placed_signs, self._next_sign_id = _UpdateSigns( self._placed_signs, self._next_sign_id = _UpdateSigns(
@ -82,7 +84,7 @@ class DiagnosticInterface( object ):
_UpdateSquiggles( self._buffer_number_to_line_to_diags ) _UpdateSquiggles( self._buffer_number_to_line_to_diags )
if self._user_options[ 'always_populate_location_list' ]: if self._user_options[ 'always_populate_location_list' ]:
self.PopulateLocationList( normalized_diags ) self._UpdateLocationList()
def _ApplyDiagnosticFilter( self, diags, extra_predicate = None ): def _ApplyDiagnosticFilter( self, diags, extra_predicate = None ):
@ -128,6 +130,11 @@ class DiagnosticInterface( object ):
return matched_diags return matched_diags
def _UpdateLocationList( self ):
vimsupport.SetLocationList(
vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )
def _UpdateSquiggles( buffer_number_to_line_to_diags ): def _UpdateSquiggles( buffer_number_to_line_to_diags ):
vimsupport.ClearYcmSyntaxMatches() vimsupport.ClearYcmSyntaxMatches()
line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ] line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ]

View File

@ -59,7 +59,7 @@ def UnplaceSign_Call( sign_id, buffer_num ):
@contextlib.contextmanager @contextlib.contextmanager
def MockArbitraryBuffer( filetype ): def MockArbitraryBuffer( filetype, ycm ):
"""Used via the with statement, set up a single buffer with an arbitrary name """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.""" and no contents. Its filetype is set to the supplied filetype."""
@ -68,7 +68,7 @@ def MockArbitraryBuffer( filetype ):
window = 1, window = 1,
filetype = filetype ) filetype = filetype )
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
yield yield
@ -105,6 +105,9 @@ def MockEventNotification( response_method, native_filetype_completer = True ):
'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = native_filetype_completer ): return_value = native_filetype_completer ):
with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True ):
yield yield
@ -121,7 +124,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_Error_test(
def ErrorResponse( *args ): def ErrorResponse( *args ):
raise ServerError( ERROR_TEXT ) raise ServerError( ERROR_TEXT )
with MockArbitraryBuffer( 'javascript' ): with MockArbitraryBuffer( 'javascript', ycm ):
with MockEventNotification( ErrorResponse ): with MockEventNotification( ErrorResponse ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
ok_( ycm.FileParseRequestReady() ) ok_( ycm.FileParseRequestReady() )
@ -153,7 +156,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_Error_test(
def EventNotification_FileReadyToParse_NonDiagnostic_Error_NonNative_test( def EventNotification_FileReadyToParse_NonDiagnostic_Error_NonNative_test(
ycm, vim_command ): ycm, vim_command ):
with MockArbitraryBuffer( 'javascript' ): with MockArbitraryBuffer( 'javascript', ycm ):
with MockEventNotification( None, False ): with MockEventNotification( None, False ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
ycm.HandleFileParseRequest() ycm.HandleFileParseRequest()
@ -179,7 +182,7 @@ def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test(
def UnknownExtraConfResponse( *args ): def UnknownExtraConfResponse( *args ):
raise UnknownExtraConf( FILE_NAME ) raise UnknownExtraConf( FILE_NAME )
with MockArbitraryBuffer( 'javascript' ): with MockArbitraryBuffer( 'javascript', ycm ):
with MockEventNotification( UnknownExtraConfResponse ): with MockEventNotification( UnknownExtraConfResponse ):
# When the user accepts the extra conf, we load it # When the user accepts the extra conf, we load it
@ -279,7 +282,7 @@ def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ):
diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' ) diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
return [ BuildDiagnosticData( diagnostic ) ] return [ BuildDiagnosticData( diagnostic ) ]
with MockArbitraryBuffer( 'cpp' ): with MockArbitraryBuffer( 'cpp', ycm ):
with MockEventNotification( DiagnosticResponse ): with MockEventNotification( DiagnosticResponse ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
ok_( ycm.FileParseRequestReady() ) ok_( ycm.FileParseRequestReady() )
@ -293,7 +296,6 @@ def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ):
# Consequent calls to HandleFileParseRequest shouldn't mess with # Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request. # existing diagnostics, when there is no new parse request.
vim_command.reset_mock() vim_command.reset_mock()
ok_( not ycm.FileParseRequestReady() )
ycm.HandleFileParseRequest() ycm.HandleFileParseRequest()
vim_command.assert_not_called() vim_command.assert_not_called()
eq_( ycm.GetErrorCount(), 1 ) eq_( ycm.GetErrorCount(), 1 )
@ -312,7 +314,7 @@ def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ):
diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' ) diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
return [ BuildDiagnosticData( diagnostic ) ] return [ BuildDiagnosticData( diagnostic ) ]
with MockArbitraryBuffer( 'cpp' ): with MockArbitraryBuffer( 'cpp', ycm ):
with MockEventNotification( DiagnosticResponse ): with MockEventNotification( DiagnosticResponse ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
ok_( ycm.FileParseRequestReady() ) ok_( ycm.FileParseRequestReady() )
@ -327,7 +329,6 @@ def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ):
# Consequent calls to HandleFileParseRequest shouldn't mess with # Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request. # existing diagnostics, when there is no new parse request.
vim_command.reset_mock() vim_command.reset_mock()
ok_( not ycm.FileParseRequestReady() )
ycm.HandleFileParseRequest() ycm.HandleFileParseRequest()
vim_command.assert_not_called() vim_command.assert_not_called()
eq_( ycm.GetErrorCount(), 0 ) eq_( ycm.GetErrorCount(), 0 )
@ -339,7 +340,7 @@ def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ):
# Tests Vim sign unplacement and error/warning count python API # Tests Vim sign unplacement and error/warning count python API
# when there are no errors/warnings left. # when there are no errors/warnings left.
# Should be called after _Check_FileReadyToParse_Diagnostic_Warning # Should be called after _Check_FileReadyToParse_Diagnostic_Warning
with MockArbitraryBuffer( 'cpp' ): with MockArbitraryBuffer( 'cpp', ycm ):
with MockEventNotification( MagicMock( return_value = [] ) ): with MockEventNotification( MagicMock( return_value = [] ) ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
ycm.HandleFileParseRequest() ycm.HandleFileParseRequest()
@ -351,6 +352,8 @@ def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ):
@patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' ) @patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@YouCompleteMeInstance( { 'collect_identifiers_from_tags_files': 1 } ) @YouCompleteMeInstance( { 'collect_identifiers_from_tags_files': 1 } )
def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test( def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
ycm, *args ): ycm, *args ):
@ -363,7 +366,7 @@ def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
with patch( 'ycm.client.event_notification.EventNotification.' with patch( 'ycm.client.event_notification.EventNotification.'
'PostDataToHandlerAsync' ) as post_data_to_handler_async: 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
with CurrentWorkingDirectory( unicode_dir ): with CurrentWorkingDirectory( unicode_dir ):
with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ): with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ), ycm ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
assert_that( assert_that(
@ -495,6 +498,8 @@ def EventNotification_BufferUnload_BuildRequestForDeletedAndUnsavedBuffers_test(
@patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer', @patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer',
return_value = [ 'foo', 'bar' ] ) return_value = [ 'foo', 'bar' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } ) @YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } )
def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test( def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
ycm, *args ): ycm, *args ):
@ -504,7 +509,7 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
with patch( 'ycm.client.event_notification.EventNotification.' with patch( 'ycm.client.event_notification.EventNotification.'
'PostDataToHandlerAsync' ) as post_data_to_handler_async: 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
assert_that( assert_that(
# Positional arguments passed to PostDataToHandlerAsync. # Positional arguments passed to PostDataToHandlerAsync.
@ -529,6 +534,8 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_SeedWithCache_test(
@patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer', @patch( 'ycm.syntax_parse.SyntaxKeywordsForCurrentBuffer',
return_value = [ 'foo', 'bar' ] ) return_value = [ 'foo', 'bar' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } ) @YouCompleteMeInstance( { 'seed_identifiers_with_syntax': 1 } )
def EventNotification_FileReadyToParse_SyntaxKeywords_ClearCacheIfRestart_test( def EventNotification_FileReadyToParse_SyntaxKeywords_ClearCacheIfRestart_test(
ycm, *args ): ycm, *args ):
@ -538,7 +545,7 @@ def EventNotification_FileReadyToParse_SyntaxKeywords_ClearCacheIfRestart_test(
with patch( 'ycm.client.event_notification.EventNotification.' with patch( 'ycm.client.event_notification.EventNotification.'
'PostDataToHandlerAsync' ) as post_data_to_handler_async: 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()
assert_that( assert_that(
# Positional arguments passed to PostDataToHandlerAsync. # Positional arguments passed to PostDataToHandlerAsync.

View File

@ -40,7 +40,7 @@ BUFWINNR_REGEX = re.compile( '^bufwinnr\((?P<buffer_number>[0-9]+)\)$' )
BWIPEOUT_REGEX = re.compile( BWIPEOUT_REGEX = re.compile(
'^(?:silent! )bwipeout!? (?P<buffer_number>[0-9]+)$' ) '^(?:silent! )bwipeout!? (?P<buffer_number>[0-9]+)$' )
GETBUFVAR_REGEX = re.compile( GETBUFVAR_REGEX = re.compile(
'^getbufvar\((?P<buffer_number>[0-9]+), "&(?P<option>.+)"\)$' ) '^getbufvar\((?P<buffer_number>[0-9]+), "(?P<option>.+)"\)$' )
MATCHADD_REGEX = re.compile( MATCHADD_REGEX = re.compile(
'^matchadd\(\'(?P<group>.+)\', \'(?P<pattern>.+)\'\)$' ) '^matchadd\(\'(?P<group>.+)\', \'(?P<pattern>.+)\'\)$' )
MATCHDELETE_REGEX = re.compile( '^matchdelete\((?P<id>)\)$' ) MATCHDELETE_REGEX = re.compile( '^matchdelete\((?P<id>)\)$' )
@ -85,10 +85,12 @@ def _MockGetBufferWindowNumber( buffer_number ):
def _MockGetBufferVariable( buffer_number, option ): def _MockGetBufferVariable( buffer_number, option ):
for vim_buffer in VIM_MOCK.buffers: for vim_buffer in VIM_MOCK.buffers:
if vim_buffer.number == buffer_number: if vim_buffer.number == buffer_number:
if option == 'mod': if option == '&mod':
return vim_buffer.modified return vim_buffer.modified
if option == 'ft': if option == '&ft':
return vim_buffer.filetype return vim_buffer.filetype
if option == 'changedtick':
return vim_buffer.changedtick
return '' return ''
return '' return ''
@ -230,6 +232,7 @@ class VimBuffer( object ):
self.modified = modified self.modified = modified
self.window = window self.window = window
self.omnifunc = omnifunc self.omnifunc = omnifunc
self.changedtick = 1
def __getitem__( self, index ): def __getitem__( self, index ):
@ -268,7 +271,8 @@ class VimMatch( object ):
@contextlib.contextmanager @contextlib.contextmanager
def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ) ): def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ),
ycm_state = None):
"""Simulates the Vim buffers list |buffers| where |current_buffer| is the """Simulates the Vim buffers list |buffers| where |current_buffer| is the
buffer displayed in the current window and |cursor_position| is the current buffer displayed in the current window and |cursor_position| is the current
cursor position. All buffers are represented by a VimBuffer object.""" cursor position. All buffers are represented by a VimBuffer object."""
@ -278,6 +282,8 @@ def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ) ):
with patch( 'vim.buffers', buffers ): with patch( 'vim.buffers', buffers ):
with patch( 'vim.current.buffer', current_buffer ): with patch( 'vim.current.buffer', current_buffer ):
with patch( 'vim.current.window.cursor', cursor_position ): with patch( 'vim.current.window.cursor', cursor_position ):
if ycm_state is not None:
ycm_state.SetCurrentBuffer()
yield yield

View File

@ -330,7 +330,7 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
ycm, set_location_list, post_vim_message, *args ): ycm, set_location_list, post_vim_message, *args ):
current_buffer = VimBuffer( 'buffer', filetype = 'cpp' ) current_buffer = VimBuffer( 'buffer', filetype = 'cpp' )
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
with patch( 'ycm.client.event_notification.EventNotification.Response', with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = {} ): return_value = {} ):
ycm.ShowDiagnostics() ycm.ShowDiagnostics()
@ -349,6 +349,8 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
'open_loclist_on_ycm_diags': 0 } ) 'open_loclist_on_ycm_diags': 0 } )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True ) return_value = True )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock )
def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test( def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
@ -365,7 +367,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
} }
current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 ) current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
with patch( 'ycm.client.event_notification.EventNotification.Response', with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = [ diagnostic ] ): return_value = [ diagnostic ] ):
ycm.ShowDiagnostics() ycm.ShowDiagnostics()
@ -388,6 +390,8 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
@YouCompleteMeInstance( { 'open_loclist_on_ycm_diags': 1 } ) @YouCompleteMeInstance( { 'open_loclist_on_ycm_diags': 1 } )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True ) return_value = True )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.OpenLocationList', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.OpenLocationList', new_callable = ExtendedMock )
@ -405,7 +409,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
} }
current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 ) current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer, ycm_state = ycm ):
with patch( 'ycm.client.event_notification.EventNotification.Response', with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = [ diagnostic ] ): return_value = [ diagnostic ] ):
ycm.ShowDiagnostics() ycm.ShowDiagnostics()
@ -431,6 +435,8 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
'enable_diagnostic_highlighting': 1 } ) 'enable_diagnostic_highlighting': 1 } )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype', @patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True ) return_value = True )
@patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReady',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'vim.command', new_callable = ExtendedMock ) @patch( 'vim.command', new_callable = ExtendedMock )
def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test( def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
@ -508,7 +514,7 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
test_utils.VIM_MATCHES = [] test_utils.VIM_MATCHES = []
with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ) ): with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ), ycm ):
with patch( 'ycm.client.event_notification.EventNotification.Response', with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = diagnostics ): return_value = diagnostics ):
ycm.OnFileReadyToParse() ycm.OnFileReadyToParse()

View File

@ -147,6 +147,14 @@ def GetBufferFilepath( buffer_object ):
return os.path.join( GetCurrentDirectory(), str( buffer_object.number ) ) return os.path.join( GetCurrentDirectory(), str( buffer_object.number ) )
def GetCurrentBufferNumber():
return vim.current.buffer.number
def GetBufferChangedTick( bufnr ):
return GetIntValue( 'getbufvar({0}, "changedtick")'.format( bufnr ) )
def UnplaceSignInBuffer( buffer_number, sign_id ): def UnplaceSignInBuffer( buffer_number, sign_id ):
if buffer_number < 0: if buffer_number < 0:
return return

View File

@ -33,10 +33,10 @@ import vim
from subprocess import PIPE from subprocess import PIPE
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from ycm import base, paths, vimsupport from ycm import base, paths, vimsupport
from ycm.buffer import BufferDict
from ycmd import utils from ycmd import utils
from ycmd import server_utils from ycmd import server_utils
from ycmd.request_wrap import RequestWrap from ycmd.request_wrap import RequestWrap
from ycm.diagnostic_interface import DiagnosticInterface
from ycm.omni_completer import OmniCompleter from ycm.omni_completer import OmniCompleter
from ycm import syntax_parse from ycm import syntax_parse
from ycm.client.ycmd_keepalive import YcmdKeepalive from ycm.client.ycmd_keepalive import YcmdKeepalive
@ -49,8 +49,7 @@ from ycm.client.completion_request import ( CompletionRequest,
from ycm.client.debug_info_request import ( SendDebugInfoRequest, from ycm.client.debug_info_request import ( SendDebugInfoRequest,
FormatDebugInfoResponse ) FormatDebugInfoResponse )
from ycm.client.omni_completion_request import OmniCompletionRequest from ycm.client.omni_completion_request import OmniCompletionRequest
from ycm.client.event_notification import ( SendEventNotificationAsync, from ycm.client.event_notification import SendEventNotificationAsync
EventNotification )
from ycm.client.shutdown_request import SendShutdownRequest from ycm.client.shutdown_request import SendShutdownRequest
@ -114,11 +113,10 @@ class YouCompleteMe( object ):
self._available_completers = {} self._available_completers = {}
self._user_options = user_options self._user_options = user_options
self._user_notified_about_crash = False self._user_notified_about_crash = False
self._diag_interface = DiagnosticInterface( user_options )
self._omnicomp = OmniCompleter( user_options ) self._omnicomp = OmniCompleter( user_options )
self._latest_file_parse_request = None self._buffers = BufferDict( user_options )
self._current_buffer = None
self._latest_completion_request = None self._latest_completion_request = None
self._latest_diagnostics = []
self._logger = logging.getLogger( 'ycm' ) self._logger = logging.getLogger( 'ycm' )
self._client_logfile = None self._client_logfile = None
self._server_stdout = None self._server_stdout = None
@ -134,6 +132,7 @@ class YouCompleteMe( object ):
'cs': lambda self: self._OnCompleteDone_Csharp() 'cs': lambda self: self._OnCompleteDone_Csharp()
} }
def _SetupServer( self ): def _SetupServer( self ):
self._available_completers = {} self._available_completers = {}
self._user_notified_about_crash = False self._user_notified_about_crash = False
@ -222,7 +221,7 @@ class YouCompleteMe( object ):
return return_code is None return return_code is None
def IsServerReady( self ): def CheckIfServerIsReady( self ):
if not self._server_is_ready_with_cache and self.IsServerAlive(): if not self._server_is_ready_with_cache and self.IsServerAlive():
with HandleServerException( display = False ): with HandleServerException( display = False ):
self._server_is_ready_with_cache = BaseRequest.GetDataFromHandler( self._server_is_ready_with_cache = BaseRequest.GetDataFromHandler(
@ -230,6 +229,10 @@ class YouCompleteMe( object ):
return self._server_is_ready_with_cache return self._server_is_ready_with_cache
def IsServerReady( self ):
return self._server_is_ready_with_cache
def _NotifyUserIfServerCrashed( self ): def _NotifyUserIfServerCrashed( self ):
if self._user_notified_about_crash or self.IsServerAlive(): if self._user_notified_about_crash or self.IsServerAlive():
return return
@ -353,11 +356,18 @@ class YouCompleteMe( object ):
self.NativeFiletypeCompletionAvailable() ) self.NativeFiletypeCompletionAvailable() )
def NeedsReparse( self ):
return self._current_buffer.NeedsReparse()
def OnFileReadyToParse( self ): def OnFileReadyToParse( self ):
if not self.IsServerAlive(): if not self.IsServerAlive():
self._NotifyUserIfServerCrashed() self._NotifyUserIfServerCrashed()
return return
if not self.IsServerReady():
return
self._omnicomp.OnFileReadyToParse( None ) self._omnicomp.OnFileReadyToParse( None )
extra_data = {} extra_data = {}
@ -365,9 +375,7 @@ class YouCompleteMe( object ):
self._AddSyntaxDataIfNeeded( extra_data ) self._AddSyntaxDataIfNeeded( extra_data )
self._AddExtraConfDataIfNeeded( extra_data ) self._AddExtraConfDataIfNeeded( extra_data )
self._latest_file_parse_request = EventNotification( self._current_buffer.SendParseRequest( extra_data )
'FileReadyToParse', extra_data = extra_data )
self._latest_file_parse_request.Start()
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, deleted_buffer_file ):
@ -380,12 +388,16 @@ class YouCompleteMe( object ):
SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data ) SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data )
def SetCurrentBuffer( self ):
self._current_buffer = self._buffers[ vimsupport.GetCurrentBufferNumber() ]
def OnInsertLeave( self ): def OnInsertLeave( self ):
SendEventNotificationAsync( 'InsertLeave' ) SendEventNotificationAsync( 'InsertLeave' )
def OnCursorMoved( self ): def OnCursorMoved( self ):
self._diag_interface.OnCursorMoved() self._current_buffer.OnCursorMoved()
def _CleanLogfile( self ): def _CleanLogfile( self ):
@ -512,11 +524,11 @@ class YouCompleteMe( object ):
def GetErrorCount( self ): def GetErrorCount( self ):
return self._diag_interface.GetErrorCount() return self._current_buffer.GetErrorCount()
def GetWarningCount( self ): def GetWarningCount( self ):
return self._diag_interface.GetWarningCount() return self._current_buffer.GetWarningCount()
def DiagnosticUiSupportedForCurrentFiletype( self ): def DiagnosticUiSupportedForCurrentFiletype( self ):
@ -530,31 +542,29 @@ class YouCompleteMe( object ):
def _PopulateLocationListWithLatestDiagnostics( self ): def _PopulateLocationListWithLatestDiagnostics( self ):
# Do nothing if loc list is already populated by diag_interface return self._current_buffer.PopulateLocationList()
if not self._user_options[ 'always_populate_location_list' ]:
self._diag_interface.PopulateLocationList( self._latest_diagnostics )
return bool( self._latest_diagnostics )
def UpdateDiagnosticInterface( self ): def FileParseRequestReady( self ):
self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics ) # Return True if server is not ready yet, to stop repeating check timer.
return ( not self.IsServerReady() or
self._current_buffer.FileParseRequestReady() )
def FileParseRequestReady( self, block = False ):
return bool( self._latest_file_parse_request and
( block or self._latest_file_parse_request.Done() ) )
def HandleFileParseRequest( self, block = False ): def HandleFileParseRequest( self, block = False ):
if not self.IsServerReady():
return
current_buffer = self._current_buffer
# Order is important here: # Order is important here:
# FileParseRequestReady has a low cost, while # FileParseRequestReady has a low cost, while
# NativeFiletypeCompletionUsable is a blocking server request # NativeFiletypeCompletionUsable is a blocking server request
if ( self.FileParseRequestReady( block ) and if ( not current_buffer.IsResponseHandled() and
current_buffer.FileParseRequestReady( block ) and
self.NativeFiletypeCompletionUsable() ): self.NativeFiletypeCompletionUsable() ):
if self.ShouldDisplayDiagnostics(): if self.ShouldDisplayDiagnostics():
self._latest_diagnostics = self._latest_file_parse_request.Response() current_buffer.UpdateDiagnostics()
self.UpdateDiagnosticInterface()
else: else:
# YCM client has a hard-coded list of filetypes which are known # YCM client has a hard-coded list of filetypes which are known
# to support diagnostics, self.DiagnosticUiSupportedForCurrentFiletype() # to support diagnostics, self.DiagnosticUiSupportedForCurrentFiletype()
@ -563,18 +573,17 @@ class YouCompleteMe( object ):
# the _latest_file_parse_request for any exception or UnknownExtraConf # the _latest_file_parse_request for any exception or UnknownExtraConf
# response, to allow the server to raise configuration warnings, etc. # response, to allow the server to raise configuration warnings, etc.
# to the user. We ignore any other supplied data. # to the user. We ignore any other supplied data.
self._latest_file_parse_request.Response() current_buffer.GetResponse()
# We set the file parse request to None because we want to prevent # We set the file parse request as handled because we want to prevent
# repeated issuing of the same warnings/errors/prompts. Setting this to # repeated issuing of the same warnings/errors/prompts. Setting this
# None makes FileParseRequestReady return False until the next # makes IsRequestHandled return True until the next request is created.
# request is created.
# #
# Note: it is the server's responsibility to determine the frequency of # Note: it is the server's responsibility to determine the frequency of
# error/warning/prompts when receiving a FileReadyToParse event, but # error/warning/prompts when receiving a FileReadyToParse event, but
# it our responsibility to ensure that we only apply the # it is our responsibility to ensure that we only apply the
# warning/error/prompt received once (for each event). # warning/error/prompt received once (for each event).
self._latest_file_parse_request = None current_buffer.MarkResponseHandled()
def DebugInfo( self ): def DebugInfo( self ):