feat(lsp): run handler in coroutine to support async response (#21026)

To illustrate a use-case this also changes `window/showMessageRequest`
to use `vim.ui.select`
This commit is contained in:
Mathias Fußenegger 2022-11-19 10:48:49 +01:00 committed by GitHub
parent 0958dccc6d
commit af204dd0f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 49 deletions

View File

@ -81,22 +81,38 @@ M['window/workDoneProgress/create'] = function(_, result, ctx)
end end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
---@param result lsp.ShowMessageRequestParams
M['window/showMessageRequest'] = function(_, result) M['window/showMessageRequest'] = function(_, result)
local actions = result.actions local actions = result.actions or {}
print(result.message) local co, is_main = coroutine.running()
local option_strings = { result.message, '\nRequest Actions:' } if co and not is_main then
for i, action in ipairs(actions) do local opts = {
local title = action.title:gsub('\r\n', '\\r\\n') prompt = result.message .. ': ',
title = title:gsub('\n', '\\n') format_item = function(action)
table.insert(option_strings, string.format('%d. %s', i, title)) return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n')
end end,
}
-- window/showMessageRequest can return either MessageActionItem[] or null. vim.ui.select(actions, opts, function(choice)
local choice = vim.fn.inputlist(option_strings) -- schedule to ensure resume doesn't happen _before_ yield with
if choice < 1 or choice > #actions then -- default synchronous vim.ui.select
return vim.NIL vim.schedule(function()
coroutine.resume(co, choice or vim.NIL)
end)
end)
return coroutine.yield()
else else
return actions[choice] local option_strings = { result.message, '\nRequest Actions:' }
for i, action in ipairs(actions) do
local title = action.title:gsub('\r\n', '\\r\\n')
title = title:gsub('\n', '\\n')
table.insert(option_strings, string.format('%d. %s', i, title))
end
local choice = vim.fn.inputlist(option_strings)
if choice < 1 or choice > #actions then
return vim.NIL
else
return actions[choice]
end
end end
end end

View File

@ -20,6 +20,14 @@ function transform_schema_to_table()
end end
--]=] --]=]
---@class lsp.ShowMessageRequestParams
---@field type lsp.MessageType
---@field message string
---@field actions nil|lsp.MessageActionItem[]
---@class lsp.MessageActionItem
---@field title string
local constants = { local constants = {
DiagnosticSeverity = { DiagnosticSeverity = {
-- Reports an error. -- Reports an error.
@ -39,6 +47,7 @@ local constants = {
Deprecated = 2, Deprecated = 2,
}, },
---@enum lsp.MessageType
MessageType = { MessageType = {
-- An error message. -- An error message.
Error = 1, Error = 1,

View File

@ -391,44 +391,46 @@ function Client:handle_body(body)
-- Schedule here so that the users functions don't trigger an error and -- Schedule here so that the users functions don't trigger an error and
-- we can still use the result. -- we can still use the result.
schedule(function() schedule(function()
local status, result coroutine.wrap(function()
status, result, err = self:try_call( local status, result
client_errors.SERVER_REQUEST_HANDLER_ERROR, status, result, err = self:try_call(
self.dispatchers.server_request, client_errors.SERVER_REQUEST_HANDLER_ERROR,
decoded.method, self.dispatchers.server_request,
decoded.params decoded.method,
) decoded.params
local _ = log.debug()
and log.debug(
'server_request: callback result',
{ status = status, result = result, err = err }
) )
if status then local _ = log.debug()
if result == nil and err == nil then and log.debug(
error( 'server_request: callback result',
string.format( { status = status, result = result, err = err }
'method %q: either a result or an error must be sent to the server in response', )
decoded.method if status then
if result == nil and err == nil then
error(
string.format(
'method %q: either a result or an error must be sent to the server in response',
decoded.method
)
) )
) end
if err then
assert(
type(err) == 'table',
'err must be a table. Use rpc_response_error to help format errors.'
)
local code_name = assert(
protocol.ErrorCodes[err.code],
'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
)
err.message = err.message or code_name
end
else
-- On an exception, result will contain the error message.
err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
result = nil
end end
if err then self:send_response(decoded.id, err, result)
assert( end)()
type(err) == 'table',
'err must be a table. Use rpc_response_error to help format errors.'
)
local code_name = assert(
protocol.ErrorCodes[err.code],
'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
)
err.message = err.message or code_name
end
else
-- On an exception, result will contain the error message.
err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
result = nil
end
self:send_response(decoded.id, err, result)
end) end)
-- This works because we are expecting vim.NIL here -- This works because we are expecting vim.NIL here
elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then