neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mruntime/doc/autocmd.txt | 14++++++++++++++
Mruntime/doc/options.txt | 3++-
Mruntime/lua/vim/_meta/api_keysets.lua | 1+
Mruntime/lua/vim/_meta/options.lua | 3++-
Msrc/nvim/auevents.lua | 3++-
Msrc/nvim/window.c | 15+++++++++++++++
Atest/old/testdir/crash/nullpointer | 0
Mtest/old/testdir/test_autocmd.vim | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtest/old/testdir/test_crash.vim | 39++++++++++++++++++---------------------
Mtest/old/testdir/test_window_cmd.vim | 3+--
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