From a03ab03a10ada68b74e292a16493047da414e3ee Mon Sep 17 00:00:00 2001 From: Olivia Kinnear Date: Fri, 2 Jan 2026 01:58:10 -0500 Subject: [PATCH] fix(lsp): `:lsp restart` restarts on client exit #37125 Problem: `:lsp restart` detects when a client has exited by using the `LspDetach` autocommand. This works correctly in common cases, but breaks when restarting a client which is not attached to any buffer. It also breaks if a client is detached in between `:lsp restart` and the actual stopping of the client. Solution: Move restart logic into `vim/lsp/client.lua`, so it can hook in to `_on_exit()`. The public `on_exit` callback cannot be used for this, as `:lsp restart` needs to ensure the restart only happens once, even if the command is run multiple times on the same client. --- runtime/doc/lsp.txt | 2 +- runtime/lua/vim/_core/ex_cmd.lua | 26 ++---------------------- runtime/lua/vim/lsp/client.lua | 30 ++++++++++++++++++++++++++++ test/functional/ex_cmds/lsp_spec.lua | 14 ++++++++----- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 7c44ea8b89..33fb48ea6f 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -123,7 +123,7 @@ COMMANDS *:lsp* *lsp-commands* Activates LSP for current and future buffers. See |vim.lsp.enable()|. :lsp disable [config_name] *:lsp-disable* - Disables (and stops) LSP for current and future buffers. See + Disables LSP (and stops if running) for current and future buffers. See |vim.lsp.enable()|. :lsp restart [client_name] *:lsp-restart* diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua index 6c482cbc7e..0b5189cae8 100644 --- a/runtime/lua/vim/_core/ex_cmd.lua +++ b/runtime/lua/vim/_core/ex_cmd.lua @@ -122,7 +122,7 @@ end --- @param client_names string[] --- @return vim.lsp.Client[] local function get_clients_from_names(client_names) - -- Default to stopping all active clients attached to the current buffer. + -- Default to all active clients attached to the current buffer. if #client_names == 0 then local clients = lsp.get_clients { bufnr = api.nvim_get_current_buf() } if #clients == 0 then @@ -149,29 +149,7 @@ local function ex_lsp_restart(client_names) local clients = get_clients_from_names(client_names) for _, client in ipairs(clients) do - --- @type integer[] - local attached_buffers = vim.tbl_keys(client.attached_buffers) - - -- Reattach new client once the old one exits - api.nvim_create_autocmd('LspDetach', { - group = api.nvim_create_augroup('nvim.lsp.ex_restart_' .. client.id, {}), - callback = function(info) - if info.data.client_id ~= client.id then - return - end - - local new_client_id = lsp.start(client.config, { attach = false }) - if new_client_id then - for _, buffer in ipairs(attached_buffers) do - lsp.buf_attach_client(buffer, new_client_id) - end - end - - return true -- Delete autocmd - end, - }) - - client:stop(client.exit_timeout) + client:_restart(client.exit_timeout) end end diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 333e123833..014ee08e17 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -908,6 +908,31 @@ function Client:stop(force) end) end +--- Stops a client, then starts a new client with the same config and attached +--- buffers. +--- +--- @param force? integer|boolean See [Client:stop()] for details. +--- (default: `self.exit_timeout`) +function Client:_restart(force) + validate('force', force, { 'number', 'boolean' }, true) + + self._handle_restart = function() + --- @type integer[] + local attached_buffers = vim.tbl_keys(self.attached_buffers) + + vim.schedule(function() + local new_client_id = lsp.start(self.config, { attach = false }) + if new_client_id then + for _, buffer in ipairs(attached_buffers) do + lsp.buf_attach_client(buffer, new_client_id) + end + end + end) + end + + self:stop(force) +end + --- Get options for a method that is registered dynamically. --- @param method vim.lsp.protocol.Method | vim.lsp.protocol.Method.Registration function Client:_supports_registration(method) @@ -1334,6 +1359,11 @@ function Client:_on_exit(code, signal) end end) + if self._handle_restart ~= nil then + self._handle_restart() + self._handle_restart = nil + end + -- Schedule the deletion of the client object so that it exists in the execution of LspDetach -- autocommands vim.schedule(function() diff --git a/test/functional/ex_cmds/lsp_spec.lua b/test/functional/ex_cmds/lsp_spec.lua index 47b52aaa56..12debc3372 100644 --- a/test/functional/ex_cmds/lsp_spec.lua +++ b/test/functional/ex_cmds/lsp_spec.lua @@ -62,18 +62,22 @@ describe(':lsp', function() end) it('restart' .. test_message_suffix, function() - local ids_differ = exec_lua(function() + --- @type boolean, integer? + local ids_differ, attached_buffer_count = exec_lua(function() vim.lsp.enable('dummy') local old_id = vim.lsp.get_clients()[1].id vim.cmd('lsp restart' .. lsp_command_suffix) - vim.wait(1000, function() - return old_id ~= vim.lsp.get_clients()[1].id + return vim.wait(1000, function() + local new_client = vim.lsp.get_clients()[1] + if new_client == nil then + return false + end + return old_id ~= new_client.id, #new_client.attached_buffers end) - local new_id = vim.lsp.get_clients()[1].id - return old_id ~= new_id end) eq(true, ids_differ) + eq(1, attached_buffer_count) end) it('stop' .. test_message_suffix, function()