Auto merge of #2367 - micbou:on-buffer-unload, r=Valloric

[READY] Use deleted buffer instead of current buffer when sending BufferUnload event notification

When deleting a buffer, we use the current buffer to send the `BufferUnload` event notification request to ycmd instead of the buffer being deleted. This is an issue when the current buffer is not of the same filetype as the deleted one. In this case, three different scenarios may occur:
 - the current buffer filetype is not allowed: no request is sent to the ycmd server. The `OnBufferUnload` method from the completer corresponding to the filetype of the deleted buffer is not called. If the filetype is a C-family language, the translation unit is not destroyed. If it is TypeScript, we don't tell TSServer to close the file (not really an issue unless the file is modified elsewhere);
 - the current buffer filetype has no semantic support in ycmd: the request is sent to the ycmd server but no semantic completer is used. Same result;
 - the current buffer filetype has semantic support in ycmd: the `OnBufferUnload` method from the wrong completer is called in ycmd. LibClang and TSServer are able to cope with that and ignore the file. Same result in definitive.

The solution is obvious: build the request for the deleted buffer in this case. However, this involves more changes than expected because the code was written with the assumption that requests are always for the current buffer.

The `include_buffer_data` parameter from `BuildRequestData` was removed as it is always used with its default value.

This fix may alleviate issue https://github.com/Valloric/YouCompleteMe/issues/2232 in the sense that it now gives a reliable way to limit memory usage by deleting buffers in the case of multiple TUs.

Next step is to update PR https://github.com/Valloric/ycmd/pull/542 by removing the `unloaded_buffer` parameter from ycmd API then remove it from the `BufferUnload` request in YCM.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/valloric/youcompleteme/2367)
<!-- Reviewable:end -->
This commit is contained in:
Homu 2016-10-11 07:15:16 +09:00
commit 768aea435b
8 changed files with 366 additions and 180 deletions

View File

@ -91,7 +91,7 @@ function! youcompleteme#Enable()
autocmd BufReadPre * call s:OnBufferReadPre( expand( '<afile>:p' ) ) autocmd BufReadPre * call s:OnBufferReadPre( expand( '<afile>:p' ) )
autocmd BufRead,FileType * call s:OnBufferRead() autocmd BufRead,FileType * call s:OnBufferRead()
autocmd BufEnter * call s:OnBufferEnter() autocmd BufEnter * call s:OnBufferEnter()
autocmd BufUnload * call s:OnBufferUnload( expand( '<afile>:p' ) ) autocmd BufUnload * call s:OnBufferUnload()
autocmd CursorHold,CursorHoldI * call s:OnCursorHold() autocmd CursorHold,CursorHoldI * call s:OnCursorHold()
autocmd InsertLeave * call s:OnInsertLeave() autocmd InsertLeave * call s:OnInsertLeave()
autocmd InsertEnter * call s:OnInsertEnter() autocmd InsertEnter * call s:OnInsertEnter()
@ -323,10 +323,12 @@ function! s:TurnOffSyntasticForCFamily()
endfunction endfunction
function! s:AllowedToCompleteInCurrentBuffer() function! s:AllowedToCompleteInBuffer( buffer )
if empty( &filetype ) || let buffer_filetype = getbufvar( a:buffer, '&filetype' )
\ getbufvar( winbufnr( winnr() ), "&buftype" ) ==# 'nofile' ||
\ &filetype ==# 'qf' if empty( buffer_filetype ) ||
\ getbufvar( a:buffer, '&buftype' ) ==# 'nofile' ||
\ buffer_filetype ==# 'qf'
return 0 return 0
endif endif
@ -335,13 +337,18 @@ function! s:AllowedToCompleteInCurrentBuffer()
endif endif
let whitelist_allows = has_key( g:ycm_filetype_whitelist, '*' ) || let whitelist_allows = has_key( g:ycm_filetype_whitelist, '*' ) ||
\ has_key( g:ycm_filetype_whitelist, &filetype ) \ has_key( g:ycm_filetype_whitelist, buffer_filetype )
let blacklist_allows = !has_key( g:ycm_filetype_blacklist, &filetype ) let blacklist_allows = !has_key( g:ycm_filetype_blacklist, buffer_filetype )
return whitelist_allows && blacklist_allows return whitelist_allows && blacklist_allows
endfunction endfunction
function! s:AllowedToCompleteInCurrentBuffer()
return s:AllowedToCompleteInBuffer( '%' )
endfunction
function! s:VisitedBufferRequiresReparse() function! s:VisitedBufferRequiresReparse()
if !s:AllowedToCompleteInCurrentBuffer() if !s:AllowedToCompleteInCurrentBuffer()
return 0 return 0
@ -471,13 +478,16 @@ function! s:OnBufferEnter()
endfunction endfunction
function! s:OnBufferUnload( deleted_buffer_file ) function! s:OnBufferUnload()
if !s:AllowedToCompleteInCurrentBuffer() || empty( a:deleted_buffer_file ) " Expanding <abuf> returns the unloaded buffer number as a string but we want
" it as a true number for the getbufvar function.
if !s:AllowedToCompleteInBuffer( str2nr( expand( '<abuf>' ) ) )
return return
endif endif
let deleted_buffer_file = expand( '<afile>:p' )
exec s:python_command "ycm_state.OnBufferUnload(" exec s:python_command "ycm_state.OnBufferUnload("
\ "vim.eval( 'a:deleted_buffer_file' ) )" \ "vim.eval( 'deleted_buffer_file' ) )"
endfunction endfunction

View File

@ -154,20 +154,29 @@ class BaseRequest( object ):
hmac_secret = '' hmac_secret = ''
def BuildRequestData( include_buffer_data = True ): def BuildRequestData( filepath = None ):
"""Build request for the current buffer or the buffer corresponding to
|filepath| if specified."""
current_filepath = vimsupport.GetCurrentBufferFilepath()
if filepath and current_filepath != filepath:
# Cursor position is irrelevant when filepath is not the current buffer.
return {
'filepath': filepath,
'line_num': 1,
'column_num': 1,
'file_data': vimsupport.GetUnsavedAndSpecifiedBufferData( filepath )
}
line, column = vimsupport.CurrentLineAndColumn() line, column = vimsupport.CurrentLineAndColumn()
filepath = vimsupport.GetCurrentBufferFilepath()
request_data = { return {
'filepath': current_filepath,
'line_num': line + 1, 'line_num': line + 1,
'column_num': column + 1, 'column_num': column + 1,
'filepath': filepath 'file_data': vimsupport.GetUnsavedAndSpecifiedBufferData( current_filepath )
} }
if include_buffer_data:
request_data[ 'file_data' ] = vimsupport.GetUnsavedAndCurrentBufferData()
return request_data
def JsonFromFuture( future ): def JsonFromFuture( future ):
response = future.result() response = future.result()

View File

@ -32,15 +32,16 @@ from ycm.client.base_request import ( BaseRequest, BuildRequestData,
class EventNotification( BaseRequest ): class EventNotification( BaseRequest ):
def __init__( self, event_name, extra_data = None ): def __init__( self, event_name, filepath = None, extra_data = None ):
super( EventNotification, self ).__init__() super( EventNotification, self ).__init__()
self._event_name = event_name self._event_name = event_name
self._filepath = filepath
self._extra_data = extra_data self._extra_data = extra_data
self._cached_response = None self._cached_response = None
def Start( self ): def Start( self ):
request_data = BuildRequestData() request_data = BuildRequestData( self._filepath )
if self._extra_data: if self._extra_data:
request_data.update( self._extra_data ) request_data.update( self._extra_data )
request_data[ 'event_name' ] = self._event_name request_data[ 'event_name' ] = self._event_name
@ -74,8 +75,10 @@ class EventNotification( BaseRequest ):
return self._cached_response if self._cached_response else [] return self._cached_response if self._cached_response else []
def SendEventNotificationAsync( event_name, extra_data = None ): def SendEventNotificationAsync( event_name,
event = EventNotification( event_name, extra_data ) filepath = None,
extra_data = None ):
event = EventNotification( event_name, filepath, extra_data )
event.Start() event.Start()

View File

@ -33,9 +33,12 @@ import functools
from ycmd.utils import ToUnicode from ycmd.utils import ToUnicode
BUFNR_REGEX = re.compile( r"^bufnr\('(.+)', ([0-9]+)\)$" ) BUFNR_REGEX = re.compile( '^bufnr\(\'(?P<buffer_filename>.+)\', ([01])\)$' )
BUFWINNR_REGEX = re.compile( r"^bufwinnr\(([0-9]+)\)$" ) BUFWINNR_REGEX = re.compile( '^bufwinnr\((?P<buffer_number>[0-9]+)\)$' )
BWIPEOUT_REGEX = re.compile( r"^(?:silent! )bwipeout!? ([0-9]+)$" ) BWIPEOUT_REGEX = re.compile(
'^(?:silent! )bwipeout!? (?P<buffer_number>[0-9]+)$' )
GETBUFVAR_REGEX = re.compile(
'^getbufvar\((?P<buffer_number>[0-9]+), "&(?P<option>.+)"\)$' )
# One-and only instance of mocked Vim object. The first 'import vim' that is # One-and only instance of mocked Vim object. The first 'import vim' that is
# executed binds the vim module to the instance of MagicMock that is created, # executed binds the vim module to the instance of MagicMock that is created,
@ -49,46 +52,74 @@ VIM_MOCK = MagicMock()
def MockGetBufferNumber( buffer_filename ): def MockGetBufferNumber( buffer_filename ):
for buffer in VIM_MOCK.buffers: for vim_buffer in VIM_MOCK.buffers:
if buffer[ 'filename' ] == buffer_filename: if vim_buffer.name == buffer_filename:
return buffer[ 'number' ] return vim_buffer.number
return -1 return -1
def MockGetBufferWindowNumber( buffer_number ): def MockGetBufferWindowNumber( buffer_number ):
for buffer in VIM_MOCK.buffers: for vim_buffer in VIM_MOCK.buffers:
if buffer[ 'number' ] == buffer_number and 'window' in buffer: if vim_buffer.number == buffer_number and vim_buffer.window:
return buffer[ 'window' ] return vim_buffer.window
return -1 return -1
def MockGetBufferVariable( buffer_number, option ):
for vim_buffer in VIM_MOCK.buffers:
if vim_buffer.number == buffer_number:
if option == 'mod':
return vim_buffer.modified
if option == 'ft':
return vim_buffer.filetype
return ''
return ''
def MockVimEval( value ): def MockVimEval( value ):
if value == "g:ycm_min_num_of_chars_for_completion": if value == 'g:ycm_min_num_of_chars_for_completion':
return 0 return 0
if value == "g:ycm_server_python_interpreter":
if value == 'g:ycm_server_python_interpreter':
return '' return ''
if value == "tempname()":
if value == 'tempname()':
return '_TEMP_FILE_' return '_TEMP_FILE_'
if value == "&previewheight":
if value == '&previewheight':
# Default value from Vim # Default value from Vim
return 12 return 12
if value == '&omnifunc':
return VIM_MOCK.current.buffer.omnifunc
if value == '&filetype':
return VIM_MOCK.current.buffer.filetype
match = BUFNR_REGEX.search( value ) match = BUFNR_REGEX.search( value )
if match: if match:
return MockGetBufferNumber( match.group( 1 ) ) buffer_filename = match.group( 'buffer_filename' )
return MockGetBufferNumber( buffer_filename )
match = BUFWINNR_REGEX.search( value ) match = BUFWINNR_REGEX.search( value )
if match: if match:
return MockGetBufferWindowNumber( int( match.group( 1 ) ) ) buffer_number = int( match.group( 'buffer_number' ) )
return MockGetBufferWindowNumber( buffer_number )
raise ValueError( 'Unexpected evaluation: ' + value ) match = GETBUFVAR_REGEX.search( value )
if match:
buffer_number = int( match.group( 'buffer_number' ) )
option = match.group( 'option' )
return MockGetBufferVariable( buffer_number, option )
raise ValueError( 'Unexpected evaluation: {0}'.format( value ) )
def MockWipeoutBuffer( buffer_number ): def MockWipeoutBuffer( buffer_number ):
buffers = VIM_MOCK.buffers buffers = VIM_MOCK.buffers
for index, buffer in enumerate( buffers ): for index, buffer in enumerate( buffers ):
if buffer[ 'number' ] == buffer_number: if buffer.number == buffer_number:
return buffers.pop( index ) return buffers.pop( index )
@ -100,6 +131,50 @@ def MockVimCommand( command ):
raise RuntimeError( 'Unexpected command: ' + command ) raise RuntimeError( 'Unexpected command: ' + command )
class VimBuffer( object ):
"""An object that looks like a vim.buffer object:
- |name| : full path of the buffer;
- |number| : buffer number;
- |contents|: list of lines representing the buffer contents;
- |filetype|: buffer filetype. Empty string if no filetype is set;
- |modified|: True if the buffer has unsaved changes, False otherwise;
- |window| : number of the buffer window. None if the buffer is hidden;
- |omnifunc|: omni completion function used by the buffer."""
def __init__( self, name,
number = 1,
contents = [],
filetype = '',
modified = True,
window = None,
omnifunc = '' ):
self.name = name
self.number = number
self.contents = contents
self.filetype = filetype
self.modified = modified
self.window = window
self.omnifunc = omnifunc
def __getitem__( self, index ):
"""Return the bytes for a given line at index |index|."""
return self.contents[ index ]
def __len__( self ):
return len( self.contents )
def __setitem__( self, key, value ):
return self.contents.__setitem__( key, value )
def GetLines( self ):
"""Return the contents of the buffer as a list of unicode strings."""
return [ ToUnicode( x ) for x in self.contents ]
def MockVimModule(): def MockVimModule():
"""The 'vim' module is something that is only present when running inside the """The 'vim' module is something that is only present when running inside the
Vim Python interpreter, so we replace it with a MagicMock for tests. If you Vim Python interpreter, so we replace it with a MagicMock for tests. If you

View File

@ -23,7 +23,7 @@ from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from builtins import * # noqa from builtins import * # noqa
from ycm.test_utils import MockVimModule, ExtendedMock from ycm.test_utils import ExtendedMock, MockVimModule, VimBuffer
MockVimModule() MockVimModule()
import contextlib import contextlib
@ -33,6 +33,7 @@ from ycm.tests.server_test import Server_test
from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range, from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range,
UnknownExtraConf, ServerError ) UnknownExtraConf, ServerError )
from hamcrest import assert_that, contains, has_entries
from mock import call, MagicMock, patch from mock import call, MagicMock, patch
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
@ -59,46 +60,19 @@ def MockArbitraryBuffer( filetype, native_available = True ):
"""Used via the with statement, set up mocked versions of the vim module such """Used via the with statement, set up mocked versions of the vim module such
that a single buffer is open with an arbitrary name and arbirary contents. Its that a single buffer is open with an arbitrary name and arbirary contents. Its
filetype is set to the supplied filetype""" filetype is set to the supplied filetype"""
with patch( 'vim.current' ) as vim_current:
def VimEval( value ):
"""Local mock of the vim.eval() function, used to ensure we get the
correct behvaiour"""
if value == '&omnifunc': # Arbitrary, but valid, single buffer open.
# The omnicompleter is not required here current_buffer = VimBuffer( os.path.realpath( 'TEST_BUFFER' ),
return '' window = 1,
filetype = filetype )
if value == 'getbufvar(0, "&mod")': # The rest just mock up the Vim module so that our single arbitrary buffer
# Ensure that we actually send the even to the server # makes sense to vimsupport module.
return 1 with patch( 'vim.buffers', [ current_buffer ] ):
with patch( 'vim.current.buffer', current_buffer ):
if value == 'getbufvar(0, "&ft")' or value == '&filetype': # Arbitrary but valid cursor position.
return filetype with patch( 'vim.current.window.cursor', ( 1, 2 ) ):
yield
if value.startswith( 'bufnr(' ):
return 0
if value.startswith( 'bufwinnr(' ):
return 0
raise ValueError( 'Unexpected evaluation' )
# Arbitrary, but valid, cursor position
vim_current.window.cursor = ( 1, 2 )
# Arbitrary, but valid, single buffer open
current_buffer = MagicMock()
current_buffer.number = 0
current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
current_buffer.name = 'TEST_BUFFER'
current_buffer.window = 0
# The rest just mock up the Vim module so that our single arbitrary buffer
# makes sense to vimsupport module.
with patch( 'vim.buffers', [ current_buffer ] ):
with patch( 'vim.current.buffer', current_buffer ):
with patch( 'vim.eval', side_effect=VimEval ):
yield
@contextlib.contextmanager @contextlib.contextmanager
@ -310,7 +284,7 @@ class EventNotification_test( Server_test ):
ok_( self._server_state.FileParseRequestReady() ) ok_( self._server_state.FileParseRequestReady() )
self._server_state.HandleFileParseRequest() self._server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [ vim_command.assert_has_calls( [
PlaceSign_Call( 1, 1, 0, True ) PlaceSign_Call( 1, 1, 1, True )
] ) ] )
eq_( self._server_state.GetErrorCount(), 1 ) eq_( self._server_state.GetErrorCount(), 1 )
eq_( self._server_state.GetWarningCount(), 0 ) eq_( self._server_state.GetWarningCount(), 0 )
@ -343,8 +317,8 @@ class EventNotification_test( Server_test ):
ok_( self._server_state.FileParseRequestReady() ) ok_( self._server_state.FileParseRequestReady() )
self._server_state.HandleFileParseRequest() self._server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [ vim_command.assert_has_calls( [
PlaceSign_Call( 2, 2, 0, False ), PlaceSign_Call( 2, 2, 1, False ),
UnplaceSign_Call( 1, 0 ) UnplaceSign_Call( 1, 1 )
] ) ] )
eq_( self._server_state.GetErrorCount(), 0 ) eq_( self._server_state.GetErrorCount(), 0 )
eq_( self._server_state.GetWarningCount(), 1 ) eq_( self._server_state.GetWarningCount(), 1 )
@ -369,7 +343,114 @@ class EventNotification_test( Server_test ):
self._server_state.OnFileReadyToParse() self._server_state.OnFileReadyToParse()
self._server_state.HandleFileParseRequest() self._server_state.HandleFileParseRequest()
vim_command.assert_has_calls( [ vim_command.assert_has_calls( [
UnplaceSign_Call( 2, 0 ) UnplaceSign_Call( 2, 1 )
] ) ] )
eq_( self._server_state.GetErrorCount(), 0 ) eq_( self._server_state.GetErrorCount(), 0 )
eq_( self._server_state.GetWarningCount(), 0 ) eq_( self._server_state.GetWarningCount(), 0 )
@patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' )
@patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync',
new_callable = ExtendedMock )
def BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test(
self, post_data_to_handler_async, *args ):
current_buffer_file = os.path.realpath( 'current_buffer' )
current_buffer = VimBuffer( name = current_buffer_file,
number = 1,
contents = [ 'current_buffer_content' ],
filetype = 'some_filetype',
modified = False )
modified_buffer_file = os.path.realpath( 'modified_buffer' )
modified_buffer = VimBuffer( name = modified_buffer_file,
number = 2,
contents = [ 'modified_buffer_content' ],
filetype = 'some_filetype',
modified = True )
unmodified_buffer_file = os.path.realpath( 'unmodified_buffer' )
unmodified_buffer = VimBuffer( name = unmodified_buffer_file,
number = 3,
contents = [ 'unmodified_buffer_content' ],
filetype = 'some_filetype',
modified = False )
with patch( 'vim.buffers', [ current_buffer,
modified_buffer,
unmodified_buffer ] ):
with patch( 'vim.current.buffer', current_buffer ):
with patch( 'vim.current.window.cursor', ( 3, 5 ) ):
self._server_state.OnBufferVisit()
assert_that(
# Positional arguments passed to PostDataToHandlerAsync.
post_data_to_handler_async.call_args[ 0 ],
contains(
has_entries( {
'filepath': current_buffer_file,
'line_num': 3,
'column_num': 6,
'file_data': has_entries( {
current_buffer_file: has_entries( {
'contents': 'current_buffer_content\n',
'filetypes': [ 'some_filetype' ]
} ),
modified_buffer_file: has_entries( {
'contents': 'modified_buffer_content\n',
'filetypes': [ 'some_filetype' ]
} )
} ),
'event_name': 'BufferVisit'
} ),
'event_notification'
)
)
@patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync',
new_callable = ExtendedMock )
def BufferUnload_BuildRequestForDeletedAndUnsavedBuffers_test(
self, post_data_to_handler_async ):
current_buffer_file = os.path.realpath( 'current_buffer' )
current_buffer = VimBuffer( name = current_buffer_file,
number = 1,
contents = [ 'current_buffer_content' ],
filetype = 'some_filetype',
modified = True )
deleted_buffer_file = os.path.realpath( 'deleted_buffer' )
deleted_buffer = VimBuffer( name = deleted_buffer_file,
number = 2,
contents = [ 'deleted_buffer_content' ],
filetype = 'some_filetype',
modified = False )
with patch( 'vim.buffers', [ current_buffer, deleted_buffer ] ):
with patch( 'vim.current.buffer', current_buffer ):
self._server_state.OnBufferUnload( deleted_buffer_file )
assert_that(
# Positional arguments passed to PostDataToHandlerAsync.
post_data_to_handler_async.call_args[ 0 ],
contains(
has_entries( {
'filepath': deleted_buffer_file,
'line_num': 1,
'column_num': 1,
'file_data': has_entries( {
current_buffer_file: has_entries( {
'contents': 'current_buffer_content\n',
'filetypes': [ 'some_filetype' ]
} ),
deleted_buffer_file: has_entries( {
'contents': 'deleted_buffer_content\n',
'filetypes': [ 'some_filetype' ]
} )
} ),
'event_name': 'BufferUnload'
} ),
'event_notification'
)
)

View File

@ -25,14 +25,15 @@ from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from builtins import * # noqa from builtins import * # noqa
from ycm.test_utils import ExtendedMock, MockVimModule, MockVimCommand from ycm.test_utils import ( ExtendedMock, MockVimCommand, VimBuffer,
MockVimModule )
MockVimModule() MockVimModule()
from ycm import vimsupport from ycm import vimsupport
from nose.tools import eq_ from nose.tools import eq_
from hamcrest import assert_that, calling, raises, none, has_entry from hamcrest import assert_that, calling, raises, none, has_entry
from mock import MagicMock, call, patch from mock import MagicMock, call, patch
from ycmd.utils import ToBytes, ToUnicode from ycmd.utils import ToBytes
import os import os
import json import json
@ -705,34 +706,6 @@ def ReplaceChunksInBuffer_UnsortedChunks_test():
AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer ) AssertBuffersAreEqualAsBytes( expected_buffer, result_buffer )
class MockBuffer( object ):
"""An object that looks like a vim.buffer object, enough for ReplaceChunk to
generate a location list"""
def __init__( self, lines, name, number ):
self.lines = lines
self.name = name
self.number = number
def __getitem__( self, index ):
""" Return the bytes for a given line at index |index| """
return self.lines[ index ]
def __len__( self ):
return len( self.lines )
def __setitem__( self, key, value ):
return self.lines.__setitem__( key, value )
def GetLines( self ):
""" Return the contents of the buffer as a list of unicode strings"""
return [ ToUnicode( x ) for x in self.lines ]
@patch( 'ycm.vimsupport.VariableExists', return_value = False ) @patch( 'ycm.vimsupport.VariableExists', return_value = False )
@patch( 'ycm.vimsupport.SetFittingHeightForCurrentWindow' ) @patch( 'ycm.vimsupport.SetFittingHeightForCurrentWindow' )
@patch( 'ycm.vimsupport.GetBufferNumberForFilename', @patch( 'ycm.vimsupport.GetBufferNumberForFilename',
@ -758,11 +731,14 @@ def ReplaceChunks_SingleFile_Open_test( vim_command,
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' ) _BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
] ]
result_buffer = MockBuffer( [ result_buffer = VimBuffer(
'line1', 'single_file',
'line2', contents = [
'line3', 'line1',
], 'single_file', 1 ) 'line2',
'line3'
]
)
with patch( 'vim.buffers', [ None, result_buffer, None ] ): with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks ) vimsupport.ReplaceChunks( chunks )
@ -845,11 +821,14 @@ def ReplaceChunks_SingleFile_NotOpen_test( vim_command,
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' ) _BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
] ]
result_buffer = MockBuffer( [ result_buffer = VimBuffer(
'line1', 'single_file',
'line2', contents = [
'line3', 'line1',
], 'single_file', 1 ) 'line2',
'line3'
]
)
with patch( 'vim.buffers', [ None, result_buffer, None ] ): with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks ) vimsupport.ReplaceChunks( chunks )
@ -954,11 +933,14 @@ def ReplaceChunks_User_Declines_To_Open_File_test(
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' ) _BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
] ]
result_buffer = MockBuffer( [ result_buffer = VimBuffer(
'line1', 'single_file',
'line2', contents = [
'line3', 'line1',
], 'single_file', 1 ) 'line2',
'line3'
]
)
with patch( 'vim.buffers', [ None, result_buffer, None ] ): with patch( 'vim.buffers', [ None, result_buffer, None ] ):
vimsupport.ReplaceChunks( chunks ) vimsupport.ReplaceChunks( chunks )
@ -1031,11 +1013,14 @@ def ReplaceChunks_User_Aborts_Opening_File_test(
_BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' ) _BuildChunk( 1, 1, 2, 1, 'replacement', 'single_file' )
] ]
result_buffer = MockBuffer( [ result_buffer = VimBuffer(
'line1', 'single_file',
'line2', contents = [
'line3', 'line1',
], 'single_file', 1 ) 'line2',
'line3'
]
)
with patch( 'vim.buffers', [ None, result_buffer, None ] ): with patch( 'vim.buffers', [ None, result_buffer, None ] ):
assert_that( calling( vimsupport.ReplaceChunks ).with_args( chunks ), assert_that( calling( vimsupport.ReplaceChunks ).with_args( chunks ),
@ -1114,15 +1099,23 @@ def ReplaceChunks_MultiFile_Open_test( vim_command,
_BuildChunk( 2, 1, 2, 1, 'second_file_replacement ', '2_another_file' ), _BuildChunk( 2, 1, 2, 1, 'second_file_replacement ', '2_another_file' ),
] ]
first_file = MockBuffer( [ first_file = VimBuffer(
'line1', '1_first_file',
'line2', number = 22,
'line3', contents = [
], '1_first_file', 22 ) 'line1',
another_file = MockBuffer( [ 'line2',
'another line1', 'line3',
'ACME line2', ]
], '2_another_file', 19 ) )
another_file = VimBuffer(
'2_another_file',
number = 19,
contents = [
'another line1',
'ACME line2',
]
)
vim_buffers = [ None ] * 23 vim_buffers = [ None ] * 23
vim_buffers[ 22 ] = first_file vim_buffers[ 22 ] = first_file
@ -1222,9 +1215,10 @@ def _BuildChunk( start_line,
@patch( 'vim.eval', new_callable = ExtendedMock ) @patch( 'vim.eval', new_callable = ExtendedMock )
def AddDiagnosticSyntaxMatch_ErrorInMiddleOfLine_test( vim_eval ): def AddDiagnosticSyntaxMatch_ErrorInMiddleOfLine_test( vim_eval ):
current_buffer = MockBuffer( [ current_buffer = VimBuffer(
'Highlight this error please' 'some_file',
], 'some_file', 1 ) contents = [ 'Highlight this error please' ]
)
with patch( 'vim.current.buffer', current_buffer ): with patch( 'vim.current.buffer', current_buffer ):
vimsupport.AddDiagnosticSyntaxMatch( 1, 16, 1, 21 ) vimsupport.AddDiagnosticSyntaxMatch( 1, 16, 1, 21 )
@ -1235,9 +1229,10 @@ def AddDiagnosticSyntaxMatch_ErrorInMiddleOfLine_test( vim_eval ):
@patch( 'vim.eval', new_callable = ExtendedMock ) @patch( 'vim.eval', new_callable = ExtendedMock )
def AddDiagnosticSyntaxMatch_WarningAtEndOfLine_test( vim_eval ): def AddDiagnosticSyntaxMatch_WarningAtEndOfLine_test( vim_eval ):
current_buffer = MockBuffer( [ current_buffer = VimBuffer(
'Highlight this warning' 'some_file',
], 'some_file', 1 ) contents = [ 'Highlight this warning' ]
)
with patch( 'vim.current.buffer', current_buffer ): with patch( 'vim.current.buffer', current_buffer ):
vimsupport.AddDiagnosticSyntaxMatch( 1, 16, 1, 23, is_error = False ) vimsupport.AddDiagnosticSyntaxMatch( 1, 16, 1, 23, is_error = False )
@ -1339,31 +1334,42 @@ def CheckFilename_test():
def BufferIsVisibleForFilename_test(): def BufferIsVisibleForFilename_test():
buffers = [ vim_buffers = [
{ VimBuffer(
'number': 1, os.path.realpath( 'visible_filename' ),
'filename': os.path.realpath( 'visible_filename' ), number = 1,
'window': 1 window = 1
}, ),
{ VimBuffer(
'number': 2, os.path.realpath( 'hidden_filename' ),
'filename': os.path.realpath( 'hidden_filename' ), number = 2,
} window = None
)
] ]
with patch( 'vim.buffers', buffers ): with patch( 'vim.buffers', vim_buffers ):
eq_( vimsupport.BufferIsVisibleForFilename( 'visible_filename' ), True ) eq_( vimsupport.BufferIsVisibleForFilename( 'visible_filename' ), True )
eq_( vimsupport.BufferIsVisibleForFilename( 'hidden_filename' ), False ) eq_( vimsupport.BufferIsVisibleForFilename( 'hidden_filename' ), False )
eq_( vimsupport.BufferIsVisibleForFilename( 'another_filename' ), False ) eq_( vimsupport.BufferIsVisibleForFilename( 'another_filename' ), False )
@patch( 'ycm.vimsupport.GetBufferNumberForFilename',
side_effect = [ 2, 5, -1 ] )
@patch( 'vim.command', @patch( 'vim.command',
side_effect = MockVimCommand, side_effect = MockVimCommand,
new_callable = ExtendedMock ) new_callable = ExtendedMock )
def CloseBuffersForFilename_test( vim_command, *args ): def CloseBuffersForFilename_test( vim_command, *args ):
vimsupport.CloseBuffersForFilename( 'some_filename' ) vim_buffers = [
VimBuffer(
os.path.realpath( 'some_filename' ),
number = 2
),
VimBuffer(
os.path.realpath( 'some_filename' ),
number = 5
)
]
with patch( 'vim.buffers', vim_buffers ):
vimsupport.CloseBuffersForFilename( 'some_filename' )
vim_command.assert_has_exact_calls( [ vim_command.assert_has_exact_calls( [
call( 'silent! bwipeout! 2' ), call( 'silent! bwipeout! 2' ),
@ -1401,17 +1407,14 @@ def OpenFilename_test( vim_current, vim_command ):
] ) ] )
@patch( 'ycm.vimsupport.BufferModified', side_effect = [ True ] ) def GetUnsavedAndSpecifiedBufferData_EncodedUnicodeCharsInBuffers_test():
@patch( 'ycm.vimsupport.FiletypesForBuffer', side_effect = [ [ 'cpp' ] ] ) filepath = os.path.realpath( 'filename' )
def GetUnsavedAndCurrentBufferData_EncodedUnicodeCharsInBuffers_test( *args ): contents = [ ToBytes( u'abc' ), ToBytes( u'fДa' ) ]
mock_buffer = MagicMock() vim_buffer = VimBuffer( filepath, contents = contents )
mock_buffer.name = os.path.realpath( 'filename' )
mock_buffer.number = 1
mock_buffer.__iter__.return_value = [ ToBytes ( u'abc' ), ToBytes( u'fДa' ) ]
with patch( 'vim.buffers', [ mock_buffer ] ): with patch( 'vim.buffers', [ vim_buffer ] ):
assert_that( vimsupport.GetUnsavedAndCurrentBufferData(), assert_that( vimsupport.GetUnsavedAndSpecifiedBufferData( filepath ),
has_entry( mock_buffer.name, has_entry( filepath,
has_entry( u'contents', u'abc\nfДa\n' ) ) ) has_entry( u'contents', u'abc\nfДa\n' ) ) )

View File

@ -116,14 +116,17 @@ def BufferModified( buffer_object ):
return bool( int( GetBufferOption( buffer_object, 'mod' ) ) ) return bool( int( GetBufferOption( buffer_object, 'mod' ) ) )
def GetUnsavedAndCurrentBufferData(): def GetUnsavedAndSpecifiedBufferData( including_filepath ):
"""Build part of the request containing the contents and filetypes of all
dirty buffers as well as the buffer with filepath |including_filepath|."""
buffers_data = {} buffers_data = {}
for buffer_object in vim.buffers: for buffer_object in vim.buffers:
buffer_filepath = GetBufferFilepath( buffer_object )
if not ( BufferModified( buffer_object ) or if not ( BufferModified( buffer_object ) or
buffer_object == vim.current.buffer ): buffer_filepath == including_filepath ):
continue continue
buffers_data[ GetBufferFilepath( buffer_object ) ] = { buffers_data[ buffer_filepath ] = {
# Add a newline to match what gets saved to disk. See #1455 for details. # Add a newline to match what gets saved to disk. See #1455 for details.
'contents': JoinLinesAsUnicode( buffer_object ) + '\n', 'contents': JoinLinesAsUnicode( buffer_object ) + '\n',
'filetypes': FiletypesForBuffer( buffer_object ) 'filetypes': FiletypesForBuffer( buffer_object )

View File

@ -311,16 +311,18 @@ class YouCompleteMe( object ):
self._AddSyntaxDataIfNeeded( extra_data ) self._AddSyntaxDataIfNeeded( extra_data )
self._AddExtraConfDataIfNeeded( extra_data ) self._AddExtraConfDataIfNeeded( extra_data )
self._latest_file_parse_request = EventNotification( 'FileReadyToParse', self._latest_file_parse_request = EventNotification(
extra_data ) 'FileReadyToParse', extra_data = extra_data )
self._latest_file_parse_request.Start() self._latest_file_parse_request.Start()
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, deleted_buffer_file ):
if not self.IsServerAlive(): if not self.IsServerAlive():
return return
SendEventNotificationAsync( 'BufferUnload', SendEventNotificationAsync(
{ 'unloaded_buffer': deleted_buffer_file } ) 'BufferUnload',
filepath = deleted_buffer_file,
extra_data = { 'unloaded_buffer': deleted_buffer_file } )
def OnBufferVisit( self ): def OnBufferVisit( self ):
@ -328,7 +330,7 @@ class YouCompleteMe( object ):
return return
extra_data = {} extra_data = {}
self._AddUltiSnipsDataIfNeeded( extra_data ) self._AddUltiSnipsDataIfNeeded( extra_data )
SendEventNotificationAsync( 'BufferVisit', extra_data ) SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data )
def OnInsertLeave( self ): def OnInsertLeave( self ):