patch 9.2.0614: opacity popup leaves stale cells

Commit: 
https://github.com/vim/vim/commit/fab9ce9996f3d0b2528db010118d733b9dc6b5a8
Author: Yasuhiro Matsumoto <[email protected]>
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 <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wXPYr-008SQu-5G%40256bit.org.

Raspunde prin e-mail lui