Merge pull request #2549 from m-pilia/custom-lsp-requests
Add API for custom LSP requests
This commit is contained in:
commit
135de34d22
3 changed files with 237 additions and 0 deletions
|
@ -8,6 +8,9 @@ if !has_key(s:, 'lsp_linter_map')
|
||||||
let s:lsp_linter_map = {}
|
let s:lsp_linter_map = {}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
" A Dictionary to track one-shot handlers for custom LSP requests
|
||||||
|
let s:custom_handlers_map = get(s:, 'custom_handlers_map', {})
|
||||||
|
|
||||||
" Check if diagnostics for a particular linter should be ignored.
|
" Check if diagnostics for a particular linter should be ignored.
|
||||||
function! s:ShouldIgnore(buffer, linter_name) abort
|
function! s:ShouldIgnore(buffer, linter_name) abort
|
||||||
" Ignore all diagnostics if LSP integration is disabled.
|
" Ignore all diagnostics if LSP integration is disabled.
|
||||||
|
@ -407,9 +410,57 @@ endfunction
|
||||||
" Clear LSP linter data for the linting engine.
|
" Clear LSP linter data for the linting engine.
|
||||||
function! ale#lsp_linter#ClearLSPData() abort
|
function! ale#lsp_linter#ClearLSPData() abort
|
||||||
let s:lsp_linter_map = {}
|
let s:lsp_linter_map = {}
|
||||||
|
let s:custom_handlers_map = {}
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Just for tests.
|
" Just for tests.
|
||||||
function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
|
function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
|
||||||
let s:lsp_linter_map = a:replacement_map
|
let s:lsp_linter_map = a:replacement_map
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
function! s:HandleLSPResponseToCustomRequests(conn_id, response) abort
|
||||||
|
if has_key(a:response, 'id')
|
||||||
|
\&& has_key(s:custom_handlers_map, a:response.id)
|
||||||
|
let l:Handler = remove(s:custom_handlers_map, a:response.id)
|
||||||
|
call l:Handler(a:response)
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:OnReadyForCustomRequests(args, linter, lsp_details) abort
|
||||||
|
let l:id = a:lsp_details.connection_id
|
||||||
|
let l:request_id = ale#lsp#Send(l:id, a:args.message)
|
||||||
|
|
||||||
|
if l:request_id > 0 && has_key(a:args, 'handler')
|
||||||
|
let l:Callback = function('s:HandleLSPResponseToCustomRequests')
|
||||||
|
call ale#lsp#RegisterCallback(l:id, l:Callback)
|
||||||
|
let s:custom_handlers_map[l:request_id] = a:args.handler
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Send a custom request to an LSP linter.
|
||||||
|
function! ale#lsp_linter#SendRequest(buffer, linter_name, message, ...) 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]
|
||||||
|
|
||||||
|
if empty(l:linter.lsp)
|
||||||
|
throw 'Linter "' . a:linter_name . '" does not support LSP!'
|
||||||
|
endif
|
||||||
|
|
||||||
|
let l:is_notification = a:message[0]
|
||||||
|
let l:callback_args = {'message': a:message}
|
||||||
|
|
||||||
|
if !l:is_notification && a:0
|
||||||
|
let l:callback_args.handler = a:1
|
||||||
|
endif
|
||||||
|
|
||||||
|
let l:Callback = function('s:OnReadyForCustomRequests', [l:callback_args])
|
||||||
|
|
||||||
|
return ale#lsp_linter#StartLSP(a:buffer, l:linter, l:Callback)
|
||||||
|
endfunction
|
||||||
|
|
27
doc/ale.txt
27
doc/ale.txt
|
@ -3191,6 +3191,33 @@ ale#linter#PreventLoading(filetype) *ale#linter#PreventLoading()*
|
||||||
|runtimepath| for that filetype. This function can be called from vimrc or
|
|runtimepath| for that filetype. This function can be called from vimrc or
|
||||||
similar to prevent ALE from loading linters.
|
similar to prevent ALE from loading linters.
|
||||||
|
|
||||||
|
|
||||||
|
ale#lsp_linter#SendRequest(buffer, linter_name, message, [Handler])
|
||||||
|
*ale#lsp_linter#SendRequest()*
|
||||||
|
|
||||||
|
Send a custom request to an LSP linter. The arguments are defined as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
`buffer` A valid buffer number.
|
||||||
|
|
||||||
|
`linter_name` A |String| identifying an LSP linter that is available and
|
||||||
|
enabled for the |filetype| of `buffer`.
|
||||||
|
|
||||||
|
`message` A |List| in the form `[is_notification, method, parameters]`,
|
||||||
|
containing three elements:
|
||||||
|
`is_notification` - an |Integer| that has value 1 if the
|
||||||
|
request is a notification, 0 otherwise;
|
||||||
|
`methdod` - a |String|, identifying an LSP method supported
|
||||||
|
by `linter`;
|
||||||
|
`parameters` - a |dictionary| of LSP parameters that are
|
||||||
|
applicable to `method`.
|
||||||
|
|
||||||
|
`Handler` Optional argument, meaningful only when `message[0]` is 0.
|
||||||
|
A |Funcref| that is called when a response to the request is
|
||||||
|
received, and takes as unique argument a dictionary
|
||||||
|
representing the response obtained from the server.
|
||||||
|
|
||||||
|
|
||||||
ale#other_source#ShowResults(buffer, linter_name, loclist)
|
ale#other_source#ShowResults(buffer, linter_name, loclist)
|
||||||
*ale#other_source#ShowResults()*
|
*ale#other_source#ShowResults()*
|
||||||
|
|
||||||
|
|
159
test/lsp/test_lsp_custom_request.vader
Normal file
159
test/lsp/test_lsp_custom_request.vader
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
Before:
|
||||||
|
runtime autoload/ale/linter.vim
|
||||||
|
runtime autoload/ale/lsp.vim
|
||||||
|
runtime autoload/ale/lsp_linter.vim
|
||||||
|
|
||||||
|
let g:address = 'ccls_address'
|
||||||
|
let g:conn_id = -1
|
||||||
|
let g:executable = 'ccls'
|
||||||
|
let g:executable_or_address = ''
|
||||||
|
let g:linter_name = 'ccls'
|
||||||
|
let g:magic_number = 42
|
||||||
|
let g:no_result = 0
|
||||||
|
let g:message_list = []
|
||||||
|
let g:message_id = 1
|
||||||
|
let g:method = '$ccls/call'
|
||||||
|
let g:parameters = {}
|
||||||
|
let g:project_root = '/project/root'
|
||||||
|
let g:response = ''
|
||||||
|
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_root},
|
||||||
|
\ 'aliases': [],
|
||||||
|
\ 'language_callback': {b -> 'cpp'},
|
||||||
|
\ 'read_buffer': 1,
|
||||||
|
\ 'command': '%e'
|
||||||
|
\ }]
|
||||||
|
|
||||||
|
let g:callback_result = g:no_result
|
||||||
|
|
||||||
|
" 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
|
||||||
|
|
||||||
|
" Replace the StartLSP function to mock an LSP linter
|
||||||
|
function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort
|
||||||
|
let g:conn_id = ale#lsp#Register(g:executable_or_address, g:project_root, {})
|
||||||
|
call ale#lsp#MarkDocumentAsOpen(g:conn_id, a:buffer)
|
||||||
|
call ale#lsp#HandleMessage(g:conn_id, Encode({'method': 'initialize'}))
|
||||||
|
|
||||||
|
let l:details = {
|
||||||
|
\ 'command': g:executable,
|
||||||
|
\ 'buffer': a:buffer,
|
||||||
|
\ 'connection_id': g:conn_id,
|
||||||
|
\ 'project_root': g:project_root,
|
||||||
|
\}
|
||||||
|
|
||||||
|
call ale#lsp_linter#OnInit(a:linter, l:details, a:Callback)
|
||||||
|
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
|
||||||
|
call add(g:message_list, a:message)
|
||||||
|
return g:message_id
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Code for a test case
|
||||||
|
function! TestCase(is_notification) abort
|
||||||
|
" Test sending a custom request
|
||||||
|
let g:return_value = ale#lsp_linter#SendRequest(
|
||||||
|
\ bufnr('%'),
|
||||||
|
\ g:linter_name,
|
||||||
|
\ [a:is_notification, g:method, g:parameters],
|
||||||
|
\ function('Callback'))
|
||||||
|
|
||||||
|
Assert index(g:message_list, [a:is_notification, g:method, g:parameters]) >= 0
|
||||||
|
|
||||||
|
" Mock an incoming response to the request
|
||||||
|
let g:response = Encode({
|
||||||
|
\ 'id': g:message_id,
|
||||||
|
\ 'jsonrpc': '2.0',
|
||||||
|
\ 'result': {'value': g:magic_number}
|
||||||
|
\ })
|
||||||
|
call ale#lsp#HandleMessage(g:conn_id, g:response)
|
||||||
|
|
||||||
|
AssertEqual
|
||||||
|
\ a:is_notification ? g:no_result : 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:is_notification
|
||||||
|
unlet! g:linter_name
|
||||||
|
unlet! g:magic_number
|
||||||
|
unlet! g:message_list
|
||||||
|
unlet! g:message_id
|
||||||
|
unlet! g:method
|
||||||
|
unlet! g:no_result
|
||||||
|
unlet! g:parameters
|
||||||
|
unlet! g:project_root
|
||||||
|
unlet! g:response
|
||||||
|
unlet! g:return_value
|
||||||
|
|
||||||
|
delfunction Encode
|
||||||
|
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):
|
||||||
|
let g:executable_or_address = g:executable
|
||||||
|
let g:linter_list[0].executable = {b -> g:executable}
|
||||||
|
let g:linter_list[0].lsp = 'stdio'
|
||||||
|
let g:is_notification = 0
|
||||||
|
|
||||||
|
call TestCase(g:is_notification)
|
||||||
|
|
||||||
|
Given cpp(Empty cpp file):
|
||||||
|
Execute(Test custom notification to server identified by executable):
|
||||||
|
let g:executable_or_address = g:executable
|
||||||
|
let g:linter_list[0].executable = {b -> g:executable}
|
||||||
|
let g:linter_list[0].lsp = 'stdio'
|
||||||
|
let g:is_notification = 1
|
||||||
|
|
||||||
|
call TestCase(g:is_notification)
|
||||||
|
|
||||||
|
Given cpp(Empty cpp file):
|
||||||
|
Execute(Test custom request to server identified by address):
|
||||||
|
let g:executable_or_address = g:address
|
||||||
|
let g:linter_list[0].address = {b -> g:address}
|
||||||
|
let g:linter_list[0].lsp = 'socket'
|
||||||
|
let g:is_notification = 0
|
||||||
|
|
||||||
|
call TestCase(g:is_notification)
|
||||||
|
|
||||||
|
Given cpp(Empty cpp file):
|
||||||
|
Execute(Test custom notification to server identified by address):
|
||||||
|
let g:executable_or_address = g:address
|
||||||
|
let g:linter_list[0].address = {b -> g:address}
|
||||||
|
let g:linter_list[0].lsp = 'socket'
|
||||||
|
let g:is_notification = 1
|
||||||
|
|
||||||
|
call TestCase(g:is_notification)
|
Reference in a new issue