From 4d974378726663dc40f05d9926fce95b7fa6ef10 Mon Sep 17 00:00:00 2001 From: Davit Samvelyan Date: Mon, 11 Jan 2016 23:16:49 +0400 Subject: [PATCH] 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. --- autoload/youcompleteme.vim | 49 +--- python/ycm/diagnostic_interface.py | 8 +- python/ycm/tests/base_test.py | 260 +++++++++----------- python/ycm/tests/event_notification_test.py | 155 ++++++++++-- python/ycm/youcompleteme.py | 76 +++--- 5 files changed, 311 insertions(+), 237 deletions(-) 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