mirror of
https://github.com/neovim/neovim.git
synced 2025-10-21 09:12:07 +00:00
feat(lsp): more annotations
This commit is contained in:

committed by
Lewis Russell

parent
320e9c1c21
commit
97bea3163a
@@ -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
|
||||
|
Reference in New Issue
Block a user