diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 37568dc0..d5d7b35e 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -84,6 +84,9 @@ function! youcompleteme#Enable() autocmd InsertLeave * call s:OnInsertLeave() autocmd InsertEnter * call s:OnInsertEnter() autocmd VimLeave * call s:OnVimLeave() + if pyeval( 'vimsupport.VimVersionAtLeast("7.3.598")' ) + autocmd CompleteDone * call s:OnCompleteDone() + endif augroup END " Calling these once solves the problem of BufReadPre/BufRead/BufEnter not @@ -359,6 +362,11 @@ function! s:OnVimLeave() endfunction +function! s:OnCompleteDone() + py ycm_state.OnCompleteDone() +endfunction + + function! s:OnBufferReadPre(filename) let threshold = g:ycm_disable_for_files_larger_than_kb * 1024 diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py index c630fb22..fafb8198 100644 --- a/python/ycm/client/completion_request.py +++ b/python/ycm/client/completion_request.py @@ -40,7 +40,7 @@ class CompletionRequest( BaseRequest ): return self._response_future.done() - def Response( self ): + def RawResponse( self ): if not self._response_future: return [] try: @@ -50,13 +50,16 @@ class CompletionRequest( BaseRequest ): for e in errors: HandleServerException( MakeServerException( e ) ) - return _ConvertCompletionResponseToVimDatas( response ) + return JsonFromFuture( self._response_future )[ 'completions' ] except Exception as e: HandleServerException( e ) - return [] + def Response( self ): + return _ConvertCompletionDatasToVimDatas( self.RawResponse() ) + + def _ConvertCompletionDataToVimData( completion_data ): # see :h complete-items for a description of the dictionary fields vim_data = { @@ -77,6 +80,6 @@ def _ConvertCompletionDataToVimData( completion_data ): return vim_data -def _ConvertCompletionResponseToVimDatas( response_data ): +def _ConvertCompletionDatasToVimDatas( response_data ): return [ _ConvertCompletionDataToVimData( x ) - for x in response_data[ 'completions' ] ] + for x in response_data ] diff --git a/python/ycm/tests/postcomplete_tests.py b/python/ycm/tests/postcomplete_tests.py new file mode 100644 index 00000000..1c0d0bc6 --- /dev/null +++ b/python/ycm/tests/postcomplete_tests.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Google Inc. +# +# 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 . + +from mock import MagicMock +from nose.tools import eq_ +from hamcrest import assert_that, empty +from ycm import vimsupport +from ycm.youcompleteme import YouCompleteMe + +def HasPostCompletionAction_TrueOnCsharp_test(): + vimsupport.CurrentFiletypes = MagicMock( return_value = [ "cs" ] ) + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + eq_( True, ycm_state.HasPostCompletionAction() ) + + +def HasPostCompletionAction_FalseOnOtherFiletype_test(): + vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] ) + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + eq_( False, ycm_state.HasPostCompletionAction() ) + + +def GetRequiredNamespaceImport_ReturnEmptyForNoExtraData_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + + eq_( "", ycm_state.GetRequiredNamespaceImport( {} ) ) + + +def GetRequiredNamespaceImport_ReturnNamespaceFromExtraData_test(): + namespace = "A_NAMESPACE" + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + + eq_( namespace, ycm_state.GetRequiredNamespaceImport( + _BuildCompletion( namespace ) + )) + + +def FilterMatchingCompletions_MatchIsReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" ) + completions = [ _BuildCompletion( "A" ) ] + + result = ycm_state.FilterMatchingCompletions( completions ) + + eq_( list( result ), completions ) + + +def FilterMatchingCompletions_ShortTextDoesntRaise_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "X" ) + completions = [ _BuildCompletion( "A" ) ] + + ycm_state.FilterMatchingCompletions( completions ) + + +def FilterMatchingCompletions_ExactMatchIsReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" ) + completions = [ _BuildCompletion( "A" ) ] + + result = ycm_state.FilterMatchingCompletions( completions ) + + eq_( list( result ), completions ) + + +def FilterMatchingCompletions_NonMatchIsntReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Quote" ) + completions = [ _BuildCompletion( "A" ) ] + + result = ycm_state.FilterMatchingCompletions( completions ) + + assert_that( list( result ), empty() ) + + +def PostComplete_EmptyDoesntInsertNamespace_test(): + ycm_state = _SetupForCompletionDone( [] ) + + ycm_state.OnCompleteDone() + + assert not vimsupport.InsertNamespace.called + +def PostComplete_ExistingWithoutNamespaceDoesntInsertNamespace_test(): + completions = [ _BuildCompletion( None ) ] + ycm_state = _SetupForCompletionDone( completions ) + + ycm_state.OnCompleteDone() + + assert not vimsupport.InsertNamespace.called + + +def PostComplete_ValueDoesInsertNamespace_test(): + namespace = "A_NAMESPACE" + completions = [ _BuildCompletion( namespace ) ] + ycm_state = _SetupForCompletionDone( completions ) + + ycm_state.OnCompleteDone() + + vimsupport.InsertNamespace.assert_called_once_with( namespace ) + +def PostComplete_InsertSecondNamespaceIfSelected_test(): + namespace = "A_NAMESPACE" + namespace2 = "ANOTHER_NAMESPACE" + completions = [ + _BuildCompletion( namespace ), + _BuildCompletion( namespace2 ), + ] + ycm_state = _SetupForCompletionDone( completions ) + vimsupport.PresentDialog = MagicMock( return_value = 1 ) + + ycm_state.OnCompleteDone() + + vimsupport.InsertNamespace.assert_called_once_with( namespace2 ) + + +def _SetupForCompletionDone( completions ): + vimsupport.CurrentFiletypes = MagicMock( return_value = [ "cs" ] ) + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + request = MagicMock(); + request.Done = MagicMock( return_value = True ) + request.RawResponse = MagicMock( return_value = completions ) + ycm_state._latest_completion_request = request + vimsupport.InsertNamespace = MagicMock() + vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" ) + return ycm_state + + +def _BuildCompletion( namespace ): + return { + 'extra_data': { 'required_namespace_import' : namespace }, + 'insertion_text': 'Test' + } diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 27c634a6..eb939822 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -21,6 +21,7 @@ import vim import os import tempfile import json +import re from ycmd.utils import ToUtf8IfNeeded from ycmd import user_options_store @@ -453,6 +454,14 @@ def FiletypesForBuffer( buffer_object ): return GetBufferOption( buffer_object, 'ft' ).split( '.' ) +def VariableExists( variable ): + return GetBoolValue( "exists( '{0}' )".format( EscapeForVim( variable ) ) ) + + +def SetVariableValue( variable, value ): + vim.command( "let {0} = '{1}'".format( variable, EscapeForVim( value ) ) ) + + def GetVariableValue( variable ): return vim.eval( variable ) @@ -509,3 +518,27 @@ def ReplaceChunk( start, end, replacement_text, line_delta, char_delta, new_line_delta = replacement_lines_count - source_lines_count return ( new_line_delta, new_char_delta ) + + +def InsertNamespace( namespace ): + if VariableExists( 'g:ycm_cs_insert_namespace_function' ): + function = GetVariableValue( 'g:ycm_cs_insert_namespace_function' ) + SetVariableValue( "g:ycm_namespace", namespace ) + vim.eval( function ) + else: + pattern = '^\s*using\(\s\+[a-zA-Z0-9]\+\s\+=\)\?\s\+[a-zA-Z0-9.]\+\s*;\s*' + line = SearchInCurrentBuffer( pattern ) + existing_line = LineTextInCurrentBuffer( line ) + existing_indent = re.sub( r"\S.*", "", existing_line ) + new_line = "{0}using {1};\n\n".format( existing_indent, namespace ) + replace_pos = { 'line_num': line + 1, 'column_num': 1 } + ReplaceChunk( replace_pos, replace_pos, new_line, 0, 0 ) + PostVimMessage( "Add namespace: {0}".format( namespace ) ) + + +def SearchInCurrentBuffer( pattern ): + return GetIntValue( "search('{0}', 'Wcnb')".format( EscapeForVim( pattern ))) + + +def LineTextInCurrentBuffer( line ): + return vim.current.buffer[ line ] diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 895296dd..f1f735cb 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -292,6 +292,61 @@ class YouCompleteMe( object ): SendEventNotificationAsync( 'CurrentIdentifierFinished' ) + def OnCompleteDone( self ): + if not self.HasPostCompletionAction(): + return + + latest_completion_request = self.GetCurrentCompletionRequest() + if not latest_completion_request.Done(): + return + + completions = latest_completion_request.RawResponse() + completions = list( self.FilterMatchingCompletions( completions ) ) + if not completions: + return + + namespaces = [ self.GetRequiredNamespaceImport( c ) + for c in completions ] + namespaces = [ n for n in namespaces if n ] + if not namespaces: + return + + if len( namespaces ) > 1: + choices = [ "{0}: {1}".format( i + 1, n ) + for i,n in enumerate( namespaces ) ] + choice = vimsupport.PresentDialog( + "Insert which namespace:", choices ) + if choice < 0: + return + namespace = namespaces[ choice ] + else: + namespace = namespaces[ 0 ] + + vimsupport.InsertNamespace( namespace ) + + + def HasPostCompletionAction( self ): + filetype = vimsupport.CurrentFiletypes()[ 0 ] + return filetype == 'cs' + + + def FilterMatchingCompletions( self, completions ): + text = vimsupport.TextBeforeCursor() # No support for multiple line completions + for completion in completions: + word = completion[ "insertion_text" ] + for i in [ None, -1 ]: + if text[ -1 * len( word ) + ( i or 0 ) : i ] == word: + yield completion + break + + + def GetRequiredNamespaceImport( self, completion ): + if ( "extra_data" not in completion + or "required_namespace_import" not in completion[ "extra_data" ] ): + return "" + return completion[ "extra_data" ][ "required_namespace_import" ] + + def DiagnosticsForCurrentFileReady( self ): return bool( self._latest_file_parse_request and self._latest_file_parse_request.Done() )