From 034fca6c539ce33760cad3d01db5e8473f61c4ac Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi Date: Fri, 4 Sep 2026 09:27:53 +0200 Subject: [PATCH] fix(move): zz does not center the cursor with wrapped lines #41656 Problem: `scroll_cursor_halfway()` resets its `above` and `below` row counters on every iteration of the `topline` scan, because they are declared inside the loop. Each iteration then takes one line from each side regardless of how many screen rows those lines occupy, so tall wrapped lines below the cursor leave it near the top of the window instead of centered. Affects the default `nosmoothscroll` path, reached from "zz" and from `update_topline()`. Regression from 9b9ccac6256 (vim-patch:9.0.1121), which moved the declarations to their first use while porting; the upstream patch did not touch them. Solution: Declare the counters at function scope, as Vim does, so they accumulate across iterations. --- src/nvim/move.c | 4 +-- test/functional/legacy/scroll_opt_spec.lua | 30 ++++++++++++++++++++++ test/functional/ui/statuscolumn_spec.lua | 16 +++--------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/nvim/move.c b/src/nvim/move.c index bc881e2db9..7519e86f08 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -2204,6 +2204,8 @@ void scroll_cursor_halfway(win_T *wp, bool atend, bool prefer_above) } int topfill = 0; + int above = 0; + int below = 0; while (topline > 1) { // If using smoothscroll, we can precisely scroll to the // exact point where the cursor is halfway down the screen. @@ -2238,8 +2240,6 @@ void scroll_cursor_halfway(win_T *wp, bool atend, bool prefer_above) // Depending on "prefer_above" we add a line above or below first. // Loop twice to avoid duplicating code. bool done = false; - int above = 0; - int below = 0; for (int round = 1; round <= 2; round++) { if (prefer_above ? (round == 2 && below < above) diff --git a/test/functional/legacy/scroll_opt_spec.lua b/test/functional/legacy/scroll_opt_spec.lua index af720063f5..2677fcae4a 100644 --- a/test/functional/legacy/scroll_opt_spec.lua +++ b/test/functional/legacy/scroll_opt_spec.lua @@ -1600,3 +1600,33 @@ describe('scrolloffpad', function() screen:expect(s1) end) end) + +describe('scroll_cursor_halfway()', function() + local screen + + before_each(function() + screen = Screen.new(40, 12) + end) + + it('weighs lines by their screen height', function() + exec([[ + set nosmoothscroll scrolloff=0 + call setline(1, map(range(1, 20), '"short " .. v:val')) + call append(20, 'CURSOR') + call append(21, map(range(1, 8), 'repeat("x", 200)')) + ]]) + feed('21Gzz') + -- The lines below the cursor wrap to five rows each, so only one of them + -- fits under the cursor once it sits on row 6 of this 11 row window. + screen:expect([[ + short 16 | + short 17 | + short 18 | + short 19 | + short 20 | + ^CURSOR | + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|*5 + | + ]]) + end) +end) diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index 2f5d9107a8..121049d4d0 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -533,8 +533,6 @@ describe('statuscolumn', function() exec_lua('vim.api.nvim_buf_set_extmark(0, ns, 15, 0, { virt_lines = {{{"END", ""}}} })') feed('GkJzz') screen:expect([[ - {8:buffer 0 12}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| - {8:wrapped 1 12}aaaaaaaaa | {8:buffer 0 13}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| {8:wrapped 1 13}aaaaaaaaa | {8:buffer 0 14}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| @@ -543,7 +541,7 @@ describe('statuscolumn', function() {15:wrapped 1 15}{19:aaaaaaaaa^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}| {15:wrapped 2 15}{19:aaaaaaaaaaaaaaaaaaa }| {8:virtual-1 15}END | - {1:~ }|*3 + {1:~ }|*5 | ]]) -- Also test virt_lines when 'cpoptions' includes "n" @@ -553,8 +551,6 @@ describe('statuscolumn', function() vim.api.nvim_buf_set_extmark(0, ns, 14, 0, { virt_lines = {{{"virt_line2", ""}}} }) ]]) screen:expect([[ - {8:buffer 0 12}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| - aaaaaaaaa | {8:buffer 0 13}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| aaaaaaaaa | {8:buffer 0 14}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| @@ -565,7 +561,7 @@ describe('statuscolumn', function() {8:virtual-1 15}virt_line1 | {8:virtual-2 15}virt_line2 | {8:virtual-3 15}END | - {1:~ }| + {1:~ }|*3 | ]]) -- Also test "col_rows" code path for 'relativenumber' cursor movement @@ -574,8 +570,6 @@ describe('statuscolumn', function() set stc=%{v:virtnum<0?'virtual':(!v:virtnum?'buffer':'wrapped')}%=%{'\ '.v:virtnum.'\ '.v:lnum.'\ '.v:relnum} ]]) screen:expect([[ - {8:buffer 0 12 3}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| - {8:wrapped 1 12 3}aaaaaaaaaaa | {8:buffer 0 13 2}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| {8:wrapped 1 13 2}aaaaaaaaaaa | {8:buffer 0 14 1}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| @@ -586,13 +580,11 @@ describe('statuscolumn', function() {8:virtual-1 15 0}virt_line1 | {8:virtual-2 15 0}virt_line2 | {8:virtual-3 15 0}END | - {1:~ }| + {1:~ }|*3 | ]]) feed('kk') screen:expect([[ - {8:buffer 0 12 1}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| - {8:wrapped 1 12 1}aaaaaaaaaaa | {8:buffer 0 13 0}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| {8:wrapped 1 13 0}aaaaaaaaaa^a | {8:buffer 0 14 1}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| @@ -603,7 +595,7 @@ describe('statuscolumn', function() {8:virtual-1 15 2}virt_line1 | {8:virtual-2 15 2}virt_line2 | {8:virtual-3 15 2}END | - {1:~ }| + {1:~ }|*3 | ]]) feed('gg5')