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

View File

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

View File

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

View File

@ -23,9 +23,11 @@ import os
from ycm.youcompleteme import YouCompleteMe from ycm.youcompleteme import YouCompleteMe
from ycmd import user_options_store 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 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 # 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 = { DEFAULT_CLIENT_OPTIONS = {
'server_log_level': 'info', 'server_log_level': 'info',
'extra_conf_vim_data': [], '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' ] ) 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 @contextlib.contextmanager
def MockArbitraryBuffer( filetype, native_available = True ): def MockArbitraryBuffer( filetype, native_available = True ):
"""Used via the with statement, set up mocked versions of the vim module such """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': if value == 'getbufvar(0, "&ft")' or value == '&filetype':
return filetype return filetype
if value.startswith( 'bufnr(' ):
return 0
if value.startswith( 'bufwinnr(' ):
return 0
raise ValueError( 'Unexpected evaluation' ) raise ValueError( 'Unexpected evaluation' )
# Arbitrary, but valid, cursor position # Arbitrary, but valid, cursor position
@ -82,6 +105,7 @@ def MockArbitraryBuffer( filetype, native_available = True ):
current_buffer.number = 0 current_buffer.number = 0
current_buffer.filename = os.path.realpath( 'TEST_BUFFER' ) current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
current_buffer.name = '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 # The rest just mock up the Vim module so that our single arbitrary buffer
# makes sense to vimsupport module. # makes sense to vimsupport module.
@ -144,8 +168,8 @@ class EventNotification_test( object ):
@patch( 'vim.command', new_callable = ExtendedMock ) @patch( 'vim.command', new_callable = ExtendedMock )
def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ): def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ):
# This test validates the behaviour of YouCompleteMe.ValidateParseRequest in # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
# combination with YouCompleteMe.OnFileReadyToParse when the completer # in combination with YouCompleteMe.OnFileReadyToParse when the completer
# raises an exception handling FileReadyToParse event notification # raises an exception handling FileReadyToParse event notification
ERROR_TEXT = 'Some completer response text' ERROR_TEXT = 'Some completer response text'
@ -155,8 +179,8 @@ class EventNotification_test( object ):
with MockArbitraryBuffer( 'javascript' ): with MockArbitraryBuffer( 'javascript' ):
with MockEventNotification( ErrorResponse ): with MockEventNotification( ErrorResponse ):
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
# The first call raises a warning # The first call raises a warning
vim_command.assert_has_exact_calls( [ vim_command.assert_has_exact_calls( [
@ -164,15 +188,15 @@ class EventNotification_test( object ):
] ) ] )
# Subsequent calls don't re-raise the warning # Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
vim_command.assert_has_exact_calls( [ vim_command.assert_has_exact_calls( [
PostVimMessage_Call( ERROR_TEXT ), PostVimMessage_Call( ERROR_TEXT ),
] ) ] )
# But it does if a subsequent event raises again # But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
vim_command.assert_has_exact_calls( [ vim_command.assert_has_exact_calls( [
PostVimMessage_Call( ERROR_TEXT ), PostVimMessage_Call( ERROR_TEXT ),
PostVimMessage_Call( ERROR_TEXT ), PostVimMessage_Call( ERROR_TEXT ),
@ -184,7 +208,7 @@ class EventNotification_test( object ):
with MockArbitraryBuffer( 'javascript' ): with MockArbitraryBuffer( 'javascript' ):
with MockEventNotification( None, False ): with MockEventNotification( None, False ):
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
vim_command.assert_not_called() vim_command.assert_not_called()
@ -198,8 +222,8 @@ class EventNotification_test( object ):
load_extra_conf, load_extra_conf,
*args ): *args ):
# This test validates the behaviour of YouCompleteMe.ValidateParseRequest in # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
# combination with YouCompleteMe.OnFileReadyToParse when the completer # in combination with YouCompleteMe.OnFileReadyToParse when the completer
# raises the (special) UnknownExtraConf exception # raises the (special) UnknownExtraConf exception
FILE_NAME = 'a_file' FILE_NAME = 'a_file'
@ -217,8 +241,8 @@ class EventNotification_test( object ):
return_value = 0, return_value = 0,
new_callable = ExtendedMock ) as present_dialog: new_callable = ExtendedMock ) as present_dialog:
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ),
@ -228,7 +252,7 @@ class EventNotification_test( object ):
] ) ] )
# Subsequent calls don't re-raise the warning # Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ) PresentDialog_Confirm_Call( MESSAGE )
@ -239,8 +263,8 @@ class EventNotification_test( object ):
# But it does if a subsequent event raises again # But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ),
@ -256,8 +280,8 @@ class EventNotification_test( object ):
return_value = 1, return_value = 1,
new_callable = ExtendedMock ) as present_dialog: new_callable = ExtendedMock ) as present_dialog:
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ),
@ -267,7 +291,7 @@ class EventNotification_test( object ):
] ) ] )
# Subsequent calls don't re-raise the warning # Subsequent calls don't re-raise the warning
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ) PresentDialog_Confirm_Call( MESSAGE )
@ -278,8 +302,8 @@ class EventNotification_test( object ):
# But it does if a subsequent event raises again # But it does if a subsequent event raises again
self.server_state.OnFileReadyToParse() self.server_state.OnFileReadyToParse()
assert self.server_state.DiagnosticsForCurrentFileReady() assert self.server_state.FileParseRequestReady()
self.server_state.ValidateParseRequest() self.server_state.HandleFileParseRequest()
present_dialog.assert_has_exact_calls( [ present_dialog.assert_has_exact_calls( [
PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ),
@ -289,3 +313,92 @@ class EventNotification_test( object ):
call( FILE_NAME ), call( FILE_NAME ),
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 " "Logfile was deleted; set 'g:ycm_server_keep_logfiles' to see errors "
"in the future." ) "in the future." )
SERVER_IDLE_SUICIDE_SECONDS = 10800 # 3 hours SERVER_IDLE_SUICIDE_SECONDS = 10800 # 3 hours
DIAGNOSTIC_UI_FILETYPES = set( [ 'cpp', 'cs', 'c', 'objc', 'objcpp' ] )
class YouCompleteMe( object ): class YouCompleteMe( object ):
@ -85,6 +86,7 @@ class YouCompleteMe( object ):
self._omnicomp = OmniCompleter( user_options ) self._omnicomp = OmniCompleter( user_options )
self._latest_file_parse_request = None self._latest_file_parse_request = None
self._latest_completion_request = None self._latest_completion_request = None
self._latest_diagnostics = []
self._server_stdout = None self._server_stdout = None
self._server_stderr = None self._server_stderr = None
self._server_popen = None self._server_popen = None
@ -447,56 +449,64 @@ class YouCompleteMe( object ):
return None return None
return completion[ "extra_data" ][ "required_namespace_import" ] return completion[ "extra_data" ][ "required_namespace_import" ]
def GetErrorCount( self ): def GetErrorCount( self ):
return self._diag_interface.GetErrorCount() return self._diag_interface.GetErrorCount()
def GetWarningCount( self ): def GetWarningCount( self ):
return self._diag_interface.GetWarningCount() return self._diag_interface.GetWarningCount()
def DiagnosticsForCurrentFileReady( self ):
return bool( self._latest_file_parse_request and def DiagnosticUiSupportedForCurrentFiletype( self ):
self._latest_file_parse_request.Done() ) return any( [ x in DIAGNOSTIC_UI_FILETYPES
for x in vimsupport.CurrentFiletypes() ] )
def GetDiagnosticsFromStoredRequest( self, qflist_format = False ): def ShouldDisplayDiagnostics( self ):
if self.DiagnosticsForCurrentFileReady(): return bool( self._user_options[ 'show_diagnostics_ui' ] and
diagnostics = self._latest_file_parse_request.Response() self.DiagnosticUiSupportedForCurrentFiletype() )
# 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 def PopulateLocationListWithLatestDiagnostics( self ):
# request is created. # Do nothing if loc list is already populated by diag_interface
self._latest_file_parse_request = None if not self._user_options[ 'always_populate_location_list' ]:
if qflist_format: self._diag_interface.PopulateLocationList( self._latest_diagnostics )
return vimsupport.ConvertDiagnosticsToQfList( diagnostics ) return bool( self._latest_diagnostics )
else:
return diagnostics
return []
def UpdateDiagnosticInterface( self ): def UpdateDiagnosticInterface( self ):
if ( self.DiagnosticsForCurrentFileReady() and self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics )
self.NativeFiletypeCompletionUsable() ):
self._diag_interface.UpdateWithNewDiagnostics(
self.GetDiagnosticsFromStoredRequest() )
def ValidateParseRequest( self ): def FileParseRequestReady( self, block = False ):
if ( self.DiagnosticsForCurrentFileReady() and 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() ): self.NativeFiletypeCompletionUsable() ):
# YCM client has a hard-coded list of filetypes which are known to support if self.ShouldDisplayDiagnostics():
# diagnostics. These are found in autoload/youcompleteme.vim in self._latest_diagnostics = self._latest_file_parse_request.Response()
# s:diagnostic_ui_filetypes. 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 # For filetypes which don't support diagnostics, we just want to check
# _latest_file_parse_request for any exception or UnknownExtraConf # the _latest_file_parse_request for any exception or UnknownExtraConf
# response, to allow the server to raise configuration warnings, etc. # response, to allow the server to raise configuration warnings, etc.
# to the user. We ignore any other supplied data. # to the user. We ignore any other supplied data.
self._latest_file_parse_request.Response() 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 # 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. # request is created.
# #
# Note: it is the server's responsibility to determine the frequency of # Note: it is the server's responsibility to determine the frequency of