Close #1753 - Implement minimum viable integration with Deoplete

This commit is contained in:
w0rp 2019-04-23 21:26:16 +01:00
parent ce0b14979e
commit 01331266a8
No known key found for this signature in database
GPG Key ID: 0FC1ECAA8C81CD83
10 changed files with 354 additions and 71 deletions

2
.gitignore vendored
View File

@ -3,6 +3,8 @@
# Ignore all hidden files everywhere.
# Use `git add -f` to add hidden files.
.*
__pycache__
*.pyc
/doc/tags
/init.vim
/test/ale-info-test-file

View File

@ -26,7 +26,7 @@ features, including:
* Diagnostics (via Language Server Protocol linters)
* Go To Definition (`:ALEGoToDefinition`)
* Completion (`let g:ale_completion_enabled = 1` before ALE is loaded)
* Completion (Built in completion support, or with Deoplete)
* Finding references (`:ALEFindReferences`)
* Hover information (`:ALEHover`)
* Symbol search (`:ALESymbolSearch`)
@ -159,6 +159,18 @@ ALE offers some support for completion via hijacking of omnicompletion while you
type. All of ALE's completion information must come from Language Server
Protocol linters, or from `tsserver` for TypeScript.
ALE integrates with [Deoplete](https://github.com/Shougo/deoplete.nvim) as a
completion source, named `'ale'`. You can configure Deoplete to only use ALE as
the source of completion information, or mix it with other sources.
```vim
" Use ALE and also some plugin 'foobar' as completion sources for all code.
let g:deoplete#sources = {'_': ['ale', 'foobar']}
```
ALE also offers its own automatic completion support, which does not require any
other plugins, and can be enabled by changing a setting before ALE is loaded.
```vim
" Enable completion where available.
" This setting must be set before ALE is loaded.

View File

@ -159,18 +159,20 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort
endfunction
function! s:ReplaceCompletionOptions() abort
" Remember the old omnifunc value, if there is one.
" If we don't store an old one, we'll just never reset the option.
" This will stop some random exceptions from appearing.
if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc)
let b:ale_old_omnifunc = &l:omnifunc
let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '')
if l:source is# 'ale-automatic' || l:source is# 'ale-manual'
" Remember the old omnifunc value, if there is one.
" If we don't store an old one, we'll just never reset the option.
" This will stop some random exceptions from appearing.
if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc)
let b:ale_old_omnifunc = &l:omnifunc
endif
let &l:omnifunc = 'ale#completion#OmniFunc'
endif
let &l:omnifunc = 'ale#completion#OmniFunc'
let l:info = get(b:, 'ale_completion_info', {})
if !get(l:info, 'manual')
if l:source is# 'ale-automatic'
if !exists('b:ale_old_completeopt')
let b:ale_old_completeopt = &l:completeopt
endif
@ -199,31 +201,49 @@ function! ale#completion#RestoreCompletionOptions() abort
endif
endfunction
function! ale#completion#GetCompletionPosition() abort
if !exists('b:ale_completion_info')
return 0
endif
let l:line = b:ale_completion_info.line
let l:column = b:ale_completion_info.column
let l:regex = s:GetFiletypeValue(s:omni_start_map, &filetype)
let l:up_to_column = getline(l:line)[: l:column - 2]
let l:match = matchstr(l:up_to_column, l:regex)
return l:column - len(l:match) - 1
endfunction
function! ale#completion#GetCompletionResult() abort
" Parse a new response if there is one.
if exists('b:ale_completion_response')
\&& exists('b:ale_completion_parser')
let l:response = b:ale_completion_response
let l:parser = b:ale_completion_parser
unlet b:ale_completion_response
unlet b:ale_completion_parser
let b:ale_completion_result = function(l:parser)(l:response)
endif
if exists('b:ale_completion_result')
return b:ale_completion_result
endif
return v:null
endfunction
function! ale#completion#OmniFunc(findstart, base) abort
if a:findstart
let l:line = b:ale_completion_info.line
let l:column = b:ale_completion_info.column
let l:regex = s:GetFiletypeValue(s:omni_start_map, &filetype)
let l:up_to_column = getline(l:line)[: l:column - 2]
let l:match = matchstr(l:up_to_column, l:regex)
return l:column - len(l:match) - 1
return ale#completion#GetCompletionPosition()
else
" Parse a new response if there is one.
if exists('b:ale_completion_response')
\&& exists('b:ale_completion_parser')
let l:response = b:ale_completion_response
let l:parser = b:ale_completion_parser
unlet b:ale_completion_response
unlet b:ale_completion_parser
let b:ale_completion_result = function(l:parser)(l:response)
endif
let l:result = ale#completion#GetCompletionResult()
call s:ReplaceCompletionOptions()
return get(b:, 'ale_completion_result', [])
return l:result isnot v:null ? l:result : []
endif
endfunction
@ -239,7 +259,14 @@ function! ale#completion#Show(response, completion_parser) abort
" Replace completion options shortly before opening the menu.
call s:ReplaceCompletionOptions()
call timer_start(0, {-> ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)")})
let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '')
if l:source is# 'ale-automatic' || l:source is# 'ale-manual'
call timer_start(
\ 0,
\ {-> ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)")}
\)
endif
endfunction
function! s:CompletionStillValid(request_id) abort
@ -249,7 +276,10 @@ function! s:CompletionStillValid(request_id) abort
\&& has_key(b:, 'ale_completion_info')
\&& b:ale_completion_info.request_id == a:request_id
\&& b:ale_completion_info.line == l:line
\&& b:ale_completion_info.column == l:column
\&& (
\ b:ale_completion_info.column == l:column
\ || b:ale_completion_info.source is# 'deoplete'
\)
endfunction
function! ale#completion#ParseTSServerCompletions(response) abort
@ -519,12 +549,12 @@ endfunction
" This function can be used to manually trigger autocomplete, even when
" g:ale_completion_enabled is set to false
function! ale#completion#GetCompletions(manual) abort
function! ale#completion#GetCompletions(source) abort
let [l:line, l:column] = getpos('.')[1:2]
let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column)
if !a:manual && empty(l:prefix)
if a:source is# 'ale-automatic' && empty(l:prefix)
return
endif
@ -537,8 +567,9 @@ function! ale#completion#GetCompletions(manual) abort
\ 'prefix': l:prefix,
\ 'conn_id': 0,
\ 'request_id': 0,
\ 'manual': a:manual,
\ 'source': a:source,
\}
unlet! b:ale_completion_result
let l:buffer = bufnr('')
let l:Callback = function('s:OnReady')
@ -562,7 +593,7 @@ function! s:TimerHandler(...) abort
" When running the timer callback, we have to be sure that the cursor
" hasn't moved from where it was when we requested completions by typing.
if s:timer_pos == [l:line, l:column] && ale#util#Mode() is# 'i'
call ale#completion#GetCompletions(0)
call ale#completion#GetCompletions('ale-automatic')
endif
endfunction

View File

@ -321,40 +321,44 @@ servers. LSP linters can be used in combination with any other linter, and
will automatically connect to LSP servers when needed. ALE also supports
`tsserver` for TypeScript, which uses a different but very similar protocol.
ALE supports the following LSP/tsserver features:
1. Diagnostics/linting - Enabled via selecting linters as usual.
2. Completion
3. Go to definition
-------------------------------------------------------------------------------
5.1 Completion *ale-completion*
ALE offers limited support for automatic completion of code while you type.
ALE offers support for automatic completion of code while you type.
Completion is only supported while at least one LSP linter is enabled. ALE
will only suggest symbols provided by the LSP servers.
Suggestions will be made while you type after completion is enabled.
Completion can be enabled by setting |g:ale_completion_enabled| to `1`. This
setting must be set to `1` before ALE is loaded. The delay for completion can
be configured with |g:ale_completion_delay|. ALE will only suggest so many
possible matches for completion. The maximum number of items can be controlled
with |g:ale_completion_max_suggestions|.
*ale-deoplete-integration*
ALE integrates with Deoplete for offering automatic completion data. ALE's
completion source for Deoplete is named `'ale'`, and should enabled
automatically if Deoplete is enabled and configured correctly. Deoplete
integration should not be combined with ALE's own implementation.
ALE also offers its own completion implementation, which does not require any
other plugins. Suggestions will be made while you type after completion is
enabled. Completion can be enabled by setting |g:ale_completion_enabled| to
`1`. This setting must be set to `1` before ALE is loaded. The delay for
completion can be configured with |g:ale_completion_delay|.
ALE will only suggest so many possible matches for completion. The maximum
number of items can be controlled with |g:ale_completion_max_suggestions|.
If you don't like some of the suggestions you see, you can filter them out
with |g:ale_completion_excluded_words| or |b:ale_completion_excluded_words|.
The |ALEComplete| command can be used to show completion suggestions manually,
even when |g:ale_completion_enabled| is set to `0`.
even when |g:ale_completion_enabled| is set to `0`. For manually requesting
completion information with Deoplete, consult Deoplete's documentation.
*ale-completion-completeopt-bug*
Automatic completion replaces |completeopt| before opening the omnicomplete
menu with <C-x><C-o>. In some versions of Vim, the value set for the option
will not be respected. If you experience issues with Vim automatically
inserting text while you type, set the following option in vimrc, and your
issues should go away. >
ALE Automatic completion implementation replaces |completeopt| before opening
the omnicomplete menu with <C-x><C-o>. In some versions of Vim, the value set
for the option will not be respected. If you experience issues with Vim
automatically inserting text while you type, set the following option in
vimrc, and your issues should go away. >
set completeopt=menu,menuone,preview,noselect,noinsert
<

View File

@ -216,7 +216,7 @@ command! -bar ALEDocumentation :call ale#hover#ShowDocumentationAtCursor()
" Search for appearances of a symbol, such as a type name or function name.
command! -nargs=1 ALESymbolSearch :call ale#symbol#Search(<q-args>)
command! -bar ALEComplete :call ale#completion#GetCompletions(1)
command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual')
" <Plug> mappings for commands
nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return>

View File

@ -0,0 +1,50 @@
"""
A Deoplete source for ALE completion via tsserver and LSP.
"""
__author__ = 'Joao Paulo, w0rp'
try:
from deoplete.source.base import Base
except ImportError:
# Mock the Base class if deoplete isn't available, as mock isn't available
# in the Docker image.
class Base(object):
def __init__(self, vim):
pass
# Make sure this code is valid in Python 2, used for running unit tests.
class Source(Base):
def __init__(self, vim):
super(Source, self).__init__(vim)
self.name = 'ale'
self.mark = '[L]'
self.rank = 100
self.is_bytepos = True
self.min_pattern_length = 1
# Returns an integer for the start position, as with omnifunc.
def get_completion_position(self):
return self.vim.call('ale#completion#GetCompletionPosition')
def gather_candidates(self, context):
if context.get('is_refresh'):
context['is_async'] = False
if context['is_async']:
# Result is the same as for omnifunc, or None.
result = self.vim.call('ale#completion#GetCompletionResult')
if result is not None:
context['is_async'] = False
return result
else:
context['is_async'] = True
# Request some completion results.
self.vim.call('ale#completion#GetCompletions', 'deoplete')
return []

View File

@ -27,7 +27,7 @@ Before:
let g:get_completions_called = 0
" We just want to check if the function is called.
function! ale#completion#GetCompletions(manual)
function! ale#completion#GetCompletions(source)
let g:get_completions_called = 1
endfunction
@ -57,6 +57,7 @@ After:
unlet! b:ale_completion_info
unlet! b:ale_completion_response
unlet! b:ale_completion_parser
unlet! b:ale_completion_result
unlet! b:ale_complete_done_time
delfunction CheckCompletionCalled
@ -86,7 +87,7 @@ Execute(ale#completion#GetCompletions should not be called when the cursor posit
call setpos('.', [bufnr(''), 1, 2, 0])
" We just want to check if the function is called.
function! ale#completion#GetCompletions(manual)
function! ale#completion#GetCompletions(source)
let g:get_completions_called = 1
endfunction
@ -105,7 +106,7 @@ Execute(ale#completion#GetCompletions should not be called if you switch to norm
let g:fake_mode = 'n'
" We just want to check if the function is called.
function! ale#completion#GetCompletions(manual)
function! ale#completion#GetCompletions(source)
let g:get_completions_called = 1
endfunction
@ -124,6 +125,7 @@ Execute(Completion should not be done shortly after the CompleteDone function):
Execute(ale#completion#Show() should remember the omnifunc setting and replace it):
let &l:omnifunc = 'FooBar'
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#Show('Response', 'Parser')
AssertEqual 'FooBar', b:ale_old_omnifunc
@ -136,6 +138,7 @@ Execute(ale#completion#Show() should remember the omnifunc setting and replace i
Execute(ale#completion#Show() should remember the completeopt setting and replace it):
let &l:completeopt = 'menu'
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#Show('Response', 'Parser')
AssertEqual 'menu', b:ale_old_completeopt
@ -148,6 +151,7 @@ Execute(ale#completion#Show() should remember the completeopt setting and replac
Execute(ale#completion#Show() should set the preview option if it's set):
let &l:completeopt = 'menu,preview'
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#Show('Response', 'Parser')
AssertEqual 'menu,preview', b:ale_old_completeopt
@ -158,7 +162,7 @@ Execute(ale#completion#Show() should set the preview option if it's set):
AssertEqual [["\<Plug>(ale_show_completion_menu)"]], g:feedkeys_calls
Execute(ale#completion#Show() should not replace the completeopt setting for manual completion):
let b:ale_completion_info = {'manual': 1}
let b:ale_completion_info = {'source': 'ale-manual'}
let &l:completeopt = 'menu,preview'
@ -173,6 +177,7 @@ Execute(ale#completion#Show() should not replace the completeopt setting for man
Execute(ale#completion#OmniFunc() should also remember the completeopt setting and replace it):
let &l:completeopt = 'menu'
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#OmniFunc(0, '')
AssertEqual 'menu', b:ale_old_completeopt
@ -181,18 +186,35 @@ Execute(ale#completion#OmniFunc() should also remember the completeopt setting a
Execute(ale#completion#OmniFunc() should set the preview option if it's set):
let &l:completeopt = 'menu,preview'
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#OmniFunc(0, '')
AssertEqual 'menu,preview', b:ale_old_completeopt
AssertEqual 'menu,menuone,preview,noselect,noinsert', &l:completeopt
Execute(ale#completion#Show() should make the correct feedkeys() call):
Execute(ale#completion#Show() should make the correct feedkeys() call for automatic completion):
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#Show('Response', 'Parser')
AssertEqual [], g:feedkeys_calls
sleep 1ms
AssertEqual [["\<Plug>(ale_show_completion_menu)"]], g:feedkeys_calls
Execute(ale#completion#Show() should make the correct feedkeys() call for manual completion):
let b:ale_completion_info = {'source': 'ale-automatic'}
call ale#completion#Show('Response', 'Parser')
AssertEqual [], g:feedkeys_calls
sleep 1ms
AssertEqual [["\<Plug>(ale_show_completion_menu)"]], g:feedkeys_calls
Execute(ale#completion#Show() should not call feedkeys() for other sources):
let b:ale_completion_info = {'source': 'deoplete'}
call ale#completion#Show('Response', 'Parser')
sleep 1ms
AssertEqual [], g:feedkeys_calls
Execute(ale#completion#Show() shouldn't do anything if you switch back to normal mode):
let &l:completeopt = 'menu,preview'
let g:fake_mode = 'n'
@ -247,9 +269,10 @@ Execute(The completion request_id should be reset when queuing again):
AssertEqual 0, b:ale_completion_info.request_id
Execute(b:ale_completion_info should be set up correctly when requesting completions):
Execute(b:ale_completion_info should be set up correctly when requesting completions automatically):
let b:ale_completion_result = []
call setpos('.', [bufnr(''), 3, 14, 0])
call ale#completion#GetCompletions(0)
call ale#completion#GetCompletions('ale-automatic')
AssertEqual
\ {
@ -259,11 +282,13 @@ Execute(b:ale_completion_info should be set up correctly when requesting complet
\ 'line_length': 14,
\ 'line': 3,
\ 'prefix': 'ab',
\ 'manual': 0,
\ 'source': 'ale-automatic',
\ },
\ b:ale_completion_info
Assert !exists('b:ale_completion_result')
Execute(b:ale_completion_info should be set up correctly when requesting completions):
Execute(b:ale_completion_info should be set up correctly when requesting completions manually):
let b:ale_completion_result = []
call setpos('.', [bufnr(''), 3, 14, 0])
ALEComplete
@ -275,9 +300,28 @@ Execute(b:ale_completion_info should be set up correctly when requesting complet
\ 'line_length': 14,
\ 'line': 3,
\ 'prefix': 'ab',
\ 'manual': 1,
\ 'source': 'ale-manual',
\ },
\ b:ale_completion_info
Assert !exists('b:ale_completion_result')
Execute(b:ale_completion_info should be set up correctly for other sources):
let b:ale_completion_result = []
call setpos('.', [bufnr(''), 3, 14, 0])
call ale#completion#GetCompletions('deoplete')
AssertEqual
\ {
\ 'request_id': 0,
\ 'conn_id': 0,
\ 'column': 14,
\ 'line_length': 14,
\ 'line': 3,
\ 'prefix': 'ab',
\ 'source': 'deoplete',
\ },
\ b:ale_completion_info
Assert !exists('b:ale_completion_result')
Execute(The correct keybinds should be configured):
redir => g:output

View File

@ -102,7 +102,7 @@ Execute(The right message should be sent for the initial tsserver request):
" The cursor position needs to match what was saved before.
call setpos('.', [bufnr(''), 1, 3, 0])
call ale#completion#GetCompletions(0)
call ale#completion#GetCompletions('ale-automatic')
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
@ -129,7 +129,7 @@ Execute(The right message should be sent for the initial tsserver request):
\ 'request_id': 1,
\ 'line': 1,
\ 'prefix': 'fo',
\ 'manual': 0,
\ 'source': 'ale-automatic',
\ },
\ get(b:, 'ale_completion_info', {})
@ -191,7 +191,7 @@ Execute(The right message should be sent for the initial LSP request):
" The cursor position needs to match what was saved before.
call setpos('.', [bufnr(''), 1, 5, 0])
call ale#completion#GetCompletions(0)
call ale#completion#GetCompletions('ale-automatic')
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)
@ -234,7 +234,7 @@ Execute(The right message should be sent for the initial LSP request):
\ 'request_id': 1,
\ 'line': 1,
\ 'prefix': 'fo',
\ 'manual': 0,
\ 'source': 'ale-automatic',
\ 'completion_filter': 'ale#completion#python#CompletionItemFilter',
\ },
\ get(b:, 'ale_completion_info', {})
@ -260,7 +260,7 @@ Execute(Two completion requests shouldn't be sent in a row):
" The cursor position needs to match what was saved before.
call setpos('.', [bufnr(''), 1, 5, 0])
call ale#completion#GetCompletions(0)
call ale#completion#GetCompletions('ale-automatic')
" We shouldn't register the callback yet.
AssertEqual '''''', string(g:Callback)

View File

@ -0,0 +1,130 @@
import unittest
import imp
ale_module = imp.load_source(
'deoplete.sources.ale',
'/testplugin/rplugin/python3/deoplete/sources/ale.py',
)
class VimMock(object):
def __init__(self, call_list, call_results):
self.__call_list = call_list
self.__call_results = call_results
def call(self, function, *args):
self.__call_list.append((function, args))
return self.__call_results.get(function, 0)
class DeopleteSourceTest(unittest.TestCase):
def setUp(self):
super(DeopleteSourceTest, self).setUp()
self.call_list = []
self.call_results = {}
self.source = ale_module.Source('vim')
self.source.vim = VimMock(self.call_list, self.call_results)
def test_attributes(self):
"""
Check all of the attributes we set.
"""
attributes = dict(
(key, getattr(self.source, key))
for key in
dir(self.source)
if not key.startswith('__')
and key != 'vim'
and not hasattr(getattr(self.source, key), '__self__')
)
self.assertEqual(attributes, {
'is_bytepos': True,
'mark': '[L]',
'min_pattern_length': 1,
'name': 'ale',
'rank': 100,
})
def test_completion_position(self):
self.call_results['ale#completion#GetCompletionPosition'] = 2
self.assertEqual(self.source.get_completion_position(), 2)
self.assertEqual(self.call_list, [
('ale#completion#GetCompletionPosition', ()),
])
def test_request_completion_results(self):
context = {'is_async': False}
self.assertEqual(self.source.gather_candidates(context), [])
self.assertEqual(context, {'is_async': True})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletions', ('deoplete',)),
])
def test_refresh_completion_results(self):
context = {'is_async': False}
self.assertEqual(self.source.gather_candidates(context), [])
self.assertEqual(context, {'is_async': True})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletions', ('deoplete',)),
])
context = {'is_async': True, 'is_refresh': True}
self.assertEqual(self.source.gather_candidates(context), [])
self.assertEqual(context, {'is_async': True, 'is_refresh': True})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletions', ('deoplete',)),
('ale#completion#GetCompletions', ('deoplete',)),
])
def test_poll_no_result(self):
context = {'is_async': True}
self.call_results['ale#completion#GetCompletionResult'] = None
self.assertEqual(self.source.gather_candidates(context), [])
self.assertEqual(context, {'is_async': True})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletionResult', ()),
])
def test_poll_empty_result_ready(self):
context = {'is_async': True}
self.call_results['ale#completion#GetCompletionResult'] = []
self.assertEqual(self.source.gather_candidates(context), [])
self.assertEqual(context, {'is_async': False})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletionResult', ()),
])
def test_poll_non_empty_result_ready(self):
context = {'is_async': True}
self.call_results['ale#completion#GetCompletionResult'] = [
{
'word': 'foobar',
'kind': 'v',
'icase': 1,
'menu': '',
'info': '',
},
]
self.assertEqual(self.source.gather_candidates(context), [
{
'word': 'foobar',
'kind': 'v',
'icase': 1,
'menu': '',
'info': '',
},
])
self.assertEqual(context, {'is_async': False})
self.assertEqual(self.call_list, [
('ale#completion#GetCompletionResult', ()),
])

View File

@ -67,4 +67,14 @@ echo
test/script/check-toc || exit_code=$?
echo '========================================'
echo 'Check Python code'
echo '========================================'
echo
docker run --rm -v "$PWD:/testplugin" "$DOCKER_RUN_IMAGE" \
python -W ignore -m unittest discover /testplugin/test/python \
|| exit_code=$?
echo
exit $exit_code