commit dde914f9260b08272f0914908717bbd6fc417699
parent 4f44705a479d7194ec1a1c0efec7bd31a91acbb2
Author: zeertzjq <zeertzjq@outlook.com>
Date: Fri, 16 Jan 2026 16:18:02 +0800
Merge pull request #37422 from zeertzjq/vim-9.1.0059
vim-patch:9.1.{0059,0117,0671,1043,partial:1110}: WinNewPre
Diffstat:
10 files changed, 165 insertions(+), 30 deletions(-)
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
@@ -1232,6 +1232,20 @@ WinLeave Before leaving a window. If the window to be
WinLeave autocommands (but not for ":new").
Not used for ":qa" or ":q" when exiting Vim.
Before WinClosed.
+ *WinNewPre*
+WinNewPre Before creating a new window. Triggered
+ before commands that modify window layout by
+ creating a split.
+ Not done when creating tab pages and for the
+ first window, as the window structure is not
+ initialized yet and so is generally not safe.
+ It is not allowed to modify window layout
+ while executing commands for the WinNewPre
+ event.
+ Most useful to store current window layout
+ and compare it with the new layout after the
+ Window has been created.
+
*WinNew*
WinNew When a new window was created. Not done for
the first window, when Vim has just started.
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -2580,7 +2580,8 @@ A jump table for the options with a short description can be found at |Q_op|.
|VimResized|,
|VimResume|,
|VimSuspend|,
- |WinNew|
+ |WinNew|,
+ |WinNewPre|
*'expandtab'* *'et'* *'noexpandtab'* *'noet'*
'expandtab' 'et' boolean (default off)
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
@@ -218,6 +218,7 @@ error('Cannot require a meta file')
--- |'WinEnter'
--- |'WinLeave'
--- |'WinNew'
+--- |'WinNewPre'
--- |'WinResized'
--- |'WinScrolled'
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -2258,7 +2258,8 @@ vim.go.ei = vim.go.eventignore
--- `VimResized`,
--- `VimResume`,
--- `VimSuspend`,
---- `WinNew`
+--- `WinNew`,
+--- `WinNewPre`
---
--- @type string
vim.o.eventignorewin = ""
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
@@ -138,7 +138,8 @@ return {
WinClosed = true, -- after closing a window
WinEnter = true, -- after entering a window
WinLeave = true, -- before leaving a window
- WinNew = false, -- when entering a new window
+ WinNewPre = false, -- before creating a new window
+ WinNew = false, -- after creating a new window
WinResized = true, -- after a window was resized
WinScrolled = true, -- after a window was scrolled or resized
},
diff --git a/src/nvim/window.c b/src/nvim/window.c
@@ -1127,6 +1127,10 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir, frame_T *to_fl
return NULL;
}
+ if (new_wp == NULL) {
+ trigger_winnewpre();
+ }
+
win_T *oldwin;
if (flags & WSP_TOP) {
oldwin = firstwin;
@@ -3069,6 +3073,13 @@ int win_close(win_T *win, bool free_buf, bool force)
return OK;
}
+static void trigger_winnewpre(void)
+{
+ window_layout_lock();
+ apply_autocmds(EVENT_WINNEWPRE, NULL, NULL, false, NULL);
+ window_layout_unlock();
+}
+
static void do_autocmd_winclosed(win_T *win)
FUNC_ATTR_NONNULL_ALL
{
@@ -4351,6 +4362,10 @@ void free_tabpage(tabpage_T *tp)
///
/// It will edit the current buffer, like after :split.
///
+/// Does not trigger WinNewPre, since the window structures
+/// are not completely setup yet and could cause dereferencing
+/// NULL pointers
+///
/// @param after Put new tabpage after tabpage "after", or after the current
/// tabpage in case of 0.
/// @param filename Will be passed to apply_autocmds().
diff --git a/test/old/testdir/crash/nullpointer b/test/old/testdir/crash/nullpointer
Binary files differ.
diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim
@@ -276,7 +276,9 @@ endfunc
func Test_win_tab_autocmd()
let g:record = []
+ defer CleanUpTestAuGroup()
augroup testing
+ au WinNewPre * call add(g:record, 'WinNewPre')
au WinNew * call add(g:record, 'WinNew')
au WinClosed * call add(g:record, 'WinClosed')
au WinEnter * call add(g:record, 'WinEnter')
@@ -293,7 +295,7 @@ func Test_win_tab_autocmd()
close
call assert_equal([
- \ 'WinLeave', 'WinNew', 'WinEnter',
+ \ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
\ 'WinLeave', 'TabLeave', 'WinNew', 'WinEnter', 'TabNew', 'TabEnter',
\ 'WinLeave', 'TabLeave', 'WinClosed', 'TabClosed', 'WinEnter', 'TabEnter',
\ 'WinLeave', 'WinClosed', 'WinEnter'
@@ -310,10 +312,81 @@ func Test_win_tab_autocmd()
\ 'WinClosed', 'TabClosed'
\ ], g:record)
+ let g:record = []
+ copen
+ help
+ tabnext
+ vnew
+
+ call assert_equal([
+ \ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
+ \ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
+ \ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter'
+ \ ], g:record)
+
+ unlet g:record
+endfunc
+
+func Test_WinNewPre()
+ " Test that the old window layout can be accessed before a new window is created.
+ let g:layouts_pre = []
+ let g:layouts_post = []
+ augroup testing
+ au WinNewPre * call add(g:layouts_pre, winlayout())
+ au WinNew * call add(g:layouts_post, winlayout())
+ augroup END
+ defer CleanUpTestAuGroup()
+ split
+ call assert_notequal(g:layouts_pre[0], g:layouts_post[0])
+ split
+ call assert_equal(g:layouts_pre[1], g:layouts_post[0])
+ call assert_notequal(g:layouts_pre[1], g:layouts_post[1])
+ " not triggered for tabnew
+ tabnew
+ call assert_equal(2, len(g:layouts_pre))
+ unlet g:layouts_pre
+ unlet g:layouts_post
+
+ " Test modifying window layout during WinNewPre throws.
+ let g:caught = 0
augroup testing
au!
+ au WinNewPre * split
augroup END
- unlet g:record
+ try
+ vnew
+ catch
+ let g:caught += 1
+ endtry
+ augroup testing
+ au!
+ au WinNewPre * tabnew
+ augroup END
+ try
+ vnew
+ catch
+ let g:caught += 1
+ endtry
+ augroup testing
+ au!
+ au WinNewPre * close
+ augroup END
+ try
+ vnew
+ catch
+ let g:caught += 1
+ endtry
+ augroup testing
+ au!
+ au WinNewPre * tabclose
+ augroup END
+ try
+ vnew
+ catch
+ let g:caught += 1
+ endtry
+ call assert_equal(4, g:caught)
+ unlet g:caught
endfunc
func Test_WinResized()
@@ -2820,7 +2893,8 @@ endfunc
func Test_autocmd_nested()
let g:did_nested = 0
- augroup Testing
+ defer CleanUpTestAuGroup()
+ augroup testing
au WinNew * edit somefile
au BufNew * let g:did_nested = 1
augroup END
@@ -2830,7 +2904,7 @@ func Test_autocmd_nested()
bwipe! somefile
" old nested argument still works
- augroup Testing
+ augroup testing
au!
au WinNew * nested edit somefile
au BufNew * let g:did_nested = 1
@@ -4378,6 +4452,38 @@ func Test_BufEnter_botline()
set hidden&vim
endfunc
+" those commands caused null pointer access, see #15464
+func Test_WinNewPre_crash()
+ defer CleanUpTestAuGroup()
+ let _cmdheight=&cmdheight
+ augroup testing
+ au!
+ autocmd WinNewPre * redraw
+ augroup END
+ tabnew
+ tabclose
+ augroup testing
+ au!
+ autocmd WinNewPre * wincmd t
+ augroup END
+ tabnew
+ tabclose
+ augroup testing
+ au!
+ autocmd WinNewPre * wincmd b
+ augroup END
+ tabnew
+ tabclose
+ augroup testing
+ au!
+ autocmd WinNewPre * set cmdheight+=1
+ augroup END
+ tabnew
+ tabclose
+ let &cmdheight=_cmdheight
+endfunc
+
+
" This was using freed memory
func Test_autocmd_BufWinLeave_with_vsp()
new
diff --git a/test/old/testdir/test_crash.vim b/test/old/testdir/test_crash.vim
@@ -191,52 +191,49 @@ func Test_crash1_3()
let buf = RunVimInTerminal('sh', #{cmd: 'sh'})
let file = 'crash/poc_ex_substitute'
- let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'\<cr>"
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 150)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/poc_uaf_exec_instructions'
- let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'\<cr>"
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 150)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/poc_uaf_check_argument_types'
- let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'\<cr>"
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 150)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/double_free'
- let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'\<cr>"
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 50)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/dialog_changed_uaf'
- let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'\<cr>"
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
+ let args = printf(cmn_args, vim, file)
+ call s:RunCommandAndWait(buf, args)
+
+ let file = 'crash/nullpointer'
+ let cmn_args = "%s -u NONE -i NONE -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 150)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/heap_overflow3'
let cmn_args = "%s -u NONE -i NONE -n -X -m -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 150)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/heap_overflow_glob2regpat'
let cmn_args = "%s -u NONE -i NONE -n -X -m -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 50)
+ call s:RunCommandAndWait(buf, args)
let file = 'crash/nullptr_regexp_nfa'
let cmn_args = "%s -u NONE -i NONE -n -X -m -n -e -s -S %s -c ':qa!'"
let args = printf(cmn_args, vim, file)
- call term_sendkeys(buf, args)
- call TermWait(buf, 50)
+ call s:RunCommandAndWait(buf, args)
" clean up
exe buf .. "bw!"
diff --git a/test/old/testdir/test_window_cmd.vim b/test/old/testdir/test_window_cmd.vim
@@ -1043,8 +1043,7 @@ func Test_win_splitmove()
let s:triggered = []
augroup WinSplitMove
au!
- " Nvim: WinNewPre not ported yet. Also needs full port of v9.1.0117 to pass.
- " au WinNewPre * let s:triggered += ['WinNewPre']
+ au WinNewPre * let s:triggered += ['WinNewPre']
au WinNew * let s:triggered += ['WinNew', win_getid()]
au WinClosed * let s:triggered += ['WinClosed', str2nr(expand('<afile>'))]
augroup END