diff options
-rw-r--r-- | runtime/doc/lua.txt | 45 | ||||
-rw-r--r-- | runtime/lua/vim/F.lua | 8 | ||||
-rw-r--r-- | runtime/lua/vim/_editor.lua | 25 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 4 | ||||
-rw-r--r-- | src/nvim/arglist.c | 329 | ||||
-rw-r--r-- | src/nvim/autocmd.c | 2 | ||||
-rw-r--r-- | src/nvim/testdir/test_arglist.vim | 37 | ||||
-rw-r--r-- | src/nvim/testdir/test_autocmd.vim | 16 | ||||
-rw-r--r-- | test/functional/legacy/006_argument_list_spec.lua | 85 | ||||
-rw-r--r-- | test/functional/legacy/arglist_spec.lua | 228 |
10 files changed, 302 insertions, 477 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 5898d04a73..8e2270bc58 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1416,11 +1416,11 @@ defer_fn({fn}, {timeout}) *vim.defer_fn()* are safe to call. Parameters: ~ - • {fn} Callback to call once `timeout` expires - • {timeout} Number of milliseconds to wait before calling `fn` + • {fn} (function) Callback to call once `timeout` expires + • {timeout} integer Number of milliseconds to wait before calling `fn` Return: ~ - timer luv timer object + (table) timer luv timer object *vim.deprecate()* deprecate({name}, {alternative}, {version}, {plugin}, {backtrace}) @@ -1514,18 +1514,19 @@ paste({lines}, {phase}) *vim.paste()* < Parameters: ~ - • {lines} |readfile()|-style list of lines to paste. |channel-lines| - • {phase} -1: "non-streaming" paste: the call contains all lines. If - paste is "streamed", `phase` indicates the stream state: + • {lines} string[] # |readfile()|-style list of lines to paste. + |channel-lines| + • {phase} paste_phase -1: "non-streaming" paste: the call contains all + lines. If paste is "streamed", `phase` indicates the stream state: • 1: starts the paste (exactly once) • 2: continues the paste (zero or more times) • 3: ends the paste (exactly once) Return: ~ - false if client should cancel the paste. + (boolean) # false if client should cancel the paste. See also: ~ - |paste| + |paste| @alias paste_phase -1 | 1 | 2 | 3 pretty_print({...}) *vim.pretty_print()* Prints given arguments in human-readable format. Example: > @@ -1534,7 +1535,7 @@ pretty_print({...}) *vim.pretty_print()* < Return: ~ - given arguments. + any # given arguments. See also: ~ |vim.inspect()| @@ -1545,18 +1546,26 @@ region({bufnr}, {pos1}, {pos2}, {regtype}, {inclusive}) *vim.region()* Parameters: ~ • {bufnr} (number) of buffer - • {pos1} (line, column) tuple marking beginning of region - • {pos2} (line, column) tuple marking end of region - • {regtype} type of selection, see |setreg()| + • {pos1} integer[] (line, column) tuple marking beginning of + region + • {pos2} integer[] (line, column) tuple marking end of region + • {regtype} (string) type of selection, see |setreg()| • {inclusive} (boolean) indicating whether the selection is end-inclusive Return: ~ - region lua table of the form {linenr = {startcol,endcol}} + table<integer, {}> region lua table of the form {linenr = + {startcol,endcol}} schedule_wrap({cb}) *vim.schedule_wrap()* Defers callback `cb` until the Nvim API is safe to call. + Parameters: ~ + • {cb} (function) + + Return: ~ + (function) + See also: ~ |lua-loop-callbacks| |vim.schedule()| @@ -1700,10 +1709,15 @@ split({s}, {sep}, {kwargs}) *vim.split()* split("|x|y|z|", "|", {trimempty=true}) --> {'x', 'y', 'z'} < + @alias split_kwargs {plain: boolean, trimempty: boolean} | boolean | nil + + See also: ~ + |vim.gsplit()| + Parameters: ~ • {s} (string) String to split • {sep} (string) Separator or pattern - • {kwargs} (table) Keyword arguments: + • {kwargs} split_kwargs Keyword arguments: • plain: (boolean) If `true` use `sep` literally (passed to string.find) • trimempty: (boolean) If `true` remove empty items from the @@ -1712,9 +1726,6 @@ split({s}, {sep}, {kwargs}) *vim.split()* Return: ~ (table) List of split components - See also: ~ - |vim.gsplit()| - startswith({s}, {prefix}) *vim.startswith()* Tests if `s` starts with `prefix`. diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua index bca5ddf68b..3e370c0a84 100644 --- a/runtime/lua/vim/F.lua +++ b/runtime/lua/vim/F.lua @@ -2,8 +2,12 @@ local F = {} --- Returns {a} if it is not nil, otherwise returns {b}. --- ----@param a ----@param b +---@generic A +---@generic B +--- +---@param a A +---@param b B +---@return A | B function F.if_nil(a, b) if a == nil then return b diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 96a87b8bb9..75df31004f 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -152,14 +152,15 @@ do --- </pre> --- ---@see |paste| + ---@alias paste_phase -1 | 1 | 2 | 3 --- - ---@param lines |readfile()|-style list of lines to paste. |channel-lines| - ---@param phase -1: "non-streaming" paste: the call contains all lines. + ---@param lines string[] # |readfile()|-style list of lines to paste. |channel-lines| + ---@param phase paste_phase -1: "non-streaming" paste: the call contains all lines. --- If paste is "streamed", `phase` indicates the stream state: --- - 1: starts the paste (exactly once) --- - 2: continues the paste (zero or more times) --- - 3: ends the paste (exactly once) - ---@returns false if client should cancel the paste. + ---@returns boolean # false if client should cancel the paste. function vim.paste(lines, phase) local now = vim.loop.now() local is_first_chunk = phase < 2 @@ -255,6 +256,8 @@ end ---@see |lua-loop-callbacks| ---@see |vim.schedule()| ---@see |vim.in_fast_event()| +---@param cb function +---@return function function vim.schedule_wrap(cb) return function(...) local args = vim.F.pack_len(...) @@ -399,11 +402,11 @@ end --- Get a table of lines with start, end columns for a region marked by two points --- ---@param bufnr number of buffer ----@param pos1 (line, column) tuple marking beginning of region ----@param pos2 (line, column) tuple marking end of region ----@param regtype type of selection, see |setreg()| +---@param pos1 integer[] (line, column) tuple marking beginning of region +---@param pos2 integer[] (line, column) tuple marking end of region +---@param regtype string type of selection, see |setreg()| ---@param inclusive boolean indicating whether the selection is end-inclusive ----@return region lua table of the form {linenr = {startcol,endcol}} +---@return table<integer, {}> region lua table of the form {linenr = {startcol,endcol}} function vim.region(bufnr, pos1, pos2, regtype, inclusive) if not vim.api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -450,9 +453,9 @@ end --- Use to do a one-shot timer that calls `fn` --- Note: The {fn} is |vim.schedule_wrap()|ped automatically, so API functions are --- safe to call. ----@param fn Callback to call once `timeout` expires ----@param timeout Number of milliseconds to wait before calling `fn` ----@return timer luv timer object +---@param fn function Callback to call once `timeout` expires +---@param timeout integer Number of milliseconds to wait before calling `fn` +---@return table timer luv timer object function vim.defer_fn(fn, timeout) vim.validate({ fn = { fn, 'c', true } }) local timer = vim.loop.new_timer() @@ -758,7 +761,7 @@ end --- local hl_normal = vim.pretty_print(vim.api.nvim_get_hl_by_name("Normal", true)) ---</pre> ---@see |vim.inspect()| ----@return given arguments. +---@return any # given arguments. function vim.pretty_print(...) local objects = {} for i = 1, select('#', ...) do diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index c5c31b6ddf..63a932479e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -107,9 +107,11 @@ end --- ---@see |vim.gsplit()| --- +---@alias split_kwargs {plain: boolean, trimempty: boolean} | boolean | nil +--- ---@param s string String to split ---@param sep string Separator or pattern ----@param kwargs table Keyword arguments: +---@param kwargs split_kwargs Keyword arguments: --- - plain: (boolean) If `true` use `sep` literally (passed to string.find) --- - trimempty: (boolean) If `true` remove empty items from the front --- and back of the list diff --git a/src/nvim/arglist.c b/src/nvim/arglist.c index 02e3a99aab..ae174403a0 100644 --- a/src/nvim/arglist.c +++ b/src/nvim/arglist.c @@ -27,19 +27,57 @@ #include "nvim/vim.h" #include "nvim/window.h" +/// State used by the :all command to open all the files in the argument list in +/// separate windows. +typedef struct { + alist_T *alist; ///< argument list to be used + int had_tab; + bool keep_tabs; + bool forceit; + + bool use_firstwin; ///< use first window for arglist + uint8_t *opened; ///< Array of weight for which args are open: + ///< 0: not opened + ///< 1: opened in other tab + ///< 2: opened in curtab + ///< 3: opened in curtab and curwin + int opened_len; ///< length of opened[] + win_T *new_curwin; + tabpage_T *new_curtab; +} arg_all_state_T; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "arglist.c.generated.h" #endif +static char e_cannot_change_arglist_recursively[] + = N_("E1156: Cannot change the argument list recursively"); + enum { AL_SET = 1, AL_ADD = 2, AL_DEL = 3, }; +/// This flag is set whenever the argument list is being changed and calling a +/// function that might trigger an autocommand. +static bool arglist_locked = false; + +static int check_arglist_locked(void) +{ + if (arglist_locked) { + emsg(_(e_cannot_change_arglist_recursively)); + return FAIL; + } + return OK; +} + /// Clear an argument list: free all file names and reset it to zero entries. void alist_clear(alist_T *al) { + if (check_arglist_locked() == FAIL) { + return; + } #define FREE_AENTRY_FNAME(arg) xfree((arg)->ae_fname) GA_DEEP_CLEAR(&al->al_ga, aentry_T, FREE_AENTRY_FNAME); } @@ -107,13 +145,9 @@ void alist_expand(int *fnum_list, int fnum_len) /// Takes over the allocated files[] and the allocated fnames in it. void alist_set(alist_T *al, int count, char **files, int use_curbuf, int *fnum_list, int fnum_len) { - static int recursive = 0; - - if (recursive) { - emsg(_(e_au_recursive)); + if (check_arglist_locked() == FAIL) { return; } - recursive++; alist_clear(al); ga_grow(&al->al_ga, count); @@ -131,7 +165,9 @@ void alist_set(alist_T *al, int count, char **files, int use_curbuf, int *fnum_l // May set buffer name of a buffer previously used for the // argument list, so that it's re-used by alist_add. if (fnum_list != NULL && i < fnum_len) { + arglist_locked = true; buf_set_name(fnum_list[i], files[i]); + arglist_locked = false; } alist_add(al, files[i], use_curbuf ? 2 : 1); @@ -143,7 +179,6 @@ void alist_set(alist_T *al, int count, char **files, int use_curbuf, int *fnum_l if (al == &global_alist) { arg_had_last = false; } - recursive--; } /// Add file "fname" to argument list "al". @@ -155,6 +190,11 @@ void alist_add(alist_T *al, char *fname, int set_fnum) if (fname == NULL) { // don't add NULL file names return; } + if (check_arglist_locked() == FAIL) { + return; + } + arglist_locked = true; + #ifdef BACKSLASH_IN_FILENAME slash_adjust(fname); #endif @@ -164,6 +204,8 @@ void alist_add(alist_T *al, char *fname, int set_fnum) buflist_add(fname, BLN_LISTED | (set_fnum == 2 ? BLN_CURBUF : 0)); } al->al_ga.ga_len++; + + arglist_locked = false; } #if defined(BACKSLASH_IN_FILENAME) @@ -285,7 +327,7 @@ static void alist_add_list(int count, char **files, int after, bool will_edit) { int old_argcount = ARGCOUNT; ga_grow(&ALIST(curwin)->al_ga, count); - { + if (check_arglist_locked() != FAIL) { if (after < 0) { after = 0; } @@ -296,11 +338,13 @@ static void alist_add_list(int count, char **files, int after, bool will_edit) memmove(&(ARGLIST[after + count]), &(ARGLIST[after]), (size_t)(ARGCOUNT - after) * sizeof(aentry_T)); } + arglist_locked = true; for (int i = 0; i < count; i++) { const int flags = BLN_LISTED | (will_edit ? BLN_CURBUF : 0); ARGLIST[after + i].ae_fname = files[i]; ARGLIST[after + i].ae_fnum = buflist_add(files[i], flags); } + arglist_locked = false; ALIST(curwin)->al_ga.ga_len += count; if (old_argcount > 0 && curwin->w_arg_idx >= after) { curwin->w_arg_idx += count; @@ -371,6 +415,10 @@ static int do_arglist(char *str, int what, int after, bool will_edit) char **exp_files; int arg_escaped = true; + if (check_arglist_locked() == FAIL) { + return FAIL; + } + // Set default argument for ":argadd" command. if (what == AL_ADD && *str == NUL) { if (curbuf->b_ffname == NULL) { @@ -461,6 +509,9 @@ void check_arg_idx(win_T *win) void ex_args(exarg_T *eap) { if (eap->cmdidx != CMD_args) { + if (check_arglist_locked() == FAIL) { + return; + } alist_unlink(ALIST(curwin)); if (eap->cmdidx == CMD_argglobal) { ALIST(curwin) = &global_alist; @@ -469,14 +520,20 @@ void ex_args(exarg_T *eap) } } + // ":args file ..": define new argument list, handle like ":next" + // Also for ":argslocal file .." and ":argsglobal file ..". if (*eap->arg != NUL) { - // ":args file ..": define new argument list, handle like ":next" - // Also for ":argslocal file .." and ":argsglobal file ..". + if (check_arglist_locked() == FAIL) { + return; + } ex_next(eap); - } else if (eap->cmdidx == CMD_args) { - // ":args": list arguments. + return; + } + + // ":args": list arguments. + if (eap->cmdidx == CMD_args) { if (ARGCOUNT <= 0) { - return; + return; // empty argument list } char **items = xmalloc(sizeof(char *) * (size_t)ARGCOUNT); @@ -490,10 +547,14 @@ void ex_args(exarg_T *eap) } list_in_columns(items, ARGCOUNT, curwin->w_arg_idx); xfree(items); - } else if (eap->cmdidx == CMD_arglocal) { + + return; + } + + // ":argslocal": make a local copy of the global argument list. + if (eap->cmdidx == CMD_arglocal) { garray_T *gap = &curwin->w_alist->al_ga; - // ":argslocal": make a local copy of the global argument list. ga_grow(gap, GARGCOUNT); for (int i = 0; i < GARGCOUNT; i++) { @@ -686,6 +747,10 @@ void ex_argadd(exarg_T *eap) /// ":argdelete" void ex_argdelete(exarg_T *eap) { + if (check_arglist_locked() == FAIL) { + return; + } + if (eap->addr_count > 0 || *eap->arg == NUL) { // ":argdel" works like ":.argdel" if (eap->addr_count == 0) { @@ -754,76 +819,32 @@ char *alist_name(aentry_T *aep) return bp->b_fname; } -/// do_arg_all(): Open up to 'count' windows, one for each argument. -/// -/// @param forceit hide buffers in current windows -/// @param keep_tabs keep current tabs, for ":tab drop file" -static void do_arg_all(int count, int forceit, int keep_tabs) +/// Close all the windows containing files which are not in the argument list. +/// Used by the ":all" command. +static void arg_all_close_unused_windows(arg_all_state_T *aall) { - uint8_t *opened; // Array of weight for which args are open: - // 0: not opened - // 1: opened in other tab - // 2: opened in curtab - // 3: opened in curtab and curwin - - int opened_len; // length of opened[] - int use_firstwin = false; // use first window for arglist - bool tab_drop_empty_window = false; - int split_ret = OK; - bool p_ea_save; - alist_T *alist; // argument list to be used - buf_T *buf; - tabpage_T *tpnext; - int had_tab = cmdmod.cmod_tab; - win_T *old_curwin, *last_curwin; - tabpage_T *old_curtab, *last_curtab; - win_T *new_curwin = NULL; - tabpage_T *new_curtab = NULL; - - assert(firstwin != NULL); // satisfy coverity - - if (ARGCOUNT <= 0) { - // Don't give an error message. We don't want it when the ":all" command is in the .vimrc. - return; - } - setpcmark(); - - opened_len = ARGCOUNT; - opened = xcalloc((size_t)opened_len, 1); - - // Autocommands may do anything to the argument list. Make sure it's not - // freed while we are working here by "locking" it. We still have to - // watch out for its size to be changed. - alist = curwin->w_alist; - alist->al_refcount++; + win_T *old_curwin = curwin; + tabpage_T *old_curtab = curtab; - old_curwin = curwin; - old_curtab = curtab; - - // Try closing all windows that are not in the argument list. - // Also close windows that are not full width; - // When 'hidden' or "forceit" set the buffer becomes hidden. - // Windows that have a changed buffer and can't be hidden won't be closed. - // When the ":tab" modifier was used do this for all tab pages. - if (had_tab > 0) { + if (aall->had_tab > 0) { goto_tabpage_tp(first_tabpage, true, true); } for (;;) { win_T *wpnext = NULL; - tpnext = curtab->tp_next; + tabpage_T *tpnext = curtab->tp_next; for (win_T *wp = firstwin; wp != NULL; wp = wpnext) { int i; wpnext = wp->w_next; - buf = wp->w_buffer; + buf_T *buf = wp->w_buffer; if (buf->b_ffname == NULL - || (!keep_tabs && (buf->b_nwindows > 1 || wp->w_width != Columns))) { - i = opened_len; + || (!aall->keep_tabs && (buf->b_nwindows > 1 || wp->w_width != Columns))) { + i = aall->opened_len; } else { // check if the buffer in this window is in the arglist - for (i = 0; i < opened_len; i++) { - if (i < alist->al_ga.ga_len - && (AARGLIST(alist)[i].ae_fnum == buf->b_fnum - || path_full_compare(alist_name(&AARGLIST(alist)[i]), + for (i = 0; i < aall->opened_len; i++) { + if (i < aall->alist->al_ga.ga_len + && (AARGLIST(aall->alist)[i].ae_fnum == buf->b_fnum + || path_full_compare(alist_name(&AARGLIST(aall->alist)[i]), buf->b_ffname, true, true) & kEqualFiles)) { int weight = 1; @@ -835,23 +856,24 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } } - if (weight > (int)opened[i]) { - opened[i] = (uint8_t)weight; + if (weight > (int)aall->opened[i]) { + aall->opened[i] = (uint8_t)weight; if (i == 0) { - if (new_curwin != NULL) { - new_curwin->w_arg_idx = opened_len; + if (aall->new_curwin != NULL) { + aall->new_curwin->w_arg_idx = aall->opened_len; } - new_curwin = wp; - new_curtab = curtab; + aall->new_curwin = wp; + aall->new_curtab = curtab; } - } else if (keep_tabs) { - i = opened_len; + } else if (aall->keep_tabs) { + i = aall->opened_len; } - if (wp->w_alist != alist) { - // Use the current argument list for all windows containing a file from it. + if (wp->w_alist != aall->alist) { + // Use the current argument list for all windows + // containing a file from it. alist_unlink(wp->w_alist); - wp->w_alist = alist; + wp->w_alist = aall->alist; wp->w_alist->al_refcount++; } break; @@ -860,8 +882,8 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } wp->w_arg_idx = i; - if (i == opened_len && !keep_tabs) { // close this window - if (buf_hide(buf) || forceit || buf->b_nwindows > 1 + if (i == aall->opened_len && !aall->keep_tabs) { // close this window + if (buf_hide(buf) || aall->forceit || buf->b_nwindows > 1 || !bufIsChanged(buf)) { // If the buffer was changed, and we would like to hide it, try autowriting. if (!buf_hide(buf) && buf->b_nwindows <= 1 && bufIsChanged(buf)) { @@ -876,8 +898,8 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } // don't close last window if (ONE_WINDOW - && (first_tabpage->tp_next == NULL || !had_tab)) { - use_firstwin = true; + && (first_tabpage->tp_next == NULL || !aall->had_tab)) { + aall->use_firstwin = true; } else { win_close(wp, !buf_hide(buf) && !bufIsChanged(buf), false); // check if autocommands removed the next window @@ -891,7 +913,7 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } // Without the ":tab" modifier only do the current tab page. - if (had_tab == 0 || tpnext == NULL) { + if (aall->had_tab == 0 || tpnext == NULL) { break; } @@ -901,39 +923,35 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } goto_tabpage_tp(tpnext, true, true); } +} - // Open a window for files in the argument list that don't have one. - // ARGCOUNT may change while doing this, because of autocommands. - if (count > opened_len || count <= 0) { - count = opened_len; - } +/// Open up to "count" windows for the files in the argument list "aall->alist". +static void arg_all_open_windows(arg_all_state_T *aall, int count) +{ + bool tab_drop_empty_window = false; - // Don't execute Win/Buf Enter/Leave autocommands here. - autocmd_no_enter++; - autocmd_no_leave++; - last_curwin = curwin; - last_curtab = curtab; - win_enter(lastwin, false); // ":tab drop file" should re-use an empty window to avoid "--remote-tab" // leaving an empty tab page when executed locally. - if (keep_tabs && buf_is_empty(curbuf) && curbuf->b_nwindows == 1 + if (aall->keep_tabs && buf_is_empty(curbuf) && curbuf->b_nwindows == 1 && curbuf->b_ffname == NULL && !curbuf->b_changed) { - use_firstwin = true; + aall->use_firstwin = true; tab_drop_empty_window = true; } + int split_ret = OK; + for (int i = 0; i < count && !got_int; i++) { - if (alist == &global_alist && i == global_alist.al_ga.ga_len - 1) { + if (aall->alist == &global_alist && i == global_alist.al_ga.ga_len - 1) { arg_had_last = true; } - if (opened[i] > 0) { + if (aall->opened[i] > 0) { // Move the already present window to below the current window if (curwin->w_arg_idx != i) { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { if (wp->w_arg_idx == i) { - if (keep_tabs) { - new_curwin = wp; - new_curtab = curtab; + if (aall->keep_tabs) { + aall->new_curwin = wp; + aall->new_curtab = curtab; } else if (wp->w_frame->fr_parent != curwin->w_frame->fr_parent) { emsg(_("E249: window layout changed unexpectedly")); i = count; @@ -950,8 +968,8 @@ static void do_arg_all(int count, int forceit, int keep_tabs) if (tab_drop_empty_window && i == count - 1) { autocmd_no_enter--; } - if (!use_firstwin) { // split current window - p_ea_save = p_ea; + if (!aall->use_firstwin) { // split current window + bool p_ea_save = p_ea; p_ea = true; // use space from all windows split_ret = win_split(0, WSP_ROOM | WSP_BELOW); p_ea = p_ea_save; @@ -965,36 +983,101 @@ static void do_arg_all(int count, int forceit, int keep_tabs) // edit file "i" curwin->w_arg_idx = i; if (i == 0) { - new_curwin = curwin; - new_curtab = curtab; + aall->new_curwin = curwin; + aall->new_curtab = curtab; } - (void)do_ecmd(0, alist_name(&AARGLIST(alist)[i]), NULL, NULL, ECMD_ONE, + (void)do_ecmd(0, alist_name(&AARGLIST(aall->alist)[i]), NULL, NULL, ECMD_ONE, ((buf_hide(curwin->w_buffer) - || bufIsChanged(curwin->w_buffer)) - ? ECMD_HIDE : 0) + ECMD_OLDBUF, + || bufIsChanged(curwin->w_buffer)) ? ECMD_HIDE : 0) + ECMD_OLDBUF, curwin); if (tab_drop_empty_window && i == count - 1) { autocmd_no_enter++; } - if (use_firstwin) { + if (aall->use_firstwin) { autocmd_no_leave++; } - use_firstwin = false; + aall->use_firstwin = false; } os_breakcheck(); // When ":tab" was used open a new tab for a new window repeatedly. - if (had_tab > 0 && tabpage_index(NULL) <= p_tpm) { + if (aall->had_tab > 0 && tabpage_index(NULL) <= p_tpm) { cmdmod.cmod_tab = 9999; } } +} + +/// do_arg_all(): Open up to 'count' windows, one for each argument. +/// +/// @param forceit hide buffers in current windows +/// @param keep_tabs keep current tabs, for ":tab drop file" +static void do_arg_all(int count, int forceit, int keep_tabs) +{ + win_T *last_curwin; + tabpage_T *last_curtab; + bool prev_arglist_locked = arglist_locked; + + assert(firstwin != NULL); // satisfy coverity + + if (cmdwin_type != 0) { + emsg(_(e_cmdwin)); + return; + } + if (ARGCOUNT <= 0) { + // Don't give an error message. We don't want it when the ":all" command is in the .vimrc. + return; + } + setpcmark(); + + arg_all_state_T aall = { + .use_firstwin = false, + .had_tab = cmdmod.cmod_tab, + .new_curwin = NULL, + .new_curtab = NULL, + .forceit = forceit, + .keep_tabs = keep_tabs, + .opened_len = ARGCOUNT, + .opened = xcalloc((size_t)ARGCOUNT, 1), + }; + + // Autocommands may do anything to the argument list. Make sure it's not + // freed while we are working here by "locking" it. We still have to + // watch out for its size to be changed. + aall.alist = curwin->w_alist; + aall.alist->al_refcount++; + arglist_locked = true; + + // Try closing all windows that are not in the argument list. + // Also close windows that are not full width; + // When 'hidden' or "forceit" set the buffer becomes hidden. + // Windows that have a changed buffer and can't be hidden won't be closed. + // When the ":tab" modifier was used do this for all tab pages. + arg_all_close_unused_windows(&aall); + + // Open a window for files in the argument list that don't have one. + // ARGCOUNT may change while doing this, because of autocommands. + if (count > aall.opened_len || count <= 0) { + count = aall.opened_len; + } + + // Don't execute Win/Buf Enter/Leave autocommands here. + autocmd_no_enter++; + autocmd_no_leave++; + last_curwin = curwin; + last_curtab = curtab; + win_enter(lastwin, false); + + // Open upto "count" windows. + arg_all_open_windows(&aall, count); // Remove the "lock" on the argument list. - alist_unlink(alist); + alist_unlink(aall.alist); + arglist_locked = prev_arglist_locked; autocmd_no_enter--; + // restore last referenced tabpage's curwin - if (last_curtab != new_curtab) { + if (last_curtab != aall.new_curtab) { if (valid_tabpage(last_curtab)) { goto_tabpage_tp(last_curtab, true, true); } @@ -1003,15 +1086,15 @@ static void do_arg_all(int count, int forceit, int keep_tabs) } } // to window with first arg - if (valid_tabpage(new_curtab)) { - goto_tabpage_tp(new_curtab, true, true); + if (valid_tabpage(aall.new_curtab)) { + goto_tabpage_tp(aall.new_curtab, true, true); } - if (win_valid(new_curwin)) { - win_enter(new_curwin, false); + if (win_valid(aall.new_curwin)) { + win_enter(aall.new_curwin, false); } autocmd_no_leave--; - xfree(opened); + xfree(aall.opened); } /// ":all" and ":sall". diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index b87ade37d3..81d495ee27 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -881,7 +881,7 @@ void do_autocmd(char *arg_in, int forceit) } } else { if (*arg == '*' || *arg == NUL || *arg == '|') { - if (!forceit && *cmd != NUL) { + if (*cmd != NUL) { emsg(_(e_cannot_define_autocommands_for_all_events)); } else { do_all_autocmd_events(pat, once, nested, cmd, forceit, group); diff --git a/src/nvim/testdir/test_arglist.vim b/src/nvim/testdir/test_arglist.vim index 521c3fcd57..c01ae87fd8 100644 --- a/src/nvim/testdir/test_arglist.vim +++ b/src/nvim/testdir/test_arglist.vim @@ -509,18 +509,14 @@ func Test_arglist_autocmd() new " redefine arglist; go to Xxx1 next! Xxx1 Xxx2 Xxx3 - " open window for all args; Reading Xxx2 will change the arglist and the - " third window will get Xxx1: - " win 1: Xxx1 - " win 2: Xxx2 - " win 3: Xxx1 - all + " open window for all args; Reading Xxx2 will try to change the arglist and + " that will fail + call assert_fails("all", "E1156:") call assert_equal('test file Xxx1', getline(1)) wincmd w - wincmd w - call assert_equal('test file Xxx1', getline(1)) - rewind call assert_equal('test file Xxx2', getline(1)) + wincmd w + call assert_equal('test file Xxx3', getline(1)) autocmd! BufReadPost Xxx2 enew! | only @@ -591,4 +587,27 @@ func Test_quit_with_arglist() call delete('.c.swp') endfunc +" Test for ":all" not working when in the cmdline window +func Test_all_not_allowed_from_cmdwin() + au BufEnter * all + next x + " Use try/catch here, somehow assert_fails() doesn't work on MS-Windows + " console. + let caught = 'no' + try + exe ":norm! 7q?apat\<CR>" + catch /E11:/ + let caught = 'yes' + endtry + call assert_equal('yes', caught) + au! BufEnter +endfunc + +func Test_clear_arglist_in_all() + n 0 00 000 0000 00000 000000 + au WinNew 0 n 0 + call assert_fails("all", "E1156") + au! * +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index d766256d4b..025bda4515 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -148,6 +148,12 @@ func Test_autocmd_bufunload_with_tabnext() quit endfunc +func Test_argdelete_in_next() + au BufNew,BufEnter,BufLeave,BufWinEnter * argdel + call assert_fails('next a b', 'E1156:') + au! BufNew,BufEnter,BufLeave,BufWinEnter * +endfunc + func Test_autocmd_bufwinleave_with_tabfirst() tabedit augroup sample @@ -2048,6 +2054,7 @@ endfunc func Test_autocommand_all_events() call assert_fails('au * * bwipe', 'E1155:') call assert_fails('au * x bwipe', 'E1155:') + call assert_fails('au! * x bwipe', 'E1155:') endfunc func Test_autocmd_user() @@ -3004,6 +3011,15 @@ func Test_Visual_doautoall_redraw() %bwipe! endfunc +" This was using freed memory. +func Test_BufNew_arglocal() + arglocal + au BufNew * arglocal + call assert_fails('drop xx', 'E1156:') + + au! BufNew +endfunc + func Test_autocmd_closes_window() au BufNew,BufWinLeave * e %e file yyy diff --git a/test/functional/legacy/006_argument_list_spec.lua b/test/functional/legacy/006_argument_list_spec.lua deleted file mode 100644 index d269bf8ec9..0000000000 --- a/test/functional/legacy/006_argument_list_spec.lua +++ /dev/null @@ -1,85 +0,0 @@ --- Test for autocommand that redefines the argument list, when doing ":all". - -local helpers = require('test.functional.helpers')(after_each) -local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert -local command, dedent, eq = helpers.command, helpers.dedent, helpers.eq -local curbuf_contents = helpers.curbuf_contents -local poke_eventloop = helpers.poke_eventloop - -describe('argument list', function() - setup(clear) - - it('is working', function() - insert([[ - start of test file Xxx - this is a test - this is a test - this is a test - this is a test - end of test file Xxx]]) - poke_eventloop() - - command('au BufReadPost Xxx2 next Xxx2 Xxx1') - command('/^start of') - - -- Write test file Xxx1 - feed('A1<Esc>:.,/end of/w! Xxx1<cr>') - - -- Write test file Xxx2 - feed('$r2:.,/end of/w! Xxx2<cr>') - - -- Write test file Xxx3 - feed('$r3:.,/end of/w! Xxx3<cr>') - poke_eventloop() - - -- Redefine arglist; go to Xxx1 - command('next! Xxx1 Xxx2 Xxx3') - - -- Open window for all args - command('all') - - -- Write contents of Xxx1 - command('%yank A') - - -- Append contents of last window (Xxx1) - feed('') - poke_eventloop() - command('%yank A') - - -- should now be in Xxx2 - command('rew') - - -- Append contents of Xxx2 - command('%yank A') - - command('%d') - command('0put=@a') - command('$d') - - eq(dedent([[ - start of test file Xxx1 - this is a test - this is a test - this is a test - this is a test - end of test file Xxx - start of test file Xxx1 - this is a test - this is a test - this is a test - this is a test - end of test file Xxx - start of test file Xxx2 - this is a test - this is a test - this is a test - this is a test - end of test file Xxx]]), curbuf_contents()) - end) - - teardown(function() - os.remove('Xxx1') - os.remove('Xxx2') - os.remove('Xxx3') - end) -end) diff --git a/test/functional/legacy/arglist_spec.lua b/test/functional/legacy/arglist_spec.lua index 4d9e88c446..a15809907b 100644 --- a/test/functional/legacy/arglist_spec.lua +++ b/test/functional/legacy/arglist_spec.lua @@ -3,7 +3,6 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') local clear, command, eq = helpers.clear, helpers.command, helpers.eq -local eval, exc_exec, neq = helpers.eval, helpers.exc_exec, helpers.neq local expect_exit = helpers.expect_exit local feed = helpers.feed local pcall_err = helpers.pcall_err @@ -11,233 +10,6 @@ local pcall_err = helpers.pcall_err describe('argument list commands', function() before_each(clear) - local function init_abc() - command('args a b c') - command('next') - end - - local function reset_arglist() - command('arga a | %argd') - end - - local function assert_fails(cmd, err) - neq(nil, exc_exec(cmd):find(err)) - end - - it('test that argidx() works', function() - command('args a b c') - command('last') - eq(2, eval('argidx()')) - command('%argdelete') - eq(0, eval('argidx()')) - - command('args a b c') - eq(0, eval('argidx()')) - command('next') - eq(1, eval('argidx()')) - command('next') - eq(2, eval('argidx()')) - command('1argdelete') - eq(1, eval('argidx()')) - command('1argdelete') - eq(0, eval('argidx()')) - command('1argdelete') - eq(0, eval('argidx()')) - end) - - it('test that argadd() works', function() - command('%argdelete') - command('argadd a b c') - eq(0, eval('argidx()')) - - command('%argdelete') - command('argadd a') - eq(0, eval('argidx()')) - command('argadd b c d') - eq(0, eval('argidx()')) - - init_abc() - command('argadd x') - eq({'a', 'b', 'x', 'c'}, eval('argv()')) - eq(1, eval('argidx()')) - - init_abc() - command('0argadd x') - eq({'x', 'a', 'b', 'c'}, eval('argv()')) - eq(2, eval('argidx()')) - - init_abc() - command('1argadd x') - eq({'a', 'x', 'b', 'c'}, eval('argv()')) - eq(2, eval('argidx()')) - - init_abc() - command('$argadd x') - eq({'a', 'b', 'c', 'x'}, eval('argv()')) - eq(1, eval('argidx()')) - - init_abc() - command('$argadd x') - command('+2argadd y') - eq({'a', 'b', 'c', 'x', 'y'}, eval('argv()')) - eq(1, eval('argidx()')) - - command('%argd') - command('edit d') - command('arga') - eq(1, eval('len(argv())')) - eq('d', eval('get(argv(), 0, "")')) - - command('%argd') - command('new') - command('arga') - eq(0, eval('len(argv())')) - end) - - it('test for 0argadd and 0argedit', function() - reset_arglist() - - command('arga a b c d') - command('2argu') - command('0arga added') - eq({'added', 'a', 'b', 'c', 'd'}, eval('argv()')) - - command('%argd') - command('arga a b c d') - command('2argu') - command('0arge edited') - eq({'edited', 'a', 'b', 'c', 'd'}, eval('argv()')) - - command('2argu') - command('arga third') - eq({'edited', 'a', 'third', 'b', 'c', 'd'}, eval('argv()')) - end) - - it('test for argc()', function() - reset_arglist() - eq(0, eval('argc()')) - command('argadd a b') - eq(2, eval('argc()')) - end) - - it('test for arglistid()', function() - reset_arglist() - command('arga a b') - eq(0, eval('arglistid()')) - command('split') - command('arglocal') - eq(1, eval('arglistid()')) - command('tabnew | tabfirst') - eq(0, eval('arglistid(2)')) - eq(1, eval('arglistid(1, 1)')) - eq(0, eval('arglistid(2, 1)')) - eq(1, eval('arglistid(1, 2)')) - command('tabonly | only | enew!') - command('argglobal') - eq(0, eval('arglistid()')) - end) - - it('test for argv()', function() - reset_arglist() - eq({}, eval('argv()')) - eq('', eval('argv(2)')) - command('argadd a b c d') - eq('c', eval('argv(2)')) - end) - - it('test for :argedit command', function() - reset_arglist() - command('argedit a') - eq({'a'}, eval('argv()')) - eq('a', eval('expand("%:t")')) - command('argedit b') - eq({'a', 'b'}, eval('argv()')) - eq('b', eval('expand("%:t")')) - command('argedit a') - eq({'a', 'b', 'a'}, eval('argv()')) - eq('a', eval('expand("%:t")')) - command('argedit c') - eq({'a', 'b', 'a', 'c'}, eval('argv()')) - command('0argedit x') - eq({'x', 'a', 'b', 'a', 'c'}, eval('argv()')) - command('set nohidden') - command('enew! | set modified') - assert_fails('argedit y', 'E37:') - command('argedit! y') - eq({'x', 'y', 'y', 'a', 'b', 'a', 'c'}, eval('argv()')) - command('set hidden') - command('%argd') - command('argedit a b') - eq({'a', 'b'}, eval('argv()')) - end) - - it('test for :argdelete command', function() - reset_arglist() - command('args aa a aaa b bb') - command('argdelete a*') - eq({'b', 'bb'}, eval('argv()')) - eq('aa', eval('expand("%:t")')) - command('last') - command('argdelete %') - eq({'b'}, eval('argv()')) - assert_fails('argdelete', 'E610:') - assert_fails('1,100argdelete', 'E16:') - reset_arglist() - command('args a b c d') - command('next') - command('argdel') - eq({'a', 'c', 'd'}, eval('argv()')) - command('%argdel') - end) - - it('test for the :next, :prev, :first, :last, :rewind commands', function() - reset_arglist() - command('args a b c d') - command('last') - eq(3, eval('argidx()')) - assert_fails('next', 'E165:') - command('prev') - eq(2, eval('argidx()')) - command('Next') - eq(1, eval('argidx()')) - command('first') - eq(0, eval('argidx()')) - assert_fails('prev', 'E164:') - command('3next') - eq(3, eval('argidx()')) - command('rewind') - eq(0, eval('argidx()')) - command('%argd') - end) - - it('test for autocommand that redefines the argument list, when doing ":all"', function() - command('autocmd BufReadPost Xxx2 next Xxx2 Xxx1') - command("call writefile(['test file Xxx1'], 'Xxx1')") - command("call writefile(['test file Xxx2'], 'Xxx2')") - command("call writefile(['test file Xxx3'], 'Xxx3')") - - command('new') - -- redefine arglist; go to Xxx1 - command('next! Xxx1 Xxx2 Xxx3') - -- open window for all args - command('all') - eq('test file Xxx1', eval('getline(1)')) - command('wincmd w') - command('wincmd w') - eq('test file Xxx1', eval('getline(1)')) - -- should now be in Xxx2 - command('rewind') - eq('test file Xxx2', eval('getline(1)')) - - command('autocmd! BufReadPost Xxx2') - command('enew! | only') - command("call delete('Xxx1')") - command("call delete('Xxx2')") - command("call delete('Xxx3')") - command('argdelete Xxx*') - command('bwipe! Xxx1 Xxx2 Xxx3') - end) - it('quitting Vim with unedited files in the argument list throws E173', function() command('set nomore') command('args a b c') |