From 28ffb334cf449c3782ef7e3bf68d150a6cd58227 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Tue, 23 Jun 2026 22:05:26 +0200 Subject: [PATCH] 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. --- src/nvim/autocmd.c | 15 ++++++------ src/nvim/autocmd.h | 3 ++- src/nvim/autocmd_defs.h | 6 +++-- src/nvim/drawscreen.c | 11 +++++++++ src/nvim/statusline.c | 29 +++++++++--------------- test/functional/ui/statuscolumn_spec.lua | 17 ++++++++++++++ test/functional/ui/statusline_spec.lua | 21 +++-------------- 7 files changed, 55 insertions(+), 47 deletions(-) diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 7b013dfd37..e1f023e779 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -1347,15 +1347,16 @@ 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) { // disable the Visual area, position may be invalid in another buffer 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. @@ -1378,6 +1379,9 @@ void aucmd_restbuf(aco_save_T *aco) if (aco->new_curbuf.br_buf == NULL) { return; } + if (autocmd_save.save_aucmd == aco) { + autocmd_save.save_aucmd = NULL; + } if (aco->use_aucmd_win_idx >= 0) { win_T *awp = aucmd_win[aco->use_aucmd_win_idx].auc_win; @@ -1468,7 +1472,6 @@ win_found: curbuf->b_nwindows++; } - curwin->w_redr_status = true; curwin = save_curwin; curbuf = curwin->w_buffer; prevwin = win_find_by_handle(aco->save_prevwin_handle); @@ -1485,10 +1488,6 @@ 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. diff --git a/src/nvim/autocmd.h b/src/nvim/autocmd.h index 048e34e7ae..971d7674e8 100644 --- a/src/nvim/autocmd.h +++ b/src/nvim/autocmd.h @@ -22,7 +22,6 @@ 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 @@ -42,6 +41,8 @@ EXTERN bool autocmd_fname_full INIT( = false); ///< autocmd_fname is full path EXTERN int autocmd_bufnr INIT( = 0); ///< fnum for on cmdline EXTERN char *autocmd_match INIT( = NULL); ///< name for on cmdline 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 { win_T *auc_win; ///< Window used in aucmd_prepbuf(). When not NULL the diff --git a/src/nvim/autocmd_defs.h b/src/nvim/autocmd_defs.h index f11a40743c..4397c657e6 100644 --- a/src/nvim/autocmd_defs.h +++ b/src/nvim/autocmd_defs.h @@ -9,9 +9,10 @@ #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 /// not the current buffer. -typedef struct { +struct aco_save_S { int use_aucmd_win_idx; ///< index in aucmd_win[] if >= 0 handle_T save_curwin_handle; ///< ID of saved curwin handle_T new_curwin_handle; ///< ID of new curwin @@ -21,7 +22,8 @@ typedef struct { char *globaldir; ///< saved value of globaldir bool save_VIsual_active; ///< saved VIsual_active int save_prompt_insert; ///< saved b_prompt_insert -} aco_save_T; + aco_save_T *save_aucmd; +}; typedef struct { size_t refcount; ///< Reference count (freed when reaches zero) diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index a4a99e21fa..be53a85266 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -465,6 +465,11 @@ int update_screen(void) 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; // 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)) { 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; } diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 9b18dd2773..8d2b22242a 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -63,18 +63,6 @@ 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. @@ -90,9 +78,7 @@ 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)) { @@ -260,6 +246,11 @@ static void win_redr_custom(win_T *wp, bool draw_winbar, bool draw_ruler, bool u } 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 if (wp == NULL) { // 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: 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) @@ -448,9 +444,6 @@ 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()) { diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index bcaef779bc..616236a585 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -1207,4 +1207,21 @@ describe('statuscolumn', function() | ]]) 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) diff --git a/test/functional/ui/statusline_spec.lua b/test/functional/ui/statusline_spec.lua index 3a73b30c75..1bcd128eeb 100644 --- a/test/functional/ui/statusline_spec.lua +++ b/test/functional/ui/statusline_spec.lua @@ -959,15 +959,13 @@ describe('statusline', function() it('no cmdline ruler for autocmd window #39938', function() command('set ruler laststatus=2') api.nvim_create_autocmd('BufDelete', { command = 'redrawstatus' }) - local expected = [[ + screen:expect([[ ^ | {1:~ }|*5 {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) @@ -1029,7 +1027,7 @@ describe('statusline', function() ) 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') local caller_win = api.nvim_get_current_win() command('split') @@ -1071,25 +1069,12 @@ describe('statusline', function() -- 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)