New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decouple completion engine internals #2143
Changes from all commits
e51a222
3ed3676
443b477
cd9558f
bd58f31
f3c3acc
ec62da2
91ddff4
005a820
940d183
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can pretty much undo the changes here. The code is basically the same, and the changes don't really improve on what was there. The previous behavior of looking at the line and column as requested instead of the current cursor position may be more appropriate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this referring to the whole block of changes above? Those were exactly what was before inside of ale#completion#OmniFunc, only turned into functions because external sources don't use the flow of omnifunctions to command their behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, thank you for clarifying. That makes sense. Remember to add tests for these function names. Let's add the deoplete source file to ALE's repo itself. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't deoplete accept autoload functions? Why not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does deoplete need to use this function at all? This function opens up the completion menu itself, so I'd imagine it's only useful when you aren't using deoplete. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a result is parsed into menu candidates, after that, the caller needs a way to know that the results are ready to be used on their end. For deoplete I've overrode this function with:
For other completion plugins, people can override this "call be when it's done callback" with something else, hence the name _After_CompletionResults. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replacing an existing function like that is bad design, and it doesn't make sense. Instead of doing that, let's add the deoplete source to ALE, and we don't need to call the function for opening the omnicomplete menu when ALE completion integration with LSP is being used as a completion source. |
||
" 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("\<Plug>(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("\<Plug>(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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a mistake that there are two return statements, but the name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Remove the |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had set it up so parsing of results was deferred until just before the menu is shown, to spread out the processing between frames a bit, but I think it should be fine to just parse the results right away. |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's probably okay to remove this check. Completion results could still be retrieved if you insert some text, set the option to |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should undo the changes here. If you need to use what this function does for deoplete, you should create a second function called by this one. Maybe rename this function to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't the person know that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They should, but I'd leave the check in here anyway. It doesn't cost much, and it will be useful later if we add |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's a need to set this variable and check it. The timer is already being stopped and restarted, and this could cause bugs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
How do I signal situation two without this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should remove the |
||
|
||
" 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,23 +592,29 @@ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the old name was better. This is the function for handling |
||
silent! pclose | ||
|
||
call ale#completion#RestoreCompletionOptions() | ||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# ============================================================================ | ||
# FILE: omni.py | ||
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com> | ||
# 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned before, you can remove this.