Add support for Java diagnostics and asynchronous messages

This implements an asynchronous message system using a long-poll request
to the server.

The server provides an endpoint /receive_messages which blocks until
either a timeout occurs or we receive a batch of asynchronous messages.
We send this request asynchronously and poll it 4 times a second to see
if we have received any messages.

The messages may either be simply for display (such as startup progress)
or diagnostics, which override the diagnostics returned by
OnFileReqdyToParse.

In the former case, we simply display the message, accepting that this
might be overwritten by any other message (indeed, requiring this), and
for the latter we fan out diagnostics to any open buffer for the file in
question.

Unfortunately, Vim has bugs related to timers when there is something
displayed (such as a "confirm" prompt or other), so we suspend
background timers when doing subcommands to avoid vim bugs. NOTE: This
requires a new version of Vim (detected by the presence of the
particular functions used).
This commit is contained in:
Ben Jackson 2017-12-21 23:23:21 +00:00
parent 4e95e5d406
commit 292de25c72
10 changed files with 1004 additions and 32 deletions

View File

@ -41,6 +41,10 @@ let s:pollers = {
\ 'server_ready': {
\ 'id': -1,
\ 'wait_milliseconds': 100
\ },
\ 'receive_messages': {
\ 'id': -1,
\ 'wait_milliseconds': 100
\ }
\ }
@ -71,6 +75,29 @@ function! s:Pyeval( eval_string )
endfunction
function! s:StartMessagePoll()
if s:pollers.receive_messages.id < 0
let s:pollers.receive_messages.id = timer_start(
\ s:pollers.receive_messages.wait_milliseconds,
\ function( 's:ReceiveMessages' ) )
endif
endfunction
function! s:ReceiveMessages( timer_id )
let poll_again = s:Pyeval( 'ycm_state.OnPeriodicTick()' )
if poll_again
let s:pollers.receive_messages.id = timer_start(
\ s:pollers.receive_messages.wait_milliseconds,
\ function( 's:ReceiveMessages' ) )
else
" Don't poll again until we open another buffer
let s:pollers.receive_messages.id = -1
endif
endfunction
function! youcompleteme#Enable()
call s:SetUpBackwardsCompatibility()
@ -451,6 +478,7 @@ function! s:OnFileTypeSet()
call s:SetUpCompleteopt()
call s:SetCompleteFunc()
call s:StartMessagePoll()
exec s:python_command "ycm_state.OnBufferVisit()"
call s:OnFileReadyToParse( 1 )
@ -464,6 +492,7 @@ function! s:OnBufferEnter()
call s:SetUpCompleteopt()
call s:SetCompleteFunc()
call s:StartMessagePoll()
exec s:python_command "ycm_state.OnBufferVisit()"
" Last parse may be outdated because of changes from other buffers. Force a
@ -801,6 +830,10 @@ endfunction
function! s:RestartServer()
exec s:python_command "ycm_state.RestartServer()"
call timer_stop( s:pollers.receive_messages.id )
let s:pollers.receive_messages.id = -1
call timer_stop( s:pollers.server_ready.id )
let s:pollers.server_ready.id = timer_start(
\ s:pollers.server_ready.wait_milliseconds,
@ -828,11 +861,11 @@ endfunction
function! s:CompleterCommand(...)
" CompleterCommand will call the OnUserCommand function of a completer.
" If the first arguments is of the form "ft=..." it can be used to specify the
" completer to use (for example "ft=cpp"). Else the native filetype completer
" of the current buffer is used. If no native filetype completer is found and
" no completer was specified this throws an error. You can use
" CompleterCommand will call the OnUserCommand function of a completer. If
" the first arguments is of the form "ft=..." it can be used to specify the
" completer to use (for example "ft=cpp"). Else the native filetype
" completer of the current buffer is used. If no native filetype completer
" is found and no completer was specified this throws an error. You can use
" "ft=ycm:ident" to select the identifier completer.
" The remaining arguments will be passed to the completer.
let arguments = copy(a:000)

View File

@ -27,17 +27,23 @@ from ycm.client.event_notification import EventNotification
from ycm.diagnostic_interface import DiagnosticInterface
DIAGNOSTIC_UI_FILETYPES = set( [ 'cpp', 'cs', 'c', 'objc', 'objcpp',
'typescript' ] )
DIAGNOSTIC_UI_ASYNC_FILETYPES = set( [ 'java' ] )
# Emulates Vim buffer
# Used to store buffer related information like diagnostics, latest parse
# request. Stores buffer change tick at the parse request moment, allowing
# to effectively determine whether reparse is needed for the buffer.
class Buffer( object ):
def __init__( self, bufnr, user_options ):
def __init__( self, bufnr, user_options, async_diags ):
self.number = bufnr
self._parse_tick = 0
self._handled_tick = 0
self._parse_request = None
self._async_diags = async_diags
self._diag_interface = DiagnosticInterface( bufnr, user_options )
@ -60,9 +66,18 @@ class Buffer( object ):
return self._parse_tick != self._ChangedTick()
def UpdateDiagnostics( self ):
self._diag_interface.UpdateWithNewDiagnostics(
self._parse_request.Response() )
def UpdateDiagnostics( self, force=False ):
if force or not self._async_diags:
self.UpdateWithNewDiagnostics( self._parse_request.Response() )
else:
# We need to call the response method, because it might throw an exception
# or require extra config confirmation, even if we don't actually use the
# diagnostics.
self._parse_request.Response()
def UpdateWithNewDiagnostics( self, diagnostics ):
self._diag_interface.UpdateWithNewDiagnostics( diagnostics )
def PopulateLocationList( self ):
@ -105,5 +120,11 @@ class BufferDict( dict ):
def __missing__( self, key ):
# Python does not allow to return assignment operation result directly
new_value = self[ key ] = Buffer( key, self._user_options )
new_value = self[ key ] = Buffer(
key,
self._user_options,
any( [ x in DIAGNOSTIC_UI_ASYNC_FILETYPES
for x in
vimsupport.GetBufferFiletypes( key ) ] ) )
return new_value

View File

@ -0,0 +1,97 @@
# Copyright (C) 2017 YouCompleteMe contributors
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# YouCompleteMe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
# Not installing aliases from python-future; it's unreliable and slow.
from builtins import * # noqa
from ycm.vimsupport import PostVimMessage
from ycm.client.base_request import ( BaseRequest, BuildRequestData,
JsonFromFuture, HandleServerException )
import logging
_logger = logging.getLogger( __name__ )
# Looooong poll
TIMEOUT_SECONDS = 60
class MessagesPoll( BaseRequest ):
def __init__( self ):
super( MessagesPoll, self ).__init__()
self._request_data = BuildRequestData()
self._response_future = None
def _SendRequest( self ):
self._response_future = self.PostDataToHandlerAsync(
self._request_data,
'receive_messages',
timeout = TIMEOUT_SECONDS )
return
def Poll( self, diagnostics_handler ):
"""This should be called regularly to check for new messages in this buffer.
Returns True if Poll should be called again in a while. Returns False when
the completer or server indicated that further polling should not be done
for the requested file."""
if self._response_future is None:
# First poll
self._SendRequest()
return True
if not self._response_future.done():
# Nothing yet...
return True
with HandleServerException( display = False ):
response = JsonFromFuture( self._response_future )
poll_again = _HandlePollResponse( response, diagnostics_handler )
if poll_again:
self._SendRequest()
return True
return False
def _HandlePollResponse( response, diagnostics_handler ):
if isinstance( response, list ):
for notification in response:
if 'message' in notification:
PostVimMessage( notification[ 'message' ],
warning = False,
truncate = True )
elif 'diagnostics' in notification:
diagnostics_handler.UpdateWithNewDiagnosticsForFile(
notification[ 'filepath' ],
notification[ 'diagnostics' ] )
elif response is False:
# Don't keep polling for this file
return False
# else any truthy response means "nothing to see here; poll again in a
# while"
# Start the next poll (only if the last poll didn't raise an exception)
return True

View File

@ -123,7 +123,8 @@ class DiagnosticInterface( object ):
def _UpdateLocationList( self ):
vimsupport.SetLocationList(
vimsupport.SetLocationListForBuffer(
self._bufnr,
vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )

View File

@ -0,0 +1,142 @@
# Copyright (C) 2017 YouCompleteMe Contributors
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# YouCompleteMe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
# Not installing aliases from python-future; it's unreliable and slow.
from builtins import * # noqa
from ycm.tests.test_utils import MockVimModule
MockVimModule()
from hamcrest import assert_that, equal_to
from mock import patch, call
from ycm.client.messages_request import _HandlePollResponse
from ycm.tests.test_utils import ExtendedMock
def HandlePollResponse_NoMessages_test():
assert_that( _HandlePollResponse( True, None ), equal_to( True ) )
# Other non-False responses mean the same thing
assert_that( _HandlePollResponse( '', None ), equal_to( True ) )
assert_that( _HandlePollResponse( 1, None ), equal_to( True ) )
assert_that( _HandlePollResponse( {}, None ), equal_to( True ) )
def HandlePollResponse_PollingNotSupported_test():
assert_that( _HandlePollResponse( False, None ), equal_to( False ) )
# 0 is not False
assert_that( _HandlePollResponse( 0, None ), equal_to( True ) )
@patch( 'ycm.client.messages_request.PostVimMessage',
new_callable = ExtendedMock )
def HandlePollResponse_SingleMessage_test( post_vim_message ):
assert_that( _HandlePollResponse( [ { 'message': 'this is a message' } ] ,
None ),
equal_to( True ) )
post_vim_message.assert_has_exact_calls( [
call( 'this is a message', warning=False, truncate=True )
] )
@patch( 'ycm.client.messages_request.PostVimMessage',
new_callable = ExtendedMock )
def HandlePollResponse_MultipleMessages_test( post_vim_message ):
assert_that( _HandlePollResponse( [ { 'message': 'this is a message' },
{ 'message': 'this is another one' } ] ,
None ),
equal_to( True ) )
post_vim_message.assert_has_exact_calls( [
call( 'this is a message', warning=False, truncate=True ),
call( 'this is another one', warning=False, truncate=True )
] )
def HandlePollResponse_SingleDiagnostic_test():
diagnostics_handler = ExtendedMock()
messages = [
{ 'filepath': 'foo', 'diagnostics': [ 'PLACEHOLDER' ] },
]
assert_that( _HandlePollResponse( messages, diagnostics_handler ),
equal_to( True ) )
diagnostics_handler.UpdateWithNewDiagnosticsForFile.assert_has_exact_calls( [
call( 'foo', [ 'PLACEHOLDER' ] )
] )
def HandlePollResponse_MultipleDiagnostics_test():
diagnostics_handler = ExtendedMock()
messages = [
{ 'filepath': 'foo', 'diagnostics': [ 'PLACEHOLDER1' ] },
{ 'filepath': 'bar', 'diagnostics': [ 'PLACEHOLDER2' ] },
{ 'filepath': 'baz', 'diagnostics': [ 'PLACEHOLDER3' ] },
{ 'filepath': 'foo', 'diagnostics': [ 'PLACEHOLDER4' ] },
]
assert_that( _HandlePollResponse( messages, diagnostics_handler ),
equal_to( True ) )
diagnostics_handler.UpdateWithNewDiagnosticsForFile.assert_has_exact_calls( [
call ( 'foo', [ 'PLACEHOLDER1' ] ),
call ( 'bar', [ 'PLACEHOLDER2' ] ),
call ( 'baz', [ 'PLACEHOLDER3' ] ),
call ( 'foo', [ 'PLACEHOLDER4' ] )
] )
@patch( 'ycm.client.messages_request.PostVimMessage',
new_callable = ExtendedMock )
def HandlePollResponse_MultipleMessagesAndDiagnostics_test( post_vim_message ):
diagnostics_handler = ExtendedMock()
messages = [
{ 'filepath': 'foo', 'diagnostics': [ 'PLACEHOLDER1' ] },
{ 'message': 'On the first day of Christmas, my VimScript gave to me' },
{ 'filepath': 'bar', 'diagnostics': [ 'PLACEHOLDER2' ] },
{ 'message': 'A test file in a Command-T' },
{ 'filepath': 'baz', 'diagnostics': [ 'PLACEHOLDER3' ] },
{ 'message': 'On the second day of Christmas, my VimScript gave to me' },
{ 'filepath': 'foo', 'diagnostics': [ 'PLACEHOLDER4' ] },
{ 'message': 'Two popup menus, and a test file in a Command-T' },
]
assert_that( _HandlePollResponse( messages, diagnostics_handler ),
equal_to( True ) )
diagnostics_handler.UpdateWithNewDiagnosticsForFile.assert_has_exact_calls( [
call ( 'foo', [ 'PLACEHOLDER1' ] ),
call ( 'bar', [ 'PLACEHOLDER2' ] ),
call ( 'baz', [ 'PLACEHOLDER3' ] ),
call ( 'foo', [ 'PLACEHOLDER4' ] )
] )
post_vim_message.assert_has_exact_calls( [
call( 'On the first day of Christmas, my VimScript gave to me',
warning=False,
truncate=True ),
call( 'A test file in a Command-T', warning=False, truncate=True ),
call( 'On the second day of Christmas, my VimScript gave to me',
warning=False,
truncate=True ),
call( 'Two popup menus, and a test file in a Command-T',
warning=False,
truncate=True ),
] )

View File

@ -0,0 +1,113 @@
# Copyright (C) 2017 YouCompleteMe contributors
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# YouCompleteMe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
# Not installing aliases from python-future; it's unreliable and slow.
from builtins import * # noqa
import mock
import requests
class FakeResponse( object ):
"""A fake version of a requests response object, just about suitable for
mocking a server response. Not usually used directly. See
MockServerResponse* methods"""
def __init__( self, response, exception ):
self._json = response
self._exception = exception
self.status_code = requests.codes.ok
self.text = not exception
def json( self ):
if self._exception:
return None
return self._json
def raise_for_status( self ):
if self._exception:
raise self._exception
class FakeFuture( object ):
"""A fake version of a future response object, just about suitable for
mocking a server response as generated by PostDataToHandlerAsync.
Not usually used directly. See MockAsyncServerResponse* methods"""
def __init__( self, done, response = None, exception = None ):
self._done = done
if not done:
self._result = None
else:
self._result = FakeResponse( response, exception )
def done( self ):
return self._done
def result( self ):
return self._result
def MockAsyncServerResponseDone( response ):
"""Return a fake future object that is complete with the supplied response
message. Suitable for mocking a response future within a client request. For
example:
server_message = {
'message': 'this message came from the server'
}
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseDone( [] ) ) as mock_future:
ycm.OnPeriodicTick() # Uses ycm._message_poll_request ...
"""
return mock.MagicMock( wraps = FakeFuture( True, response ) )
def MockAsyncServerResponseInProgress():
"""Return a fake future object that is incomplete. Suitable for mocking a
response future within a client request. For example:
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseInProgress() ):
ycm.OnPeriodicTick() # Uses ycm._message_poll_request ...
"""
return mock.MagicMock( wraps = FakeFuture( False ) )
def MockAsyncServerResponseException( exception ):
"""Return a fake future object that is complete, but raises an exception.
Suitable for mocking a response future within a client request. For example:
exception = RuntimeError( 'Check client handles exception' )
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseException( exception ) ):
ycm.OnPeriodicTick() # Uses ycm._message_poll_request ...
"""
return mock.MagicMock( wraps = FakeFuture( True, None, exception ) )
# TODO: In future, implement MockServerResponse and MockServerResponseException
# for synchronous cases when such test cases are needed.

View File

@ -39,6 +39,74 @@ import os
import json
@patch( 'vim.eval', new_callable = ExtendedMock )
def SetLocationListForBuffer_Current_test( vim_eval ):
diagnostics = [ {
'bufnr': 3,
'filename': 'some_filename',
'lnum': 5,
'col': 22,
'type': 'E',
'valid': 1
} ]
current_buffer = VimBuffer( '/test', number = 3, window = 7 )
with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 1 ) ):
vimsupport.SetLocationListForBuffer( 3, diagnostics )
# We asked for the buffer which is current, so we use winnr 0
vim_eval.assert_has_exact_calls( [
call( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
] )
@patch( 'vim.eval', new_callable = ExtendedMock, side_effect = [ 8, 1 ] )
def SetLocationListForBuffer_NotCurrent_test( vim_eval ):
diagnostics = [ {
'bufnr': 3,
'filename': 'some_filename',
'lnum': 5,
'col': 22,
'type': 'E',
'valid': 1
} ]
current_buffer = VimBuffer( '/test', number = 3, window = 7 )
other_buffer = VimBuffer( '/notcurrent', number = 1, window = 8 )
with MockVimBuffers( [ current_buffer, other_buffer ],
current_buffer,
( 1, 1 ) ):
vimsupport.SetLocationListForBuffer( 1, diagnostics )
# We asked for a buffer which is not current, so we find the window
vim_eval.assert_has_exact_calls( [
call( 'bufwinnr(1)' ), # returns 8 due to side_effect
call( 'setloclist( 8, {0} )'.format( json.dumps( diagnostics ) ) )
] )
@patch( 'vim.eval', new_callable = ExtendedMock, side_effect = [ -1, 1 ] )
def SetLocationListForBuffer_NotVisible_test( vim_eval ):
diagnostics = [ {
'bufnr': 3,
'filename': 'some_filename',
'lnum': 5,
'col': 22,
'type': 'E',
'valid': 1
} ]
current_buffer = VimBuffer( '/test', number = 3, window = 7 )
other_buffer = VimBuffer( '/notcurrent', number = 1, window = 8 )
with MockVimBuffers( [ current_buffer, other_buffer ],
current_buffer,
( 1, 1 ) ):
vimsupport.SetLocationListForBuffer( 1, diagnostics )
# We asked for a buffer which is not current, so we find the window
vim_eval.assert_has_exact_calls( [
call( 'bufwinnr(1)' ), # returns -1 due to side_effect
call( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
] )
@patch( 'vim.eval', new_callable = ExtendedMock )
def SetLocationList_test( vim_eval ):
diagnostics = [ {
@ -49,9 +117,36 @@ def SetLocationList_test( vim_eval ):
'type': 'E',
'valid': 1
} ]
vimsupport.SetLocationList( diagnostics )
vim_eval.assert_called_once_with(
'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
current_buffer = VimBuffer( '/test', number = 3, window = 7 )
with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 1 ) ):
vimsupport.SetLocationList( diagnostics )
vim_eval.assert_has_calls( [
call( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) ),
] )
@patch( 'vim.eval', new_callable = ExtendedMock )
def SetLocationList_NotCurrent_test( vim_eval ):
diagnostics = [ {
'bufnr': 3,
'filename': 'some_filename',
'lnum': 5,
'col': 22,
'type': 'E',
'valid': 1
} ]
current_buffer = VimBuffer( '/test', number = 3, window = 7 )
other_buffer = VimBuffer( '/notcurrent', number = 1, window = 8 )
with MockVimBuffers( [ current_buffer, other_buffer ],
current_buffer,
( 1, 1 ) ):
vimsupport.SetLocationList( diagnostics )
# This version does not check the current buffer and just sets the current win
vim_eval.assert_has_exact_calls( [
call( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) ),
] )
@patch( 'ycm.vimsupport.VariableExists', return_value = True )

View File

@ -38,6 +38,12 @@ from ycm.tests import ( MakeUserOptions, StopServer, test_utils,
WaitUntilReady, YouCompleteMeInstance )
from ycm.client.base_request import _LoadExtraConfFile
from ycmd.responses import ServerError
from ycm.tests.mock_utils import ( MockAsyncServerResponseDone,
MockAsyncServerResponseInProgress,
MockAsyncServerResponseException )
from ycm import buffer as ycm_buffer_module
@YouCompleteMeInstance()
@ -394,11 +400,11 @@ def YouCompleteMe_ShowDiagnostics_FiletypeNotSupported_test( ycm,
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationListForWindow', new_callable = ExtendedMock )
def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
ycm, set_location_list, post_vim_message, *args ):
ycm, set_location_list_for_window, post_vim_message, *args ):
current_buffer = VimBuffer( 'buffer', filetype = 'cpp' )
current_buffer = VimBuffer( 'buffer', filetype = 'cpp', window = 99 )
with MockVimBuffers( [ current_buffer ], current_buffer ):
with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = {} ):
@ -410,7 +416,7 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
call( 'Diagnostics refreshed', warning = False ),
call( 'No warnings or errors detected.', warning = False )
] )
set_location_list.assert_called_once_with( [] )
set_location_list_for_window.assert_called_once_with( 0, [] )
@YouCompleteMeInstance( { 'log_level': 'debug',
@ -419,9 +425,9 @@ def YouCompleteMe_ShowDiagnostics_NoDiagnosticsDetected_test(
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationListForWindow', new_callable = ExtendedMock )
def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
ycm, set_location_list, post_vim_message, *args ):
ycm, set_location_list_for_window, post_vim_message, *args ):
diagnostic = {
'kind': 'ERROR',
@ -433,7 +439,10 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
}
}
current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
current_buffer = VimBuffer( 'buffer',
filetype = 'cpp',
number = 3,
window = 99 )
with MockVimBuffers( [ current_buffer ], current_buffer ):
with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = [ diagnostic ] ):
@ -444,7 +453,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
warning = False ),
call( 'Diagnostics refreshed', warning = False )
] )
set_location_list.assert_called_once_with( [ {
set_location_list_for_window.assert_called_once_with( 0, [ {
'bufnr': 3,
'lnum': 19,
'col': 2,
@ -458,10 +467,14 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_DoNotOpenLocationList_test(
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationList', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.SetLocationListForWindow', new_callable = ExtendedMock )
@patch( 'ycm.vimsupport.OpenLocationList', new_callable = ExtendedMock )
def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
ycm, open_location_list, set_location_list, post_vim_message, *args ):
ycm,
open_location_list,
set_location_list_for_window,
post_vim_message,
*args ):
diagnostic = {
'kind': 'ERROR',
@ -473,7 +486,10 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
}
}
current_buffer = VimBuffer( 'buffer', filetype = 'cpp', number = 3 )
current_buffer = VimBuffer( 'buffer',
filetype = 'cpp',
number = 3,
window = 99 )
with MockVimBuffers( [ current_buffer ], current_buffer ):
with patch( 'ycm.client.event_notification.EventNotification.Response',
return_value = [ diagnostic ] ):
@ -484,7 +500,7 @@ def YouCompleteMe_ShowDiagnostics_DiagnosticsFound_OpenLocationList_test(
warning = False ),
call( 'Diagnostics refreshed', warning = False )
] )
set_location_list.assert_called_once_with( [ {
set_location_list_for_window.assert_called_once_with( 0, [ {
'bufnr': 3,
'lnum': 19,
'col': 2,
@ -641,3 +657,361 @@ def YouCompleteMe_UpdateDiagnosticInterface_PrioritizeErrorsOverWarnings_test(
call( 'sign place 2 name=YcmWarning line=3 buffer=5' ),
call( 'try | exec "sign unplace 1 buffer=5" | catch /E158/ | endtry' )
] )
@YouCompleteMeInstance( { 'echo_current_diagnostic': 1,
'always_populate_location_list': 1 } )
@patch.object( ycm_buffer_module,
'DIAGNOSTIC_UI_ASYNC_FILETYPES',
[ 'ycmtest' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
def YouCompleteMe_AsyncDiagnosticUpdate_SingleFile_test( ycm,
post_vim_message,
*args ):
# This test simulates asynchronous diagnostic updates associated with a single
# file (e.g. Translation Unit), but where the actual errors refer to other
# open files and other non-open files. This is not strictly invalid, nor is it
# completely normal, but it is supported and does work.
# Contrast with the next test which sends the diagnostics filewise, which is
# what the language server protocol will do.
diagnostics = [
{
'kind': 'ERROR',
'text': 'error text in current buffer',
'location': {
'filepath': '/current',
'line_num': 1,
'column_num': 1
},
},
{
'kind': 'ERROR',
'text': 'error text in hidden buffer',
'location': {
'filepath': '/has_diags',
'line_num': 4,
'column_num': 2
},
},
{
'kind': 'ERROR',
'text': 'error text in buffer not open in Vim',
'location': {
'filepath': '/not_open',
'line_num': 8,
'column_num': 4
},
},
]
current_buffer = VimBuffer( '/current',
filetype = 'ycmtest',
number = 1,
window = 10 )
buffers = [
current_buffer,
VimBuffer( '/no_diags',
filetype = 'ycmtest',
number = 2,
window = 9 ),
VimBuffer( '/has_diags',
filetype = 'ycmtest',
number = 3,
window = 8 ),
]
# Register each buffer internally with YCM
for current in buffers:
with MockVimBuffers( buffers, current, ( 1, 1 ) ):
ycm.OnFileReadyToParse()
with patch( 'ycm.vimsupport.SetLocationListForWindow',
new_callable = ExtendedMock ) as set_location_list_for_window:
with MockVimBuffers( buffers, current_buffer, ( 1, 1 ) ):
ycm.UpdateWithNewDiagnosticsForFile( '/current', diagnostics )
# We update the diagnostic on the current cursor position
post_vim_message.assert_has_exact_calls( [
call( "error text in current buffer", truncate = True, warning = False ),
] )
# Ensure we included all the diags though
set_location_list_for_window.assert_has_exact_calls( [
call( 0, [
{
'lnum': 1,
'col': 1,
'bufnr': 1,
'valid': 1,
'type': 'E',
'text': 'error text in current buffer',
},
{
'lnum': 4,
'col': 2,
'bufnr': 3,
'valid': 1,
'type': 'E',
'text': 'error text in hidden buffer',
},
{
'lnum': 8,
'col': 4,
'bufnr': -1, # sic: Our mocked bufnr function actually returns -1,
# even though YCM is passing "create if needed".
# FIXME? we shouldn't do that, and we should pass
# filename instead
'valid': 1,
'type': 'E',
'text': 'error text in buffer not open in Vim'
}
] )
] )
@YouCompleteMeInstance( { 'echo_current_diagnostic': 1,
'always_populate_location_list': 1 } )
@patch.object( ycm_buffer_module,
'DIAGNOSTIC_UI_ASYNC_FILETYPES',
[ 'ycmtest' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
def YouCompleteMe_AsyncDiagnosticUpdate_PerFile_test( ycm,
post_vim_message,
*args ):
# This test simulates asynchronous diagnostic updates which are delivered per
# file, including files which are open and files which are not.
# Ordered to ensure that the calls to update are deterministic
diagnostics_per_file = [
( '/current', [ {
'kind': 'ERROR',
'text': 'error text in current buffer',
'location': {
'filepath': '/current',
'line_num': 1,
'column_num': 1
}, }, ] ),
( '/has_diags', [ {
'kind': 'ERROR',
'text': 'error text in hidden buffer',
'location': {
'filepath': '/has_diags',
'line_num': 4,
'column_num': 2
}, }, ] ),
( '/not_open', [ {
'kind': 'ERROR',
'text': 'error text in buffer not open in Vim',
'location': {
'filepath': '/not_open',
'line_num': 8,
'column_num': 4
}, }, ] )
]
current_buffer = VimBuffer( '/current',
filetype = 'ycmtest',
number = 1,
window = 10 )
buffers = [
current_buffer,
VimBuffer( '/no_diags',
filetype = 'ycmtest',
number = 2,
window = 9 ),
VimBuffer( '/has_diags',
filetype = 'ycmtest',
number = 3,
window = 8 ),
]
# Register each buffer internally with YCM
for current in buffers:
with MockVimBuffers( buffers, current, ( 1, 1 ) ):
ycm.OnFileReadyToParse()
with patch( 'ycm.vimsupport.SetLocationListForWindow',
new_callable = ExtendedMock ) as set_location_list_for_window:
with MockVimBuffers( buffers, current_buffer, ( 1, 1 ) ):
for filename, diagnostics in diagnostics_per_file:
ycm.UpdateWithNewDiagnosticsForFile( filename, diagnostics )
# We update the diagnostic on the current cursor position
post_vim_message.assert_has_exact_calls( [
call( "error text in current buffer", truncate = True, warning = False ),
] )
# Ensure we included all the diags though
set_location_list_for_window.assert_has_exact_calls( [
call( 0, [
{
'lnum': 1,
'col': 1,
'bufnr': 1,
'valid': 1,
'type': 'E',
'text': 'error text in current buffer',
},
] ),
call( 8, [
{
'lnum': 4,
'col': 2,
'bufnr': 3,
'valid': 1,
'type': 'E',
'text': 'error text in hidden buffer',
},
] )
] )
@YouCompleteMeInstance()
def YouCompleteMe_OnPeriodicTick_ServerNotRunning_test( ycm, *args ):
with patch.object( ycm, 'IsServerAlive', return_value = False ):
assert_that( ycm.OnPeriodicTick(), equal_to( False ) )
@YouCompleteMeInstance()
def YouCompleteMe_OnPeriodicTick_ServerNotReady_test( ycm, *args ):
with patch.object( ycm, 'IsServerAlive', return_value = True ):
with patch.object( ycm, 'IsServerReady', return_value = False ):
assert_that( ycm.OnPeriodicTick(), equal_to( True ) )
@YouCompleteMeInstance()
@patch.object( ycm_buffer_module,
'DIAGNOSTIC_UI_ASYNC_FILETYPES',
[ 'ycmtest' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.client.base_request._ValidateResponseObject', return_value = True )
@patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync' )
def YouCompelteMe_OnPeriodicTick_DontRetry_test( ycm,
post_data_to_handler_async,
*args ):
current_buffer = VimBuffer( '/current',
filetype = 'ycmtest',
number = 1,
window = 10 )
buffers = [ current_buffer ]
# Create the request and make the first poll; we expect no response
with MockVimBuffers( buffers, current_buffer, ( 1, 1 ) ):
assert_that( ycm.OnPeriodicTick(), equal_to( True ) )
post_data_to_handler_async.assert_called()
assert ycm._message_poll_request is not None
post_data_to_handler_async.reset_mock()
# OK that sent the request, now poll to check if it is complete (say it is
# not)
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseInProgress() ) as mock_future:
poll_again = ycm.OnPeriodicTick()
mock_future.done.assert_called()
mock_future.result.assert_not_called()
assert_that( poll_again, equal_to( True ) )
# Poll again, but return a response (telling us to stop polling)
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseDone( False ) ) \
as mock_future:
poll_again = ycm.OnPeriodicTick()
mock_future.done.assert_called()
mock_future.result.assert_called()
post_data_to_handler_async.assert_not_called()
# We reset and don't poll anymore
assert_that( ycm._message_poll_request is None )
assert_that( poll_again, equal_to( False ) )
@YouCompleteMeInstance()
@patch.object( ycm_buffer_module,
'DIAGNOSTIC_UI_ASYNC_FILETYPES',
[ 'ycmtest' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.client.base_request._ValidateResponseObject', return_value = True )
@patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync' )
def YouCompelteMe_OnPeriodicTick_Exception_test( ycm,
post_data_to_handler_async,
*args ):
current_buffer = VimBuffer( '/current',
filetype = 'ycmtest',
number = 1,
window = 10 )
buffers = [ current_buffer ]
# Create the request and make the first poll; we expect no response
with MockVimBuffers( buffers, current_buffer, ( 1, 1 ) ):
assert_that( ycm.OnPeriodicTick(), equal_to( True ) )
post_data_to_handler_async.assert_called()
post_data_to_handler_async.reset_mock()
# Poll again, but return an exception response
mock_response = MockAsyncServerResponseException( RuntimeError( 'test' ) )
with patch.object( ycm._message_poll_request,
'_response_future',
new = mock_response ) as mock_future:
assert_that( ycm.OnPeriodicTick(), equal_to( False ) )
mock_future.done.assert_called()
mock_future.result.assert_called()
post_data_to_handler_async.assert_not_called()
# We reset and don't poll anymore
assert_that( ycm._message_poll_request is None )
@YouCompleteMeInstance()
@patch.object( ycm_buffer_module,
'DIAGNOSTIC_UI_ASYNC_FILETYPES',
[ 'ycmtest' ] )
@patch( 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
return_value = True )
@patch( 'ycm.client.base_request._ValidateResponseObject', return_value = True )
@patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync' )
@patch( 'ycm.client.messages_request._HandlePollResponse' )
def YouCompelteMe_OnPeriodicTick_ValidResponse_test( ycm,
handle_poll_response,
post_data_to_handler_async,
*args ):
current_buffer = VimBuffer( '/current',
filetype = 'ycmtest',
number = 1,
window = 10 )
buffers = [ current_buffer ]
# Create the request and make the first poll; we expect no response
with MockVimBuffers( buffers, current_buffer, ( 1, 1 ) ):
assert_that( ycm.OnPeriodicTick(), equal_to( True ) )
post_data_to_handler_async.assert_called()
post_data_to_handler_async.reset_mock()
# Poll again, and return a _proper_ response (finally!).
# Note, _HandlePollResponse is tested independently (for simplicity)
with patch.object( ycm._message_poll_request,
'_response_future',
new = MockAsyncServerResponseDone( [] ) ) as mock_future:
assert_that( ycm.OnPeriodicTick(), equal_to( True ) )
handle_poll_response.assert_called()
mock_future.done.assert_called()
mock_future.result.assert_called()
post_data_to_handler_async.assert_called() # Poll again!
assert_that( ycm._message_poll_request is not None )

View File

@ -235,9 +235,47 @@ def LineAndColumnNumbersClamped( line_num, column_num ):
def SetLocationList( diagnostics ):
"""Set the location list for the current window to the supplied diagnostics"""
SetLocationListForWindow( 0, diagnostics )
def GetWindowNumberForBufferDiagnostics( buffer_number ):
"""Return an appropriate window number to use for displaying diagnostics
associated with the buffer number supplied. Always returns a valid window
number or 0 meaning the current window."""
# Location lists are associated with _windows_ not _buffers_. This makes a lot
# of sense, but YCM associates diagnostics with _buffers_, because it is the
# buffer that actually gets parsed.
#
# The heuristic we use is to determine any open window for a specified buffer,
# and set that. If there is no such window on the current tab page, we use the
# current window (by passing 0 as the window number)
if buffer_number == vim.current.buffer.number:
return 0
window_number = GetIntValue( "bufwinnr({0})".format( buffer_number ) )
if window_number < 0:
return 0
return window_number
def SetLocationListForBuffer( buffer_number, diagnostics ):
"""Populate the location list of an apppropriate window for the supplied
buffer number. See SetLocationListForWindow for format of diagnostics."""
return SetLocationListForWindow(
GetWindowNumberForBufferDiagnostics( buffer_number ),
diagnostics )
def SetLocationListForWindow( window_number, diagnostics ):
"""Populate the location list with diagnostics. Diagnostics should be in
qflist format; see ":h setqflist" for details."""
vim.eval( 'setloclist( 0, {0} )'.format( json.dumps( diagnostics ) ) )
vim.eval( 'setloclist( {0}, {1} )'.format( window_number,
json.dumps( diagnostics ) ) )
def OpenLocationList( focus = False, autoclose = False ):

View File

@ -32,7 +32,9 @@ import vim
from subprocess import PIPE
from tempfile import NamedTemporaryFile
from ycm import base, paths, vimsupport
from ycm.buffer import BufferDict
from ycm.buffer import ( BufferDict,
DIAGNOSTIC_UI_FILETYPES,
DIAGNOSTIC_UI_ASYNC_FILETYPES )
from ycmd import utils
from ycmd import server_utils
from ycmd.request_wrap import RequestWrap
@ -50,6 +52,7 @@ from ycm.client.debug_info_request import ( SendDebugInfoRequest,
from ycm.client.omni_completion_request import OmniCompletionRequest
from ycm.client.event_notification import SendEventNotificationAsync
from ycm.client.shutdown_request import SendShutdownRequest
from ycm.client.messages_request import MessagesPoll
def PatchNoProxy():
@ -97,8 +100,6 @@ CORE_OUTDATED_MESSAGE = (
'YCM core library too old; PLEASE RECOMPILE by running the install.py '
'script. See the documentation for more details.' )
SERVER_IDLE_SUICIDE_SECONDS = 1800 # 30 minutes
DIAGNOSTIC_UI_FILETYPES = set( [ 'cpp', 'cs', 'c', 'objc', 'objcpp',
'typescript' ] )
CLIENT_LOGFILE_FORMAT = 'ycm_'
SERVER_LOGFILE_FORMAT = 'ycmd_{port}_{std}_'
@ -136,6 +137,7 @@ class YouCompleteMe( object ):
self._user_notified_about_crash = False
self._filetypes_with_keywords_loaded = set()
self._server_is_ready_with_cache = False
self._message_poll_request = None
hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
options_dict = dict( self._user_options )
@ -364,6 +366,59 @@ class YouCompleteMe( object ):
return self.CurrentBuffer().NeedsReparse()
def UpdateWithNewDiagnosticsForFile( self, filepath, diagnostics ):
bufnr = vimsupport.GetBufferNumberForFilename( filepath )
if bufnr in self._buffers and vimsupport.BufferIsVisible( bufnr ):
# Note: We only update location lists, etc. for visible buffers, because
# otherwise we defualt to using the curren location list and the results
# are that non-visible buffer errors clobber visible ones.
self._buffers[ bufnr ].UpdateWithNewDiagnostics( diagnostics )
else:
# The project contains errors in file "filepath", but that file is not
# open in any buffer. This happens for Language Server Protocol-based
# completers, as they return diagnostics for the entire "project"
# asynchronously (rather than per-file in the response to the parse
# request).
#
# There are a number of possible approaches for
# this, but for now we simply ignore them. Other options include:
# - Use the QuickFix list to report project errors?
# - Use a special buffer for project errors
# - Put them in the location list of whatever the "current" buffer is
# - Store them in case the buffer is opened later
# - add a :YcmProjectDiags command
# - Add them to errror/warning _counts_ but not any actual location list
# or other
# - etc.
#
# However, none of those options are great, and lead to their own
# complexities. So for now, we just ignore these diagnostics for files not
# open in any buffer.
pass
def OnPeriodicTick( self ):
if not self.IsServerAlive():
# Server has died. We'll reset when the server is started again.
return False
elif not self.IsServerReady():
# Try again in a jiffy
return True
if not self._message_poll_request:
self._message_poll_request = MessagesPoll()
if not self._message_poll_request.Poll( self ):
# Don't poll again until some event which might change the server's mind
# about whether to provide messages for the current buffer (e.g. buffer
# visit, file ready to parse, etc.)
self._message_poll_request = None
return False
# Poll again in a jiffy
return True
def OnFileReadyToParse( self ):
if not self.IsServerAlive():
self.NotifyUserIfServerCrashed()
@ -534,7 +589,8 @@ class YouCompleteMe( object ):
def DiagnosticUiSupportedForCurrentFiletype( self ):
return any( [ x in DIAGNOSTIC_UI_FILETYPES
return any( [ x in DIAGNOSTIC_UI_FILETYPES or
x in DIAGNOSTIC_UI_ASYNC_FILETYPES
for x in vimsupport.CurrentFiletypes() ] )
@ -566,7 +622,9 @@ class YouCompleteMe( object ):
self.NativeFiletypeCompletionUsable() ):
if self.ShouldDisplayDiagnostics():
current_buffer.UpdateDiagnostics()
# Forcefuly update the location list, etc. from the parse request when
# doing something like :YcmDiags
current_buffer.UpdateDiagnostics( block is True )
else:
# YCM client has a hard-coded list of filetypes which are known
# to support diagnostics, self.DiagnosticUiSupportedForCurrentFiletype()