summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorw0rp <devw0rp@gmail.com>2017-07-01 01:22:03 +0100
committerw0rp <devw0rp@gmail.com>2017-07-02 10:58:18 +0100
commit9f21e45156fd6dc8bb03f984e7009f476bf0a517 (patch)
treece22166fb2f6eee76ef2f5725ae2e0e2dce58d06
parent0d8be55c511502c116fdfcba16283cb88bd9b9b2 (diff)
downloadale-9f21e45156fd6dc8bb03f984e7009f476bf0a517.zip
Add some experimental completion code for tsserver
-rw-r--r--autoload/ale/completion.vim192
-rw-r--r--autoload/ale/engine.vim11
-rw-r--r--autoload/ale/lsp.vim19
-rw-r--r--autoload/ale/lsp/tsserver_message.vim20
-rw-r--r--test/lsp/test_lsp_client_messages.vader31
5 files changed, 262 insertions, 11 deletions
diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim
new file mode 100644
index 00000000..f6862db1
--- /dev/null
+++ b/autoload/ale/completion.vim
@@ -0,0 +1,192 @@
+" Author: w0rp <devw0rp@gmail.com>
+" 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 f7c25b04..b56558fe 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 ce7efd1e..1f63904e 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 e78b29e3..2ccbf755 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 5decbf6c..3b5c64fa 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'])