aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzeertzjq <zeertzjq@outlook.com>2022-11-23 09:54:48 +0800
committerGitHub <noreply@github.com>2022-11-23 09:54:48 +0800
commit4571ba4d0a5234408e544c3a98f107688a792f0d (patch)
tree0418fe6a170f8d2f6f97dd6182753dfdd89eb7bf
parentd41e93d5a83642f90898cae211e017d99ff97fd9 (diff)
downloadrneovim-4571ba4d0a5234408e544c3a98f107688a792f0d.tar.gz
rneovim-4571ba4d0a5234408e544c3a98f107688a792f0d.tar.bz2
rneovim-4571ba4d0a5234408e544c3a98f107688a792f0d.zip
vim-patch:partial:9.0.0917: the WinScrolled autocommand event is not enough (#21161)
Problem: The WinScrolled autocommand event is not enough. Solution: Add WinResized and provide information about what changed. (closes vim/vim#11576) https://github.com/vim/vim/commit/35fc61cb5b5eba8bbb9d8f0700332fbab38f40ca Omit "func_name" comment in tv_dict_extend(): Vim9 script only. Skip layout locking and E1312. Skip list_alloc_with_items() and list_set_item(). Since this overrides remaining changes in patch 9.0.0913, that patch can now be marked as fully ported: vim-patch:9.0.0913: only change in current window triggers the WinScrolled event N/A patches for version.c: vim-patch:9.0.0919: build failure with tiny features Problem: Build failure with tiny features. Solution: Adjust #ifdef's. https://github.com/vim/vim/commit/9c5b7cb4cf67c64648a324e9dfd1e17d793335a4 Co-authored-by: Bram Moolenaar <Bram@vim.org>
-rw-r--r--runtime/doc/autocmd.txt30
-rw-r--r--runtime/doc/windows.txt45
-rw-r--r--src/nvim/auevents.lua3
-rw-r--r--src/nvim/autocmd.c11
-rw-r--r--src/nvim/edit.c3
-rw-r--r--src/nvim/eval/typval.c34
-rw-r--r--src/nvim/normal.c3
-rw-r--r--src/nvim/testdir/test_autocmd.vim84
-rw-r--r--src/nvim/window.c242
-rw-r--r--test/functional/autocmd/win_scrolled_resized_spec.lua285
-rw-r--r--test/functional/autocmd/winscrolled_spec.lua162
11 files changed, 692 insertions, 210 deletions
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
index b6c9253a41..9bb835e661 100644
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -1095,21 +1095,24 @@ WinNew When a new window was created. Not done for
Before WinEnter.
*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
@@ -1117,8 +1120,17 @@ WinScrolled After scrolling the content of a window or
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.
+
+ *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-pattern* *{aupat}*
diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt
index dfc2fb5abf..1776c47d33 100644
--- a/runtime/doc/windows.txt
+++ b/runtime/doc/windows.txt
@@ -607,6 +607,51 @@ 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|:
+ {
+ 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.
+
==============================================================================
7. Argument and buffer list commands *buffer-list*
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
index 65c22c922a..2c0cb771c3 100644
--- a/src/nvim/auevents.lua
+++ b/src/nvim/auevents.lua
@@ -123,7 +123,8 @@ return {
'WinEnter', -- after entering a window
'WinLeave', -- before leaving a window
'WinNew', -- when entering a new window
- 'WinScrolled', -- after scrolling a window
+ 'WinResized', -- after a window was resized
+ 'WinScrolled', -- after a window was scrolled or resized
},
aliases = {
BufCreate = 'BufAdd',
diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c
index 54fa57c7a0..426899c581 100644
--- a/src/nvim/autocmd.c
+++ b/src/nvim/autocmd.c
@@ -1141,10 +1141,11 @@ int autocmd_register(int64_t id, event_T event, char *pat, int patlen, int group
curwin->w_last_cursormoved = curwin->w_cursor;
}
- // 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_event(EVENT_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_event(EVENT_WINSCROLLED) || has_event(EVENT_WINRESIZED))) {
tabpage_T *save_curtab = curtab;
FOR_ALL_TABS(tp) {
unuse_tabpage(curtab);
@@ -1781,7 +1782,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
|| event == EVENT_SIGNAL || event == EVENT_SPELLFILEMISSING
|| event == EVENT_SYNTAX || event == EVENT_TABCLOSED
|| event == EVENT_USER || event == EVENT_WINCLOSED
- || event == EVENT_WINSCROLLED) {
+ || event == EVENT_WINRESIZED || event == EVENT_WINSCROLLED) {
fname = xstrdup(fname);
} else {
fname = FullName_save(fname, false);
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
index 91a67c7c50..cfcc33c65e 100644
--- a/src/nvim/edit.c
+++ b/src/nvim/edit.c
@@ -1339,8 +1339,7 @@ void ins_redraw(bool ready)
}
if (ready) {
- // Trigger Scroll if viewport changed.
- may_trigger_winscrolled();
+ may_trigger_win_scrolled_resized();
}
// Trigger BufModified if b_changed_invalid is set.
diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c
index f38e07f09d..47c3551a56 100644
--- a/src/nvim/eval/typval.c
+++ b/src/nvim/eval/typval.c
@@ -2446,10 +2446,10 @@ void tv_dict_clear(dict_T *const d)
///
/// @param d1 Dictionary to extend.
/// @param[in] d2 Dictionary to extend with.
-/// @param[in] action "error", "force", "keep":
-///
+/// @param[in] action "error", "force", "move", "keep":
/// e*, including "error": duplicate key gives an error.
/// f*, including "force": duplicate d2 keys override d1.
+/// m*, including "move": move items instead of copying.
/// other, including "keep": duplicate d2 keys ignored.
void tv_dict_extend(dict_T *const d1, dict_T *const d2, const char *const action)
FUNC_ATTR_NONNULL_ALL
@@ -2458,19 +2458,31 @@ void tv_dict_extend(dict_T *const d1, dict_T *const d2, const char *const action
const char *const arg_errmsg = _("extend() argument");
const size_t arg_errmsg_len = strlen(arg_errmsg);
- TV_DICT_ITER(d2, di2, {
+ if (*action == 'm') {
+ hash_lock(&d2->dv_hashtab); // don't rehash on hash_remove()
+ }
+
+ HASHTAB_ITER(&d2->dv_hashtab, hi2, {
+ dictitem_T *const di2 = TV_DICT_HI2DI(hi2);
dictitem_T *const di1 = tv_dict_find(d1, (const char *)di2->di_key, -1);
// Check the key to be valid when adding to any scope.
if (d1->dv_scope != VAR_NO_SCOPE && !valid_varname((const char *)di2->di_key)) {
break;
}
if (di1 == NULL) {
- dictitem_T *const new_di = tv_dict_item_copy(di2);
- if (tv_dict_add(d1, new_di) == FAIL) {
- tv_dict_item_free(new_di);
- } else if (watched) {
- tv_dict_watcher_notify(d1, (const char *)new_di->di_key, &new_di->di_tv,
- NULL);
+ if (*action == 'm') {
+ // cheap way to move a dict item from "d2" to "d1"
+ dictitem_T *const new_di = di2;
+ tv_dict_add(d1, new_di);
+ hash_remove(&d2->dv_hashtab, hi2);
+ tv_dict_watcher_notify(d1, (const char *)new_di->di_key, &new_di->di_tv, NULL);
+ } else {
+ dictitem_T *const new_di = tv_dict_item_copy(di2);
+ if (tv_dict_add(d1, new_di) == FAIL) {
+ tv_dict_item_free(new_di);
+ } else if (watched) {
+ tv_dict_watcher_notify(d1, (const char *)new_di->di_key, &new_di->di_tv, NULL);
+ }
}
} else if (*action == 'e') {
semsg(_("E737: Key already exists: %s"), di2->di_key);
@@ -2501,6 +2513,10 @@ void tv_dict_extend(dict_T *const d1, dict_T *const d2, const char *const action
}
}
});
+
+ if (*action == 'm') {
+ hash_unlock(&d2->dv_hashtab);
+ }
}
/// Compare two dictionaries
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
index f9b32a9bd6..71fc7165ea 100644
--- a/src/nvim/normal.c
+++ b/src/nvim/normal.c
@@ -1230,8 +1230,7 @@ static void normal_check_interrupt(NormalState *s)
static void normal_check_window_scrolled(NormalState *s)
{
if (!finish_op) {
- // Trigger Scroll if the viewport changed.
- may_trigger_winscrolled();
+ may_trigger_win_scrolled_resized();
}
}
diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim
index 3030ecdfa9..2945b4a03a 100644
--- a/src/nvim/testdir/test_autocmd.vim
+++ b/src/nvim/testdir/test_autocmd.vim
@@ -295,6 +295,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
@@ -305,11 +360,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})
@@ -321,15 +380,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.
@@ -337,6 +414,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)
@@ -345,6 +428,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
diff --git a/src/nvim/window.c b/src/nvim/window.c
index f25e25e905..beb96aaa03 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -5301,39 +5301,241 @@ void may_make_initial_scroll_size_snapshot(void)
}
}
-/// Trigger WinScrolled if any window scrolled or changed size.
-void may_trigger_winscrolled(void)
+/// Create a dictionary with information about size and scroll changes in a
+/// window.
+/// Returns the dictionary with refcount set to one.
+/// Returns NULL on internal error.
+static dict_T *make_win_info_dict(int width, int height, int topline, int leftcol, int skipcol)
+{
+ dict_T *const d = tv_dict_alloc();
+ d->dv_refcount = 1;
+
+ // not actually looping, for breaking out on error
+ while (1) {
+ typval_T tv = {
+ .v_lock = VAR_UNLOCKED,
+ .v_type = VAR_NUMBER,
+ };
+
+ tv.vval.v_number = width;
+ if (tv_dict_add_tv(d, S_LEN("width"), &tv) == FAIL) {
+ break;
+ }
+ tv.vval.v_number = height;
+ if (tv_dict_add_tv(d, S_LEN("height"), &tv) == FAIL) {
+ break;
+ }
+ tv.vval.v_number = topline;
+ if (tv_dict_add_tv(d, S_LEN("topline"), &tv) == FAIL) {
+ break;
+ }
+ tv.vval.v_number = leftcol;
+ if (tv_dict_add_tv(d, S_LEN("leftcol"), &tv) == FAIL) {
+ break;
+ }
+ tv.vval.v_number = skipcol;
+ if (tv_dict_add_tv(d, S_LEN("skipcol"), &tv) == FAIL) {
+ break;
+ }
+ return d;
+ }
+ tv_dict_unref(d);
+ return NULL;
+}
+
+/// Return values of check_window_scroll_resize():
+enum {
+ CWSR_SCROLLED = 1, ///< at least one window scrolled
+ 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;
+
+ FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
+ const bool 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 = {
+ .v_lock = VAR_UNLOCKED,
+ .v_type = VAR_NUMBER,
+ .vval.v_number = wp->handle,
+ };
+ // tv_list_set_item(winlist, listidx++, &tv);
+ tv_list_append_owned_tv(winlist, 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;
+ }
+ }
+ }
+
+ const bool 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];
+ int key_len = vim_snprintf(winid, sizeof(winid), "%d", wp->handle);
+ if (tv_dict_add_dict(v_event, winid, (size_t)key_len, d) == FAIL) {
+ tv_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) {
+ if (tv_dict_add_dict(v_event, S_LEN("all"), alldict) == FAIL) {
+ tv_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 bool recursive = false;
+ const bool do_resize = has_event(EVENT_WINRESIZED);
+ const bool do_scroll = has_event(EVENT_WINSCROLLED);
if (recursive
- || !has_event(EVENT_WINSCROLLED)
+ || !(do_scroll || do_resize)
|| !did_initial_scroll_size_snapshot) {
return;
}
- 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();
+ 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
+ }
- char winid[NUMBUFLEN];
- vim_snprintf(winid, sizeof(winid), "%d", wp->handle);
+ list_T *windows_list = NULL;
+ if (trigger_resize) {
+ // Create the list for v:event.windows before making the snapshot.
+ // windows_list = tv_list_alloc_with_items(size_count);
+ windows_list = tv_list_alloc(size_count);
+ (void)check_window_scroll_resize(NULL, NULL, NULL, windows_list, NULL);
+ }
- recursive = true;
- apply_autocmds(EVENT_WINSCROLLED, winid, winid, false, wp->w_buffer);
- recursive = false;
+ dict_T *scroll_dict = NULL;
+ if (trigger_scroll) {
+ // Create the dict with entries for v:event before making the snapshot.
+ scroll_dict = tv_dict_alloc();
+ scroll_dict->dv_refcount = 1;
+ (void)check_window_scroll_resize(NULL, NULL, NULL, NULL, scroll_dict);
+ }
- break;
+ // 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();
+
+ 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 (tv_dict_add_list(v_event, S_LEN("windows"), windows_list) == OK) {
+ tv_dict_set_keys_readonly(v_event);
+
+ char winid[NUMBUFLEN];
+ vim_snprintf(winid, sizeof(winid), "%d", first_size_win->handle);
+ 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.
+ tv_dict_extend(v_event, scroll_dict, "move");
+ tv_dict_set_keys_readonly(v_event);
+ tv_dict_unref(scroll_dict);
+
+ char winid[NUMBUFLEN];
+ vim_snprintf(winid, sizeof(winid), "%d", first_scroll_win->handle);
+ apply_autocmds(EVENT_WINSCROLLED, winid, winid, false, first_scroll_win->w_buffer);
+
+ restore_v_event(v_event, &save_v_event);
+ }
+
+ recursive = false;
}
// Save the size of all windows in "gap".
diff --git a/test/functional/autocmd/win_scrolled_resized_spec.lua b/test/functional/autocmd/win_scrolled_resized_spec.lua
new file mode 100644
index 0000000000..bebb21bc31
--- /dev/null
+++ b/test/functional/autocmd/win_scrolled_resized_spec.lua
@@ -0,0 +1,285 @@
+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
+local assert_alive = helpers.assert_alive
+
+before_each(clear)
+
+describe('WinResized', function()
+ -- oldtest: Test_WinResized()
+ it('works', function()
+ exec([[
+ 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
+ au WinResized * let g:v_event = deepcopy(v:event)
+ ]])
+ eq(0, eval('g:resized'))
+
+ -- increase window height, two windows will be reported
+ feed('<C-W>+')
+ eq(1, eval('g:resized'))
+ eq({windows = {1002, 1001}}, eval('g:v_event'))
+
+ -- increase window width, three windows will be reported
+ feed('<C-W>>')
+ eq(2, eval('g:resized'))
+ eq({windows = {1002, 1001, 1000}}, eval('g:v_event'))
+ end)
+end)
+
+describe('WinScrolled', function()
+ local win_id
+
+ before_each(function()
+ win_id = meths.get_current_win().id
+ command(string.format('autocmd WinScrolled %d let g:matched = v:true', win_id))
+ exec([[
+ 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 * let g:v_event = deepcopy(v:event)
+ ]])
+ end)
+
+ after_each(function()
+ eq(true, eval('g:matched'))
+ eq(win_id, eval('g:amatch'))
+ eq(win_id, eval('g:afile'))
+ end)
+
+ it('is triggered by scrolling vertically', function()
+ local lines = {'123', '123'}
+ meths.buf_set_lines(0, 0, -1, true, lines)
+ eq(0, eval('g:scrolled'))
+
+ feed('<C-E>')
+ eq(1, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('<C-Y>')
+ eq(2, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 0, topline = -1, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+ end)
+
+ it('is triggered by scrolling horizontally', function()
+ command('set nowrap')
+ local width = meths.win_get_width(0)
+ local line = '123' .. ('*'):rep(width * 2)
+ local lines = {line, line}
+ meths.buf_set_lines(0, 0, -1, true, lines)
+ eq(0, eval('g:scrolled'))
+
+ feed('zl')
+ eq(1, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('zh')
+ eq(2, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = -1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+ end)
+
+ it('is triggered by horizontal scrolling from cursor move', function()
+ command('set nowrap')
+ local lines = {'', '', 'Foo'}
+ meths.buf_set_lines(0, 0, -1, true, lines)
+ meths.win_set_cursor(0, {3, 0})
+ eq(0, eval('g:scrolled'))
+
+ feed('zl')
+ eq(1, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('zl')
+ eq(2, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('h')
+ eq(3, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = -1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('zh')
+ eq(4, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 1, topline = 0, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = -1, topline = 0, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+ end)
+
+ -- oldtest: Test_WinScrolled_long_wrapped()
+ it('is triggered by scrolling on a long wrapped line #19968', function()
+ local height = meths.win_get_height(0)
+ local width = meths.win_get_width(0)
+ meths.buf_set_lines(0, 0, -1, true, {('foo'):rep(height * width)})
+ meths.win_set_cursor(0, {1, height * width - 1})
+ eq(0, eval('g:scrolled'))
+
+ feed('gj')
+ eq(1, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 0, width = 0, height = 0, skipcol = width},
+ ['1000'] = {leftcol = 0, topline = 0, width = 0, height = 0, skipcol = width},
+ }, eval('g:v_event'))
+
+ feed('0')
+ eq(2, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 0, width = 0, height = 0, skipcol = width},
+ ['1000'] = {leftcol = 0, topline = 0, width = 0, height = 0, skipcol = -width},
+ }, eval('g:v_event'))
+
+ feed('$')
+ eq(3, eval('g:scrolled'))
+ end)
+
+ it('is triggered when the window scrolls in Insert mode', function()
+ local height = meths.win_get_height(0)
+ local lines = {}
+ for i = 1, height * 2 do
+ lines[i] = tostring(i)
+ end
+ meths.buf_set_lines(0, 0, -1, true, lines)
+
+ feed('M')
+ eq(0, eval('g:scrolled'))
+
+ feed('i<C-X><C-E><Esc>')
+ eq(1, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('i<C-X><C-Y><Esc>')
+ eq(2, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 0, topline = -1, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ feed('L')
+ eq(2, eval('g:scrolled'))
+
+ feed('A<CR><Esc>')
+ eq(3, eval('g:scrolled'))
+ eq({
+ all = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ ['1000'] = {leftcol = 0, topline = 1, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+ end)
+end)
+
+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'
+ })
+ local winid_str = tostring(win.id)
+ exec([[
+ let g:scrolled = 0
+ autocmd WinScrolled * let g:scrolled += 1
+ autocmd WinScrolled * let g:amatch = expand('<amatch>')
+ autocmd WinScrolled * let g:v_event = deepcopy(v:event)
+ ]])
+ eq(0, eval('g:scrolled'))
+
+ meths.input_mouse('wheel', 'down', '', 0, 3, 3)
+ eq(1, eval('g:scrolled'))
+ eq(winid_str, eval('g:amatch'))
+ eq({
+ all = {leftcol = 0, topline = 3, width = 0, height = 0, skipcol = 0},
+ [winid_str] = {leftcol = 0, topline = 3, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+
+ meths.input_mouse('wheel', 'up', '', 0, 3, 3)
+ eq(2, eval('g:scrolled'))
+ eq(tostring(win.id), eval('g:amatch'))
+ eq({
+ all = {leftcol = 0, topline = 3, width = 0, height = 0, skipcol = 0},
+ [winid_str] = {leftcol = 0, topline = -3, width = 0, height = 0, skipcol = 0},
+ }, eval('g:v_event'))
+ end)
+end)
diff --git a/test/functional/autocmd/winscrolled_spec.lua b/test/functional/autocmd/winscrolled_spec.lua
deleted file mode 100644
index 5a426fcfa1..0000000000
--- a/test/functional/autocmd/winscrolled_spec.lua
+++ /dev/null
@@ -1,162 +0,0 @@
-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
-local assert_alive = helpers.assert_alive
-
-before_each(clear)
-
-describe('WinScrolled', function()
- local win_id
-
- before_each(function()
- win_id = meths.get_current_win().id
- command(string.format('autocmd WinScrolled %d let g:matched = v:true', win_id))
- command('let g:scrolled = 0')
- command('autocmd WinScrolled * let g:scrolled += 1')
- command([[autocmd WinScrolled * let g:amatch = str2nr(expand('<amatch>'))]])
- command([[autocmd WinScrolled * let g:afile = str2nr(expand('<afile>'))]])
- end)
-
- after_each(function()
- eq(true, eval('g:matched'))
- eq(win_id, eval('g:amatch'))
- eq(win_id, eval('g:afile'))
- end)
-
- it('is triggered by scrolling vertically', function()
- local lines = {'123', '123'}
- meths.buf_set_lines(0, 0, -1, true, lines)
- eq(0, eval('g:scrolled'))
- feed('<C-E>')
- eq(1, eval('g:scrolled'))
- end)
-
- it('is triggered by scrolling horizontally', function()
- command('set nowrap')
- local width = meths.win_get_width(0)
- local line = '123' .. ('*'):rep(width * 2)
- local lines = {line, line}
- meths.buf_set_lines(0, 0, -1, true, lines)
- eq(0, eval('g:scrolled'))
- feed('zl')
- eq(1, eval('g:scrolled'))
- end)
-
- it('is triggered by horizontal scrolling from cursor move', function()
- command('set nowrap')
- local lines = {'', '', 'Foo'}
- meths.buf_set_lines(0, 0, -1, true, lines)
- meths.win_set_cursor(0, {3, 0})
- eq(0, eval('g:scrolled'))
- feed('zl')
- eq(1, eval('g:scrolled'))
- feed('zl')
- eq(2, eval('g:scrolled'))
- feed('h')
- eq(3, eval('g:scrolled'))
- end)
-
- it('is triggered by scrolling on a long wrapped line #19968', function()
- local height = meths.win_get_height(0)
- local width = meths.win_get_width(0)
- meths.buf_set_lines(0, 0, -1, true, {('foo'):rep(height * width)})
- meths.win_set_cursor(0, {1, height * width - 1})
- eq(0, eval('g:scrolled'))
- feed('gj')
- eq(1, eval('g:scrolled'))
- feed('0')
- eq(2, eval('g:scrolled'))
- feed('$')
- eq(3, eval('g:scrolled'))
- end)
-
- it('is triggered when the window scrolls in Insert mode', function()
- local height = meths.win_get_height(0)
- local lines = {}
- for i = 1, height * 2 do
- lines[i] = tostring(i)
- end
- meths.buf_set_lines(0, 0, -1, true, lines)
- feed('L')
- eq(0, eval('g:scrolled'))
- feed('A<CR><Esc>')
- eq(1, eval('g:scrolled'))
- end)
-end)
-
-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)