Moved parse request and diagnostics handling to python.

Moved File parse request handling and diagnostic extraction flow into
python to simplify flow and allow easier addition of new parse request
handlers such as semantic highlighter.
Refactored base_test to patch separate vimsupport functions instead of
the whole module, and interfering the test results afterwards.
Added new tests for diagnostic sign place/unplace and error/warning
count extraction API.
This commit is contained in:
Davit Samvelyan 2016-01-11 23:16:49 +04:00
parent 8ad4044203
commit 4d97437872
5 changed files with 311 additions and 237 deletions

View File

@ -29,14 +29,6 @@ let s:cursor_moved = 0
let s:moved_vertically_in_insert_mode = 0
let s:previous_num_chars_on_current_line = strlen( getline('.') )
let s:diagnostic_ui_filetypes = {
\ 'cpp': 1,
\ 'cs': 1,
\ 'c': 1,
\ 'objc': 1,
\ 'objcpp': 1,
\ }
function! youcompleteme#Enable()
" When vim is in diff mode, don't run
@ -308,11 +300,6 @@ function! s:TurnOffSyntasticForCFamily()
endfunction
function! s:DiagnosticUiSupportedForCurrentFiletype()
return get( s:diagnostic_ui_filetypes, &filetype, 0 )
endfunction
function! s:AllowedToCompleteInCurrentFile()
if empty( &filetype ) ||
\ getbufvar( winbufnr( winnr() ), "&buftype" ) ==# 'nofile' ||
@ -462,11 +449,11 @@ function! s:OnFileReadyToParse()
" happen for special buffers.
call s:SetUpYcmChangedTick()
" Order is important here; we need to extract any done diagnostics before
" Order is important here; we need to extract any information before
" reparsing the file again. If we sent the new parse request first, then
" the response would always be pending when we called
" UpdateDiagnosticNotifications.
call s:UpdateDiagnosticNotifications()
" HandleFileParseRequest.
py ycm_state.HandleFileParseRequest()
let buffer_changed = b:changedtick != b:ycm_changedtick.file_ready_to_parse
if buffer_changed
@ -612,19 +599,6 @@ function! s:ClosePreviewWindowIfNeeded()
endfunction
function! s:UpdateDiagnosticNotifications()
let should_display_diagnostics = g:ycm_show_diagnostics_ui &&
\ s:DiagnosticUiSupportedForCurrentFiletype()
if !should_display_diagnostics
py ycm_state.ValidateParseRequest()
return
endif
py ycm_state.UpdateDiagnosticInterface()
endfunction
function! s:IdentifierFinishedOperations()
if !pyeval( 'base.CurrentIdentifierFinished()' )
return
@ -853,15 +827,8 @@ function! s:ForceCompile()
echom "Forcing compilation, this will block Vim until done."
py ycm_state.OnFileReadyToParse()
while 1
let diagnostics_ready = pyeval(
\ 'ycm_state.DiagnosticsForCurrentFileReady()' )
if diagnostics_ready
break
endif
py ycm_state.HandleFileParseRequest( True )
sleep 100m
endwhile
return 1
endfunction
@ -871,8 +838,6 @@ function! s:ForceCompileAndDiagnostics()
if !compilation_succeeded
return
endif
call s:UpdateDiagnosticNotifications()
echom "Diagnostics refreshed."
endfunction
@ -883,11 +848,7 @@ function! s:ShowDiagnostics()
return
endif
let diags = pyeval(
\ 'ycm_state.GetDiagnosticsFromStoredRequest( qflist_format = True )' )
if !empty( diags )
call setloclist( 0, diags )
if pyeval( 'ycm_state.PopulateLocationListWithLatestDiagnostics()' )
if g:ycm_open_loclist_on_ycm_diags
lopen
endif

View File

@ -50,6 +50,11 @@ class DiagnosticInterface( object ):
return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
def PopulateLocationList( self, diags ):
vimsupport.SetLocationList(
vimsupport.ConvertDiagnosticsToQfList( diags ) )
def UpdateWithNewDiagnostics( self, diags ):
normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags ]
self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
@ -65,8 +70,7 @@ class DiagnosticInterface( object ):
_UpdateSquiggles( self._buffer_number_to_line_to_diags )
if self._user_options[ 'always_populate_location_list' ]:
vimsupport.SetLocationList(
vimsupport.ConvertDiagnosticsToQfList( normalized_diags ) )
self.PopulateLocationList( normalized_diags )
def _EchoDiagnosticForLine( self, line_num ):
buffer_num = vim.current.buffer.number

View File

@ -17,85 +17,85 @@
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from nose.tools import eq_, ok_, with_setup
from mock import MagicMock
import contextlib
from nose.tools import eq_, ok_
from mock import patch
from ycm.test_utils import MockVimModule
vim_mock = MockVimModule()
from ycm import base
from ycm import vimsupport
import sys
# column is 0-based
def SetVimCurrentColumnAndLineValue( column, line_value ):
vimsupport.CurrentColumn = MagicMock( return_value = column )
vimsupport.CurrentLineContents = MagicMock( return_value = line_value )
def Setup():
sys.modules[ 'ycm.vimsupport' ] = MagicMock()
vimsupport.CurrentFiletypes = MagicMock( return_value = [''] )
vimsupport.CurrentColumn = MagicMock( return_value = 1 )
vimsupport.CurrentLineContents = MagicMock( return_value = '' )
@contextlib.contextmanager
def MockCurrentFiletypes( filetypes = [''] ):
with patch( 'ycm.vimsupport.CurrentFiletypes', return_value = filetypes ):
yield
@contextlib.contextmanager
def MockCurrentColumnAndLineContents( column, line_contents ):
with patch( 'ycm.vimsupport.CurrentColumn', return_value = column ):
with patch( 'ycm.vimsupport.CurrentLineContents',
return_value = line_contents ):
yield
@contextlib.contextmanager
def MockTextAfterCursor( text ):
with patch( 'ycm.vimsupport.TextAfterCursor', return_value = text ):
yield
@with_setup( Setup )
def AdjustCandidateInsertionText_Basic_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
with MockTextAfterCursor( 'bar' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_ParenInTextAfterCursor_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' )
with MockTextAfterCursor( 'bar(zoo' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_PlusInTextAfterCursor_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar+zoo' )
with MockTextAfterCursor( 'bar+zoo' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_WhitespaceInTextAfterCursor_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar zoo' )
with MockTextAfterCursor( 'bar zoo' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_MoreThanWordMatchingAfterCursor_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar.h' )
with MockTextAfterCursor( 'bar.h' ):
eq_( [ { 'abbr': 'foobar.h', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar.h' ] ) )
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' )
with MockTextAfterCursor( 'bar(zoo' ):
eq_( [ { 'abbr': 'foobar(zoo', 'word': 'foo' } ],
base.AdjustCandidateInsertionText( [ 'foobar(zoo' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_NotSuffix_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
with MockTextAfterCursor( 'bar' ):
eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ],
base.AdjustCandidateInsertionText( [ 'foofoo' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_NothingAfterCursor_test():
vimsupport.TextAfterCursor = MagicMock( return_value = '' )
with MockTextAfterCursor( '' ):
eq_( [ 'foofoo',
'zobar' ],
base.AdjustCandidateInsertionText( [ 'foofoo',
'zobar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_MultipleStrings_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
with MockTextAfterCursor( 'bar' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' },
{ 'abbr': 'zobar', 'word': 'zo' },
{ 'abbr': 'qbar', 'word': 'q' },
@ -107,172 +107,158 @@ def AdjustCandidateInsertionText_MultipleStrings_test():
'bar' ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_DictInput_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
with MockTextAfterCursor( 'bar' ):
eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
base.AdjustCandidateInsertionText(
[ { 'word': 'foobar' } ] ) )
@with_setup( Setup )
def AdjustCandidateInsertionText_DontTouchAbbr_test():
vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
with MockTextAfterCursor( 'bar' ):
eq_( [ { 'abbr': '1234', 'word': 'foo' } ],
base.AdjustCandidateInsertionText(
[ { 'abbr': '1234', 'word': 'foobar' } ] ) )
@with_setup( Setup )
def OverlapLength_Basic_test():
eq_( 3, base.OverlapLength( 'foo bar', 'bar zoo' ) )
eq_( 3, base.OverlapLength( 'foobar', 'barzoo' ) )
@with_setup( Setup )
def OverlapLength_BasicWithUnicode_test():
eq_( 3, base.OverlapLength( u'bar fäö', u'fäö bar' ) )
eq_( 3, base.OverlapLength( u'zoofäö', u'fäözoo' ) )
@with_setup( Setup )
def OverlapLength_OneCharOverlap_test():
eq_( 1, base.OverlapLength( 'foo b', 'b zoo' ) )
@with_setup( Setup )
def OverlapLength_SameStrings_test():
eq_( 6, base.OverlapLength( 'foobar', 'foobar' ) )
@with_setup( Setup )
def OverlapLength_Substring_test():
eq_( 6, base.OverlapLength( 'foobar', 'foobarzoo' ) )
eq_( 6, base.OverlapLength( 'zoofoobar', 'foobar' ) )
@with_setup( Setup )
def OverlapLength_LongestOverlap_test():
eq_( 7, base.OverlapLength( 'bar foo foo', 'foo foo bar' ) )
@with_setup( Setup )
def OverlapLength_EmptyInput_test():
eq_( 0, base.OverlapLength( '', 'goobar' ) )
eq_( 0, base.OverlapLength( 'foobar', '' ) )
eq_( 0, base.OverlapLength( '', '' ) )
@with_setup( Setup )
def OverlapLength_NoOverlap_test():
eq_( 0, base.OverlapLength( 'foobar', 'goobar' ) )
eq_( 0, base.OverlapLength( 'foobar', '(^($@#$#@' ) )
eq_( 0, base.OverlapLength( 'foo bar zoo', 'foo zoo bar' ) )
@with_setup( Setup )
def LastEnteredCharIsIdentifierChar_Basic_test():
SetVimCurrentColumnAndLineValue( 3, 'abc' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 3, 'abc' ):
ok_( base.LastEnteredCharIsIdentifierChar() )
SetVimCurrentColumnAndLineValue( 2, 'abc' )
with MockCurrentColumnAndLineContents( 2, 'abc' ):
ok_( base.LastEnteredCharIsIdentifierChar() )
SetVimCurrentColumnAndLineValue( 1, 'abc' )
with MockCurrentColumnAndLineContents( 1, 'abc' ):
ok_( base.LastEnteredCharIsIdentifierChar() )
@with_setup( Setup )
def LastEnteredCharIsIdentifierChar_FiletypeHtml_test():
SetVimCurrentColumnAndLineValue( 3, 'ab-' )
vimsupport.CurrentFiletypes = MagicMock( return_value = ['html'] )
with MockCurrentFiletypes( ['html'] ):
with MockCurrentColumnAndLineContents( 3, 'ab-' ):
ok_( base.LastEnteredCharIsIdentifierChar() )
@with_setup( Setup )
def LastEnteredCharIsIdentifierChar_ColumnIsZero_test():
SetVimCurrentColumnAndLineValue( 0, 'abc' )
with MockCurrentColumnAndLineContents( 0, 'abc' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
@with_setup( Setup )
def LastEnteredCharIsIdentifierChar_LineEmpty_test():
SetVimCurrentColumnAndLineValue( 3, '' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 3, '' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
SetVimCurrentColumnAndLineValue( 0, '' )
with MockCurrentColumnAndLineContents( 0, '' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
@with_setup( Setup )
def LastEnteredCharIsIdentifierChar_NotIdentChar_test():
SetVimCurrentColumnAndLineValue( 3, 'ab;' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 3, 'ab;' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
SetVimCurrentColumnAndLineValue( 1, ';' )
with MockCurrentColumnAndLineContents( 1, ';' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
SetVimCurrentColumnAndLineValue( 3, 'ab-' )
with MockCurrentColumnAndLineContents( 3, 'ab-' ):
ok_( not base.LastEnteredCharIsIdentifierChar() )
@with_setup( Setup )
def CurrentIdentifierFinished_Basic_test():
SetVimCurrentColumnAndLineValue( 3, 'ab;' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 3, 'ab;' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 2, 'ab;' )
with MockCurrentColumnAndLineContents( 2, 'ab;' ):
ok_( not base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 1, 'ab;' )
with MockCurrentColumnAndLineContents( 1, 'ab;' ):
ok_( not base.CurrentIdentifierFinished() )
@with_setup( Setup )
def CurrentIdentifierFinished_NothingBeforeColumn_test():
SetVimCurrentColumnAndLineValue( 0, 'ab;' )
with MockCurrentColumnAndLineContents( 0, 'ab;' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 0, '' )
with MockCurrentColumnAndLineContents( 0, '' ):
ok_( base.CurrentIdentifierFinished() )
@with_setup( Setup )
def CurrentIdentifierFinished_InvalidColumn_test():
SetVimCurrentColumnAndLineValue( 5, '' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 5, '' ):
ok_( not base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 5, 'abc' )
with MockCurrentColumnAndLineContents( 5, 'abc' ):
ok_( not base.CurrentIdentifierFinished() )
@with_setup( Setup )
def CurrentIdentifierFinished_InMiddleOfLine_test():
SetVimCurrentColumnAndLineValue( 4, 'bar.zoo' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 4, 'bar.zoo' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 4, 'bar(zoo' )
with MockCurrentColumnAndLineContents( 4, 'bar(zoo' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 4, 'bar-zoo' )
with MockCurrentColumnAndLineContents( 4, 'bar-zoo' ):
ok_( base.CurrentIdentifierFinished() )
@with_setup( Setup )
def CurrentIdentifierFinished_Html_test():
SetVimCurrentColumnAndLineValue( 4, 'bar-zoo' )
vimsupport.CurrentFiletypes = MagicMock( return_value = ['html'] )
with MockCurrentFiletypes( ['html'] ):
with MockCurrentColumnAndLineContents( 4, 'bar-zoo' ):
ok_( not base.CurrentIdentifierFinished() )
@with_setup( Setup )
def CurrentIdentifierFinished_WhitespaceOnly_test():
SetVimCurrentColumnAndLineValue( 1, '\n' )
with MockCurrentFiletypes():
with MockCurrentColumnAndLineContents( 1, '\n' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 3, '\n ' )
with MockCurrentColumnAndLineContents( 3, '\n ' ):
ok_( base.CurrentIdentifierFinished() )
SetVimCurrentColumnAndLineValue( 3, '\t\t\t\t' )
with MockCurrentColumnAndLineContents( 3, '\t\t\t\t' ):
ok_( base.CurrentIdentifierFinished() )

View File

@ -23,9 +23,11 @@ import os
from ycm.youcompleteme import YouCompleteMe
from ycmd import user_options_store
from ycmd.responses import UnknownExtraConf
from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range,
UnknownExtraConf )
from mock import call, MagicMock, patch
from nose.tools import eq_, ok_
# The default options which are only relevant to the client, not the server and
@ -34,6 +36,10 @@ from mock import call, MagicMock, patch
DEFAULT_CLIENT_OPTIONS = {
'server_log_level': 'info',
'extra_conf_vim_data': [],
'show_diagnostics_ui': 1,
'enable_diagnostic_signs': 1,
'enable_diagnostic_highlighting': 0,
'always_populate_location_list': 0,
}
@ -51,6 +57,17 @@ def PresentDialog_Confirm_Call( message ):
return call( message, [ 'Ok', 'Cancel' ] )
def PlaceSign_Call( sign_id, line_num, buffer_num, is_error ):
sign_name = 'YcmError' if is_error else 'YcmWarning'
return call( 'sign place {0} line={1} name={2} buffer={3}'
.format( sign_id, line_num, sign_name, buffer_num ) )
def UnplaceSign_Call( sign_id, buffer_num ):
return call( 'try | exec "sign unplace {0} buffer={1}" |'
' catch /E158/ | endtry'.format( sign_id, buffer_num ) )
@contextlib.contextmanager
def MockArbitraryBuffer( filetype, native_available = True ):
"""Used via the with statement, set up mocked versions of the vim module such
@ -72,6 +89,12 @@ def MockArbitraryBuffer( filetype, native_available = True ):
if value == 'getbufvar(0, "&ft")' or value == '&filetype':
return filetype
if value.startswith( 'bufnr(' ):
return 0
if value.startswith( 'bufwinnr(' ):
return 0
raise ValueError( 'Unexpected evaluation' )
# Arbitrary, but valid, cursor position
@ -82,6 +105,7 @@ def MockArbitraryBuffer( filetype, native_available = True ):
current_buffer.number = 0
current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
current_buffer.name = 'TEST_BUFFER'
current_buffer.window = 0
# The rest just mock up the Vim module so that our single arbitrary buffer
# makes sense to vimsupport module.
@ -144,8 +168,8 @@ class EventNotification_test( object ):
@patch( 'vim.command', new_callable = ExtendedMock )
def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ):
# This test validates the behaviour of YouCompleteMe.ValidateParseRequest in
# combination with YouCompleteMe.OnFileReadyToParse when the completer
# This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
# in combination with YouCompleteMe.OnFileReadyToParse when the completer
# raises an exception handling FileReadyToParse event notification
ERROR_TEXT = 'Some completer response text'
@ -155,8 +179,8 @@ class EventNotification_test( object ):
with MockArbitraryBuffer( 'javascript' ):
with MockEventNotification( ErrorResponse ):
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
# The first call raises a warning
vim_command.assert_has_exact_calls( [
@ -164,15 +188,15 @@ class EventNotification_test( object ):
] )
# Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest()
self.server_state.HandleFileParseRequest()
vim_command.assert_has_exact_calls( [
PostVimMessage_Call( ERROR_TEXT ),
] )
# But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
vim_command.assert_has_exact_calls( [
PostVimMessage_Call( ERROR_TEXT ),
PostVimMessage_Call( ERROR_TEXT ),
@ -184,7 +208,7 @@ class EventNotification_test( object ):
with MockArbitraryBuffer( 'javascript' ):
with MockEventNotification( None, False ):
self.server_state.OnFileReadyToParse()
self.server_state.ValidateParseRequest()
self.server_state.HandleFileParseRequest()
vim_command.assert_not_called()
@ -198,8 +222,8 @@ class EventNotification_test( object ):
load_extra_conf,
*args ):
# This test validates the behaviour of YouCompleteMe.ValidateParseRequest in
# combination with YouCompleteMe.OnFileReadyToParse when the completer
# This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
# in combination with YouCompleteMe.OnFileReadyToParse when the completer
# raises the (special) UnknownExtraConf exception
FILE_NAME = 'a_file'
@ -217,8 +241,8 @@ class EventNotification_test( object ):
return_value = 0,
new_callable = ExtendedMock ) as present_dialog:
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ),
@ -228,7 +252,7 @@ class EventNotification_test( object ):
] )
# Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE )
@ -239,8 +263,8 @@ class EventNotification_test( object ):
# But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ),
@ -256,8 +280,8 @@ class EventNotification_test( object ):
return_value = 1,
new_callable = ExtendedMock ) as present_dialog:
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ),
@ -267,7 +291,7 @@ class EventNotification_test( object ):
] )
# Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE )
@ -278,8 +302,8 @@ class EventNotification_test( object ):
# But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady()
self.server_state.ValidateParseRequest()
assert self.server_state.FileParseRequestReady()
self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ),
@ -289,3 +313,92 @@ class EventNotification_test( object ):
call( FILE_NAME ),
call( FILE_NAME ),
] )
def FileReadyToParse_Diagnostic_Error_Native_test( self ):
self._Check_FileReadyToParse_Diagnostic_Error()
self._Check_FileReadyToParse_Diagnostic_Warning()
self._Check_FileReadyToParse_Diagnostic_Clean()
@patch( 'vim.command' )
def _Check_FileReadyToParse_Diagnostic_Error( self, vim_command ):
# Tests Vim sign placement and error/warning count python API
# when one error is returned.
def DiagnosticResponse( *args ):
start = Location( 1, 2, 'TEST_BUFFER' )
end = Location( 1, 4, 'TEST_BUFFER' )
extent = Range( start, end )
diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
return [ BuildDiagnosticData( diagnostic ) ]
with MockArbitraryBuffer( 'cpp' ):
with MockEventNotification( DiagnosticResponse ):
self.server_state.OnFileReadyToParse()
ok_( self.server_state.FileParseRequestReady() )
self.server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [
PlaceSign_Call( 1, 1, 0, True )
] )
eq_( self.server_state.GetErrorCount(), 1 )
eq_( self.server_state.GetWarningCount(), 0 )
# Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request.
vim_command.reset_mock()
ok_( not self.server_state.FileParseRequestReady() )
self.server_state.HandleFileParseRequest()
vim_command.assert_not_called()
eq_( self.server_state.GetErrorCount(), 1 )
eq_( self.server_state.GetWarningCount(), 0 )
@patch( 'vim.command' )
def _Check_FileReadyToParse_Diagnostic_Warning( self, vim_command ):
# Tests Vim sign placement/unplacement and error/warning count python API
# when one warning is returned.
# Should be called after _Check_FileReadyToParse_Diagnostic_Error
def DiagnosticResponse( *args ):
start = Location( 2, 2, 'TEST_BUFFER' )
end = Location( 2, 4, 'TEST_BUFFER' )
extent = Range( start, end )
diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
return [ BuildDiagnosticData( diagnostic ) ]
with MockArbitraryBuffer( 'cpp' ):
with MockEventNotification( DiagnosticResponse ):
self.server_state.OnFileReadyToParse()
ok_( self.server_state.FileParseRequestReady() )
self.server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [
PlaceSign_Call( 2, 2, 0, False ),
UnplaceSign_Call( 1, 0 )
] )
eq_( self.server_state.GetErrorCount(), 0 )
eq_( self.server_state.GetWarningCount(), 1 )
# Consequent calls to HandleFileParseRequest shouldn't mess with
# existing diagnostics, when there is no new parse request.
vim_command.reset_mock()
ok_( not self.server_state.FileParseRequestReady() )
self.server_state.HandleFileParseRequest()
vim_command.assert_not_called()
eq_( self.server_state.GetErrorCount(), 0 )
eq_( self.server_state.GetWarningCount(), 1 )
@patch( 'vim.command' )
def _Check_FileReadyToParse_Diagnostic_Clean( self, 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 MockArbitraryBuffer( 'cpp' ):
with MockEventNotification( MagicMock( return_value = [] ) ):
self.server_state.OnFileReadyToParse()
self.server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [
UnplaceSign_Call( 2, 0 )
] )
eq_( self.server_state.GetErrorCount(), 0 )
eq_( self.server_state.GetWarningCount(), 0 )

View File

@ -75,6 +75,7 @@ SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED = (
"Logfile was deleted; set 'g:ycm_server_keep_logfiles' to see errors "
"in the future." )
SERVER_IDLE_SUICIDE_SECONDS = 10800 # 3 hours
DIAGNOSTIC_UI_FILETYPES = set( [ 'cpp', 'cs', 'c', 'objc', 'objcpp' ] )
class YouCompleteMe( object ):
@ -85,6 +86,7 @@ class YouCompleteMe( object ):
self._omnicomp = OmniCompleter( user_options )
self._latest_file_parse_request = None
self._latest_completion_request = None
self._latest_diagnostics = []
self._server_stdout = None
self._server_stderr = None
self._server_popen = None
@ -447,56 +449,64 @@ class YouCompleteMe( object ):
return None
return completion[ "extra_data" ][ "required_namespace_import" ]
def GetErrorCount( self ):
return self._diag_interface.GetErrorCount()
def GetWarningCount( self ):
return self._diag_interface.GetWarningCount()
def DiagnosticsForCurrentFileReady( self ):
return bool( self._latest_file_parse_request and
self._latest_file_parse_request.Done() )
def DiagnosticUiSupportedForCurrentFiletype( self ):
return any( [ x in DIAGNOSTIC_UI_FILETYPES
for x in vimsupport.CurrentFiletypes() ] )
def GetDiagnosticsFromStoredRequest( self, qflist_format = False ):
if self.DiagnosticsForCurrentFileReady():
diagnostics = self._latest_file_parse_request.Response()
# We set the diagnostics request to None because we want to prevent
# repeated refreshing of the buffer with the same diags. Setting this to
# None makes DiagnosticsForCurrentFileReady return False until the next
# request is created.
self._latest_file_parse_request = None
if qflist_format:
return vimsupport.ConvertDiagnosticsToQfList( diagnostics )
else:
return diagnostics
return []
def ShouldDisplayDiagnostics( self ):
return bool( self._user_options[ 'show_diagnostics_ui' ] and
self.DiagnosticUiSupportedForCurrentFiletype() )
def PopulateLocationListWithLatestDiagnostics( self ):
# Do nothing if loc list is already populated by diag_interface
if not self._user_options[ 'always_populate_location_list' ]:
self._diag_interface.PopulateLocationList( self._latest_diagnostics )
return bool( self._latest_diagnostics )
def UpdateDiagnosticInterface( self ):
if ( self.DiagnosticsForCurrentFileReady() and
self.NativeFiletypeCompletionUsable() ):
self._diag_interface.UpdateWithNewDiagnostics(
self.GetDiagnosticsFromStoredRequest() )
self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics )
def ValidateParseRequest( self ):
if ( self.DiagnosticsForCurrentFileReady() and
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 ):
# Order is important here:
# FileParseRequestReady has a low cost, while
# NativeFiletypeCompletionUsable is a blocking server request
if ( self.FileParseRequestReady( block ) and
self.NativeFiletypeCompletionUsable() ):
# YCM client has a hard-coded list of filetypes which are known to support
# diagnostics. These are found in autoload/youcompleteme.vim in
# s:diagnostic_ui_filetypes.
if self.ShouldDisplayDiagnostics():
self._latest_diagnostics = self._latest_file_parse_request.Response()
self.UpdateDiagnosticInterface()
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
# 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.
self._latest_file_parse_request.Response()
# We set the diagnostics request to None because we want to prevent
# We set the file parse request to None because we want to prevent
# repeated issuing of the same warnings/errors/prompts. Setting this to
# None makes DiagnosticsForCurrentFileReady return False until the next
# None makes FileParseRequestReady return False until the next
# request is created.
#
# Note: it is the server's responsibility to determine the frequency of