Auto merge of #1636 - mispencer:CsAddImport, r=puremourning
Add required namespace in C# after completing 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. Demo: ![c namespaceinsertdemo](https://cloud.githubusercontent.com/assets/1434266/9337289/b7994542-459b-11e5-8f52-e48af76a3aab.gif) By default, the using statement is inserted after the last using statement. If there aren't any, it's inserted before the first line. This behavior is customization by setting a function to the vim global ``g:ycm_cs_insert_namespace_function``. I implemented this in pure vimscript largely so I could provide such customization... Caveats: vim does not provide a way to get which completion was inserted. To get this, we store the completions, hook the CompleteDone event, and check than if the preceding text matches any of the completions. This is imperfect, and sometimes matches when it shouldn't. Also, the CompleteDone event requires 7.3.598 or newer.
This commit is contained in:
commit
0d426e4922
27
README.md
27
README.md
@ -167,7 +167,7 @@ that are conservatively turned off by default that you may want to turn on.
|
||||
Please refer to the full Installation Guide below; the following commands are
|
||||
provided on a best-effort basis and may not work for you.
|
||||
|
||||
Make sure you have Vim 7.3.584 with python2 support. Ubuntu 14.04 and later have
|
||||
Make sure you have Vim 7.3.598 with python2 support. Ubuntu 14.04 and later have
|
||||
a Vim that's recent enough. You can see the version of Vim installed by running
|
||||
`vim --version`. If the version is too old, you may need to [compile Vim
|
||||
from source][vim-build] (don't worry, it's easy).
|
||||
@ -219,7 +219,7 @@ Please refer to the full Installation Guide below; the following commands are
|
||||
provided on a best-effort basis and may not work for you. OpenBSD / FreeBSD are
|
||||
not officially supported platforms by YCM.
|
||||
|
||||
Make sure you have Vim 7.3.584 with python2 support.
|
||||
Make sure you have Vim 7.3.598 with python2 support.
|
||||
|
||||
OpenBSD 5.5 and later have a Vim that's recent enough. You can see the version of
|
||||
Vim installed by running `vim --version`.
|
||||
@ -277,19 +277,19 @@ process.
|
||||
|
||||
**Please follow the instructions carefully. Read EVERY WORD.**
|
||||
|
||||
1. **Ensure that your version of Vim is _at least_ 7.3.584 _and_ that it has
|
||||
1. **Ensure that your version of Vim is _at least_ 7.3.598 _and_ that it has
|
||||
support for python2 scripting**.
|
||||
|
||||
Inside Vim, type `:version`. Look at the first two to three lines of output;
|
||||
it should say `Vi IMproved X.Y`, where X.Y is the major version of vim. If
|
||||
your version is greater than 7.3, then you're all set. If your version is
|
||||
7.3 then look below that where it says, `Included patches: 1-Z`, where Z
|
||||
will be some number. That number needs to be 584 or higher.
|
||||
will be some number. That number needs to be 598 or higher.
|
||||
|
||||
If your version of Vim is not recent enough, you may need to [compile Vim
|
||||
from source][vim-build] (don't worry, it's easy).
|
||||
|
||||
After you have made sure that you have Vim 7.3.584+, type the following in
|
||||
After you have made sure that you have Vim 7.3.598+, type the following in
|
||||
Vim: `:echo has('python')`. The output should be 1. If it's 0, then get a
|
||||
version of Vim with Python support.
|
||||
|
||||
@ -1364,6 +1364,23 @@ Default: `0`
|
||||
|
||||
let g:ycm_csharp_server_port = 0
|
||||
|
||||
### The `g:ycm_csharp_insert_namespace_expr` option
|
||||
|
||||
By default, when YCM inserts a namespace, it will insert the `using` statement
|
||||
under the nearest `using` statement. You may prefer that the `using` statement is
|
||||
inserted somewhere, for example, to preserve sorting. If so, you can set this
|
||||
option to override this behaviour.
|
||||
|
||||
When this option is set, instead of inserting the `using` statement itself, YCM
|
||||
will 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 option's
|
||||
expression is responsible for inserting the namespace - the default insertion
|
||||
will not occur.
|
||||
|
||||
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
|
||||
|
@ -84,6 +84,7 @@ function! youcompleteme#Enable()
|
||||
autocmd InsertLeave * call s:OnInsertLeave()
|
||||
autocmd InsertEnter * call s:OnInsertEnter()
|
||||
autocmd VimLeave * call s:OnVimLeave()
|
||||
autocmd CompleteDone * call s:OnCompleteDone()
|
||||
augroup END
|
||||
|
||||
" Calling these once solves the problem of BufReadPre/BufRead/BufEnter not
|
||||
@ -359,6 +360,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
|
||||
|
||||
|
@ -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|
|
||||
@ -311,7 +312,7 @@ Ubuntu Linux x64 super-quick installation ~
|
||||
Please refer to the full Installation Guide below; the following commands are
|
||||
provided on a best-effort basis and may not work for you.
|
||||
|
||||
Make sure you have Vim 7.3.584 with python2 support. Ubuntu 14.04 and later
|
||||
Make sure you have Vim 7.3.598 with python2 support. Ubuntu 14.04 and later
|
||||
have a Vim that's recent enough. You can see the version of Vim installed by
|
||||
running 'vim --version'. If the version is too old, you may need to compile Vim
|
||||
from source [16] (don't worry, it's easy).
|
||||
@ -368,7 +369,7 @@ Please refer to the full Installation Guide below; the following commands are
|
||||
provided on a best-effort basis and may not work for you. OpenBSD / FreeBSD are
|
||||
not officially supported platforms by YCM.
|
||||
|
||||
Make sure you have Vim 7.3.584 with python2 support.
|
||||
Make sure you have Vim 7.3.598 with python2 support.
|
||||
|
||||
OpenBSD 5.5 and later have a Vim that's recent enough. You can see the version
|
||||
of Vim installed by running 'vim --version'.
|
||||
@ -429,19 +430,19 @@ process.
|
||||
|
||||
**Please follow the instructions carefully. Read EVERY WORD.**
|
||||
|
||||
1. **Ensure that your version of Vim is _at least_ 7.3.584 _and_ that it has
|
||||
1. **Ensure that your version of Vim is _at least_ 7.3.598 _and_ that it has
|
||||
support for python2 scripting**.
|
||||
|
||||
Inside Vim, type ':version'. Look at the first two to three lines of
|
||||
output; it should say 'Vi IMproved X.Y', where X.Y is the major version
|
||||
of vim. If your version is greater than 7.3, then you're all set. If your
|
||||
version is 7.3 then look below that where it says, 'Included patches:
|
||||
1-Z', where Z will be some number. That number needs to be 584 or higher.
|
||||
1-Z', where Z will be some number. That number needs to be 598 or higher.
|
||||
|
||||
If your version of Vim is not recent enough, you may need to compile Vim
|
||||
from source [16] (don't worry, it's easy).
|
||||
|
||||
After you have made sure that you have Vim 7.3.584+, type the following
|
||||
After you have made sure that you have Vim 7.3.598+, type the following
|
||||
in Vim: ":echo has('python')". The output should be 1. If it's 0, then
|
||||
get a version of Vim with Python support.
|
||||
|
||||
@ -1580,6 +1581,24 @@ Default: '0'
|
||||
let g:ycm_csharp_server_port = 0
|
||||
<
|
||||
-------------------------------------------------------------------------------
|
||||
The *g:ycm_csharp_insert_namespace_expr* option
|
||||
|
||||
By default, when YCM inserts a namespace, it will insert the 'using' statement
|
||||
under the nearest 'using' statement. You may prefer that the 'using' statement
|
||||
is inserted somewhere, for example, to preserve sorting. If so, you can set
|
||||
this option to override this behaviour.
|
||||
|
||||
When this option is set, instead of inserting the 'using' statement itself, YCM
|
||||
will 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 option's
|
||||
expression is responsible for inserting the namespace - the default insertion
|
||||
will not occur.
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -41,7 +41,7 @@ class CompletionRequest( BaseRequest ):
|
||||
return self._response_future.done()
|
||||
|
||||
|
||||
def Response( self ):
|
||||
def RawResponse( self ):
|
||||
if not self._response_future:
|
||||
return []
|
||||
try:
|
||||
@ -51,14 +51,17 @@ 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 _ConvertCompletionDataToVimData( completion_data ):
|
||||
def Response( self ):
|
||||
return _ConvertCompletionDatasToVimDatas( self.RawResponse() )
|
||||
|
||||
|
||||
def ConvertCompletionDataToVimData( completion_data ):
|
||||
# see :h complete-items for a description of the dictionary fields
|
||||
vim_data = {
|
||||
'word' : ToUtf8IfNeeded( completion_data[ 'insertion_text' ] ),
|
||||
@ -89,6 +92,6 @@ def _ConvertCompletionDataToVimData( completion_data ):
|
||||
return vim_data
|
||||
|
||||
|
||||
def _ConvertCompletionResponseToVimDatas( response_data ):
|
||||
return [ _ConvertCompletionDataToVimData( x )
|
||||
for x in response_data[ 'completions' ] ]
|
||||
def _ConvertCompletionDatasToVimDatas( response_data ):
|
||||
return [ ConvertCompletionDataToVimData( x )
|
||||
for x in response_data ]
|
||||
|
@ -29,7 +29,7 @@ class ConvertCompletionResponseToVimDatas_test:
|
||||
completion_request._ConvertCompletionResponseToVimDatas method """
|
||||
|
||||
def _Check( self, completion_data, expected_vim_data ):
|
||||
vim_data = completion_request._ConvertCompletionDataToVimData(
|
||||
vim_data = completion_request.ConvertCompletionDataToVimData(
|
||||
completion_data )
|
||||
|
||||
try:
|
||||
|
393
python/ycm/tests/postcomplete_tests.py
Normal file
393
python/ycm/tests/postcomplete_tests.py
Normal file
@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 YouCompleteMe contributors
|
||||
#
|
||||
# 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, DEFAULT )
|
||||
from nose.tools import eq_
|
||||
from hamcrest import assert_that, empty
|
||||
from ycm import vimsupport
|
||||
from ycm.youcompleteme import YouCompleteMe
|
||||
|
||||
def GetCompleteDoneHooks_ResultOnCsharp_test():
|
||||
vimsupport.CurrentFiletypes = MagicMock( return_value = [ "cs" ] )
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
result = ycm_state.GetCompleteDoneHooks()
|
||||
eq_( 1, len( list( result ) ) )
|
||||
|
||||
|
||||
def GetCompleteDoneHooks_EmptyOnOtherFiletype_test():
|
||||
vimsupport.CurrentFiletypes = MagicMock( return_value = [ "txt" ] )
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
result = ycm_state.GetCompleteDoneHooks()
|
||||
eq_( 0, len( list( result ) ) )
|
||||
|
||||
|
||||
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 ) )
|
||||
|
||||
ycm_state.OnCompleteDone()
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_NewVim_MatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
eq_( list( result ), completions )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_NewVim_ShortTextDoesntRaise_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "A" )
|
||||
completions = [ _BuildCompletion( "AAA" ) ]
|
||||
|
||||
ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_NewVim_ExactMatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
eq_( list( result ), completions )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_NewVim_NonMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( " Quote" )
|
||||
completions = [ _BuildCompletion( "A" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
assert_that( list( result ), empty() )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_OldVim_MatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
eq_( list( result ), completions )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_OldVim_ShortTextDoesntRaise_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = "X" )
|
||||
completions = [ _BuildCompletion( "AAA" ) ]
|
||||
|
||||
ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_OldVim_ExactMatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
eq_( list( result ), completions )
|
||||
|
||||
|
||||
def FilterToCompletedCompletions_OldVim_NonMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Quote" )
|
||||
completions = [ _BuildCompletion( "A" ) ]
|
||||
|
||||
result = ycm_state._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
assert_that( list( result ), empty() )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_OldVim_MatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Te" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
eq_( result, True )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_OldVim_ShortTextDoesntRaise_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = "X" )
|
||||
completions = [ _BuildCompletion( "AAA" ) ]
|
||||
|
||||
ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_OldVim_ExactMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = "Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
eq_( result, False )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_OldVim_NonMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Quote" )
|
||||
completions = [ _BuildCompletion( "A" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
eq_( result, False )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_NewVim_MatchIsReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Te" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
eq_( result, True )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_NewVim_ShortTextDoesntRaise_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "X" )
|
||||
completions = [ _BuildCompletion( "AAA" ) ]
|
||||
|
||||
ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_NewVim_ExactMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Test" )
|
||||
completions = [ _BuildCompletion( "Test" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
eq_( result, False )
|
||||
|
||||
|
||||
def HasCompletionsThatCouldBeCompletedWithMoreText_NewVim_NonMatchIsntReturned_test():
|
||||
ycm_state = YouCompleteMe( MagicMock( spec_set = dict ) )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( " Quote" )
|
||||
completions = [ _BuildCompletion( "A" ) ]
|
||||
|
||||
result = ycm_state._HasCompletionsThatCouldBeCompletedWithMoreText( 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 GetCompletionsUserMayHaveCompleted_ReturnEmptyIfNotDone_test():
|
||||
ycm_state = _SetupForCsharpCompletionDone( [] )
|
||||
ycm_state._latest_completion_request.Done = MagicMock( return_value = False )
|
||||
|
||||
eq_( [], ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnEmptyIfPendingMatches_NewVim_test():
|
||||
completions = [ _BuildCompletion( None ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Te" )
|
||||
|
||||
eq_( [], ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnEmptyIfPendingMatches_OldVim_test():
|
||||
completions = [ _BuildCompletion( None ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Te" )
|
||||
|
||||
eq_( [], ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnMatchIfExactMatches_NewVim_test():
|
||||
info = [ "NS","Test", "Abbr", "Menu", "Info", "Kind" ]
|
||||
completions = [ _BuildCompletion( *info ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( *info[ 1 : ] )
|
||||
|
||||
eq_( completions, ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnMatchIfExactMatchesEvenIfPartial_NewVim_test():
|
||||
info = [ "NS", "Test", "Abbr", "Menu", "Info", "Kind" ]
|
||||
completions = [ _BuildCompletion( *info ),
|
||||
_BuildCompletion( insertion_text = "TestTest" ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( *info[ 1 : ] )
|
||||
|
||||
eq_( [ completions[ 0 ] ], ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_DontReturnMatchIfNontExactMatchesAndPartial_NewVim_test():
|
||||
info = [ "NS", "Test", "Abbr", "Menu", "Info", "Kind" ]
|
||||
completions = [ _BuildCompletion( insertion_text = info[ 0 ] ),
|
||||
_BuildCompletion( insertion_text = "TestTest" ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( *info[ 1 : ] )
|
||||
|
||||
eq_( [], ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnMatchIfMatches_NewVim_test():
|
||||
completions = [ _BuildCompletion( None ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = True )
|
||||
vimsupport.GetVariableValue = GetVariableValue_CompleteItemIs( "Test" )
|
||||
|
||||
eq_( completions, ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def GetCompletionsUserMayHaveCompleted_ReturnMatchIfMatches_OldVim_test():
|
||||
completions = [ _BuildCompletion( None ) ]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.VimVersionAtLeast = MagicMock( return_value = False )
|
||||
vimsupport.TextBeforeCursor = MagicMock( return_value = " Test" )
|
||||
|
||||
eq_( completions, ycm_state.GetCompletionsUserMayHaveCompleted() )
|
||||
|
||||
|
||||
def PostCompleteCsharp_EmptyDoesntInsertNamespace_test():
|
||||
ycm_state = _SetupForCsharpCompletionDone( [] )
|
||||
|
||||
ycm_state._OnCompleteDone_Csharp()
|
||||
|
||||
assert not vimsupport.InsertNamespace.called
|
||||
|
||||
|
||||
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 = _SetupForCsharpCompletionDone( completions )
|
||||
|
||||
ycm_state._OnCompleteDone_Csharp()
|
||||
|
||||
vimsupport.InsertNamespace.assert_called_once_with( namespace )
|
||||
|
||||
def PostCompleteCsharp_InsertSecondNamespaceIfSelected_test():
|
||||
namespace = "A_NAMESPACE"
|
||||
namespace2 = "ANOTHER_NAMESPACE"
|
||||
completions = [
|
||||
_BuildCompletion( namespace ),
|
||||
_BuildCompletion( namespace2 ),
|
||||
]
|
||||
ycm_state = _SetupForCsharpCompletionDone( completions )
|
||||
vimsupport.PresentDialog = MagicMock( return_value = 1 )
|
||||
|
||||
ycm_state._OnCompleteDone_Csharp()
|
||||
|
||||
vimsupport.InsertNamespace.assert_called_once_with( namespace2 )
|
||||
|
||||
|
||||
def _SetupForCsharpCompletionDone( completions ):
|
||||
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 = None, insertion_text = 'Test',
|
||||
menu_text = None, extra_menu_info = None,
|
||||
detailed_info = None, kind = None ):
|
||||
return {
|
||||
'extra_data': { 'required_namespace_import' : namespace },
|
||||
'insertion_text': insertion_text,
|
||||
'menu_text': menu_text,
|
||||
'extra_menu_info': extra_menu_info,
|
||||
'kind': kind,
|
||||
'detailed_info': detailed_info,
|
||||
}
|
||||
|
||||
|
||||
def GetVariableValue_CompleteItemIs( word, abbr = None, menu = None,
|
||||
info = None, kind = None ):
|
||||
def Result( variable ):
|
||||
if variable == 'v:completed_item':
|
||||
return {
|
||||
'word': word,
|
||||
'abbr': abbr,
|
||||
'menu': menu,
|
||||
'info': info,
|
||||
'kind': kind,
|
||||
}
|
||||
else:
|
||||
return DEFAULT
|
||||
return MagicMock( side_effect = Result )
|
@ -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
|
||||
|
||||
@ -60,14 +61,20 @@ def TextAfterCursor():
|
||||
return vim.current.line[ CurrentColumn(): ]
|
||||
|
||||
|
||||
def TextBeforeCursor():
|
||||
"""Returns the text before CurrentColumn."""
|
||||
return vim.current.line[ :CurrentColumn() ]
|
||||
|
||||
|
||||
# Expects version_string in 'MAJOR.MINOR.PATCH' format, e.g. '7.4.301'
|
||||
def VimVersionAtLeast( version_string ):
|
||||
major, minor, patch = [ int( x ) for x in version_string.split( '.' ) ]
|
||||
|
||||
# For Vim 7.4.301, v:version is '704'
|
||||
actual_major_and_minor = GetIntValue( 'v:version' )
|
||||
if actual_major_and_minor != major * 100 + minor:
|
||||
return False
|
||||
matching_major_and_minor = major * 100 + minor
|
||||
if actual_major_and_minor != matching_major_and_minor:
|
||||
return actual_major_and_minor > matching_major_and_minor
|
||||
|
||||
return GetBoolValue( 'has("patch{0}")'.format( patch ) )
|
||||
|
||||
@ -447,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 )
|
||||
|
||||
@ -534,3 +549,29 @@ 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_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 ):
|
||||
return GetIntValue( "search('{0}', 'Wcnb')".format( EscapeForVim( pattern )))
|
||||
|
||||
|
||||
def LineTextInCurrentBuffer( line ):
|
||||
return vim.current.buffer[ line ]
|
||||
|
@ -21,6 +21,7 @@ import os
|
||||
import vim
|
||||
import tempfile
|
||||
import json
|
||||
import re
|
||||
import signal
|
||||
import base64
|
||||
from subprocess import PIPE
|
||||
@ -34,7 +35,8 @@ from ycm.client.ycmd_keepalive import YcmdKeepalive
|
||||
from ycm.client.base_request import BaseRequest, BuildRequestData
|
||||
from ycm.client.completer_available_request import SendCompleterAvailableRequest
|
||||
from ycm.client.command_request import SendCommandRequest
|
||||
from ycm.client.completion_request import CompletionRequest
|
||||
from ycm.client.completion_request import ( CompletionRequest,
|
||||
ConvertCompletionDataToVimData )
|
||||
from ycm.client.omni_completion_request import OmniCompletionRequest
|
||||
from ycm.client.event_notification import ( SendEventNotificationAsync,
|
||||
EventNotification )
|
||||
@ -96,6 +98,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 = {}
|
||||
@ -294,6 +299,159 @@ class YouCompleteMe( object ):
|
||||
SendEventNotificationAsync( 'CurrentIdentifierFinished' )
|
||||
|
||||
|
||||
def OnCompleteDone( self ):
|
||||
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 GetCompletionsUserMayHaveCompleted( self ):
|
||||
latest_completion_request = self.GetCurrentCompletionRequest()
|
||||
if not latest_completion_request or not latest_completion_request.Done():
|
||||
return []
|
||||
|
||||
completions = latest_completion_request.RawResponse()
|
||||
|
||||
result = self._FilterToMatchingCompletions( completions, True )
|
||||
result = list( result )
|
||||
if result:
|
||||
return result
|
||||
|
||||
if self._HasCompletionsThatCouldBeCompletedWithMoreText( 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._FilterToMatchingCompletions( completions, False )
|
||||
|
||||
return list( result )
|
||||
|
||||
|
||||
def _FilterToMatchingCompletions( self, completions, full_match_only ):
|
||||
self._PatchBasedOnVimVersion()
|
||||
return self._FilterToMatchingCompletions( completions, full_match_only)
|
||||
|
||||
|
||||
def _HasCompletionsThatCouldBeCompletedWithMoreText( self, completions ):
|
||||
self._PatchBasedOnVimVersion()
|
||||
return self._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
|
||||
|
||||
|
||||
def _PatchBasedOnVimVersion( self ):
|
||||
if vimsupport.VimVersionAtLeast( "7.4.774" ):
|
||||
self._HasCompletionsThatCouldBeCompletedWithMoreText = \
|
||||
self._HasCompletionsThatCouldBeCompletedWithMoreText_NewerVim
|
||||
self._FilterToMatchingCompletions = \
|
||||
self._FilterToMatchingCompletions_NewerVim
|
||||
else:
|
||||
self._FilterToMatchingCompletions = \
|
||||
self._FilterToMatchingCompletions_OlderVim
|
||||
self._HasCompletionsThatCouldBeCompletedWithMoreText = \
|
||||
self._HasCompletionsThatCouldBeCompletedWithMoreText_OlderVim
|
||||
|
||||
|
||||
def _FilterToMatchingCompletions_NewerVim( self, completions,
|
||||
full_match_only ):
|
||||
""" Filter to completions matching the item Vim said was completed """
|
||||
completed = vimsupport.GetVariableValue( 'v:completed_item' )
|
||||
for completion in completions:
|
||||
item = ConvertCompletionDataToVimData( completion )
|
||||
match_keys = ( [ "word", "abbr", "menu", "info" ] if full_match_only
|
||||
else [ 'word' ] )
|
||||
matcher = lambda key: completed.get( key, "" ) == item.get( key, "" )
|
||||
if all( [ matcher( i ) for i in match_keys ] ):
|
||||
yield completion
|
||||
|
||||
|
||||
def _FilterToMatchingCompletions_OlderVim( self, completions,
|
||||
full_match_only ):
|
||||
""" Filter to completions matching the buffer text """
|
||||
if full_match_only:
|
||||
return # Only supported in 7.4.774+
|
||||
# No support for multiple line completions
|
||||
text = vimsupport.TextBeforeCursor()
|
||||
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 _HasCompletionsThatCouldBeCompletedWithMoreText_NewerVim( self,
|
||||
completions ):
|
||||
completed_item = vimsupport.GetVariableValue( 'v:completed_item' )
|
||||
completed_word = completed_item[ 'word' ]
|
||||
if not completed_word:
|
||||
return False
|
||||
|
||||
# Sometime CompleteDone is called after the next character is inserted
|
||||
# If so, use inserted character to filter possible completions further
|
||||
text = vimsupport.TextBeforeCursor()
|
||||
reject_exact_match = True
|
||||
if text and text[ -1 ] != completed_word[ -1 ]:
|
||||
reject_exact_match = False
|
||||
completed_word += text[ -1 ]
|
||||
|
||||
for completion in completions:
|
||||
word = ConvertCompletionDataToVimData( completion )[ 'word' ]
|
||||
if reject_exact_match and word == completed_word:
|
||||
continue
|
||||
if word.startswith( completed_word ):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _HasCompletionsThatCouldBeCompletedWithMoreText_OlderVim( self,
|
||||
completions ):
|
||||
# No support for multiple line completions
|
||||
text = vimsupport.TextBeforeCursor()
|
||||
for completion in completions:
|
||||
word = ConvertCompletionDataToVimData( completion )[ 'word' ]
|
||||
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.GetCompletionsUserMayHaveCompleted()
|
||||
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 _GetRequiredNamespaceImport( self, completion ):
|
||||
if ( "extra_data" not in completion
|
||||
or "required_namespace_import" not in completion[ "extra_data" ] ):
|
||||
return None
|
||||
return completion[ "extra_data" ][ "required_namespace_import" ]
|
||||
|
||||
|
||||
def DiagnosticsForCurrentFileReady( self ):
|
||||
return bool( self._latest_file_parse_request and
|
||||
self._latest_file_parse_request.Done() )
|
||||
@ -418,5 +576,3 @@ def _AddUltiSnipsDataIfNeeded( extra_data ):
|
||||
extra_data[ 'ultisnips_snippets' ] = [ { 'trigger': x.trigger,
|
||||
'description': x.description
|
||||
} for x in rawsnips ]
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user