patch 9.2.0477: popup: leftover content after popup_free under layout change
Commit:
https://github.com/vim/vim/commit/3a9e1bb7e245f862f506b02845fc6132013ef6e7
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Tue May 12 17:47:41 2026 +0000
patch 9.2.0477: popup: leftover content after popup_free under layout change
Problem: popup_mask still marks the freed popup's cells as covered
until may_update_popup_mask() runs inside the next
update_screen. Any screen_fill / screen_puts called in
between (for example msg_clr_eos triggered by a status message
from :copen) hits skip_for_popup() and silently drops writes
to those cells, so the popup's chars survive on screen until
those cells happen to be redrawn for another reason.
Solution: Add popup_clear_mask_for() and call it from popup_hide() and
popup_free() when the popup was visible, so the upcoming
writes take effect immediately (Yasuhiro Matsumoto)
Note: The test is limited to MS-Windows because the original report
(#20178) was reproduced there and the redraw timing required to
surface the bug differs on other platforms.
fixes: #20178
closes: #20188
Signed-off-by: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/popupwin.c b/src/popupwin.c
index 685c3cae7..0767fa913 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -3522,10 +3522,38 @@ f_popup_close(typval_T *argvars, typval_T *rettv UNUSED)
popup_close_and_callback(wp, &argvars[1]);
}
+/*
+ * Clear popup_mask entries for the cells covered by "wp" so that
+ * screen_fill / screen_puts calls made before the next update_screen()
+ * (e.g. msg_clr_eos triggered by a status message) are not silently
+ * dropped by skip_for_popup(). Without this the popup's chars survive
+ * on screen until may_update_popup_mask() runs and the affected cells
+ * happen to be redrawn.
+ */
+ static void
+popup_clear_mask_for(win_T *wp)
+{
+ int r, c;
+ int row_start, col_start, row_end, col_end;
+
+ if (popup_mask == NULL || !popup_visible)
+ return;
+
+ row_start = MAX(wp->w_winrow, 0);
+ col_start = MAX(wp->w_wincol, 0);
+ row_end = MIN(wp->w_winrow + popup_height(wp), (int)screen_Rows);
+ col_end = MIN(wp->w_wincol + popup_width(wp), (int)screen_Columns);
+
+ for (r = row_start; r < row_end; ++r)
+ for (c = col_start; c < col_end; ++c)
+ popup_mask[r * screen_Columns + c] = 0;
+}
+
void
popup_hide(win_T *wp)
{
popup_area_T old_area;
+ int was_visible = (wp->w_popup_flags & POPF_HIDDEN) == 0;
#ifdef FEAT_TERMINAL
if (error_if_term_popup_window())
@@ -3541,6 +3569,9 @@ popup_hide(win_T *wp)
if (wp->w_winrow + popup_height(wp) >= cmdline_row)
clear_cmdline = TRUE;
+ if (was_visible)
+ popup_clear_mask_for(wp);
+
if (old_area.active)
popup_redraw_exposed_area(&old_area);
else
@@ -3725,6 +3756,7 @@ f_popup_setbuf(typval_T *argvars, typval_T *rettv UNUSED)
popup_free(win_T *wp)
{
popup_area_T old_area;
+ int was_visible = (wp->w_popup_flags & POPF_HIDDEN) == 0;
popup_save_area(wp, &old_area);
@@ -3733,6 +3765,9 @@ popup_free(win_T *wp)
if (wp->w_winrow + popup_height(wp) >= cmdline_row)
clear_cmdline = TRUE;
+ if (was_visible)
+ popup_clear_mask_for(wp);
+
popup_redraw_exposed_area(&old_area);
win_free_popup(wp);
diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index 873840d8e..56cce4fb4 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -5408,4 +5408,52 @@ func Test_popupwin_close_status_redraw()
call StopVimInTerminal(buf)
endfunc
+func Test_popupwin_close_copen_redraw()
+ CheckMSWindows
+ CheckFeature quickfix
+
+ func! s:OpenPopup()
+ call popup_create(repeat(['ZZZZZZZZZ'], 10), #{
+ \ pos: 'botright',
+ \ col: &columns,
+ \ line: &lines,
+ \ filter: function('s:PopupFilter'),
+ \ })
+ endfunc
+ func! s:PopupFilter(winid, key)
+ if a:key ==# 'q'
+ call popup_close(a:winid)
+ copen
+ endif
+ return 1
+ endfunc
+
+ enew!
+ call setline(1, range(1, 30))
+ call setqflist(map(range(1, 20),
+ \ {_, v -> {'bufnr': bufnr('%'), 'lnum': v, 'col': 1, 'text': 'item ' .. v}}))
+
+ call s:OpenPopup()
+ redraw
+ call feedkeys('q', 'xt')
+ redraw
+ cclose
+ redraw
+
+ call s:OpenPopup()
+ redraw
+ call feedkeys('q', 'xt')
+ redraw
+
+ for row in range(&lines - 9, &lines)
+ let line = join(map(range(1, &columns), 'screenstring(row, v:val)'), '')
+ call assert_notmatch('Z', line)
+ endfor
+
+ cclose
+ bwipe!
+ delfunc s:OpenPopup
+ delfunc s:PopupFilter
+endfunc
+
" vim: shiftwidth=2 sts=2
diff --git a/src/version.c b/src/version.c
index 6a8735c6b..d52a2c468 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 477,
/**/
476,
/**/