瀏覽代碼

fixup! Add CompleteDone hook, with namespace insertion for C#

Spencer G. Jones 9 年之前
父節點
當前提交
9f568be39a
共有 7 個文件被更改,包括 231 次插入97 次删除
  1. 12 0
      README.md
  2. 1 3
      autoload/youcompleteme.vim
  3. 31 17
      doc/youcompleteme.txt
  4. 2 2
      plugin/youcompleteme.vim
  5. 117 37
      python/ycm/tests/postcomplete_tests.py
  6. 15 13
      python/ycm/vimsupport.py
  7. 53 25
      python/ycm/youcompleteme.py

+ 12 - 0
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

+ 1 - 3
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

+ 31 - 17
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

+ 2 - 2
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

+ 117 - 37
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,113 +23,193 @@ 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()
 
-  eq_( "", ycm_state.GetRequiredNamespaceImport( {} ) )
+  assert action.called
 
 
-def GetRequiredNamespaceImport_ReturnNamespaceFromExtraData_test():
-  namespace = "A_NAMESPACE"
+def OnCompleteDone_NoActionNoError_test():
+  vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] )
   ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
 
-  eq_( namespace, ycm_state.GetRequiredNamespaceImport(
-    _BuildCompletion( namespace )
-  ))
+  ycm_state.OnCompleteDone()
 
 
-def FilterMatchingCompletions_MatchIsReturned_test():
+def FilterToCompletionsMatchingOnCursor_MatchIsReturned_test():
   ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
   vimsupport.TextBeforeCursor = MagicMock( return_value = "   Test" )
-  completions = [ _BuildCompletion( "A" ) ]
+  completions = [ _BuildCompletion( "Test" ) ]
 
-  result = ycm_state.FilterMatchingCompletions( completions )
+  result = ycm_state.FilterToCompletionsMatchingOnCursor( completions )
 
   eq_( list( result ), completions )
 
 
-def FilterMatchingCompletions_ShortTextDoesntRaise_test():
+def FilterToCompletionsMatchingOnCursor_ShortTextDoesntRaise_test():
   ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
   vimsupport.TextBeforeCursor = MagicMock( return_value = "X" )
-  completions = [ _BuildCompletion( "A" ) ]
+  completions = [ _BuildCompletion( "AAA" ) ]
 
-  ycm_state.FilterMatchingCompletions( completions )
+  ycm_state.FilterToCompletionsMatchingOnCursor( completions )
 
 
-def FilterMatchingCompletions_ExactMatchIsReturned_test():
+def FilterToCompletionsMatchingOnCursor_ExactMatchIsReturned_test():
   ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
   vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" )
-  completions = [ _BuildCompletion( "A" ) ]
+  completions = [ _BuildCompletion( "Test" ) ]
 
-  result = ycm_state.FilterMatchingCompletions( completions )
+  result = ycm_state.FilterToCompletionsMatchingOnCursor( completions )
 
   eq_( list( result ), completions )
 
 
-def FilterMatchingCompletions_NonMatchIsntReturned_test():
+def FilterToCompletionsMatchingOnCursor_NonMatchIsntReturned_test():
   ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
   vimsupport.TextBeforeCursor = MagicMock( return_value = "   Quote" )
   completions = [ _BuildCompletion( "A" ) ]
 
-  result = ycm_state.FilterMatchingCompletions( completions )
+  result = ycm_state.FilterToCompletionsMatchingOnCursor( completions )
 
   assert_that( list( result ), empty() )
 
 
-def PostComplete_EmptyDoesntInsertNamespace_test():
-  ycm_state = _SetupForCompletionDone( [] )
+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 )
 
-  ycm_state.OnCompleteDone()
+  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():
+  namespace = "A_NAMESPACE"
+  ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
+
+  eq_( namespace, ycm_state.GetRequiredNamespaceImport(
+    _BuildCompletion( namespace )
+  ))
+
+
+def GetMatchingCompletionsOnCursor_ReturnEmptyIfNotDone_test():
+  ycm_state = _SetupForCsharpCompletionDone( [] )
+  ycm_state._latest_completion_request.Done = MagicMock( return_value = False )
+
+  eq_( [], ycm_state.GetMatchingCompletionsOnCursor() )
+  
+
+def GetMatchingCompletionsOnCursor_ReturnEmptyIfPendingMatches_test():
+  completions = [ _BuildCompletion( None ) ]
+  ycm_state = _SetupForCsharpCompletionDone( completions )
+  vimsupport.TextBeforeCursor = MagicMock( return_value = "   Te" )
+
+  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_ExistingWithoutNamespaceDoesntInsertNamespace_test():
+
+def PostCompleteCsharp_ExistingWithoutNamespaceDoesntInsertNamespace_test():
   completions = [ _BuildCompletion( None ) ]
-  ycm_state = _SetupForCompletionDone( completions )
+  ycm_state = _SetupForCsharpCompletionDone( completions )
 
-  ycm_state.OnCompleteDone()
+  ycm_state.OnCompleteDone_Csharp()
 
   assert not vimsupport.InsertNamespace.called
 
 
-def PostComplete_ValueDoesInsertNamespace_test():
+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 )

+ 15 - 13
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 ):

+ 53 - 25
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" ]