aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/autocmd.txt31
-rw-r--r--src/nvim/autocmd.c16
-rw-r--r--src/nvim/normal.c3
-rw-r--r--src/nvim/testdir/test_autocmd.vim96
-rw-r--r--src/nvim/window.c100
-rw-r--r--test/functional/autocmd/winscrolled_spec.lua77
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)