diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim new file mode 100644 index 0000000..f6862db --- /dev/null +++ b/autoload/ale/completion.vim @@ -0,0 +1,192 @@ +" Author: w0rp +" Description: Completion support for LSP linters + +let s:timer = -1 +let s:delay = 300 +let s:max_suggestions = 20 +let s:buffer_completion_map = {} + +function! s:RememberCompletionInfo(buffer, executable, request_id, line, column) abort + let s:buffer_completion_map[a:buffer] = { + \ 'executable': a:executable, + \ 'request_id': a:request_id, + \ 'line': a:line, + \ 'column': a:column, + \} +endfunction + +" Find completion information for a response, and delete the information +" if the request failed. +function! s:FindCompletionInfo(response) abort + let l:matched_buffer = -1 + let l:matched_data = {} + + for l:key in keys(s:buffer_completion_map) + let l:obj = s:buffer_completion_map[l:key] + + if l:obj.request_id ==# a:response.request_seq + if get(a:response, 'success') + let l:matched_buffer = str2nr(l:key) + let l:matched_data = l:obj + else + " Clean up the data we remembered if the request failed. + call remove(s:buffer_completion_map, l:matched_buffer) + endif + endif + endfor + + return [l:matched_buffer, l:matched_data] +endfunction + +function! s:HandleCompletions(response) abort + let [l:buffer, l:info] = s:FindCompletionInfo(a:response) + + if l:buffer >= 0 + let l:names = [] + + for l:suggestion in a:response.body[: s:max_suggestions] + call add(l:names, l:suggestion.name) + endfor + + let l:request_id = ale#lsp#SendMessageToProgram( + \ l:info.executable, + \ ale#lsp#tsserver_message#CompletionEntryDetails( + \ l:buffer, + \ l:info.line, + \ l:info.column, + \ l:names, + \ ), + \) + + if l:request_id + let l:info.request_id = l:request_id + else + " Remove the info now if we failed to start the request. + call remove(s:buffer_completion_map, l:buffer) + endif + endif +endfunction + +function! s:HandleCompletionDetails(response) abort + let [l:buffer, l:info] = s:FindCompletionInfo(a:response) + + if l:buffer >= 0 + call remove(s:buffer_completion_map, l:buffer) + + let l:name_list = [] + + for l:suggestion in a:response.body[: s:max_suggestions] + " Each suggestion has 'kind' and 'kindModifier' properties + " which could be useful. + " Each one of these parts has 'kind' properties + let l:displayParts = [] + + for l:part in l:suggestion.displayParts + call add(l:displayParts, l:part.text) + endfor + + " Each one of these parts has 'kind' properties + let l:documentationParts = [] + + for l:part in l:suggestion.documentation + call add(l:documentationParts, l:part.text) + endfor + + let l:text = l:suggestion.name + \ . ' - ' + \ . join(l:displayParts, '') + \ . (!empty(l:documentationParts) ? ' ' : '') + \ . join(l:documentationParts, '') + + call add(l:name_list, l:text) + endfor + + echom string(l:name_list) + endif +endfunction + +function! s:HandleLSPResponse(response) abort + let l:command = get(a:response, 'command', '') + + if l:command ==# 'completions' + call s:HandleCompletions(a:response) + elseif l:command ==# 'completionEntryDetails' + call s:HandleCompletionDetails(a:response) + endif +endfunction + +function! s:GetCompletionsForTSServer(buffer, linter, line, column) abort + let l:executable = has_key(a:linter, 'executable_callback') + \ ? ale#util#GetFunction(a:linter.executable_callback)(a:buffer) + \ : a:linter.executable + let l:command = l:executable + + let l:job_id = ale#lsp#StartProgram( + \ l:executable, + \ l:executable, + \ function('s:HandleLSPResponse') + \) + + if !l:job_id + if g:ale_history_enabled + call ale#history#Add(a:buffer, 'failed', l:job_id, l:command) + endif + endif + + if ale#lsp#OpenTSServerDocumentIfNeeded(l:executable, a:buffer) + if g:ale_history_enabled + call ale#history#Add(a:buffer, 'started', l:job_id, l:command) + endif + endif + + call ale#lsp#SendMessageToProgram( + \ l:executable, + \ ale#lsp#tsserver_message#Change(a:buffer), + \) + + let l:request_id = ale#lsp#SendMessageToProgram( + \ l:executable, + \ ale#lsp#tsserver_message#Completions(a:buffer, a:line, a:column), + \) + + if l:request_id + call s:RememberCompletionInfo( + \ a:buffer, + \ l:executable, + \ l:request_id, + \ a:line, + \ a:column, + \) + endif +endfunction + +function! ale#completion#GetCompletions() abort + let l:buffer = bufnr('') + let [l:line, l:column] = getcurpos()[1:2] + + for l:linter in ale#linter#Get(getbufvar(l:buffer, '&filetype')) + if l:linter.lsp ==# 'tsserver' + call s:GetCompletionsForTSServer(l:buffer, l:linter, l:line, l:column) + endif + endfor +endfunction + +function! s:TimerHandler(...) abort + call ale#completion#GetCompletions() +endfunction + +function! ale#completion#Queue() abort + if s:timer != -1 + call timer_stop(s:timer) + let s:timer = -1 + endif + + let s:timer = timer_start(s:delay, function('s:TimerHandler')) +endfunction + +function! ale#completion#Start() abort + augroup ALECompletionGroup + autocmd! + autocmd TextChangedI * call ale#completion#Queue() + augroup END +endfunction diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index f7c25b0..b56558f 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -42,7 +42,6 @@ function! ale#engine#InitBufferInfo(buffer) abort \ 'temporary_file_list': [], \ 'temporary_directory_list': [], \ 'history': [], - \ 'open_lsp_documents': [], \} endif endfunction @@ -563,8 +562,6 @@ endfunction function! s:CheckWithTSServer(buffer, linter, executable) abort let l:info = g:ale_buffer_info[a:buffer] - let l:open_documents = l:info.open_lsp_documents - let l:is_open = index(l:open_documents, a:linter.name) >= 0 let l:command = ale#job#PrepareCommand(a:executable) let l:job_id = ale#lsp#StartProgram(a:executable, l:command, function('s:HandleLSPResponse')) @@ -577,16 +574,10 @@ function! s:CheckWithTSServer(buffer, linter, executable) abort return endif - if !l:is_open + if ale#lsp#OpenTSServerDocumentIfNeeded(a:executable, a:buffer) if g:ale_history_enabled call ale#history#Add(a:buffer, 'started', l:job_id, l:command) endif - - call add(l:open_documents, a:linter.name) - call ale#lsp#SendMessageToProgram( - \ a:executable, - \ ale#lsp#tsserver_message#Open(a:buffer), - \) endif call ale#lsp#SendMessageToProgram( diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index ce7efd1..1f63904 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -16,6 +16,7 @@ function! s:NewConnection() abort \ 'address': '', \ 'executable': '', \ 'job_id': -1, + \ 'open_documents': [], \} call add(s:connections, l:conn) @@ -283,3 +284,21 @@ function! ale#lsp#SendMessageToAddress(address, message) abort return l:id == 0 ? -1 : l:id endfunction + +function! ale#lsp#OpenTSServerDocumentIfNeeded(executable, buffer) abort + let l:opened = 0 + let l:matches = filter(s:connections[:], 'v:val.executable ==# a:executable') + + " Send the command for opening the document only if needed. + if !empty(l:matches) && index(l:matches[0].open_documents, a:buffer) < 0 + call ale#lsp#SendMessageToProgram( + \ a:executable, + \ ale#lsp#tsserver_message#Open(a:buffer), + \) + call add(l:matches[0].open_documents, a:buffer) + + let l:opened = 1 + endif + + return l:opened +endfunction diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index e78b29e..2ccbf75 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -26,7 +26,7 @@ function! ale#lsp#tsserver_message#Change(buffer) abort \ 'file': expand('#' . a:buffer . ':p'), \ 'line': 1, \ 'offset': 1, - \ 'endLine': 1073741824 , + \ 'endLine': 1073741824, \ 'endOffset': 1, \ 'insertString': join(l:lines, "\n"), \}] @@ -35,3 +35,21 @@ endfunction function! ale#lsp#tsserver_message#Geterr(buffer) abort return [1, 'ts@geterr', {'files': [expand('#' . a:buffer . ':p')]}] endfunction + +function! ale#lsp#tsserver_message#Completions(buffer, line, column) abort + " An optional 'prefix' key can be added here for a completion prefix. + return [0, 'ts@completions', { + \ 'line': a:line, + \ 'offset': a:column, + \ 'file': expand('#' . a:buffer . ':p'), + \}] +endfunction + +function! ale#lsp#tsserver_message#CompletionEntryDetails(buffer, line, column, entry_names) abort + return [0, 'ts@completionEntryDetails', { + \ 'line': a:line, + \ 'offset': a:column, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'entryNames': a:entry_names, + \}] +endfunction diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader index 5decbf6..3b5c64f 100644 --- a/test/lsp/test_lsp_client_messages.vader +++ b/test/lsp/test_lsp_client_messages.vader @@ -146,3 +146,34 @@ Execute(ale#lsp#tsserver_message#Geterr() should return correct messages): \ } \ ], \ ale#lsp#tsserver_message#Geterr(bufnr('')) + +Execute(ale#lsp#tsserver_message#Completions() should return correct messages): + silent! noautocmd file foo.ts + + AssertEqual + \ [ + \ 0, + \ 'ts@completions', + \ { + \ 'file': b:dir . '/foo.ts', + \ 'line': 347, + \ 'offset': 12, + \ } + \ ], + \ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12) + +Execute(ale#lsp#tsserver_message#CompletionEntryDetails() should return correct messages): + silent! noautocmd file foo.ts + + AssertEqual + \ [ + \ 0, + \ 'ts@completionEntryDetails', + \ { + \ 'file': b:dir . '/foo.ts', + \ 'line': 347, + \ 'offset': 12, + \ 'entryNames': ['foo', 'bar'], + \ } + \ ], + \ ale#lsp#tsserver_message#CompletionEntryDetails(bufnr(''), 347, 12, ['foo', 'bar'])