Latest upstream changes with buffer emulation.

Contains diagnostic interface improvents as well.
This commit is contained in:
Davit Samvelyan 2017-05-21 18:26:50 +04:00
parent 263bd88bd5
commit 0846673aa4
8 changed files with 327 additions and 216 deletions

View File

@ -479,15 +479,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

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

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -30,16 +30,17 @@ import vim
class DiagnosticInterface( object ): class DiagnosticInterface( object ):
def __init__( self, user_options ): def __init__( self, bufnr, user_options ):
self._bufnr = bufnr
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._line_to_diags = defaultdict( list )
lambda: defaultdict( list ) ) self._placed_signs = []
self._next_sign_id = 1 self._next_sign_id = 1
self._previous_line_number = -1 self._previous_line_number = -1
self._diag_message_needs_clearing = False self._diag_message_needs_clearing = False
self._placed_signs = []
def OnCursorMoved( self ): def OnCursorMoved( self ):
@ -53,54 +54,43 @@ class DiagnosticInterface( object ):
def GetErrorCount( self ): def GetErrorCount( self ):
return len( self._FilterDiagnostics( _DiagnosticIsError ) ) return self._DiagnosticsCount( _DiagnosticIsError )
def GetWarningCount( self ): def GetWarningCount( self ):
return len( self._FilterDiagnostics( _DiagnosticIsWarning ) ) return self._DiagnosticsCount( _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._ConvertDiagListToDict()
normalized_diags )
if self._user_options[ 'enable_diagnostic_signs' ]: if self._user_options[ 'enable_diagnostic_signs' ]:
self._placed_signs, self._next_sign_id = _UpdateSigns( self._UpdateSigns()
self._placed_signs,
self._buffer_number_to_line_to_diags,
self._next_sign_id )
if self._user_options[ 'enable_diagnostic_highlighting' ]: 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' ]: 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 ):
filetypes = vimsupport.CurrentFiletypes() filetypes = vimsupport.GetBufferFiletypes( self._bufnr )
diag_filter = self._diag_filter.SubsetForTypes( filetypes ) diag_filter = self._diag_filter.SubsetForTypes( filetypes )
predicate = diag_filter.IsAllowed return filter( diag_filter.IsAllowed, diags )
if extra_predicate is not None:
def Filter( diag ):
return extra_predicate( diag ) and diag_filter.IsAllowed( diag )
predicate = Filter
return filter( predicate, diags )
def _EchoDiagnosticForLine( self, line_num ): def _EchoDiagnosticForLine( self, line_num ):
buffer_num = vim.current.buffer.number diags = self._line_to_diags[ line_num ]
diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ]
if not diags: if not diags:
if self._diag_message_needs_clearing: if self._diag_message_needs_clearing:
# Clear any previous diag echo # Clear any previous diag echo
@ -117,144 +107,105 @@ class DiagnosticInterface( object ):
self._diag_message_needs_clearing = True self._diag_message_needs_clearing = True
def _FilterDiagnostics( self, predicate ): def _DiagnosticsCount( self, predicate ):
matched_diags = [] count = 0
line_to_diags = self._buffer_number_to_line_to_diags[ for diags in itervalues( self._line_to_diags ):
vim.current.buffer.number ] count += sum( 1 for d in diags if predicate( d ) )
return count
for diags in itervalues( line_to_diags ):
matched_diags.extend( list(
self._ApplyDiagnosticFilter( diags, predicate ) ) )
return matched_diags
def _UpdateSquiggles( buffer_number_to_line_to_diags ): def _ConvertDiagListToDict( self ):
vimsupport.ClearYcmSyntaxMatches() self._line_to_diags = defaultdict( list )
line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ] 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 ): for diags in itervalues( self._line_to_diags ):
# Insert squiggles in reverse order so that errors overlap warnings. # We also want errors to be listed before warnings so that errors aren't
for diag in reversed( diags ): # hidden by the warnings; Vim won't place a sign over an existing one.
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.
diags.sort( key = lambda diag: ( diag[ 'kind' ], diags.sort( key = lambda diag: ( diag[ 'kind' ],
diag[ 'location' ][ 'column_num' ] ) ) 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' ) _DiagnosticIsError = CompileLevel( 'error' )
@ -271,12 +222,10 @@ def _NormalizeDiagnostic( diag ):
return diag return diag
class _DiagSignPlacement( class _DiagSignPlacement( namedtuple( "_DiagSignPlacement",
namedtuple( "_DiagSignPlacement", [ 'id', 'line', 'is_error' ] ) ):
[ 'id', 'line', 'buffer', 'is_error' ] ) ):
# We want two signs that have different ids but the same location to compare # We want two signs that have different ids but the same location to compare
# equal. ID doesn't matter. # equal. ID doesn't matter.
def __eq__( self, other ): def __eq__( self, other ):
return ( self.line == other.line and return ( self.line == other.line and
self.buffer == other.buffer and
self.is_error == other.is_error ) self.is_error == other.is_error )

View File

@ -105,7 +105,10 @@ 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 ):
yield with patch( 'ycm.youcompleteme.YouCompleteMe.IsServerReadyWithCache',
return_value = True ):
yield
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @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 # 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 )
@ -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 )
@ -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.IsServerReadyWithCache',
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 ):
@ -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.IsServerReadyWithCache',
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 ):
@ -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.IsServerReadyWithCache',
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 ):

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 ):
@ -250,6 +253,10 @@ class VimBuffer( object ):
return [ ToUnicode( x ) for x in self.contents ] return [ ToUnicode( x ) for x in self.contents ]
def EmulateCurrentBufferChange():
VIM_MOCK.current.buffer.changedtick += 1
class VimMatch( object ): class VimMatch( object ):
def __init__( self, group, pattern ): def __init__( self, group, pattern ):

View File

@ -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.IsServerReadyWithCache',
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(
@ -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.IsServerReadyWithCache',
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 )
@ -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.IsServerReadyWithCache',
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(
@ -526,11 +532,7 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
# Only the error sign is placed. # Only the error sign is placed.
vim_command.assert_has_exact_calls( [ vim_command.assert_has_exact_calls( [
call( 'sign define ycm_dummy_sign' ),
call( 'sign place 3 name=ycm_dummy_sign line=3 buffer=5' ),
call( 'sign place 1 name=YcmError line=3 buffer=5' ), call( 'sign place 1 name=YcmError line=3 buffer=5' ),
call( 'sign undefine ycm_dummy_sign' ),
call( 'sign unplace 3 buffer=5' )
] ) ] )
# When moving the cursor on the diagnostics, the error is displayed to the # When moving the cursor on the diagnostics, the error is displayed to the
@ -540,3 +542,29 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
call( "expected ';' after expression (FixIt)", call( "expected ';' after expression (FixIt)",
truncate = True, warning = False ) truncate = True, warning = False )
] ) ] )
# Test the case when error is fixed, but warning remains.
with MockVimBuffers( [ current_buffer ], current_buffer, ( 3, 1 ) ):
with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = diagnostics[ 1 : 2 ] ):
with patch( 'ycm.vimsupport.ClearYcmSyntaxMatches' ) as clear_matches:
test_utils.VIM_MATCHES = []
vim_command.reset_mock()
ycm.OnFileReadyToParse()
ycm.HandleFileParseRequest( block = True )
clear_matches.assert_has_calls( [ call() ] )
# Error match is added after warning matches.
assert_that(
test_utils.VIM_MATCHES,
contains(
VimMatch( 'YcmWarningSection', '\%3l\%5c\_.\{-}\%3l\%7c' ),
VimMatch( 'YcmWarningSection', '\%3l\%3c\_.\{-}\%3l\%9c' ),
)
)
# Only the error sign is placed.
vim_command.assert_has_exact_calls( [
call( 'sign place 2 name=YcmWarning line=3 buffer=5' ),
call( 'try | exec "sign unplace 1 buffer=5" | catch /E158/ | endtry' ),
] )

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
@ -584,6 +592,11 @@ def CurrentFiletypes():
return VimExpressionToPythonType( "&filetype" ).split( '.' ) return VimExpressionToPythonType( "&filetype" ).split( '.' )
def GetBufferFiletypes( bufnr ):
command = 'getbufvar({0}, "&ft")'.format( bufnr )
return VimExpressionToPythonType( command ).split( '.' )
def FiletypesForBuffer( buffer_object ): def FiletypesForBuffer( buffer_object ):
# NOTE: Getting &ft for other buffers only works when the buffer has been # 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 # visited by the user at least once, which is true for modified buffers

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,9 @@ 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._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 +131,11 @@ class YouCompleteMe( object ):
'cs': lambda self: self._OnCompleteDone_Csharp() 'cs': lambda self: self._OnCompleteDone_Csharp()
} }
def _GetCurrentBuffer( self ):
return self._buffers[ vimsupport.GetCurrentBufferNumber() ]
def _SetupServer( self ): def _SetupServer( self ):
self._available_completers = {} self._available_completers = {}
self._user_notified_about_crash = False self._user_notified_about_crash = False
@ -230,6 +232,10 @@ class YouCompleteMe( object ):
return self._server_is_ready_with_cache return self._server_is_ready_with_cache
def IsServerReadyWithCache( 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 +359,18 @@ class YouCompleteMe( object ):
self.NativeFiletypeCompletionAvailable() ) self.NativeFiletypeCompletionAvailable() )
def NeedsReparse( self ):
return self._GetCurrentBuffer().NeedsReparse()
def OnFileReadyToParse( self ): def OnFileReadyToParse( self ):
if not self.IsServerAlive(): if not self.IsServerAlive():
self._NotifyUserIfServerCrashed() self._NotifyUserIfServerCrashed()
return return
if not self.IsServerReadyWithCache():
return
self._omnicomp.OnFileReadyToParse( None ) self._omnicomp.OnFileReadyToParse( None )
extra_data = {} extra_data = {}
@ -365,9 +378,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._GetCurrentBuffer().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 ):
@ -385,7 +396,7 @@ class YouCompleteMe( object ):
def OnCursorMoved( self ): def OnCursorMoved( self ):
self._diag_interface.OnCursorMoved() self._GetCurrentBuffer().OnCursorMoved()
def _CleanLogfile( self ): def _CleanLogfile( self ):
@ -512,11 +523,11 @@ class YouCompleteMe( object ):
def GetErrorCount( self ): def GetErrorCount( self ):
return self._diag_interface.GetErrorCount() return self._GetCurrentBuffer().GetErrorCount()
def GetWarningCount( self ): def GetWarningCount( self ):
return self._diag_interface.GetWarningCount() return self._GetCurrentBuffer().GetWarningCount()
def DiagnosticUiSupportedForCurrentFiletype( self ): def DiagnosticUiSupportedForCurrentFiletype( self ):
@ -530,31 +541,33 @@ class YouCompleteMe( object ):
def _PopulateLocationListWithLatestDiagnostics( self ): def _PopulateLocationListWithLatestDiagnostics( self ):
# Do nothing if loc list is already populated by diag_interface return self._GetCurrentBuffer().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 UpdateDiagnosticInterface( self ):
self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics ) self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics )
def FileParseRequestReady( self, block = False ): def FileParseRequestReady( self ):
return bool( self._latest_file_parse_request and # Return True if server is not ready yet, to stop repeating check timer.
( block or self._latest_file_parse_request.Done() ) ) return ( not self.IsServerReadyWithCache() or
self._GetCurrentBuffer().FileParseRequestReady() )
def HandleFileParseRequest( self, block = False ): def HandleFileParseRequest( self, block = False ):
if not self.IsServerReadyWithCache():
return
current_buffer = self._GetCurrentBuffer()
# 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 +576,9 @@ 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 current_buffer.MarkResponseHandled()
# repeated issuing of the same warnings/errors/prompts. Setting this to
# None makes FileParseRequestReady return False 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).
self._latest_file_parse_request = None
def DebugInfo( self ): def DebugInfo( self ):
@ -709,7 +713,7 @@ class YouCompleteMe( object ):
if filetype in self._filetypes_with_keywords_loaded: if filetype in self._filetypes_with_keywords_loaded:
return return
if self.IsServerReady(): if self.IsServerReadyWithCache():
self._filetypes_with_keywords_loaded.add( filetype ) self._filetypes_with_keywords_loaded.add( filetype )
extra_data[ 'syntax_keywords' ] = list( extra_data[ 'syntax_keywords' ] = list(
syntax_parse.SyntaxKeywordsForCurrentBuffer() ) syntax_parse.SyntaxKeywordsForCurrentBuffer() )