refactor(lsp): better tracking of requests

Not essential, but adds robustness and hardening for future
changes.
This commit is contained in:
Lewis Russell 2024-12-07 10:39:56 +00:00 committed by Lewis Russell
parent ca4f688ad4
commit 4bfdd1ee9d
2 changed files with 72 additions and 48 deletions

View File

@ -946,7 +946,7 @@ Lua module: vim.lsp.client *lsp-client*
• {handlers} (`table<string,lsp.Handler>`) The handlers • {handlers} (`table<string,lsp.Handler>`) The handlers
used by the client as described in used by the client as described in
|lsp-handler|. |lsp-handler|.
• {requests} (`table<integer,{ type: string, bufnr: integer, method: string}>`) • {requests} (`table<integer,{ type: string, bufnr: integer, method: string}?>`)
The current pending requests in flight to the The current pending requests in flight to the
server. Entries are key-value pairs with the server. Entries are key-value pairs with the
key being the request id while the value is a key being the request id while the value is a
@ -982,9 +982,9 @@ Lua module: vim.lsp.client *lsp-client*
lenses, ...) triggers the command. Client lenses, ...) triggers the command. Client
commands take precedence over the global commands take precedence over the global
command registry. command registry.
• {settings} (`table`) Map with language server specific • {settings} (`lsp.LSPObject`) Map with language server
settings. These are returned to the language specific settings. These are returned to the
server if requested via language server if requested via
`workspace/configuration`. Keys are `workspace/configuration`. Keys are
case-sensitive. case-sensitive.
• {flags} (`table`) A table with flags for the client. • {flags} (`table`) A table with flags for the client.
@ -1077,8 +1077,8 @@ Lua module: vim.lsp.client *lsp-client*
an array. an array.
• {handlers}? (`table<string,function>`) Map of language • {handlers}? (`table<string,function>`) Map of language
server method names to |lsp-handler| server method names to |lsp-handler|
• {settings}? (`table`) Map with language server specific • {settings}? (`lsp.LSPObject`) Map with language server
settings. See the {settings} in specific settings. See the {settings} in
|vim.lsp.Client|. |vim.lsp.Client|.
• {commands}? (`table<string,fun(command: lsp.Command, ctx: table)>`) • {commands}? (`table<string,fun(command: lsp.Command, ctx: table)>`)
Table that maps string of clientside commands to Table that maps string of clientside commands to
@ -1088,9 +1088,10 @@ Lua module: vim.lsp.client *lsp-client*
command name, and the value is a function which command name, and the value is a function which
is called if any LSP action (code action, code is called if any LSP action (code action, code
lenses, ...) triggers the command. lenses, ...) triggers the command.
• {init_options}? (`table`) Values to pass in the initialization • {init_options}? (`lsp.LSPObject`) Values to pass in the
request as `initializationOptions`. See initialization request as
`initialize` in the LSP spec. `initializationOptions`. See `initialize` in the
LSP spec.
• {name}? (`string`, default: client-id) Name in log • {name}? (`string`, default: client-id) Name in log
messages. messages.
• {get_language_id}? (`fun(bufnr: integer, filetype: string): string`) • {get_language_id}? (`fun(bufnr: integer, filetype: string): string`)

View File

@ -75,7 +75,7 @@ local validate = vim.validate
--- ---
--- Map with language server specific settings. --- Map with language server specific settings.
--- See the {settings} in |vim.lsp.Client|. --- See the {settings} in |vim.lsp.Client|.
--- @field settings? table --- @field settings? lsp.LSPObject
--- ---
--- Table that maps string of clientside commands to user-defined functions. --- Table that maps string of clientside commands to user-defined functions.
--- Commands passed to `start()` take precedence over the global command registry. Each key --- Commands passed to `start()` take precedence over the global command registry. Each key
@ -85,7 +85,7 @@ local validate = vim.validate
--- ---
--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in --- Values to pass in the initialization request as `initializationOptions`. See `initialize` in
--- the LSP spec. --- the LSP spec.
--- @field init_options? table --- @field init_options? lsp.LSPObject
--- ---
--- Name in log messages. --- Name in log messages.
--- (default: client-id) --- (default: client-id)
@ -164,7 +164,7 @@ local validate = vim.validate
--- for an active request, or "cancel" for a cancel request. It will be --- for an active request, or "cancel" for a cancel request. It will be
--- "complete" ephemerally while executing |LspRequest| autocmds when replies --- "complete" ephemerally while executing |LspRequest| autocmds when replies
--- are received from the server. --- are received from the server.
--- @field requests table<integer,{ type: string, bufnr: integer, method: string}> --- @field requests table<integer,{ type: string, bufnr: integer, method: string}?>
--- ---
--- copy of the table that was passed by the user --- copy of the table that was passed by the user
--- to |vim.lsp.start()|. --- to |vim.lsp.start()|.
@ -189,9 +189,6 @@ local validate = vim.validate
--- ---
--- @field attached_buffers table<integer,true> --- @field attached_buffers table<integer,true>
--- ---
--- Buffers that should be attached to upon initialize()
--- @field package _buffers_to_attach table<integer,true>
---
--- @field private _log_prefix string --- @field private _log_prefix string
--- ---
--- Track this so that we can escalate automatically if we've already tried a --- Track this so that we can escalate automatically if we've already tried a
@ -210,7 +207,7 @@ local validate = vim.validate
--- Map with language server specific settings. These are returned to the --- Map with language server specific settings. These are returned to the
--- language server if requested via `workspace/configuration`. Keys are --- language server if requested via `workspace/configuration`. Keys are
--- case-sensitive. --- case-sensitive.
--- @field settings table --- @field settings lsp.LSPObject
--- ---
--- A table with flags for the client. The current (experimental) flags are: --- A table with flags for the client. The current (experimental) flags are:
--- @field flags vim.lsp.Client.Flags --- @field flags vim.lsp.Client.Flags
@ -265,9 +262,6 @@ local valid_encodings = {
['utf8'] = 'utf-8', ['utf8'] = 'utf-8',
['utf16'] = 'utf-16', ['utf16'] = 'utf-16',
['utf32'] = 'utf-32', ['utf32'] = 'utf-32',
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32',
} }
--- Normalizes {encoding} to valid LSP encoding names. --- Normalizes {encoding} to valid LSP encoding names.
@ -276,7 +270,7 @@ local valid_encodings = {
local function validate_encoding(encoding) local function validate_encoding(encoding)
validate('encoding', encoding, 'string', true) validate('encoding', encoding, 'string', true)
if not encoding then if not encoding then
return valid_encodings.UTF16 return valid_encodings.utf16
end end
return valid_encodings[encoding:lower()] return valid_encodings[encoding:lower()]
or error( or error(
@ -604,6 +598,57 @@ function Client:_resolve_handler(method)
return self.handlers[method] or lsp.handlers[method] return self.handlers[method] or lsp.handlers[method]
end end
--- @private
--- @param id integer
--- @param req_type 'pending'|'complete'|'cancel'|
--- @param bufnr? integer (only required for req_type='pending')
--- @param method? string (only required for req_type='pending')
function Client:_process_request(id, req_type, bufnr, method)
local pending = req_type == 'pending'
validate('id', id, 'number')
if pending then
validate('bufnr', bufnr, 'number')
validate('method', method, 'string')
end
local cur_request = self.requests[id]
if pending and cur_request then
log.error(
self._log_prefix,
('Cannot create request with id %d as one already exists'):format(id)
)
return
elseif not pending and not cur_request then
log.error(
self._log_prefix,
('Cannot find request with id %d whilst attempting to %s'):format(id, req_type)
)
return
end
if cur_request then
bufnr = cur_request.bufnr
method = cur_request.method
end
assert(bufnr and method)
local request = { type = req_type, bufnr = bufnr, method = method }
-- Clear 'complete' requests
-- Note 'pending' and 'cancelled' requests are cleared when the server sends a response
-- which is processed via the notify_reply_callback argument to rpc.request.
self.requests[id] = req_type ~= 'complete' and request or nil
api.nvim_exec_autocmds('LspRequest', {
buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
modeline = false,
data = { client_id = self.id, request_id = id, request = request },
})
end
--- Sends a request to the server. --- Sends a request to the server.
--- ---
--- This is a thin wrapper around {client.rpc.request} with some additional --- This is a thin wrapper around {client.rpc.request} with some additional
@ -632,33 +677,20 @@ function Client:request(method, params, handler, bufnr)
local version = lsp.util.buf_versions[bufnr] local version = lsp.util.buf_versions[bufnr]
log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr)
local success, request_id = self.rpc.request(method, params, function(err, result) local success, request_id = self.rpc.request(method, params, function(err, result)
local context = { handler(err, result, {
method = method, method = method,
client_id = self.id, client_id = self.id,
bufnr = bufnr, bufnr = bufnr,
params = params, params = params,
version = version, version = version,
}
handler(err, result, context)
end, function(request_id)
local request = self.requests[request_id]
request.type = 'complete'
api.nvim_exec_autocmds('LspRequest', {
buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
modeline = false,
data = { client_id = self.id, request_id = request_id, request = request },
}) })
self.requests[request_id] = nil end, function(request_id)
-- Called when the server sends a response to the request (including cancelled acknowledgment).
self:_process_request(request_id, 'complete')
end) end)
if success and request_id then if success and request_id then
local request = { type = 'pending', bufnr = bufnr, method = method } self:_process_request(request_id, 'pending', bufnr, method)
self.requests[request_id] = request
api.nvim_exec_autocmds('LspRequest', {
buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
modeline = false,
data = { client_id = self.id, request_id = request_id, request = request },
})
end end
return success, request_id return success, request_id
@ -756,16 +788,7 @@ end
--- @return boolean status indicating if the notification was successful. --- @return boolean status indicating if the notification was successful.
--- @see |Client:notify()| --- @see |Client:notify()|
function Client:cancel_request(id) function Client:cancel_request(id)
validate('id', id, 'number') self:_process_request(id, 'cancel')
local request = self.requests[id]
if request and request.type == 'pending' then
request.type = 'cancel'
api.nvim_exec_autocmds('LspRequest', {
buffer = api.nvim_buf_is_valid(request.bufnr) and request.bufnr or nil,
modeline = false,
data = { client_id = self.id, request_id = id, request = request },
})
end
return self.rpc.notify(ms.dollar_cancelRequest, { id = id }) return self.rpc.notify(ms.dollar_cancelRequest, { id = id })
end end