From dd27184970ea54d7e31f7ff2ed5d7b630c532341 Mon Sep 17 00:00:00 2001 From: "Spencer G. Jones" Date: Fri, 28 Aug 2015 08:26:18 -0600 Subject: [PATCH] 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. --- autoload/youcompleteme.vim | 8 ++ python/ycm/client/completion_request.py | 13 ++- python/ycm/tests/postcomplete_tests.py | 147 ++++++++++++++++++++++++ python/ycm/vimsupport.py | 33 ++++++ python/ycm/youcompleteme.py | 55 +++++++++ 5 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 python/ycm/tests/postcomplete_tests.py 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() )