Merge pull request #39248 from zeertzjq/vim-9.2.0356

vim-patch: 'scrolloffpad'
This commit is contained in:
zeertzjq
2026-04-22 11:01:43 +08:00
committed by GitHub
16 changed files with 1044 additions and 17 deletions

View File

@@ -159,6 +159,7 @@ the same range instances now compare equal.
OPTIONS
• 'scrolloffpad' allows vertically centering cursor at the end of file.
• 'winpinned' prevents window from closing unless specifically targeted.
PERFORMANCE

View File

@@ -5278,14 +5278,32 @@ A jump table for the options with a short description can be found at |Q_op|.
Minimal number of screen lines to keep above and below the cursor.
This will make some context visible around where you are working. If
you set it to a very large value (999) the cursor line will always be
in the middle of the window (except at the start or end of the file or
when long lines wrap).
in the middle of the window (except at the start or end of the file,
see 'scrolloffpad', or when long lines wrap).
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloff<
setlocal scrolloff=-1
< For scrolling horizontally see 'sidescrolloff'.
*'scrolloffpad'* *'sop'*
'scrolloffpad' 'sop' number (default 0)
global or local to window |global-local|
When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
the cursor to remain centered when at the end of the file.
Normally, 'scrolloff' will not keep the cursor centered at the
end of the file.
A value of 0 disables this feature. Any value above 0 enables it.
For a window-local value, -1 means to use the global value.
Values below -1 are invalid.
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloffpad<
setlocal scrolloffpad=-1
<
*'scrollopt'* *'sbo'*
'scrollopt' 'sbo' string (default "ver,jump")
global

View File

@@ -841,6 +841,7 @@ Short explanation of each option: *option-list*
'scrollbind' 'scb' scroll in window as other windows scroll
'scrolljump' 'sj' minimum number of lines to scroll
'scrolloff' 'so' minimum nr. of lines above and below cursor
'scrolloffpad' 'sop' vertically center cursor at end of file
'scrollopt' 'sbo' how 'scrollbind' should behave
'sections' 'sect' nroff macros that separate sections
'secure' secure mode for reading .vimrc in current dir

View File

@@ -5488,8 +5488,8 @@ vim.go.sj = vim.go.scrolljump
--- Minimal number of screen lines to keep above and below the cursor.
--- This will make some context visible around where you are working. If
--- you set it to a very large value (999) the cursor line will always be
--- in the middle of the window (except at the start or end of the file or
--- when long lines wrap).
--- in the middle of the window (except at the start or end of the file,
--- see 'scrolloffpad', or when long lines wrap).
--- After using the local value, go back the global value with one of
--- these two:
---
@@ -5507,6 +5507,32 @@ vim.wo.so = vim.wo.scrolloff
vim.go.scrolloff = vim.o.scrolloff
vim.go.so = vim.go.scrolloff
--- When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
--- the cursor to remain centered when at the end of the file.
--- Normally, 'scrolloff' will not keep the cursor centered at the
--- end of the file.
---
--- A value of 0 disables this feature. Any value above 0 enables it.
--- For a window-local value, -1 means to use the global value.
--- Values below -1 are invalid.
---
--- After using the local value, go back the global value with one of
--- these two:
---
--- ```vim
--- setlocal scrolloffpad<
--- setlocal scrolloffpad=-1
--- ```
---
---
--- @type integer
vim.o.scrolloffpad = 0
vim.o.sop = vim.o.scrolloffpad
vim.wo.scrolloffpad = vim.o.scrolloffpad
vim.wo.sop = vim.wo.scrolloffpad
vim.go.scrolloffpad = vim.o.scrolloffpad
vim.go.sop = vim.go.scrolloffpad
--- This is a comma-separated list of words that specifies how
--- 'scrollbind' windows should behave. 'sbo' stands for ScrollBind
--- Options.

View File

@@ -59,6 +59,7 @@ local options_list = {
{ 'scroll', N_ 'number of lines to scroll for CTRL-U and CTRL-D' },
{ 'smoothscroll', N_ 'scroll by screen line' },
{ 'scrolloff', N_ 'number of screen lines to show around the cursor' },
{ 'scrolloffpad', N_ 'vertically center cursor even at end of file' },
{ 'wrap', N_ 'long lines wrap' },
{ 'linebreak', N_ "wrap long lines at a character in 'breakat'" },
{ 'breakindent', N_ 'preserve indentation in wrapped text' },

View File

@@ -204,6 +204,8 @@ typedef struct {
#define w_p_siso w_onebuf_opt.wo_siso // 'sidescrolloff' local value
OptInt wo_so;
#define w_p_so w_onebuf_opt.wo_so // 'scrolloff' local value
OptInt wo_sop;
#define w_p_sop w_onebuf_opt.wo_sop // 'scrolloffpad' local value
char *wo_winhl;
#define w_p_winhl w_onebuf_opt.wo_winhl // 'winhighlight'
char *wo_lcs;

View File

@@ -246,6 +246,26 @@ static void reset_skipcol(win_T *wp)
redraw_later(wp, UPD_SOME_VALID);
}
/// Return true when 'scrolloffpad' may augment 'scrolloff'.
/// This only applies to automatic cursor visibility correction.
/// For now 'scrolloffpad' is treated as boolean: 0 disables, > 0 enables.
static bool use_scrolloffpad(win_T *wp)
{
return get_scrolloff_value(wp) > 0 && get_scrolloffpad_value(wp) > 0;
}
/// Return true when there are not enough real buffer lines below "lnum" to
/// satisfy the requested "so" context.
static bool scrolloffpad_eof_pressure(win_T *wp, linenr_T lnum, OptInt so)
{
if (!use_scrolloffpad(wp) || so <= 0) {
return false;
}
// Use subtraction to avoid signed overflow in "lnum + so".
return lnum > wp->w_buffer->b_ml.ml_line_count - so;
}
// Update wp->w_topline to move the cursor onto the screen.
void update_topline(win_T *wp)
{
@@ -278,6 +298,7 @@ void update_topline(win_T *wp)
if (mouse_dragging > 0) {
*so_ptr = mouse_dragging - 1;
}
bool eof_pressure = scrolloffpad_eof_pressure(wp, wp->w_cursor.lnum, *so_ptr);
linenr_T old_topline = wp->w_topline;
int old_topfill = wp->w_topfill;
@@ -350,10 +371,18 @@ void update_topline(win_T *wp)
// cursor in the middle of the window. Otherwise put the cursor
// near the top of the window.
if (n >= halfheight) {
scroll_cursor_halfway(wp, false, false);
if (eof_pressure) {
scroll_cursor_halfway(wp, true, true);
} else {
scroll_cursor_halfway(wp, false, false);
}
} else {
scroll_cursor_top(wp, scrolljump_value(wp), false);
check_botline = true;
if (eof_pressure) {
scroll_cursor_halfway(wp, true, true);
} else {
scroll_cursor_top(wp, scrolljump_value(wp), false);
check_botline = true;
}
}
} else {
// Make sure topline is the first line of a fold.
@@ -374,7 +403,7 @@ void update_topline(win_T *wp)
}
assert(wp->w_buffer != 0);
if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count) {
if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count || use_scrolloffpad(wp)) {
if (wp->w_cursor.lnum < wp->w_botline) {
if ((wp->w_cursor.lnum >= wp->w_botline - *so_ptr || win_lines_concealed(wp))) {
lineoff_T loff;
@@ -382,7 +411,7 @@ void update_topline(win_T *wp)
// Cursor is (a few lines) above botline, check if there are
// 'scrolloff' window lines below the cursor. If not, need to
// scroll.
int n = wp->w_empty_rows;
int n = eof_pressure ? 0 : wp->w_empty_rows;
loff.lnum = wp->w_cursor.lnum;
// In a fold go to its last line.
hasFolding(wp, loff.lnum, NULL, &loff.lnum);
@@ -397,7 +426,7 @@ void update_topline(win_T *wp)
}
botline_forw(wp, &loff);
}
if (n >= *so_ptr) {
if (n >= *so_ptr && !eof_pressure) {
// sufficient context, no need to scroll
check_botline = false;
}
@@ -424,9 +453,13 @@ void update_topline(win_T *wp)
n = wp->w_cursor.lnum - wp->w_botline + 1 + *so_ptr;
}
if (n <= wp->w_view_height + 1) {
scroll_cursor_bot(wp, scrolljump_value(wp), false);
if (eof_pressure) {
scroll_cursor_halfway(wp, true, true);
} else {
scroll_cursor_bot(wp, scrolljump_value(wp), false);
}
} else {
scroll_cursor_halfway(wp, false, false);
scroll_cursor_halfway(wp, eof_pressure, eof_pressure);
}
}
}
@@ -2111,8 +2144,9 @@ void scroll_cursor_bot(win_T *wp, int min_scroll, bool set_topbot)
// Scroll up if the cursor is off the bottom of the screen a bit.
// Otherwise put it at 1/2 of the screen.
bool eof_pressure = scrolloffpad_eof_pressure(wp, cln, so);
if (line_count >= wp->w_view_height && line_count > min_scroll) {
scroll_cursor_halfway(wp, false, true);
scroll_cursor_halfway(wp, eof_pressure, true);
} else if (line_count > 0) {
if (do_sms) {
scrollup(wp, scrolled, true); // TODO(vim):
@@ -2289,7 +2323,9 @@ void cursor_correct(win_T *wp)
validate_botline_win(wp);
if (wp->w_botline == wp->w_buffer->b_ml.ml_line_count + 1
&& mouse_dragging == 0) {
below_wanted = 0;
if (!use_scrolloffpad(wp)) {
below_wanted = 0;
}
int max_off = (wp->w_view_height - 1) / 2;
above_wanted = MIN(above_wanted, max_off);
}

View File

@@ -3111,6 +3111,12 @@ static const char *validate_num_option(OptIndex opt_idx, OptInt *newval, char *e
return e_positive;
}
break;
case kOptScrolloffpad:
// if (value < 0 && full_screen) {
if (value < 0) {
return e_invarg;
}
break;
case kOptSidescrolloff:
if (value < 0 && full_screen) {
return e_positive;
@@ -3600,6 +3606,7 @@ static OptVal get_option_unset_value(OptIndex opt_idx)
case kOptFsync:
return BOOLEAN_OPTVAL(kNone);
case kOptScrolloff:
case kOptScrolloffpad:
case kOptSidescrolloff:
return NUMBER_OPTVAL(-1);
case kOptUndolevels:
@@ -4673,6 +4680,8 @@ void *get_varp_scope_from(vimoption_T *p, int opt_flags, buf_T *buf, win_T *win)
return &(win->w_p_siso);
case kOptScrolloff:
return &(win->w_p_so);
case kOptScrolloffpad:
return &(win->w_p_sop);
case kOptDefine:
return &(buf->b_p_def);
case kOptInclude:
@@ -4760,6 +4769,8 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return win->w_p_siso >= 0 ? &(win->w_p_siso) : p->var;
case kOptScrolloff:
return win->w_p_so >= 0 ? &(win->w_p_so) : p->var;
case kOptScrolloffpad:
return win->w_p_sop >= 0 ? &(win->w_p_sop) : p->var;
case kOptBackupcopy:
return *buf->b_p_bkc != NUL ? &(buf->b_p_bkc) : p->var;
case kOptDefine:
@@ -5121,6 +5132,7 @@ void copy_winopt(winopt_T *from, winopt_T *to)
to->wo_crb_save = from->wo_crb_save;
to->wo_siso = from->wo_siso;
to->wo_so = from->wo_so;
to->wo_sop = from->wo_sop;
to->wo_spell = from->wo_spell;
to->wo_cuc = from->wo_cuc;
to->wo_cul = from->wo_cul;
@@ -6560,6 +6572,13 @@ int64_t get_scrolloff_value(win_T *wp)
return wp->w_p_so < 0 ? p_so : wp->w_p_so;
}
/// Return the effective 'scrolloffpad' value for the current window, using the
/// global value when appropriate.
int64_t get_scrolloffpad_value(win_T *wp)
{
return wp->w_p_sop == -1 ? p_sop : curwin->w_p_sop;
}
/// Return the effective 'sidescrolloff' value for the current window, using the
/// global value when appropriate.
int64_t get_sidescrolloff_value(win_T *wp)

View File

@@ -472,6 +472,7 @@ EXTERN char *p_rtp; ///< 'runtimepath'
EXTERN OptInt p_scbk; ///< 'scrollback'
EXTERN OptInt p_sj; ///< 'scrolljump'
EXTERN OptInt p_so; ///< 'scrolloff'
EXTERN OptInt p_sop; ///< 'scrolloffpad'
EXTERN char *p_sbo; ///< 'scrollopt'
EXTERN char *p_sections; ///< 'sections'
EXTERN int p_secure; ///< 'secure'

View File

@@ -7237,8 +7237,8 @@ local options = {
Minimal number of screen lines to keep above and below the cursor.
This will make some context visible around where you are working. If
you set it to a very large value (999) the cursor line will always be
in the middle of the window (except at the start or end of the file or
when long lines wrap).
in the middle of the window (except at the start or end of the file,
see 'scrolloffpad', or when long lines wrap).
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloff<
@@ -7251,6 +7251,31 @@ local options = {
type = 'number',
varname = 'p_so',
},
{
abbreviation = 'sop',
defaults = 0,
desc = [=[
When 'scrolloff' and 'scrolloffpad' are greater than zero, allow
the cursor to remain centered when at the end of the file.
Normally, 'scrolloff' will not keep the cursor centered at the
end of the file.
A value of 0 disables this feature. Any value above 0 enables it.
For a window-local value, -1 means to use the global value.
Values below -1 are invalid.
After using the local value, go back the global value with one of
these two: >vim
setlocal scrolloffpad<
setlocal scrolloffpad=-1
<
]=],
full_name = 'scrolloffpad',
scope = { 'global', 'win' },
short_desc = N_('vertically center cursor even at end of file'),
type = 'number',
varname = 'p_sop',
},
{
abbreviation = 'sbo',
defaults = 'ver,jump',

View File

@@ -5532,6 +5532,7 @@ win_T *win_alloc(win_T *after, bool hidden)
// use global option for global-local options
new_wp->w_allbuf_opt.wo_so = new_wp->w_p_so = -1;
new_wp->w_allbuf_opt.wo_sop = new_wp->w_p_sop = -1;
new_wp->w_allbuf_opt.wo_siso = new_wp->w_p_siso = -1;
// We won't calculate w_fraction until resizing the window

View File

@@ -1397,3 +1397,205 @@ describe('smoothscroll', function()
]])
end)
end)
describe('scrolloffpad', function()
local screen
before_each(function()
screen = Screen.new(78, 20)
end)
-- oldtest: Test_scrolloffpad_basic()
it('works', function()
exec([[
set scrolloff=10
set scrolloffpad=5
enew!
call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
normal! gg
]])
-- Enabled: scrolloffpad > 0, expect EOF centering/padding
exec('normal! G')
screen:expect([[
line 91 |
line 92 |
line 93 |
line 94 |
line 95 |
line 96 |
line 97 |
line 98 |
line 99 |
^line 100 |
{1:~ }|*9
|
]])
-- Beginning-of-file is unchanged (Top)
exec('normal! gg')
screen:expect([[
^line 1 |
line 2 |
line 3 |
line 4 |
line 5 |
line 6 |
line 7 |
line 8 |
line 9 |
line 10 |
line 11 |
line 12 |
line 13 |
line 14 |
line 15 |
line 16 |
line 17 |
line 18 |
line 19 |
|
]])
-- Gating: disable scrolloffpad, then go to EOF again
-- Expect normal EOF behavior (no extra centering/padding)
exec('set scrolloffpad=0')
exec('normal! G')
screen:expect([[
line 82 |
line 83 |
line 84 |
line 85 |
line 86 |
line 87 |
line 88 |
line 89 |
line 90 |
line 91 |
line 92 |
line 93 |
line 94 |
line 95 |
line 96 |
line 97 |
line 98 |
line 99 |
^line 100 |
|
]])
end)
-- oldtest: Test_scrolloffpad_smoothscroll()
it('works with smoothscroll', function()
exec([[
set smoothscroll scrolloff=10 scrolloffpad=1
enew!
call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
normal! gg
]])
exec('normal! G')
screen:expect([[
line 91 |
line 92 |
line 93 |
line 94 |
line 95 |
line 96 |
line 97 |
line 98 |
line 99 |
^line 100 |
{1:~ }|*9
|
]])
exec([[call setline(line('$'), repeat('LONG ', 30))]])
exec('normal! 41|')
screen:expect([[
line 92 |
line 93 |
line 94 |
line 95 |
line 96 |
line 97 |
line 98 |
line 99 |
LONG LONG LONG LONG LONG LONG LONG LONG ^LONG LONG LONG LONG LONG LONG LONG LON|
G LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG LONG |
{1:~ }|*9
|
]])
end)
-- oldtest: Test_scrolloffpad_with_folds()
it('works with folds', function()
exec([[
set scrolloff=10
set scrolloffpad=1
enew
call setline(1, map(range(1, 120), {_, v -> 'line ' . v}))
" Create a large fold near the end of the file.
" Fold lines 60-110, leaving 111-120 visible after the fold.
set foldmethod=manual
set foldenable
normal! gg
normal! 60G
normal! zf50j
normal! gg
]])
-- Case 1: Jump to end-of-file
-- With folds present, scrolloffpad should still
-- keep the cursor positioned with padding below EOF
exec('normal! G')
local s1 = [[
line 111 |
line 112 |
line 113 |
line 114 |
line 115 |
line 116 |
line 117 |
line 118 |
line 119 |
^line 120 |
{1:~ }|*9
|
]]
screen:expect(s1)
-- Case 2: Move to the folded line to ensure the fold is actually in view
exec('normal! 60G')
screen:expect([[
line 51 |
line 52 |
line 53 |
line 54 |
line 55 |
line 56 |
line 57 |
line 58 |
line 59 |
{13:^+-- 51 lines: line 60·························································}|
line 111 |
line 112 |
line 113 |
line 114 |
line 115 |
line 116 |
line 117 |
line 118 |
line 119 |
|
]])
-- Case 3: Close the fold explicitly and go to EOF again
-- Behavior should remain stable with closed folds
exec('normal! zc')
exec('normal! G')
screen:expect(s1)
end)
end)

View File

@@ -24,6 +24,7 @@ while search("^'[^']*'.*\\n.*|global-local", 'W')
endwhile
call extend(global_locals, #{
\ scrolloff: -1,
\ scrolloffpad: -1,
\ sidescrolloff: -1,
\ undolevels: -123456,
\})
@@ -127,6 +128,7 @@ let test_values = {
\ 'scroll': [[0, 1, 2, 15], [-1, 999]],
\ 'scrolljump': [[-100, -1, 0, 1, 2, 15], [-101, 999]],
\ 'scrolloff': [[0, 1, 8, 999], [-1]],
\ 'scrolloffpad': [[0, 1, 2, 3], [-1]],
\ 'shiftwidth': [[0, 1, 8, 999], [-1]],
\ 'sidescroll': [[0, 1, 8, 999], [-1]],
\ 'sidescrolloff': [[0, 1, 8, 999], [-1]],

View 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))

View File

@@ -1470,6 +1470,46 @@ func Test_local_scrolloff()
set siso&
endfunc
func Test_local_scrolloffpad()
let save_g_sop = &g:sop
let save_l_sop = &l:sop
set sop=0
call assert_equal(0, &g:sop)
call assert_equal(-1, &l:sop)
call assert_equal(0, &sop)
setglobal sop=1
call assert_equal(1, &g:sop)
call assert_equal(1, &sop)
split
call assert_equal(1, &g:sop)
call assert_equal(-1, &l:sop)
call assert_equal(1, &sop)
setlocal sop=0
call assert_equal(0, &l:sop)
call assert_equal(0, &sop)
call assert_equal(1, &g:sop)
wincmd p
call assert_equal(1, &sop)
wincmd p
"setlocal sop<
set sop<
call assert_equal(-1, &l:sop)
call assert_equal(1, &sop)
setlocal sop=2
call assert_equal(2, &l:sop)
call assert_equal(2, &sop)
setlocal sop=-1
call assert_equal(-1, &l:sop)
call assert_equal(1, &sop) " Uses global value because local is -1
call assert_fails("setlocal sop=-2", 'E474:')
call assert_equal(-1, &l:sop)
call assert_equal(1, &sop)
call assert_fails("setlocal sop=foo", 'E521:')
close
let &g:sop = save_g_sop
let &l:sop = save_l_sop
endfunc
func Test_writedelay()
CheckFunction reltimefloat

View File

@@ -1443,6 +1443,657 @@ func Test_smoothscroll_listchars_eol()
bwipe!
endfunc
" scrolloffpad contract:
" - augment scrolloff only under EOF pressure (insufficient real lines below);
" - do not change explicit "z" viewport placement command semantics;
" - current scope is EOF-only, so BOF behavior remains unchanged.
func Test_scrolloffpad_zb_keeps_bottom_command_semantics()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 300), 'printf("line %d", v:val)'))
setlocal scrolloffpad=0
normal! gg150Gzb
let baseline = [line('.'), line('w$'), winline()]
setlocal scrolloffpad=1
normal! gg150Gzb
call assert_equal(baseline, [line('.'), line('w$'), winline()])
bwipe!
endfunc
func Test_scrolloffpad_zminus_keeps_bottom_beginline_semantics()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 300), 'printf(" line %d", v:val)'))
setlocal scrolloffpad=0
normal! gg150Gz-
let baseline = [line('.'), line('w$'), winline(), col('.')]
call assert_equal(match(getline('.'), '\S') + 1, col('.'))
setlocal scrolloffpad=1
normal! gg150Gz-
call assert_equal(baseline, [line('.'), line('w$'), winline(), col('.')])
call assert_equal(match(getline('.'), '\S') + 1, col('.'))
bwipe!
endfunc
func Test_scrolloffpad_zb_is_one_shot_then_scrolloff_reapplies()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 300), 'printf("line %d", v:val)'))
let after_zb = {}
let after_j = {}
for sop in [0, 1]
let &l:scrolloffpad = sop
normal! gg150Gzb
let after_zb[sop] = [line('.'), line('w$'), winline(), winsaveview().topline]
normal! j
let after_j[sop] = [line('.'), line('w$'), winline(), winsaveview().topline]
call assert_notequal(after_zb[sop][3], after_j[sop][3])
call assert_true(line('.') < line('w$'))
endfor
call assert_equal(after_zb[0], after_zb[1])
call assert_equal(after_j[0], after_j[1])
bwipe!
endfunc
func Test_scrolloffpad_has_no_mid_buffer_effect()
new
resize 12
setlocal scrolloff=10 scrolloffpad=0
call setline(1, map(range(1, 500), 'printf("line %d", v:val)'))
normal! gg150G
let topline_without_pad = winsaveview().topline
setlocal scrolloffpad=1
normal! gg150G
let topline_with_pad = winsaveview().topline
call assert_equal(topline_without_pad, topline_with_pad)
bwipe!
endfunc
func Test_scrolloffpad_changes_eof_pressure_only()
new
resize 12
setlocal scrolloff=10 scrolloffpad=0
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
normal! ggG
let view_without_pad = winsaveview()
let cursor_without_pad = line('.')
let row_without_pad = winline()
setlocal scrolloffpad=1
normal! ggG
let view_with_pad = winsaveview()
let row_with_pad = winline()
call assert_equal(line('$'), line('.'))
call assert_equal(cursor_without_pad, line('.'))
call assert_notequal(view_without_pad.topline, view_with_pad.topline)
call assert_true(row_with_pad < row_without_pad)
bwipe!
endfunc
func Test_scrolloffpad_large_scrolloff_no_overflow()
new
resize 12
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
setlocal scrolloff=2147483647 scrolloffpad=0
normal! ggG
let view_without_pad = winsaveview()
let row_without_pad = winline()
setlocal scrolloffpad=1
normal! ggG
let view_with_pad = winsaveview()
let row_with_pad = winline()
call assert_equal(line('$'), line('.'))
call assert_notequal(view_without_pad.topline, view_with_pad.topline)
call assert_true(row_with_pad < row_without_pad)
bwipe!
endfunc
func Test_scrolloffpad_boolean_gate_values()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
let views = {}
let rows = {}
for sop in [0, 1, 2]
let &l:scrolloffpad = sop
normal! ggG
let views[sop] = winsaveview()
let rows[sop] = winline()
call assert_equal(line('$'), line('.'))
endfor
call assert_equal(views[1].topline, views[2].topline)
call assert_equal(rows[1], rows[2])
call assert_notequal(views[0].topline, views[1].topline)
call assert_true(rows[1] < rows[0])
bwipe!
endfunc
func Test_scrolloffpad_requires_scrolloff_nonzero()
new
resize 12
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
let states = {}
for so in [0, 10]
let states[so] = {}
for sop in [0, 1]
let &l:scrolloff = so
let &l:scrolloffpad = sop
normal! ggG
let states[so][sop] = [line('.'), line('w0'), line('w$'), winline()]
call assert_equal(line('$'), line('.'))
endfor
endfor
call assert_equal(states[0][0], states[0][1])
call assert_notequal(states[10][0], states[10][1])
call assert_true(states[10][1][3] < states[10][0][3])
bwipe!
endfunc
func Test_scrolloffpad_search_to_eof()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
call setline(line('$'), 'EOF TARGET')
let states = {}
for sop in [0, 1]
let &l:scrolloffpad = sop
normal! gg
call assert_true(search('EOF TARGET') > 0)
let states[sop] = [line('.'), line('w0'), line('w$'), winline()]
call assert_equal(line('$'), line('.'))
endfor
call assert_notequal(states[0], states[1])
call assert_true(states[1][3] < states[0][3])
bwipe!
endfunc
func Test_scrolloffpad_paging_to_eof()
new
resize 12
setlocal scrolloff=10
call setline(1, map(range(1, 240), 'printf("line %d", v:val)'))
let states = {}
for sop in [0, 1]
let &l:scrolloffpad = sop
normal! gg
let prev = -1
for _ in range(1, 200)
execute "normal! \<C-D>"
if line('.') == prev
break
endif
let prev = line('.')
endfor
let states[sop] = [line('.'), line('w0'), line('w$'), winline()]
call assert_equal(line('$'), line('w$'))
endfor
call assert_notequal(states[0], states[1])
call assert_true(states[1][3] < states[0][3])
bwipe!
endfunc
func Test_scrolloffpad_autocmd_append_at_eof()
let states = {}
for sop in [0, 1]
new
resize 12
setlocal scrolloff=10
let &l:scrolloffpad = sop
call setline(1, map(range(1, 120), 'printf("line %d", v:val)'))
let b:scrolloffpad_appended = 0
augroup ScrolloffpadAppendAtEof
autocmd!
autocmd CursorMoved <buffer> if b:scrolloffpad_appended == 0 && line('.') == line('$') | call append('$', 'appended') | let b:scrolloffpad_appended = 1 | endif
augroup END
normal! ggG
doautocmd <nomodeline> CursorMoved
let states[sop] = [
\ line('.'),
\ line('$'),
\ line('w0'),
\ line('w$'),
\ winline(),
\ b:scrolloffpad_appended,
\ ]
call assert_equal(1, b:scrolloffpad_appended)
call assert_equal(states[sop][1] - 1, states[sop][0])
augroup ScrolloffpadAppendAtEof
autocmd!
augroup END
bwipe!
endfor
call assert_notequal(states[0], states[1])
call assert_true(states[1][4] < states[0][4])
endfunc
func Test_scrolloffpad_eof_no_reverse_scroll_on_j()
new
resize 20
setlocal scrolloff=20 scrolloffpad=1
call setline(1, map(range(1, 80), 'printf("line %d", v:val)'))
normal! gg
let prev_topline = winsaveview().topline
for lnum in range(2, line('$'))
normal! j
let cur_topline = winsaveview().topline
call assert_true(
\ cur_topline >= prev_topline,
\ printf('topline moved backwards at line %d: %d -> %d',
\ lnum, prev_topline, cur_topline))
let prev_topline = cur_topline
endfor
bwipe!
endfunc
func Test_scrolloffpad_bof_unchanged()
new
resize 12
setlocal scrolloff=10 scrolloffpad=0
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
normal! Ggg
let view_without_pad = winsaveview()
let w0_without_pad = line('w0')
setlocal scrolloffpad=1
normal! Ggg
let view_with_pad = winsaveview()
let w0_with_pad = line('w0')
call assert_equal(1, w0_without_pad)
call assert_equal(1, w0_with_pad)
call assert_equal(view_without_pad.topline, view_with_pad.topline)
bwipe!
endfunc
func Test_scrolloffpad_mouse_drag_uses_drag_scrolloff()
CheckFeature mouse
let save_mouse = &mouse
set mouse=a
new
resize 20
call setline(1, map(range(1, 240), 'printf("line %d", v:val)'))
setlocal scrolloff=50
let after_drag = {}
for sop in [0, 1]
let &l:scrolloffpad = sop
normal! gg160Gzt
normal! v
call Ntest_setmouse(2, 1)
call feedkeys("\<LeftMouse>", 'xt')
call Ntest_setmouse(3, 1)
call feedkeys("\<LeftDrag>", 'xt')
let after_drag[sop] = [winsaveview().topline, line('.'), winline()]
call feedkeys("\<Esc>", 'xt')
endfor
call assert_equal(after_drag[0], after_drag[1])
bwipe!
let &mouse = save_mouse
endfunc
func Test_scrolloffpad_basic()
CheckScreendump
CheckRunVimInTerminal
let save_termwinsize = &termwinsize
set termwinsize=
let lines =<< trim END
set scrolloff=10
set scrolloffpad=5
enew!
call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
normal! gg
END
call writefile(lines, 'XScrolloffpadBasic', 'D')
let buf = RunVimInTerminal('-S XScrolloffpadBasic', {'rows': 20, 'cols': 78})
" Enabled: scrolloffpad > 0, expect EOF centering/padding
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_1', {})
" Beginning-of-file is unchanged (Top)
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! gg\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_2', {})
" Gating: disable scrolloffpad, then go to EOF again
" Expect normal EOF behavior (no extra centering/padding)
call term_sendkeys(buf, "\<Esc>:\<C-U>set scrolloffpad=0\<CR>")
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_basic_3', {})
call StopVimInTerminal(buf)
let &termwinsize = save_termwinsize
endfunc
func Test_scrolloffpad_smoothscroll()
CheckScreendump
CheckRunVimInTerminal
let save_termwinsize = &termwinsize
set termwinsize=
let lines =<< trim END
set smoothscroll scrolloff=10 scrolloffpad=1
enew!
call setline(1, map(range(1, 100), 'printf("line %d", v:val)'))
normal! gg
END
call writefile(lines, 'XScrolloffpadSmoothscroll', 'D')
let buf = RunVimInTerminal('-S XScrolloffpadSmoothscroll', #{rows: 20, cols: 78})
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_smoothscroll_1', {})
call term_sendkeys(buf, "\<Esc>:\<C-U>call setline(line('$'), repeat('LONG ', 30))\<CR>")
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! 41|\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_smoothscroll_2', {})
call StopVimInTerminal(buf)
let &termwinsize = save_termwinsize
endfunc
func Test_scrolloffpad_insert_eof()
let save_so = &scrolloff
let save_sop = &scrolloffpad
set scrolloff=10 scrolloffpad=1
enew!
call setline(1, map(range(1, 200), 'printf("line %d", v:val)'))
normal! G
let topline_before = winsaveview().topline
call feedkeys("i\<Esc>", 'xt')
call assert_equal(topline_before, winsaveview().topline)
exe "normal! \<C-E>"
let topline_after = winsaveview().topline
call feedkeys("i\<Esc>", 'xt')
call assert_equal(topline_after, winsaveview().topline)
let &scrolloff = save_so
let &scrolloffpad = save_sop
bwipe!
endfunc
func Test_scrolloffpad_in_diff_mode()
CheckFeature diff
let save_so = &scrolloff
let save_sop = &scrolloffpad
let save_splitright = &splitright
set nosplitright
set scrolloff=10
set scrolloffpad=0
enew
call setline(1, map(range(1, 100), {_, v -> 'line ' .. v}))
diffthis
vnew
call setline(1, map(range(1, 100), {_, v -> 'line ' .. v}))
" Make buffers minimally different to avoid diff folding everything.
call setline(50, 'DIFF LINE 50')
diffthis
windo normal! zR
windo normal! gg
wincmd =
let rows_without = []
let rows_with = []
let near_states = []
let eof_states = []
for sop in [0, 1]
let &scrolloffpad = sop
" Near EOF with real text visible in both windows.
windo normal! 99G
for w in range(1, winnr('$'))
execute w .. 'wincmd w'
let state = [line('.'), line('w0'), line('w$'), winline()]
call assert_equal(99, state[0])
call assert_equal(100, state[2])
if sop == 0
call add(near_states, state)
endif
endfor
call assert_equal(near_states[0], near_states[1])
" EOF in both windows: scrolloffpad should raise the cursor row.
windo normal! G
for w in range(1, winnr('$'))
execute w .. 'wincmd w'
let state = [line('.'), line('w0'), line('w$'), winline()]
call assert_equal(line('$'), state[0])
if sop == 0
call add(eof_states, state)
call add(rows_without, state[3])
else
call add(rows_with, state[3])
endif
endfor
call assert_equal(eof_states[0], eof_states[1])
endfor
call assert_true(rows_with[0] < rows_without[0])
call assert_true(rows_with[1] < rows_without[1])
windo diffoff
%bwipe!
let &scrolloff = save_so
let &scrolloffpad = save_sop
let &splitright = save_splitright
endfunc
func Test_scrolloffpad_diff_eof_filler_behavior()
CheckFeature diff
let save_so = &scrolloff
let save_sop = &scrolloffpad
let save_diffopt = &diffopt
let save_splitright = &splitright
set diffopt+=filler
set scrolloff=10
set scrolloffpad=0
set nosplitright
20new
call setline(1, map(range(1, 100), {_, v -> 'left ' .. v}))
diffthis
let short_wid = win_getid()
vnew
call setline(1, map(range(1, 120), {_, v -> 'right ' .. v}))
diffthis
let long_wid = win_getid()
call assert_true(win_gotoid(short_wid))
let short_height = winheight(0)
call assert_true(win_gotoid(long_wid))
let long_height = winheight(0)
call assert_equal(short_height, long_height)
call assert_equal(20, short_height)
let ordered_diff_wids = [long_wid, short_wid]
let states = {}
for sop in [0, 1]
execute 'set scrolloffpad=' .. sop
for wid in ordered_diff_wids
call assert_true(win_gotoid(wid))
normal! gg
endfor
for wid in ordered_diff_wids
call assert_true(win_gotoid(wid))
normal! G
endfor
call assert_true(win_gotoid(short_wid))
let short_view = winsaveview()
let short_state = [
\ line('.'),
\ line('$'),
\ winline(),
\ short_view.topline,
\ short_view.topfill,
\ diff_filler(line('$') + 1),
\ ]
call assert_equal(short_state[1], short_state[0])
call assert_true(short_state[5] > 0)
call assert_true(win_gotoid(long_wid))
let long_view = winsaveview()
let long_state = [
\ line('.'),
\ line('$'),
\ winline(),
\ long_view.topline,
\ long_view.topfill,
\ ]
call assert_true(long_state[0] > 0 && long_state[0] <= long_state[1])
call assert_equal(short_state[0], long_state[0])
let states[sop] = [short_state, long_state]
endfor
let short_without = states[0][0]
let short_with = states[1][0]
" Environment/layout can shift direction of movement; require only that
" scrolloffpad changes the short-window viewport state under EOF filler.
call assert_true(short_with[2] != short_without[2]
\ || short_with[3] != short_without[3]
\ || short_with[4] != short_without[4])
windo diffoff
call assert_true(win_gotoid(short_wid))
only!
%bwipe!
let &scrolloff = save_so
let &scrolloffpad = save_sop
let &diffopt = save_diffopt
let &splitright = save_splitright
endfunc
func Test_scrolloffpad_with_folds()
CheckScreendump
CheckRunVimInTerminal
CheckFeature folding
let save_termwinsize = &termwinsize
set termwinsize=
let lines =<< trim END
set scrolloff=10
set scrolloffpad=1
enew
call setline(1, map(range(1, 120), {_, v -> 'line ' . v}))
" Create a large fold near the end of the file.
" Fold lines 60-110, leaving 111-120 visible after the fold.
set foldmethod=manual
set foldenable
normal! gg
normal! 60G
normal! zf50j
normal! gg
END
call writefile(lines, 'XScrolloffpadFolds', 'D')
let buf = RunVimInTerminal('-S XScrolloffpadFolds', #{rows: 20, cols: 78})
" Case 1: Jump to end-of-file
" With folds present, scrolloffpad should still
" keep the cursor positioned with padding below EOF
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_1', {})
" Case 2: Move to the folded line to ensure the fold is actually in view
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! 60G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_2', {})
" Case 3: Close the fold explicitly and go to EOF again
" Behavior should remain stable with closed folds
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! zc\<CR>")
call term_sendkeys(buf, "\<Esc>:\<C-U>normal! G\<CR>")
call term_sendkeys(buf, "\<C-L>")
call TermWait(buf)
call VerifyScreenDump(buf, 'Test_scrolloffpad_folds_3', {})
call StopVimInTerminal(buf)
let &termwinsize = save_termwinsize
endfunc
" Resizing to "textoff" after 'smoothscroll' skips part of a wrapped line must
" not crash.