diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index 9dd913f545..76582f586f 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -17,7 +17,9 @@ let g:ale_completion_excluded_words = get(g:, 'ale_completion_excluded_words', [ let g:ale_completion_max_suggestions = get(g:, 'ale_completion_max_suggestions', 50) let s:timer_id = -1 +let s:timer_pos = [] let s:last_done_pos = [] +let s:currently_queued_pos = [] " CompletionItemKind values from the LSP protocol. let s:LSP_COMPLETION_TEXT_KIND = 1 @@ -183,52 +185,71 @@ function! ale#completion#RestoreCompletionOptions() abort endif endfunction +function! ale#completion#FindCompletionStart() abort + let [l:line, l:column] = getcurpos()[1:2] + 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) + + let b:ale_completion_start = l:column - len(l:match) - 1 + + return b:ale_completion_start +endfunction + +function! ale#completion#PollCompletionResults() abort + if ale#completion#Queue() + return [] + else + return get(b:, 'ale_completion_result', []) + endif +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#FindCompletionStart() 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 + return ale#completion#PollCompletionResults() + endif +endfunction - unlet b:ale_completion_response - unlet b:ale_completion_parser +function! ale#completion#QueryDone() abort + let s:last_done_pos = getcurpos()[1:2] - let b:ale_completion_result = function(l:parser)(l:response) - endif + let b:ale_completion_result = ALEProcessCompletionResults(get(b:, 'ale_completion_result', [])) - call s:ReplaceCompletionOptions() + call ALEAfterCompletionResults() +endfunction - return get(b:, 'ale_completion_result', []) - endif +function! ALEProcessCompletionResults(completion_results) abort + " Users may override this function to act on the results however they + " see fit. Needs to be not namespaced. + return a:completion_results endfunction -function! ale#completion#Show(response, completion_parser) abort - if ale#util#Mode() isnot# 'i' +function! ALEAfterCompletionResults() abort + " Users may override this function to act on the results however they + " see fit. Needs to be not namespaced. + if !ale#util#Mode() is# 'i' return endif - " Set the list in the buffer, temporarily replace omnifunc with our - " function, and then start omni-completion. - let b:ale_completion_response = a:response - let b:ale_completion_parser = a:completion_parser - " Replace completion options shortly before opening the menu. - call s:ReplaceCompletionOptions() - - call timer_start(0, {-> ale#util#FeedKeys("\(ale_show_completion_menu)")}) + " If the completion events are not managed by ALE, don't mess with + " the omnifunc option. Otherwise, replace completion options shortly + " before opening the menu. + if g:ale_completion_enabled + call s:ReplaceCompletionOptions() + call timer_start(0, {-> ale#util#FeedKeys("\(ale_show_completion_menu)")}) + endif endfunction function! s:CompletionStillValid(request_id) abort let [l:line, l:column] = getcurpos()[1:2] + return 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 + return ale#util#Mode() is# 'i' \&& has_key(b:, 'ale_completion_info') \&& b:ale_completion_info.request_id == a:request_id @@ -418,10 +439,9 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort \) endif elseif l:command is# 'completionEntryDetails' - call ale#completion#Show( - \ a:response, - \ 'ale#completion#ParseTSServerCompletionEntryDetails', - \) + let b:ale_completion_result = ale#completion#ParseTSServerCompletionEntryDetails(a:response) + + call ale#completion#QueryDone() endif endfunction @@ -431,10 +451,9 @@ function! ale#completion#HandleLSPResponse(conn_id, response) abort return endif - call ale#completion#Show( - \ a:response, - \ 'ale#completion#ParseLSPCompletions', - \) + let b:ale_completion_result = ale#completion#ParseLSPCompletions(a:response) + + call ale#completion#QueryDone() endfunction function! s:OnReady(linter, lsp_details, ...) abort @@ -505,10 +524,6 @@ function! s:GetLSPCompletions(linter) abort endfunction function! ale#completion#GetCompletions() abort - if !g:ale_completion_enabled - return - endif - let [l:line, l:column] = getcurpos()[1:2] let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) @@ -557,18 +572,17 @@ function! ale#completion#StopTimer() abort endfunction function! ale#completion#Queue() abort - if !g:ale_completion_enabled - return - endif - let s:timer_pos = getcurpos()[1:2] - if s:timer_pos == s:last_done_pos + if s:timer_pos == s:last_done_pos || s:timer_pos == s:currently_queued_pos " Do not ask for completions if the cursor rests on the position we - " last completed on. - return + " last completed on, or if the position is the one currently being + " queried by the engine. + return 0 endif + let s:currently_queued_pos = s:timer_pos + " If we changed the text again while we're still waiting for a response, " then invalidate the requests before the timer ticks again. if exists('b:ale_completion_info') @@ -578,9 +592,11 @@ function! ale#completion#Queue() abort call ale#completion#StopTimer() let s:timer_id = timer_start(g:ale_completion_delay, function('s:TimerHandler')) + + return 1 endfunction -function! ale#completion#Done() abort +function! ale#completion#Finished() abort silent! pclose call ale#completion#RestoreCompletionOptions() @@ -588,13 +604,17 @@ function! ale#completion#Done() abort let s:last_done_pos = getcurpos()[1:2] endfunction +function! ale#completion#ClearBufferResults() abort + let b:ale_completion_result = [] +endfunction + function! s:Setup(enabled) abort augroup ALECompletionGroup autocmd! if a:enabled autocmd TextChangedI * call ale#completion#Queue() - autocmd CompleteDone * call ale#completion#Done() + autocmd CompleteDone * call ale#completion#Finished() endif augroup END @@ -605,10 +625,10 @@ endfunction function! ale#completion#Enable() abort let g:ale_completion_enabled = 1 - call s:Setup(1) + call s:Setup(g:ale_completion_enabled) endfunction function! ale#completion#Disable() abort let g:ale_completion_enabled = 0 - call s:Setup(0) + call s:Setup(g:ale_completion_enabled) endfunction diff --git a/plugin/ale.vim b/plugin/ale.vim index 7231d1bd64..5d593b90b6 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -133,7 +133,7 @@ let g:ale_history_enabled = get(g:, 'ale_history_enabled', 1) " A flag for storing the full output of commands in the history. let g:ale_history_log_output = get(g:, 'ale_history_log_output', 1) -" Enable automatic completion with LSP servers and tsserver +" Enable automatic completion events with LSP servers and tsserver let g:ale_completion_enabled = get(g:, 'ale_completion_enabled', 0) " Enable automatic detection of pipenv for Python linters. diff --git a/rplugin/python3/deoplete/sources/deoplete_ale.py b/rplugin/python3/deoplete/sources/deoplete_ale.py new file mode 100644 index 0000000000..6ed46c0ec5 --- /dev/null +++ b/rplugin/python3/deoplete/sources/deoplete_ale.py @@ -0,0 +1,73 @@ +# ============================================================================ +# FILE: omni.py +# AUTHOR: Shougo Matsushita +# License: MIT license +# ============================================================================ + +from deoplete.source.base import ( + Base +) +from deoplete.util import ( + convert2candidates +) + + +class Source(Base): + + def __init__(self, vim): + super().__init__(vim) + + self.name = 'ale' + self.mark = '[L]' + self.rank = 100 + self.is_bytepos = True + self.min_pattern_length = 0 + self.events = ['CompleteDone'] + + self.poll_func = 'ale#completion#PollCompletionResults' + self.position_func = 'ale#completion#FindCompletionStart' + self.reset_func = 'ale#completion#ClearBufferResults' + + def on_event(self, context): + if context['event'] == 'CompleteDone': + context['completion_done'] = True + + def get_completion_position(self): + return self.vim.call(self.position_func) + + def patch_candidates(self, candidates): + if isinstance(candidates, dict): + candidates = candidates['words'] + elif not isinstance(candidates, list): + candidates = convert2candidates(candidates) + + for c in candidates: + c['dup'] = 1 + + return candidates + + def gather_candidates(self, context): + candidates = [] + + try: + if 'initial_position' not in context: + self.vim.call(self.reset_func) + context['initial_position'] = self.get_completion_position() + + if context['initial_position'] < 0: + return [] + else: + context['is_async'] = True + + candidates = self.vim.call(self.poll_func) + + if 'completion_done' in context or candidates: + context['is_async'] = False + self.vim.call(self.reset_func) + candidates = self.patch_candidates(candidates) + + # self.vim.command('echo localtime()') + return candidates + except Exception: + self.print_error('Error occurred calling ALE') + return None diff --git a/test/completion/vimrc/minimal_autocompletion_vimrc b/test/completion/vimrc/minimal_autocompletion_vimrc new file mode 100644 index 0000000000..b7e73ace9f --- /dev/null +++ b/test/completion/vimrc/minimal_autocompletion_vimrc @@ -0,0 +1,17 @@ +" For testing ALE autocompletion with TSServer. +" You need to include ALE at $VIMRUNTIME/pack/completion/opt/ale + +" Launch with VIMRUNTIME=[dummy directory] vim -u [path to this file] --noplugin new.js + +" Example: VIMRUNTIME=~/vim/mock vim -u ~/vim/mock/pack/completion/opt/ale/test/completion/vimrc/minimal_autocompletion_vimrc --noplugin new.js + +set nocompatible +filetype on +set filetype=javascript + +" Should enable autocompletion automatically +let g:ale_completion_enabled = 1 +" Set up TSServer to enable completion +let g:ale_linters = { 'javascript': ['tsserver'] } + +packadd ale