neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 83156974497df69a5f449c15d1fb472afdf7b6ff
parent e45ec5a8527e155c2df68edd69c149f1342486a2
Author: Bartłomiej Maryńczak <marynczakbartlomiej@gmail.com>
Date:   Sat, 26 Apr 2025 16:08:03 +0200

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().
Diffstat:
Mruntime/lua/vim/lsp/client.lua | 14++++++++++++--
Mtest/functional/plugin/lsp_spec.lua | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 73 insertions(+), 2 deletions(-)

diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua @@ -678,6 +678,12 @@ function Client:request(method, params, handler, bufnr) bufnr = vim._resolve_bufnr(bufnr) local version = lsp.util.buf_versions[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) handler(err, result, { method = method, @@ -688,11 +694,15 @@ function Client:request(method, params, handler, bufnr) }) end, function(request_id) -- 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) - if success and request_id then + if success and request_id and not already_responded then self:_process_request(request_id, 'pending', bufnr, method) + request_registered = true end return success, request_id diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua @@ -1252,6 +1252,67 @@ describe('LSP', function() } 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() local expected_handlers = { { NIL, {}, { method = 'finish', client_id = 1 } },