Commit: patch 9.2.0614: opacity popup leaves stale cells

1 view
Skip to first unread message

Christian Brabandt

unread,
4:30 PM (4 hours ago) 4:30 PM
to vim...@googlegroups.com
patch 9.2.0614: opacity popup leaves stale cells

Commit: https://github.com/vim/vim/commit/fab9ce9996f3d0b2528db010118d733b9dc6b5a8
Author: Yasuhiro Matsumoto <matt...@gmail.com>
Date: Wed Jun 10 20:24:48 2026 +0000

patch 9.2.0614: opacity popup leaves stale cells

Problem: Background redraws under an opacity popup update ScreenLines[]
but suppress terminal output, so the terminal no longer
matches ScreenLines[] for those cells. Later draws skipped
them as "unchanged", leaving parts of the old popup on screen
after popup_settext() or popup_clear().
Solution: Track cells whose output was suppressed under an opacity popup
and force their next output.

fixes: #20459
closes: #20471

Signed-off-by: Yasuhiro Matsumoto <matt...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/src/screen.c b/src/screen.c
index 97d2f189c..7ade748b7 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -467,6 +467,56 @@ skip_for_popup(int row, int col)
return FALSE;
}

+#ifdef FEAT_PROP_POPUP
+// Cells where ScreenLines[] was updated but terminal output was suppressed
+// because the cell was under an opacity popup. For these cells the terminal
+// no longer matches ScreenLines[], so the "cell unchanged, skip output"
+// optimization must not be applied until they are output again.
+static char_u *suppressed_cells = NULL;
+static int suppressed_rows = 0;
+static int suppressed_cols = 0;
+
+ static void
+mark_suppressed_cell(int row, int col)
+{
+ if (suppressed_cells == NULL
+ || suppressed_rows != screen_Rows
+ || suppressed_cols != screen_Columns)
+ {
+ vim_free(suppressed_cells);
+ suppressed_cells = alloc_clear(
+ (size_t)screen_Rows * screen_Columns);
+ if (suppressed_cells == NULL)
+ {
+ suppressed_rows = 0;
+ suppressed_cols = 0;
+ return;
+ }
+ suppressed_rows = screen_Rows;
+ suppressed_cols = screen_Columns;
+ }
+ suppressed_cells[row * suppressed_cols + col] = TRUE;
+}
+
+ static void
+unmark_suppressed_cell(int row, int col)
+{
+ if (suppressed_cells != NULL
+ && row >= 0 && row < suppressed_rows
+ && col >= 0 && col < suppressed_cols)
+ suppressed_cells[row * suppressed_cols + col] = FALSE;
+}
+
+ static int
+is_suppressed_cell(int row, int col)
+{
+ return suppressed_cells != NULL
+ && row >= 0 && row < suppressed_rows
+ && col >= 0 && col < suppressed_cols
+ && suppressed_cells[row * suppressed_cols + col];
+}
+#endif
+
#ifdef FEAT_PROP_POPUP
/*
* Cached attributes of the current opacity popup, computed once per
@@ -696,6 +746,13 @@ screen_line(
redraw_next = force || char_needs_redraw(off_from + char_cells,
off_to + char_cells, endcol - col - char_cells);

+#ifdef FEAT_PROP_POPUP
+ // A cell whose output was suppressed under an opacity popup does not
+ // match the terminal even when ScreenLines[] is unchanged.
+ if (!redraw_this && is_suppressed_cell(row, col + coloff))
+ redraw_this = TRUE;
+#endif
+
#ifdef FEAT_GUI
# ifdef FEAT_GUI_MSWIN
changed_this = changed_next;
@@ -1079,7 +1136,11 @@ skip_opacity:
// blank out the rest of the line
while (col < clear_width && ScreenLines[off_to] == ' '
&& ScreenAttrs[off_to] == 0
- && (!enc_utf8 || ScreenLinesUC[off_to] == 0))
+ && (!enc_utf8 || ScreenLinesUC[off_to] == 0)
+#ifdef FEAT_PROP_POPUP
+ && !is_suppressed_cell(row, col + coloff)
+#endif
+ )
{
ScreenCols[off_to] =
(flags & SLF_INC_VCOL) ? ++last_vcol : last_vcol;
@@ -1892,7 +1953,13 @@ screen_puts_len(
|| (ScreenLinesUC[off] != 0
&& screen_comp_differs(off, u8cc))))
|| ScreenAttrs[off] != cell_attr
- || exmode_active;
+ || exmode_active
+#ifdef FEAT_PROP_POPUP
+ // Output was suppressed under an opacity popup: the terminal
+ // does not match ScreenLines[] even when unchanged.
+ || is_suppressed_cell(row, col)
+#endif
+ ;

if ((need_redraw || force_redraw_this) && !skip_for_popup(row, col))
{
@@ -2450,6 +2517,12 @@ screen_char(unsigned off, int row, int col)
ScreenLinesUC[off2] = 0;
screen_char(off2, row, col + 1);
}
+ mark_suppressed_cell(row, col);
+ if (enc_utf8 && ScreenLinesUC[off] != 0
+ && utf_char2cells(ScreenLinesUC[off]) == 2
+ && col + 1 < screen_Columns
+ && popup_is_under_opacity(row, col + 1))
+ mark_suppressed_cell(row, col + 1);
screen_cur_col = 9999;
return;
}
@@ -2458,6 +2531,8 @@ screen_char(unsigned off, int row, int col)
&& col + 1 < screen_Columns
&& popup_is_under_opacity(row, col + 1))
{
+ mark_suppressed_cell(row, col);
+ mark_suppressed_cell(row, col + 1);
screen_cur_col = 9999;
return;
}
@@ -2491,6 +2566,14 @@ screen_char(unsigned off, int row, int col)

windgoto(row, col);

+#ifdef FEAT_PROP_POPUP
+ // The cell is output below, the terminal matches ScreenLines again.
+ unmark_suppressed_cell(row, col);
+ if (enc_utf8 && ScreenLinesUC[off] != 0
+ && utf_char2cells(ScreenLinesUC[off]) == 2)
+ unmark_suppressed_cell(row, col + 1);
+#endif
+
if (screen_attr != attr)
screen_start_highlight(attr);

@@ -2574,6 +2657,8 @@ screen_char_2(unsigned off, int row, int col)
// If under a higher-zindex opacity popup, suppress output.
if (popup_is_under_opacity(row, col))
{
+ mark_suppressed_cell(row, col);
+ mark_suppressed_cell(row, col + 1);
screen_cur_col = 9999;
return;
}
@@ -2583,6 +2668,9 @@ screen_char_2(unsigned off, int row, int col)
// second byte directly.
screen_char(off, row, col);
out_char(ScreenLines[off + 1]);
+#ifdef FEAT_PROP_POPUP
+ unmark_suppressed_cell(row, col + 1);
+#endif
++screen_cur_col;
}

@@ -2770,11 +2858,21 @@ screen_fill(
// skip blanks (used often, keep it fast!)
if (enc_utf8)
while (off < end_off && ScreenLines[off] == ' '
- && ScreenAttrs[off] == 0 && ScreenLinesUC[off] == 0)
+ && ScreenAttrs[off] == 0 && ScreenLinesUC[off] == 0
+#ifdef FEAT_PROP_POPUP
+ && !is_suppressed_cell(row,
+ (int)(off - LineOffset[row]))
+#endif
+ )
++off;
else
while (off < end_off && ScreenLines[off] == ' '
- && ScreenAttrs[off] == 0)
+ && ScreenAttrs[off] == 0
+#ifdef FEAT_PROP_POPUP
+ && !is_suppressed_cell(row,
+ (int)(off - LineOffset[row]))
+#endif
+ )
++off;
if (off < end_off) // something to be cleared
{
@@ -2787,6 +2885,10 @@ screen_fill(
while (col--) // clear chars in ScreenLines
{
space_to_screenline(off, 0);
+#ifdef FEAT_PROP_POPUP
+ // T_CE cleared the terminal cell, it matches again.
+ unmark_suppressed_cell(row, (int)(off - LineOffset[row]));
+#endif
++off;
}
}
@@ -2804,6 +2906,12 @@ screen_fill(
|| must_redraw == UPD_CLEAR // screen clear pending
#if defined(FEAT_GUI) || defined(UNIX)
|| force_next
+#endif
+#ifdef FEAT_PROP_POPUP
+ // Output was suppressed under an opacity popup: the
+ // terminal does not match ScreenLines[] even when
+ // unchanged.
+ || is_suppressed_cell(row, col)
#endif
)
// Skip if under a(nother) popup.
@@ -3448,6 +3556,9 @@ free_screenlines(void)
VIM_CLEAR(popup_mask);
VIM_CLEAR(popup_mask_next);
VIM_CLEAR(popup_transparent);
+ VIM_CLEAR(suppressed_cells);
+ suppressed_rows = 0;
+ suppressed_cols = 0;
#endif
}

@@ -3512,6 +3623,12 @@ screenclear2(int doclear)
did_clear = TRUE;
clear_cmdline = FALSE;
mode_displayed = FALSE;
+#ifdef FEAT_PROP_POPUP
+ // The display was cleared, the terminal matches ScreenLines again.
+ if (suppressed_cells != NULL)
+ vim_memset(suppressed_cells, 0,
+ (size_t)suppressed_rows * suppressed_cols);
+#endif
}
else
{
diff --git a/src/testdir/dumps/Test_popupwin_opacity_settext_1.dump b/src/testdir/dumps/Test_popupwin_opacity_settext_1.dump
new file mode 100644
index 000000000..56ec5dfd8
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_opacity_settext_1.dump
@@ -0,0 +1,12 @@
+>s+0&#ffffff0|o|m|e| |t|e|x|t| |h|e|r|e| @60
+@1|╔+0#0000001#ffffff255|═@2|╗| +0#0000000#ffffff0@68
+@1|║+0#0000001#ffffff255|a|b|c|║| +0#0000000#ffffff0@68
+@1|║+0#0000001#ffffff255|A|B|C|║| +0#0000000#ffffff0@68
+@1|║+0#0000001#ffffff255|1|2|3|║| +0#0000000#ffffff0@68
+@1|║+0#0000001#ffffff255|4|5|6|║| +0#0000000#ffffff0@68
+|m|╚+0#0000001#ffffff255|═@2|╝|e+0#0000000#ffffff0|x|t| @65
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|~| @73
+| +0#0000000&@56|1|,|1| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_popupwin_opacity_settext_2.dump b/src/testdir/dumps/Test_popupwin_opacity_settext_2.dump
new file mode 100644
index 000000000..237ca18f9
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_opacity_settext_2.dump
@@ -0,0 +1,12 @@
+>s+0&#ffffff0|o|m|e| |t|e|x|t| |h|e|r|e| @60
+@1|╔+0#0000001#ffffff255|═@25|╗| +0#0000000#ffffff0@45
+@1|║+0#0000001#ffffff255|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|║| +0#0000000#ffffff0@45
+@1|║+0#0000001#ffffff255| +0#818181255&@25|║+0#0000001&| +0#0000000#ffffff0@45
+@1|║+0#0000001#ffffff255| +0#818181255&@25|║+0#0000001&| +0#0000000#ffffff0@45
+@1|║+0#0000001#ffffff255| +0#818181255&@25|║+0#0000001&| +0#0000000#ffffff0@45
+|m|║+0#0000001#ffffff255|r+0#818181255&|e| |t|e|x|t| @18|║+0#0000001&| +0#0000000#ffffff0@45
+|~+0#4040ff13&|║+0#0000001#ffffff255|.| +0#8787ff255&@24|║+0#0000001&| +0#4040ff13#ffffff0@45
+|~|╚+0#0000001#ffffff255|═@25|╝| +0#4040ff13#ffffff0@45
+|~| @73
+|~| @73
+| +0#0000000&@56|1|,|1| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_popupwin_opacity_settext_3.dump b/src/testdir/dumps/Test_popupwin_opacity_settext_3.dump
new file mode 100644
index 000000000..419898c8d
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_opacity_settext_3.dump
@@ -0,0 +1,12 @@
+>s+0&#ffffff0|o|m|e| |t|e|x|t| |h|e|r|e| @60
+@75
+@75
+@75
+@75
+@75
+|m|o|r|e| |t|e|x|t| @65
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|~| @73
+|:+0#0000000&|c|a|l@1| |p|o|p|u|p|_|c|l|e|a|r|(|)| @37|1|,|1| @10|A|l@1|
diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index 02352f2d7..5156584e4 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -5273,6 +5273,34 @@ func Test_popup_opacity_zero()
call StopVimInTerminal(buf)
endfunc

+func Test_popup_opacity_settext_no_leftover()
+ CheckScreendump
+
+ " Growing a no-highlight opacity popup with popup_settext() used to leave
+ " cells of the old, smaller popup on the screen: the background redraw
+ " under an opacity popup suppresses terminal output, so a later draw must
+ " not skip cells that look unchanged in ScreenLines.
+ let lines =<< trim END
+ call setline(1, ['some text here', '', '', '', '', '', 'more text'])
+ let g:winid = popup_create(['abc', 'ABC', '123', '456'],
+ \ #{line: 2, col: 2, border: [], highlight: 'None', opacity: 50})
+ END
+ call writefile(lines, 'XtestPopupOpacitySettext', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupOpacitySettext', #{rows: 12})
+ call VerifyScreenDump(buf, 'Test_popupwin_opacity_settext_1', {})
+
+ " Replace with larger content: no cells of the small popup may remain.
+ call term_sendkeys(buf, ":call popup_settext(g:winid,"
+ \ .. " ['ABCDEFGHIJKLMNOPQRSTUVWXYZ', '', '', '', '', '.'])\<CR>")
+ call VerifyScreenDump(buf, 'Test_popupwin_opacity_settext_2', {})
+
+ " After closing the popup the screen must be fully restored.
+ call term_sendkeys(buf, ":call popup_clear()\<CR>")
+ call VerifyScreenDump(buf, 'Test_popupwin_opacity_settext_3', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
func Test_popup_opacity_terminal_no_freeze()
CheckFeature terminal
CheckUnix
diff --git a/src/version.c b/src/version.c
index f32381821..9b9abccd7 100644
--- a/src/version.c
+++ b/src/version.c
@@ -754,6 +754,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 614,
/**/
613,
/**/
Reply all
Reply to author
Forward
0 new messages