Auto merge of #1905 - davits:easy_handler, r=Valloric

[READY] Simplify parse request handling

Moved file parse request handling into python, trying to simplify the overall flow, and allow easier additions of additional post handlers such as semantic token extraction.

<!-- Reviewable:start -->
[<img src="https://reviewable.io/review_button.png" height=40 alt="Review on Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/1905)
<!-- Reviewable:end -->
This commit is contained in:
Homu 2016-02-22 03:56:58 +09:00
commit 7b8b1acb5e
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