From 223ae6ab9fda819030ea0a71223a1b05b45ece66 Mon Sep 17 00:00:00 2001 From: micbou Date: Sat, 4 Feb 2017 21:46:54 +0100 Subject: [PATCH 1/2] 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 and keys. --- autoload/youcompleteme.vim | 203 +++++++++--------- python/ycm/base.py | 8 - python/ycm/client/completion_request.py | 25 ++- python/ycm/client/omni_completion_request.py | 10 +- python/ycm/omni_completer.py | 2 + .../client/omni_completion_request_tests.py | 14 +- python/ycm/tests/completion_test.py | 191 +++++++++------- python/ycm/tests/event_notification_test.py | 14 +- python/ycm/tests/test_utils.py | 3 - python/ycm/youcompleteme.py | 30 ++- 10 files changed, 264 insertions(+), 236 deletions(-) diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index de7a13c8..0d01de46 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -21,12 +21,18 @@ set cpo&vim " This needs to be called outside of a function let s:script_folder_path = escape( expand( ':p:h' ), '\' ) -let s:omnifunc_mode = 0 - -let s:old_cursor_position = [] -let s:cursor_moved = 0 +let s:force_semantic = 0 +let s:default_completion = { + \ 'start_column': -1, + \ 'candidates': [] + \ } +let s:completion = s:default_completion let s:previous_allowed_buffer_number = 0 let s:pollers = { + \ 'completion': { + \ 'id': -1, + \ 'wait_milliseconds': 10 + \ }, \ 'file_parse_response': { \ 'id': -1, \ 'wait_milliseconds': 100 @@ -100,7 +106,6 @@ function! youcompleteme#Enable() autocmd BufEnter * call s:OnBufferEnter() autocmd BufUnload * call s:OnBufferUnload() autocmd InsertLeave * call s:OnInsertLeave() - autocmd InsertEnter * call s:OnInsertEnter() autocmd VimLeave * call s:OnVimLeave() autocmd CompleteDone * call s:OnCompleteDone() augroup END @@ -119,6 +124,10 @@ function! youcompleteme#EnableCursorMovedAutocommands() autocmd CursorMoved * call s:OnCursorMovedNormalMode() autocmd TextChanged * call s:OnTextChangedNormalMode() 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 endfunction @@ -221,15 +230,22 @@ function! s:SetUpKeyMappings() imap endif - " trigger omni completion, deselects the first completion - " candidate that vim selects by default - silent! exe 'inoremap ' . invoke_key . ' ' + silent! exe 'inoremap ' . invoke_key . + \ ' =InvokeSemanticCompletion()' endif if !empty( g:ycm_key_detailed_diagnostics ) silent! exe 'nnoremap ' . g:ycm_key_detailed_diagnostics . - \ ' :YcmShowDetailedDiagnostic' + \ ' :YcmShowDetailedDiagnostic' 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 [ "", "" ] + silent! exe 'inoremap ' . key . + \ ' OnDeleteChar( "\' . key . '" )' + endfor endfunction @@ -406,6 +422,11 @@ function! s:SetUpCompleteopt() endfunction +function! s:SetCompleteFunc() + let &completefunc = 'youcompleteme#CompleteFunc' +endfunction + + function! s:OnVimLeave() exec s:python_command "ycm_state.OnVimLeave()" endfunction @@ -423,7 +444,6 @@ function! s:OnFileTypeSet() call s:SetUpCompleteopt() call s:SetCompleteFunc() - call s:SetOmnicompleteFunc() exec s:python_command "ycm_state.OnBufferVisit()" call s:OnFileReadyToParse( 1 ) @@ -437,7 +457,6 @@ function! s:OnBufferEnter() call s:SetUpCompleteopt() call s:SetCompleteFunc() - call s:SetOmnicompleteFunc() exec s:python_command "ycm_state.OnBufferVisit()" " Last parse may be outdated because of changes from other buffers. Force a @@ -502,24 +521,20 @@ function! s:PollFileParseResponse( ... ) endfunction -function! s:SetCompleteFunc() - let &completefunc = 'youcompleteme#Complete' - let &l:completefunc = 'youcompleteme#Complete' +function! s:OnInsertChar() + call timer_stop( s:pollers.completion.id ) + if pumvisible() + call feedkeys( "\", 'n' ) + endif endfunction -function! s:SetOmnicompleteFunc() - if s:Pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) - let &omnifunc = 'youcompleteme#OmniComplete' - let &l:omnifunc = 'youcompleteme#OmniComplete' - - " 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 = '' +function! s:OnDeleteChar( key ) + call timer_stop( s:pollers.completion.id ) + if pumvisible() + return "\" . a:key endif + return a:key endfunction @@ -546,23 +561,28 @@ function! s:OnTextChangedInsertMode() return endif - exec s:python_command "ycm_state.OnCursorMoved()" - call s:UpdateCursorMoved() - 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 - 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() endif - " We have to make sure we correctly leave omnifunc mode even when the user - " inserts something like a "operator[]" candidate string which fails - " CurrentIdentifierFinished check. - if s:omnifunc_mode && !s:Pyeval( 'base.LastEnteredCharIsIdentifierChar()') - let s:omnifunc_mode = 0 + exec s:python_command "ycm_state.OnCursorMoved()" + + if g:ycm_autoclose_preview_window_after_completion + call s:ClosePreviewWindowIfNeeded() endif endfunction @@ -572,7 +592,8 @@ function! s:OnInsertLeave() return endif - let s:omnifunc_mode = 0 + let s:force_semantic = 0 + let s:completion = s:default_completion call s:OnFileReadyToParse() exec s:python_command "ycm_state.OnInsertLeave()" if g:ycm_autoclose_preview_window_after_completion || @@ -582,22 +603,6 @@ function! s:OnInsertLeave() 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() let current_buffer_name = bufname('') @@ -619,7 +624,8 @@ function! s:IdentifierFinishedOperations() return endif exec s:python_command "ycm_state.OnCurrentIdentifierFinished()" - let s:omnifunc_mode = 0 + let s:force_semantic = 0 + let s:completion = s:default_completion endfunction @@ -662,28 +668,45 @@ endfunction 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 + " =, 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 endif - if s:InsideCommentOrStringAndShouldStop() || s:OnBlankLine() - return - endif + let response = s:Pyeval( 'ycm_state.GetCompletionResponse()' ) + let s:completion = { + \ '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() " invokes the user's completion function (which we have set to - " youcompleteme#Complete), and tells Vim to select the previous + " youcompleteme#CompleteFunc), and tells Vim to select the previous " completion candidate. This is necessary because by default, Vim selects the " first candidate when completion is invoked, and selecting a candidate " automatically replaces the current text with it. Calling forces Vim to @@ -693,43 +716,17 @@ function! s:InvokeCompletion() endfunction -" This is our main entry point. This is what vim calls to get completions. -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 - +function! youcompleteme#CompleteFunc( findstart, base ) if a:findstart - " InvokeCompletion has this check but we also need it here because of random - " Vim bugs and unfortunate interactions with the autocommands of other - " plugins - if !s:cursor_moved - " for vim, -2 means not found but don't trigger an error message - " see :h complete-functions + if s:completion.start_column > col( '.' ) || + \ empty( s:completion.candidates ) + " For vim, -2 means not found but don't trigger an error message. + " See :h complete-functions. return -2 endif - - 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()' ) + return s:completion.start_column - 1 endif + return s:completion.candidates endfunction diff --git a/python/ycm/base.py b/python/ycm/base.py index 257e09bb..1fb73dba 100644 --- a/python/ycm/base.py +++ b/python/ycm/base.py @@ -25,7 +25,6 @@ from builtins import * # noqa from future.utils import iteritems from ycm import vimsupport from ycmd import user_options_store -from ycmd import request_wrap from ycmd import identifier_utils YCM_VAR_PREFIX = 'ycm_' @@ -57,13 +56,6 @@ def LoadJsonDefaultsIntoVim(): vimsupport.SetVariableValue( new_key, value ) -def CompletionStartColumn(): - return ( request_wrap.CompletionStartColumn( - vimsupport.CurrentLineContents(), - vimsupport.CurrentColumn() + 1, - vimsupport.CurrentFiletypes()[ 0 ] ) - 1 ) - - def CurrentIdentifierFinished(): line, current_column = vimsupport.CurrentLineContentsAndCodepointColumn() previous_char_index = current_column - 1 diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index 76c969a0..1b530823 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -27,20 +27,18 @@ from ycm.client.base_request import ( BaseRequest, JsonFromFuture, HandleServerException, MakeServerException ) -TIMEOUT_SECONDS = 0.5 - class CompletionRequest( BaseRequest ): def __init__( self, request_data ): super( CompletionRequest, self ).__init__() self.request_data = request_data self._response_future = None + self._response = { 'completions': [], 'completion_start_column': -1 } def Start( self ): self._response_future = self.PostDataToHandlerAsync( self.request_data, - 'completions', - TIMEOUT_SECONDS ) + 'completions' ) def Done( self ): @@ -49,21 +47,26 @@ class CompletionRequest( BaseRequest ): def RawResponse( self ): if not self._response_future: - return [] - with HandleServerException( truncate = True ): - response = JsonFromFuture( self._response_future ) + return self._response - 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: with HandleServerException( truncate = True ): raise MakeServerException( e ) - return response[ 'completions' ] - return [] + return self._response def Response( self ): - return _ConvertCompletionDatasToVimDatas( self.RawResponse() ) + response = self.RawResponse() + response[ 'completions' ] = _ConvertCompletionDatasToVimDatas( + response[ 'completions' ] ) + return response def ConvertCompletionDataToVimData( completion_data ): diff --git a/python/ycm/client/omni_completion_request.py b/python/ycm/client/omni_completion_request.py index 7fc76594..35b639ca 100644 --- a/python/ycm/client/omni_completion_request.py +++ b/python/ycm/client/omni_completion_request.py @@ -40,11 +40,17 @@ class OmniCompletionRequest( CompletionRequest ): def RawResponse( self ): - return _ConvertVimDatasToCompletionDatas( self._results ) + return { + 'completions': _ConvertVimDatasToCompletionDatas( self._results ), + 'completion_start_column': self.request_data[ 'start_column' ] + } def Response( self ): - return self._results + return { + 'completions': self._results, + 'completion_start_column': self.request_data[ 'start_column' ] + } def ConvertVimDataToCompletionData( vim_data ): diff --git a/python/ycm/omni_completer.py b/python/ycm/omni_completer.py index 2587e088..1ea274b5 100644 --- a/python/ycm/omni_completer.py +++ b/python/ycm/omni_completer.py @@ -59,6 +59,8 @@ class OmniCompleter( Completer ): def ShouldUseNowInner( self, request_data ): if not self._omnifunc: return False + if request_data.get( 'force_semantic', False ): + return True return super( OmniCompleter, self ).ShouldUseNowInner( request_data ) diff --git a/python/ycm/tests/client/omni_completion_request_tests.py b/python/ycm/tests/client/omni_completion_request_tests.py index 6b699622..db2cf498 100644 --- a/python/ycm/tests/client/omni_completion_request_tests.py +++ b/python/ycm/tests/client/omni_completion_request_tests.py @@ -29,11 +29,14 @@ from hamcrest import assert_that, has_entries from ycm.client.omni_completion_request import OmniCompletionRequest -def BuildOmnicompletionRequest( results ): +def BuildOmnicompletionRequest( results, start_column = 1 ): omni_completer = MagicMock() 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() return request @@ -49,7 +52,10 @@ def Response_FromOmniCompleter_test(): results = [ { "word": "test" } ] request = BuildOmnicompletionRequest( results ) - eq_( request.Response(), results ) + eq_( request.Response(), { + 'completions': results, + 'completion_start_column': 1 + } ) def RawResponse_ConvertedFromOmniCompleter_test(): @@ -73,7 +79,7 @@ def RawResponse_ConvertedFromOmniCompleter_test(): ] request = BuildOmnicompletionRequest( vim_results ) - results = request.RawResponse() + results = request.RawResponse()[ 'completions' ] eq_( len( results ), len( expected_results ) ) for result, expected_result in zip( results, expected_results ): diff --git a/python/ycm/tests/completion_test.py b/python/ycm/tests/completion_test.py index e16de3fa..831e034b 100644 --- a/python/ycm/tests/completion_test.py +++ b/python/ycm/tests/completion_test.py @@ -28,111 +28,138 @@ from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock, MockVimModule, MockVimBuffers, VimBuffer ) MockVimModule() +import contextlib 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 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() -def CreateCompletionRequest_UnicodeWorkingDirectory_test( ycm ): +def SendCompletionRequest_UnicodeWorkingDirectory_test( ycm ): unicode_dir = PathToTestFile( 'uni¢𐍈d€' ) current_buffer = VimBuffer( PathToTestFile( 'uni¢𐍈d€', 'current_buffer' ) ) + def ServerResponse( *args ): + return { 'completions': [], 'completion_start_column': 1 } + with CurrentWorkingDirectory( unicode_dir ): with MockVimBuffers( [ current_buffer ], current_buffer ): - ycm.CreateCompletionRequest(), - - results = ycm.GetCompletions() - - assert_that( - results, - has_entries( { - 'words': empty(), - 'refresh': 'always' - } ) - ) + with MockCompletionRequest( ServerResponse ): + ycm.SendCompletionRequest() + ok_( ycm.CompletionRequestReady() ) + assert_that( + ycm.GetCompletionResponse(), + has_entries( { + 'completions': empty(), + 'completion_start_column': 1 + } ) + ) @YouCompleteMeInstance() @patch( 'ycm.client.base_request._logger', autospec = True ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) -def CreateCompletionRequest_ResponseContainingError_test( ycm, - post_vim_message, - logger ): +def SendCompletionRequest_ResponseContainingError_test( ycm, + post_vim_message, + logger ): 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 ): - ycm.CreateCompletionRequest(), - - response = { - '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 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' - } ) - ) + with MockCompletionRequest( ServerResponse ): + ycm.SendCompletionRequest() + ok_( ycm.CompletionRequestReady() ) + response = ycm.GetCompletionResponse() + logger.exception.assert_called_with( 'Error while handling server ' + 'response' ) + post_vim_message.assert_has_exact_calls( [ + call( 'Exception: message', truncate = True ) + ] ) + assert_that( + response, + has_entries( { + 'completions': contains( has_entries( { + 'word': 'insertion_text', + 'abbr': 'menu_text', + 'menu': 'extra_menu_info', + 'info': 'detailed_info\ndoc_string', + 'kind': 'k', + 'dup': 1, + 'empty': 1 + } ) ), + 'completion_start_column': 3 + } ) + ) @YouCompleteMeInstance() @patch( 'ycm.client.base_request._logger', autospec = True ) @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock ) -def CreateCompletionRequest_ErrorFromServer_test( ycm, - post_vim_message, - logger ): +def SendCompletionRequest_ErrorFromServer_test( ycm, + post_vim_message, + logger ): current_buffer = VimBuffer( 'buffer' ) with MockVimBuffers( [ current_buffer ], current_buffer ): - ycm.CreateCompletionRequest(), - - with patch( 'ycm.client.completion_request.JsonFromFuture', - side_effect = ServerError( 'Server error' ) ): - results = ycm.GetCompletions() - - logger.exception.assert_called_with( 'Error while handling server response' ) - post_vim_message.assert_has_exact_calls( [ - call( 'Server error', truncate = True ) - ] ) - assert_that( - results, - has_entries( { - 'words': empty(), - 'refresh': 'always' - } ) - ) + with MockCompletionRequest( ServerError( 'Server error' ) ): + ycm.SendCompletionRequest() + ok_( ycm.CompletionRequestReady() ) + response = ycm.GetCompletionResponse() + logger.exception.assert_called_with( 'Error while handling server ' + 'response' ) + post_vim_message.assert_has_exact_calls( [ + call( 'Server error', truncate = True ) + ] ) + assert_that( + response, + has_entries( { + 'completions': empty(), + 'completion_start_column': -1 + } ) + ) diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py index 7d53c29e..6c4bebdf 100644 --- a/python/ycm/tests/event_notification_test.py +++ b/python/ycm/tests/event_notification_test.py @@ -86,15 +86,15 @@ def MockEventNotification( response_method, native_filetype_completer = True ): 'PostDataToHandlerAsync', return_value = MagicMock( return_value=True ) ): - # We set up a fake a Response (as called by EventNotification.Response) - # which calls the supplied callback method. Generally this callback just - # raises an apropriate exception, otherwise it would have to return a mock - # future object. + # We set up a fake response (as called by EventNotification.Response) which + # calls the supplied callback method. Generally this callback just raises an + # apropriate exception, otherwise it would have to return a mock future + # object. # # 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. + # 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.event_notification.JsonFromFuture', side_effect = response_method ): diff --git a/python/ycm/tests/test_utils.py b/python/ycm/tests/test_utils.py index 7935238a..2b804f6f 100644 --- a/python/ycm/tests/test_utils.py +++ b/python/ycm/tests/test_utils.py @@ -173,9 +173,6 @@ def _MockVimEval( value ): if value == 'tempname()': return '_TEMP_FILE_' - if value == 'complete_check()': - return 0 - if value == 'tagfiles()': return [ 'tags' ] diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index db25e607..1999f465 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -277,37 +277,35 @@ class YouCompleteMe( object ): self._SetupServer() - def CreateCompletionRequest( self, force_semantic = False ): + def SendCompletionRequest( self, force_semantic = False ): request_data = BuildRequestData() + request_data[ 'force_semantic' ] = force_semantic if ( not self.NativeFiletypeCompletionAvailable() and self.CurrentFiletypeCompletionEnabled() ): wrapped_request_data = RequestWrap( request_data ) if self._omnicomp.ShouldUseNow( wrapped_request_data ): self._latest_completion_request = OmniCompletionRequest( self._omnicomp, wrapped_request_data ) - return self._latest_completion_request + self._latest_completion_request.Start() + return request_data[ 'working_dir' ] = utils.GetCurrentDirectory() self._AddExtraConfDataIfNeeded( request_data ) - if force_semantic: - request_data[ 'force_semantic' ] = True self._latest_completion_request = CompletionRequest( request_data ) - return self._latest_completion_request + self._latest_completion_request.Start() - def GetCompletions( self ): - request = self.GetCurrentCompletionRequest() - request.Start() - while not request.Done(): - try: - if vimsupport.GetBoolValue( 'complete_check()' ): - return { 'words' : [], 'refresh' : 'always' } - except KeyboardInterrupt: - return { 'words' : [], 'refresh' : 'always' } + def CompletionRequestReady( self ): + return bool( self._latest_completion_request and + self._latest_completion_request.Done() ) - 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 ): From 377e472b7e2c20168c81c56ef5b7c431a316899c Mon Sep 17 00:00:00 2001 From: micbou Date: Sat, 15 Apr 2017 15:42:23 +0200 Subject: [PATCH 2/2] Add key mappings to close completion menu --- README.md | 13 +++++++++++++ autoload/youcompleteme.vim | 25 ++++++++++++++++++++++++- doc/youcompleteme.txt | 37 +++++++++++++++++++++++++------------ plugin/youcompleteme.vim | 3 +++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 97b9b885..08ce73f9 100644 --- a/README.md +++ b/README.md @@ -2313,6 +2313,19 @@ Default: `['', '']` let g:ycm_key_list_previous_completion = ['', ''] ``` +### The `g:ycm_key_list_stop_completion` option + +This option controls the key mappings used to close the completion menu. This is +useful when the menu is blocking the view, when you need to insert the `` +character, or when you want to expand a snippet from [UltiSnips][] and navigate +through it. + +Default: `['']` + +```viml +let g:ycm_key_list_stop_completion = [''] +``` + ### The `g:ycm_key_invoke_completion` option This option controls the key mapping used to invoke the completion menu for diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 0d01de46..fd531fee 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -22,6 +22,7 @@ set cpo&vim " This needs to be called outside of a function let s:script_folder_path = escape( expand( ':p:h' ), '\' ) let s:force_semantic = 0 +let s:completion_stopped = 0 let s:default_completion = { \ 'start_column': -1, \ 'candidates': [] @@ -215,13 +216,19 @@ function! s:SetUpKeyMappings() \ ' pumvisible() ? "\" : "\' . key .'"' endfor - for key in g:ycm_key_list_previous_completion " This selects the previous candidate for shift-tab (default) exe 'inoremap ' . key . \ ' pumvisible() ? "\" : "\' . key .'"' endfor + for key in g:ycm_key_list_stop_completion + " When selecting a candidate and closing the completion menu with the + " key, the menu will automatically be reopened because of the TextChangedI + " event. We define a command to prevent that. + exe 'inoremap ' . key . ' StopCompletion( "\' . key . '" )' + endfor + if !empty( g:ycm_key_invoke_completion ) let invoke_key = g:ycm_key_invoke_completion @@ -538,6 +545,16 @@ function! s:OnDeleteChar( key ) endfunction +function! s:StopCompletion( key ) + call timer_stop( s:pollers.completion.id ) + if pumvisible() + let s:completion_stopped = 1 + return "\" + endif + return a:key +endfunction + + function! s:OnCursorMovedNormalMode() if !s:AllowedToCompleteInCurrentBuffer() return @@ -561,6 +578,12 @@ function! s:OnTextChangedInsertMode() return endif + if s:completion_stopped + let s:completion_stopped = 0 + let s:completion = s:default_completion + return + endif + call s:IdentifierFinishedOperations() " We have to make sure we correctly leave semantic mode even when the user diff --git a/doc/youcompleteme.txt b/doc/youcompleteme.txt index 21207b62..2fabf52a 100644 --- a/doc/youcompleteme.txt +++ b/doc/youcompleteme.txt @@ -115,18 +115,19 @@ Contents ~ 32. The |g:ycm_max_diagnostics_to_display| option 33. The |g:ycm_key_list_select_completion| option 34. The |g:ycm_key_list_previous_completion| option - 35. The |g:ycm_key_invoke_completion| option - 36. The |g:ycm_key_detailed_diagnostics| option - 37. The |g:ycm_global_ycm_extra_conf| option - 38. The |g:ycm_confirm_extra_conf| option - 39. The |g:ycm_extra_conf_globlist| option - 40. The |g:ycm_filepath_completion_use_working_dir| option - 41. The |g:ycm_semantic_triggers| option - 42. The |g:ycm_cache_omnifunc| option - 43. The |g:ycm_use_ultisnips_completer| option - 44. The |g:ycm_goto_buffer_command| option - 45. The |g:ycm_disable_for_files_larger_than_kb| option - 46. The |g:ycm_python_binary_path| option + 35. The |g:ycm_key_list_stop_completion| option + 36. The |g:ycm_key_invoke_completion| option + 37. The |g:ycm_key_detailed_diagnostics| option + 38. The |g:ycm_global_ycm_extra_conf| option + 39. The |g:ycm_confirm_extra_conf| option + 40. The |g:ycm_extra_conf_globlist| option + 41. The |g:ycm_filepath_completion_use_working_dir| option + 42. The |g:ycm_semantic_triggers| option + 43. The |g:ycm_cache_omnifunc| option + 44. The |g:ycm_use_ultisnips_completer| option + 45. The |g:ycm_goto_buffer_command| option + 46. The |g:ycm_disable_for_files_larger_than_kb| option + 47. The |g:ycm_python_binary_path| option 11. FAQ |youcompleteme-faq| 1. I used to be able to 'import vim' in '.ycm_extra_conf.py', but now can't |youcompleteme-i-used-to-be-able-to-import-vim-in-.ycm_extra_conf.py-but-now-cant| 2. I get 'ImportError' exceptions that mention 'PyInit_ycm_core' or 'initycm_core' |youcompleteme-i-get-importerror-exceptions-that-mention-pyinit_ycm_core-or-initycm_core| @@ -2565,6 +2566,18 @@ Default: "['', '']" let g:ycm_key_list_previous_completion = ['', ''] < ------------------------------------------------------------------------------- +The *g:ycm_key_list_stop_completion* option + +This option controls the key mappings used to close the completion menu. This +is useful when the menu is blocking the view, when you need to insert the +'' character, or when you want to expand a snippet from UltiSnips [21] and +navigate through it. + +Default: "['']" +> + let g:ycm_key_list_stop_completion = [''] +< +------------------------------------------------------------------------------- The *g:ycm_key_invoke_completion* option This option controls the key mapping used to invoke the completion menu for diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index ec890674..8ef4ccdc 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -82,6 +82,9 @@ let g:ycm_key_list_select_completion = let g:ycm_key_list_previous_completion = \ get( g:, 'ycm_key_list_previous_completion', ['', ''] ) +let g:ycm_key_list_stop_completion = + \ get( g:, 'ycm_key_list_stop_completion', [''] ) + let g:ycm_key_invoke_completion = \ get( g:, 'ycm_key_invoke_completion', '' )