summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--autoload/ale/lsp.vim32
-rw-r--r--autoload/ale/lsp_linter.vim26
-rw-r--r--doc/ale.txt18
-rw-r--r--test/lsp/test_lsp_custom_request.vader120
4 files changed, 192 insertions, 4 deletions
diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim
index 017096cd..1ffdba2e 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -5,6 +5,9 @@
let s:connections = get(s:, 'connections', {})
let g:ale_lsp_next_message_id = 1
+" A Dictionary to track one-shot callbacks for custom LSP requests
+let s:custom_callbacks = get(s:, 'custom_callbacks', {})
+
" Given an id, which can be an executable or address, and a project path,
" create a new connection if needed. Return a unique ID for the connection.
function! ale#lsp#Register(executable_or_address, project, init_options) abort
@@ -296,10 +299,19 @@ function! ale#lsp#HandleMessage(conn_id, message) abort
" responses.
if l:conn.initialized
for l:response in l:response_list
- " Call all of the registered handlers with the response.
- for l:Callback in l:conn.callback_list
- call ale#util#GetFunction(l:Callback)(a:conn_id, l:response)
- endfor
+ if has_key(l:response, 'id') && has_key(s:custom_callbacks, l:response.id)
+ " Response to a custom request, call the registered one-shot handler.
+ try
+ call s:custom_callbacks[l:response.id](l:response)
+ finally
+ call remove(s:custom_callbacks, l:response.id)
+ endtry
+ else
+ " Call all of the registered handlers with the response.
+ for l:Callback in l:conn.callback_list
+ call ale#util#GetFunction(l:Callback)(a:conn_id, l:response)
+ endfor
+ endif
endfor
endif
endfunction
@@ -525,6 +537,18 @@ function! ale#lsp#Send(conn_id, message) abort
return l:id == 0 ? -1 : l:id
endfunction
+" Send a custom request to an LSP server.
+" The given callback is called on response.
+function! ale#lsp#SendCustomRequest(conn_id, message, Callback) abort
+ let l:id = ale#lsp#Send(a:conn_id, a:message)
+
+ if l:id > 0
+ let s:custom_callbacks[l:id] = a:Callback
+ endif
+
+ return l:id
+endfunction
+
" Notify LSP servers or tsserver if a document is opened, if needed.
" If a document is opened, 1 will be returned, otherwise 0 will be returned.
function! ale#lsp#OpenDocument(conn_id, buffer, language_id) abort
diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim
index 4f439b28..82ae60a2 100644
--- a/autoload/ale/lsp_linter.vim
+++ b/autoload/ale/lsp_linter.vim
@@ -413,3 +413,29 @@ endfunction
function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
let s:lsp_linter_map = a:replacement_map
endfunction
+
+" Send a custom request to an LSP linter.
+function! ale#lsp_linter#SendRequest(buffer, linter_name, method, parameters, Callback) abort
+ let l:filetype = ale#linter#ResolveFiletype(getbufvar(a:buffer, '&filetype'))
+ let l:linter_list = ale#linter#GetAll(l:filetype)
+ let l:linter_list = filter(l:linter_list, {_, v -> v.name is# a:linter_name})
+
+ if len(l:linter_list) < 1
+ throw 'Linter "' . a:linter_name . '" not found!'
+ endif
+
+ let l:linter = l:linter_list[0]
+ let l:executable_or_address = ''
+
+ if l:linter.lsp is# 'socket'
+ let l:executable_or_address = ale#linter#GetAddress(a:buffer, l:linter)
+ else
+ let l:executable_or_address = ale#linter#GetExecutable(a:buffer, l:linter)
+ endif
+
+ let l:root = ale#util#GetFunction(l:linter.project_root)(a:buffer)
+ let l:conn_id = l:executable_or_address . ':' . l:root
+ let l:message = [0, a:method, a:parameters]
+
+ return ale#lsp#SendCustomRequest(l:conn_id, l:message, a:Callback) == 0
+endfunction
diff --git a/doc/ale.txt b/doc/ale.txt
index 17f1dde6..8dcbea60 100644
--- a/doc/ale.txt
+++ b/doc/ale.txt
@@ -3191,6 +3191,24 @@ ale#linter#PreventLoading(filetype) *ale#linter#PreventLoading()*
|runtimepath| for that filetype. This function can be called from vimrc or
similar to prevent ALE from loading linters.
+
+ale#lsp_linter#SendRequest(buffer, linter_name, method, parameters, Callback)
+ *ale#lsp_linter#SendRequest()*
+
+ Send a custom request to an LSP linter.
+
+ `buffer` must be a valid buffer number, and `linter_name` is a |String|
+ identifying an LSP linter that is available and enabled for the |filetype|
+ of `buffer`. `method` is a |String| identifying an LSP method supported by
+ `linter`, while `parameters` is a |dictionary| of LSP parameters applicable
+ to `method`. `Callback` is a |Funcref| that is called when a response to the
+ request is received, and takes as unique argument a dictionary representing
+ the response to the request obtained from the server.
+
+ The function returns zero if the request is succesfully sent, non-zero
+ otherwise.
+
+
ale#other_source#ShowResults(buffer, linter_name, loclist)
*ale#other_source#ShowResults()*
diff --git a/test/lsp/test_lsp_custom_request.vader b/test/lsp/test_lsp_custom_request.vader
new file mode 100644
index 00000000..e5fecafc
--- /dev/null
+++ b/test/lsp/test_lsp_custom_request.vader
@@ -0,0 +1,120 @@
+Before:
+ runtime autoload/ale/linter.vim
+ runtime autoload/ale/lsp.vim
+ runtime autoload/ale/lsp_linter.vim
+
+ let g:address = 'ccls_address'
+ let g:callback_result = 0
+ let g:conn_id = -1
+ let g:executable = 'ccls'
+ let g:linter_name = 'ccls'
+ let g:magic_number = 42
+ let g:message = -1
+ let g:message_id = 1
+ let g:method = '$ccls/call'
+ let g:parameters = {}
+ let g:project = '/project/root'
+ let g:return_value = -1
+
+ let g:linter_list = [{
+ \ 'output_stream': 'stdout',
+ \ 'lint_file': 0,
+ \ 'language': 'cpp',
+ \ 'name': g:linter_name,
+ \ 'project_root': {b -> g:project},
+ \ 'aliases': [],
+ \ 'language_callback': {b -> 'cpp'},
+ \ 'read_buffer': 1,
+ \ 'command': '%e'
+ \ }]
+
+ " Encode dictionary to jsonrpc
+ function! Encode(obj) abort
+ let l:body = json_encode(a:obj)
+ return 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body
+ endfunction
+
+ " Register the server with given executable or address
+ function! InitServer(executable_or_address) abort
+ let g:conn_id = ale#lsp#Register(a:executable_or_address, g:project, {})
+ call ale#lsp#HandleMessage(g:conn_id, Encode({'method': 'initialize'}))
+ endfunction
+
+ " Dummy callback
+ function! Callback(response) abort
+ let g:callback_result = a:response.result.value
+ endfunction
+
+ " Replace the GetAll function to mock an LSP linter
+ function! ale#linter#GetAll(filetype) abort
+ return g:linter_list
+ endfunction
+
+ " Replace the Send function to mock an LSP linter
+ function! ale#lsp#Send(conn_id, message) abort
+ let g:message = a:message
+ return g:message_id
+ endfunction
+
+ " Code for a test case
+ function! TestCase() abort
+ " Test sending a custom request
+ let g:return_value = ale#lsp_linter#SendRequest(bufnr('%'), g:linter_name, g:method, g:parameters, function('Callback'))
+
+ AssertEqual
+ \ 0,
+ \ g:return_value
+
+ AssertEqual
+ \ [0, g:method, g:parameters],
+ \ g:message
+
+ " Mock an incoming response to the request
+ call ale#lsp#HandleMessage(g:conn_id, Encode({'id': g:message_id, 'jsonrpc': '2.0', 'result': {'value': g:magic_number}}))
+
+ AssertEqual
+ \ g:magic_number,
+ \ g:callback_result
+ endfunction
+
+After:
+ if g:conn_id isnot v:null
+ call ale#lsp#RemoveConnectionWithID(g:conn_id)
+ endif
+
+ unlet! g:callback_result
+ unlet! g:conn_id
+ unlet! g:executable
+ unlet! g:linter_name
+ unlet! g:magic_number
+ unlet! g:message
+ unlet! g:message_id
+ unlet! g:method
+ unlet! g:parameters
+ unlet! g:project
+ unlet! g:return_value
+
+ delfunction Encode
+ delfunction InitServer
+ delfunction Callback
+ delfunction TestCase
+
+ runtime autoload/ale/linter.vim
+ runtime autoload/ale/lsp.vim
+ runtime autoload/ale/lsp_linter.vim
+
+Given cpp(Empty cpp file):
+Execute(Test custom request to server identified by executable):
+ call InitServer(g:executable)
+ let g:linter_list[0].executable = {b -> g:executable}
+ let g:linter_list[0].lsp = 'stdio'
+
+ call TestCase()
+
+Given cpp(Empty cpp file):
+Execute(Test custom request to server identified by address):
+ call InitServer(g:address)
+ let g:linter_list[0].address = {b -> g:address}
+ let g:linter_list[0].lsp = 'socket'
+
+ call TestCase()