Rewrite completion system

Bring fully asynchronous completion by polling for completions with a timer
then calling completefunc once the completions are ready. Use the start column
returned by the server in completefunc. Immediately display the last completion
on the TextChangedI event to prevent the popup menu disappearing while waiting
for the completions. Handle the TextChangedI event not being triggered while
the completion menu is open by closing the menu when inserting a character
through the InsertCharPre event, and when deleting a character on the <BS> and
<C-h> keys.
This commit is contained in:
micbou 2017-02-04 21:46:54 +01:00
parent 37d63e1aec
commit 223ae6ab9f
No known key found for this signature in database
GPG Key ID: C7E8FD1F3BDA1E05
10 changed files with 264 additions and 236 deletions

View File

@ -21,12 +21,18 @@ set cpo&vim
" This needs to be called outside of a function " This needs to be called outside of a function
let s:script_folder_path = escape( expand( '<sfile>:p:h' ), '\' ) let s:script_folder_path = escape( expand( '<sfile>:p:h' ), '\' )
let s:omnifunc_mode = 0 let s:force_semantic = 0
let s:default_completion = {
let s:old_cursor_position = [] \ 'start_column': -1,
let s:cursor_moved = 0 \ 'candidates': []
\ }
let s:completion = s:default_completion
let s:previous_allowed_buffer_number = 0 let s:previous_allowed_buffer_number = 0
let s:pollers = { let s:pollers = {
\ 'completion': {
\ 'id': -1,
\ 'wait_milliseconds': 10
\ },
\ 'file_parse_response': { \ 'file_parse_response': {
\ 'id': -1, \ 'id': -1,
\ 'wait_milliseconds': 100 \ 'wait_milliseconds': 100
@ -100,7 +106,6 @@ function! youcompleteme#Enable()
autocmd BufEnter * call s:OnBufferEnter() autocmd BufEnter * call s:OnBufferEnter()
autocmd BufUnload * call s:OnBufferUnload() autocmd BufUnload * call s:OnBufferUnload()
autocmd InsertLeave * call s:OnInsertLeave() autocmd InsertLeave * call s:OnInsertLeave()
autocmd InsertEnter * call s:OnInsertEnter()
autocmd VimLeave * call s:OnVimLeave() autocmd VimLeave * call s:OnVimLeave()
autocmd CompleteDone * call s:OnCompleteDone() autocmd CompleteDone * call s:OnCompleteDone()
augroup END augroup END
@ -119,6 +124,10 @@ function! youcompleteme#EnableCursorMovedAutocommands()
autocmd CursorMoved * call s:OnCursorMovedNormalMode() autocmd CursorMoved * call s:OnCursorMovedNormalMode()
autocmd TextChanged * call s:OnTextChangedNormalMode() autocmd TextChanged * call s:OnTextChangedNormalMode()
autocmd TextChangedI * call s:OnTextChangedInsertMode() autocmd TextChangedI * call s:OnTextChangedInsertMode()
" The TextChangedI event is not triggered when inserting a character while
" the completion menu is open. We handle this by closing the completion menu
" just before inserting a character.
autocmd InsertCharPre * call s:OnInsertChar()
augroup END augroup END
endfunction endfunction
@ -221,15 +230,22 @@ function! s:SetUpKeyMappings()
imap <Nul> <C-Space> imap <Nul> <C-Space>
endif endif
" <c-x><c-o> trigger omni completion, <c-p> deselects the first completion silent! exe 'inoremap <unique> <silent> ' . invoke_key .
" candidate that vim selects by default \ ' <C-R>=<SID>InvokeSemanticCompletion()<CR>'
silent! exe 'inoremap <unique> ' . invoke_key . ' <C-X><C-O><C-P>'
endif endif
if !empty( g:ycm_key_detailed_diagnostics ) if !empty( g:ycm_key_detailed_diagnostics )
silent! exe 'nnoremap <unique> ' . g:ycm_key_detailed_diagnostics . silent! exe 'nnoremap <unique> ' . g:ycm_key_detailed_diagnostics .
\ ' :YcmShowDetailedDiagnostic<cr>' \ ' :YcmShowDetailedDiagnostic<CR>'
endif endif
" The TextChangedI event is not triggered when deleting a character while the
" completion menu is open. We handle this by closing the completion menu on
" the keys that delete a character in insert mode.
for key in [ "<BS>", "<C-h>" ]
silent! exe 'inoremap <unique> <expr> ' . key .
\ ' <SID>OnDeleteChar( "\' . key . '" )'
endfor
endfunction endfunction
@ -406,6 +422,11 @@ function! s:SetUpCompleteopt()
endfunction endfunction
function! s:SetCompleteFunc()
let &completefunc = 'youcompleteme#CompleteFunc'
endfunction
function! s:OnVimLeave() function! s:OnVimLeave()
exec s:python_command "ycm_state.OnVimLeave()" exec s:python_command "ycm_state.OnVimLeave()"
endfunction endfunction
@ -423,7 +444,6 @@ function! s:OnFileTypeSet()
call s:SetUpCompleteopt() call s:SetUpCompleteopt()
call s:SetCompleteFunc() call s:SetCompleteFunc()
call s:SetOmnicompleteFunc()
exec s:python_command "ycm_state.OnBufferVisit()" exec s:python_command "ycm_state.OnBufferVisit()"
call s:OnFileReadyToParse( 1 ) call s:OnFileReadyToParse( 1 )
@ -437,7 +457,6 @@ function! s:OnBufferEnter()
call s:SetUpCompleteopt() call s:SetUpCompleteopt()
call s:SetCompleteFunc() call s:SetCompleteFunc()
call s:SetOmnicompleteFunc()
exec s:python_command "ycm_state.OnBufferVisit()" exec s:python_command "ycm_state.OnBufferVisit()"
" Last parse may be outdated because of changes from other buffers. Force a " Last parse may be outdated because of changes from other buffers. Force a
@ -502,24 +521,20 @@ function! s:PollFileParseResponse( ... )
endfunction endfunction
function! s:SetCompleteFunc() function! s:OnInsertChar()
let &completefunc = 'youcompleteme#Complete' call timer_stop( s:pollers.completion.id )
let &l:completefunc = 'youcompleteme#Complete' if pumvisible()
call feedkeys( "\<C-e>", 'n' )
endif
endfunction endfunction
function! s:SetOmnicompleteFunc() function! s:OnDeleteChar( key )
if s:Pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) call timer_stop( s:pollers.completion.id )
let &omnifunc = 'youcompleteme#OmniComplete' if pumvisible()
let &l:omnifunc = 'youcompleteme#OmniComplete' return "\<C-y>" . a:key
" If we don't have native filetype support but the omnifunc is set to YCM's
" omnifunc because the previous file the user was editing DID have native
" support, we remove our omnifunc.
elseif &omnifunc == 'youcompleteme#OmniComplete'
let &omnifunc = ''
let &l:omnifunc = ''
endif endif
return a:key
endfunction endfunction
@ -546,23 +561,28 @@ function! s:OnTextChangedInsertMode()
return return
endif endif
exec s:python_command "ycm_state.OnCursorMoved()"
call s:UpdateCursorMoved()
call s:IdentifierFinishedOperations() call s:IdentifierFinishedOperations()
if g:ycm_autoclose_preview_window_after_completion
call s:ClosePreviewWindowIfNeeded() " We have to make sure we correctly leave semantic mode even when the user
" inserts something like a "operator[]" candidate string which fails
" CurrentIdentifierFinished check.
if s:force_semantic && !s:Pyeval( 'base.LastEnteredCharIsIdentifierChar()' )
let s:force_semantic = 0
endif endif
if g:ycm_auto_trigger || s:omnifunc_mode if &completefunc == "youcompleteme#CompleteFunc" &&
\ ( g:ycm_auto_trigger || s:force_semantic ) &&
\ !s:InsideCommentOrStringAndShouldStop() &&
\ !s:OnBlankLine()
" Immediately call previous completion to avoid flickers.
call s:Complete()
call s:InvokeCompletion() call s:InvokeCompletion()
endif endif
" We have to make sure we correctly leave omnifunc mode even when the user exec s:python_command "ycm_state.OnCursorMoved()"
" inserts something like a "operator[]" candidate string which fails
" CurrentIdentifierFinished check. if g:ycm_autoclose_preview_window_after_completion
if s:omnifunc_mode && !s:Pyeval( 'base.LastEnteredCharIsIdentifierChar()') call s:ClosePreviewWindowIfNeeded()
let s:omnifunc_mode = 0
endif endif
endfunction endfunction
@ -572,7 +592,8 @@ function! s:OnInsertLeave()
return return
endif endif
let s:omnifunc_mode = 0 let s:force_semantic = 0
let s:completion = s:default_completion
call s:OnFileReadyToParse() call s:OnFileReadyToParse()
exec s:python_command "ycm_state.OnInsertLeave()" exec s:python_command "ycm_state.OnInsertLeave()"
if g:ycm_autoclose_preview_window_after_completion || if g:ycm_autoclose_preview_window_after_completion ||
@ -582,22 +603,6 @@ function! s:OnInsertLeave()
endfunction endfunction
function! s:OnInsertEnter()
if !s:AllowedToCompleteInCurrentBuffer()
return
endif
let s:old_cursor_position = []
endfunction
function! s:UpdateCursorMoved()
let current_position = getpos('.')
let s:cursor_moved = current_position != s:old_cursor_position
let s:old_cursor_position = current_position
endfunction
function! s:ClosePreviewWindowIfNeeded() function! s:ClosePreviewWindowIfNeeded()
let current_buffer_name = bufname('') let current_buffer_name = bufname('')
@ -619,7 +624,8 @@ function! s:IdentifierFinishedOperations()
return return
endif endif
exec s:python_command "ycm_state.OnCurrentIdentifierFinished()" exec s:python_command "ycm_state.OnCurrentIdentifierFinished()"
let s:omnifunc_mode = 0 let s:force_semantic = 0
let s:completion = s:default_completion
endfunction endfunction
@ -662,28 +668,45 @@ endfunction
function! s:InvokeCompletion() function! s:InvokeCompletion()
if &completefunc != "youcompleteme#Complete" exec s:python_command "ycm_state.SendCompletionRequest(" .
\ "vimsupport.GetBoolValue( 's:force_semantic' ) )"
call s:PollCompletion()
endfunction
function! s:InvokeSemanticCompletion()
let s:force_semantic = 1
exec s:python_command "ycm_state.SendCompletionRequest( True )"
call s:PollCompletion()
" Since this function is called in a mapping through the expression register
" <C-R>=, its return value is inserted (see :h c_CTRL-R_=). We don't want to
" insert anything so we return an empty string.
return ''
endfunction
function! s:PollCompletion( ... )
if !s:Pyeval( 'ycm_state.CompletionRequestReady()' )
let s:pollers.completion.id = timer_start(
\ s:pollers.completion.wait_milliseconds,
\ function( 's:PollCompletion' ) )
return return
endif endif
if s:InsideCommentOrStringAndShouldStop() || s:OnBlankLine() let response = s:Pyeval( 'ycm_state.GetCompletionResponse()' )
return let s:completion = {
endif \ 'start_column': response.completion_start_column,
\ 'candidates': response.completions
\ }
call s:Complete()
endfunction
" This is tricky. First, having 'refresh' set to 'always' in the dictionary
" that our completion function returns makes sure that our completion function
" is called on every keystroke. Second, when the sequence of characters the
" user typed produces no results in our search an infinite loop can occur. The
" problem is that our feedkeys call triggers the OnCursorMovedI event which we
" are tied to. We prevent this infinite loop from starting by making sure that
" the user has moved the cursor since the last time we provided completion
" results.
if !s:cursor_moved
return
endif
function! s:Complete()
" <c-x><c-u> invokes the user's completion function (which we have set to " <c-x><c-u> invokes the user's completion function (which we have set to
" youcompleteme#Complete), and <c-p> tells Vim to select the previous " youcompleteme#CompleteFunc), and <c-p> tells Vim to select the previous
" completion candidate. This is necessary because by default, Vim selects the " completion candidate. This is necessary because by default, Vim selects the
" first candidate when completion is invoked, and selecting a candidate " first candidate when completion is invoked, and selecting a candidate
" automatically replaces the current text with it. Calling <c-p> forces Vim to " automatically replaces the current text with it. Calling <c-p> forces Vim to
@ -693,43 +716,17 @@ function! s:InvokeCompletion()
endfunction endfunction
" This is our main entry point. This is what vim calls to get completions. function! youcompleteme#CompleteFunc( findstart, base )
function! youcompleteme#Complete( findstart, base )
" After the user types one character after the call to the omnifunc, the
" completefunc will be called because of our mapping that calls the
" completefunc on every keystroke. Therefore we need to delegate the call we
" 'stole' back to the omnifunc
if s:omnifunc_mode
return youcompleteme#OmniComplete( a:findstart, a:base )
endif
if a:findstart if a:findstart
" InvokeCompletion has this check but we also need it here because of random if s:completion.start_column > col( '.' ) ||
" Vim bugs and unfortunate interactions with the autocommands of other \ empty( s:completion.candidates )
" plugins " For vim, -2 means not found but don't trigger an error message.
if !s:cursor_moved " See :h complete-functions.
" for vim, -2 means not found but don't trigger an error message
" see :h complete-functions
return -2 return -2
endif endif
return s:completion.start_column - 1
exec s:python_command "ycm_state.CreateCompletionRequest()"
return s:Pyeval( 'base.CompletionStartColumn()' )
else
return s:Pyeval( 'ycm_state.GetCompletions()' )
endif
endfunction
function! youcompleteme#OmniComplete( findstart, base )
if a:findstart
let s:omnifunc_mode = 1
exec s:python_command "ycm_state.CreateCompletionRequest(" .
\ "force_semantic = True )"
return s:Pyeval( 'base.CompletionStartColumn()' )
else
return s:Pyeval( 'ycm_state.GetCompletions()' )
endif endif
return s:completion.candidates
endfunction endfunction

View File

@ -25,7 +25,6 @@ from builtins import * # noqa
from future.utils import iteritems from future.utils import iteritems
from ycm import vimsupport from ycm import vimsupport
from ycmd import user_options_store from ycmd import user_options_store
from ycmd import request_wrap
from ycmd import identifier_utils from ycmd import identifier_utils
YCM_VAR_PREFIX = 'ycm_' YCM_VAR_PREFIX = 'ycm_'
@ -57,13 +56,6 @@ def LoadJsonDefaultsIntoVim():
vimsupport.SetVariableValue( new_key, value ) vimsupport.SetVariableValue( new_key, value )
def CompletionStartColumn():
return ( request_wrap.CompletionStartColumn(
vimsupport.CurrentLineContents(),
vimsupport.CurrentColumn() + 1,
vimsupport.CurrentFiletypes()[ 0 ] ) - 1 )
def CurrentIdentifierFinished(): def CurrentIdentifierFinished():
line, current_column = vimsupport.CurrentLineContentsAndCodepointColumn() line, current_column = vimsupport.CurrentLineContentsAndCodepointColumn()
previous_char_index = current_column - 1 previous_char_index = current_column - 1

View File

@ -27,20 +27,18 @@ from ycm.client.base_request import ( BaseRequest, JsonFromFuture,
HandleServerException, HandleServerException,
MakeServerException ) MakeServerException )
TIMEOUT_SECONDS = 0.5
class CompletionRequest( BaseRequest ): class CompletionRequest( BaseRequest ):
def __init__( self, request_data ): def __init__( self, request_data ):
super( CompletionRequest, self ).__init__() super( CompletionRequest, self ).__init__()
self.request_data = request_data self.request_data = request_data
self._response_future = None self._response_future = None
self._response = { 'completions': [], 'completion_start_column': -1 }
def Start( self ): def Start( self ):
self._response_future = self.PostDataToHandlerAsync( self.request_data, self._response_future = self.PostDataToHandlerAsync( self.request_data,
'completions', 'completions' )
TIMEOUT_SECONDS )
def Done( self ): def Done( self ):
@ -49,21 +47,26 @@ class CompletionRequest( BaseRequest ):
def RawResponse( self ): def RawResponse( self ):
if not self._response_future: if not self._response_future:
return [] return self._response
with HandleServerException( truncate = True ):
response = JsonFromFuture( self._response_future )
errors = response[ 'errors' ] if 'errors' in response else [] with HandleServerException( truncate = True ):
self._response = JsonFromFuture( self._response_future )
# Vim may not be able to convert the 'errors' entry to its internal format
# so we remove it from the response.
errors = self._response.pop( 'errors', [] )
for e in errors: for e in errors:
with HandleServerException( truncate = True ): with HandleServerException( truncate = True ):
raise MakeServerException( e ) raise MakeServerException( e )
return response[ 'completions' ] return self._response
return []
def Response( self ): def Response( self ):
return _ConvertCompletionDatasToVimDatas( self.RawResponse() ) response = self.RawResponse()
response[ 'completions' ] = _ConvertCompletionDatasToVimDatas(
response[ 'completions' ] )
return response
def ConvertCompletionDataToVimData( completion_data ): def ConvertCompletionDataToVimData( completion_data ):

View File

@ -40,11 +40,17 @@ class OmniCompletionRequest( CompletionRequest ):
def RawResponse( self ): def RawResponse( self ):
return _ConvertVimDatasToCompletionDatas( self._results ) return {
'completions': _ConvertVimDatasToCompletionDatas( self._results ),
'completion_start_column': self.request_data[ 'start_column' ]
}
def Response( self ): def Response( self ):
return self._results return {
'completions': self._results,
'completion_start_column': self.request_data[ 'start_column' ]
}
def ConvertVimDataToCompletionData( vim_data ): def ConvertVimDataToCompletionData( vim_data ):

View File

@ -59,6 +59,8 @@ class OmniCompleter( Completer ):
def ShouldUseNowInner( self, request_data ): def ShouldUseNowInner( self, request_data ):
if not self._omnifunc: if not self._omnifunc:
return False return False
if request_data.get( 'force_semantic', False ):
return True
return super( OmniCompleter, self ).ShouldUseNowInner( request_data ) return super( OmniCompleter, self ).ShouldUseNowInner( request_data )

View File

@ -29,11 +29,14 @@ from hamcrest import assert_that, has_entries
from ycm.client.omni_completion_request import OmniCompletionRequest from ycm.client.omni_completion_request import OmniCompletionRequest
def BuildOmnicompletionRequest( results ): def BuildOmnicompletionRequest( results, start_column = 1 ):
omni_completer = MagicMock() omni_completer = MagicMock()
omni_completer.ComputeCandidates = MagicMock( return_value = results ) omni_completer.ComputeCandidates = MagicMock( return_value = results )
request = OmniCompletionRequest( omni_completer, None ) request_data = {
'start_column': start_column
}
request = OmniCompletionRequest( omni_completer, request_data )
request.Start() request.Start()
return request return request
@ -49,7 +52,10 @@ def Response_FromOmniCompleter_test():
results = [ { "word": "test" } ] results = [ { "word": "test" } ]
request = BuildOmnicompletionRequest( results ) request = BuildOmnicompletionRequest( results )
eq_( request.Response(), results ) eq_( request.Response(), {
'completions': results,
'completion_start_column': 1
} )
def RawResponse_ConvertedFromOmniCompleter_test(): def RawResponse_ConvertedFromOmniCompleter_test():
@ -73,7 +79,7 @@ def RawResponse_ConvertedFromOmniCompleter_test():
] ]
request = BuildOmnicompletionRequest( vim_results ) request = BuildOmnicompletionRequest( vim_results )
results = request.RawResponse() results = request.RawResponse()[ 'completions' ]
eq_( len( results ), len( expected_results ) ) eq_( len( results ), len( expected_results ) )
for result, expected_result in zip( results, expected_results ): for result, expected_result in zip( results, expected_results ):

View File

@ -28,111 +28,138 @@ from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock,
MockVimModule, MockVimBuffers, VimBuffer ) MockVimModule, MockVimBuffers, VimBuffer )
MockVimModule() MockVimModule()
import contextlib
from hamcrest import assert_that, contains, empty, has_entries from hamcrest import assert_that, contains, empty, has_entries
from mock import call, patch from mock import call, MagicMock, patch
from nose.tools import ok_
from ycm.tests import PathToTestFile, YouCompleteMeInstance from ycm.tests import PathToTestFile, YouCompleteMeInstance
from ycmd.responses import ServerError from ycmd.responses import ServerError
@contextlib.contextmanager
def MockCompletionRequest( response_method ):
"""Mock out the CompletionRequest, replacing the response handler
JsonFromFuture with the |response_method| parameter."""
# We don't want the event to actually be sent to the server, just have it
# return success.
with patch( 'ycm.client.completion_request.CompletionRequest.'
'PostDataToHandlerAsync',
return_value = MagicMock( return_value=True ) ):
# We set up a fake response (as called by CompletionRequest.RawResponse)
# which calls the supplied callback method.
#
# Note: JsonFromFuture is actually part of ycm.client.base_request, but we
# must patch where an object is looked up, not where it is defined.
# See https://docs.python.org/dev/library/unittest.mock.html#where-to-patch
# for details.
with patch( 'ycm.client.completion_request.JsonFromFuture',
side_effect = response_method ):
yield
@YouCompleteMeInstance() @YouCompleteMeInstance()
def CreateCompletionRequest_UnicodeWorkingDirectory_test( ycm ): def SendCompletionRequest_UnicodeWorkingDirectory_test( ycm ):
unicode_dir = PathToTestFile( 'uni¢𐍈d€' ) unicode_dir = PathToTestFile( 'uni¢𐍈d€' )
current_buffer = VimBuffer( PathToTestFile( 'uni¢𐍈d€', 'current_buffer' ) ) current_buffer = VimBuffer( PathToTestFile( 'uni¢𐍈d€', 'current_buffer' ) )
def ServerResponse( *args ):
return { 'completions': [], 'completion_start_column': 1 }
with CurrentWorkingDirectory( unicode_dir ): with CurrentWorkingDirectory( unicode_dir ):
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer ):
ycm.CreateCompletionRequest(), with MockCompletionRequest( ServerResponse ):
ycm.SendCompletionRequest()
results = ycm.GetCompletions() ok_( ycm.CompletionRequestReady() )
assert_that(
assert_that( ycm.GetCompletionResponse(),
results, has_entries( {
has_entries( { 'completions': empty(),
'words': empty(), 'completion_start_column': 1
'refresh': 'always' } )
} ) )
)
@YouCompleteMeInstance() @YouCompleteMeInstance()
@patch( 'ycm.client.base_request._logger', autospec = True ) @patch( 'ycm.client.base_request._logger', autospec = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
def CreateCompletionRequest_ResponseContainingError_test( ycm, def SendCompletionRequest_ResponseContainingError_test( ycm,
post_vim_message, post_vim_message,
logger ): logger ):
current_buffer = VimBuffer( 'buffer' ) current_buffer = VimBuffer( 'buffer' )
def ServerResponse( *args ):
return {
'completions': [ {
'insertion_text': 'insertion_text',
'menu_text': 'menu_text',
'extra_menu_info': 'extra_menu_info',
'detailed_info': 'detailed_info',
'kind': 'kind',
'extra_data': {
'doc_string': 'doc_string'
}
} ],
'completion_start_column': 3,
'errors': [ {
'exception': {
'TYPE': 'Exception'
},
'message': 'message',
'traceback': 'traceback'
} ]
}
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer ):
ycm.CreateCompletionRequest(), with MockCompletionRequest( ServerResponse ):
ycm.SendCompletionRequest()
response = { ok_( ycm.CompletionRequestReady() )
'completions': [ { response = ycm.GetCompletionResponse()
'insertion_text': 'insertion_text', logger.exception.assert_called_with( 'Error while handling server '
'menu_text': 'menu_text', 'response' )
'extra_menu_info': 'extra_menu_info', post_vim_message.assert_has_exact_calls( [
'detailed_info': 'detailed_info', call( 'Exception: message', truncate = True )
'kind': 'kind', ] )
'extra_data': { assert_that(
'doc_string': 'doc_string' response,
} has_entries( {
} ], 'completions': contains( has_entries( {
'completion_start_column': 3, 'word': 'insertion_text',
'errors': [ { 'abbr': 'menu_text',
'exception': { 'menu': 'extra_menu_info',
'TYPE': 'Exception' 'info': 'detailed_info\ndoc_string',
}, 'kind': 'k',
'message': 'message', 'dup': 1,
'traceback': 'traceback' 'empty': 1
} ] } ) ),
} 'completion_start_column': 3
} )
with patch( 'ycm.client.completion_request.JsonFromFuture', )
return_value = response ):
results = ycm.GetCompletions()
logger.exception.assert_called_with( 'Error while handling server response' )
post_vim_message.assert_has_exact_calls( [
call( 'Exception: message', truncate = True )
] )
assert_that(
results,
has_entries( {
'words': contains( has_entries( {
'word': 'insertion_text',
'abbr': 'menu_text',
'menu': 'extra_menu_info',
'info': 'detailed_info\ndoc_string',
'kind': 'k',
'dup': 1,
'empty': 1
} ) ),
'refresh': 'always'
} )
)
@YouCompleteMeInstance() @YouCompleteMeInstance()
@patch( 'ycm.client.base_request._logger', autospec = True ) @patch( 'ycm.client.base_request._logger', autospec = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
def CreateCompletionRequest_ErrorFromServer_test( ycm, def SendCompletionRequest_ErrorFromServer_test( ycm,
post_vim_message, post_vim_message,
logger ): logger ):
current_buffer = VimBuffer( 'buffer' ) current_buffer = VimBuffer( 'buffer' )
with MockVimBuffers( [ current_buffer ], current_buffer ): with MockVimBuffers( [ current_buffer ], current_buffer ):
ycm.CreateCompletionRequest(), with MockCompletionRequest( ServerError( 'Server error' ) ):
ycm.SendCompletionRequest()
with patch( 'ycm.client.completion_request.JsonFromFuture', ok_( ycm.CompletionRequestReady() )
side_effect = ServerError( 'Server error' ) ): response = ycm.GetCompletionResponse()
results = ycm.GetCompletions() logger.exception.assert_called_with( 'Error while handling server '
'response' )
logger.exception.assert_called_with( 'Error while handling server response' ) post_vim_message.assert_has_exact_calls( [
post_vim_message.assert_has_exact_calls( [ call( 'Server error', truncate = True )
call( 'Server error', truncate = True ) ] )
] ) assert_that(
assert_that( response,
results, has_entries( {
has_entries( { 'completions': empty(),
'words': empty(), 'completion_start_column': -1
'refresh': 'always' } )
} ) )
)

View File

@ -86,15 +86,15 @@ def MockEventNotification( response_method, native_filetype_completer = True ):
'PostDataToHandlerAsync', 'PostDataToHandlerAsync',
return_value = MagicMock( return_value=True ) ): return_value = MagicMock( return_value=True ) ):
# We set up a fake a Response (as called by EventNotification.Response) # We set up a fake response (as called by EventNotification.Response) which
# which calls the supplied callback method. Generally this callback just # calls the supplied callback method. Generally this callback just raises an
# raises an apropriate exception, otherwise it would have to return a mock # apropriate exception, otherwise it would have to return a mock future
# future object. # object.
# #
# Note: JsonFromFuture is actually part of ycm.client.base_request, but we # Note: JsonFromFuture is actually part of ycm.client.base_request, but we
# must patch where an object is looked up, not where it is defined. # must patch where an object is looked up, not where it is defined. See
# See https://docs.python.org/dev/library/unittest.mock.html#where-to-patch # https://docs.python.org/dev/library/unittest.mock.html#where-to-patch for
# for details. # details.
with patch( 'ycm.client.event_notification.JsonFromFuture', with patch( 'ycm.client.event_notification.JsonFromFuture',
side_effect = response_method ): side_effect = response_method ):

View File

@ -173,9 +173,6 @@ def _MockVimEval( value ):
if value == 'tempname()': if value == 'tempname()':
return '_TEMP_FILE_' return '_TEMP_FILE_'
if value == 'complete_check()':
return 0
if value == 'tagfiles()': if value == 'tagfiles()':
return [ 'tags' ] return [ 'tags' ]

View File

@ -277,37 +277,35 @@ class YouCompleteMe( object ):
self._SetupServer() self._SetupServer()
def CreateCompletionRequest( self, force_semantic = False ): def SendCompletionRequest( self, force_semantic = False ):
request_data = BuildRequestData() request_data = BuildRequestData()
request_data[ 'force_semantic' ] = force_semantic
if ( not self.NativeFiletypeCompletionAvailable() and if ( not self.NativeFiletypeCompletionAvailable() and
self.CurrentFiletypeCompletionEnabled() ): self.CurrentFiletypeCompletionEnabled() ):
wrapped_request_data = RequestWrap( request_data ) wrapped_request_data = RequestWrap( request_data )
if self._omnicomp.ShouldUseNow( wrapped_request_data ): if self._omnicomp.ShouldUseNow( wrapped_request_data ):
self._latest_completion_request = OmniCompletionRequest( self._latest_completion_request = OmniCompletionRequest(
self._omnicomp, wrapped_request_data ) self._omnicomp, wrapped_request_data )
return self._latest_completion_request self._latest_completion_request.Start()
return
request_data[ 'working_dir' ] = utils.GetCurrentDirectory() request_data[ 'working_dir' ] = utils.GetCurrentDirectory()
self._AddExtraConfDataIfNeeded( request_data ) self._AddExtraConfDataIfNeeded( request_data )
if force_semantic:
request_data[ 'force_semantic' ] = True
self._latest_completion_request = CompletionRequest( request_data ) self._latest_completion_request = CompletionRequest( request_data )
return self._latest_completion_request self._latest_completion_request.Start()
def GetCompletions( self ): def CompletionRequestReady( self ):
request = self.GetCurrentCompletionRequest() return bool( self._latest_completion_request and
request.Start() self._latest_completion_request.Done() )
while not request.Done():
try:
if vimsupport.GetBoolValue( 'complete_check()' ):
return { 'words' : [], 'refresh' : 'always' }
except KeyboardInterrupt:
return { 'words' : [], 'refresh' : 'always' }
results = base.AdjustCandidateInsertionText( request.Response() )
return { 'words' : results, 'refresh' : 'always' } def GetCompletionResponse( self ):
response = self._latest_completion_request.Response()
response[ 'completions' ] = base.AdjustCandidateInsertionText(
response[ 'completions' ] )
return response
def SendCommandRequest( self, arguments, completer ): def SendCommandRequest( self, arguments, completer ):