]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0356: Cannot apply 'scrolloff' context lines at end of file v9.2.0356
authorMcAuley Penney <jacobmpenney@gmail.com>
Wed, 15 Apr 2026 19:11:12 +0000 (19:11 +0000)
committerChristian Brabandt <cb@256bit.org>
Wed, 15 Apr 2026 19:17:13 +0000 (19:17 +0000)
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 <jacobmpenney@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
29 files changed:
runtime/doc/options.txt
runtime/doc/quickref.txt
runtime/doc/tags
runtime/doc/version9.txt
runtime/optwin.vim
runtime/syntax/vim.vim
src/move.c
src/option.c
src/option.h
src/optiondefs.h
src/po/vim.pot
src/popupwin.c
src/proto/option.pro
src/structs.h
src/testdir/dumps/Test_scrolloffpad_basic_1.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_basic_2.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_basic_3.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_folds_1.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_folds_2.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_folds_3.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_smoothscroll_1.dump [new file with mode: 0644]
src/testdir/dumps/Test_scrolloffpad_smoothscroll_2.dump [new file with mode: 0644]
src/testdir/test_cursor_func.vim
src/testdir/test_options.vim
src/testdir/test_popupwin.vim
src/testdir/test_scroll_opt.vim
src/testdir/util/gen_opt_test.vim
src/version.c
src/window.c

index 627d96540207aff4006ae20dfc726f86e88d1191..31a9629c0b54d8602f36efe4d153b9051ee656c2 100644 (file)
@@ -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
index 8f6aca04632bfed37afebaa8da7e38aab5e8004c..1ada2bf9b3e226b900fbbfa822a06d77312cebe5 100644 (file)
@@ -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
index 4d31cc51a6fbc9ac2669b85d0e3ea8ad971894d5..7effaf824f2294f3a68da8c8de4d192e63a5f675 100644 (file)
@@ -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'*
index fa8c5355c7707ec40a26b96991ea8aed481e9e61..ee24e290104d6e651c51d55d5f22d2e4041d81cc 100644 (file)
@@ -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.
index 73243e77f3106a58cad7ed3816565ed79689ef78..62c2ec2e55916f8dab8d0693f87b517f05b27077 100644 (file)
@@ -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 <Bram@vim.org>
 
 " If there already is an option window, jump to that one.
@@ -351,6 +351,8 @@ call append("$", "\t" .. s:local_to_window)
 call <SID>BinOptionL("sms")
 call <SID>AddOption("scrolloff", gettext("number of screen lines to show around the cursor"))
 call append("$", " \tset so=" . &so)
+call <SID>AddOption("scrolloffpad", gettext("keep 'scrolloff' context even at end of file"))
+call append("$", " \tset sop=" . &sop)
 call <SID>AddOption("wrap", gettext("long lines wrap"))
 call append("$", "\t" .. s:local_to_window)
 call <SID>BinOptionL("wrap")
index f213710c3777caaf3005fc022ad51b1c088a054b..a390df78ed5a17a9295ce7273831139c6b2d7f45 100644 (file)
@@ -2,7 +2,7 @@
 " Language:       Vim script
 " Maintainer:     Hirohito Higashi <h.east.727 ATMARK gmail.com>
 "         Doug Kearns <dougkearns@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
index ee9a8eb5aaed93409e8f76d8193703f97e9ae2c1..1e35101303f3a46fd0da1bf45298b23a609b3a4b 100644 (file)
@@ -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;
index 46009341d1ed1ab2a30ee8cd30fe3d39262a2204..dc6f0966d5b6575d07649b929cf498f7176d4524 100644 (file)
@@ -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.
index 6bf6169bbf2ff6094981ed112b4ad6343ca10a76..e5f9de8e16e4c9f74aa3410b7eacd5f2bbadb196 100644 (file)
@@ -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
index 38962790bf93a04d1f818e37134a0ae089a1978d..0214a34b1d8207aff95dd553768d0a8e605f2414 100644 (file)
 #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}
index b09ade556d00f0da27e18834f442f169abe3d193..936961c92ffa4bb4ba273906eb53a099c7872255 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim\n"
 "Report-Msgid-Bugs-To: vim-dev@vim.org\n"
-"POT-Creation-Date: 2026-04-09 20:33+0000\n"
+"POT-Creation-Date: 2026-04-15 19:10+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -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 ""
 
index bdf53cdc4fc8ead8e0bd2c361d9d1b5467ec2b2b..319d882c55d7749142f126108b928b4d94a757de 100644 (file)
@@ -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)
     {
index 892a6ce3b225fbd17b56f8c5a8bedbb87b397d3a..ae586ea9787d813873b5681a0414d0bb9e2f8aa2 100644 (file)
@@ -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);
index 955b9f1001bc6ef0b409fd657e47f13ca5349c0a..648ceec90551e69797afd5472fa004ff08dd2cce 100644 (file)
@@ -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 (file)
index 0000000..af38b01
--- /dev/null
@@ -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 (file)
index 0000000..0702019
--- /dev/null
@@ -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 (file)
index 0000000..72eed1e
--- /dev/null
@@ -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 (file)
index 0000000..1c64333
--- /dev/null
@@ -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 (file)
index 0000000..63ac801
--- /dev/null
@@ -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 (file)
index 0000000..1c64333
--- /dev/null
@@ -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 (file)
index 0000000..af38b01
--- /dev/null
@@ -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 (file)
index 0000000..3b66ce6
--- /dev/null
@@ -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| 
index 73169649df3afe0fcf4e592dfd3e4658cbb30eed..e541da886d326496b5cdd65a6a6a46c4c69b936a 100644 (file)
@@ -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))
index 828a5ca7106e48ca314bf92ffe17fecd0f45784a..76a1bff344ee108b83b8803e73b970c21959fd3f 100644 (file)
@@ -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
 
index a134acabd90070152d4f642cd4b91acae2d96389..030e9e65980ba792f4cf5eeac9e7d6346b594b51 100644 (file)
@@ -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()
index 99fd32a0094b44319b6acacb0dbeab41131eeaf1..390a8b59a2f4fd6352738bd71bdf3f37e156430c 100644 (file)
@@ -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
index 6a02c531bdc9c2dd7e5507556922df2bb3298ea8..bc54d272d8db4ab1b857d095c9bade547da84589 100644 (file)
@@ -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]],
index c0f826a17dd9a9ac4ffd415541f4a01b67c49a23..0e22232f131d757bdd4767d6ab4c0d9102c3e323 100644 (file)
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    356,
 /**/
     355,
 /**/
index 1d0d478442faf54202bf54c7cae30e85890f5feb..26d923d9c0e8fccac6d6c4090d97f67ba5d46cea 100644 (file)
@@ -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