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.
This commit is contained in:
Olivia Kinnear
2026-01-02 01:58:10 -05:00
committed by GitHub
parent 3ac76977bc
commit a03ab03a10
4 changed files with 42 additions and 30 deletions

View File

@@ -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*

View File

@@ -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

View File

@@ -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()

View File

@@ -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()