diff options
-rw-r--r-- | runtime/doc/options.txt | 16 | ||||
-rw-r--r-- | runtime/doc/quickref.txt | 1 | ||||
-rw-r--r-- | runtime/optwin.vim | 2 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/edit.c | 149 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 2 | ||||
-rw-r--r-- | src/nvim/globals.h | 7 | ||||
-rw-r--r-- | src/nvim/main.c | 1 | ||||
-rw-r--r-- | src/nvim/move.c | 5 | ||||
-rw-r--r-- | src/nvim/option_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/options.lua | 7 | ||||
-rw-r--r-- | src/nvim/optionstr.c | 5 | ||||
-rw-r--r-- | src/nvim/testdir/test_window_cmd.vim | 256 | ||||
-rw-r--r-- | src/nvim/window.c | 147 | ||||
-rw-r--r-- | test/functional/legacy/window_cmd_spec.lua | 196 |
15 files changed, 725 insertions, 72 deletions
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 065fe1af70..8a629e0c05 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5961,6 +5961,22 @@ A jump table for the options with a short description can be found at |Q_op|. When on, splitting a window will put the new window below the current one. |:split| + *'splitkeep'* *'spk'* +'splitkeep' 'spk' string (default "cursor") + global + The value of this option determines the scroll behavior when opening, + closing or resizing horizontal splits. + + Possible values are: + cursor Keep the same relative cursor position. + screen Keep the text on the same screen line. + topline Keep the topline the same. + + For the "screen" and "topline" values, the cursor position will be + changed when necessary. In this case, the jumplist will be populated + with the previous cursor position. For "screen", the text cannot always + be kept on the same screen line when 'wrap' is enabled. + *'splitright'* *'spr'* *'nosplitright'* *'nospr'* 'splitright' 'spr' boolean (default off) global diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt index bdb0f7447c..5b100c73a9 100644 --- a/runtime/doc/quickref.txt +++ b/runtime/doc/quickref.txt @@ -890,6 +890,7 @@ Short explanation of each option: *option-list* 'spelloptions' 'spo' options for spell checking 'spellsuggest' 'sps' method(s) used to suggest spelling corrections 'splitbelow' 'sb' new window from split is below the current one +'splitkeep' 'spk' determines scroll behavior for split windows 'splitright' 'spr' new window is put right of the current one 'startofline' 'sol' commands move cursor to first non-blank in line 'statusline' 'stl' custom format for the status line diff --git a/runtime/optwin.vim b/runtime/optwin.vim index a6637441ed..5f0bee6be4 100644 --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -511,6 +511,8 @@ call append("$", "\tto a buffer") call <SID>OptionG("swb", &swb) call append("$", "splitbelow\ta new window is put below the current one") call <SID>BinOptionG("sb", &sb) +call append("$", "splitkeep\ta determines scroll behavior for split windows") +call <SID>BinOptionG("spk", &spk) call append("$", "splitright\ta new window is put right of the current one") call <SID>BinOptionG("spr", &spr) call append("$", "scrollbind\tthis window scrolls together with other bound windows") diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 5850763bcb..630e1d14a8 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -1202,6 +1202,8 @@ struct window_S { int w_winrow; // first row of window in screen int w_height; // number of rows in window, excluding // status/command line(s) + int w_prev_winrow; // previous winrow used for 'splitkeep' + int w_prev_height; // previous height used for 'splitkeep' int w_status_height; // number of status lines (0 or 1) int w_winbar_height; // number of window bars (0 or 1) int w_wincol; // Leftmost column of window in screen. diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 781e9e3553..09f20baebf 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -2541,97 +2541,112 @@ int oneleft(void) return OK; } -/// @oaram upd_topline When true: update topline -int cursor_up(long n, int upd_topline) +/// Move the cursor up "n" lines in window "wp". +/// Takes care of closed folds. +/// Returns the new cursor line or zero for failure. +linenr_T cursor_up_inner(win_T *wp, long n) { - linenr_T lnum; + linenr_T lnum = wp->w_cursor.lnum; - if (n > 0) { - lnum = curwin->w_cursor.lnum; - - // This fails if the cursor is already in the first line. - if (lnum <= 1) { - return FAIL; - } - if (n >= lnum) { - lnum = 1; - } else if (hasAnyFolding(curwin)) { - // Count each sequence of folded lines as one logical line. + // This fails if the cursor is already in the first line. + if (lnum <= 1) { + return 0; + } + if (n >= lnum) { + lnum = 1; + } else if (hasAnyFolding(wp)) { + // Count each sequence of folded lines as one logical line. - // go to the start of the current fold - (void)hasFolding(lnum, &lnum, NULL); + // go to the start of the current fold + (void)hasFoldingWin(wp, lnum, &lnum, NULL, true, NULL); - while (n--) { - // move up one line - lnum--; - if (lnum <= 1) { - break; - } - // If we entered a fold, move to the beginning, unless in - // Insert mode or when 'foldopen' contains "all": it will open - // in a moment. - if (n > 0 || !((State & MODE_INSERT) || (fdo_flags & FDO_ALL))) { - (void)hasFolding(lnum, &lnum, NULL); - } + while (n--) { + // move up one line + lnum--; + if (lnum <= 1) { + break; } - if (lnum < 1) { - lnum = 1; + // If we entered a fold, move to the beginning, unless in + // Insert mode or when 'foldopen' contains "all": it will open + // in a moment. + if (n > 0 || !((State & MODE_INSERT) || (fdo_flags & FDO_ALL))) { + (void)hasFoldingWin(wp, lnum, &lnum, NULL, true, NULL); } - } else { - lnum -= (linenr_T)n; } - curwin->w_cursor.lnum = lnum; + if (lnum < 1) { + lnum = 1; + } + } else { + lnum -= (linenr_T)n; + } + + wp->w_cursor.lnum = lnum; + return lnum; +} + +/// @param upd_topline When true: update topline +int cursor_up(long n, int upd_topline) +{ + if (n > 0 && cursor_up_inner(curwin, n) == 0) { + return FAIL; } // try to advance to the column we want to be at coladvance(curwin->w_curswant); if (upd_topline) { - update_topline(curwin); // make sure curwin->w_topline is valid + update_topline(curwin); // make sure curwin->w_topline is valid } return OK; } -/// Cursor down a number of logical lines. -/// -/// @param upd_topline When true: update topline -int cursor_down(long n, int upd_topline) +/// Move the cursor down "n" lines in window "wp". +/// Takes care of closed folds. +/// Returns the new cursor line or zero for failure. +linenr_T cursor_down_inner(win_T *wp, long n) { - linenr_T lnum; - - if (n > 0) { - lnum = curwin->w_cursor.lnum; - // Move to last line of fold, will fail if it's the end-of-file. - (void)hasFolding(lnum, NULL, &lnum); + linenr_T lnum = wp->w_cursor.lnum; + linenr_T line_count = wp->w_buffer->b_ml.ml_line_count; - // This fails if the cursor is already in the last line. - if (lnum >= curbuf->b_ml.ml_line_count) { - return FAIL; - } - if (lnum + n >= curbuf->b_ml.ml_line_count) { - lnum = curbuf->b_ml.ml_line_count; - } else if (hasAnyFolding(curwin)) { - linenr_T last; + // Move to last line of fold, will fail if it's the end-of-file. + (void)hasFoldingWin(wp, lnum, NULL, &lnum, true, NULL); + // This fails if the cursor is already in the last line. + if (lnum >= line_count) { + return FAIL; + } + if (lnum + n >= line_count) { + lnum = line_count; + } else if (hasAnyFolding(wp)) { + linenr_T last; - // count each sequence of folded lines as one logical line - while (n--) { - if (hasFolding(lnum, NULL, &last)) { - lnum = last + 1; - } else { - lnum++; - } - if (lnum >= curbuf->b_ml.ml_line_count) { - break; - } + // count each sequence of folded lines as one logical line + while (n--) { + if (hasFoldingWin(wp, lnum, NULL, &last, true, NULL)) { + lnum = last + 1; + } else { + lnum++; } - if (lnum > curbuf->b_ml.ml_line_count) { - lnum = curbuf->b_ml.ml_line_count; + if (lnum >= line_count) { + break; } - } else { - lnum += (linenr_T)n; } - curwin->w_cursor.lnum = lnum; + if (lnum > line_count) { + lnum = line_count; + } + } else { + lnum += (linenr_T)n; + } + + wp->w_cursor.lnum = lnum; + return lnum; +} + +/// @param upd_topline When true: update topline +int cursor_down(long n, int upd_topline) +{ + if (n > 0 && cursor_down_inner(curwin, n) == 0) { + return FAIL; } // try to advance to the column we want to be at diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 6883dc5b14..031226c5a1 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -4359,6 +4359,7 @@ static int open_cmdwin(void) // First go back to the original window. wp = curwin; set_bufref(&bufref, curbuf); + skip_win_fix_cursor = true; win_goto(old_curwin); // win_goto() may trigger an autocommand that already closes the @@ -4375,6 +4376,7 @@ static int open_cmdwin(void) // Restore window sizes. win_size_restore(&winsizes); + skip_win_fix_cursor = false; } ga_clear(&winsizes); diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 578ed478c6..1a6c639261 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -1071,4 +1071,11 @@ EXTERN char windowsVersion[20] INIT(= { 0 }); EXTERN int exit_need_delay INIT(= 0); +///< Skip win_fix_cursor() call for 'splitkeep' when cmdwin is closed. +EXTERN bool skip_win_fix_cursor INIT(= false); +///< Skip win_fix_scroll() call for 'splitkeep' when closing tab page. +EXTERN bool skip_win_fix_scroll INIT(= false); +///< Skip update_topline() call while executing win_fix_scroll(). +EXTERN bool skip_update_topline INIT(= false); + #endif // NVIM_GLOBALS_H diff --git a/src/nvim/main.c b/src/nvim/main.c index 673b177463..5687e0a6a9 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -346,6 +346,7 @@ int main(int argc, char **argv) ui_builtin_start(); } TIME_MSG("done waiting for UI"); + firstwin->w_prev_height = firstwin->w_height; // may have changed } // prepare screen now diff --git a/src/nvim/move.c b/src/nvim/move.c index 9d1d2e9cad..9b7755a828 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -140,6 +140,11 @@ void update_topline(win_T *wp) long *so_ptr = wp->w_p_so >= 0 ? &wp->w_p_so : &p_so; long save_so = *so_ptr; + // Cursor is updated instead when this is true for 'splitkeep'. + if (skip_update_topline) { + return; + } + // If there is no valid screen and when the window height is zero just use // the cursor line. if (!default_grid.chars || wp->w_height_inner == 0) { diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 4e48c992c2..25ef1fc091 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -729,6 +729,7 @@ EXTERN unsigned int tpf_flags; ///< flags from 'termpastefilter' EXTERN char *p_tfu; ///< 'tagfunc' EXTERN char *p_spc; ///< 'spellcapcheck' EXTERN char *p_spf; ///< 'spellfile' +EXTERN char *p_spk; ///< 'splitkeep' EXTERN char *p_spl; ///< 'spelllang' EXTERN char *p_spo; // 'spelloptions' EXTERN unsigned int spo_flags; diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 82fd0d7428..149d0bace4 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -2357,6 +2357,13 @@ return { defaults={if_true=false} }, { + full_name='splitkeep', abbreviation='spk', + short_desc=N_("determines scroll behavior for split windows"), + type='string', scope={'global'}, + varname='p_spk', + defaults={if_true='cursor'} + }, + { full_name='splitright', abbreviation='spr', short_desc=N_("new window is put right of the current one"), type='bool', scope={'global'}, diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index aff88a755b..1da6fa15d7 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -75,6 +75,7 @@ static char *(p_ssop_values[]) = { "buffers", "winpos", "resize", "winsize", "lo // Keep in sync with SWB_ flags in option_defs.h static char *(p_swb_values[]) = { "useopen", "usetab", "split", "newtab", "vsplit", "uselast", NULL }; +static char *(p_spk_values[]) = { "cursor", "screen", "topline", NULL }; static char *(p_tc_values[]) = { "followic", "ignore", "match", "followscs", "smart", NULL }; static char *(p_ve_values[]) = { "block", "insert", "all", "onemore", "none", "NONE", NULL }; static char *(p_wop_values[]) = { "tagfile", "pum", NULL }; @@ -1095,6 +1096,10 @@ char *did_set_string_option(int opt_idx, char **varp, char *oldval, char *errbuf if (opt_strings_flags(p_swb, p_swb_values, &swb_flags, true) != OK) { errmsg = e_invarg; } + } else if (varp == &p_spk) { // 'splitkeep' + if (check_opt_strings(p_spk, p_spk_values, false) != OK) { + errmsg = e_invarg; + } } else if (varp == &p_debug) { // 'debug' if (check_opt_strings(p_debug, p_debug_values, true) != OK) { errmsg = e_invarg; diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim index b64f44360b..c4ce4d638c 100644 --- a/src/nvim/testdir/test_window_cmd.vim +++ b/src/nvim/testdir/test_window_cmd.vim @@ -1,5 +1,8 @@ " Tests for window cmd (:wincmd, :split, :vsplit, :resize and etc...) +source check.vim +source screendump.vim + func Test_window_cmd_ls0_with_split() set ls=0 set splitbelow @@ -1486,5 +1489,258 @@ func Test_win_equal_last_status() set laststatus& endfunc +" Test "screen" and "cursor" values for 'splitkeep' with a sequence of +" split operations for various options: with and without a winbar, +" tabline, for each possible value of 'laststatus', 'scrolloff', +" 'equalalways', and with the cursor at the top, middle and bottom. +func Test_splitkeep_options() + " disallow window resizing + " let save_WS = &t_WS + " set t_WS= + + let gui = has("gui_running") + inoremap <expr> c "<cmd>copen<bar>wincmd k<CR>" + for run in range(0, 20) + let &splitkeep = run > 10 ? 'topline' : 'screen' + let &scrolloff = (!(run % 4) ? 0 : run) + let &laststatus = (run % 3) + let &splitbelow = (run % 3) + let &equalalways = (run % 2) + " Nvim: both windows have a winbar after splitting + " let wsb = (run % 2) && &splitbelow + let wsb = 0 + let tl = (gui ? 0 : ((run % 5) ? 1 : 0)) + let pos = !(run % 3) ? 'H' : ((run % 2) ? 'M' : 'L') + tabnew | tabonly! | redraw + execute (run % 5) ? 'tabnew' : '' + " execute (run % 2) ? 'nnoremenu 1.10 WinBar.Test :echo' : '' + let &winbar = (run % 2) ? '%f' : '' + call setline(1, range(1, 256)) + " No scroll for restore_snapshot + norm G + try + copen | close | colder + catch /E380/ + endtry + call assert_equal(257 - winheight(0), line("w0")) + + " No scroll for firstwin horizontal split + execute 'norm gg' . pos + split | redraw | wincmd k + call assert_equal(1, line("w0")) + call assert_equal(&scroll, winheight(0) / 2) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + + " No scroll when resizing windows + wincmd k | resize +2 | redraw + call assert_equal(1, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + + " No scroll when dragging statusline + call win_move_statusline(1, -3) + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + wincmd k + call assert_equal(1, line("w0")) + + " No scroll when changing shellsize + set lines+=2 + call assert_equal(1, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + set lines-=2 + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + wincmd k + call assert_equal(1, line("w0")) + + " No scroll when equalizing windows + wincmd = + call assert_equal(1, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + wincmd k + call assert_equal(1, line("w0")) + + " No scroll in windows split multiple times + vsplit | split | 4wincmd w + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + 1wincmd w | quit | wincmd l | split + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + + " No scroll in small window + 2wincmd w | only | 5split | wincmd k + call assert_equal(1, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + + " No scroll for vertical split + quit | vsplit | wincmd l + call assert_equal(1, line("w0")) + wincmd h + call assert_equal(1, line("w0")) + + " No scroll in windows split and quit multiple times + quit | redraw | split | split | quit | redraw + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl - wsb, line("w0")) + + " No scroll for new buffer + 1wincmd w | only | copen | wincmd k + call assert_equal(1, line("w0")) + only + call assert_equal(1, line("w0")) + above copen | wincmd j + call assert_equal(&spk == 'topline' ? 1 : win_screenpos(0)[0] - tl, line("w0")) + + " No scroll when opening cmdwin, and no cursor move when closing cmdwin. + only | norm ggL + let curpos = getcurpos() + norm q: + call assert_equal(1, line("w0")) + call assert_equal(curpos, getcurpos()) + + " Scroll when cursor becomes invalid in insert mode + norm Lic + call assert_equal(curpos, getcurpos()) + + " No scroll when topline not equal to 1 + only | execute "norm gg5\<C-e>" | split | wincmd k + call assert_equal(6, line("w0")) + wincmd j + call assert_equal(&spk == 'topline' ? 6 : 5 + win_screenpos(0)[0] - tl - wsb, line("w0")) + endfor + + tabnew | tabonly! | %bwipeout! + iunmap c + set scrolloff& + set splitbelow& + set laststatus& + set equalalways& + set splitkeep& + " let &t_WS = save_WS +endfunc + +function Test_splitkeep_cmdwin_cursor_position() + set splitkeep=screen + call setline(1, range(&lines)) + + " No scroll when cursor is at near bottom of window and cusor position + " recompution (done by line('w0') in this test) happens while in cmdwin. + normal! G + let firstline = line('w0') + autocmd CmdwinEnter * ++once autocmd WinEnter * ++once call line('w0') + execute "normal! q:\<C-w>q" + redraw! + call assert_equal(firstline, line('w0')) + + " User script can change cursor position successfully while in cmdwin and it + " shouldn't be changed when closing cmdwin. + execute "normal! Gq:\<Cmd>call win_execute(winnr('#')->win_getid(), 'call cursor(1, 1)')\<CR>\<C-w>q" + call assert_equal(1, line('.')) + call assert_equal(1, col('.')) + + execute "normal! Gq:\<Cmd>autocmd WinEnter * ++once call cursor(1, 1)\<CR>\<C-w>q" + call assert_equal(1, line('.')) + call assert_equal(1, col('.')) + + %bwipeout! + set splitkeep& +endfunction + +function Test_splitkeep_misc() + set splitkeep=screen + set splitbelow + + call setline(1, range(1, &lines)) + norm Gzz + let top = line('w0') + " No scroll when aucmd_win is opened + call setbufvar(bufnr("test", 1) , '&buftype', 'nofile') + call assert_equal(top, line('w0')) + " No scroll when tab is changed/closed + tab help | close + call assert_equal(top, line('w0')) + " No scroll when help is closed and buffer line count < window height + norm ggdG + call setline(1, range(1, &lines - 10)) + norm G + let top = line('w0') + help | quit + call assert_equal(top, line('w0')) + " No error when resizing window in autocmd and buffer length changed + autocmd FileType qf exe "resize" line('$') + cexpr getline(1, '$') + copen + wincmd p + norm dd + cexpr getline(1, '$') + + %bwipeout! + set splitbelow& + set splitkeep& +endfunc + +function Test_splitkeep_callback() + CheckScreendump + let lines =<< trim END + set splitkeep=screen + call setline(1, range(&lines)) + function C1(a, b) + split | wincmd p + endfunction + function C2(a, b) + close | split + endfunction + nn j <cmd>call job_start([&sh, &shcf, "true"], { 'exit_cb': 'C1' })<CR> + nn t <cmd>call popup_create(term_start([&sh, &shcf, "true"], + \ { 'hidden': 1, 'exit_cb': 'C2' }), {})<CR> + END + call writefile(lines, 'XTestSplitkeepCallback', 'D') + let buf = RunVimInTerminal('-S XTestSplitkeepCallback', #{rows: 8}) + + call term_sendkeys(buf, "j") + call VerifyScreenDump(buf, 'Test_splitkeep_callback_1', {}) + + call term_sendkeys(buf, ":quit\<CR>Ht") + call VerifyScreenDump(buf, 'Test_splitkeep_callback_2', {}) + + call term_sendkeys(buf, ":set sb\<CR>:quit\<CR>Gj") + call VerifyScreenDump(buf, 'Test_splitkeep_callback_3', {}) + + call term_sendkeys(buf, ":quit\<CR>Gt") + call VerifyScreenDump(buf, 'Test_splitkeep_callback_4', {}) +endfunc + +function Test_splitkeep_fold() + CheckScreendump + + let lines =<< trim END + set splitkeep=screen + set foldmethod=marker + set number + let line = 1 + for n in range(1, &lines) + call setline(line, ['int FuncName() {/*{{{*/', 1, 2, 3, 4, 5, '}/*}}}*/', + \ 'after fold']) + let line += 8 + endfor + END + call writefile(lines, 'XTestSplitkeepFold', 'D') + let buf = RunVimInTerminal('-S XTestSplitkeepFold', #{rows: 10}) + + call term_sendkeys(buf, "L:wincmd s\<CR>") + call VerifyScreenDump(buf, 'Test_splitkeep_fold_1', {}) + + call term_sendkeys(buf, ":quit\<CR>") + call VerifyScreenDump(buf, 'Test_splitkeep_fold_2', {}) + + call term_sendkeys(buf, "H:below split\<CR>") + call VerifyScreenDump(buf, 'Test_splitkeep_fold_3', {}) + + call term_sendkeys(buf, ":wincmd k\<CR>:quit\<CR>") + call VerifyScreenDump(buf, 'Test_splitkeep_fold_4', {}) +endfunction " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/window.c b/src/nvim/window.c index 242ff1fb12..ecd713013f 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1487,6 +1487,8 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir) // equalize the window sizes. if (do_equal || dir != 0) { win_equal(wp, true, (flags & WSP_VERT) ? (dir == 'v' ? 'b' : 'h') : (dir == 'h' ? 'b' : 'v')); + } else if (*p_spk != 'c' && wp != aucmd_win) { + win_fix_scroll(false); } // Don't change the window height/width to 'winheight' / 'winwidth' if a @@ -1558,6 +1560,12 @@ static void win_init(win_T *newp, win_T *oldp, int flags) newp->w_prevdir = (oldp->w_prevdir == NULL) ? NULL : xstrdup(oldp->w_prevdir); + if (*p_spk != 'c') { + newp->w_botline = oldp->w_botline; + newp->w_prev_height = oldp->w_height; + newp->w_prev_winrow = oldp->w_winrow; + } + // copy tagstack and folds for (i = 0; i < oldp->w_tagstacklen; i++) { taggy_T *tag = &newp->w_tagstack[i]; @@ -2066,6 +2074,9 @@ void win_equal(win_T *next_curwin, bool current, int dir) win_equal_rec(next_curwin == NULL ? curwin : next_curwin, current, topframe, dir, 0, tabline_height(), Columns, topframe->fr_height); + if (*p_spk != 'c' && next_curwin != aucmd_win) { + win_fix_scroll(true); + } } /// Set a frame to a new position and height, spreading the available room @@ -2858,6 +2869,9 @@ int win_close(win_T *win, bool free_buf, bool force) win_equal(curwin, curwin->w_frame->fr_parent == win_frame, dir); } else { (void)win_comp_pos(); + if (*p_spk != 'c') { + win_fix_scroll(false); + } } } @@ -3944,6 +3958,7 @@ static void new_frame(win_T *wp) void win_init_size(void) { firstwin->w_height = (int)ROWS_AVAIL; + firstwin->w_prev_height = (int)ROWS_AVAIL; firstwin->w_height_inner = firstwin->w_height - firstwin->w_winbar_height; firstwin->w_height_outer = firstwin->w_height; firstwin->w_winrow_off = firstwin->w_winbar_height; @@ -4050,6 +4065,7 @@ int win_new_tabpage(int after, char_u *filename) win_init_size(); firstwin->w_winrow = tabline_height(); + firstwin->w_prev_winrow = firstwin->w_winrow; win_comp_scroll(curwin); newtp->tp_topframe = topframe; @@ -4407,6 +4423,7 @@ void goto_tabpage_tp(tabpage_T *tp, bool trigger_enter_autocmds, bool trigger_le // Don't repeat a message in another tab page. set_keep_msg(NULL, 0); + skip_win_fix_scroll = true; if (tp != curtab && leave_tabpage(tp->tp_curwin->w_buffer, trigger_leave_autocmds) == OK) { if (valid_tabpage(tp)) { @@ -4417,6 +4434,7 @@ void goto_tabpage_tp(tabpage_T *tp, bool trigger_enter_autocmds, bool trigger_le trigger_leave_autocmds); } } + skip_win_fix_scroll = false; } /// Go to the last accessed tab page, if there is one. @@ -4747,7 +4765,9 @@ static void win_enter_ext(win_T *const wp, const int flags) // Might need to scroll the old window before switching, e.g., when the // cursor was moved. - update_topline(curwin); + if (*p_spk == 'c') { + update_topline(curwin); + } // may have to copy the buffer options when 'cpo' contains 'S' if (wp->w_buffer != curbuf) { @@ -4764,7 +4784,11 @@ static void win_enter_ext(win_T *const wp, const int flags) if (!virtual_active()) { curwin->w_cursor.coladd = 0; } - changed_line_abv_curs(); // assume cursor position needs updating + if (*p_spk == 'c') { + changed_line_abv_curs(); // assume cursor position needs updating + } else { + win_fix_cursor(true); + } fix_current_dir(); @@ -5238,6 +5262,10 @@ void win_new_screen_rows(void) win_reconfig_floats(); // The size of floats might change compute_cmdrow(); curtab->tp_ch_used = p_ch; + + if (*p_spk != 'c' && !skip_win_fix_scroll) { + win_fix_scroll(true); + } } /// Called from win_new_screensize() after Columns changed. @@ -5435,6 +5463,11 @@ void win_setheight_win(int height, win_T *win) curtab->tp_ch_used = p_ch; msg_row = row; msg_col = 0; + + if (*p_spk != 'c') { + win_fix_scroll(true); + } + redraw_all_later(UPD_NOT_VALID); redraw_cmdline = true; } @@ -5921,6 +5954,11 @@ void win_drag_status_line(win_T *dragwin, int offset) cmdline_row = row; p_ch = MAX(Rows - cmdline_row, p_ch_was_zero ? 0 : 1); curtab->tp_ch_used = p_ch; + + if (*p_spk != 'c') { + win_fix_scroll(true); + } + redraw_all_later(UPD_SOME_VALID); showmode(); } @@ -6043,6 +6081,99 @@ void set_fraction(win_T *wp) } } +/// Handle scroll position for 'splitkeep'. Replaces scroll_to_fraction() +/// call from win_set_inner_size(). Instead we iterate over all windows in a +/// tabpage and calculate the new scroll position. +/// TODO(luukvbaal): Ensure this also works with wrapped lines. +/// Requires topline to be able to be set to a bufferline with some +/// offset(row-wise scrolling/smoothscroll). +void win_fix_scroll(int resize) +{ + linenr_T lnum; + + skip_update_topline = true; + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + // Skip when window height has not changed or when floating. + if (!wp->w_floating && wp->w_height != wp->w_prev_height) { + // If window has moved update botline to keep the same screenlines. + if (*p_spk == 's' && wp->w_winrow != wp->w_prev_winrow + && wp->w_botline - 1 <= wp->w_buffer->b_ml.ml_line_count) { + lnum = wp->w_cursor.lnum; + int diff = (wp->w_winrow - wp->w_prev_winrow) + + (wp->w_height - wp->w_prev_height); + wp->w_cursor.lnum = wp->w_botline - 1; + // Add difference in height and row to botline. + if (diff > 0) { + cursor_down_inner(wp, diff); + } else { + cursor_up_inner(wp, -diff); + } + // Bring the new cursor position to the bottom of the screen. + wp->w_fraction = FRACTION_MULT; + scroll_to_fraction(wp, wp->w_prev_height); + wp->w_cursor.lnum = lnum; + } else if (wp == curwin) { + wp->w_valid &= ~VALID_CROW; + } + invalidate_botline_win(wp); + validate_botline(wp); + } + win_comp_scroll(wp); + wp->w_prev_height = wp->w_height; + wp->w_prev_winrow = wp->w_winrow; + } + skip_update_topline = false; + // Ensure cursor is valid when not in normal mode or when resized. + if (!(get_real_state() & (MODE_NORMAL|MODE_CMDLINE|MODE_TERMINAL))) { + win_fix_cursor(false); + } else if (resize) { + win_fix_cursor(true); + } +} + +/// Make sure the cursor position is valid for 'splitkeep'. +/// If it is not, put the cursor position in the jumplist and move it. +/// If we are not in normal mode, scroll to make valid instead. +static void win_fix_cursor(int normal) +{ + win_T *wp = curwin; + long so = get_scrolloff_value(wp); + linenr_T nlnum = 0; + linenr_T lnum = wp->w_cursor.lnum; + + if (wp->w_buffer->b_ml.ml_line_count < wp->w_height + || skip_win_fix_cursor) { + return; + } + + // Determine valid cursor range. + so = MIN(wp->w_height_inner / 2, so); + wp->w_cursor.lnum = wp->w_topline; + linenr_T top = cursor_down_inner(wp, so); + wp->w_cursor.lnum = wp->w_botline - 1; + linenr_T bot = cursor_up_inner(wp, so); + wp->w_cursor.lnum = lnum; + // Check if cursor position is above or below valid cursor range. + if (lnum > bot && (wp->w_botline - wp->w_buffer->b_ml.ml_line_count) != 1) { + nlnum = bot; + } else if (lnum < top && wp->w_topline != 1) { + nlnum = (so == wp->w_height / 2) ? bot : top; + } + + if (nlnum) { // Cursor is invalid for current scroll position. + if (normal) { + // Save to jumplist and set cursor to avoid scrolling. + setmark('\''); + wp->w_cursor.lnum = nlnum; + } else { + // Scroll instead when not in normal mode. + wp->w_fraction = (nlnum == bot) ? FRACTION_MULT : 0; + scroll_to_fraction(wp, wp->w_prev_height); + validate_botline(curwin); + } + } +} + // Set the height of a window. // "height" excludes any window toolbar. // This takes care of the things inside the window, not what happens to the @@ -6181,7 +6312,7 @@ void win_set_inner_size(win_T *wp, bool valid_cursor) if (height != prev_height) { if (height > 0 && valid_cursor) { - if (wp == curwin) { + if (wp == curwin && *p_spk == 'c') { // w_wrow needs to be valid. When setting 'laststatus' this may // call win_new_height() recursively. validate_cursor(); @@ -6198,7 +6329,7 @@ void win_set_inner_size(win_T *wp, bool valid_cursor) // There is no point in adjusting the scroll position when exiting. Some // values might be invalid. - if (!exiting && valid_cursor) { + if (valid_cursor && !exiting && *p_spk == 'c') { scroll_to_fraction(wp, prev_height); } redraw_later(wp, UPD_SOME_VALID); @@ -6211,8 +6342,10 @@ void win_set_inner_size(win_T *wp, bool valid_cursor) changed_line_abv_curs_win(wp); invalidate_botline_win(wp); if (wp == curwin) { + skip_update_topline = (*p_spk != 'c'); update_topline(wp); curs_columns(wp, true); // validate w_wrow + skip_update_topline = false; } } redraw_later(wp, UPD_NOT_VALID); @@ -6252,7 +6385,7 @@ void win_comp_scroll(win_T *wp) { const long old_w_p_scr = wp->w_p_scr; - wp->w_p_scr = wp->w_height / 2; + wp->w_p_scr = wp->w_height_inner / 2; if (wp->w_p_scr == 0) { wp->w_p_scr = 1; } @@ -6606,6 +6739,10 @@ static void last_status_rec(frame_T *fr, bool statusline, bool is_stl_global) } comp_col(); } + // Set prev_height when difference is due to 'laststatus'. + if (abs(wp->w_height - wp->w_prev_height) == 1) { + wp->w_prev_height = wp->w_height; + } } else if (wp->w_status_height != 0 && is_stl_global) { // If statusline is global and the window has a statusline, replace it with a horizontal // separator diff --git a/test/functional/legacy/window_cmd_spec.lua b/test/functional/legacy/window_cmd_spec.lua new file mode 100644 index 0000000000..8b89c55f5b --- /dev/null +++ b/test/functional/legacy/window_cmd_spec.lua @@ -0,0 +1,196 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear = helpers.clear +local exec = helpers.exec +local exec_lua = helpers.exec_lua +local feed = helpers.feed + +describe('splitkeep', function() + local screen = Screen.new() + before_each(function() + clear('--cmd', 'set splitkeep=screen') + screen:attach() + end) + + -- oldtest: Test_splitkeep_callback() + it('does not scroll when split in callback', function() + exec([[ + call setline(1, range(&lines)) + function C1(a, b, c) + split | wincmd p + endfunction + function C2(a, b, c) + close | split + endfunction + ]]) + exec_lua([[ + vim.api.nvim_set_keymap("n", "j", "", { callback = function() + vim.cmd("call jobstart([&sh, &shcf, 'true'], { 'on_exit': 'C1' })") + end + })]]) + exec_lua([[ + vim.api.nvim_set_keymap("n", "t", "", { callback = function() + vim.api.nvim_set_current_win( + vim.api.nvim_open_win(vim.api.nvim_create_buf(false, {}), false, { + width = 10, + relative = "cursor", + height = 4, + row = 0, + col = 0, + })) + vim.cmd("call termopen([&sh, &shcf, 'true'], { 'on_exit': 'C2' })") + end + })]]) + feed('j') + screen:expect([[ + 0 | + 1 | + 2 | + 3 | + 4 | + 5 | + [No Name] [+] | + ^7 | + 8 | + 9 | + 10 | + 11 | + [No Name] [+] | + | + ]]) + feed(':quit<CR>Ht') + screen:expect([[ + ^0 | + 1 | + 2 | + 3 | + 4 | + 5 | + [No Name] [+] | + 7 | + 8 | + 9 | + 10 | + 11 | + [No Name] [+] | + :quit | + ]]) + feed(':set sb<CR>:quit<CR>Gj') + screen:expect([[ + 1 | + 2 | + 3 | + 4 | + ^5 | + [No Name] [+] | + 7 | + 8 | + 9 | + 10 | + 11 | + 12 | + [No Name] [+] | + :quit | + ]]) + feed(':quit<CR>Gt') + screen:expect([[ + 1 | + 2 | + 3 | + 4 | + 5 | + [No Name] [+] | + 7 | + 8 | + 9 | + 10 | + 11 | + ^12 | + [No Name] [+] | + :quit | + ]]) + end) + + -- oldtest: Test_splitkeep_fold() + it('does not scroll when window has closed folds', function() + exec([[ + set splitkeep=screen + set foldmethod=marker + set number + let line = 1 + for n in range(1, &lines) + call setline(line, ['int FuncName() {/*{{{*/', 1, 2, 3, 4, 5, '}/*}}}*/', + \ 'after fold']) + let line += 8 + endfor + ]]) + feed('L:wincmd s<CR>') + screen:expect([[ + 1 +-- 7 lines: int FuncName() {···················| + 8 after fold | + 9 +-- 7 lines: int FuncName() {···················| + 16 after fold | + 17 +-- 7 lines: int FuncName() {···················| + 24 ^after fold | + [No Name] [+] | + 32 after fold | + 33 +-- 7 lines: int FuncName() {···················| + 40 after fold | + 41 +-- 7 lines: int FuncName() {···················| + 48 after fold | + [No Name] [+] | + :wincmd s | + ]]) + feed(':quit<CR>') + screen:expect([[ + 1 +-- 7 lines: int FuncName() {···················| + 8 after fold | + 9 +-- 7 lines: int FuncName() {···················| + 16 after fold | + 17 +-- 7 lines: int FuncName() {···················| + 24 after fold | + 25 +-- 7 lines: int FuncName() {···················| + 32 after fold | + 33 +-- 7 lines: int FuncName() {···················| + 40 after fold | + 41 +-- 7 lines: int FuncName() {···················| + 48 after fold | + 49 ^+-- 7 lines: int FuncName() {···················| + :quit | + ]]) + feed('H:below split<CR>') + screen:expect([[ + 1 +-- 7 lines: int FuncName() {···················| + 8 after fold | + 9 +-- 7 lines: int FuncName() {···················| + 16 after fold | + 17 +-- 7 lines: int FuncName() {···················| + [No Name] [+] | + 25 ^+-- 7 lines: int FuncName() {···················| + 32 after fold | + 33 +-- 7 lines: int FuncName() {···················| + 40 after fold | + 41 +-- 7 lines: int FuncName() {···················| + 48 after fold | + [No Name] [+] | + :below split | + ]]) + feed(':wincmd k<CR>:quit<CR>') + screen:expect([[ + 1 +-- 7 lines: int FuncName() {···················| + 8 after fold | + 9 +-- 7 lines: int FuncName() {···················| + 16 after fold | + 17 +-- 7 lines: int FuncName() {···················| + 24 after fold | + 25 ^+-- 7 lines: int FuncName() {···················| + 32 after fold | + 33 +-- 7 lines: int FuncName() {···················| + 40 after fold | + 41 +-- 7 lines: int FuncName() {···················| + 48 after fold | + 49 +-- 7 lines: int FuncName() {···················| + :quit | + ]]) + end) +end) |