summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorzeertzjq <zeertzjq@outlook.com>2026-04-22 11:01:43 +0800
committerGitHub <noreply@github.com>2026-04-22 11:01:43 +0800
commit56fb9ed82de5082f2ce2ff869fdca42225a0204e (patch)
treeab817505804e15d855741d930499c7b6275a86d2
parent1569a71c8a51287628cb5257e45d2e68f1181551 (diff)
parent25b7fe5ada09a987352298f3674dc36f6409ece8 (diff)
Merge pull request #39248 from zeertzjq/vim-9.2.0356
vim-patch: 'scrolloffpad'
-rw-r--r--runtime/doc/news.txt1
-rw-r--r--runtime/doc/options.txt22
-rw-r--r--runtime/doc/quickref.txt1
-rw-r--r--runtime/lua/vim/_meta/options.gen.lua30
-rw-r--r--runtime/scripts/optwin.lua1
-rw-r--r--src/nvim/buffer_defs.h2
-rw-r--r--src/nvim/move.c56
-rw-r--r--src/nvim/option.c19
-rw-r--r--src/nvim/option_vars.h1
-rw-r--r--src/nvim/options.lua29
-rw-r--r--src/nvim/window.c1
-rw-r--r--test/functional/legacy/scroll_opt_spec.lua202
-rw-r--r--test/old/testdir/gen_opt_test.vim2
-rw-r--r--test/old/testdir/test_cursor_func.vim3
-rw-r--r--test/old/testdir/test_options.vim40
-rw-r--r--test/old/testdir/test_scroll_opt.vim651
16 files changed, 1044 insertions, 17 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 9da054ed87..9f78179c61 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -159,6 +159,7 @@ the same range instances now compare equal.
OPTIONS
+• 'scrolloffpad' allows vertically centering cursor at the end of file.
• 'winpinned' prevents window from closing unless specifically targeted.
PERFORMANCE
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index f7a796c24d..bedd423e37 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -5278,14 +5278,32 @@ A jump table for the options with a short description can be found at |Q_op|.
Minimal number of screen lines to keep above and below the cursor.
This will make some context visible around where you are working. If
you set it to a very large value (999) the cursor line will always be
- in the middle of the window (except at the start or end of the file or
- when long lines wrap).
+ in the middle of the window (except at the start or end of the file,
+ see 'scrolloffpad', or when long lines wrap).
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloff<
setlocal scrolloff=-1
< For scrolling horizontally see 'sidescrolloff'.
+ *'scrolloffpad'* *'sop'*
+'scrolloffpad' 'sop' number (default 0)
+ global or local to window |global-local|
+ When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
+ the cursor to remain centered when at the end of the file.
+ Normally, 'scrolloff' will not keep the cursor centered at the
+ end of the file.
+
+ A value of 0 disables this feature. Any value above 0 enables it.
+ For a window-local value, -1 means to use the global value.
+ Values below -1 are invalid.
+
+ After using the local value, go back the global value with one of
+ these two: >vim
+ setlocal scrolloffpad<
+ setlocal scrolloffpad=-1
+<
+
*'scrollopt'* *'sbo'*
'scrollopt' 'sbo' string (default "ver,jump")
global
diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt
index 78444dbd33..9fc8770dd3 100644
--- a/runtime/doc/quickref.txt
+++ b/runtime/doc/quickref.txt
@@ -841,6 +841,7 @@ Short explanation of each option: *option-list*
'scrollbind' 'scb' scroll in window as other windows scroll
'scrolljump' 'sj' minimum number of lines to scroll
'scrolloff' 'so' minimum nr. of lines above and below cursor
+'scrolloffpad' 'sop' vertically center cursor at end of file
'scrollopt' 'sbo' how 'scrollbind' should behave
'sections' 'sect' nroff macros that separate sections
'secure' secure mode for reading .vimrc in current dir
diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua
index 3842c9a56d..38fdaa774c 100644
--- a/runtime/lua/vim/_meta/options.gen.lua
+++ b/runtime/lua/vim/_meta/options.gen.lua
@@ -5488,8 +5488,8 @@ vim.go.sj = vim.go.scrolljump
--- Minimal number of screen lines to keep above and below the cursor.
--- This will make some context visible around where you are working. If
--- you set it to a very large value (999) the cursor line will always be
---- in the middle of the window (except at the start or end of the file or
---- when long lines wrap).
+--- in the middle of the window (except at the start or end of the file,
+--- see 'scrolloffpad', or when long lines wrap).
--- After using the local value, go back the global value with one of
--- these two:
---
@@ -5507,6 +5507,32 @@ vim.wo.so = vim.wo.scrolloff
vim.go.scrolloff = vim.o.scrolloff
vim.go.so = vim.go.scrolloff
+--- When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
+--- the cursor to remain centered when at the end of the file.
+--- Normally, 'scrolloff' will not keep the cursor centered at the
+--- end of the file.
+---
+--- A value of 0 disables this feature. Any value above 0 enables it.
+--- For a window-local value, -1 means to use the global value.
+--- Values below -1 are invalid.
+---
+--- After using the local value, go back the global value with one of
+--- these two:
+---
+--- ```vim
+--- setlocal scrolloffpad<
+--- setlocal scrolloffpad=-1
+--- ```
+---
+---
+--- @type integer
+vim.o.scrolloffpad = 0
+vim.o.sop = vim.o.scrolloffpad
+vim.wo.scrolloffpad = vim.o.scrolloffpad
+vim.wo.sop = vim.wo.scrolloffpad
+vim.go.scrolloffpad = vim.o.scrolloffpad
+vim.go.sop = vim.go.scrolloffpad
+
--- This is a comma-separated list of words that specifies how
--- 'scrollbind' windows should behave. 'sbo' stands for ScrollBind
--- Options.
diff --git a/runtime/scripts/optwin.lua b/runtime/scripts/optwin.lua
index 6961182cf3..ff5bd8d7f6 100644
--- a/runtime/scripts/optwin.lua
+++ b/runtime/scripts/optwin.lua
@@ -59,6 +59,7 @@ local options_list = {
{ 'scroll', N_ 'number of lines to scroll for CTRL-U and CTRL-D' },
{ 'smoothscroll', N_ 'scroll by screen line' },
{ 'scrolloff', N_ 'number of screen lines to show around the cursor' },
+ { 'scrolloffpad', N_ 'vertically center cursor even at end of file' },
{ 'wrap', N_ 'long lines wrap' },
{ 'linebreak', N_ "wrap long lines at a character in 'breakat'" },
{ 'breakindent', N_ 'preserve indentation in wrapped text' },
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index e3ed8a67c7..72e67d4fd6 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -204,6 +204,8 @@ typedef struct {
#define w_p_siso w_onebuf_opt.wo_siso // 'sidescrolloff' local value
OptInt wo_so;
#define w_p_so w_onebuf_opt.wo_so // 'scrolloff' local value
+ OptInt wo_sop;
+#define w_p_sop w_onebuf_opt.wo_sop // 'scrolloffpad' local value
char *wo_winhl;
#define w_p_winhl w_onebuf_opt.wo_winhl // 'winhighlight'
char *wo_lcs;
diff --git a/src/nvim/move.c b/src/nvim/move.c
index a1ef9634d2..99b1e73e32 100644
--- a/src/nvim/move.c
+++ b/src/nvim/move.c
@@ -246,6 +246,26 @@ static void reset_skipcol(win_T *wp)
redraw_later(wp, UPD_SOME_VALID);
}
+/// Return true when 'scrolloffpad' may augment 'scrolloff'.
+/// This only applies to automatic cursor visibility correction.
+/// For now 'scrolloffpad' is treated as boolean: 0 disables, > 0 enables.
+static bool use_scrolloffpad(win_T *wp)
+{
+ return get_scrolloff_value(wp) > 0 && get_scrolloffpad_value(wp) > 0;
+}
+
+/// Return true when there are not enough real buffer lines below "lnum" to
+/// satisfy the requested "so" context.
+static bool scrolloffpad_eof_pressure(win_T *wp, linenr_T lnum, OptInt so)
+{
+ if (!use_scrolloffpad(wp) || so <= 0) {
+ return false;
+ }
+
+ // Use subtraction to avoid signed overflow in "lnum + so".
+ return lnum > wp->w_buffer->b_ml.ml_line_count - so;
+}
+
// Update wp->w_topline to move the cursor onto the screen.
void update_topline(win_T *wp)
{
@@ -278,6 +298,7 @@ void update_topline(win_T *wp)
if (mouse_dragging > 0) {
*so_ptr = mouse_dragging - 1;
}
+ bool eof_pressure = scrolloffpad_eof_pressure(wp, wp->w_cursor.lnum, *so_ptr);
linenr_T old_topline = wp->w_topline;
int old_topfill = wp->w_topfill;
@@ -350,10 +371,18 @@ void update_topline(win_T *wp)
// cursor in the middle of the window. Otherwise put the cursor
// near the top of the window.
if (n >= halfheight) {
- scroll_cursor_halfway(wp, false, false);
+ if (eof_pressure) {
+ scroll_cursor_halfway(wp, true, true);
+ } else {
+ scroll_cursor_halfway(wp, false, false);
+ }
} else {
- scroll_cursor_top(wp, scrolljump_value(wp), false);
- check_botline = true;
+ if (eof_pressure) {
+ scroll_cursor_halfway(wp, true, true);
+ } else {
+ scroll_cursor_top(wp, scrolljump_value(wp), false);
+ check_botline = true;
+ }
}
} else {
// Make sure topline is the first line of a fold.
@@ -374,7 +403,7 @@ void update_topline(win_T *wp)
}
assert(wp->w_buffer != 0);
- if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count) {
+ if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count || use_scrolloffpad(wp)) {
if (wp->w_cursor.lnum < wp->w_botline) {
if ((wp->w_cursor.lnum >= wp->w_botline - *so_ptr || win_lines_concealed(wp))) {
lineoff_T loff;
@@ -382,7 +411,7 @@ void update_topline(win_T *wp)
// Cursor is (a few lines) above botline, check if there are
// 'scrolloff' window lines below the cursor. If not, need to
// scroll.
- int n = wp->w_empty_rows;
+ int n = eof_pressure ? 0 : wp->w_empty_rows;
loff.lnum = wp->w_cursor.lnum;
// In a fold go to its last line.
hasFolding(wp, loff.lnum, NULL, &loff.lnum);
@@ -397,7 +426,7 @@ void update_topline(win_T *wp)
}
botline_forw(wp, &loff);
}
- if (n >= *so_ptr) {
+ if (n >= *so_ptr && !eof_pressure) {
// sufficient context, no need to scroll
check_botline = false;
}
@@ -424,9 +453,13 @@ void update_topline(win_T *wp)
n = wp->w_cursor.lnum - wp->w_botline + 1 + *so_ptr;
}
if (n <= wp->w_view_height + 1) {
- scroll_cursor_bot(wp, scrolljump_value(wp), false);
+ if (eof_pressure) {
+ scroll_cursor_halfway(wp, true, true);
+ } else {
+ scroll_cursor_bot(wp, scrolljump_value(wp), false);
+ }
} else {
- scroll_cursor_halfway(wp, false, false);
+ scroll_cursor_halfway(wp, eof_pressure, eof_pressure);
}
}
}
@@ -2111,8 +2144,9 @@ void scroll_cursor_bot(win_T *wp, int min_scroll, bool set_topbot)
// Scroll up if the cursor is off the bottom of the screen a bit.
// Otherwise put it at 1/2 of the screen.
+ bool eof_pressure = scrolloffpad_eof_pressure(wp, cln, so);
if (line_count >= wp->w_view_height && line_count > min_scroll) {
- scroll_cursor_halfway(wp, false, true);
+ scroll_cursor_halfway(wp, eof_pressure, true);
} else if (line_count > 0) {
if (do_sms) {
scrollup(wp, scrolled, true); // TODO(vim):
@@ -2289,7 +2323,9 @@ void cursor_correct(win_T *wp)
validate_botline_win(wp);
if (wp->w_botline == wp->w_buffer->b_ml.ml_line_count + 1
&& mouse_dragging == 0) {
- below_wanted = 0;
+ if (!use_scrolloffpad(wp)) {
+ below_wanted = 0;
+ }
int max_off = (wp->w_view_height - 1) / 2;
above_wanted = MIN(above_wanted, max_off);
}
diff --git a/src/nvim/option.c b/src/nvim/option.c
index 2318625720..0e33292f77 100644
--- a/src/nvim/option.c
+++ b/src/nvim/option.c
@@ -3111,6 +3111,12 @@ static const char *validate_num_option(OptIndex opt_idx, OptInt *newval, char *e
return e_positive;
}
break;
+ case kOptScrolloffpad:
+ // if (value < 0 && full_screen) {
+ if (value < 0) {
+ return e_invarg;
+ }
+ break;
case kOptSidescrolloff:
if (value < 0 && full_screen) {
return e_positive;
@@ -3600,6 +3606,7 @@ static OptVal get_option_unset_value(OptIndex opt_idx)
case kOptFsync:
return BOOLEAN_OPTVAL(kNone);
case kOptScrolloff:
+ case kOptScrolloffpad:
case kOptSidescrolloff:
return NUMBER_OPTVAL(-1);
case kOptUndolevels:
@@ -4673,6 +4680,8 @@ void *get_varp_scope_from(vimoption_T *p, int opt_flags, buf_T *buf, win_T *win)
return &(win->w_p_siso);
case kOptScrolloff:
return &(win->w_p_so);
+ case kOptScrolloffpad:
+ return &(win->w_p_sop);
case kOptDefine:
return &(buf->b_p_def);
case kOptInclude:
@@ -4760,6 +4769,8 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return win->w_p_siso >= 0 ? &(win->w_p_siso) : p->var;
case kOptScrolloff:
return win->w_p_so >= 0 ? &(win->w_p_so) : p->var;
+ case kOptScrolloffpad:
+ return win->w_p_sop >= 0 ? &(win->w_p_sop) : p->var;
case kOptBackupcopy:
return *buf->b_p_bkc != NUL ? &(buf->b_p_bkc) : p->var;
case kOptDefine:
@@ -5121,6 +5132,7 @@ void copy_winopt(winopt_T *from, winopt_T *to)
to->wo_crb_save = from->wo_crb_save;
to->wo_siso = from->wo_siso;
to->wo_so = from->wo_so;
+ to->wo_sop = from->wo_sop;
to->wo_spell = from->wo_spell;
to->wo_cuc = from->wo_cuc;
to->wo_cul = from->wo_cul;
@@ -6560,6 +6572,13 @@ int64_t get_scrolloff_value(win_T *wp)
return wp->w_p_so < 0 ? p_so : wp->w_p_so;
}
+/// Return the effective 'scrolloffpad' value for the current window, using the
+/// global value when appropriate.
+int64_t get_scrolloffpad_value(win_T *wp)
+{
+ return wp->w_p_sop == -1 ? p_sop : curwin->w_p_sop;
+}
+
/// Return the effective 'sidescrolloff' value for the current window, using the
/// global value when appropriate.
int64_t get_sidescrolloff_value(win_T *wp)
diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h
index a8f26f4730..c57020b553 100644
--- a/src/nvim/option_vars.h
+++ b/src/nvim/option_vars.h
@@ -472,6 +472,7 @@ EXTERN char *p_rtp; ///< 'runtimepath'
EXTERN OptInt p_scbk; ///< 'scrollback'
EXTERN OptInt p_sj; ///< 'scrolljump'
EXTERN OptInt p_so; ///< 'scrolloff'
+EXTERN OptInt p_sop; ///< 'scrolloffpad'
EXTERN char *p_sbo; ///< 'scrollopt'
EXTERN char *p_sections; ///< 'sections'
EXTERN int p_secure; ///< 'secure'
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index cb3db201bd..666888abcd 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -7237,8 +7237,8 @@ local options = {
Minimal number of screen lines to keep above and below the cursor.
This will make some context visible around where you are working. If
you set it to a very large value (999) the cursor line will always be
- in the middle of the window (except at the start or end of the file or
- when long lines wrap).
+ in the middle of the window (except at the start or end of the file,
+ see 'scrolloffpad', or when long lines wrap).
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloff<
@@ -7252,6 +7252,31 @@ local options = {
varname = 'p_so',
},
{
+ abbreviation = 'sop',
+ defaults = 0,
+ desc = [=[
+ When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
+ the cursor to remain centered when at the end of the file.
+ Normally, 'scrolloff' will not keep the cursor centered at the
+ end of the file.
+
+ A value of 0 disables this feature. Any value above 0 enables it.
+ For a window-local value, -1 means to use the global value.
+ Values below -1 are invalid.
+
+ After using the local value, go back the global value with one of
+ these two: >vim
+ setlocal scrolloffpad<
+ setlocal scrolloffpad=-1
+ <
+ ]=],
+ full_name = 'scrolloffpad',
+ scope = { 'global', 'win' },
+ short_desc = N_('vertically center cursor even at end of file'),
+ type = 'number',
+ varname = 'p_sop',
+ },
+ {
abbreviation = 'sbo',
defaults = 'ver,jump',
values = { 'ver', 'hor', 'jump' },
diff --git a/src/nvim/window.c b/src/nvim/window.c
index 5f9082f6e6..40f471a9fb 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -5532,6 +5532,7 @@ win_T *win_alloc(win_T *after, bool hidden)
// use global option for global-local options
new_wp->w_allbuf_opt.wo_so = new_wp->w_p_so = -1;
+ new_wp->w_allbuf_opt.wo_sop = new_wp->w_p_sop = -1;
new_wp->w_allbuf_opt.wo_siso = new_wp->w_p_siso = -1;
// We won't calculate w_fraction until resizing the window
diff --git a/test/functional/legacy/scroll_opt_spec.lua b/test/functional/legacy/scroll_opt_spec.lua
index 16f061a5c7..505d479418 100644
--- a/test/functional/legacy/scroll_opt_spec.lua
+++ b/test/functional/legacy/scroll_opt_spec.lua
@@ -1397,3 +1397,205 @@ describe('smoothscroll', function()
]])
end)
end)
+
+describe('scrolloffpad', function()
+ local screen
+
+ before_each(function()
+ screen = Screen.new(78, 20)
+ end)
+
+ -- oldtest: Test_scrolloffpad_basic()
+ it('works', function()
+ exec([[
+ set scrolloff=10
+ set scrolloffpad=5
+ enew!
+ call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
+ normal! gg
+ ]])
+
+ -- Enabled: scrolloffpad > 0, expect EOF centering/padding
+ exec('normal! G')
+ screen:expect([[
+ line 91 |
+ line 92 |
+ line 93 |
+ line 94 |
+ line 95 |
+ line 96 |
+ line 97 |
+ line 98 |
+ line 99 |
+ ^line 100 |
+ {1:~ }|*9
+ |
+ ]])
+
+ -- Beginning-of-file is unchanged (Top)
+ exec('normal! gg')
+ screen:expect([[
+ ^line 1 |
+ line 2 |
+ line 3 |
+ line 4 |
+ line 5 |
+ line 6 |
+ line 7 |
+ line 8 |
+ line 9 |
+ line 10 |
+ line 11 |
+ line 12 |
+ line 13 |
+ line 14 |
+ line 15 |
+ line 16 |
+ line 17 |
+ line 18 |
+ line 19 |
+ |
+ ]])
+
+ -- Gating: disable scrolloffpad, then go to EOF again
+ -- Expect normal EOF behavior (no extra centering/padding)
+ exec('set scrolloffpad=0')
+ exec('normal! G')
+ screen:expect([[
+ line 82 |
+ line 83 |
+ line 84 |
+ line 85 |
+ line 86 |
+ line 87 |
+ line 88 |
+ line 89 |
+ line 90 |
+ line 91 |
+ line 92 |
+ line 93 |
+ line 94 |
+ line 95 |
+ line 96 |
+ line 97 |
+ line 98 |
+ line 99 |
+ ^line 100 |
+ |
+ ]])
+ end)
+
+ -- oldtest: Test_scrolloffpad_smoothscroll()
+ it('works with smoothscroll', function()
+ exec([[
+ set smoothscroll scrolloff=10 scrolloffpad=1
+ enew!
+ call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
+ normal! gg
+ ]])
+
+ exec('normal! G')
+ screen:expect([[
+ line 91 |
+ line 92 |
+ line 93 |
+ line 94 |
+ line 95 |
+ line 96 |
+ line 97 |
+ line 98 |
+ line 99 |
+ ^line 100 |
+ {1:~ }|*9
+ |
+ ]])
+
+ exec([[call setline(line('$'), repeat('LONG ', 30))]])
+ exec('normal! 41|')
+ screen:expect([[
+ line 92 |
+ line 93 |
+ line 94 |
+ line 95 |
+ line 96 |
+ line 97 |
+ line 98 |
+ line 99 |
+ LONG LONG LONG LONG LONG LONG LONG LONG ^LONG LONG LONG LONG LONG LONG LONG LON|
+ G LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG |
+ {1:~ }|*9
+ |
+ ]])
+ end)
+
+ -- oldtest: Test_scrolloffpad_with_folds()
+ it('works with folds', function()
+ exec([[
+ set scrolloff=10
+ set scrolloffpad=1
+
+ enew
+ call setline(1, map(range(1, 120), {_, v -> 'line ' . v}))
+
+ " Create a large fold near the end of the file.
+ " Fold lines 60-110, leaving 111-120 visible after the fold.
+ set foldmethod=manual
+ set foldenable
+ normal! gg
+ normal! 60G
+ normal! zf50j
+ normal! gg
+ ]])
+
+ -- Case 1: Jump to end-of-file
+ -- With folds present, scrolloffpad should still
+ -- keep the cursor positioned with padding below EOF
+ exec('normal! G')
+ local s1 = [[
+ line 111 |
+ line 112 |
+ line 113 |
+ line 114 |
+ line 115 |
+ line 116 |
+ line 117 |
+ line 118 |
+ line 119 |
+ ^line 120 |
+ {1:~ }|*9
+ |
+ ]]
+ screen:expect(s1)
+
+ -- Case 2: Move to the folded line to ensure the fold is actually in view
+ exec('normal! 60G')
+ screen:expect([[
+ line 51 |
+ line 52 |
+ line 53 |
+ line 54 |
+ line 55 |
+ line 56 |
+ line 57 |
+ line 58 |
+ line 59 |
+ {13:^+-- 51 lines: line 60·························································}|
+ line 111 |
+ line 112 |
+ line 113 |
+ line 114 |
+ line 115 |
+ line 116 |
+ line 117 |
+ line 118 |
+ line 119 |
+ |
+ ]])
+
+ -- Case 3: Close the fold explicitly and go to EOF again
+ -- Behavior should remain stable with closed folds
+ exec('normal! zc')
+ exec('normal! G')
+ screen:expect(s1)
+ end)
+end)
diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim
index 07997a5ee5..733d8099cc 100644
--- a/test/old/testdir/gen_opt_test.vim
+++ b/test/old/testdir/gen_opt_test.vim
@@ -24,6 +24,7 @@ while search("^'[^']*'.*\\n.*|global-local", 'W')
endwhile
call extend(global_locals, #{
\ scrolloff: -1,
+ \ scrolloffpad: -1,
\ sidescrolloff: -1,
\ undolevels: -123456,
\})
@@ -127,6 +128,7 @@ let test_values = {
\ 'scroll': [[0, 1, 2, 15], [-1, 999]],
\ 'scrolljump': [[-100, -1, 0, 1, 2, 15], [-101, 999]],
\ 'scrolloff': [[0, 1, 8, 999], [-1]],
+ \ 'scrolloffpad': [[0, 1, 2, 3], [-1]],
\ 'shiftwidth': [[0, 1, 8, 999], [-1]],
\ 'sidescroll': [[0, 1, 8, 999], [-1]],
\ 'sidescrolloff': [[0, 1, 8, 999], [-1]],
diff --git a/test/old/testdir/test_cursor_func.vim b/test/old/testdir/test_cursor_func.vim
index 2554167c82..912a2d5473 100644
--- a/test/old/testdir/test_cursor_func.vim
+++ b/test/old/testdir/test_cursor_func.vim
@@ -124,7 +124,8 @@ func Test_screenpos()
setlocal nonumber display=lastline so=0
exe "normal G\<C-Y>\<C-Y>"
redraw
- call assert_equal({'row': winrow + wininfo.height - 1,
+ let winbar_height = get(wininfo, 'winbar', 0)
+ call assert_equal({'row': winrow + wininfo.height - 1 + winbar_height,
\ 'col': wincol + 7,
\ 'curscol': wincol + 7,
\ 'endcol': wincol + 7}, winid->screenpos(line('$'), 8))
diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim
index b424383791..cbd1f74bd9 100644
--- a/test/old/testdir/test_options.vim
+++ b/test/old/testdir/test_options.vim
@@ -1470,6 +1470,46 @@ func Test_local_scrolloff()
set siso&
endfunc
+func Test_local_scrolloffpad()
+ let save_g_sop = &g:sop
+ let save_l_sop = &l:sop
+ set sop=0
+ call assert_equal(0, &g:sop)
+ call assert_equal(-1, &l:sop)
+ call assert_equal(0, &sop)
+ setglobal sop=1
+ call assert_equal(1, &g:sop)
+ call assert_equal(1, &sop)
+ split
+ call assert_equal(1, &g:sop)
+ call assert_equal(-1, &l:sop)
+ call assert_equal(1, &sop)
+ setlocal sop=0
+ call assert_equal(0, &l:sop)
+ call assert_equal(0, &sop)
+ call assert_equal(1, &g:sop)
+ wincmd p
+ call assert_equal(1, &sop)
+ wincmd p
+ "setlocal sop<
+ set sop<
+ call assert_equal(-1, &l:sop)
+ call assert_equal(1, &sop)
+ setlocal sop=2
+ call assert_equal(2, &l:sop)
+ call assert_equal(2, &sop)
+ setlocal sop=-1
+ call assert_equal(-1, &l:sop)
+ call assert_equal(1, &sop) " Uses global value because local is -1
+ call assert_fails("setlocal sop=-2", 'E474:')
+ call assert_equal(-1, &l:sop)
+ call assert_equal(1, &sop)
+ call assert_fails("setlocal sop=foo", 'E521:')
+ close
+ let &g:sop = save_g_sop
+ let &l:sop = save_l_sop
+endfunc
+
func Test_writedelay()
CheckFunction reltimefloat
diff --git a/test/old/testdir/test_scroll_opt.vim b/test/old/testdir/test_scroll_opt.vim
index e5e5067b54..6f2f8010f4 100644
--- a/test/old/testdir/test_scroll_opt.vim
+++ b/test/old/testdir/test_scroll_opt.vim
@@ -1443,6 +1443,657 @@ func Test_smoothscroll_listchars_eol()
bwipe!
endfunc
+" scrolloffpad contract:
+" - augment scrolloff only under EOF pressure (insufficient real lines below);
+" - do not change explicit "z" viewport placement command semantics;
+" - current scope is EOF-only, so BOF behavior remains unchanged.
+func Test_scrolloffpad_zb_keeps_bottom_command_semantics()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 300), 'printf("line %d", v:val)'))
+
+ setlocal scrolloffpad=0
+ normal! gg150Gzb
+ let baseline = [line('.'), line('w$'), winline()]
+
+ setlocal scrolloffpad=1
+ normal! gg150Gzb
+ call assert_equal(baseline, [line('.'), line('w$'), winline()])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_zminus_keeps_bottom_beginline_semantics()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 300), 'printf(" line %d", v:val)'))
+
+ setlocal scrolloffpad=0
+ normal! gg150Gz-
+ let baseline = [line('.'), line('w$'), winline(), col('.')]
+ call assert_equal(match(getline('.'), '\S') + 1, col('.'))
+
+ setlocal scrolloffpad=1
+ normal! gg150Gz-
+ call assert_equal(baseline, [line('.'), line('w$'), winline(), col('.')])
+ call assert_equal(match(getline('.'), '\S') + 1, col('.'))
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_zb_is_one_shot_then_scrolloff_reapplies()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 300), 'printf("line %d", v:val)'))
+
+ let after_zb = {}
+ let after_j = {}
+ for sop in [0, 1]
+ let &l:scrolloffpad = sop
+ normal! gg150Gzb
+ let after_zb[sop] = [line('.'), line('w$'), winline(), winsaveview().topline]
+
+ normal! j
+ let after_j[sop] = [line('.'), line('w$'), winline(), winsaveview().topline]
+ call assert_notequal(after_zb[sop][3], after_j[sop][3])
+ call assert_true(line('.') < line('w$'))
+ endfor
+ call assert_equal(after_zb[0], after_zb[1])
+ call assert_equal(after_j[0], after_j[1])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_has_no_mid_buffer_effect()
+ new
+ resize 12
+ setlocal scrolloff=10 scrolloffpad=0
+ call setline(1, map(range(1, 500), 'printf("line %d", v:val)'))
+
+ normal! gg150G
+ let topline_without_pad = winsaveview().topline
+
+ setlocal scrolloffpad=1
+ normal! gg150G
+ let topline_with_pad = winsaveview().topline
+
+ call assert_equal(topline_without_pad, topline_with_pad)
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_changes_eof_pressure_only()
+ new
+ resize 12
+ setlocal scrolloff=10 scrolloffpad=0
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+
+ normal! ggG
+ let view_without_pad = winsaveview()
+ let cursor_without_pad = line('.')
+ let row_without_pad = winline()
+
+ setlocal scrolloffpad=1
+ normal! ggG
+ let view_with_pad = winsaveview()
+ let row_with_pad = winline()
+
+ call assert_equal(line('$'), line('.'))
+ call assert_equal(cursor_without_pad, line('.'))
+ call assert_notequal(view_without_pad.topline, view_with_pad.topline)
+ call assert_true(row_with_pad < row_without_pad)
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_large_scrolloff_no_overflow()
+ new
+ resize 12
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+ setlocal scrolloff=2147483647 scrolloffpad=0
+
+ normal! ggG
+ let view_without_pad = winsaveview()
+ let row_without_pad = winline()
+
+ setlocal scrolloffpad=1
+ normal! ggG
+ let view_with_pad = winsaveview()
+ let row_with_pad = winline()
+
+ call assert_equal(line('$'), line('.'))
+ call assert_notequal(view_without_pad.topline, view_with_pad.topline)
+ call assert_true(row_with_pad < row_without_pad)
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_boolean_gate_values()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+
+ let views = {}
+ let rows = {}
+ for sop in [0, 1, 2]
+ let &l:scrolloffpad = sop
+ normal! ggG
+ let views[sop] = winsaveview()
+ let rows[sop] = winline()
+ call assert_equal(line('$'), line('.'))
+ endfor
+
+ call assert_equal(views[1].topline, views[2].topline)
+ call assert_equal(rows[1], rows[2])
+ call assert_notequal(views[0].topline, views[1].topline)
+ call assert_true(rows[1] < rows[0])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_requires_scrolloff_nonzero()
+ new
+ resize 12
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+
+ let states = {}
+ for so in [0, 10]
+ let states[so] = {}
+ for sop in [0, 1]
+ let &l:scrolloff = so
+ let &l:scrolloffpad = sop
+ normal! ggG
+ let states[so][sop] = [line('.'), line('w0'), line('w$'), winline()]
+ call assert_equal(line('$'), line('.'))
+ endfor
+ endfor
+
+ call assert_equal(states[0][0], states[0][1])
+ call assert_notequal(states[10][0], states[10][1])
+ call assert_true(states[10][1][3] < states[10][0][3])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_search_to_eof()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+ call setline(line('$'), 'EOF TARGET')
+
+ let states = {}
+ for sop in [0, 1]
+ let &l:scrolloffpad = sop
+ normal! gg
+ call assert_true(search('EOF TARGET') > 0)
+ let states[sop] = [line('.'), line('w0'), line('w$'), winline()]
+ call assert_equal(line('$'), line('.'))
+ endfor
+
+ call assert_notequal(states[0], states[1])
+ call assert_true(states[1][3] < states[0][3])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_paging_to_eof()
+ new
+ resize 12
+ setlocal scrolloff=10
+ call setline(1, map(range(1, 240), 'printf("line %d", v:val)'))
+
+ let states = {}
+ for sop in [0, 1]
+ let &l:scrolloffpad = sop
+ normal! gg
+
+ let prev = -1
+ for _ in range(1, 200)
+ execute "normal! \<C-D>"
+ if line('.') == prev
+ break
+ endif
+ let prev = line('.')
+ endfor
+
+ let states[sop] = [line('.'), line('w0'), line('w$'), winline()]
+ call assert_equal(line('$'), line('w$'))
+ endfor
+
+ call assert_notequal(states[0], states[1])
+ call assert_true(states[1][3] < states[0][3])
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_autocmd_append_at_eof()
+ let states = {}
+ for sop in [0, 1]
+ new
+ resize 12
+ setlocal scrolloff=10
+ let &l:scrolloffpad = sop
+ call setline(1, map(range(1, 120), 'printf("line %d", v:val)'))
+
+ let b:scrolloffpad_appended = 0
+ augroup ScrolloffpadAppendAtEof
+ autocmd!
+ autocmd CursorMoved <buffer> if b:scrolloffpad_appended == 0 && line('.') == line('$') | call append('$', 'appended') | let b:scrolloffpad_appended = 1 | endif
+ augroup END
+
+ normal! ggG
+ doautocmd <nomodeline> CursorMoved
+ let states[sop] = [
+ \ line('.'),
+ \ line('$'),
+ \ line('w0'),
+ \ line('w$'),
+ \ winline(),
+ \ b:scrolloffpad_appended,
+ \ ]
+
+ call assert_equal(1, b:scrolloffpad_appended)
+ call assert_equal(states[sop][1] - 1, states[sop][0])
+
+ augroup ScrolloffpadAppendAtEof
+ autocmd!
+ augroup END
+ bwipe!
+ endfor
+
+ call assert_notequal(states[0], states[1])
+ call assert_true(states[1][4] < states[0][4])
+
+endfunc
+
+func Test_scrolloffpad_eof_no_reverse_scroll_on_j()
+ new
+ resize 20
+ setlocal scrolloff=20 scrolloffpad=1
+ call setline(1, map(range(1, 80), 'printf("line %d", v:val)'))
+
+ normal! gg
+ let prev_topline = winsaveview().topline
+ for lnum in range(2, line('$'))
+ normal! j
+ let cur_topline = winsaveview().topline
+ call assert_true(
+ \ cur_topline >= prev_topline,
+ \ printf('topline moved backwards at line %d: %d -> %d',
+ \ lnum, prev_topline, cur_topline))
+ let prev_topline = cur_topline
+ endfor
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_bof_unchanged()
+ new
+ resize 12
+ setlocal scrolloff=10 scrolloffpad=0
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+
+ normal! Ggg
+ let view_without_pad = winsaveview()
+ let w0_without_pad = line('w0')
+
+ setlocal scrolloffpad=1
+ normal! Ggg
+ let view_with_pad = winsaveview()
+ let w0_with_pad = line('w0')
+
+ call assert_equal(1, w0_without_pad)
+ call assert_equal(1, w0_with_pad)
+ call assert_equal(view_without_pad.topline, view_with_pad.topline)
+
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_mouse_drag_uses_drag_scrolloff()
+ CheckFeature mouse
+
+ let save_mouse = &mouse
+ set mouse=a
+
+ new
+ resize 20
+ call setline(1, map(range(1, 240), 'printf("line %d", v:val)'))
+ setlocal scrolloff=50
+
+ let after_drag = {}
+ for sop in [0, 1]
+ let &l:scrolloffpad = sop
+ normal! gg160Gzt
+ normal! v
+ call Ntest_setmouse(2, 1)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call Ntest_setmouse(3, 1)
+ call feedkeys("\<LeftDrag>", 'xt')
+ let after_drag[sop] = [winsaveview().topline, line('.'), winline()]
+ call feedkeys("\<Esc>", 'xt')
+ endfor
+
+ call assert_equal(after_drag[0], after_drag[1])
+
+ bwipe!
+ let &mouse = save_mouse
+endfunc
+
+func Test_scrolloffpad_basic()
+ CheckScreendump
+ CheckRunVimInTerminal
+
+ let save_termwinsize = &termwinsize
+ set termwinsize=
+
+ let lines =<< trim END
+ set scrolloff=10
+ set scrolloffpad=5
+ enew!
+ call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
+ normal! gg
+ END
+ call writefile(lines, 'XScrolloffpadBasic', 'D')
+
+ let buf = RunVimInTerminal('-S XScrolloffpadBasic', {'rows': 20, 'cols': 78})
+
+ " Enabled: scrolloffpad > 0, expect EOF centering/padding
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_1', {})
+
+ " Beginning-of-file is unchanged (Top)
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! gg\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_2', {})
+
+ " Gating: disable scrolloffpad, then go to EOF again
+ " Expect normal EOF behavior (no extra centering/padding)
+ call term_sendkeys(buf, "\<Esc>:\<C-U>set scrolloffpad=0\<CR>")
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_3', {})
+
+ call StopVimInTerminal(buf)
+ let &termwinsize = save_termwinsize
+endfunc
+
+func Test_scrolloffpad_smoothscroll()
+ CheckScreendump
+ CheckRunVimInTerminal
+
+ let save_termwinsize = &termwinsize
+ set termwinsize=
+
+ let lines =<< trim END
+ set smoothscroll scrolloff=10 scrolloffpad=1
+ enew!
+ call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
+ normal! gg
+ END
+ call writefile(lines, 'XScrolloffpadSmoothscroll', 'D')
+
+ let buf = RunVimInTerminal('-S XScrolloffpadSmoothscroll', #{rows: 20, cols: 78})
+
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_smoothscroll_1', {})
+
+ call term_sendkeys(buf, "\<Esc>:\<C-U>call setline(line('$'), repeat('LONG ', 30))\<CR>")
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! 41|\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_smoothscroll_2', {})
+
+ call StopVimInTerminal(buf)
+ let &termwinsize = save_termwinsize
+endfunc
+
+func Test_scrolloffpad_insert_eof()
+ let save_so = &scrolloff
+ let save_sop = &scrolloffpad
+
+ set scrolloff=10 scrolloffpad=1
+ enew!
+ call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
+ normal! G
+
+ let topline_before = winsaveview().topline
+ call feedkeys("i\<Esc>", 'xt')
+ call assert_equal(topline_before, winsaveview().topline)
+
+ exe "normal! \<C-E>"
+ let topline_after = winsaveview().topline
+ call feedkeys("i\<Esc>", 'xt')
+ call assert_equal(topline_after, winsaveview().topline)
+
+ let &scrolloff = save_so
+ let &scrolloffpad = save_sop
+ bwipe!
+endfunc
+
+func Test_scrolloffpad_in_diff_mode()
+ CheckFeature diff
+
+ let save_so = &scrolloff
+ let save_sop = &scrolloffpad
+ let save_splitright = &splitright
+
+ set nosplitright
+ set scrolloff=10
+ set scrolloffpad=0
+
+ enew
+ call setline(1, map(range(1, 100), {_, v -> 'line ' .. v}))
+ diffthis
+
+ vnew
+ call setline(1, map(range(1, 100), {_, v -> 'line ' .. v}))
+ " Make buffers minimally different to avoid diff folding everything.
+ call setline(50, 'DIFF LINE 50')
+ diffthis
+
+ windo normal! zR
+ windo normal! gg
+ wincmd =
+
+ let rows_without = []
+ let rows_with = []
+ let near_states = []
+ let eof_states = []
+ for sop in [0, 1]
+ let &scrolloffpad = sop
+
+ " Near EOF with real text visible in both windows.
+ windo normal! 99G
+ for w in range(1, winnr('$'))
+ execute w .. 'wincmd w'
+ let state = [line('.'), line('w0'), line('w$'), winline()]
+ call assert_equal(99, state[0])
+ call assert_equal(100, state[2])
+ if sop == 0
+ call add(near_states, state)
+ endif
+ endfor
+ call assert_equal(near_states[0], near_states[1])
+
+ " EOF in both windows: scrolloffpad should raise the cursor row.
+ windo normal! G
+ for w in range(1, winnr('$'))
+ execute w .. 'wincmd w'
+ let state = [line('.'), line('w0'), line('w$'), winline()]
+ call assert_equal(line('$'), state[0])
+ if sop == 0
+ call add(eof_states, state)
+ call add(rows_without, state[3])
+ else
+ call add(rows_with, state[3])
+ endif
+ endfor
+ call assert_equal(eof_states[0], eof_states[1])
+ endfor
+
+ call assert_true(rows_with[0] < rows_without[0])
+ call assert_true(rows_with[1] < rows_without[1])
+
+ windo diffoff
+ %bwipe!
+ let &scrolloff = save_so
+ let &scrolloffpad = save_sop
+ let &splitright = save_splitright
+endfunc
+
+func Test_scrolloffpad_diff_eof_filler_behavior()
+ CheckFeature diff
+
+ let save_so = &scrolloff
+ let save_sop = &scrolloffpad
+ let save_diffopt = &diffopt
+ let save_splitright = &splitright
+
+ set diffopt+=filler
+ set scrolloff=10
+ set scrolloffpad=0
+ set nosplitright
+
+ 20new
+ call setline(1, map(range(1, 100), {_, v -> 'left ' .. v}))
+ diffthis
+ let short_wid = win_getid()
+
+ vnew
+ call setline(1, map(range(1, 120), {_, v -> 'right ' .. v}))
+ diffthis
+ let long_wid = win_getid()
+
+ call assert_true(win_gotoid(short_wid))
+ let short_height = winheight(0)
+ call assert_true(win_gotoid(long_wid))
+ let long_height = winheight(0)
+ call assert_equal(short_height, long_height)
+ call assert_equal(20, short_height)
+
+ let ordered_diff_wids = [long_wid, short_wid]
+ let states = {}
+ for sop in [0, 1]
+ execute 'set scrolloffpad=' .. sop
+ for wid in ordered_diff_wids
+ call assert_true(win_gotoid(wid))
+ normal! gg
+ endfor
+ for wid in ordered_diff_wids
+ call assert_true(win_gotoid(wid))
+ normal! G
+ endfor
+
+ call assert_true(win_gotoid(short_wid))
+ let short_view = winsaveview()
+ let short_state = [
+ \ line('.'),
+ \ line('$'),
+ \ winline(),
+ \ short_view.topline,
+ \ short_view.topfill,
+ \ diff_filler(line('$') + 1),
+ \ ]
+ call assert_equal(short_state[1], short_state[0])
+ call assert_true(short_state[5] > 0)
+
+ call assert_true(win_gotoid(long_wid))
+ let long_view = winsaveview()
+ let long_state = [
+ \ line('.'),
+ \ line('$'),
+ \ winline(),
+ \ long_view.topline,
+ \ long_view.topfill,
+ \ ]
+ call assert_true(long_state[0] > 0 && long_state[0] <= long_state[1])
+ call assert_equal(short_state[0], long_state[0])
+
+ let states[sop] = [short_state, long_state]
+ endfor
+
+ let short_without = states[0][0]
+ let short_with = states[1][0]
+ " Environment/layout can shift direction of movement; require only that
+ " scrolloffpad changes the short-window viewport state under EOF filler.
+ call assert_true(short_with[2] != short_without[2]
+ \ || short_with[3] != short_without[3]
+ \ || short_with[4] != short_without[4])
+
+ windo diffoff
+ call assert_true(win_gotoid(short_wid))
+ only!
+ %bwipe!
+ let &scrolloff = save_so
+ let &scrolloffpad = save_sop
+ let &diffopt = save_diffopt
+ let &splitright = save_splitright
+endfunc
+
+func Test_scrolloffpad_with_folds()
+ CheckScreendump
+ CheckRunVimInTerminal
+ CheckFeature folding
+
+ let save_termwinsize = &termwinsize
+ set termwinsize=
+
+ let lines =<< trim END
+ set scrolloff=10
+ set scrolloffpad=1
+
+ enew
+ call setline(1, map(range(1, 120), {_, v -> 'line ' . v}))
+
+ " Create a large fold near the end of the file.
+ " Fold lines 60-110, leaving 111-120 visible after the fold.
+ set foldmethod=manual
+ set foldenable
+ normal! gg
+ normal! 60G
+ normal! zf50j
+ normal! gg
+ END
+ call writefile(lines, 'XScrolloffpadFolds', 'D')
+
+ let buf = RunVimInTerminal('-S XScrolloffpadFolds', #{rows: 20, cols: 78})
+
+ " Case 1: Jump to end-of-file
+ " With folds present, scrolloffpad should still
+ " keep the cursor positioned with padding below EOF
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_1', {})
+
+ " Case 2: Move to the folded line to ensure the fold is actually in view
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! 60G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_2', {})
+
+ " Case 3: Close the fold explicitly and go to EOF again
+ " Behavior should remain stable with closed folds
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! zc\<CR>")
+ call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
+ call term_sendkeys(buf, "\<C-L>")
+ call TermWait(buf)
+ call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_3', {})
+
+ call StopVimInTerminal(buf)
+ let &termwinsize = save_termwinsize
+endfunc
" Resizing to "textoff" after 'smoothscroll' skips part of a wrapped line must
" not crash.