commit a03ab03a10ada68b74e292a16493047da414e3ee
parent 3ac76977bca5ce204fea856d69d52895f67332e5
Author: Olivia Kinnear <git@superatomic.dev>
Date: Fri, 2 Jan 2026 01:58:10 -0500
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.
Diffstat:
4 files changed, 42 insertions(+), 30 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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()