commit 647f11e6ae795901e9bf5d0f4d84f73eab2b836b
parent 8aafe4f26ce330bcb29baecbd6ac0a77c130cca9
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 8 Dec 2025 09:24:26 +0800
vim-patch:9.1.1958: Wrong display with sign_unplace() and setline() in CursorMoved (#36851)
Problem: Wrong display when scrolling with 'scrolloff' and calling
sign_unplace() and setline() in CursorMoved (after 8.2.3204).
Solution: Still scroll for changed lines below the top area when the top
is scrolled down (zeertzjq)
closes: vim/vim#18878
https://github.com/vim/vim/commit/2da433cff7a7fff965a1e14dbee3dc4160d0ed74
Diffstat:
3 files changed, 144 insertions(+), 5 deletions(-)
diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c
@@ -1376,6 +1376,7 @@ static void win_update(win_T *wp)
int bot_start = 999; // first row of the bot area that needs
// updating. 999 when no bot area updating
bool scrolled_down = false; // true when scrolled down when w_topline got smaller a bit
+ bool scrolled_for_mod = false; // true after scrolling for changed lines
bool top_to_mod = false; // redraw above mod_top
int bot_scroll_start = 999; // first line that needs to be redrawn due to
@@ -2038,10 +2039,13 @@ static void win_update(win_T *wp)
// When at start of changed lines: May scroll following lines
// up or down to minimize redrawing.
// Don't do this when the change continues until the end.
- // Don't scroll when redrawing the top, scrolled already above.
- if (lnum == mod_top
- && mod_bot != MAXLNUM
+ // Don't scroll the top area which was already scrolled above,
+ // but do scroll for changed lines below the top area.
+ if (!scrolled_for_mod && mod_bot != MAXLNUM
+ && lnum >= mod_top && lnum < MAX(mod_bot, mod_top + 1)
&& row >= top_end) {
+ scrolled_for_mod = true;
+
int old_cline_height = 0;
int old_rows = 0;
linenr_T l;
diff --git a/test/functional/legacy/display_spec.lua b/test/functional/legacy/display_spec.lua
@@ -12,7 +12,6 @@ describe('display', function()
-- oldtest: Test_display_scroll_at_topline()
it('scroll when modified at topline vim-patch:8.2.1488', function()
local screen = Screen.new(20, 4)
-
command([[call setline(1, repeat('a', 21))]])
feed('O')
screen:expect([[
@@ -26,7 +25,6 @@ describe('display', function()
-- oldtest: Test_display_scroll_update_visual()
it('scrolling when modified at topline in Visual mode vim-patch:8.2.4626', function()
local screen = Screen.new(60, 8)
-
exec([[
set scrolloff=0
call setline(1, repeat(['foo'], 10))
@@ -43,6 +41,111 @@ describe('display', function()
]])
end)
+ -- oldtest: Test_display_scroll_setline()
+ it('scrolling with sign_unplace() and setline() in CursorMoved', function()
+ local screen = Screen.new(20, 15)
+ exec([[
+ setlocal scrolloff=5 signcolumn=yes
+ call setline(1, range(1, 100))
+ call sign_define('foo', #{text: '>'})
+ call sign_place(1, 'bar', 'foo', bufnr(), #{lnum: 73})
+ call sign_place(2, 'bar', 'foo', bufnr(), #{lnum: 74})
+ call sign_place(3, 'bar', 'foo', bufnr(), #{lnum: 75})
+ normal! G
+ autocmd CursorMoved * if line('.') == 79
+ \ | call sign_unplace('bar', #{id: 2})
+ \ | call setline(80, repeat('foo', 15))
+ \ | endif
+ ]])
+ screen:expect([[
+ {7: }87 |
+ {7: }88 |
+ {7: }89 |
+ {7: }90 |
+ {7: }91 |
+ {7: }92 |
+ {7: }93 |
+ {7: }94 |
+ {7: }95 |
+ {7: }96 |
+ {7: }97 |
+ {7: }98 |
+ {7: }99 |
+ {7: }^100 |
+ |
+ ]])
+ feed('19k')
+ screen:expect([[
+ {7:> }75 |
+ {7: }76 |
+ {7: }77 |
+ {7: }78 |
+ {7: }79 |
+ {7: }80 |
+ {7: }^81 |
+ {7: }82 |
+ {7: }83 |
+ {7: }84 |
+ {7: }85 |
+ {7: }86 |
+ {7: }87 |
+ {7: }88 |
+ |
+ ]])
+ feed('k')
+ screen:expect([[
+ {7:> }75 |
+ {7: }76 |
+ {7: }77 |
+ {7: }78 |
+ {7: }79 |
+ {7: }^80 |
+ {7: }81 |
+ {7: }82 |
+ {7: }83 |
+ {7: }84 |
+ {7: }85 |
+ {7: }86 |
+ {7: }87 |
+ {7: }88 |
+ |
+ ]])
+ feed('k')
+ screen:expect([[
+ {7: }74 |
+ {7:> }75 |
+ {7: }76 |
+ {7: }77 |
+ {7: }78 |
+ {7: }^79 |
+ {7: }foofoofoofoofoofoo|*2
+ {7: }foofoofoo |
+ {7: }81 |
+ {7: }82 |
+ {7: }83 |
+ {7: }84 |
+ {7: }85 |
+ |
+ ]])
+ feed('k')
+ screen:expect([[
+ {7:> }73 |
+ {7: }74 |
+ {7:> }75 |
+ {7: }76 |
+ {7: }77 |
+ {7: }^78 |
+ {7: }79 |
+ {7: }foofoofoofoofoofoo|*2
+ {7: }foofoofoo |
+ {7: }81 |
+ {7: }82 |
+ {7: }83 |
+ {7: }84 |
+ |
+ ]])
+ end)
+
local function run_test_display_lastline(euro)
local screen = Screen.new(75, 10)
exec([[
diff --git a/test/old/testdir/test_display.vim b/test/old/testdir/test_display.vim
@@ -268,6 +268,38 @@ func Test_display_scroll_update_visual()
call StopVimInTerminal(buf)
endfunc
+func Test_display_scroll_setline()
+ CheckScreendump
+
+ let lines =<< trim END
+ setlocal scrolloff=5 signcolumn=yes
+ call setline(1, range(1, 100))
+ call sign_define('foo', #{text: '>'})
+ call sign_place(1, 'bar', 'foo', bufnr(), #{lnum: 73})
+ call sign_place(2, 'bar', 'foo', bufnr(), #{lnum: 74})
+ call sign_place(3, 'bar', 'foo', bufnr(), #{lnum: 75})
+ normal! G
+ autocmd CursorMoved * if line('.') == 79
+ \ | call sign_unplace('bar', #{id: 2})
+ \ | call setline(80, repeat('foo', 15))
+ \ | endif
+ END
+ call writefile(lines, 'XscrollSetline.vim', 'D')
+
+ let buf = RunVimInTerminal('-S XscrollSetline.vim', #{rows: 15, cols: 20})
+ call VerifyScreenDump(buf, 'Test_display_scroll_setline_1', {})
+ call term_sendkeys(buf, '19k')
+ call VerifyScreenDump(buf, 'Test_display_scroll_setline_2', {})
+ call term_sendkeys(buf, 'k')
+ call VerifyScreenDump(buf, 'Test_display_scroll_setline_3', {})
+ call term_sendkeys(buf, 'k')
+ call VerifyScreenDump(buf, 'Test_display_scroll_setline_4', {})
+ call term_sendkeys(buf, 'k')
+ call VerifyScreenDump(buf, 'Test_display_scroll_setline_5', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
" Test for 'eob' (EndOfBuffer) item in 'fillchars'
func Test_eob_fillchars()
" default value