changeset 31166:a86ee6c0309e v9.0.0917

patch 9.0.0917: the WinScrolled autocommand event is not enough Commit: https://github.com/vim/vim/commit/35fc61cb5b5eba8bbb9d8f0700332fbab38f40ca Author: Bram Moolenaar <Bram@vim.org> Date: Tue Nov 22 12:40:50 2022 +0000 patch 9.0.0917: the WinScrolled autocommand event is not enough Problem: The WinScrolled autocommand event is not enough. Solution: Add WinResized and provide information about what changed. (closes #11576)
author Bram Moolenaar <Bram@vim.org>
date Tue, 22 Nov 2022 13:45:04 +0100
parents b1c24c44852a
children 8cf717ffd0de
files runtime/doc/autocmd.txt runtime/doc/windows.txt src/autocmd.c src/dict.c src/edit.c src/gui.c src/main.c src/mouse.c src/proto/autocmd.pro src/proto/window.pro src/testdir/test_autocmd.vim src/version.c src/vim.h src/window.c
diffstat 14 files changed, 463 insertions(+), 62 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -1371,21 +1371,24 @@ WinNew				When a new window was created.
 				Before a WinEnter event.
 
 							*WinScrolled*
-WinScrolled			After scrolling the content of a window or
-				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.
+WinScrolled			After any window in the current tab page
+				scrolled the text (horizontally or vertically)
+				or changed width or height.  See
+				|win-scrolled-resized|.
 
 				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|.
 
+				|v:event| is set with information about size
+				and scroll changes. |WinScrolled-event|
+
 				Only starts triggering after startup finished
 				and the first screen redraw was done.
+				Does not trigger when defining the first
+				WinScrolled or WinResized event, but may
+				trigger when adding more.
 
 				Non-recursive: the event will not trigger
 				while executing commands for the WinScrolled
@@ -1393,11 +1396,17 @@ WinScrolled			After scrolling the conten
 				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.
-							*E1312*
-				It is not allowed to change the window layout
-				here (split, close or move windows).
+
+							*WinResized*
+WinResized			After a window in the current tab page changed
+				width or height.
+				See |win-scrolled-resized|.
+
+				|v:event| is set with information about size
+				changes. |WinResized-event|
+
+				Same behavior as |WinScrolled| for the
+				pattern, triggering and recursiveness.
 
 ==============================================================================
 6. Patterns					*autocmd-patterns* *{aupat}*
--- a/runtime/doc/windows.txt
+++ b/runtime/doc/windows.txt
@@ -631,6 +631,54 @@ it).
 The minimal height and width of a window is set with 'winminheight' and
 'winminwidth'.  These are hard values, a window will never become smaller.
 
+
+WinScrolled and WinResized autocommands ~
+						*win-scrolled-resized*
+If you want to get notified of changes in window sizes, the |WinResized|
+autocommand event can be used.
+If you want to get notified of text in windows scrolling vertically or
+horizontally, the |WinScrolled| autocommand event can be used.  This will also
+trigger in window size changes.
+							*WinResized-event*
+The |WinResized| event is triggered after updating the display, several
+windows may have changed size then.  A list of the IDs of windows that changed
+since last time is provided in the v:event.windows variable, for example:
+	[1003, 1006]
+							*WinScrolled-event*
+The |WinScrolled| event is triggered after |WinResized|, and also if a window
+was scrolled.  That can be vertically (the text at the top of the window
+changed) or horizontally (when 'wrap' is off or when the first displayed part
+of the first line changes).  Note that |WinScrolled| will trigger many more
+times than |WinResized|, it may slow down editing a bit.
+
+The information provided by |WinScrolled| is a dictionary for each window that
+has changes, using the window ID as the key, and a total count of the changes
+with the key "all".  Example value for |v:event| (|Vim9| syntax):
+	{
+	   all: {width: 0, height: 2, leftcol: 0, topline: 1, skipcol: 0},
+	   1003: {width: 0, height: -1, leftcol: 0, topline: 0, skipcol: 0},
+	   1006: {width: 0, height: 1, leftcol: 0, topline: 1, skipcol: 0},
+	}
+
+Note that the "all" entry has the absolute values of the individual windows
+accumulated.
+
+If you need more information about what changed, or you want to "debounce" the
+events (not handle every event to avoid doing too much work), you may want to
+use the `winlayout()` and `getwininfo()` functions.
+
+|WinScrolled| and |WinResized| do not trigger when the first autocommand is
+added, only after the first scroll or resize.  They may trigger when switching
+to another tab page.
+
+The commands executed are expected to not cause window size or scroll changes.
+If this happens anyway, the event will trigger again very soon.  In other
+words: Just before triggering the event, the current sizes and scroll
+positions are stored and used to decide whether there was a change.
+								*E1312*
+It is not allowed to change the window layout here (split, close or move
+windows).
+
 ==============================================================================
 7. Argument and buffer list commands			*buffer-list*
 
--- a/src/autocmd.c
+++ b/src/autocmd.c
@@ -191,6 +191,7 @@ static struct event_name
     {"WinClosed",	EVENT_WINCLOSED},
     {"WinEnter",	EVENT_WINENTER},
     {"WinLeave",	EVENT_WINLEAVE},
+    {"WinResized",	EVENT_WINRESIZED},
     {"WinScrolled",	EVENT_WINSCROLLED},
     {"VimResized",	EVENT_VIMRESIZED},
     {"TextYankPost",	EVENT_TEXTYANKPOST},
@@ -1263,10 +1264,11 @@ do_autocmd_event(
 		if (event == EVENT_MODECHANGED && !has_modechanged())
 		    get_mode(last_mode);
 #endif
-		// Initialize the fields checked by the WinScrolled trigger to
-		// prevent it from firing right after the first autocmd is
-		// defined.
-		if (event == EVENT_WINSCROLLED && !has_winscrolled())
+		// Initialize the fields checked by the WinScrolled and
+		// WinResized trigger to prevent them from firing right after
+		// the first autocmd is defined.
+		if ((event == EVENT_WINSCROLLED || event == EVENT_WINRESIZED)
+			&& !(has_winscrolled() || has_winresized()))
 		{
 		    tabpage_T *save_curtab = curtab;
 		    tabpage_T *tp;
@@ -1811,6 +1813,15 @@ trigger_cursorhold(void)
 }
 
 /*
+ * Return TRUE when there is a WinResized autocommand defined.
+ */
+    int
+has_winresized(void)
+{
+    return (first_autopat[(int)EVENT_WINRESIZED] != NULL);
+}
+
+/*
  * Return TRUE when there is a WinScrolled autocommand defined.
  */
     int
@@ -2117,6 +2128,7 @@ apply_autocmds_group(
 		|| event == EVENT_MENUPOPUP
 		|| event == EVENT_USER
 		|| event == EVENT_WINCLOSED
+		|| event == EVENT_WINRESIZED
 		|| event == EVENT_WINSCROLLED)
 	{
 	    fname = vim_strsave(fname);
--- a/src/dict.c
+++ b/src/dict.c
@@ -1082,13 +1082,14 @@ failret:
  * Go over all entries in "d2" and add them to "d1".
  * When "action" is "error" then a duplicate key is an error.
  * When "action" is "force" then a duplicate key is overwritten.
+ * When "action" is "move" then move items instead of copying.
  * Otherwise duplicate keys are ignored ("action" is "keep").
+ * "func_name" is used for reporting where an error occurred.
  */
     void
 dict_extend(dict_T *d1, dict_T *d2, char_u *action, char *func_name)
 {
     dictitem_T	*di1;
-    hashitem_T	*hi2;
     int		todo;
     char_u	*arg_errmsg = (char_u *)N_("extend() argument");
     type_T	*type;
@@ -1098,8 +1099,11 @@ dict_extend(dict_T *d1, dict_T *d2, char
     else
 	type = NULL;
 
+    if (*action == 'm')
+	hash_lock(&d2->dv_hashtab);  // don't rehash on hash_remove()
+
     todo = (int)d2->dv_hashtab.ht_used;
-    for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2)
+    for (hashitem_T *hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2)
     {
 	if (!HASHITEM_EMPTY(hi2))
 	{
@@ -1116,9 +1120,19 @@ dict_extend(dict_T *d1, dict_T *d2, char
 
 	    if (di1 == NULL)
 	    {
-		di1 = dictitem_copy(HI2DI(hi2));
-		if (di1 != NULL && dict_add(d1, di1) == FAIL)
-		    dictitem_free(di1);
+		if (*action == 'm')
+		{
+		    // cheap way to move a dict item from "d2" to "d1"
+		    di1 = HI2DI(hi2);
+		    dict_add(d1, di1);
+		    hash_remove(&d2->dv_hashtab, hi2);
+		}
+		else
+		{
+		    di1 = dictitem_copy(HI2DI(hi2));
+		    if (di1 != NULL && dict_add(d1, di1) == FAIL)
+			dictitem_free(di1);
+		}
 	    }
 	    else if (*action == 'e')
 	    {
@@ -1138,6 +1152,9 @@ dict_extend(dict_T *d1, dict_T *d2, char
 	    }
 	}
     }
+
+    if (*action == 'm')
+	hash_unlock(&d2->dv_hashtab);
 }
 
 /*
@@ -1272,7 +1289,7 @@ dict_extend_func(
 	    action = (char_u *)"force";
 
 	if (type != NULL && check_typval_arg_type(type, &argvars[1],
-		    func_name, 2) == FAIL)
+							 func_name, 2) == FAIL)
 	    return;
 	dict_extend(d1, d2, action, func_name);
 
--- a/src/edit.c
+++ b/src/edit.c
@@ -1510,7 +1510,7 @@ ins_redraw(int ready)	    // not busy wi
     }
 
     if (ready)
-	may_trigger_winscrolled();
+	may_trigger_win_scrolled_resized();
 
     // Trigger SafeState if nothing is pending.
     may_trigger_safestate(ready
--- a/src/gui.c
+++ b/src/gui.c
@@ -5097,7 +5097,7 @@ gui_update_screen(void)
     }
 
     if (!finish_op)
-	may_trigger_winscrolled();
+	may_trigger_win_scrolled_resized();
 
 # ifdef FEAT_CONCEAL
     if (conceal_update_lines
--- a/src/main.c
+++ b/src/main.c
@@ -1358,7 +1358,7 @@ main_loop(
 	    validate_cursor();
 
 	    if (!finish_op)
-		may_trigger_winscrolled();
+		may_trigger_win_scrolled_resized();
 
 	    // If nothing is pending and we are going to wait for the user to
 	    // type a character, trigger SafeState.
--- a/src/mouse.c
+++ b/src/mouse.c
@@ -1171,7 +1171,7 @@ do_mousescroll(cmdarg_T *cap)
 	    leftcol = 0;
 	do_mousescroll_horiz((long_u)leftcol);
     }
-    may_trigger_winscrolled();
+    may_trigger_win_scrolled_resized();
 }
 
 /*
--- a/src/proto/autocmd.pro
+++ b/src/proto/autocmd.pro
@@ -16,6 +16,7 @@ int apply_autocmds(event_T event, char_u
 int apply_autocmds_exarg(event_T event, char_u *fname, char_u *fname_io, int force, buf_T *buf, exarg_T *eap);
 int apply_autocmds_retval(event_T event, char_u *fname, char_u *fname_io, int force, buf_T *buf, int *retval);
 int trigger_cursorhold(void);
+int has_winresized(void);
 int has_winscrolled(void);
 int has_cursormoved(void);
 int has_cursormovedI(void);
--- a/src/proto/window.pro
+++ b/src/proto/window.pro
@@ -20,7 +20,7 @@ int one_window(void);
 int win_close(win_T *win, int free_buf);
 void snapshot_windows_scroll_size(void);
 void may_make_initial_scroll_size_snapshot(void);
-void may_trigger_winscrolled(void);
+void may_trigger_win_scrolled_resized(void);
 void win_close_othertab(win_T *win, int free_buf, tabpage_T *tp);
 void win_free_all(void);
 win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp);
--- a/src/testdir/test_autocmd.vim
+++ b/src/testdir/test_autocmd.vim
@@ -306,6 +306,61 @@ func Test_win_tab_autocmd()
   unlet g:record
 endfunc
 
+func Test_WinResized()
+  CheckRunVimInTerminal
+
+  let lines =<< trim END
+    set scrolloff=0
+    call setline(1, ['111', '222'])
+    vnew
+    call setline(1, ['aaa', 'bbb'])
+    new
+    call setline(1, ['foo', 'bar'])
+
+    let g:resized = 0
+    au WinResized * let g:resized += 1
+
+    func WriteResizedEvent()
+      call writefile([json_encode(v:event)], 'XresizeEvent')
+    endfunc
+    au WinResized * call WriteResizedEvent()
+  END
+  call writefile(lines, 'Xtest_winresized', 'D')
+  let buf = RunVimInTerminal('-S Xtest_winresized', {'rows': 10})
+
+  " redraw now to avoid a redraw after the :echo command
+  call term_sendkeys(buf, ":redraw!\<CR>")
+  call TermWait(buf)
+
+  call term_sendkeys(buf, ":echo g:resized\<CR>")
+  call WaitForAssert({-> assert_match('^0$', term_getline(buf, 10))}, 1000)
+
+  " increase window height, two windows will be reported
+  call term_sendkeys(buf, "\<C-W>+")
+  call TermWait(buf)
+  call term_sendkeys(buf, ":echo g:resized\<CR>")
+  call WaitForAssert({-> assert_match('^1$', term_getline(buf, 10))}, 1000)
+
+  let event = readfile('XresizeEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'windows': [1002, 1001],
+        \ }, event)
+
+  " increase window width, three windows will be reported
+  call term_sendkeys(buf, "\<C-W>>")
+  call TermWait(buf)
+  call term_sendkeys(buf, ":echo g:resized\<CR>")
+  call WaitForAssert({-> assert_match('^2$', term_getline(buf, 10))}, 1000)
+
+  let event = readfile('XresizeEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'windows': [1002, 1001, 1000],
+        \ }, event)
+
+  call delete('XresizeEvent')
+  call StopVimInTerminal(buf)
+endfunc
+
 func Test_WinScrolled()
   CheckRunVimInTerminal
 
@@ -316,11 +371,15 @@ func Test_WinScrolled()
     endfor
     let win_id = win_getid()
     let g:matched = v:false
+    func WriteScrollEvent()
+      call writefile([json_encode(v:event)], 'XscrollEvent')
+    endfunc
     execute 'au WinScrolled' win_id 'let g:matched = v:true'
     let g:scrolled = 0
     au WinScrolled * let g:scrolled += 1
     au WinScrolled * let g:amatch = str2nr(expand('<amatch>'))
     au WinScrolled * let g:afile = str2nr(expand('<afile>'))
+    au WinScrolled * call WriteScrollEvent()
   END
   call writefile(lines, 'Xtest_winscrolled', 'D')
   let buf = RunVimInTerminal('-S Xtest_winscrolled', {'rows': 6})
@@ -332,15 +391,33 @@ func Test_WinScrolled()
   call term_sendkeys(buf, "zlzh:echo g:scrolled\<CR>")
   call WaitForAssert({-> assert_match('^2 ', term_getline(buf, 6))}, 1000)
 
+  let event = readfile('XscrollEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'all': {'leftcol': 1, 'topline': 0, 'width': 0, 'height': 0, 'skipcol': 0},
+        \ '1000': {'leftcol': -1, 'topline': 0, 'width': 0, 'height': 0, 'skipcol': 0}
+        \ }, event)
+
   " Scroll up/down in Normal mode.
   call term_sendkeys(buf, "\<c-e>\<c-y>:echo g:scrolled\<CR>")
   call WaitForAssert({-> assert_match('^4 ', term_getline(buf, 6))}, 1000)
 
+  let event = readfile('XscrollEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'all': {'leftcol': 0, 'topline': 1, 'width': 0, 'height': 0, 'skipcol': 0},
+        \ '1000': {'leftcol': 0, 'topline': -1, 'width': 0, 'height': 0, 'skipcol': 0}
+        \ }, event)
+
   " Scroll up/down in Insert mode.
   call term_sendkeys(buf, "Mi\<c-x>\<c-e>\<Esc>i\<c-x>\<c-y>\<Esc>")
   call term_sendkeys(buf, ":echo g:scrolled\<CR>")
   call WaitForAssert({-> assert_match('^6 ', term_getline(buf, 6))}, 1000)
 
+  let event = readfile('XscrollEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'all': {'leftcol': 0, 'topline': 1, 'width': 0, 'height': 0, 'skipcol': 0},
+        \ '1000': {'leftcol': 0, 'topline': -1, 'width': 0, 'height': 0, 'skipcol': 0}
+        \ }, event)
+
   " Scroll the window horizontally to focus the last letter of the third line
   " containing only six characters. Moving to the previous and shorter lines
   " should trigger another autocommand as Vim has to make them visible.
@@ -348,6 +425,12 @@ func Test_WinScrolled()
   call term_sendkeys(buf, ":echo g:scrolled\<CR>")
   call WaitForAssert({-> assert_match('^8 ', term_getline(buf, 6))}, 1000)
 
+  let event = readfile('XscrollEvent')[0]->json_decode()
+  call assert_equal({
+        \ 'all': {'leftcol': 5, 'topline': 0, 'width': 0, 'height': 0, 'skipcol': 0},
+        \ '1000': {'leftcol': -5, 'topline': 0, 'width': 0, 'height': 0, 'skipcol': 0}
+        \ }, event)
+
   " Ensure the command was triggered for the specified window ID.
   call term_sendkeys(buf, ":echo g:matched\<CR>")
   call WaitForAssert({-> assert_match('^v:true ', term_getline(buf, 6))}, 1000)
@@ -356,6 +439,7 @@ func Test_WinScrolled()
   call term_sendkeys(buf, ":echo g:amatch == win_id && g:afile == win_id\<CR>")
   call WaitForAssert({-> assert_match('^v:true ', term_getline(buf, 6))}, 1000)
 
+  call delete('XscrollEvent')
   call StopVimInTerminal(buf)
 endfunc
 
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    917,
+/**/
     916,
 /**/
     915,
--- a/src/vim.h
+++ b/src/vim.h
@@ -1407,7 +1407,8 @@ enum auto_event
     EVENT_WINCLOSED,		// after closing a window
     EVENT_VIMSUSPEND,		// before Vim is suspended
     EVENT_VIMRESUME,		// after Vim is resumed
-    EVENT_WINSCROLLED,		// after Vim window was scrolled
+    EVENT_WINRESIZED,		// after a window was resized
+    EVENT_WINSCROLLED,		// after a window was scrolled or resized
 
     NUM_EVENTS			// MUST be the last one
 };
--- a/src/window.c
+++ b/src/window.c
@@ -2873,46 +2873,273 @@ may_make_initial_scroll_size_snapshot(vo
 }
 
 /*
- * Trigger WinScrolled if any window scrolled or changed size.
- */
-    void
-may_trigger_winscrolled(void)
-{
-    static int	    recursive = FALSE;
-
-    if (recursive
-	    || !has_winscrolled()
-	    || !did_initial_scroll_size_snapshot)
-	return;
+ * Create a dictionary with information about size and scroll changes in a
+ * window.
+ * Returns the dictionary with refcount set to one.
+ * Returns NULL when out of memory.
+ */
+    static dict_T *
+make_win_info_dict(
+	int width,
+	int height,
+	int topline,
+	int leftcol,
+	int skipcol)
+{
+    dict_T *d = dict_alloc();
+    if (d == NULL)
+	return NULL;
+    d->dv_refcount = 1;
+
+    // not actually looping, for breaking out on error
+    while (1)
+    {
+	typval_T tv;
+	tv.v_lock = 0;
+	tv.v_type = VAR_NUMBER;
+
+	tv.vval.v_number = width;
+	if (dict_add_tv(d, "width", &tv) == FAIL)
+	    break;
+	tv.vval.v_number = height;
+	if (dict_add_tv(d, "height", &tv) == FAIL)
+	    break;
+	tv.vval.v_number = topline;
+	if (dict_add_tv(d, "topline", &tv) == FAIL)
+	    break;
+	tv.vval.v_number = leftcol;
+	if (dict_add_tv(d, "leftcol", &tv) == FAIL)
+	    break;
+	tv.vval.v_number = skipcol;
+	if (dict_add_tv(d, "skipcol", &tv) == FAIL)
+	    break;
+	return d;
+    }
+    dict_unref(d);
+    return NULL;
+}
+
+// Return values of check_window_scroll_resize():
+#define CWSR_SCROLLED	1  // at least one window scrolled
+#define CWSR_RESIZED	2  // at least one window size changed
+
+/*
+ * This function is used for three purposes:
+ * 1. Goes over all windows in the current tab page and returns:
+ *	0				no scrolling and no size changes found
+ *	CWSR_SCROLLED			at least one window scrolled
+ *	CWSR_RESIZED			at least one window changed size
+ *	CWSR_SCROLLED + CWSR_RESIZED	both
+ *    "size_count" is set to the nr of windows with size changes.
+ *    "first_scroll_win" is set to the first window with any relevant changes.
+ *    "first_size_win" is set to the first window with size changes.
+ *
+ * 2. When the first three arguments are NULL and "winlist" is not NULL,
+ *    "winlist" is set to the list of window IDs with size changes.
+ *
+ * 3. When the first three arguments are NULL and "v_event" is not NULL,
+ *    information about changed windows is added to "v_event".
+ */
+    static int
+check_window_scroll_resize(
+	int	*size_count,
+	win_T	**first_scroll_win,
+	win_T	**first_size_win,
+	list_T	*winlist,
+	dict_T	*v_event)
+{
+    int result = 0;
+    int listidx = 0;
+    int tot_width = 0;
+    int tot_height = 0;
+    int tot_topline = 0;
+    int tot_leftcol = 0;
+    int tot_skipcol = 0;
 
     win_T *wp;
     FOR_ALL_WINDOWS(wp)
-	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)
+    {
+	int size_changed = wp->w_last_width != wp->w_width
+					  || wp->w_last_height != wp->w_height;
+	if (size_changed)
+	{
+	    result |= CWSR_RESIZED;
+	    if (winlist != NULL)
+	    {
+		// Add this window to the list of changed windows.
+		typval_T tv;
+		tv.v_lock = 0;
+		tv.v_type = VAR_NUMBER;
+		tv.vval.v_number = wp->w_id;
+		list_set_item(winlist, listidx++, &tv);
+	    }
+	    else if (size_count != NULL)
+	    {
+		++*size_count;
+		if (*first_size_win == NULL)
+		    *first_size_win = wp;
+		// For WinScrolled the first window with a size change is used
+		// even when it didn't scroll.
+		if (*first_scroll_win == NULL)
+		    *first_scroll_win = wp;
+	    }
+	}
+
+	int scroll_changed = wp->w_last_topline != wp->w_topline
+				|| wp->w_last_leftcol != wp->w_leftcol
+				|| wp->w_last_skipcol != wp->w_skipcol;
+	if (scroll_changed)
+	{
+	    result |= CWSR_SCROLLED;
+	    if (first_scroll_win != NULL && *first_scroll_win == NULL)
+		*first_scroll_win = wp;
+	}
+
+	if ((size_changed || scroll_changed) && v_event != NULL)
+	{
+	    // Add info about this window to the v:event dictionary.
+	    int width = wp->w_width - wp->w_last_width;
+	    int height = wp->w_height - wp->w_last_height;
+	    int topline = wp->w_topline - wp->w_last_topline;
+	    int leftcol = wp->w_leftcol - wp->w_last_leftcol;
+	    int skipcol = wp->w_skipcol - wp->w_last_skipcol;
+	    dict_T *d = make_win_info_dict(width, height,
+						    topline, leftcol, skipcol);
+	    if (d == NULL)
+		break;
+	    char winid[NUMBUFLEN];
+	    vim_snprintf(winid, sizeof(winid), "%d", wp->w_id);
+	    if (dict_add_dict(v_event, winid, d) == FAIL)
+	    {
+		dict_unref(d);
+		break;
+	    }
+	    --d->dv_refcount;
+
+	    tot_width += abs(width);
+	    tot_height += abs(height);
+	    tot_topline += abs(topline);
+	    tot_leftcol += abs(leftcol);
+	    tot_skipcol += abs(skipcol);
+	}
+    }
+
+    if (v_event != NULL)
+    {
+	dict_T *alldict = make_win_info_dict(tot_width, tot_height,
+					tot_topline, tot_leftcol, tot_skipcol);
+	if (alldict != NULL)
 	{
-	    // 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();
-
-	    // "curwin" may be different from the actual current window, make
-	    // sure it can be restored.
-	    window_layout_lock();
-
-	    recursive = TRUE;
+	    if (dict_add_dict(v_event, "all", alldict) == FAIL)
+		dict_unref(alldict);
+	    else
+		--alldict->dv_refcount;
+	}
+    }
+
+    return result;
+}
+
+/*
+ * Trigger WinScrolled and/or WinResized if any window in the current tab page
+ * scrolled or changed size.
+ */
+    void
+may_trigger_win_scrolled_resized(void)
+{
+    static int	    recursive = FALSE;
+    int		    do_resize = has_winresized();
+    int		    do_scroll = has_winscrolled();
+
+    // Do not trigger WinScrolled or WinResized recursively.  Do not trigger
+    // before the initial snapshot of the w_last_ values was made.
+    if (recursive
+	    || !(do_scroll || do_resize)
+	    || !did_initial_scroll_size_snapshot)
+	return;
+
+    int size_count = 0;
+    win_T *first_scroll_win = NULL, *first_size_win = NULL;
+    int cwsr = check_window_scroll_resize(&size_count,
+					   &first_scroll_win, &first_size_win,
+					   NULL, NULL);
+    int trigger_resize = do_resize && size_count > 0;
+    int trigger_scroll = do_scroll && cwsr != 0;
+    if (!trigger_resize && !trigger_scroll)
+	return;  // no relevant changes
+
+    list_T *windows_list = NULL;
+    if (trigger_resize)
+    {
+	// Create the list for v:event.windows before making the snapshot.
+	windows_list = list_alloc_with_items(size_count);
+	(void)check_window_scroll_resize(NULL, NULL, NULL, windows_list, NULL);
+    }
+
+    dict_T *scroll_dict = NULL;
+    if (trigger_scroll)
+    {
+	// Create the dict with entries for v:event before making the snapshot.
+	scroll_dict = dict_alloc();
+	if (scroll_dict != NULL)
+	{
+	    scroll_dict->dv_refcount = 1;
+	    (void)check_window_scroll_resize(NULL, NULL, NULL, NULL,
+								  scroll_dict);
+	}
+    }
+
+    // WinScrolled/WinResized are 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/WinResized is triggered for that later.
+    snapshot_windows_scroll_size();
+
+    // "curwin" may be different from the actual current window, make
+    // sure it can be restored.
+    window_layout_lock();
+    recursive = TRUE;
+
+    // If both are to be triggered do WinResized first.
+    if (trigger_resize)
+    {
+	save_v_event_T  save_v_event;
+	dict_T		*v_event = get_v_event(&save_v_event);
+
+	if (dict_add_list(v_event, "windows", windows_list) == OK)
+	{
+	    dict_set_items_ro(v_event);
+
 	    char_u winid[NUMBUFLEN];
-	    vim_snprintf((char *)winid, sizeof(winid), "%d", wp->w_id);
-	    apply_autocmds(EVENT_WINSCROLLED, winid, winid, FALSE,
-								 wp->w_buffer);
-	    recursive = FALSE;
-	    window_layout_unlock();
-
-	    break;
+	    vim_snprintf((char *)winid, sizeof(winid), "%d",
+							 first_size_win->w_id);
+	    apply_autocmds(EVENT_WINRESIZED, winid, winid, FALSE,
+						     first_size_win->w_buffer);
 	}
+	restore_v_event(v_event, &save_v_event);
+    }
+
+    if (trigger_scroll)
+    {
+	save_v_event_T  save_v_event;
+	dict_T		*v_event = get_v_event(&save_v_event);
+
+	// Move the entries from scroll_dict to v_event.
+	dict_extend(v_event, scroll_dict, (char_u *)"move", NULL);
+	dict_set_items_ro(v_event);
+	dict_unref(scroll_dict);
+
+	char_u winid[NUMBUFLEN];
+	vim_snprintf((char *)winid, sizeof(winid), "%d",
+						       first_scroll_win->w_id);
+	apply_autocmds(EVENT_WINSCROLLED, winid, winid, FALSE,
+						   first_scroll_win->w_buffer);
+
+	restore_v_event(v_event, &save_v_event);
+    }
+
+    recursive = FALSE;
+    window_layout_unlock();
 }
 
 /*