feat(lsp): more annotations

This commit is contained in:
Lewis Russell
2023-12-13 12:00:11 +00:00
committed by Lewis Russell
parent 320e9c1c21
commit 97bea3163a
16 changed files with 364 additions and 228 deletions

View File

@@ -26,23 +26,42 @@ local function format_message_with_content_length(encoded_message)
})
end
local function log_error(...)
if log.error() then
log.error(...)
end
end
local function log_info(...)
if log.info() then
log.info(...)
end
end
local function log_debug(...)
if log.debug() then
log.debug(...)
end
end
--- Parses an LSP Message's header
---
---@param header string: The header to parse.
---@return table # parsed headers
local function parse_headers(header)
assert(type(header) == 'string', 'header must be a string')
local headers = {}
local headers = {} --- @type table<string,string>
for line in vim.gsplit(header, '\r\n', { plain = true }) do
if line == '' then
break
end
--- @type string?, string?
local key, value = line:match('^%s*(%S+)%s*:%s*(.+)%s*$')
if key then
key = key:lower():gsub('%-', '_')
key = key:lower():gsub('%-', '_') --- @type string
headers[key] = value
else
local _ = log.error() and log.error('invalid header line %q', line)
log_error('invalid header line %q', line)
error(string.format('invalid header line %q', line))
end
end
@@ -96,17 +115,17 @@ local function request_parser_loop()
end
local body = table.concat(body_chunks)
-- Yield our data.
buffer = rest
.. (
coroutine.yield(headers, body)
or error('Expected more data for the body. The server may have died.')
) -- TODO hmm.
--- @type string
local data = coroutine.yield(headers, body)
or error('Expected more data for the body. The server may have died.')
buffer = rest .. data
else
-- Get more data since we don't have enough.
buffer = buffer
.. (
coroutine.yield() or error('Expected more data for the header. The server may have died.')
) -- TODO hmm.
--- @type string
local data = coroutine.yield()
or error('Expected more data for the header. The server may have died.')
buffer = buffer .. data
end
end
end
@@ -138,7 +157,7 @@ function M.format_rpc_error(err)
-- There is ErrorCodes in the LSP specification,
-- but in ResponseError.code it is not used and the actual type is number.
local code
local code --- @type string
if protocol.ErrorCodes[err.code] then
code = string.format('code_name = %s,', protocol.ErrorCodes[err.code])
else
@@ -174,48 +193,51 @@ function M.rpc_response_error(code, message, data)
})
end
local default_dispatchers = {}
--- @class vim.rpc.Dispatchers
--- @field notification fun(method: string, params: table)
--- @field server_request fun(method: string, params: table): any?, string?
--- @field on_exit fun(code: integer, signal: integer)
--- @field on_error fun(code: integer, err: any)
---@private
--- Default dispatcher for notifications sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
function default_dispatchers.notification(method, params)
local _ = log.debug() and log.debug('notification', method, params)
end
--- @type vim.rpc.Dispatchers
local default_dispatchers = {
--- Default dispatcher for notifications sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
notification = function(method, params)
log_debug('notification', method, params)
end,
---@private
--- Default dispatcher for requests sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
---@return nil
---@return table `vim.lsp.protocol.ErrorCodes.MethodNotFound`
function default_dispatchers.server_request(method, params)
local _ = log.debug() and log.debug('server_request', method, params)
return nil, M.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
--- Default dispatcher for requests sent to an LSP server.
---
---@param method (string) The invoked LSP method
---@param params (table): Parameters for the invoked LSP method
---@return nil
---@return table, `vim.lsp.protocol.ErrorCodes.MethodNotFound`
server_request = function(method, params)
log_debug('server_request', method, params)
return nil, M.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end,
---@private
--- Default dispatcher for when a client exits.
---
---@param code (integer): Exit code
---@param signal (integer): Number describing the signal used to terminate (if
---any)
function default_dispatchers.on_exit(code, signal)
local _ = log.info() and log.info('client_exit', { code = code, signal = signal })
end
--- Default dispatcher for when a client exits.
---
---@param code (integer): Exit code
---@param signal (integer): Number describing the signal used to terminate (if
---any)
on_exit = function(code, signal)
log_info('client_exit', { code = code, signal = signal })
end,
---@private
--- Default dispatcher for client errors.
---
---@param code (integer): Error code
---@param err (any): Details about the error
---any)
function default_dispatchers.on_error(code, err)
local _ = log.error() and log.error('client_error:', M.client_errors[code], err)
end
--- Default dispatcher for client errors.
---
---@param code (integer): Error code
---@param err (any): Details about the error
---any)
on_error = function(code, err)
log_error('client_error:', M.client_errors[code], err)
end,
}
---@private
function M.create_read_loop(handle_body, on_no_chunk, on_error)
@@ -248,8 +270,8 @@ end
---@class RpcClient
---@field message_index integer
---@field message_callbacks table
---@field notify_reply_callbacks table
---@field message_callbacks table<integer,function>
---@field notify_reply_callbacks table<integer,function>
---@field transport table
---@field dispatchers table
@@ -258,7 +280,7 @@ local Client = {}
---@private
function Client:encode_and_send(payload)
local _ = log.debug() and log.debug('rpc.send', payload)
log_debug('rpc.send', payload)
if self.transport.is_closing() then
return false
end
@@ -267,7 +289,7 @@ function Client:encode_and_send(payload)
return true
end
---@private
---@package
--- Sends a notification to the LSP server.
---@param method (string) The invoked LSP method
---@param params (any): Parameters for the invoked LSP method
@@ -291,7 +313,7 @@ function Client:send_response(request_id, err, result)
})
end
---@private
---@package
--- Sends a request to the LSP server and runs {callback} upon response.
---
---@param method (string) The invoked LSP method
@@ -329,7 +351,7 @@ function Client:request(method, params, callback, notify_reply_callback)
end
end
---@private
---@package
function Client:on_error(errkind, ...)
assert(M.client_errors[errkind])
-- TODO what to do if this fails?
@@ -354,17 +376,17 @@ end
-- time and log them. This would require storing the timestamp. I could call
-- them with an error then, perhaps.
---@private
---@package
function Client:handle_body(body)
local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
if not ok then
self:on_error(M.client_errors.INVALID_SERVER_JSON, decoded)
return
end
local _ = log.debug() and log.debug('rpc.receive', decoded)
log_debug('rpc.receive', decoded)
if type(decoded.method) == 'string' and decoded.id then
local err
local err --- @type table?
-- Schedule here so that the users functions don't trigger an error and
-- we can still use the result.
schedule(function()
@@ -376,11 +398,10 @@ function Client:handle_body(body)
decoded.method,
decoded.params
)
local _ = log.debug()
and log.debug(
'server_request: callback result',
{ status = status, result = result, err = err }
)
log_debug(
'server_request: callback result',
{ status = status, result = result, err = err }
)
if status then
if result == nil and err == nil then
error(
@@ -431,7 +452,7 @@ function Client:handle_body(body)
if decoded.error then
local mute_error = false
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
local _ = log.debug() and log.debug('Received cancellation ack', decoded)
log_debug('Received cancellation ack', decoded)
mute_error = true
end
@@ -467,7 +488,7 @@ function Client:handle_body(body)
)
else
self:on_error(M.client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
local _ = log.error() and log.error('No callback found for server response id ' .. result_id)
log_error('No callback found for server response id ' .. result_id)
end
elseif type(decoded.method) == 'string' then
-- Notification
@@ -495,7 +516,14 @@ local function new_client(dispatchers, transport)
return setmetatable(state, { __index = Client })
end
--- @class RpcClientPublic
--- @field is_closing fun(): boolean
--- @field terminate fun()
--- @field request fun(method: string, params: table?, callback: function, notify_reply_callbacks?: function)
--- @field notify fun(methid: string, params: table?): boolean
---@param client RpcClient
---@return RpcClientPublic
local function public_client(client)
local result = {}
@@ -531,12 +559,14 @@ local function public_client(client)
return result
end
--- @param dispatchers vim.rpc.Dispatchers?
--- @return vim.rpc.Dispatchers
local function merge_dispatchers(dispatchers)
if dispatchers then
local user_dispatchers = dispatchers
dispatchers = {}
for dispatch_name, default_dispatch in pairs(default_dispatchers) do
local user_dispatcher = user_dispatchers[dispatch_name]
local user_dispatcher = user_dispatchers[dispatch_name] --- @type function
if user_dispatcher then
if type(user_dispatcher) ~= 'function' then
error(string.format('dispatcher.%s must be a function', dispatch_name))
@@ -547,8 +577,10 @@ local function merge_dispatchers(dispatchers)
then
user_dispatcher = schedule_wrap(user_dispatcher)
end
--- @diagnostic disable-next-line:no-unknown
dispatchers[dispatch_name] = user_dispatcher
else
--- @diagnostic disable-next-line:no-unknown
dispatchers[dispatch_name] = default_dispatch
end
end
@@ -567,7 +599,7 @@ end
function M.connect(host, port)
return function(dispatchers)
dispatchers = merge_dispatchers(dispatchers)
local tcp = uv.new_tcp()
local tcp = assert(uv.new_tcp())
local closing = false
local transport = {
write = function(msg)
@@ -624,15 +656,13 @@ end
--- server process. May contain:
--- - {cwd} (string) Working directory for the LSP server process
--- - {env} (table) Additional environment variables for LSP server process
---@return table|nil Client RPC object, with these methods:
---@return RpcClientPublic|nil Client RPC object, with these methods:
--- - `notify()` |vim.lsp.rpc.notify()|
--- - `request()` |vim.lsp.rpc.request()|
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
--- - `terminate()` terminates the RPC client.
function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
if log.info() then
log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
end
log_info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
validate({
cmd = { cmd, 's' },
@@ -671,8 +701,8 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
end)
local stderr_handler = function(_, chunk)
if chunk and log.error() then
log.error('rpc', cmd, 'stderr', chunk)
if chunk then
log_error('rpc', cmd, 'stderr', chunk)
end
end
@@ -697,13 +727,13 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
if not ok then
local err = sysobj_or_err --[[@as string]]
local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
local sfx --- @type string
if string.match(err, 'ENOENT') then
msg = msg
.. '. The language server is either not installed, missing from PATH, or not executable.'
sfx = '. The language server is either not installed, missing from PATH, or not executable.'
else
msg = msg .. string.format(' with error message: %s', err)
sfx = string.format(' with error message: %s', err)
end
local msg = string.format('Spawning language server with cmd: `%s` failed%s', cmd, sfx)
vim.notify(msg, vim.log.levels.WARN)
return
end