mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00
fix(api): on_detach consistently before buf_freeall autocmds (#35369)
Problem: on_detach may be called after buf_freeall and other important things,
plus its textlock restrictions are insufficient. This can cause issues such as
leaks, internal errors and crashes.
Solution: disable buffer updates in buf_freeall, before autocommands (like the
order after #35355 and when do_ecmd reloads a buffer). Don't do so in
free_buffer_stuff; it's not safe to run user code there, and buf_freeall already
runs before then; just free them to avoid leaks if buf_freeall autocommands
registered more for some reason.
(cherry picked from commit 2211953266
)
Co-authored-by: Sean Dewar <6256228+seandewar@users.noreply.github.com>
This commit is contained in:
![175700243+neovim-backports[bot]@users.noreply.github.com](/assets/img/avatar_default.png)
committed by
GitHub

parent
37b2d42459
commit
3ab06d5188
@@ -657,10 +657,6 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
|
|||||||
|
|
||||||
buf->b_nwindows = nwindows;
|
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)
|
buf_freeall(buf, ((del_buf ? BFA_DEL : 0)
|
||||||
+ (wipe_buf ? BFA_WIPE : 0)
|
+ (wipe_buf ? BFA_WIPE : 0)
|
||||||
+ (ignore_abort ? BFA_IGNORE_ABORT : 0)));
|
+ (ignore_abort ? BFA_IGNORE_ABORT : 0)));
|
||||||
@@ -789,6 +785,11 @@ void buf_freeall(buf_T *buf, int flags)
|
|||||||
bufref_T bufref;
|
bufref_T bufref;
|
||||||
set_bufref(&bufref, buf);
|
set_bufref(&bufref, buf);
|
||||||
|
|
||||||
|
buf_updates_unload(buf, false);
|
||||||
|
if (!bufref_valid(&bufref)) {
|
||||||
|
// on_detach callback deleted the buffer.
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ((buf->b_ml.ml_mfp != NULL)
|
if ((buf->b_ml.ml_mfp != NULL)
|
||||||
&& apply_autocmds(EVENT_BUFUNLOAD, buf->b_fname, buf->b_fname, false, buf)
|
&& apply_autocmds(EVENT_BUFUNLOAD, buf->b_fname, buf->b_fname, false, buf)
|
||||||
&& !bufref_valid(&bufref)) {
|
&& !bufref_valid(&bufref)) {
|
||||||
@@ -935,7 +936,7 @@ static void free_buffer_stuff(buf_T *buf, int free_flags)
|
|||||||
map_clear_mode(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
|
map_clear_mode(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
|
||||||
XFREE_CLEAR(buf->b_start_fenc);
|
XFREE_CLEAR(buf->b_start_fenc);
|
||||||
|
|
||||||
buf_updates_unload(buf, false);
|
buf_free_callbacks(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go to another buffer. Handles the result of the ATTENTION dialog.
|
/// Go to another buffer. Handles the result of the ATTENTION dialog.
|
||||||
|
@@ -2512,13 +2512,11 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum
|
|||||||
goto theend;
|
goto theend;
|
||||||
}
|
}
|
||||||
u_unchanged(curbuf);
|
u_unchanged(curbuf);
|
||||||
buf_updates_unload(curbuf, false);
|
|
||||||
buf_freeall(curbuf, BFA_KEEP_UNDO);
|
buf_freeall(curbuf, BFA_KEEP_UNDO);
|
||||||
|
|
||||||
// Tell readfile() not to clear or reload undo info.
|
// Tell readfile() not to clear or reload undo info.
|
||||||
readfile_flags = READ_KEEP_UNDO;
|
readfile_flags = READ_KEEP_UNDO;
|
||||||
} else {
|
} else {
|
||||||
buf_updates_unload(curbuf, false);
|
|
||||||
buf_freeall(curbuf, 0); // Free all things for buffer.
|
buf_freeall(curbuf, 0); // Free all things for buffer.
|
||||||
}
|
}
|
||||||
// If autocommands deleted the buffer we were going to re-edit, give
|
// If autocommands deleted the buffer we were going to re-edit, give
|
||||||
|
@@ -14,6 +14,8 @@ local feed = n.feed
|
|||||||
local expect_events = t.expect_events
|
local expect_events = t.expect_events
|
||||||
local write_file = t.write_file
|
local write_file = t.write_file
|
||||||
local dedent = t.dedent
|
local dedent = t.dedent
|
||||||
|
local matches = t.matches
|
||||||
|
local pcall_err = t.pcall_err
|
||||||
|
|
||||||
local origlines = {
|
local origlines = {
|
||||||
'original line 1',
|
'original line 1',
|
||||||
@@ -1386,42 +1388,138 @@ describe('lua: nvim_buf_attach on_bytes', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
describe('nvim_buf_attach on_detach', function()
|
describe('nvim_buf_attach on_detach', function()
|
||||||
it('is invoked before unloading buffer', function()
|
it('called before buf_freeall autocommands', function()
|
||||||
exec_lua(function()
|
exec_lua(function()
|
||||||
_G.logs = {} ---@type table<integer, string[]>
|
vim.api.nvim_create_autocmd({ 'BufUnload', 'BufDelete', 'BufWipeout' }, {
|
||||||
|
callback = function(args)
|
||||||
|
table.insert(
|
||||||
|
_G.events,
|
||||||
|
('%s: %d %s'):format(
|
||||||
|
args.event,
|
||||||
|
args.buf,
|
||||||
|
tostring(vim.api.nvim_buf_is_loaded(args.buf))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
function _G.on_detach(_, b)
|
||||||
|
table.insert(
|
||||||
|
_G.events,
|
||||||
|
('on_detach: %d %s'):format(b, tostring(vim.api.nvim_buf_is_loaded(b)))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
_G.events = {}
|
||||||
|
vim.cmd 'new'
|
||||||
|
vim.bo.bufhidden = 'wipe'
|
||||||
|
vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
|
||||||
|
vim.cmd 'quit!'
|
||||||
end)
|
end)
|
||||||
local function setup(bufnr)
|
|
||||||
exec_lua(function()
|
eq(
|
||||||
_G.logs[bufnr] = {}
|
{ 'on_detach: 2 true', 'BufUnload: 2 true', 'BufDelete: 2 true', 'BufWipeout: 2 true' },
|
||||||
vim.api.nvim_create_autocmd({ 'BufUnload', 'BufWipeout' }, {
|
exec_lua('return _G.events')
|
||||||
buffer = bufnr,
|
)
|
||||||
callback = function(ev)
|
eq(false, api.nvim_buf_is_valid(2))
|
||||||
table.insert(_G.logs[bufnr], ev.event)
|
|
||||||
end,
|
exec_lua(function()
|
||||||
})
|
_G.events = {}
|
||||||
vim.api.nvim_buf_attach(bufnr, false, {
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
on_detach = function()
|
vim.api.nvim_buf_attach(buf, false, { on_detach = _G.on_detach })
|
||||||
table.insert(_G.logs[bufnr], 'on_detach')
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
end,
|
end)
|
||||||
})
|
|
||||||
end)
|
-- Was unlisted, so no BufDelete.
|
||||||
end
|
eq(
|
||||||
-- Test with two buffers because the :bw works differently for the last buffer.
|
{ 'on_detach: 3 true', 'BufUnload: 3 true', 'BufWipeout: 3 true' },
|
||||||
-- Before #35355, the order was as follows:
|
exec_lua('return _G.events')
|
||||||
-- * non-last buffers: BufUnload → BufWipeout → on_detach
|
)
|
||||||
-- * the last buffer (with text): BufUnload → on_detach → BufWipeout
|
eq(false, api.nvim_buf_is_valid(3))
|
||||||
local buf1 = api.nvim_get_current_buf()
|
|
||||||
local buf2 = api.nvim_create_buf(true, false)
|
exec_lua(function()
|
||||||
api.nvim_open_win(buf2, false, { split = 'below' })
|
_G.events = {}
|
||||||
api.nvim_buf_set_lines(buf1, 0, -1, true, { 'abc' })
|
vim.api.nvim_buf_attach(1, false, { on_detach = _G.on_detach })
|
||||||
api.nvim_buf_set_lines(buf2, 0, -1, true, { 'abc' })
|
vim.api.nvim_create_autocmd('BufUnload', {
|
||||||
setup(buf1)
|
buffer = 1,
|
||||||
setup(buf2)
|
once = true,
|
||||||
api.nvim_buf_delete(buf1, { force = true })
|
callback = function()
|
||||||
api.nvim_buf_delete(buf2, { force = true })
|
vim.api.nvim_buf_attach(1, false, {
|
||||||
local logs = exec_lua('return _G.logs')
|
on_detach = function(...)
|
||||||
local order = { 'on_detach', 'BufUnload', 'BufWipeout' }
|
vim.fn.bufload(1) -- Leaks the memfile it were to run inside free_buffer_stuff.
|
||||||
eq(order, logs[buf1])
|
return _G.on_detach(...)
|
||||||
eq(order, logs[buf2])
|
end,
|
||||||
|
})
|
||||||
|
table.insert(_G.events, 'local BufUnload')
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.cmd 'edit asdf' -- Reuses buffer 1.
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- on_detach shouldn't run after autocommands when reusing a buffer (in free_buffer_stuff), even
|
||||||
|
-- if those autocommands registered it, as curbuf may be in a semi-unloaded state at that point.
|
||||||
|
eq({
|
||||||
|
'on_detach: 1 true',
|
||||||
|
'BufUnload: 1 true',
|
||||||
|
'local BufUnload',
|
||||||
|
'BufDelete: 1 true',
|
||||||
|
'BufWipeout: 1 true',
|
||||||
|
}, exec_lua('return _G.events'))
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
_G.events = {}
|
||||||
|
vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
|
||||||
|
vim.cmd 'edit'
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Re-edit buffer; on_detach is called.
|
||||||
|
eq({ 'on_detach: 1 true', 'BufUnload: 1 true' }, exec_lua('return _G.events'))
|
||||||
|
eq(true, api.nvim_buf_is_valid(1))
|
||||||
|
|
||||||
|
exec_lua(function()
|
||||||
|
vim.cmd '%bwipeout!'
|
||||||
|
vim.bo.modified = true
|
||||||
|
_G.events = {}
|
||||||
|
vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
|
||||||
|
vim.api.nvim_buf_delete(0, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- on_detach must still be first when wiping the last buffer if it's listed and non-reusable.
|
||||||
|
-- Previously: BufUnload → BufDelete → on_detach → BufWipeout.
|
||||||
|
eq(
|
||||||
|
{ 'on_detach: 4 true', 'BufUnload: 4 true', 'BufDelete: 4 true', 'BufWipeout: 4 false' },
|
||||||
|
exec_lua('return _G.events')
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('disallows splitting', function()
|
||||||
|
command('new | setlocal bufhidden=wipe')
|
||||||
|
local buf = api.nvim_get_current_buf()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.api.nvim_buf_attach(0, false, {
|
||||||
|
on_detach = function()
|
||||||
|
-- Used to allow opening more views into a closing buffer, resulting in open windows to an
|
||||||
|
-- unloaded buffer.
|
||||||
|
vim.cmd [=[execute "normal! \<C-W>s"]=]
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
matches('E1159: Cannot split a window when closing the buffer$', pcall_err(command, 'quit!'))
|
||||||
|
eq({}, fn.win_findbuf(buf))
|
||||||
|
eq(false, api.nvim_buf_is_valid(buf))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('nvim_buf_attach from buf_freeall autocommands does not leak', function()
|
||||||
|
exec_lua(function()
|
||||||
|
local b = vim.api.nvim_create_buf(true, true)
|
||||||
|
vim.api.nvim_create_autocmd('BufWipeout', {
|
||||||
|
buffer = b,
|
||||||
|
once = true,
|
||||||
|
callback = function()
|
||||||
|
vim.api.nvim_buf_attach(b, false, {})
|
||||||
|
_G.autocmd_fired = true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.api.nvim_buf_delete(b, { force = true })
|
||||||
|
end)
|
||||||
|
eq(true, exec_lua('return _G.autocmd_fired'))
|
||||||
|
end)
|
||||||
|
Reference in New Issue
Block a user