changeset 30219:bc8ad1c28b51

patch 9.0.0445: when opening/closing window text moves up/down Commit: https://github.com/vim/vim/commit/29ab524358ba429bcf6811710afc97a978641f0b Author: Luuk van Baal <luukvbaal@gmail.com> Date: Sun Sep 11 16:59:53 2022 +0100 patch 9.0.0445: when opening/closing window text moves up/down Problem: When opening/closing window text moves up/down. Solution: Add the 'splitscroll' option. When off text will keep its position as much as possible.
author Bram Moolenaar <Bram@vim.org>
date Sun, 11 Sep 2022 18:00:03 +0200
parents 759e9dfee593
children 088a3715ea69
files runtime/doc/options.txt runtime/doc/quickref.txt runtime/optwin.vim src/move.c src/option.h src/optiondefs.h src/structs.h src/testdir/test_window_cmd.vim src/version.c src/window.c
diffstat 10 files changed, 288 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -7499,6 +7499,18 @@ A jump table for the options with a shor
 	When on, splitting a window will put the new window right of the
 	current one. |:vsplit|
 
+			*'splitscroll'* *'spsc'* *'nosplitscroll'* *'nospsc'*
+'splitscroll' 'spsc'	boolean	(default on)
+			global
+	The value of this option determines the scroll behavior when opening,
+	closing or resizing horizontal splits. When "on", splitting a window
+	horizontally will keep the same relative cursor position in the old and
+	new window, as well windows that are resized. When "off", scrolling
+	will be avoided to stabilize the window content. Instead, the cursor
+	position will be changed when necessary. In this case, the jumplist
+	will be populated with the previous cursor position. Scrolling cannot
+	be guaranteed to be avoided when 'wrap' is enabled.
+
 			   *'startofline'* *'sol'* *'nostartofline'* *'nosol'*
 'startofline' 'sol'	boolean	(default on)
 			global
--- a/runtime/doc/quickref.txt
+++ b/runtime/doc/quickref.txt
@@ -919,6 +919,7 @@ Short explanation of each option:		*opti
 'spellsuggest'	  'sps'     method(s) used to suggest spelling corrections
 'splitbelow'	  'sb'	    new window from split is below the current one
 'splitright'	  'spr'     new window is put right of the current one
+'splitscroll'	  'spsc'    determines scroll behavior when splitting windows
 'startofline'	  'sol'     commands move cursor to first non-blank in line
 'statusline'	  'stl'     custom format for the status line
 'suffixes'	  'su'	    suffixes that are ignored with multiple match
--- a/runtime/optwin.vim
+++ b/runtime/optwin.vim
@@ -515,6 +515,8 @@ call <SID>AddOption("splitbelow", gettex
 call <SID>BinOptionG("sb", &sb)
 call <SID>AddOption("splitright", gettext("a new window is put right of the current one"))
 call <SID>BinOptionG("spr", &spr)
+call <SID>AddOption("splitscroll", gettext("determines scroll behavior when spliting windows"))
+call <SID>BinOptionG("spsc", &spsc)
 call <SID>AddOption("scrollbind", gettext("this window scrolls together with other bound windows"))
 call append("$", "\t" .. s:local_to_window)
 call <SID>BinOptionL("scb")
--- a/src/move.c
+++ b/src/move.c
@@ -981,7 +981,8 @@ curs_columns(
     /*
      * First make sure that w_topline is valid (after moving the cursor).
      */
-    update_topline();
+    if (p_spsc)
+	update_topline();
 
     /*
      * Next make sure that w_cline_row is valid.
--- a/src/option.h
+++ b/src/option.h
@@ -924,6 +924,7 @@ EXTERN char_u	*p_spo;		// 'spelloptions'
 EXTERN char_u	*p_sps;		// 'spellsuggest'
 #endif
 EXTERN int	p_spr;		// 'splitright'
+EXTERN int	p_spsc;		// 'splitscroll'
 EXTERN int	p_sol;		// 'startofline'
 EXTERN char_u	*p_su;		// 'suffixes'
 EXTERN char_u	*p_sws;		// 'swapsync'
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -2349,6 +2349,9 @@ static struct vimoption options[] =
     {"splitright",  "spr",  P_BOOL|P_VI_DEF,
 			    (char_u *)&p_spr, PV_NONE,
 			    {(char_u *)FALSE, (char_u *)0L} SCTX_INIT},
+    {"splitscroll", "spsc", P_BOOL,
+			    (char_u *)&p_spsc, PV_NONE,
+			    {(char_u *)TRUE, (char_u *)TRUE} SCTX_INIT},
     {"startofline", "sol",  P_BOOL|P_VI_DEF|P_VIM,
 			    (char_u *)&p_sol, PV_NONE,
 			    {(char_u *)TRUE, (char_u *)0L} SCTX_INIT},
--- a/src/structs.h
+++ b/src/structs.h
@@ -3570,6 +3570,8 @@ struct window_S
     int		w_winrow;	    // first row of window in screen
     int		w_height;	    // number of rows in window, excluding
 				    // status/command/winbar line(s)
+    int		w_prev_winrow;	    // previous winrow used for 'splitscroll'
+    int		w_prev_height;	    // previous height used for 'splitscroll'
 
     int		w_status_height;    // number of status lines (0 or 1)
     int		w_wincol;	    // Leftmost column of window in screen.
--- a/src/testdir/test_window_cmd.vim
+++ b/src/testdir/test_window_cmd.vim
@@ -1631,5 +1631,133 @@ func Test_win_equal_last_status()
   set laststatus&
 endfunc
 
+" Ensure no scrolling happens with 'nosplitscroll' with and without a
+" winbar, tabline, for each possible value of 'laststatus', 'scrolloff',
+" 'equalalways', and regardless of the cursor position.
+func Test_splitscroll_with_splits()
+  set nowrap
+  set nosplitscroll
+  let gui = has("gui_running")
+  inoremap c <cmd>:copen<CR>
+  for winbar in [0, 1]
+    for sb in [0, 1]
+      for ea in [0, 1]
+        for tab in [0, 1]
+          for so in [0, 5]
+            for ls in range(0, 2)
+              for pos in ["H", "M", "L"]
+              let tabline = (gui ? 0 : (tab ? 1 : 0))
+              let winbar_sb = (sb ? winbar : 0)
+              execute 'set scrolloff=' . so
+              execute 'set laststatus=' . ls
+              execute 'set ' . (ea ? 'equalalways' : 'noequalalways')
+              execute 'set ' . (sb ? 'splitbelow' : 'nosplitbelow')
+              execute tab ? 'tabnew' : ''
+              execute winbar ? 'nnoremenu 1.10 WinBar.Test :echo' : ''
+              call setline(1, range(1, 256))
+              execute 'norm gg' . pos
+              " No scroll for vertical split and quit
+              vsplit | quit
+              call assert_equal(1, line("w0"))
+
+              " No scroll for horizontal split
+              split | redraw! | wincmd k
+              call assert_equal(1, line("w0"))
+
+              " No scroll when resizing windows
+              resize +2
+              call assert_equal(1, line("w0"))
+              wincmd j
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+
+              " No scroll when dragging statusline
+              call win_move_statusline(1, -3)
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, 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(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+              set lines-=2
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, 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(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+              wincmd k
+              call assert_equal(1, line("w0"))
+
+              " No scroll in windows split multiple times
+              vsplit | split | 4wincmd w
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+              1wincmd w | quit | wincmd l | split
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+              wincmd j
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+
+              " No scroll in small window
+              2wincmd w | only | 5split | wincmd k
+              call assert_equal(1, line("w0"))
+              wincmd j
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, 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 | split | split | quit
+              call assert_equal(win_screenpos(0)[0] - tabline - winbar_sb, 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(win_screenpos(0)[0] - tabline, line("w0"))
+
+              " No scroll when opening cmdwin
+              only | norm ggLq:
+              call assert_equal(1, line("w0"))
+
+              " Scroll when cursor becomes invalid in insert mode
+              norm Lic
+              wincmd k | only
+              call assert_notequal(1, line("w0"))
+
+              " No scroll when topline not equal to 1
+              execute "norm gg5\<C-e>" | split | wincmd k
+              call assert_equal(6, line("w0"))
+              wincmd j
+              call assert_equal(5 + win_screenpos(0)[0] - tabline - winbar_sb, line("w0"))
+              only
+              endfor
+            endfor
+          endfor
+          tabonly!
+        endfor
+      endfor
+    endfor
+  endfor
+
+  tabnew | tabonly! | %bwipeout!
+  iunmap c
+  set wrap&
+  set scrolloff&
+  set splitbelow&
+  set laststatus&
+  set equalalways&
+  set splitscroll&
+endfunc
 
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    445,
+/**/
     444,
 /**/
     443,
--- a/src/window.c
+++ b/src/window.c
@@ -25,6 +25,8 @@ static frame_T *win_altframe(win_T *win,
 static tabpage_T *alt_tabpage(void);
 static win_T *frame2win(frame_T *frp);
 static int frame_has_win(frame_T *frp, win_T *wp);
+static void win_fix_scroll(int resize);
+static void win_fix_cursor(int normal);
 static void frame_new_height(frame_T *topfrp, int height, int topfirst, int wfh);
 static int frame_fixed_height(frame_T *frp);
 static int frame_fixed_width(frame_T *frp);
@@ -1323,6 +1325,8 @@ win_split_ins(
 	win_equal(wp, TRUE,
 		(flags & WSP_VERT) ? (dir == 'v' ? 'b' : 'h')
 		: dir == 'h' ? 'b' : 'v');
+    else if (!p_spsc)
+	win_fix_scroll(FALSE);
 
     // Don't change the window height/width to 'winheight' / 'winwidth' if a
     // size was given.
@@ -1407,6 +1411,13 @@ win_init(win_T *newp, win_T *oldp, int f
     newp->w_prevdir = (oldp->w_prevdir == NULL)
 				    ? NULL : vim_strsave(oldp->w_prevdir);
 
+    if (!p_spsc)
+    {
+	newp->w_botline = oldp->w_botline;
+	newp->w_prev_height = oldp->w_height - WINBAR_HEIGHT(oldp);
+	newp->w_prev_winrow = oldp->w_winrow + 2 * WINBAR_HEIGHT(oldp);
+    }
+
     // copy tagstack and folds
     for (i = 0; i < oldp->w_tagstacklen; i++)
     {
@@ -1914,6 +1925,8 @@ win_equal(
     win_equal_rec(next_curwin == NULL ? curwin : next_curwin, current,
 		      topframe, dir, 0, tabline_height(),
 					   (int)Columns, topframe->fr_height);
+    if (!p_spsc)
+	win_fix_scroll(TRUE);
 }
 
 /*
@@ -2725,7 +2738,11 @@ win_close(win_T *win, int free_buf)
 	// only resize that frame.  Otherwise resize all windows.
 	win_equal(curwin, curwin->w_frame->fr_parent == win_frame, dir);
     else
+    {
 	win_comp_pos();
+	if (!p_spsc)
+	    win_fix_scroll(FALSE);
+    }
     if (close_curwin)
     {
 	// Pass WEE_ALLOW_PARSE_MESSAGES to decrement dont_parse_messages
@@ -4912,7 +4929,8 @@ win_enter_ext(win_T *wp, int flags)
 
     // Might need to scroll the old window before switching, e.g., when the
     // cursor was moved.
-    update_topline();
+    if (p_spsc)
+	update_topline();
 
     // may have to copy the buffer options when 'cpo' contains 'S'
     if (wp->w_buffer != curbuf)
@@ -4927,7 +4945,10 @@ win_enter_ext(win_T *wp, int flags)
     check_cursor();
     if (!virtual_active())
 	curwin->w_cursor.coladd = 0;
-    changed_line_abv_curs();	// assume cursor position needs updating
+    if (p_spsc) // assume cursor position needs updating.
+	changed_line_abv_curs();
+    else
+	win_fix_cursor(TRUE);
 
     // Now it is OK to parse messages again, which may be needed in
     // autocommands.
@@ -5458,6 +5479,9 @@ shell_new_rows(void)
     compute_cmdrow();
     curtab->tp_ch_used = p_ch;
 
+    if (!p_spsc)
+	win_fix_scroll(TRUE);
+
 #if 0
     // Disabled: don't want making the screen smaller make a window larger.
     if (p_ea)
@@ -5662,6 +5686,9 @@ win_setheight_win(int height, win_T *win
     msg_row = row;
     msg_col = 0;
 
+    if (!p_spsc)
+	win_fix_scroll(TRUE);
+
     redraw_all_later(UPD_NOT_VALID);
 }
 
@@ -6190,6 +6217,9 @@ win_drag_status_line(win_T *dragwin, int
     p_ch = MAX(Rows - cmdline_row, 1);
     curtab->tp_ch_used = p_ch;
 
+    if (!p_spsc)
+	win_fix_scroll(TRUE);
+
     redraw_all_later(UPD_SOME_VALID);
     showmode();
 }
@@ -6317,6 +6347,97 @@ set_fraction(win_T *wp)
 }
 
 /*
+ * Handle scroll position for 'nosplitscroll'.  Replaces scroll_to_fraction()
+ * call from win_new_height().  Instead we iterate over all windows in a
+ * tabpage and calculate the new scroll/cursor position.
+ * TODO: 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).
+ */
+    static void
+win_fix_scroll(int resize)
+{
+    win_T    *wp;
+    linenr_T lnum;
+
+    FOR_ALL_WINDOWS(wp)
+    {
+	// Skip when window height has not changed or when
+	// buffer has less lines than the window height.
+	if (wp->w_height != wp->w_prev_height
+		&& wp->w_height < wp->w_buffer->b_ml.ml_line_count)
+	{
+	    // Determine botline needed to avoid scrolling and set cursor.
+	    if (wp->w_winrow != wp->w_prev_winrow)
+	    {
+		lnum = wp->w_cursor.lnum;
+		wp->w_cursor.lnum = MIN(wp->w_buffer->b_ml.ml_line_count,
+			wp->w_botline - 1 + (wp->w_prev_height
+			    ? (wp->w_winrow - wp->w_prev_winrow)
+					   + (wp->w_height - wp->w_prev_height)
+			    : -WINBAR_HEIGHT(wp)));
+		// 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;
+	    }
+	    invalidate_botline_win(wp);
+	    validate_botline_win(wp);
+	}
+	wp->w_prev_height = wp->w_height;
+	wp->w_prev_winrow = wp->w_winrow;
+    }
+    // Ensure cursor is valid when not in normal mode or when resized.
+    if (!(get_real_state() & (MODE_NORMAL|MODE_CMDLINE)))
+	win_fix_cursor(FALSE);
+    else if (resize)
+	win_fix_cursor(TRUE);
+}
+
+/*
+ * Make sure the cursor position is valid for 'nosplitscroll'.
+ * 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)
+{
+    int      top = FALSE;
+    win_T    *wp = curwin;
+    long     so = get_scrolloff_value();
+    linenr_T nlnum = 0;
+
+    if (wp->w_buffer->b_ml.ml_line_count < wp->w_height)
+	return;
+
+    so = MIN(wp->w_height / 2, so);
+    // Check if cursor position is above topline or below botline.
+    if (wp->w_cursor.lnum < (wp->w_topline + so) && wp->w_topline != 1)
+	top = nlnum = MIN(wp->w_topline + so, wp->w_buffer->b_ml.ml_line_count);
+    else if (wp->w_cursor.lnum > (wp->w_botline - so - 1)
+	    && (wp->w_botline - wp->w_buffer->b_ml.ml_line_count) != 1)
+	nlnum = MAX(wp->w_botline - so - 1, 1);
+    // If cursor was invalid scroll or change cursor.
+    if (nlnum)
+    {
+	if (normal)
+	{   // Make sure cursor is closer to topline than botline.
+	    if (so == wp->w_height / 2
+			  && nlnum - wp->w_topline > wp->w_botline - 1 - nlnum)
+		nlnum--;
+	    setmark('\'');		// save cursor position
+	    wp->w_cursor.lnum = nlnum;	// change to avoid scrolling
+	    curs_columns(TRUE);		// validate w_wrow
+	}
+	else
+	{   // Ensure cursor stays visible if we are not in normal mode.
+	    wp->w_fraction = top ? 0 : FRACTION_MULT;
+	    scroll_to_fraction(wp, wp->w_prev_height);
+	}
+    }
+}
+
+/*
  * 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
@@ -6336,7 +6457,7 @@ win_new_height(win_T *wp, int height)
 
     if (wp->w_height > 0)
     {
-	if (wp == curwin)
+	if (wp == curwin && p_spsc)
 	    // w_wrow needs to be valid. When setting 'laststatus' this may
 	    // call win_new_height() recursively.
 	    validate_cursor();
@@ -6352,7 +6473,7 @@ win_new_height(win_T *wp, int height)
 
     // There is no point in adjusting the scroll position when exiting.  Some
     // values might be invalid.
-    if (!exiting)
+    if (!exiting && p_spsc)
 	scroll_to_fraction(wp, prev_height);
 }
 
@@ -6466,7 +6587,7 @@ scroll_to_fraction(win_T *wp, int prev_h
 
     if (wp == curwin)
     {
-	if (get_scrolloff_value())
+	if (p_spsc && get_scrolloff_value())
 	    update_topline();
 	curs_columns(FALSE);	// validate w_wrow
     }
@@ -6488,11 +6609,15 @@ win_new_width(win_T *wp, int width)
     wp->w_width = width;
     wp->w_lines_valid = 0;
     changed_line_abv_curs_win(wp);
-    invalidate_botline_win(wp);
-    if (wp == curwin)
-    {
-	update_topline();
-	curs_columns(TRUE);	// validate w_wrow
+    // Handled in win_fix_scroll()
+    if (p_spsc)
+    {
+	invalidate_botline_win(wp);
+	if (wp == curwin)
+	{
+	    update_topline();
+	    curs_columns(TRUE);	// validate w_wrow
+	}
     }
     redraw_win_later(wp, UPD_NOT_VALID);
     wp->w_redr_status = TRUE;