fix(lsp): detect if Client:request resolved synchronously #33624

Problem:
In cases when the (in-process) LSP server responds to the request
immediately and calls `notify_reply_callback` the request will still be
marked as pending, because the code assumes that the response will occur
asynchronously. Then the request will be pending forever, because it was
already set as "completed" before we even set it as "pending".

A workaround is to wrap `notify_replay_callback` in `vim.shedule` ([like
so](https://github.com/neovim/neovim/pull/24338#issuecomment-2809568617)]
but that seems counterintuitive.

Solution:
Handle this case in Client:request().

(cherry picked from commit 8315697449)
This commit is contained in:
Bartłomiej Maryńczak
2025-04-26 16:08:03 +02:00
committed by github-actions[bot]
parent 32842b0ee3
commit f184c562c5
2 changed files with 73 additions and 2 deletions

View File

@ -678,6 +678,12 @@ function Client:request(method, params, handler, bufnr)
bufnr = vim._resolve_bufnr(bufnr) bufnr = vim._resolve_bufnr(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)
-- Detect if request resolved synchronously (only possible with in-process servers).
local already_responded = false
local request_registered = false
-- NOTE: rpc.request might call an in-process (Lua) server, thus may be synchronous.
local success, request_id = self.rpc.request(method, params, function(err, result) local success, request_id = self.rpc.request(method, params, function(err, result)
handler(err, result, { handler(err, result, {
method = method, method = method,
@ -688,11 +694,15 @@ function Client:request(method, params, handler, bufnr)
}) })
end, function(request_id) end, function(request_id)
-- Called when the server sends a response to the request (including cancelled acknowledgment). -- Called when the server sends a response to the request (including cancelled acknowledgment).
self:_process_request(request_id, 'complete') if request_registered then
self:_process_request(request_id, 'complete')
end
already_responded = true
end) end)
if success and request_id then if success and request_id and not already_responded then
self:_process_request(request_id, 'pending', bufnr, method) self:_process_request(request_id, 'pending', bufnr, method)
request_registered = true
end end
return success, request_id return success, request_id

View File

@ -1252,6 +1252,67 @@ describe('LSP', function()
} }
end) end)
it('request should not be pending for sync responses (in-process LS)', function()
clear()
--- @type boolean
local pending_request = exec_lua(function()
local function server(dispatchers)
local closing = false
local srv = {}
local request_id = 0
function srv.request(method, _params, callback, notify_reply_callback)
if method == 'textDocument/formatting' then
callback(nil, {})
elseif method == 'initialize' then
callback(nil, {
capabilities = {
textDocument = {
formatting = true,
},
},
})
elseif method == 'shutdown' then
callback(nil, nil)
end
request_id = request_id + 1
if notify_reply_callback then
notify_reply_callback(request_id)
end
return true, request_id
end
function srv.notify(method)
if method == 'exit' then
dispatchers.on_exit(0, 15)
end
end
function srv.is_closing()
return closing
end
function srv.terminate()
closing = true
end
return srv
end
local client_id = assert(vim.lsp.start({ cmd = server }))
local client = assert(vim.lsp.get_client_by_id(client_id))
local ok, request_id = client:request('textDocument/formatting', {})
assert(ok)
local has_pending = client.requests[request_id] ~= nil
vim.lsp.stop_client(client_id)
return has_pending
end)
eq(false, pending_request, 'expected no pending requests')
end)
it('should trigger LspRequest autocmd when requests table changes', function() it('should trigger LspRequest autocmd when requests table changes', function()
local expected_handlers = { local expected_handlers = {
{ NIL, {}, { method = 'finish', client_id = 1 } }, { NIL, {}, { method = 'finish', client_id = 1 } },