fix(statusline): defer redraw in aucmd context #40309

Problem: `nvim_exec_autocmds({buf=...})` may temporarily switch curwin/curbuf
through `aucmd_prepbuf()`. Requested statuslines/winbars before
`aucmd_restbuf()` may erroneously see the target window as current.

Solution: Track `aucmd_prepbuf()` window-switch depth and leave statusline/winbar
redraws marked dirty until the original window is restored.
This commit is contained in:
Barrett Ruth
2026-06-23 05:05:11 -05:00
committed by GitHub
parent 487176dcfc
commit e4fbe162c1
4 changed files with 125 additions and 5 deletions

View File

@@ -1346,6 +1346,9 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf)
curbuf = buf;
aco->new_curwin_handle = curwin->handle;
set_bufref(&aco->new_curbuf, curbuf);
if (aco->new_curwin_handle != aco->save_curwin_handle) {
aucmd_prepbuf_depth++;
}
aco->save_VIsual_active = VIsual_active;
if (!same_buffer) {
@@ -1481,6 +1484,10 @@ win_found:
if (VIsual_active) {
check_pos(curbuf, &VIsual);
}
if (aco->new_curwin_handle != aco->save_curwin_handle) {
assert(aucmd_prepbuf_depth > 0);
aucmd_prepbuf_depth--;
}
}
/// Schedules an autocommand event, to be executed at the next event-loop tick.

View File

@@ -22,6 +22,7 @@ EXTERN win_T *last_cursormoved_win INIT( = NULL);
EXTERN pos_T last_cursormoved INIT( = { 0, 0, 0 });
EXTERN bool autocmd_busy INIT( = false); ///< Is apply_autocmds() busy?
EXTERN int aucmd_prepbuf_depth INIT( = 0); ///< Nested aucmd_prepbuf() window switches
EXTERN int autocmd_no_enter INIT( = false); ///< Buf/WinEnter autocmds disabled
EXTERN int autocmd_no_leave INIT( = false); ///< Buf/WinLeave autocmds disabled

View File

@@ -9,6 +9,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
@@ -62,6 +63,18 @@ typedef enum {
kNumBaseHexadecimal = 16,
} NumberBase;
static bool stl_defer_redraw(win_T *wp)
{
if (aucmd_prepbuf_depth == 0) {
return false;
}
// aucmd_prepbuf() temporarily changes curwin/curbuf. Statusline and winbar
// expressions can observe that context, so evaluate them after aucmd_restbuf().
wp->w_redr_status = true;
return true;
}
/// Redraw the status line of window `wp`.
///
/// If inversion is possible we use it. Else '=' characters are used.
@@ -77,6 +90,9 @@ void win_redr_status(win_T *wp)
|| (wild_menu_showing != 0 && !ui_has(kUIWildmenu))) {
return;
}
if (stl_defer_redraw(wp)) {
return;
}
busy = true;
wp->w_redr_status = false;
if (wp->w_status_height == 0 && !(is_stl_global && wp == curwin)) {
@@ -432,6 +448,9 @@ void win_redr_winbar(win_T *wp)
if (entered) {
return;
}
if (stl_defer_redraw(wp)) {
return;
}
entered = true;
if (wp->w_winbar_height == 0 || !redrawing()) {

View File

@@ -959,13 +959,16 @@ describe('statusline', function()
it('no cmdline ruler for autocmd window #39938', function()
command('set ruler laststatus=2')
api.nvim_create_autocmd('BufDelete', { command = 'redrawstatus' })
api.nvim_exec_autocmds('BufDelete', { buf = api.nvim_create_buf(true, true) })
screen:expect([[
local expected = [[
^ |
{1:~ }|*5
{2:[No Name] 0,0-1 All}|
{3:[No Name] 0,0-1 All}|
|
]])
]]
screen:expect(expected)
api.nvim_exec_autocmds('BufDelete', { buf = api.nvim_create_buf(true, true) })
-- The first matching frame can arrive before the deferred redraw settles.
screen:expect_unchanged(true)
end)
it('active window is correct after nvim_exec_autocmds({buf}) #40153', function()
@@ -976,17 +979,32 @@ describe('statusline', function()
api.nvim_set_current_buf(target)
api.nvim_set_current_win(caller_win)
exec_lua(function(buf)
_G.in_aucmd_statusline = false
_G.statusline_contexts = {}
_G.Status = function()
return vim.api.nvim_get_current_win() == vim.g.statusline_winid and 'CUR' or 'nc'
local current = vim.api.nvim_get_current_win()
local statusline = vim.g.statusline_winid
local focus = current == statusline
table.insert(_G.statusline_contexts, {
current = current,
focus = focus,
in_aucmd = _G.in_aucmd_statusline,
statusline = statusline,
})
return focus and 'CUR' or 'nc'
end
vim.o.statusline = '%!v:lua.Status()'
vim.api.nvim_create_autocmd('User', {
buffer = buf,
callback = function(ev)
_G.in_aucmd_statusline = true
vim.api.nvim__redraw({ buf = ev.buf, statusline = true })
_G.in_aucmd_statusline = false
end,
})
end, target)
-- Ignore statusline evaluations from setup; only the autocmd redraw matters.
exec_lua('_G.statusline_contexts = {}')
api.nvim_exec_autocmds('User', { buf = target })
screen:expect([[
|
@@ -997,6 +1015,81 @@ describe('statusline', function()
{3:CUR }|
|
]])
-- `%!` may evaluate during the callback, but must not see the target as focused.
eq(
{},
exec_lua(function(win)
return vim
.iter(_G.statusline_contexts)
:filter(function(context)
return context.in_aucmd and context.focus and context.current ~= win
end)
:totable()
end, caller_win)
)
end)
it('defers statusline evaluation during nvim_exec_autocmds({buf}) #40153', function()
command('set laststatus=2')
local caller_win = api.nvim_get_current_win()
command('split')
local target = api.nvim_create_buf(true, false)
api.nvim_set_current_buf(target)
api.nvim_set_current_win(caller_win)
exec_lua(function(buf)
_G.in_aucmd_statusline = false
_G.statusline_contexts = {}
_G.Status = function()
table.insert(_G.statusline_contexts, {
in_aucmd = _G.in_aucmd_statusline,
current = vim.api.nvim_get_current_win(),
actual = tonumber(vim.g.actual_curwin or -1),
})
return vim.api.nvim_get_current_win() == tonumber(vim.g.actual_curwin or -1) and 'CUR'
or 'nc'
end
-- `%{%...%}` sets g:actual_curwin while evaluating the statusline.
vim.o.statusline = '%{%v:lua.Status()%}'
vim.api.nvim_create_autocmd('User', {
buffer = buf,
callback = function(ev)
_G.in_aucmd_statusline = true
vim.api.nvim__redraw({ buf = ev.buf, statusline = true })
_G.in_aucmd_statusline = false
end,
})
end, target)
screen:expect([[
|
{1:~ }|*2
{2:nc }|
^ |
{1:~ }|
{3:CUR }|
|
]])
-- Ignore statusline evaluations from setup; only the autocmd redraw matters.
exec_lua('_G.statusline_contexts = {}')
api.nvim_exec_autocmds('User', { buf = target })
-- No different frame should be flushed while the deferred redraw settles.
screen:expect({ unchanged = true })
local contexts = exec_lua(function()
return _G.statusline_contexts
end)
eq(true, #contexts > 0)
eq(caller_win, contexts[#contexts].actual)
-- The statusline must be evaluated only after the autocmd context is restored.
eq(
{},
exec_lua(function()
return vim
.iter(_G.statusline_contexts)
:filter(function(context)
return context.in_aucmd
end)
:totable()
end)
)
end)
end)