diff options
author | zeertzjq <zeertzjq@outlook.com> | 2022-11-23 09:54:48 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-23 09:54:48 +0800 |
commit | 4571ba4d0a5234408e544c3a98f107688a792f0d (patch) | |
tree | 0418fe6a170f8d2f6f97dd6182753dfdd89eb7bf | |
parent | d41e93d5a83642f90898cae211e017d99ff97fd9 (diff) | |
download | rneovim-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.txt | 30 | ||||
-rw-r--r-- | runtime/doc/windows.txt | 45 | ||||
-rw-r--r-- | src/nvim/auevents.lua | 3 | ||||
-rw-r--r-- | src/nvim/autocmd.c | 11 | ||||
-rw-r--r-- | src/nvim/edit.c | 3 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 34 | ||||
-rw-r--r-- | src/nvim/normal.c | 3 | ||||
-rw-r--r-- | src/nvim/testdir/test_autocmd.vim | 84 | ||||
-rw-r--r-- | src/nvim/window.c | 242 | ||||
-rw-r--r-- | test/functional/autocmd/win_scrolled_resized_spec.lua | 285 | ||||
-rw-r--r-- | test/functional/autocmd/winscrolled_spec.lua | 162 |
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) |