fix(autocmd): never draw aucmd_prepbuf temp win #40379

Problem:  An autocommand that redraws may do so while curwin is
          temporarily set for the autocommand scope. This can result in
          flickering or unexpected state with UI components (statusline,
          winbar, decor providers...) that depend on the current window.
          Current workaround for statusline and winbar specifically
          delays the redraw, which can itself be unexpected for the
          autocommand.

Solution: If redrawing happens with a temporary autocmd current window,
          temporarily restore the current window while redrawing.
This commit is contained in:
luukvbaal
2026-06-23 22:05:26 +02:00
committed by GitHub
parent 8e3b216d0b
commit 28ffb334cf
7 changed files with 55 additions and 47 deletions

View File

@@ -1347,15 +1347,16 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf)
curbuf = buf; curbuf = buf;
aco->new_curwin_handle = curwin->handle; aco->new_curwin_handle = curwin->handle;
set_bufref(&aco->new_curbuf, curbuf); set_bufref(&aco->new_curbuf, curbuf);
if (aco->new_curwin_handle != aco->save_curwin_handle) {
aucmd_prepbuf_depth++;
}
aco->save_VIsual_active = VIsual_active; aco->save_VIsual_active = VIsual_active;
if (!same_buffer) { if (!same_buffer) {
// disable the Visual area, position may be invalid in another buffer // disable the Visual area, position may be invalid in another buffer
VIsual_active = false; VIsual_active = false;
} }
if (autocmd_save.save_aucmd == NULL) {
autocmd_save = *aco;
autocmd_save.save_aucmd = aco;
}
} }
/// Cleanup after executing autocommands for a (hidden) buffer. /// Cleanup after executing autocommands for a (hidden) buffer.
@@ -1378,6 +1379,9 @@ void aucmd_restbuf(aco_save_T *aco)
if (aco->new_curbuf.br_buf == NULL) { if (aco->new_curbuf.br_buf == NULL) {
return; return;
} }
if (autocmd_save.save_aucmd == aco) {
autocmd_save.save_aucmd = NULL;
}
if (aco->use_aucmd_win_idx >= 0) { if (aco->use_aucmd_win_idx >= 0) {
win_T *awp = aucmd_win[aco->use_aucmd_win_idx].auc_win; win_T *awp = aucmd_win[aco->use_aucmd_win_idx].auc_win;
@@ -1468,7 +1472,6 @@ win_found:
curbuf->b_nwindows++; curbuf->b_nwindows++;
} }
curwin->w_redr_status = true;
curwin = save_curwin; curwin = save_curwin;
curbuf = curwin->w_buffer; curbuf = curwin->w_buffer;
prevwin = win_find_by_handle(aco->save_prevwin_handle); prevwin = win_find_by_handle(aco->save_prevwin_handle);
@@ -1485,10 +1488,6 @@ win_found:
if (VIsual_active) { if (VIsual_active) {
check_pos(curbuf, &VIsual); 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. /// Schedules an autocommand event, to be executed at the next event-loop tick.

View File

@@ -22,7 +22,6 @@ EXTERN win_T *last_cursormoved_win INIT( = NULL);
EXTERN pos_T last_cursormoved INIT( = { 0, 0, 0 }); EXTERN pos_T last_cursormoved INIT( = { 0, 0, 0 });
EXTERN bool autocmd_busy INIT( = false); ///< Is apply_autocmds() busy? 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_enter INIT( = false); ///< Buf/WinEnter autocmds disabled
EXTERN int autocmd_no_leave INIT( = false); ///< Buf/WinLeave autocmds disabled EXTERN int autocmd_no_leave INIT( = false); ///< Buf/WinLeave autocmds disabled
@@ -42,6 +41,8 @@ EXTERN bool autocmd_fname_full INIT( = false); ///< autocmd_fname is full path
EXTERN int autocmd_bufnr INIT( = 0); ///< fnum for <abuf> on cmdline EXTERN int autocmd_bufnr INIT( = 0); ///< fnum for <abuf> on cmdline
EXTERN char *autocmd_match INIT( = NULL); ///< name for <amatch> on cmdline EXTERN char *autocmd_match INIT( = NULL); ///< name for <amatch> on cmdline
EXTERN bool did_cursorhold INIT( = true); ///< set when CursorHold t'gerd EXTERN bool did_cursorhold INIT( = true); ///< set when CursorHold t'gerd
// Used to restore the actual current buffer/window when redrawing.
EXTERN aco_save_T autocmd_save INIT( = { 0 });
typedef struct { typedef struct {
win_T *auc_win; ///< Window used in aucmd_prepbuf(). When not NULL the win_T *auc_win; ///< Window used in aucmd_prepbuf(). When not NULL the

View File

@@ -9,9 +9,10 @@
#include "auevents_enum.generated.h" #include "auevents_enum.generated.h"
typedef struct aco_save_S aco_save_T;
/// Struct to save values in before executing autocommands for a buffer that is /// Struct to save values in before executing autocommands for a buffer that is
/// not the current buffer. /// not the current buffer.
typedef struct { struct aco_save_S {
int use_aucmd_win_idx; ///< index in aucmd_win[] if >= 0 int use_aucmd_win_idx; ///< index in aucmd_win[] if >= 0
handle_T save_curwin_handle; ///< ID of saved curwin handle_T save_curwin_handle; ///< ID of saved curwin
handle_T new_curwin_handle; ///< ID of new curwin handle_T new_curwin_handle; ///< ID of new curwin
@@ -21,7 +22,8 @@ typedef struct {
char *globaldir; ///< saved value of globaldir char *globaldir; ///< saved value of globaldir
bool save_VIsual_active; ///< saved VIsual_active bool save_VIsual_active; ///< saved VIsual_active
int save_prompt_insert; ///< saved b_prompt_insert int save_prompt_insert; ///< saved b_prompt_insert
} aco_save_T; aco_save_T *save_aucmd;
};
typedef struct { typedef struct {
size_t refcount; ///< Reference count (freed when reaches zero) size_t refcount; ///< Reference count (freed when reaches zero)

View File

@@ -465,6 +465,11 @@ int update_screen(void)
return FAIL; return FAIL;
} }
// Restore actual curwin before redrawing.
if (autocmd_save.save_aucmd != NULL && curwin->handle != autocmd_save.save_curwin_handle) {
aucmd_restbuf(&autocmd_save);
}
int type = must_redraw; int type = must_redraw;
// must_redraw is reset here, so that when we run into some weird // must_redraw is reset here, so that when we run into some weird
@@ -740,6 +745,12 @@ int update_screen(void)
if (!ui_has(kUICmdline)) { if (!ui_has(kUICmdline)) {
cmdline_was_last_drawn = false; cmdline_was_last_drawn = false;
} }
// Restore temporary autocmd curwin.
if (autocmd_save.save_aucmd != NULL && curwin->handle != autocmd_save.new_curwin_handle) {
aucmd_prepbuf(&autocmd_save, autocmd_save.new_curbuf.br_buf);
}
return OK; return OK;
} }

View File

@@ -63,18 +63,6 @@ typedef enum {
kNumBaseHexadecimal = 16, kNumBaseHexadecimal = 16,
} NumberBase; } 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`. /// Redraw the status line of window `wp`.
/// ///
/// If inversion is possible we use it. Else '=' characters are used. /// If inversion is possible we use it. Else '=' characters are used.
@@ -90,9 +78,7 @@ void win_redr_status(win_T *wp)
|| (wild_menu_showing != 0 && !ui_has(kUIWildmenu))) { || (wild_menu_showing != 0 && !ui_has(kUIWildmenu))) {
return; return;
} }
if (stl_defer_redraw(wp)) {
return;
}
busy = true; busy = true;
wp->w_redr_status = false; wp->w_redr_status = false;
if (wp->w_status_height == 0 && !(is_stl_global && wp == curwin)) { if (wp->w_status_height == 0 && !(is_stl_global && wp == curwin)) {
@@ -260,6 +246,11 @@ static void win_redr_custom(win_T *wp, bool draw_winbar, bool draw_ruler, bool u
} }
entered = true; entered = true;
// Restore actual curwin before redrawing.
if (autocmd_save.save_aucmd != NULL && curwin->handle != autocmd_save.save_curwin_handle) {
aucmd_restbuf(&autocmd_save);
}
// setup environment for the task at hand // setup environment for the task at hand
if (wp == NULL) { if (wp == NULL) {
// Use 'tabline'. Always at the first line of the screen. // Use 'tabline'. Always at the first line of the screen.
@@ -437,6 +428,11 @@ static void win_redr_custom(win_T *wp, bool draw_winbar, bool draw_ruler, bool u
theend: theend:
entered = false; entered = false;
// Restore temporary autocmd curwin.
if (autocmd_save.save_aucmd != NULL && curwin->handle == autocmd_save.new_curwin_handle) {
aucmd_prepbuf(&autocmd_save, autocmd_save.new_curbuf.br_buf);
}
} }
void win_redr_winbar(win_T *wp) void win_redr_winbar(win_T *wp)
@@ -448,9 +444,6 @@ void win_redr_winbar(win_T *wp)
if (entered) { if (entered) {
return; return;
} }
if (stl_defer_redraw(wp)) {
return;
}
entered = true; entered = true;
if (wp->w_winbar_height == 0 || !redrawing()) { if (wp->w_winbar_height == 0 || !redrawing()) {

View File

@@ -1207,4 +1207,21 @@ describe('statuscolumn', function()
| |
]]) ]])
end) end)
it('redrawn during nvim_exec_autocmds({buf})', function()
command([[let &statuscolumn='%{g:actual_curwin == win_getid() ? "CUR" : "NC"}']])
local buf = api.nvim_create_buf(true, false)
api.nvim_open_win(buf, false, { split = 'right' })
api.nvim_create_autocmd('User', { command = 'redraw!' })
screen:expect([[
{8:CUR}aaaaa │{8:NC} |
{8:CUR}aaaaa │{1:~ }|*3
{8:CUR}^aaaaa │{1:~ }|
{8:CUR}aaaaa │{1:~ }|*7
{3:[No Name] [+] }{2:[No Name] }|
|
]])
api.nvim_exec_autocmds('User', { buf = buf })
screen:expect_unchanged()
end)
end) end)

View File

@@ -959,15 +959,13 @@ describe('statusline', function()
it('no cmdline ruler for autocmd window #39938', function() it('no cmdline ruler for autocmd window #39938', function()
command('set ruler laststatus=2') command('set ruler laststatus=2')
api.nvim_create_autocmd('BufDelete', { command = 'redrawstatus' }) api.nvim_create_autocmd('BufDelete', { command = 'redrawstatus' })
local expected = [[ screen:expect([[
^ | ^ |
{1:~ }|*5 {1:~ }|*5
{3:[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) }) 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) screen:expect_unchanged(true)
end) end)
@@ -1029,7 +1027,7 @@ describe('statusline', function()
) )
end) end)
it('defers statusline evaluation during nvim_exec_autocmds({buf}) #40153', function() it('statusline evaluation during nvim_exec_autocmds({buf}) #40153', function()
command('set laststatus=2') command('set laststatus=2')
local caller_win = api.nvim_get_current_win() local caller_win = api.nvim_get_current_win()
command('split') command('split')
@@ -1071,25 +1069,12 @@ describe('statusline', function()
-- Ignore statusline evaluations from setup; only the autocmd redraw matters. -- Ignore statusline evaluations from setup; only the autocmd redraw matters.
exec_lua('_G.statusline_contexts = {}') exec_lua('_G.statusline_contexts = {}')
api.nvim_exec_autocmds('User', { buf = target }) api.nvim_exec_autocmds('User', { buf = target })
-- No different frame should be flushed while the deferred redraw settles.
screen:expect({ unchanged = true }) screen:expect({ unchanged = true })
local contexts = exec_lua(function() local contexts = exec_lua(function()
return _G.statusline_contexts return _G.statusline_contexts
end) end)
eq(true, #contexts > 0) eq(true, #contexts > 0)
eq(caller_win, contexts[#contexts].actual) 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)
end) end)