diff --git a/README.md b/README.md index 84439b71..76e9212e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ features, including: * Diagnostics (via Language Server Protocol linters) * Go To Definition (`:ALEGoToDefinition`) -* Completion (Built in completion support) +* Completion (Built in completion support, or with Deoplete) * Finding references (`:ALEFindReferences`) * Hover information (`:ALEHover`) * Symbol search (`:ALESymbolSearch`) @@ -159,12 +159,24 @@ 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 offers its own automatic completion support, which does not require any +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. +" +" You should not turn this setting on if you wish to use ALE as a completion +" source for other completion plugins, like Deoplete. let g:ale_completion_enabled = 1 ``` diff --git a/doc/ale.txt b/doc/ale.txt index ac3661fc..4bb34947 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -334,7 +334,14 @@ 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. -ALE offers its own completion implementation, which does not require any + *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. ALE's own completion implementation can be enabled by setting |g:ale_completion_enabled| to `1`. This setting must be set to `1` before ALE @@ -355,7 +362,8 @@ 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* diff --git a/rplugin/python3/deoplete/sources/ale.py b/rplugin/python3/deoplete/sources/ale.py new file mode 100644 index 00000000..7ed2f6c0 --- /dev/null +++ b/rplugin/python3/deoplete/sources/ale.py @@ -0,0 +1,54 @@ +""" +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): + # Stop early if ALE can't provide completion data for this buffer. + if not self.vim.call('ale#completion#CanProvideCompletions'): + return None + + 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 [] diff --git a/test/python/test_deoplete_source.py b/test/python/test_deoplete_source.py new file mode 100644 index 00000000..28eec5cd --- /dev/null +++ b/test/python/test_deoplete_source.py @@ -0,0 +1,147 @@ +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 = {'ale#completion#CanProvideCompletions': 1} + 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#CanProvideCompletions', ()), + ('ale#completion#GetCompletions', ('deoplete',)), + ]) + + def test_request_completion_results_from_buffer_without_providers(self): + self.call_results['ale#completion#CanProvideCompletions'] = 0 + context = {'is_async': False} + + self.assertIsNone(self.source.gather_candidates(context), []) + self.assertEqual(context, {'is_async': False}) + self.assertEqual(self.call_list, [ + ('ale#completion#CanProvideCompletions', ()), + ]) + + 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#CanProvideCompletions', ()), + ('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#CanProvideCompletions', ()), + ('ale#completion#GetCompletions', ('deoplete',)), + ('ale#completion#CanProvideCompletions', ()), + ('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#CanProvideCompletions', ()), + ('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#CanProvideCompletions', ()), + ('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#CanProvideCompletions', ()), + ('ale#completion#GetCompletionResult', ()), + ]) diff --git a/test/script/custom-checks b/test/script/custom-checks index d4027fec..20dbfb80 100755 --- a/test/script/custom-checks +++ b/test/script/custom-checks @@ -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