diff options
-rw-r--r-- | runtime/doc/autocmd.txt | 31 | ||||
-rw-r--r-- | src/nvim/autocmd.c | 16 | ||||
-rw-r--r-- | src/nvim/normal.c | 3 | ||||
-rw-r--r-- | src/nvim/testdir/test_autocmd.vim | 96 | ||||
-rw-r--r-- | src/nvim/window.c | 100 | ||||
-rw-r--r-- | test/functional/autocmd/winscrolled_spec.lua | 77 |
6 files changed, 263 insertions, 60 deletions
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index c30c190301..b6c9253a41 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -1096,22 +1096,35 @@ WinNew When a new window was created. Not done for *WinScrolled* WinScrolled After scrolling the content of a window or - resizing a window. - The pattern is matched against the - |window-ID|. Both <amatch> and <afile> are - set to the |window-ID|. - Non-recursive (the event cannot trigger - itself). However, if the command causes the - window to scroll or change size another + resizing a window in the current tab page. + + When more than one window scrolled or resized + only one WinScrolled event is triggered. You + can use the `winlayout()` and `getwininfo()` + functions to see what changed. + + The pattern is matched against the |window-ID| + of the first window that scrolled or resized. + Both <amatch> and <afile> are set to the + |window-ID|. + + Only starts triggering after startup finished + and the first screen redraw was done. + + Non-recursive: the event will not trigger + while executing commands for the WinScrolled + event. However, if the command causes a + window to scroll or change size, then another WinScrolled event will be triggered later. + Does not trigger when the command is added, only after the first scroll or resize. ============================================================================== 6. Patterns *autocmd-pattern* *{aupat}* -The {aupat} argument of `:autocmd` can be a comma-separated list. This works -as if the command was given with each pattern separately. Thus this command: > +The {aupat} argument of `:autocmd` can be a comma-separated list. This works as +if the command was given with each pattern separately. Thus this command: > :autocmd BufRead *.txt,*.info set et Is equivalent to: > :autocmd BufRead *.txt set et diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 4df14411c5..54fa57c7a0 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -1142,13 +1142,17 @@ int autocmd_register(int64_t id, event_T event, char *pat, int patlen, int group } // Initialize the fields checked by the WinScrolled trigger to - // stop it from firing right after the first autocmd is defined. + // prevent it from firing right after the first autocmd is + // defined. if (event == EVENT_WINSCROLLED && !has_event(EVENT_WINSCROLLED)) { - curwin->w_last_topline = curwin->w_topline; - curwin->w_last_leftcol = curwin->w_leftcol; - curwin->w_last_skipcol = curwin->w_skipcol; - curwin->w_last_width = curwin->w_width; - curwin->w_last_height = curwin->w_height; + tabpage_T *save_curtab = curtab; + FOR_ALL_TABS(tp) { + unuse_tabpage(curtab); + use_tabpage(tp); + snapshot_windows_scroll_size(); + } + unuse_tabpage(curtab); + use_tabpage(save_curtab); } ap->cmds = NULL; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index ed689df91c..8207710b6b 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1397,6 +1397,9 @@ static int normal_check(VimState *state) fclose(time_fd); time_fd = NULL; } + // After the first screen update may start triggering WinScrolled + // autocmd events. Store all the scroll positions and sizes now. + may_make_initial_scroll_size_snapshot(); } // May perform garbage collection when waiting for a character, but diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 70da0a9ba2..3030ecdfa9 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -311,7 +311,7 @@ func Test_WinScrolled() au WinScrolled * let g:amatch = str2nr(expand('<amatch>')) au WinScrolled * let g:afile = str2nr(expand('<afile>')) END - call writefile(lines, 'Xtest_winscrolled') + call writefile(lines, 'Xtest_winscrolled', 'D') let buf = RunVimInTerminal('-S Xtest_winscrolled', {'rows': 6}) call term_sendkeys(buf, ":echo g:scrolled\<CR>") @@ -346,7 +346,36 @@ func Test_WinScrolled() call WaitForAssert({-> assert_match('^v:true ', term_getline(buf, 6))}, 1000) call StopVimInTerminal(buf) - call delete('Xtest_winscrolled') +endfunc + +func Test_WinScrolled_mouse() + CheckRunVimInTerminal + + let lines =<< trim END + set nowrap scrolloff=0 + set mouse=a term=xterm ttymouse=sgr mousetime=200 clipboard= + call setline(1, ['foo']->repeat(32)) + split + let g:scrolled = 0 + au WinScrolled * let g:scrolled += 1 + END + call writefile(lines, 'Xtest_winscrolled_mouse', 'D') + let buf = RunVimInTerminal('-S Xtest_winscrolled_mouse', {'rows': 10}) + + " With the upper split focused, send a scroll-down event to the unfocused one. + call test_setmouse(7, 1) + call term_sendkeys(buf, "\<ScrollWheelDown>") + call TermWait(buf) + call term_sendkeys(buf, ":echo g:scrolled\<CR>") + call WaitForAssert({-> assert_match('^1', term_getline(buf, 10))}, 1000) + + " Again, but this time while we're in insert mode. + call term_sendkeys(buf, "i\<ScrollWheelDown>\<Esc>") + call TermWait(buf) + call term_sendkeys(buf, ":echo g:scrolled\<CR>") + call WaitForAssert({-> assert_match('^2', term_getline(buf, 10))}, 1000) + + call StopVimInTerminal(buf) endfunc func Test_WinScrolled_close_curwin() @@ -359,7 +388,7 @@ func Test_WinScrolled_close_curwin() au WinScrolled * close au VimLeave * call writefile(['123456'], 'Xtestout') END - call writefile(lines, 'Xtest_winscrolled_close_curwin') + call writefile(lines, 'Xtest_winscrolled_close_curwin', 'D') let buf = RunVimInTerminal('-S Xtest_winscrolled_close_curwin', {'rows': 6}) " This was using freed memory @@ -367,12 +396,64 @@ func Test_WinScrolled_close_curwin() call TermWait(buf) call StopVimInTerminal(buf) + " check the startup script finished to the end call assert_equal(['123456'], readfile('Xtestout')) - - call delete('Xtest_winscrolled_close_curwin') call delete('Xtestout') endfunc +func Test_WinScrolled_once_only() + CheckRunVimInTerminal + + let lines =<< trim END + set cmdheight=2 + call setline(1, ['aaa', 'bbb']) + let trigger_count = 0 + func ShowInfo(id) + echo g:trigger_count g:winid winlayout() + endfunc + + vsplit + split + " use a timer to show the info after a redraw + au WinScrolled * let trigger_count += 1 | let winid = expand('<amatch>') | call timer_start(100, 'ShowInfo') + wincmd j + wincmd l + END + call writefile(lines, 'Xtest_winscrolled_once', 'D') + let buf = RunVimInTerminal('-S Xtest_winscrolled_once', #{rows: 10, cols: 60, statusoff: 2}) + + call term_sendkeys(buf, "\<C-E>") + call VerifyScreenDump(buf, 'Test_winscrolled_once_only_1', {}) + + call StopVimInTerminal(buf) +endfunc + +" Check that WinScrolled is not triggered immediately when defined and there +" are split windows. +func Test_WinScrolled_not_when_defined() + CheckRunVimInTerminal + + let lines =<< trim END + call setline(1, ['aaa', 'bbb']) + echo 'nothing happened' + func ShowTriggered(id) + echo 'triggered' + endfunc + END + call writefile(lines, 'Xtest_winscrolled_not', 'D') + let buf = RunVimInTerminal('-S Xtest_winscrolled_not', #{rows: 10, cols: 60, statusoff: 2}) + call term_sendkeys(buf, ":split\<CR>") + call TermWait(buf) + " use a timer to show the message after redrawing + call term_sendkeys(buf, ":au WinScrolled * call timer_start(100, 'ShowTriggered')\<CR>") + call VerifyScreenDump(buf, 'Test_winscrolled_not_when_defined_1', {}) + + call term_sendkeys(buf, "\<C-E>") + call VerifyScreenDump(buf, 'Test_winscrolled_not_when_defined_2', {}) + + call StopVimInTerminal(buf) +endfunc + func Test_WinScrolled_long_wrapped() CheckRunVimInTerminal @@ -385,7 +466,7 @@ func Test_WinScrolled_long_wrapped() call setline(1, repeat('foo', height * width)) call cursor(1, height * width) END - call writefile(lines, 'Xtest_winscrolled_long_wrapped') + call writefile(lines, 'Xtest_winscrolled_long_wrapped', 'D') let buf = RunVimInTerminal('-S Xtest_winscrolled_long_wrapped', {'rows': 6}) call term_sendkeys(buf, ":echo g:scrolled\<CR>") @@ -402,8 +483,6 @@ func Test_WinScrolled_long_wrapped() call term_sendkeys(buf, '$') call term_sendkeys(buf, ":echo g:scrolled\<CR>") call WaitForAssert({-> assert_match('^3 ', term_getline(buf, 6))}, 1000) - - call delete('Xtest_winscrolled_long_wrapped') endfunc func Test_WinClosed() @@ -2788,6 +2867,7 @@ func Test_SpellFileMissing_bwipe() call assert_fails('set spell spelllang=0', 'E937:') au! SpellFileMissing + set nospell spelllang=en bwipe endfunc diff --git a/src/nvim/window.c b/src/nvim/window.c index 54ab9a0471..f25e25e905 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -3868,6 +3868,27 @@ void close_others(int message, int forceit) } } +/// Store the relevant window pointers for tab page "tp". To be used before +/// use_tabpage(). +void unuse_tabpage(tabpage_T *tp) +{ + tp->tp_topframe = topframe; + tp->tp_firstwin = firstwin; + tp->tp_lastwin = lastwin; + tp->tp_curwin = curwin; +} + +/// Set the relevant pointers to use tab page "tp". May want to call +/// unuse_tabpage() first. +void use_tabpage(tabpage_T *tp) +{ + curtab = tp; + topframe = curtab->tp_topframe; + firstwin = curtab->tp_firstwin; + lastwin = curtab->tp_lastwin; + curwin = curtab->tp_curwin; +} + // Allocate the first window and put an empty buffer in it. // Only called from main(). void win_alloc_first(void) @@ -3878,11 +3899,8 @@ void win_alloc_first(void) } first_tabpage = alloc_tabpage(); - first_tabpage->tp_topframe = topframe; curtab = first_tabpage; - curtab->tp_firstwin = firstwin; - curtab->tp_lastwin = lastwin; - curtab->tp_curwin = curwin; + unuse_tabpage(first_tabpage); } // Init `aucmd_win`. This can only be done after the first window @@ -4253,10 +4271,7 @@ static void enter_tabpage(tabpage_T *tp, buf_T *old_curbuf, bool trigger_enter_a win_T *next_prevwin = tp->tp_prevwin; tabpage_T *old_curtab = curtab; - curtab = tp; - firstwin = tp->tp_firstwin; - lastwin = tp->tp_lastwin; - topframe = tp->tp_topframe; + use_tabpage(tp); if (old_curtab != curtab) { tabpage_check_windows(old_curtab); @@ -5263,35 +5278,60 @@ void win_new_screen_cols(void) win_reconfig_floats(); // The size of floats might change } -/// Trigger WinScrolled for "curwin" if needed. +/// Make a snapshot of all the window scroll positions and sizes of the current +/// tab page. +void snapshot_windows_scroll_size(void) +{ + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + wp->w_last_topline = wp->w_topline; + wp->w_last_leftcol = wp->w_leftcol; + wp->w_last_skipcol = wp->w_skipcol; + wp->w_last_width = wp->w_width; + wp->w_last_height = wp->w_height; + } +} + +static bool did_initial_scroll_size_snapshot = false; + +void may_make_initial_scroll_size_snapshot(void) +{ + if (!did_initial_scroll_size_snapshot) { + did_initial_scroll_size_snapshot = true; + snapshot_windows_scroll_size(); + } +} + +/// Trigger WinScrolled if any window scrolled or changed size. void may_trigger_winscrolled(void) { static bool recursive = false; - if (recursive || !has_event(EVENT_WINSCROLLED)) { + if (recursive + || !has_event(EVENT_WINSCROLLED) + || !did_initial_scroll_size_snapshot) { return; } - win_T *wp = curwin; - if (wp->w_last_topline != wp->w_topline - || wp->w_last_leftcol != wp->w_leftcol - || wp->w_last_skipcol != wp->w_skipcol - || wp->w_last_width != wp->w_width - || wp->w_last_height != wp->w_height) { - char winid[NUMBUFLEN]; - vim_snprintf(winid, sizeof(winid), "%d", wp->handle); - - recursive = true; - apply_autocmds(EVENT_WINSCROLLED, winid, winid, false, wp->w_buffer); - recursive = false; - - // an autocmd may close the window, "wp" may be invalid now - if (win_valid_any_tab(wp)) { - wp->w_last_topline = wp->w_topline; - wp->w_last_leftcol = wp->w_leftcol; - wp->w_last_skipcol = wp->w_skipcol; - wp->w_last_width = wp->w_width; - wp->w_last_height = wp->w_height; + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_last_topline != wp->w_topline + || wp->w_last_leftcol != wp->w_leftcol + || wp->w_last_skipcol != wp->w_skipcol + || wp->w_last_width != wp->w_width + || wp->w_last_height != wp->w_height) { + // WinScrolled is triggered only once, even when multiple windows + // scrolled or changed size. Store the current values before + // triggering the event, if a scroll or resize happens as a side + // effect then WinScrolled is triggered again later. + snapshot_windows_scroll_size(); + + char winid[NUMBUFLEN]; + vim_snprintf(winid, sizeof(winid), "%d", wp->handle); + + recursive = true; + apply_autocmds(EVENT_WINSCROLLED, winid, winid, false, wp->w_buffer); + recursive = false; + + break; } } } diff --git a/test/functional/autocmd/winscrolled_spec.lua b/test/functional/autocmd/winscrolled_spec.lua index 12b8e7c42d..5a426fcfa1 100644 --- a/test/functional/autocmd/winscrolled_spec.lua +++ b/test/functional/autocmd/winscrolled_spec.lua @@ -1,8 +1,10 @@ local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') local clear = helpers.clear local eq = helpers.eq local eval = helpers.eval +local exec = helpers.exec local command = helpers.command local feed = helpers.feed local meths = helpers.meths @@ -89,11 +91,72 @@ describe('WinScrolled', function() end) end) -it('closing window in WinScrolled does not cause use-after-free #13265', function() - local lines = {'aaa', 'bbb'} - meths.buf_set_lines(0, 0, -1, true, lines) - command('vsplit') - command('autocmd WinScrolled * close') - feed('<C-E>') - assert_alive() +describe('WinScrolled', function() + -- oldtest: Test_WinScrolled_mouse() + it('is triggered by mouse scrolling in another window', function() + local screen = Screen.new(75, 10) + screen:attach() + exec([[ + set nowrap scrolloff=0 + set mouse=a + call setline(1, ['foo']->repeat(32)) + split + let g:scrolled = 0 + au WinScrolled * let g:scrolled += 1 + ]]) + eq(0, eval('g:scrolled')) + + -- With the upper split focused, send a scroll-down event to the unfocused one. + meths.input_mouse('wheel', 'down', '', 0, 6, 0) + eq(1, eval('g:scrolled')) + + -- Again, but this time while we're in insert mode. + feed('i') + meths.input_mouse('wheel', 'down', '', 0, 6, 0) + feed('<Esc>') + eq(2, eval('g:scrolled')) + end) + + -- oldtest: Test_WinScrolled_close_curwin() + it('closing window does not cause use-after-free #13265', function() + exec([[ + set nowrap scrolloff=0 + call setline(1, ['aaa', 'bbb']) + vsplit + au WinScrolled * close + ]]) + + -- This was using freed memory + feed('<C-E>') + assert_alive() + end) + + it('is triggered by mouse scrolling in unfocused floating window #18222', function() + local screen = Screen.new(80, 24) + screen:attach() + local buf = meths.create_buf(true, true) + meths.buf_set_lines(buf, 0, -1, false, {'a', 'b', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'}) + local win = meths.open_win(buf, false, { + height = 5, + width = 10, + col = 0, + row = 1, + relative = 'editor', + style = 'minimal' + }) + exec([[ + let g:scrolled = 0 + autocmd WinScrolled * let g:scrolled += 1 + autocmd WinScrolled * let g:amatch = expand('<amatch>') + ]]) + eq(0, eval('g:scrolled')) + + meths.input_mouse('wheel', 'down', '', 0, 3, 3) + eq(1, eval('g:scrolled')) + eq(tostring(win.id), eval('g:amatch')) + + meths.input_mouse('wheel', 'down', '', 0, 3, 3) + eq(2, eval('g:scrolled')) + eq(tostring(win.id), eval('g:amatch')) + end) end) |