Add CompleteDone hook, with namespace insertion for C#

Add a new vim hook on CompleteDone. This hook is called when a
completions is selected.

When forcing semantic completion with the keybind, C# completions can
return a list of importable types. These types are from namespaces which
havn't been imported, and thus are not valid to use without also adding
their namespace's import statement. This change makes YCM automatically
insert the necessary using statement to import that namespace on
completion completion. In the case there are multiple possible
namespaces, it prompts you to choose one.
This commit is contained in:
Spencer G. Jones 2015-08-28 08:26:18 -06:00
parent 7e9333a9c2
commit dd27184970
5 changed files with 251 additions and 5 deletions

View File

@ -84,6 +84,9 @@ function! youcompleteme#Enable()
autocmd InsertLeave * call s:OnInsertLeave() autocmd InsertLeave * call s:OnInsertLeave()
autocmd InsertEnter * call s:OnInsertEnter() autocmd InsertEnter * call s:OnInsertEnter()
autocmd VimLeave * call s:OnVimLeave() autocmd VimLeave * call s:OnVimLeave()
if pyeval( 'vimsupport.VimVersionAtLeast("7.3.598")' )
autocmd CompleteDone * call s:OnCompleteDone()
endif
augroup END augroup END
" Calling these once solves the problem of BufReadPre/BufRead/BufEnter not " Calling these once solves the problem of BufReadPre/BufRead/BufEnter not
@ -359,6 +362,11 @@ function! s:OnVimLeave()
endfunction endfunction
function! s:OnCompleteDone()
py ycm_state.OnCompleteDone()
endfunction
function! s:OnBufferReadPre(filename) function! s:OnBufferReadPre(filename)
let threshold = g:ycm_disable_for_files_larger_than_kb * 1024 let threshold = g:ycm_disable_for_files_larger_than_kb * 1024

View File

@ -40,7 +40,7 @@ class CompletionRequest( BaseRequest ):
return self._response_future.done() return self._response_future.done()
def Response( self ): def RawResponse( self ):
if not self._response_future: if not self._response_future:
return [] return []
try: try:
@ -50,13 +50,16 @@ class CompletionRequest( BaseRequest ):
for e in errors: for e in errors:
HandleServerException( MakeServerException( e ) ) HandleServerException( MakeServerException( e ) )
return _ConvertCompletionResponseToVimDatas( response ) return JsonFromFuture( self._response_future )[ 'completions' ]
except Exception as e: except Exception as e:
HandleServerException( e ) HandleServerException( e )
return [] return []
def Response( self ):
return _ConvertCompletionDatasToVimDatas( self.RawResponse() )
def _ConvertCompletionDataToVimData( completion_data ): def _ConvertCompletionDataToVimData( completion_data ):
# see :h complete-items for a description of the dictionary fields # see :h complete-items for a description of the dictionary fields
vim_data = { vim_data = {
@ -77,6 +80,6 @@ def _ConvertCompletionDataToVimData( completion_data ):
return vim_data return vim_data
def _ConvertCompletionResponseToVimDatas( response_data ): def _ConvertCompletionDatasToVimDatas( response_data ):
return [ _ConvertCompletionDataToVimData( x ) return [ _ConvertCompletionDataToVimData( x )
for x in response_data[ 'completions' ] ] for x in response_data ]

View File

@ -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 <http://www.gnu.org/licenses/>.
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'
}

View File

@ -21,6 +21,7 @@ import vim
import os import os
import tempfile import tempfile
import json import json
import re
from ycmd.utils import ToUtf8IfNeeded from ycmd.utils import ToUtf8IfNeeded
from ycmd import user_options_store from ycmd import user_options_store
@ -453,6 +454,14 @@ def FiletypesForBuffer( buffer_object ):
return GetBufferOption( buffer_object, 'ft' ).split( '.' ) 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 ): def GetVariableValue( variable ):
return vim.eval( 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 new_line_delta = replacement_lines_count - source_lines_count
return ( new_line_delta, new_char_delta ) 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 ]

View File

@ -292,6 +292,61 @@ class YouCompleteMe( object ):
SendEventNotificationAsync( 'CurrentIdentifierFinished' ) 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 ): def DiagnosticsForCurrentFileReady( self ):
return bool( self._latest_file_parse_request and return bool( self._latest_file_parse_request and
self._latest_file_parse_request.Done() ) self._latest_file_parse_request.Done() )