Commit: patch 9.2.0356: Cannot apply 'scrolloff' context lines at end of file

3 views
Skip to first unread message

Christian Brabandt

unread,
Apr 15, 2026, 3:30:18 PM (2 days ago) Apr 15
to vim...@googlegroups.com
patch 9.2.0356: Cannot apply 'scrolloff' context lines at end of file

Commit: https://github.com/vim/vim/commit/a414630393f81c9a5b8fa4d0fcc1287155f67751
Author: McAuley Penney <jacobm...@gmail.com>
Date: Wed Apr 15 19:11:12 2026 +0000

patch 9.2.0356: Cannot apply 'scrolloff' context lines at end of file

Problem: Cannot apply 'scrolloff' context lines at end of file
Solution: Add the 'scrolloffpad' option to keep 'scrolloff' context even
when at the end of the file (McAuley Penney).

closes: #19040

Signed-off-by: McAuley Penney <jacobm...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 627d96540..31a9629c0 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1,4 +1,4 @@
-*options.txt* For Vim version 9.2. Last change: 2026 Apr 14
+*options.txt* For Vim version 9.2. Last change: 2026 Apr 15


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -7577,8 +7577,8 @@ 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: >
setlocal scrolloff<
@@ -7586,7 +7586,24 @@ A jump table for the options with a short description can be found at |Q_op|.
< For scrolling horizontally see 'sidescrolloff'.
NOTE: This option is set to 0 when 'compatible' is set.

- *'scrollopt'* *'sbo'*
+ *'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: >
+ setlocal scrolloffpad<
+ setlocal scrolloffpad=-1
+
+< *'scrollopt'* *'sbo'*
'scrollopt' 'sbo' string (default "ver,jump")
global
This is a comma-separated list of words that specifies how
diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt
index 8f6aca046..1ada2bf9b 100644
--- a/runtime/doc/quickref.txt
+++ b/runtime/doc/quickref.txt
@@ -1,4 +1,4 @@
-*quickref.txt* For Vim version 9.2. Last change: 2026 Apr 09
+*quickref.txt* For Vim version 9.2. Last change: 2026 Apr 15


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -895,6 +895,7 @@ Short explanation of each option: *option-list*
'scrollfocus' 'scf' scroll wheel applies to window under pointer
'scrolljump' 'sj' minimum number of lines to scroll
'scrolloff' 'so' minimum nr. of lines above and below cursor
+'scrolloffpad' 'sop' keep 'scrolloff' context 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/doc/tags b/runtime/doc/tags
index 4d31cc51a..7effaf824 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -953,6 +953,7 @@ $quote eval.txt /*$quote*
'scrollfocus' options.txt /*'scrollfocus'*
'scrolljump' options.txt /*'scrolljump'*
'scrolloff' options.txt /*'scrolloff'*
+'scrolloffpad' options.txt /*'scrolloffpad'*
'scrollopt' options.txt /*'scrollopt'*
'scs' options.txt /*'scs'*
'sect' options.txt /*'sect'*
@@ -1011,6 +1012,7 @@ $quote eval.txt /*$quote*
'so' options.txt /*'so'*
'softtabstop' options.txt /*'softtabstop'*
'sol' options.txt /*'sol'*
+'sop' options.txt /*'sop'*
'sourceany' vi_diff.txt /*'sourceany'*
'sp' options.txt /*'sp'*
'spc' options.txt /*'spc'*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index fa8c5355c..ee24e2901 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2. Last change: 2026 Apr 14
+*version9.txt* For Vim version 9.2. Last change: 2026 Apr 15


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52654,6 +52654,7 @@ Options: ~

'modelinestrict' Only allow safe options to be set from a modeline.
'pumopt' Additional options for the popup menu
+'scrolloffpad' Keep 'scrolloff' context at end of file
'statuslineopt' Extra window-local options for the 'statusline', to
configure the height.
't_BS' Begin synchronized update.
diff --git a/runtime/optwin.vim b/runtime/optwin.vim
index 73243e77f..62c2ec2e5 100644
--- a/runtime/optwin.vim
+++ b/runtime/optwin.vim
@@ -1,7 +1,7 @@
" These commands create the option window.
"
" Maintainer: The Vim Project <https://github.com/vim/vim>
-" Last Change: 2026 Apr 09
+" Last Change: 2026 Apr 15
" Former Maintainer: Bram Moolenaar <Br...@vim.org>

" If there already is an option window, jump to that one.
@@ -351,6 +351,8 @@ call append("$", " " .. s:local_to_window)
call <SID>BinOptionL("sms")
call <SID>AddOption("scrolloff", gettext("number of screen lines to show around the cursor"))
call append("$", " set so=" . &so)
+call <SID>AddOption("scrolloffpad", gettext("keep 'scrolloff' context even at end of file"))
+call append("$", " set sop=" . &sop)
call <SID>AddOption("wrap", gettext("long lines wrap"))
call append("$", " " .. s:local_to_window)
call <SID>BinOptionL("wrap")
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
index f213710c3..a390df78e 100644
--- a/runtime/syntax/vim.vim
+++ b/runtime/syntax/vim.vim
@@ -2,7 +2,7 @@
" Language: Vim script
" Maintainer: Hirohito Higashi <h.east.727 ATMARK gmail.com>
" Doug Kearns <dougk...@gmail.com>
-" Last Change: 2026 Apr 14
+" Last Change: 2026 Apr 15
" Former Maintainer: Charles E. Campbell

" DO NOT CHANGE DIRECTLY.
@@ -69,8 +69,8 @@ syn keyword vimOption contained co columns com comments cms commentstring cp com
syn keyword vimOption contained efm errorformat ek esckeys ei eventignore eiw eventignorewin et expandtab ex exrc fenc fileencoding fencs fileencodings ff fileformat ffs fileformats fic fileignorecase ft filetype fcs fillchars ffu findfunc fixeol fixendofline fcl foldclose fdc foldcolumn fen foldenable fde foldexpr fdi foldignore fdl foldlevel fdls foldlevelstart fmr foldmarker fdm foldmethod fml foldminlines fdn foldnestmax fdo foldopen fdt foldtext fex formatexpr flp formatlistpat fo formatoptions fp formatprg fs fsync gd gdefault gfm grepformat gp grepprg gcr guicursor gfn guifont gfs guifontset gfw guifontwide ghr guiheadroom gli guiligatures go guioptions guipty gtl guitablabel gtt guitabtooltip hf helpfile hh helpheight hlg helplang hid hidden hl highlight skipwhite nextgroup=vimSetEqual,vimSetMod
syn keyword vimOption contained hi history hk hkmap hkp hkmapp hls hlsearch icon iconstring ic ignorecase imaf imactivatefunc imak imactivatekey imc imcmdline imd imdisable imi iminsert ims imsearch imsf imstatusfunc imst imstyle inc include inex includeexpr is incsearch inde indentexpr indk indentkeys inf infercase im insertmode isf isfname isi isident isk iskeyword isp isprint js joinspaces jop jumpoptions key kmp keymap km keymodel kpc keyprotocol kp keywordprg lmap langmap lm langmenu lnr langnoremap lrm langremap ls laststatus lz lazyredraw lhi lhistory lbr linebreak lines lsp linespace lisp lop lispoptions lw lispwords list lcs listchars lpl loadplugins luadll magic mef makeef menc makeencoding mp makeprg mps matchpairs mat matchtime mco maxcombine mfd maxfuncdepth skipwhite nextgroup=vimSetEqual,vimSetMod
syn keyword vimOption contained mmd maxmapdepth mm maxmem mmp maxmempattern mmt maxmemtot msc maxsearchcount mis menuitems mopt messagesopt msm mkspellmem ml modeline mle modelineexpr mls modelines mlst modelinestrict ma modifiable mod modified more mouse mousef mousefocus mh mousehide mousem mousemodel mousemev mousemoveevent mouses mouseshape mouset mousetime mzq mzquantum mzschemedll mzschemegcdll nf nrformats nu number nuw numberwidth ofu omnifunc odev opendevice opfunc operatorfunc ost osctimeoutlen pp packpath para paragraphs paste pt pastetoggle pex patchexpr pm patchmode pa path perldll pi preserveindent pvh previewheight pvp previewpopup pvw previewwindow pdev printdevice penc printencoding pexpr printexpr pfn printfont pheader printheader pmbcs printmbcharset skipwhite nextgroup=vimSetEqual,vimSetMod
-syn keyword vimOption contained pmbfn printmbfont popt printoptions prompt pb pumborder ph pumheight pmw pummaxwidth pumopt pw pumwidth pythondll pythonhome pythonthreedll pythonthreehome pyx pyxversion qftf quickfixtextfunc qe quoteescape ro readonly rdt redrawtime re regexpengine rnu relativenumber remap rop renderoptions report rs restorescreen ri revins rl rightleft rlc rightleftcmd rubydll ru ruler ruf rulerformat rtp runtimepath scr scroll scb scrollbind scf scrollfocus sj scrolljump so scrolloff sbo scrollopt sect sections secure sel selection slm selectmode ssop sessionoptions sh shell shcf shellcmdflag sp shellpipe shq shellquote srr shellredir ssl shellslash stmp shelltemp st shelltype sxe shellxescape sxq shellxquote sr shiftround sw shiftwidth shm shortmess skipwhite nextgroup=vimSetEqual,vimSetMod
-syn keyword vimOption contained sn shortname sbr showbreak sc showcmd sloc showcmdloc sft showfulltag sm showmatch smd showmode stal showtabline stpl showtabpanel ss sidescroll siso sidescrolloff scl signcolumn scs smartcase si smartindent sta smarttab sms smoothscroll sts softtabstop spell spc spellcapcheck spf spellfile spl spelllang spo spelloptions sps spellsuggest sb splitbelow spk splitkeep spr splitright sol startofline stl statusline stlo statuslineopt su suffixes sua suffixesadd swf swapfile sws swapsync swb switchbuf smc synmaxcol syn syntax tcl tabclose tal tabline tpm tabpagemax tpl tabpanel tplo tabpanelopt ts tabstop tbs tagbsearch tc tagcase tfu tagfunc tl taglength tr tagrelative tag tags tgst tagstack tcldll term tbidi termbidi tenc termencoding skipwhite nextgroup=vimSetEqual,vimSetMod
+syn keyword vimOption contained pmbfn printmbfont popt printoptions prompt pb pumborder ph pumheight pmw pummaxwidth pumopt pw pumwidth pythondll pythonhome pythonthreedll pythonthreehome pyx pyxversion qftf quickfixtextfunc qe quoteescape ro readonly rdt redrawtime re regexpengine rnu relativenumber remap rop renderoptions report rs restorescreen ri revins rl rightleft rlc rightleftcmd rubydll ru ruler ruf rulerformat rtp runtimepath scr scroll scb scrollbind scf scrollfocus sj scrolljump so scrolloff sop scrolloffpad sbo scrollopt sect sections secure sel selection slm selectmode ssop sessionoptions sh shell shcf shellcmdflag sp shellpipe shq shellquote srr shellredir ssl shellslash stmp shelltemp st shelltype sxe shellxescape sxq shellxquote sr shiftround sw shiftwidth skipwhite nextgroup=vimSetEqual,vimSetMod
+syn keyword vimOption contained shm shortmess sn shortname sbr showbreak sc showcmd sloc showcmdloc sft showfulltag sm showmatch smd showmode stal showtabline stpl showtabpanel ss sidescroll siso sidescrolloff scl signcolumn scs smartcase si smartindent sta smarttab sms smoothscroll sts softtabstop spell spc spellcapcheck spf spellfile spl spelllang spo spelloptions sps spellsuggest sb splitbelow spk splitkeep spr splitright sol startofline stl statusline stlo statuslineopt su suffixes sua suffixesadd swf swapfile sws swapsync swb switchbuf smc synmaxcol syn syntax tcl tabclose tal tabline tpm tabpagemax tpl tabpanel tplo tabpanelopt ts tabstop tbs tagbsearch tc tagcase tfu tagfunc tl taglength tr tagrelative tag tags tgst tagstack tcldll term tbidi termbidi tenc termencoding skipwhite nextgroup=vimSetEqual,vimSetMod
syn keyword vimOption contained tgc termguicolors trz termresize tsy termsync twk termwinkey twsl termwinscroll tws termwinsize twt termwintype terse ta textauto tx textmode tw textwidth tsr thesaurus tsrfu thesaurusfunc top tildeop to timeout tm timeoutlen title titlelen titleold titlestring tb toolbar tbis toolbariconsize ttimeout ttm ttimeoutlen tbi ttybuiltin tf ttyfast ttym ttymouse tsl ttyscroll tty ttytype udir undodir udf undofile ul undolevels ur undoreload uc updatecount ut updatetime vsts varsofttabstop vts vartabstop vbs verbose vfile verbosefile vdir viewdir vop viewoptions vi viminfo vif viminfofile ve virtualedit vb visualbell warn wiv weirdinvert ww whichwrap wc wildchar wcm wildcharm wig wildignore wic wildignorecase wmnu wildmenu wim wildmode skipwhite nextgroup=vimSetEqual,vimSetMod
syn keyword vimOption contained wop wildoptions wak winaltkeys wcr wincolor wi window wfb winfixbuf wfh winfixheight wfw winfixwidth wh winheight whl winhighlight wmh winminheight wmw winminwidth winptydll wiw winwidth wse wlseat wst wlsteal wtm wltimeoutlen wrap wm wrapmargin ws wrapscan write wa writeany wb writebackup wd writedelay xtermcodes skipwhite nextgroup=vimSetEqual,vimSetMod

@@ -108,9 +108,9 @@ syn keyword vimOptionVarName contained co columns com comments cms commentstring
syn keyword vimOptionVarName contained efm errorformat ek esckeys ei eventignore eiw eventignorewin et expandtab ex exrc fenc fileencoding fencs fileencodings ff fileformat ffs fileformats fic fileignorecase ft filetype fcs fillchars ffu findfunc fixeol fixendofline fcl foldclose fdc foldcolumn fen foldenable fde foldexpr fdi foldignore fdl foldlevel fdls foldlevelstart fmr foldmarker fdm foldmethod fml foldminlines fdn foldnestmax fdo foldopen fdt foldtext fex formatexpr flp formatlistpat fo formatoptions fp formatprg fs fsync gd gdefault gfm grepformat gp grepprg gcr guicursor gfn guifont gfs guifontset gfw guifontwide ghr guiheadroom gli guiligatures go guioptions guipty gtl guitablabel gtt guitabtooltip hf helpfile hh helpheight hlg helplang hid hidden hl highlight
syn keyword vimOptionVarName contained hi history hk hkmap hkp hkmapp hls hlsearch icon iconstring ic ignorecase imaf imactivatefunc imak imactivatekey imc imcmdline imd imdisable imi iminsert ims imsearch imsf imstatusfunc imst imstyle inc include inex includeexpr is incsearch inde indentexpr indk indentkeys inf infercase im insertmode isf isfname isi isident isk iskeyword isp isprint js joinspaces jop jumpoptions key kmp keymap km keymodel kpc keyprotocol kp keywordprg lmap langmap lm langmenu lnr langnoremap lrm langremap ls laststatus lz lazyredraw lhi lhistory lbr linebreak lines lsp linespace lisp lop lispoptions lw lispwords list lcs listchars lpl loadplugins luadll magic mef makeef menc makeencoding mp makeprg mps matchpairs mat matchtime mco maxcombine
syn keyword vimOptionVarName contained mfd maxfuncdepth mmd maxmapdepth mm maxmem mmp maxmempattern mmt maxmemtot msc maxsearchcount mis menuitems mopt messagesopt msm mkspellmem ml modeline mle modelineexpr mls modelines mlst modelinestrict ma modifiable mod modified more mouse mousef mousefocus mh mousehide mousem mousemodel mousemev mousemoveevent mouses mouseshape mouset mousetime mzq mzquantum mzschemedll mzschemegcdll nf nrformats nu number nuw numberwidth ofu omnifunc odev opendevice opfunc operatorfunc ost osctimeoutlen pp packpath para paragraphs paste pt pastetoggle pex patchexpr pm patchmode pa path perldll pi preserveindent pvh previewheight pvp previewpopup pvw previewwindow pdev printdevice penc printencoding pexpr printexpr pfn printfont pheader printheader
-syn keyword vimOptionVarName contained pmbcs printmbcharset pmbfn printmbfont popt printoptions prompt pb pumborder ph pumheight pmw pummaxwidth pumopt pw pumwidth pythondll pythonhome pythonthreedll pythonthreehome pyx pyxversion qftf quickfixtextfunc qe quoteescape ro readonly rdt redrawtime re regexpengine rnu relativenumber remap rop renderoptions report rs restorescreen ri revins rl rightleft rlc rightleftcmd rubydll ru ruler ruf rulerformat rtp runtimepath scr scroll scb scrollbind scf scrollfocus sj scrolljump so scrolloff sbo scrollopt sect sections secure sel selection slm selectmode ssop sessionoptions sh shell shcf shellcmdflag sp shellpipe shq shellquote srr shellredir ssl shellslash stmp shelltemp st shelltype sxe shellxescape sxq shellxquote sr shiftround
-syn keyword vimOptionVarName contained sw shiftwidth shm shortmess sn shortname sbr showbreak sc showcmd sloc showcmdloc sft showfulltag sm showmatch smd showmode stal showtabline stpl showtabpanel ss sidescroll siso sidescrolloff scl signcolumn scs smartcase si smartindent sta smarttab sms smoothscroll sts softtabstop spell spc spellcapcheck spf spellfile spl spelllang spo spelloptions sps spellsuggest sb splitbelow spk splitkeep spr splitright sol startofline stl statusline stlo statuslineopt su suffixes sua suffixesadd swf swapfile sws swapsync swb switchbuf smc synmaxcol syn syntax tcl tabclose tal tabline tpm tabpagemax tpl tabpanel tplo tabpanelopt ts tabstop tbs tagbsearch tc tagcase tfu tagfunc tl taglength tr tagrelative tag tags tgst tagstack tcldll
-syn keyword vimOptionVarName contained term tbidi termbidi tenc termencoding tgc termguicolors trz termresize tsy termsync twk termwinkey twsl termwinscroll tws termwinsize twt termwintype terse ta textauto tx textmode tw textwidth tsr thesaurus tsrfu thesaurusfunc top tildeop to timeout tm timeoutlen title titlelen titleold titlestring tb toolbar tbis toolbariconsize ttimeout ttm ttimeoutlen tbi ttybuiltin tf ttyfast ttym ttymouse tsl ttyscroll tty ttytype udir undodir udf undofile ul undolevels ur undoreload uc updatecount ut updatetime vsts varsofttabstop vts vartabstop vbs verbose vfile verbosefile vdir viewdir vop viewoptions vi viminfo vif viminfofile ve virtualedit vb visualbell warn wiv weirdinvert ww whichwrap wc wildchar wcm wildcharm wig wildignore
+syn keyword vimOptionVarName contained pmbcs printmbcharset pmbfn printmbfont popt printoptions prompt pb pumborder ph pumheight pmw pummaxwidth pumopt pw pumwidth pythondll pythonhome pythonthreedll pythonthreehome pyx pyxversion qftf quickfixtextfunc qe quoteescape ro readonly rdt redrawtime re regexpengine rnu relativenumber remap rop renderoptions report rs restorescreen ri revins rl rightleft rlc rightleftcmd rubydll ru ruler ruf rulerformat rtp runtimepath scr scroll scb scrollbind scf scrollfocus sj scrolljump so scrolloff sop scrolloffpad sbo scrollopt sect sections secure sel selection slm selectmode ssop sessionoptions sh shell shcf shellcmdflag sp shellpipe shq shellquote srr shellredir ssl shellslash stmp shelltemp st shelltype sxe shellxescape sxq shellxquote
+syn keyword vimOptionVarName contained sr shiftround sw shiftwidth shm shortmess sn shortname sbr showbreak sc showcmd sloc showcmdloc sft showfulltag sm showmatch smd showmode stal showtabline stpl showtabpanel ss sidescroll siso sidescrolloff scl signcolumn scs smartcase si smartindent sta smarttab sms smoothscroll sts softtabstop spell spc spellcapcheck spf spellfile spl spelllang spo spelloptions sps spellsuggest sb splitbelow spk splitkeep spr splitright sol startofline stl statusline stlo statuslineopt su suffixes sua suffixesadd swf swapfile sws swapsync swb switchbuf smc synmaxcol syn syntax tcl tabclose tal tabline tpm tabpagemax tpl tabpanel tplo tabpanelopt ts tabstop tbs tagbsearch tc tagcase tfu tagfunc tl taglength tr tagrelative tag tags tgst tagstack
+syn keyword vimOptionVarName contained tcldll term tbidi termbidi tenc termencoding tgc termguicolors trz termresize tsy termsync twk termwinkey twsl termwinscroll tws termwinsize twt termwintype terse ta textauto tx textmode tw textwidth tsr thesaurus tsrfu thesaurusfunc top tildeop to timeout tm timeoutlen title titlelen titleold titlestring tb toolbar tbis toolbariconsize ttimeout ttm ttimeoutlen tbi ttybuiltin tf ttyfast ttym ttymouse tsl ttyscroll tty ttytype udir undodir udf undofile ul undolevels ur undoreload uc updatecount ut updatetime vsts varsofttabstop vts vartabstop vbs verbose vfile verbosefile vdir viewdir vop viewoptions vi viminfo vif viminfofile ve virtualedit vb visualbell warn wiv weirdinvert ww whichwrap wc wildchar wcm wildcharm wig wildignore
syn keyword vimOptionVarName contained wic wildignorecase wmnu wildmenu wim wildmode wop wildoptions wak winaltkeys wcr wincolor wi window wfb winfixbuf wfh winfixheight wfw winfixwidth wh winheight whl winhighlight wmh winminheight wmw winminwidth winptydll wiw winwidth wse wlseat wst wlsteal wtm wltimeoutlen wrap wm wrapmargin ws wrapscan write wa writeany wb writebackup wd writedelay xtermcodes
" GEN_SYN_VIM: vimOption term output code variable, START_STR='syn keyword vimOptionVarName contained', END_STR=''
syn keyword vimOptionVarName contained t_AB t_AF t_AU t_AL t_al t_bc t_BE t_BD t_cd t_ce t_Ce t_CF t_cl t_cm t_Co t_CS t_Cs t_cs t_CV t_da t_db t_DL t_dl t_ds t_Ds t_EC t_EI t_fs t_fd t_fe t_GP t_IE t_IS t_ke t_ks t_le t_mb t_md t_me t_mr t_ms t_nd t_op t_RF t_RB t_RC t_RI t_Ri t_RK t_RS t_RT t_RV t_Sb t_SC t_se t_Sf t_SH t_SI t_Si t_so t_SR t_sr t_ST t_Te t_te t_TE t_ti t_TI t_Ts t_ts t_u7 t_ue t_us t_Us t_ut t_vb t_ve t_vi t_VS t_vs t_WP t_WS t_XM t_xn t_xs t_ZH t_ZR t_8f t_8b t_8u t_xo t_BS t_ES
diff --git a/src/move.c b/src/move.c
index ee9a8eb5a..1e3510130 100644
--- a/src/move.c
+++ b/src/move.c
@@ -279,6 +279,31 @@ update_topline_redraw(void)
update_screen(0);
}

+/*
+ * 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(void)
+{
+ return get_scrolloff_value() > 0 && get_scrolloffpad_value() > 0;
+}
+
+/*
+ * Return TRUE when there are not enough real buffer lines below "lnum" to
+ * satisfy the requested "so" context.
+ */
+ static bool
+scrolloffpad_eof_pressure(linenr_T lnum, long so)
+{
+ if (!use_scrolloffpad() || so <= 0)
+ return false;
+
+ // Use subtraction to avoid signed overflow in "lnum + so".
+ return lnum > curbuf->b_ml.ml_line_count - so;
+}
+
/*
* Update curwin->w_topline to move the cursor onto the screen.
*/
@@ -295,6 +320,7 @@ update_topline(void)
int check_botline = FALSE;
long *so_ptr = curwin->w_p_so >= 0 ? &curwin->w_p_so : &p_so;
int save_so = *so_ptr;
+ bool eof_pressure;

// Cursor is updated instead when this is TRUE for 'splitkeep'.
if (skip_update_topline)
@@ -318,6 +344,7 @@ update_topline(void)
// When dragging with the mouse, don't scroll that quickly
if (mouse_dragging > 0)
*so_ptr = mouse_dragging - 1;
+ eof_pressure = scrolloffpad_eof_pressure(curwin->w_cursor.lnum, *so_ptr);

linenr_T old_topline = curwin->w_topline;
#ifdef FEAT_DIFF
@@ -405,11 +432,21 @@ update_topline(void)
// cursor in the middle of the window. Otherwise put the cursor
// near the top of the window.
if (n >= halfheight)
- scroll_cursor_halfway(FALSE, FALSE);
+ {
+ if (eof_pressure)
+ scroll_cursor_halfway(TRUE, TRUE);
+ else
+ scroll_cursor_halfway(FALSE, FALSE);
+ }
else
{
- scroll_cursor_top(scrolljump_value(), FALSE);
- check_botline = TRUE;
+ if (eof_pressure)
+ scroll_cursor_halfway(TRUE, TRUE);
+ else
+ {
+ scroll_cursor_top(scrolljump_value(), FALSE);
+ check_botline = TRUE;
+ }
}
}

@@ -436,7 +473,7 @@ update_topline(void)
if (!(curwin->w_valid & VALID_BOTLINE_AP))
validate_botline();

- if (curwin->w_botline <= curbuf->b_ml.ml_line_count)
+ if (curwin->w_botline <= curbuf->b_ml.ml_line_count || use_scrolloffpad())
{
if (curwin->w_cursor.lnum < curwin->w_botline)
{
@@ -452,7 +489,7 @@ update_topline(void)
// Cursor is (a few lines) above botline, check if there are
// 'scrolloff' window lines below the cursor. If not, need to
// scroll.
- n = curwin->w_empty_rows;
+ n = eof_pressure ? 0 : curwin->w_empty_rows;
loff.lnum = curwin->w_cursor.lnum;
#ifdef FEAT_FOLDING
// In a fold go to its last line.
@@ -473,14 +510,14 @@ update_topline(void)
if (n >= *so_ptr)
break;
botline_forw(&loff);
+ }
+ if (n >= *so_ptr && !eof_pressure)
+ // sufficient context, no need to scroll
+ check_botline = FALSE;
}
- if (n >= *so_ptr)
+ else
// sufficient context, no need to scroll
check_botline = FALSE;
- }
- else
- // sufficient context, no need to scroll
- check_botline = FALSE;
}
if (check_botline)
{
@@ -506,9 +543,14 @@ update_topline(void)
line_count = curwin->w_cursor.lnum - curwin->w_botline
+ 1 + *so_ptr;
if (line_count <= curwin->w_height + 1)
- scroll_cursor_bot(scrolljump_value(), FALSE);
+ {
+ if (eof_pressure)
+ scroll_cursor_halfway(TRUE, TRUE);
+ else
+ scroll_cursor_bot(scrolljump_value(), FALSE);
+ }
else
- scroll_cursor_halfway(FALSE, FALSE);
+ scroll_cursor_halfway(eof_pressure, eof_pressure);
}
}
}
@@ -2350,9 +2392,11 @@ botline_forw(lineoff_T *lp)
else
#ifdef FEAT_FOLDING
if (hasFolding(lp->lnum, NULL, &lp->lnum))
- // Add a closed fold
- lp->height = 1;
- else
+ {
+ // Add a closed fold.
+ lp->height = 1;
+ }
+ else
#endif
lp->height = PLINES_NOFILL(lp->lnum);
}
@@ -2804,8 +2848,9 @@ scroll_cursor_bot(int min_scroll, int 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(cln, so);
if (line_count >= curwin->w_height && line_count > min_scroll)
- scroll_cursor_halfway(FALSE, TRUE);
+ scroll_cursor_halfway(eof_pressure, TRUE);
else if (line_count > 0)
{
if (do_sms)
@@ -3046,7 +3091,8 @@ cursor_correct(void)
if (curwin->w_botline == curbuf->b_ml.ml_line_count + 1
&& mouse_dragging == 0)
{
- below_wanted = 0;
+ if (!use_scrolloffpad())
+ below_wanted = 0;
max_off = (curwin->w_height - 1) / 2;
if (above_wanted > max_off)
above_wanted = max_off;
diff --git a/src/option.c b/src/option.c
index 46009341d..dc6f0966d 100644
--- a/src/option.c
+++ b/src/option.c
@@ -804,8 +804,9 @@ set_option_default(
long def_val = (long)(long_i)options[opt_idx].def_val[dvi];

if ((long *)varp == &curwin->w_p_so
- || (long *)varp == &curwin->w_p_siso)
- // 'scrolloff' and 'sidescrolloff' local values have a
+ || (long *)varp == &curwin->w_p_siso
+ || (long *)varp == &curwin->w_p_sop)
+ // 'scrolloff', 'sidescrolloff', and 'scrolloffpad' local values have a
// different default value than the global default.
*(long *)varp = -1;
else
@@ -2581,8 +2582,9 @@ do_set_option_numeric(
value = NO_LOCAL_UNDOLEVEL;
else if (opt_flags == OPT_LOCAL
&& ((long *)varp == &curwin->w_p_siso
- || (long *)varp == &curwin->w_p_so))
- // for 'scrolloff'/'sidescrolloff' -1 means using the global value
+ || (long *)varp == &curwin->w_p_so
+ || (long *)varp == &curwin->w_p_sop))
+ // for 'scrolloff'/'sidescrolloff'/'scrolloffpad' -1 means using the global value
value = -1;
else
value = *(long *)get_varp_scope(&(options[opt_idx]), OPT_GLOBAL);
@@ -2624,6 +2626,12 @@ do_set_option_numeric(
else if (op == OP_REMOVING)
value = *(long *)varp - value;

+ if ((long *)varp == &curwin->w_p_sop && value < -1)
+ {
+ errmsg = e_invalid_argument;
+ goto skip;
+ }
+
errmsg = set_num_option(opt_idx, varp, value, errbuf, errbuflen,
opt_flags);

@@ -5401,6 +5409,11 @@ check_num_option_bounds(
errmsg = e_argument_must_be_positive;
p_so = 0;
}
+ if (p_sop < 0 && full_screen)
+ {
+ errmsg = e_invalid_argument;
+ p_sop = 0;
+ }
if (p_siso < 0 && full_screen)
{
errmsg = e_argument_must_be_positive;
@@ -6818,6 +6831,9 @@ unset_global_local_option(char_u *name, void *from)
case PV_SO:
curwin->w_p_so = -1;
break;
+ case PV_SOP:
+ curwin->w_p_sop = -1;
+ break;
# ifdef FEAT_FIND_ID
case PV_DEF:
clear_string_option(&buf->b_p_def);
@@ -6959,6 +6975,7 @@ get_varp_scope(struct vimoption *p, int scope)
case PV_TC: return (char_u *)&(curbuf->b_p_tc);
case PV_SISO: return (char_u *)&(curwin->w_p_siso);
case PV_SO: return (char_u *)&(curwin->w_p_so);
+ case PV_SOP: return (char_u *)&(curwin->w_p_sop);
#ifdef FEAT_FIND_ID
case PV_DEF: return (char_u *)&(curbuf->b_p_def);
case PV_INC: return (char_u *)&(curbuf->b_p_inc);
@@ -7044,6 +7061,8 @@ get_varp(struct vimoption *p)
? (char_u *)&(curwin->w_p_siso) : p->var;
case PV_SO: return curwin->w_p_so >= 0
? (char_u *)&(curwin->w_p_so) : p->var;
+ case PV_SOP: return curwin->w_p_sop != -1
+ ? (char_u *)&(curwin->w_p_sop) : p->var;
#ifdef FEAT_FIND_ID
case PV_DEF: return *curbuf->b_p_def != NUL
? (char_u *)&(curbuf->b_p_def) : p->var;
@@ -7445,6 +7464,7 @@ 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;
#ifdef FEAT_SPELL
to->wo_spell = from->wo_spell;
#endif
@@ -9072,6 +9092,16 @@ get_scrolloff_value(void)
return curwin->w_p_so < 0 ? p_so : curwin->w_p_so;
}

+/*
+ * Return the effective 'scrolloffpad' value for the current window, using the
+ * global value when appropriate.
+ */
+ long
+get_scrolloffpad_value(void)
+{
+ return curwin->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.
diff --git a/src/option.h b/src/option.h
index 6bf6169bb..e5f9de8e1 100644
--- a/src/option.h
+++ b/src/option.h
@@ -901,6 +901,7 @@ EXTERN long p_sj; // 'scrolljump'
EXTERN int p_scf; // 'scrollfocus'
#endif
EXTERN long p_so; // 'scrolloff'
+EXTERN long p_sop; // 'scrolloffpad'
EXTERN char_u *p_sbo; // 'scrollopt'
EXTERN char_u *p_sections; // 'sections'
EXTERN int p_secure; // 'secure'
@@ -1378,6 +1379,7 @@ enum
, WV_SMS
, WV_SISO
, WV_SO
+ , WV_SOP
#ifdef FEAT_SPELL
, WV_SPELL
#endif
diff --git a/src/optiondefs.h b/src/optiondefs.h
index 38962790b..0214a34b1 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -209,6 +209,7 @@
#define PV_SMS OPT_WIN(WV_SMS)
#define PV_SISO OPT_BOTH(OPT_WIN(WV_SISO))
#define PV_SO OPT_BOTH(OPT_WIN(WV_SO))
+#define PV_SOP OPT_BOTH(OPT_WIN(WV_SOP))
#ifdef FEAT_SPELL
# define PV_SPELL OPT_WIN(WV_SPELL)
#endif
@@ -2266,6 +2267,9 @@ static struct vimoption options[] =
{"scrolloff", "so", P_NUM|P_VI_DEF|P_VIM|P_RALL,
(char_u *)&p_so, PV_SO, NULL, NULL,
{(char_u *)0L, (char_u *)0L} SCTX_INIT},
+ {"scrolloffpad", "sop", P_NUM|P_VI_DEF|P_VIM|P_RALL,
+ (char_u *)&p_sop, PV_SOP, NULL, NULL,
+ {(char_u *)0L, (char_u *)0L} SCTX_INIT},
{"scrollopt", "sbo", P_STRING|P_VI_DEF|P_ONECOMMA|P_NODUP,
(char_u *)&p_sbo, PV_NONE, did_set_scrollopt, expand_set_scrollopt,
{(char_u *)"ver,jump", (char_u *)0L}
diff --git a/src/po/vim.pot b/src/po/vim.pot
index b09ade556..936961c92 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Vim
"
"Report-Msgid-Bugs-To: vim...@vim.org
"
-"POT-Creation-Date: 2026-04-09 20:33+0000
"
+"POT-Creation-Date: 2026-04-15 19:10+0000
"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
"Language-Team: LANGUAGE <L...@li.org>
"
@@ -9383,6 +9383,9 @@ msgstr ""
msgid "number of screen lines to show around the cursor"
msgstr ""

+msgid "keep 'scrolloff' context even at end of file"
+msgstr ""
+
msgid "long lines wrap"
msgstr ""

diff --git a/src/popupwin.c b/src/popupwin.c
index bdf53cdc4..319d882c5 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -2399,6 +2399,7 @@ popup_create(typval_T *argvars, typval_T *rettv, create_type_T type)
}
wp->w_p_wrap = TRUE; // 'wrap' is default on
wp->w_p_so = 0; // 'scrolloff' zero
+ wp->w_p_sop = 0; // 'scrolloffpad' zero

if (tp != NULL)
{
diff --git a/src/proto/option.pro b/src/proto/option.pro
index 892a6ce3b..ae586ea97 100644
--- a/src/proto/option.pro
+++ b/src/proto/option.pro
@@ -145,6 +145,7 @@ int option_was_set(char_u *name);
int reset_option_was_set(char_u *name);
int can_bs(int what);
long get_scrolloff_value(void);
+long get_scrolloffpad_value(void);
long get_sidescrolloff_value(void);
unsigned int get_bkc_flags(buf_T *buf);
char_u *get_flp_value(buf_T *buf);
diff --git a/src/structs.h b/src/structs.h
index 955b9f100..648ceec90 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -349,6 +349,8 @@ typedef struct
#define w_p_siso w_onebuf_opt.wo_siso // 'sidescrolloff' local value
long wo_so;
#define w_p_so w_onebuf_opt.wo_so // 'scrolloff' local value
+ long wo_sop;
+#define w_p_sop w_onebuf_opt.wo_sop // 'scrolloffpad' local value
#ifdef FEAT_TERMINAL
char_u *wo_twk;
# define w_p_twk w_onebuf_opt.wo_twk // 'termwinkey'
diff --git a/src/testdir/dumps/Test_scrolloffpad_basic_1.dump b/src/testdir/dumps/Test_scrolloffpad_basic_1.dump
new file mode 100644
index 000000000..af38b016a
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_basic_1.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |9|1| @70
+|l|i|n|e| |9|2| @70
+|l|i|n|e| |9|3| @70
+|l|i|n|e| |9|4| @70
+|l|i|n|e| |9|5| @70
+|l|i|n|e| |9|6| @70
+|l|i|n|e| |9|7| @70
+|l|i|n|e| |9|8| @70
+|l|i|n|e| |9@1| @70
+>l|i|n|e| |1|0@1| @69
+|~+0#4040ff13&| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+| +0#0000000&@59|1|0@1|,|1| @8|B|o|t|
diff --git a/src/testdir/dumps/Test_scrolloffpad_basic_2.dump b/src/testdir/dumps/Test_scrolloffpad_basic_2.dump
new file mode 100644
index 000000000..070201946
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_basic_2.dump
@@ -0,0 +1,20 @@
+>l+0&#ffffff0|i|n|e| |1| @71
+|l|i|n|e| |2| @71
+|l|i|n|e| |3| @71
+|l|i|n|e| |4| @71
+|l|i|n|e| |5| @71
+|l|i|n|e| |6| @71
+|l|i|n|e| |7| @71
+|l|i|n|e| |8| @71
+|l|i|n|e| |9| @71
+|l|i|n|e| |1|0| @70
+|l|i|n|e| |1@1| @70
+|l|i|n|e| |1|2| @70
+|l|i|n|e| |1|3| @70
+|l|i|n|e| |1|4| @70
+|l|i|n|e| |1|5| @70
+|l|i|n|e| |1|6| @70
+|l|i|n|e| |1|7| @70
+|l|i|n|e| |1|8| @70
+|l|i|n|e| |1|9| @70
+@60|1|,|1| @10|T|o|p|
diff --git a/src/testdir/dumps/Test_scrolloffpad_basic_3.dump b/src/testdir/dumps/Test_scrolloffpad_basic_3.dump
new file mode 100644
index 000000000..72eed1e33
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_basic_3.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |8|2| @70
+|l|i|n|e| |8|3| @70
+|l|i|n|e| |8|4| @70
+|l|i|n|e| |8|5| @70
+|l|i|n|e| |8|6| @70
+|l|i|n|e| |8|7| @70
+|l|i|n|e| |8@1| @70
+|l|i|n|e| |8|9| @70
+|l|i|n|e| |9|0| @70
+|l|i|n|e| |9|1| @70
+|l|i|n|e| |9|2| @70
+|l|i|n|e| |9|3| @70
+|l|i|n|e| |9|4| @70
+|l|i|n|e| |9|5| @70
+|l|i|n|e| |9|6| @70
+|l|i|n|e| |9|7| @70
+|l|i|n|e| |9|8| @70
+|l|i|n|e| |9@1| @70
+>l|i|n|e| |1|0@1| @69
+@60|1|0@1|,|1| @8|B|o|t|
diff --git a/src/testdir/dumps/Test_scrolloffpad_folds_1.dump b/src/testdir/dumps/Test_scrolloffpad_folds_1.dump
new file mode 100644
index 000000000..1c6433300
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_folds_1.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |1@2| @69
+|l|i|n|e| |1@1|2| @69
+|l|i|n|e| |1@1|3| @69
+|l|i|n|e| |1@1|4| @69
+|l|i|n|e| |1@1|5| @69
+|l|i|n|e| |1@1|6| @69
+|l|i|n|e| |1@1|7| @69
+|l|i|n|e| |1@1|8| @69
+|l|i|n|e| |1@1|9| @69
+>l|i|n|e| |1|2|0| @69
+|~+0#4040ff13&| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+| +0#0000000&@59|1|2|0|,|1| @8|B|o|t|
diff --git a/src/testdir/dumps/Test_scrolloffpad_folds_2.dump b/src/testdir/dumps/Test_scrolloffpad_folds_2.dump
new file mode 100644
index 000000000..63ac801b4
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_folds_2.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |5|1| @70
+|l|i|n|e| |5|2| @70
+|l|i|n|e| |5|3| @70
+|l|i|n|e| |5|4| @70
+|l|i|n|e| |5@1| @70
+|l|i|n|e| |5|6| @70
+|l|i|n|e| |5|7| @70
+|l|i|n|e| |5|8| @70
+|l|i|n|e| |5|9| @70
+>++0#0000e05#a8a8a8255|-@1| |5|1| |l|i|n|e|s|:| |l|i|n|e| |6|0|-@56
+|l+0#0000000#ffffff0|i|n|e| |1@2| @69
+|l|i|n|e| |1@1|2| @69
+|l|i|n|e| |1@1|3| @69
+|l|i|n|e| |1@1|4| @69
+|l|i|n|e| |1@1|5| @69
+|l|i|n|e| |1@1|6| @69
+|l|i|n|e| |1@1|7| @69
+|l|i|n|e| |1@1|8| @69
+|l|i|n|e| |1@1|9| @69
+@60|6|0|,|1| @9|9|8|%|
diff --git a/src/testdir/dumps/Test_scrolloffpad_folds_3.dump b/src/testdir/dumps/Test_scrolloffpad_folds_3.dump
new file mode 100644
index 000000000..1c6433300
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_folds_3.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |1@2| @69
+|l|i|n|e| |1@1|2| @69
+|l|i|n|e| |1@1|3| @69
+|l|i|n|e| |1@1|4| @69
+|l|i|n|e| |1@1|5| @69
+|l|i|n|e| |1@1|6| @69
+|l|i|n|e| |1@1|7| @69
+|l|i|n|e| |1@1|8| @69
+|l|i|n|e| |1@1|9| @69
+>l|i|n|e| |1|2|0| @69
+|~+0#4040ff13&| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+| +0#0000000&@59|1|2|0|,|1| @8|B|o|t|
diff --git a/src/testdir/dumps/Test_scrolloffpad_smoothscroll_1.dump b/src/testdir/dumps/Test_scrolloffpad_smoothscroll_1.dump
new file mode 100644
index 000000000..af38b016a
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_smoothscroll_1.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |9|1| @70
+|l|i|n|e| |9|2| @70
+|l|i|n|e| |9|3| @70
+|l|i|n|e| |9|4| @70
+|l|i|n|e| |9|5| @70
+|l|i|n|e| |9|6| @70
+|l|i|n|e| |9|7| @70
+|l|i|n|e| |9|8| @70
+|l|i|n|e| |9@1| @70
+>l|i|n|e| |1|0@1| @69
+|~+0#4040ff13&| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+| +0#0000000&@59|1|0@1|,|1| @8|B|o|t|
diff --git a/src/testdir/dumps/Test_scrolloffpad_smoothscroll_2.dump b/src/testdir/dumps/Test_scrolloffpad_smoothscroll_2.dump
new file mode 100644
index 000000000..3b66ce621
--- /dev/null
+++ b/src/testdir/dumps/Test_scrolloffpad_smoothscroll_2.dump
@@ -0,0 +1,20 @@
+|l+0&#ffffff0|i|n|e| |9|2| @70
+|l|i|n|e| |9|3| @70
+|l|i|n|e| |9|4| @70
+|l|i|n|e| |9|5| @70
+|l|i|n|e| |9|6| @70
+|l|i|n|e| |9|7| @70
+|l|i|n|e| |9|8| @70
+|l|i|n|e| |9@1| @70
+|L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| >L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N
+|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| |L|O|N|G| @6
+|~+0#4040ff13&| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+|~| @76
+| +0#0000000&@59|1|0@1|,|4|1| @7|B|o|t|
diff --git a/src/testdir/test_cursor_func.vim b/src/testdir/test_cursor_func.vim
index 73169649d..e541da886 100644
--- a/src/testdir/test_cursor_func.vim
+++ b/src/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/src/testdir/test_options.vim b/src/testdir/test_options.vim
index 828a5ca71..76a1bff34 100644
--- a/src/testdir/test_options.vim
+++ b/src/testdir/test_options.vim
@@ -1495,6 +1495,45 @@ 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<
+ 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/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index a134acabd..030e9e659 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -1412,6 +1412,18 @@ endfunc

func Test_popup_option_values()
new
+ " Save old values
+ let save_g_so = &g:so
+ let save_l_so = &l:so
+ let save_g_sop = &g:sop
+ let save_l_sop = &l:sop
+
+ let save_nu = &l:nu
+ let save_wrap = &l:wrap
+ let save_ofu = &l:ofu
+ let save_path = &l:path
+ let save_stl = &l:stl
+
" window-local
setlocal number
setlocal nowrap
@@ -1421,6 +1433,8 @@ func Test_popup_option_values()
setlocal path=/there
" global/window-local
setlocal statusline=2
+ set scrolloff=5
+ set scrolloffpad=1

let winid = popup_create('hello', {})
call assert_equal(0, getwinvar(winid, '&number'))
@@ -1429,12 +1443,27 @@ func Test_popup_option_values()
call assert_equal(&g:path, getwinvar(winid, '&path'))
call assert_equal(&g:statusline, getwinvar(winid, '&statusline'))

- " 'scrolloff' is reset to zero
+ " 'scrolloff' and 'scrolloffpad' are reset to zero
call assert_equal(5, &scrolloff)
call assert_equal(0, getwinvar(winid, '&scrolloff'))
+ call assert_equal(1, &scrolloffpad)
+ call assert_equal(0, getwinvar(winid, '&scrolloffpad'))

call popup_close(winid)
- bwipe
+
+ " Restore old values
+ let &g:so = save_g_so
+ let &l:so = save_l_so
+ let &g:sop = save_g_sop
+ let &l:sop = save_l_sop
+
+ let &l:nu = save_nu
+ let &l:wrap = save_wrap
+ let &l:ofu = save_ofu
+ let &l:path = save_path
+ let &l:stl = save_stl
+
+ bwipe!
endfunc

func Test_popup_atcursor()
diff --git a/src/testdir/test_scroll_opt.vim b/src/testdir/test_scroll_opt.vim
index 99fd32a00..390a8b59a 100644
--- a/src/testdir/test_scroll_opt.vim
+++ b/src/testdir/test_scroll_opt.vim
@@ -1439,4 +1439,655 @@ 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 test_setmouse(2, 1)
+ call feedkeys("\<LeftMouse>", 'xt')
+ call test_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
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim
index 6a02c531b..bc54d272d 100644
--- a/src/testdir/util/gen_opt_test.vim
+++ b/src/testdir/util/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,
\})
@@ -93,6 +94,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]],
\ 'showtabpanel': [[0, 1, 2], []],
\ 'sidescroll': [[0, 1, 8, 999], [-1]],
diff --git a/src/version.c b/src/version.c
index c0f826a17..0e22232f1 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 356,
/**/
355,
/**/
diff --git a/src/window.c b/src/window.c
index 1d0d47844..26d923d9c 100644
--- a/src/window.c
+++ b/src/window.c
@@ -5974,6 +5974,7 @@ win_alloc(win_T *after, int hidden)

// use global option value 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
Reply all
Reply to author
Forward
0 new messages