commit 285c04e2d0e3adc2a05d6fd8d0fdcee313ec1b08
parent 35a7642647858f7b4ddc204ee869c399b678e7e8
Author: Jaehwang Jung <tomtomjhj@gmail.com>
Date: Mon, 18 Aug 2025 03:37:24 +0900
fix(api,lsp): call on_detach before wiping out the buffer #35355
Problem:
Buffer-updates on_detach callback is invoked before buf_freeall(), which
deletes autocmds of the buffer (via apply_autocmds(EVENT_BUFWIPEOUT,
...)). Due to this, buffer-local autocmds executed in on_detach (e.g.,
LspDetach) are not actually invoked.
Solution:
Call buf_updates_unload() before buf_freeall().
Diffstat:
3 files changed, 76 insertions(+), 4 deletions(-)
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
@@ -656,6 +656,10 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
buf->b_nwindows = nwindows;
+ // Disable buffer-updates for the current buffer.
+ // No need to check `unload_buf`: in that case the function returned above.
+ buf_updates_unload(buf, false);
+
buf_freeall(buf, ((del_buf ? BFA_DEL : 0)
+ (wipe_buf ? BFA_WIPE : 0)
+ (ignore_abort ? BFA_IGNORE_ABORT : 0)));
@@ -678,10 +682,6 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
return false;
}
- // Disable buffer-updates for the current buffer.
- // No need to check `unload_buf`: in that case the function returned above.
- buf_updates_unload(buf, false);
-
if (win != NULL // Avoid bogus clang warning.
&& win_valid_any_tab(win)
&& win->w_buffer == buf) {
diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua
@@ -1426,3 +1426,44 @@ describe('lua: nvim_buf_attach on_bytes', function()
do_both(false)
end)
end)
+
+describe('nvim_buf_attach on_detach', function()
+ it('is invoked before unloading buffer', function()
+ exec_lua(function()
+ _G.logs = {} ---@type table<integer, string[]>
+ end)
+ local function setup(bufnr)
+ exec_lua(function()
+ _G.logs[bufnr] = {}
+ vim.api.nvim_create_autocmd({ 'BufUnload', 'BufWipeout' }, {
+ buffer = bufnr,
+ callback = function(ev)
+ table.insert(_G.logs[bufnr], ev.event)
+ end,
+ })
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_detach = function()
+ table.insert(_G.logs[bufnr], 'on_detach')
+ end,
+ })
+ end)
+ end
+ -- Test with two buffers because the :bw works differently for the last buffer.
+ -- Before #35355, the order was as follows:
+ -- * non-last buffers: BufUnload → BufWipeout → on_detach
+ -- * the last buffer (with text): BufUnload → on_detach → BufWipeout
+ local buf1 = api.nvim_get_current_buf()
+ local buf2 = api.nvim_create_buf(true, false)
+ api.nvim_open_win(buf2, false, { split = 'below' })
+ api.nvim_buf_set_lines(buf1, 0, -1, true, { 'abc' })
+ api.nvim_buf_set_lines(buf2, 0, -1, true, { 'abc' })
+ setup(buf1)
+ setup(buf2)
+ api.nvim_buf_delete(buf1, { force = true })
+ api.nvim_buf_delete(buf2, { force = true })
+ local logs = exec_lua('return _G.logs')
+ local order = { 'on_detach', 'BufUnload', 'BufWipeout' }
+ eq(order, logs[buf1])
+ eq(order, logs[buf2])
+ end)
+end)
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
@@ -624,6 +624,37 @@ describe('LSP', function()
eq(true, result.detach_called)
end)
+ it('should detach buffer on bufwipe 2', function()
+ exec_lua(create_server_definition)
+ local result = exec_lua(function()
+ local server = _G._create_server()
+ local bufnr1 = vim.api.nvim_create_buf(false, true)
+ local bufnr2 = vim.api.nvim_create_buf(false, true)
+ local detach_called1 = false
+ local detach_called2 = false
+ vim.api.nvim_create_autocmd('LspDetach', {
+ buffer = bufnr1,
+ callback = function()
+ detach_called1 = true
+ end,
+ })
+ vim.api.nvim_create_autocmd('LspDetach', {
+ buffer = bufnr2,
+ callback = function()
+ detach_called2 = true
+ end,
+ })
+ vim.api.nvim_set_current_buf(bufnr1)
+ vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })
+ vim.api.nvim_set_current_buf(bufnr2)
+ vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })
+ vim.api.nvim_buf_delete(bufnr1, { force = true })
+ vim.api.nvim_buf_delete(bufnr2, { force = true })
+ return detach_called1 and detach_called2
+ end)
+ eq(true, result)
+ end)
+
it('should not re-attach buffer if it was deleted in on_init #28575', function()
exec_lua(create_server_definition)
exec_lua(function()