diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index 6311d308..55be6e9d 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -69,6 +69,8 @@ class CommandRequest( BaseRequest ): self._HandleFixitResponse() elif 'message' in self._response: self._HandleMessageResponse() + elif 'detailed_info' in self._response: + self._HandleDetailedInfoResponse() def _HandleGotoResponse( self ): @@ -99,6 +101,10 @@ class CommandRequest( BaseRequest ): vimsupport.EchoText( self._response[ 'message' ] ) + def _HandleDetailedInfoResponse( self ): + vimsupport.WriteToPreviewWindow( self._response[ 'detailed_info' ] ) + + def SendCommandRequest( arguments, completer ): request = CommandRequest( arguments, completer ) # This is a blocking call. diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index 4cb2abd9..584a9bd0 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -21,10 +21,10 @@ from ycmd.utils import ToUtf8IfNeeded from ycm.client.base_request import ( BaseRequest, JsonFromFuture, HandleServerException, MakeServerException ) -import os TIMEOUT_SECONDS = 0.5 + class CompletionRequest( BaseRequest ): def __init__( self, request_data ): super( CompletionRequest, self ).__init__() @@ -85,7 +85,7 @@ def ConvertCompletionDataToVimData( completion_data ): if 'detailed_info' in completion_data: vim_data[ 'info' ] = ToUtf8IfNeeded( completion_data[ 'detailed_info' ] ) if doc_string: - vim_data[ 'info' ] += os.linesep + doc_string + vim_data[ 'info' ] += '\n' + doc_string elif doc_string: vim_data[ 'info' ] = doc_string diff --git a/python/ycm/client/tests/completion_request_test.py b/python/ycm/client/tests/completion_request_test.py index 1ca0a0c6..b23514ea 100644 --- a/python/ycm/client/tests/completion_request_test.py +++ b/python/ycm/client/tests/completion_request_test.py @@ -20,7 +20,6 @@ from nose.tools import eq_ from ycm.test_utils import MockVimModule vim_mock = MockVimModule() -import os from .. import completion_request @@ -57,7 +56,7 @@ class ConvertCompletionResponseToVimDatas_test: 'abbr': 'MENU TEXT', 'menu': 'EXTRA MENU INFO', 'kind': 'k', - 'info': 'DETAILED INFO' + os.linesep + 'DOC STRING', + 'info': 'DETAILED INFO\nDOC STRING', 'dup' : 1, } ) diff --git a/python/ycm/test_utils.py b/python/ycm/test_utils.py index 1a7c15d6..f060466b 100644 --- a/python/ycm/test_utils.py +++ b/python/ycm/test_utils.py @@ -20,17 +20,44 @@ from mock import MagicMock import sys +# 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, +# and subsquent assignments to sys.modules[ 'vim' ] don't retrospectively update +# them. The result is that while running the tests, we must assign only one +# instance of MagicMock to sys.modules[ 'vim' ] and always return it. +# +# More explanation is available: +# https://github.com/Valloric/YouCompleteMe/pull/1694 +VIM_MOCK = MagicMock() + def MockVimModule(): """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. """ + Vim Python interpreter, so we replace it with a MagicMock for tests. If you + need to add additional mocks to vim module functions, then use 'patch' from + mock module, to ensure that the state of the vim mock is returned before the + next test. That is: + + from ycm.test_utils import MockVimModule + from mock import patch + + # Do this once + MockVimModule() + + @patch( 'vim.eval', return_value='test' ) + @patch( 'vim.command', side_effect=ValueError ) + def test( vim_command, vim_eval ): + # use vim.command via vim_command, e.g.: + vim_command.assert_has_calls( ... ) + + Failure to use this approach may lead to unexpected failures in other + tests.""" def VimEval( value ): if value == "g:ycm_min_num_of_chars_for_completion": return 0 return '' - vim_mock = MagicMock() - vim_mock.eval = MagicMock( side_effect = VimEval ) - sys.modules[ 'vim' ] = vim_mock - return vim_mock + VIM_MOCK.eval = MagicMock( side_effect = VimEval ) + sys.modules[ 'vim' ] = VIM_MOCK + return VIM_MOCK diff --git a/python/ycm/tests/__init__.py b/python/ycm/tests/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/python/ycm/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/python/ycm/tests/syntax_parse_test.py b/python/ycm/tests/syntax_parse_test.py index 17e1184a..8692792b 100644 --- a/python/ycm/tests/syntax_parse_test.py +++ b/python/ycm/tests/syntax_parse_test.py @@ -17,11 +17,12 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +from ycm.test_utils import MockVimModule +MockVimModule() + import os from nose.tools import eq_ from hamcrest import assert_that, has_items -from ycm.test_utils import MockVimModule -vim_mock = MockVimModule() from ycm import syntax_parse diff --git a/python/ycm/tests/vimsupport_test.py b/python/ycm/tests/vimsupport_test.py index ff4e8e1c..936101a0 100644 --- a/python/ycm/tests/vimsupport_test.py +++ b/python/ycm/tests/vimsupport_test.py @@ -17,8 +17,12 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +from ycm.test_utils import MockVimModule +MockVimModule() + from ycm import vimsupport from nose.tools import eq_ +from mock import MagicMock, call, patch def ReplaceChunk_SingleLine_Repl_1_test(): @@ -576,3 +580,88 @@ def _BuildChunk( start_line, start_column, end_line, end_column, }, 'replacement_text': replacement_text } + + +def _Mock_tempname( arg ): + if arg == 'tempname()': + return '_TEMP_FILE_' + + raise ValueError( 'Unexpected evaluation: ' + arg ) + + +@patch( 'vim.eval', side_effect=_Mock_tempname ) +@patch( 'vim.command' ) +@patch( 'vim.current' ) +def WriteToPreviewWindow_test( vim_current, vim_command, vim_eval ): + vim_current.window.options.__getitem__ = MagicMock( return_value = True ) + + vimsupport.WriteToPreviewWindow( "test" ) + + vim_command.assert_has_calls( [ + call( 'silent! pclose!' ), + call( 'silent! pedit! _TEMP_FILE_' ), + call( 'silent! wincmd P' ), + call( 'silent! wincmd p' ) ] ) + + vim_current.buffer.__setitem__.assert_called_with( + slice( None, None, None ), [ 'test' ] ) + + vim_current.buffer.options.__setitem__.assert_has_calls( [ + call( 'buftype', 'nofile' ), + call( 'swapfile', False ), + call( 'modifiable', False ), + call( 'modified', False ), + call( 'readonly', True ), + ], any_order = True ) + + +@patch( 'vim.eval', side_effect=_Mock_tempname ) +@patch( 'vim.current' ) +def WriteToPreviewWindow_MultiLine_test( vim_current, vim_eval ): + vim_current.window.options.__getitem__ = MagicMock( return_value = True ) + vimsupport.WriteToPreviewWindow( "test\ntest2" ) + + vim_current.buffer.__setitem__.assert_called_with( + slice( None, None, None ), [ 'test', 'test2' ] ) + + +@patch( 'vim.eval', side_effect=_Mock_tempname ) +@patch( 'vim.command' ) +@patch( 'vim.current' ) +def WriteToPreviewWindow_JumpFail_test( vim_current, vim_command, vim_eval ): + vim_current.window.options.__getitem__ = MagicMock( return_value = False ) + + vimsupport.WriteToPreviewWindow( "test" ) + + vim_command.assert_has_calls( [ + call( 'silent! pclose!' ), + call( 'silent! pedit! _TEMP_FILE_' ), + call( 'silent! wincmd P' ), + call( "echom 'test'" ), + ] ) + + vim_current.buffer.__setitem__.assert_not_called() + vim_current.buffer.options.__setitem__.assert_not_called() + + +@patch( 'vim.eval', side_effect=_Mock_tempname ) +@patch( 'vim.command' ) +@patch( 'vim.current' ) +def WriteToPreviewWindow_JumpFail_MultiLine_test( vim_current, + vim_command, + vim_eval ): + + vim_current.window.options.__getitem__ = MagicMock( return_value = False ) + + vimsupport.WriteToPreviewWindow( "test\ntest2" ) + + vim_command.assert_has_calls( [ + call( 'silent! pclose!' ), + call( 'silent! pedit! _TEMP_FILE_' ), + call( 'silent! wincmd P' ), + call( "echom 'test'" ), + call( "echom 'test2'" ), + ] ) + + vim_current.buffer.__setitem__.assert_not_called() + vim_current.buffer.options.__setitem__.assert_not_called() diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index e00a12df..50f95d03 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -575,3 +575,65 @@ def SearchInCurrentBuffer( pattern ): def LineTextInCurrentBuffer( line ): return vim.current.buffer[ line ] + + +def ClosePreviewWindow(): + """ Close the preview window if it is present, otherwise do nothing """ + vim.command( 'silent! pclose!' ) + + +def JumpToPreviewWindow(): + """ Jump the vim cursor to the preview window, which must be active. Returns + boolean indicating if the cursor ended up in the preview window """ + vim.command( 'silent! wincmd P' ) + return vim.current.window.options[ 'previewwindow' ] + + +def JumpToPreviousWindow(): + """ Jump the vim cursor to its previous window position """ + vim.command( 'silent! wincmd p' ) + + +def OpenFileInPreviewWindow( filename ): + """ Open the supplied filename in the preview window """ + vim.command( 'silent! pedit! ' + filename ) + + +def WriteToPreviewWindow( message ): + """ Display the supplied message in the preview window """ + + # This isn't something that comes naturally to Vim. Vim only wants to show + # tags and/or actual files in the preview window, so we have to hack it a + # little bit. We generate a temporary file name and "open" that, then write + # the data to it. We make sure the buffer can't be edited or saved. Other + # approaches include simply opening a split, but we want to take advantage of + # the existing Vim options for preview window height, etc. + + ClosePreviewWindow() + + OpenFileInPreviewWindow( vim.eval( 'tempname()' ) ) + + if JumpToPreviewWindow(): + # We actually got to the preview window. By default the preview window can't + # be changed, so we make it writable, write to it, then make it read only + # again. + vim.current.buffer.options[ 'modifiable' ] = True + vim.current.buffer.options[ 'readonly' ] = False + + vim.current.buffer[:] = message.splitlines() + + vim.current.buffer.options[ 'buftype' ] = 'nofile' + vim.current.buffer.options[ 'swapfile' ] = False + vim.current.buffer.options[ 'modifiable' ] = False + vim.current.buffer.options[ 'readonly' ] = True + + # We need to prevent closing the window causing a warning about unsaved + # file, so we pretend to Vim that the buffer has not been changed. + vim.current.buffer.options[ 'modified' ] = False + + JumpToPreviousWindow() + else: + # We couldn't get to the preview window, but we still want to give the user + # the information we have. The only remaining option is to echo to the + # status area. + EchoText( message )