patch 9.1.0116: win_split_ins may not check available room

Problem:  win_split_ins has no check for E36 when moving an existing
          window
Solution: check for room and fix the issues in f_win_splitmove()
          (Sean Dewar)

win_split_ins has no check for E36 when moving an existing window,
allowing for layouts with many overlapping zero-sized windows to be
created (which may also cause drawing issues with tablines and such).
f_win_splitmove also has some bugs.

So check for room and fix the issues in f_win_splitmove. Handle failure
in the two relevant win_split_ins callers by restoring the original
layout, and factor the common logic into win_splitmove.

Don't check for room when opening an autocommand window, as it's a
temporary window that's rarely interacted with or drawn anyhow, and is
rather important for some autocommands.

Issues fixed in f_win_splitmove:
- Error if splitting is disallowed.
- Fix heap-use-after-frees if autocommands fired from switching to "targetwin"
  close "wp" or "oldwin".
- Fix splitting the wrong window if autocommands fired from switching to
  "targetwin" switch to a different window.
- Ensure -1 is returned for all errors.

Also handle allocation failure a bit earlier in make_snapshot (callers,
except win_splitmove, don't really care if a snapshot can't be made, so
just ignore the return value).

Note: Test_smoothscroll_in_zero_width_window failed after these changes with
E36, as it was using the previous behaviour to create a zero-width window.
I've fixed the test such that it fails with UBSAN as expected when v9.0.1367 is
reverted (and simplified it too).

related: #14042

Signed-off-by: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/testdir/test_window_cmd.vim b/src/testdir/test_window_cmd.vim
index 6b7dccb..c898a23 100644
--- a/src/testdir/test_window_cmd.vim
+++ b/src/testdir/test_window_cmd.vim
@@ -290,6 +290,16 @@
   for s in range(1, hor_split_count) | split | endfor
   call assert_fails('split', 'E36:')
 
+  botright vsplit
+  wincmd |
+  let layout = winlayout()
+  let restcmd = winrestcmd()
+  call assert_fails('wincmd J', 'E36:')
+  call assert_fails('wincmd K', 'E36:')
+  call assert_equal(layout, winlayout())
+  call assert_equal(restcmd, winrestcmd())
+  only
+
   " N vertical windows need >= 2*(N - 1) + 1 columns:
   " - 1 column + 1 separator for each window (except last window)
   " - 1 column for the last window which does not have separator
@@ -302,7 +312,35 @@
   for s in range(1, ver_split_count) | vsplit | endfor
   call assert_fails('vsplit', 'E36:')
 
+  split
+  wincmd |
+  let layout = winlayout()
+  let restcmd = winrestcmd()
+  call assert_fails('wincmd H', 'E36:')
+  call assert_fails('wincmd L', 'E36:')
+  call assert_equal(layout, winlayout())
+  call assert_equal(restcmd, winrestcmd())
+
+  " Check that the last statusline isn't lost.
+  set laststatus=0
+  let restcmd = winrestcmd()
+  wincmd j
+  call setwinvar(winnr('k'), '&statusline', '@#')
+  let last_stl_row = win_screenpos(0)[0] - 1
+  redraw
+  call assert_equal('@#|', GetScreenStr(last_stl_row))
+  call assert_equal('~ |', GetScreenStr(&lines - &cmdheight))
+  call assert_fails('wincmd H', 'E36:')
+  call assert_fails('wincmd L', 'E36:')
+  call assert_equal(layout, winlayout())
+  call assert_equal(restcmd, winrestcmd())
+  call setwinvar(winnr('k'), '&statusline', '=-')
+  redraw
+  call assert_equal('=-|', GetScreenStr(last_stl_row))
+  call assert_equal('~ |', GetScreenStr(&lines - &cmdheight))
+
   %bw!
+  set laststatus&
 endfunc
 
 func Test_window_exchange()
@@ -1097,6 +1135,44 @@
   tabnew
   call assert_fails('call win_splitmove(1, win_getid(1, 1))', 'E957:')
   tabclose
+
+  split
+  augroup WinSplitMove
+    au!
+    au WinEnter * ++once call win_gotoid(win_getid(winnr('#')))
+  augroup END
+  call assert_fails('call win_splitmove(winnr(), winnr("#"))', 'E855:')
+
+  augroup WinSplitMove
+    au!
+    au WinLeave * ++once quit
+  augroup END
+  call assert_fails('call win_splitmove(winnr(), winnr("#"))', 'E855:')
+
+  split
+  split
+  augroup WinSplitMove
+    au!
+    au WinEnter * ++once let s:triggered = v:true
+          \| call assert_fails('call win_splitmove(winnr("$"), winnr())', 'E242:')
+  augroup END
+  quit
+  call assert_equal(v:true, s:triggered)
+  unlet! s:triggered
+
+  new
+  augroup WinSplitMove
+    au!
+    au BufHidden * ++once let s:triggered = v:true
+          \| call assert_fails('call win_splitmove(winnr("#"), winnr())', 'E1159:')
+  augroup END
+  hide
+  call assert_equal(v:true, s:triggered)
+  unlet! s:triggered
+
+  au! WinSplitMove
+  augroup! WinSplitMove
+  %bw!
 endfunc
 
 " Test for the :only command
@@ -2061,23 +2137,75 @@
 endfunc
 
 func Test_smoothscroll_in_zero_width_window()
-  let save_lines = &lines
-  let save_columns = &columns
+  set cpo+=n number smoothscroll
+  set winwidth=99999 winminwidth=0
 
-  winsize 0 24
-  set cpo+=n
-  exe "noremap 0 \<C-W>n\<C-W>L"
-  norm 000000
-  set number smoothscroll
-  exe "norm \<C-Y>"
+  vsplit
+  call assert_equal(0, winwidth(winnr('#')))
+  call win_execute(win_getid(winnr('#')), "norm! \<C-Y>")
 
   only!
-  let &lines = save_lines
-  let &columns = save_columns
-  set cpo-=n
-  unmap 0
-  set nonumber nosmoothscroll
+  set winwidth& winminwidth&
+  set cpo-=n nonumber nosmoothscroll
 endfunc
 
+func Test_splitmove_flatten_frame()
+  split
+  vsplit
+
+  wincmd L
+  let layout = winlayout()
+  wincmd K
+  wincmd L
+  call assert_equal(winlayout(), layout)
+
+  only!
+endfunc
+
+func Test_splitmove_autocmd_window_no_room()
+  " Open as many windows as possible
+  while v:true
+    try
+      split
+    catch /E36:/
+      break
+    endtry
+  endwhile
+  while v:true
+    try
+      vsplit
+    catch /E36:/
+      break
+    endtry
+  endwhile
+
+  wincmd j
+  vsplit
+  call assert_fails('wincmd H', 'E36:')
+  call assert_fails('wincmd J', 'E36:')
+  call assert_fails('wincmd K', 'E36:')
+  call assert_fails('wincmd L', 'E36:')
+
+  edit unload me
+  enew
+  bunload! unload\ me
+  augroup SplitMoveAucmdWin
+    au!
+    au BufEnter * ++once let s:triggered = v:true
+                      \| call assert_equal('autocmd', win_gettype())
+  augroup END
+  let layout = winlayout()
+  let restcmd = winrestcmd()
+  " bufload opening the autocommand window shouldn't give E36.
+  call bufload('unload me')
+  call assert_equal(v:true, s:triggered)
+  call assert_equal(winlayout(), layout)
+  call assert_equal(winrestcmd(), restcmd)
+
+  unlet! s:triggered
+  au! SplitMoveAucmdWin
+  augroup! SplitMoveAucmdWin
+  %bw!
+endfunc
 
 " vim: shiftwidth=2 sts=2 expandtab