diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 20902939..9d0fe102 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -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 diff --git a/python/ycm/diagnostic_interface.py b/python/ycm/diagnostic_interface.py index 75eebd71..03b8de02 100644 --- a/python/ycm/diagnostic_interface.py +++ b/python/ycm/diagnostic_interface.py @@ -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 diff --git a/python/ycm/tests/base_test.py b/python/ycm/tests/base_test.py index 9902dd51..11e4ae79 100644 --- a/python/ycm/tests/base_test.py +++ b/python/ycm/tests/base_test.py @@ -17,262 +17,248 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -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' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + 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' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + 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' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + 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' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar' ] ) ) + 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' ) - eq_( [ { 'abbr': 'foobar.h', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar.h' ] ) ) + with MockTextAfterCursor( 'bar.h' ): + eq_( [ { 'abbr': 'foobar.h', 'word': 'foo' } ], + base.AdjustCandidateInsertionText( [ 'foobar.h' ] ) ) - vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' ) - eq_( [ { 'abbr': 'foobar(zoo', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( [ 'foobar(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' ) - eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ], - base.AdjustCandidateInsertionText( [ 'foofoo' ] ) ) + with MockTextAfterCursor( 'bar' ): + eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ], + base.AdjustCandidateInsertionText( [ 'foofoo' ] ) ) -@with_setup( Setup ) def AdjustCandidateInsertionText_NothingAfterCursor_test(): - vimsupport.TextAfterCursor = MagicMock( return_value = '' ) - eq_( [ 'foofoo', - 'zobar' ], - base.AdjustCandidateInsertionText( [ 'foofoo', - 'zobar' ] ) ) + with MockTextAfterCursor( '' ): + eq_( [ 'foofoo', + 'zobar' ], + base.AdjustCandidateInsertionText( [ 'foofoo', + 'zobar' ] ) ) -@with_setup( Setup ) def AdjustCandidateInsertionText_MultipleStrings_test(): - vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' }, - { 'abbr': 'zobar', 'word': 'zo' }, - { 'abbr': 'qbar', 'word': 'q' }, - { 'abbr': 'bar', 'word': '' }, - ], - base.AdjustCandidateInsertionText( [ 'foobar', - 'zobar', - 'qbar', - 'bar' ] ) ) + with MockTextAfterCursor( 'bar' ): + eq_( [ { 'abbr': 'foobar', 'word': 'foo' }, + { 'abbr': 'zobar', 'word': 'zo' }, + { 'abbr': 'qbar', 'word': 'q' }, + { 'abbr': 'bar', 'word': '' }, + ], + base.AdjustCandidateInsertionText( [ 'foobar', + 'zobar', + 'qbar', + 'bar' ] ) ) -@with_setup( Setup ) def AdjustCandidateInsertionText_DictInput_test(): - vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' ) - eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( - [ { 'word': 'foobar' } ] ) ) + 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' ) - eq_( [ { 'abbr': '1234', 'word': 'foo' } ], - base.AdjustCandidateInsertionText( - [ { 'abbr': '1234', 'word': 'foobar' } ] ) ) + 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' ) - ok_( base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 3, 'abc' ): + ok_( base.LastEnteredCharIsIdentifierChar() ) - SetVimCurrentColumnAndLineValue( 2, 'abc' ) - ok_( base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 2, 'abc' ): + ok_( base.LastEnteredCharIsIdentifierChar() ) - SetVimCurrentColumnAndLineValue( 1, 'abc' ) - ok_( base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 1, 'abc' ): + ok_( base.LastEnteredCharIsIdentifierChar() ) -@with_setup( Setup ) def LastEnteredCharIsIdentifierChar_FiletypeHtml_test(): - SetVimCurrentColumnAndLineValue( 3, 'ab-' ) - vimsupport.CurrentFiletypes = MagicMock( return_value = ['html'] ) - ok_( base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentFiletypes( ['html'] ): + with MockCurrentColumnAndLineContents( 3, 'ab-' ): + ok_( base.LastEnteredCharIsIdentifierChar() ) -@with_setup( Setup ) def LastEnteredCharIsIdentifierChar_ColumnIsZero_test(): - SetVimCurrentColumnAndLineValue( 0, 'abc' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 0, 'abc' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) -@with_setup( Setup ) def LastEnteredCharIsIdentifierChar_LineEmpty_test(): - SetVimCurrentColumnAndLineValue( 3, '' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 3, '' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) - SetVimCurrentColumnAndLineValue( 0, '' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 0, '' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) -@with_setup( Setup ) def LastEnteredCharIsIdentifierChar_NotIdentChar_test(): - SetVimCurrentColumnAndLineValue( 3, 'ab;' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 3, 'ab;' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) - SetVimCurrentColumnAndLineValue( 1, ';' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 1, ';' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) - SetVimCurrentColumnAndLineValue( 3, 'ab-' ) - ok_( not base.LastEnteredCharIsIdentifierChar() ) + with MockCurrentColumnAndLineContents( 3, 'ab-' ): + ok_( not base.LastEnteredCharIsIdentifierChar() ) -@with_setup( Setup ) def CurrentIdentifierFinished_Basic_test(): - SetVimCurrentColumnAndLineValue( 3, 'ab;' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 3, 'ab;' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 2, 'ab;' ) - ok_( not base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 2, 'ab;' ): + ok_( not base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 1, 'ab;' ) - ok_( not base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 1, 'ab;' ): + ok_( not base.CurrentIdentifierFinished() ) -@with_setup( Setup ) def CurrentIdentifierFinished_NothingBeforeColumn_test(): - SetVimCurrentColumnAndLineValue( 0, 'ab;' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 0, 'ab;' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 0, '' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 0, '' ): + ok_( base.CurrentIdentifierFinished() ) -@with_setup( Setup ) def CurrentIdentifierFinished_InvalidColumn_test(): - SetVimCurrentColumnAndLineValue( 5, '' ) - ok_( not base.CurrentIdentifierFinished() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 5, '' ): + ok_( not base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 5, 'abc' ) - ok_( not base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 5, 'abc' ): + ok_( not base.CurrentIdentifierFinished() ) -@with_setup( Setup ) def CurrentIdentifierFinished_InMiddleOfLine_test(): - SetVimCurrentColumnAndLineValue( 4, 'bar.zoo' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 4, 'bar.zoo' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 4, 'bar(zoo' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 4, 'bar(zoo' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 4, 'bar-zoo' ) - ok_( base.CurrentIdentifierFinished() ) + 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'] ) - ok_( not base.CurrentIdentifierFinished() ) + with MockCurrentFiletypes( ['html'] ): + with MockCurrentColumnAndLineContents( 4, 'bar-zoo' ): + ok_( not base.CurrentIdentifierFinished() ) -@with_setup( Setup ) def CurrentIdentifierFinished_WhitespaceOnly_test(): - SetVimCurrentColumnAndLineValue( 1, '\n' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentFiletypes(): + with MockCurrentColumnAndLineContents( 1, '\n' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 3, '\n ' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 3, '\n ' ): + ok_( base.CurrentIdentifierFinished() ) - SetVimCurrentColumnAndLineValue( 3, '\t\t\t\t' ) - ok_( base.CurrentIdentifierFinished() ) + with MockCurrentColumnAndLineContents( 3, '\t\t\t\t' ): + ok_( base.CurrentIdentifierFinished() ) diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py index 190acf2f..4bfb7895 100644 --- a/python/ycm/tests/event_notification_test.py +++ b/python/ycm/tests/event_notification_test.py @@ -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 ) + diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 83e389ac..b3e74138 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -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. - # - # 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() + 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 + # 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