patch 9.2.0483: popup: terminal embedded in an opacity popup freezes Vim on input
Commit:
https://github.com/vim/vim/commit/f281493c491c58e2452111e0e9b78edce486f07e
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Fri May 15 15:02:48 2026 +0000
patch 9.2.0483: popup: terminal embedded in an opacity popup freezes Vim on input
Problem: When a terminal buffer is shown inside a popup with 'opacity'
set to a value other than 100, typing into it freezes Vim.
Only the first keystroke is drawn; afterwards no input is
processed and the screen stops updating.
Solution: When marking background lines for redraw to keep opacity
blend cells fresh, do not raise must_redraw. This marking
happens from inside update_screen() (via
may_update_popup_mask()), so raising must_redraw makes
terminal_loop()'s "while (must_redraw != 0) update_screen()"
loop never terminate. Add redraw_win_range_now() that
updates only the per-window state and use it from
redraw_win_under_opacity_popup() (Hirohito Higashi)
fixes: #20214
closes: #20220
Co-Authored-By: Claude Opus 4.7 (1M context) <
nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <
h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/drawscreen.c b/src/drawscreen.c
index 751e09aa3..94b616214 100644
--- a/src/drawscreen.c
+++ b/src/drawscreen.c
@@ -3497,6 +3497,28 @@ redraw_win_range_later(
}
}
+/*
+ * Like redraw_win_range_later() but do not raise the global must_redraw.
+ * Use this from inside an update_screen() pass (where the redraw will be
+ * picked up this cycle), to avoid triggering an extra full redraw cycle.
+ */
+ void
+redraw_win_range_now(
+ win_T *wp,
+ linenr_T first,
+ linenr_T last)
+{
+ if (last >= wp->w_topline && first < wp->w_botline)
+ {
+ if (wp->w_redraw_top == 0 || wp->w_redraw_top > first)
+ wp->w_redraw_top = first;
+ if (wp->w_redraw_bot == 0 || wp->w_redraw_bot < last)
+ wp->w_redraw_bot = last;
+ if (wp->w_redr_type < UPD_VALID)
+ wp->w_redr_type = UPD_VALID;
+ }
+}
+
#ifdef FEAT_EVAL
static bool redraw_cb_in_progress = false;
diff --git a/src/popupwin.c b/src/popupwin.c
index 0767fa913..772148f61 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -5311,7 +5311,9 @@ redraw_win_under_opacity_popup(win_T *wp)
linenr_T lnum;
(void)mouse_comp_pos(twp, &line_cp, &col_cp, &lnum, NULL);
- redrawWinline(twp, lnum);
+ // Called from inside update_screen(); raising must_redraw
+ // would loop the outer redraw indefinitely.
+ redraw_win_range_now(twp, lnum, lnum);
}
else if (line_cp == twp->w_height)
// Status bar line: mark for redraw to prevent
diff --git a/src/proto/
drawscreen.pro b/src/proto/
drawscreen.pro
index dcf98cb6f..da2662aca 100644
--- a/src/proto/
drawscreen.pro
+++ b/src/proto/
drawscreen.pro
@@ -25,6 +25,7 @@ void redraw_statuslines(void);
void win_redraw_last_status(frame_T *frp);
void redrawWinline(win_T *wp, linenr_T lnum);
void redraw_win_range_later(win_T *wp, linenr_T first, linenr_T last);
+void redraw_win_range_now(win_T *wp, linenr_T first, linenr_T last);
void f_redraw_listener_add(typval_T *argvars, typval_T *rettv);
void f_redraw_listener_remove(typval_T *argvars, typval_T *rettv);
/* vim: set ft=c : */
diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index 56cce4fb4..783138b7e 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -5180,6 +5180,32 @@ func Test_popup_opacity_zero()
call StopVimInTerminal(buf)
endfunc
+func Test_popup_opacity_terminal_no_freeze()
+ CheckFeature terminal
+ CheckUnix
+ let g:test_is_flaky = 1
+
+ let origwin = win_getid()
+ let termbuf = term_start(&shell, #{hidden: 1})
+ let winid = popup_create(termbuf, #{minwidth: 40, minheight: 10,
+ \ border: [1, 1, 1, 1], opacity: 10})
+ call WaitForAssert({-> assert_equal("run", job_status(term_getjob(termbuf)))})
+ call WaitForAssert({-> assert_equal(' ', screenstring(screenrow(), screencol() - 1))})
+
+ " Before the fix typing froze Vim: redraw under an opacity popup raised
+ " must_redraw every cycle, trapping terminal_loop in its redraw loop.
+ call feedkeys('x', 'xt')
+ call term_wait(termbuf)
+ redraw
+ call WaitForAssert({-> assert_equal('x', screenstring(screenrow(), screencol() - 1))})
+
+ call feedkeys("\<BS>", 'xt')
+ call feedkeys("exit\<CR>", 'xt')
+ call WaitForAssert({-> assert_equal("dead", job_status(term_getjob(termbuf)))})
+ call feedkeys(":quit\<CR>", 'xt')
+ call assert_equal(origwin, win_getid())
+endfunc
+
func Test_popup_getwininfo_tabnr()
tab split
let winid1 = popup_create('sup', #{tabpage: 1})
diff --git a/src/version.c b/src/version.c
index 692c510e6..acf47f67a 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 */
+/**/
+ 483,
/**/
482,
/**/