From 9f568be39a36738f649f937e99f37f596a6ccb72 Mon Sep 17 00:00:00 2001 From: "Spencer G. Jones" Date: Mon, 31 Aug 2015 10:51:23 -0600 Subject: [PATCH] fixup! Add CompleteDone hook, with namespace insertion for C# --- README.md | 12 ++ autoload/youcompleteme.vim | 4 +- doc/youcompleteme.txt | 48 +++--- plugin/youcompleteme.vim | 4 +- python/ycm/tests/postcomplete_tests.py | 202 +++++++++++++++++-------- python/ycm/vimsupport.py | 28 ++-- python/ycm/youcompleteme.py | 78 +++++++--- 7 files changed, 255 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index e4619698..5a503ded 100644 --- a/README.md +++ b/README.md @@ -1365,6 +1365,18 @@ Default: `0` let g:ycm_csharp_server_port = 0 +### The `g:ycm_csharp_insert_namespace_expr` option + +When YCM inserts a namespace, by default, it will insert it under the nearest +using statement. When this option is set, YCM will instead set the global +variable `g:ycm_namespace_to_insert` to the namespace to insert, and then +evaluate this option's value as an expression. The expression is responsible +for inserting the namespace. + +Default: `''` + + let g:ycm_csharp_insert_namespace_expr = '' + ### The `g:ycm_add_preview_to_completeopt` option When this option is set to `1`, YCM will add the `preview` string to Vim's diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index d5d7b35e..56a9162e 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -84,9 +84,7 @@ 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 + autocmd CompleteDone * call s:OnCompleteDone() augroup END " Calling these once solves the problem of BufReadPre/BufRead/BufEnter not diff --git a/doc/youcompleteme.txt b/doc/youcompleteme.txt index f47138ae..d7a40446 100644 --- a/doc/youcompleteme.txt +++ b/doc/youcompleteme.txt @@ -73,23 +73,24 @@ Contents ~ 26. The |g:ycm_auto_start_csharp_server| option 27. The |g:ycm_auto_stop_csharp_server| option 28. The |g:ycm_csharp_server_port| option - 29. The |g:ycm_add_preview_to_completeopt| option - 30. The |g:ycm_autoclose_preview_window_after_completion| option - 31. The |g:ycm_autoclose_preview_window_after_insertion| option - 32. The |g:ycm_max_diagnostics_to_display| option - 33. The |g:ycm_key_list_select_completion| option - 34. The |g:ycm_key_list_previous_completion| option - 35. The |g:ycm_key_invoke_completion| option - 36. The |g:ycm_key_detailed_diagnostics| option - 37. The |g:ycm_global_ycm_extra_conf| option - 38. The |g:ycm_confirm_extra_conf| option - 39. The |g:ycm_extra_conf_globlist| option - 40. The |g:ycm_filepath_completion_use_working_dir| option - 41. The |g:ycm_semantic_triggers| option - 42. The |g:ycm_cache_omnifunc| option - 43. The |g:ycm_use_ultisnips_completer| option - 44. The |g:ycm_goto_buffer_command| option - 45. The |g:ycm_disable_for_files_larger_than_kb| option + 29. The |g:ycm_csharp_insert_namespace_expr| option + 30. The |g:ycm_add_preview_to_completeopt| option + 31. The |g:ycm_autoclose_preview_window_after_completion| option + 32. The |g:ycm_autoclose_preview_window_after_insertion| option + 33. The |g:ycm_max_diagnostics_to_display| option + 34. The |g:ycm_key_list_select_completion| option + 35. The |g:ycm_key_list_previous_completion| option + 36. The |g:ycm_key_invoke_completion| option + 37. The |g:ycm_key_detailed_diagnostics| option + 38. The |g:ycm_global_ycm_extra_conf| option + 39. The |g:ycm_confirm_extra_conf| option + 40. The |g:ycm_extra_conf_globlist| option + 41. The |g:ycm_filepath_completion_use_working_dir| option + 42. The |g:ycm_semantic_triggers| option + 43. The |g:ycm_cache_omnifunc| option + 44. The |g:ycm_use_ultisnips_completer| option + 45. The |g:ycm_goto_buffer_command| option + 46. The |g:ycm_disable_for_files_larger_than_kb| option 8. FAQ |youcompleteme-faq| 1. I used to be able to 'import vim' in '.ycm_extra_conf.py', but now can't |import-vim| 2. On very rare occasions Vim crashes when I tab through the completion menu |youcompleteme-on-very-rare-occasions-vim-crashes-when-i-tab-through-completion-menu| @@ -1581,6 +1582,19 @@ Default: '0' let g:ycm_csharp_server_port = 0 < ------------------------------------------------------------------------------- +The *g:ycm_csharp_insert_namespace_expr* option + +When YCM inserts a namespace, by default, it will insert it under the nearest +using statement. When this option is set, YCM will instead set the global +variable 'g:ycm_namespace_to_insert' to the namespace to insert, and then +evaluate this option's value as an expression. The expression is responsible +for inserting the namespace. + +Default: "''" +> + let g:ycm_csharp_insert_namespace_expr = '' +< +------------------------------------------------------------------------------- The *g:ycm_add_preview_to_completeopt* option When this option is set to '1', YCM will add the 'preview' string to Vim's diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index f1624cf5..e2fde723 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -27,9 +27,9 @@ endfunction if exists( "g:loaded_youcompleteme" ) call s:restore_cpo() finish -elseif v:version < 703 || (v:version == 703 && !has('patch584')) +elseif v:version < 703 || (v:version == 703 && !has('patch598')) echohl WarningMsg | - \ echomsg "YouCompleteMe unavailable: requires Vim 7.3.584+" | + \ echomsg "YouCompleteMe unavailable: requires Vim 7.3.598+" | \ echohl None call s:restore_cpo() finish diff --git a/python/ycm/tests/postcomplete_tests.py b/python/ycm/tests/postcomplete_tests.py index 1c0d0bc6..b9efcefb 100644 --- a/python/ycm/tests/postcomplete_tests.py +++ b/python/ycm/tests/postcomplete_tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (C) 2013 Google Inc. +# Copyright (C) 2015 YouCompleteMe contributors # # This file is part of YouCompleteMe. # @@ -23,22 +23,117 @@ from hamcrest import assert_that, empty from ycm import vimsupport from ycm.youcompleteme import YouCompleteMe -def HasPostCompletionAction_TrueOnCsharp_test(): +def GetCompleteDoneHooks_ResultOnCsharp_test(): vimsupport.CurrentFiletypes = MagicMock( return_value = [ "cs" ] ) ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) - eq_( True, ycm_state.HasPostCompletionAction() ) + result = ycm_state.GetCompleteDoneHooks() + eq_( 1, len( list( result ) ) ) -def HasPostCompletionAction_FalseOnOtherFiletype_test(): +def GetCompleteDoneHooks_EmptyOnOtherFiletype_test(): vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] ) ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) - eq_( False, ycm_state.HasPostCompletionAction() ) + result = ycm_state.GetCompleteDoneHooks() + eq_( 0, len( list( result ) ) ) -def GetRequiredNamespaceImport_ReturnEmptyForNoExtraData_test(): +def OnCompleteDone_WithActionCallsIt_test(): + vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] ) + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + action = MagicMock() + ycm_state._complete_done_hooks[ "txt" ] = action + ycm_state.OnCompleteDone() + + assert action.called + + +def OnCompleteDone_NoActionNoError_test(): + vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] ) ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) - eq_( "", ycm_state.GetRequiredNamespaceImport( {} ) ) + ycm_state.OnCompleteDone() + + +def FilterToCompletionsMatchingOnCursor_MatchIsReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" ) + completions = [ _BuildCompletion( "Test" ) ] + + result = ycm_state.FilterToCompletionsMatchingOnCursor( completions ) + + eq_( list( result ), completions ) + + +def FilterToCompletionsMatchingOnCursor_ShortTextDoesntRaise_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "X" ) + completions = [ _BuildCompletion( "AAA" ) ] + + ycm_state.FilterToCompletionsMatchingOnCursor( completions ) + + +def FilterToCompletionsMatchingOnCursor_ExactMatchIsReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" ) + completions = [ _BuildCompletion( "Test" ) ] + + result = ycm_state.FilterToCompletionsMatchingOnCursor( completions ) + + eq_( list( result ), completions ) + + +def FilterToCompletionsMatchingOnCursor_NonMatchIsntReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Quote" ) + completions = [ _BuildCompletion( "A" ) ] + + result = ycm_state.FilterToCompletionsMatchingOnCursor( completions ) + + assert_that( list( result ), empty() ) + + +def HasCompletionsThatCouldMatchOnCursorWithMoreText_MatchIsReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Te" ) + completions = [ _BuildCompletion( "Test" ) ] + + result = ycm_state.HasCompletionsThatCouldMatchOnCursorWithMoreText( completions ) + + eq_( result, True ) + + +def HasCompletionsThatCouldMatchOnCursorWithMoreText_ShortTextDoesntRaise_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "X" ) + completions = [ _BuildCompletion( "AAA" ) ] + + ycm_state.HasCompletionsThatCouldMatchOnCursorWithMoreText( completions ) + + +def HasCompletionsThatCouldMatchOnCursorWithMoreText_ExactMatchIsntReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" ) + completions = [ _BuildCompletion( "Test" ) ] + + result = ycm_state.HasCompletionsThatCouldMatchOnCursorWithMoreText( completions ) + + eq_( result, False ) + + +def HasCompletionsThatCouldMatchOnCursorWithMoreText_NonMatchIsntReturned_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Quote" ) + completions = [ _BuildCompletion( "A" ) ] + + result = ycm_state.HasCompletionsThatCouldMatchOnCursorWithMoreText( completions ) + + eq_( result, False ) + + +def GetRequiredNamespaceImport_ReturnNoneForNoExtraData_test(): + ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) + + eq_( None, ycm_state.GetRequiredNamespaceImport( {} ) ) def GetRequiredNamespaceImport_ReturnNamespaceFromExtraData_test(): @@ -50,86 +145,71 @@ def GetRequiredNamespaceImport_ReturnNamespaceFromExtraData_test(): )) -def FilterMatchingCompletions_MatchIsReturned_test(): - ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) - vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" ) - completions = [ _BuildCompletion( "A" ) ] +def GetMatchingCompletionsOnCursor_ReturnEmptyIfNotDone_test(): + ycm_state = _SetupForCsharpCompletionDone( [] ) + ycm_state._latest_completion_request.Done = MagicMock( return_value = False ) - result = ycm_state.FilterMatchingCompletions( completions ) + eq_( [], ycm_state.GetMatchingCompletionsOnCursor() ) + - 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(): +def GetMatchingCompletionsOnCursor_ReturnEmptyIfPendingMatches_test(): completions = [ _BuildCompletion( None ) ] - ycm_state = _SetupForCompletionDone( completions ) + ycm_state = _SetupForCsharpCompletionDone( completions ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Te" ) - ycm_state.OnCompleteDone() + eq_( [], ycm_state.GetMatchingCompletionsOnCursor() ) + + +def GetMatchingCompletionsOnCursor_ReturnMatchIfMatches_test(): + completions = [ _BuildCompletion( None ) ] + ycm_state = _SetupForCsharpCompletionDone( completions ) + vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" ) + + eq_( completions, ycm_state.GetMatchingCompletionsOnCursor() ) + + +def PostCompleteCsharp_EmptyDoesntInsertNamespace_test(): + ycm_state = _SetupForCsharpCompletionDone( [] ) + + ycm_state.OnCompleteDone_Csharp() assert not vimsupport.InsertNamespace.called -def PostComplete_ValueDoesInsertNamespace_test(): +def PostCompleteCsharp_ExistingWithoutNamespaceDoesntInsertNamespace_test(): + completions = [ _BuildCompletion( None ) ] + ycm_state = _SetupForCsharpCompletionDone( completions ) + + ycm_state.OnCompleteDone_Csharp() + + assert not vimsupport.InsertNamespace.called + + +def PostCompleteCsharp_ValueDoesInsertNamespace_test(): namespace = "A_NAMESPACE" completions = [ _BuildCompletion( namespace ) ] - ycm_state = _SetupForCompletionDone( completions ) + ycm_state = _SetupForCsharpCompletionDone( completions ) - ycm_state.OnCompleteDone() + ycm_state.OnCompleteDone_Csharp() vimsupport.InsertNamespace.assert_called_once_with( namespace ) -def PostComplete_InsertSecondNamespaceIfSelected_test(): +def PostCompleteCsharp_InsertSecondNamespaceIfSelected_test(): namespace = "A_NAMESPACE" namespace2 = "ANOTHER_NAMESPACE" completions = [ _BuildCompletion( namespace ), _BuildCompletion( namespace2 ), ] - ycm_state = _SetupForCompletionDone( completions ) + ycm_state = _SetupForCsharpCompletionDone( completions ) vimsupport.PresentDialog = MagicMock( return_value = 1 ) - ycm_state.OnCompleteDone() + ycm_state.OnCompleteDone_Csharp() vimsupport.InsertNamespace.assert_called_once_with( namespace2 ) -def _SetupForCompletionDone( completions ): - vimsupport.CurrentFiletypes = MagicMock( return_value = [ "cs" ] ) +def _SetupForCsharpCompletionDone( completions ): ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) ) request = MagicMock(); request.Done = MagicMock( return_value = True ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index eb939822..d80a2914 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -521,19 +521,21 @@ def ReplaceChunk( start, end, replacement_text, line_delta, 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 ) ) + if VariableExists( 'g:ycm_csharp_insert_namespace_expr' ): + expr = GetVariableValue( 'g:ycm_csharp_insert_namespace_expr' ) + if expr: + SetVariableValue( "g:ycm_namespace_to_insert", namespace ) + vim.eval( expr ) + return + + 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 ): diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index f1f735cb..4f9e9ac8 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -21,6 +21,7 @@ import os import vim import tempfile import json +import re import signal import base64 from subprocess import PIPE @@ -96,6 +97,9 @@ class YouCompleteMe( object ): self._ycmd_keepalive = YcmdKeepalive() self._SetupServer() self._ycmd_keepalive.Start() + self._complete_done_hooks = { + 'cs': lambda( self ): self.OnCompleteDone_Csharp() + } def _SetupServer( self ): self._available_completers = {} @@ -293,18 +297,58 @@ class YouCompleteMe( object ): def OnCompleteDone( self ): - if not self.HasPostCompletionAction(): - return + complete_done_actions = self.GetCompleteDoneHooks() + for action in complete_done_actions: + action(self) + + def GetCompleteDoneHooks( self ): + filetypes = vimsupport.CurrentFiletypes() + for key, value in self._complete_done_hooks.iteritems(): + if key in filetypes: + yield value + + + def GetMatchingCompletionsOnCursor( self ): latest_completion_request = self.GetCurrentCompletionRequest() if not latest_completion_request.Done(): - return + return [] completions = latest_completion_request.RawResponse() - completions = list( self.FilterMatchingCompletions( completions ) ) - if not completions: - return + if self.HasCompletionsThatCouldMatchOnCursorWithMoreText( completions ): + # Since the way that YCM works leads to CompleteDone called on every + # character, return blank if the completion might not be done. This won't + # match if the completion is ended with typing a non-keyword character. + return [] + result = self.FilterToCompletionsMatchingOnCursor( completions ) + + return list( result ) + + + def FilterToCompletionsMatchingOnCursor( self, completions ): + text = vimsupport.TextBeforeCursor() # No support for multiple line completions + for completion in completions: + word = completion[ "insertion_text" ] + # Trim complete-ending character if needed + text = re.sub( r"[^a-zA-Z0-9_]$", "", text ) + buffer_text = text[ -1 * len( word ) : ] + if buffer_text == word: + yield completion + + + def HasCompletionsThatCouldMatchOnCursorWithMoreText( self, completions ): + text = vimsupport.TextBeforeCursor() # No support for multiple line completions + for completion in completions: + word = completion[ "insertion_text" ] + for i in range( 1, len( word ) - 1 ): # Excluding full word + if text[ -1 * i : ] == word[ : i ]: + return True + return False + + + def OnCompleteDone_Csharp( self ): + completions = self.GetMatchingCompletionsOnCursor() namespaces = [ self.GetRequiredNamespaceImport( c ) for c in completions ] namespaces = [ n for n in namespaces if n ] @@ -312,10 +356,9 @@ class YouCompleteMe( object ): return if len( namespaces ) > 1: - choices = [ "{0}: {1}".format( i + 1, n ) + choices = [ "{0} {1}".format( i + 1, n ) for i,n in enumerate( namespaces ) ] - choice = vimsupport.PresentDialog( - "Insert which namespace:", choices ) + choice = vimsupport.PresentDialog( "Insert which namespace:", choices ) if choice < 0: return namespace = namespaces[ choice ] @@ -325,25 +368,10 @@ class YouCompleteMe( object ): 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 None return completion[ "extra_data" ][ "required_namespace_import" ]