autocmd: introduce "once" feature

Adds a new feature to :autocmd which sets the handler to be executed at
most one times.

Before:

    augroup FooGroup
      autocmd!
      autocmd FileType foo call Foo() | autocmd! FooGroup * <buffer>
    augroup END

After:

    autocmd FileType foo once call Foo()
This commit is contained in:
Justin M. Keyes 2019-03-10 04:32:58 +01:00
parent 092e7e6c60
commit c12cf5bde7
5 changed files with 198 additions and 101 deletions

View File

@ -40,15 +40,18 @@ effects. Be careful not to destroy your text.
2. Defining autocommands *autocmd-define* 2. Defining autocommands *autocmd-define*
*:au* *:autocmd* *:au* *:autocmd*
:au[tocmd] [group] {event} {pat} [nested] {cmd} :au[tocmd] [group] {event} {pat} [once] [nested] {cmd}
Add {cmd} to the list of commands that Vim will Add {cmd} to the list of commands that Vim will
execute automatically on {event} for a file matching execute automatically on {event} for a file matching
{pat} |autocmd-patterns|. {pat} |autocmd-patterns|.
Note: A quote character is seen as argument to the Note: A quote character is seen as argument to the
:autocmd and won't start a comment. :autocmd and won't start a comment.
Vim always adds the {cmd} after existing autocommands, Nvim always adds {cmd} after existing autocommands so
so that the autocommands execute in the order in which they execute in the order in which they were defined.
they were given. See |autocmd-nested| for [nested]. See |autocmd-nested| for [nested].
*autocmd-once*
If [once] is supplied the command is executed once,
then removed ("one shot").
The special pattern <buffer> or <buffer=N> defines a buffer-local autocommand. The special pattern <buffer> or <buffer=N> defines a buffer-local autocommand.
See |autocmd-buflocal|. See |autocmd-buflocal|.
@ -116,10 +119,11 @@ prompt. When one command outputs two messages this can happen anyway.
============================================================================== ==============================================================================
3. Removing autocommands *autocmd-remove* 3. Removing autocommands *autocmd-remove*
:au[tocmd]! [group] {event} {pat} [nested] {cmd} :au[tocmd]! [group] {event} {pat} [once] [nested] {cmd}
Remove all autocommands associated with {event} and Remove all autocommands associated with {event} and
{pat}, and add the command {cmd}. See {pat}, and add the command {cmd}.
|autocmd-nested| for [nested]. See |autocmd-once| for [once].
See |autocmd-nested| for [nested].
:au[tocmd]! [group] {event} {pat} :au[tocmd]! [group] {event} {pat}
Remove all autocommands associated with {event} and Remove all autocommands associated with {event} and
@ -1323,8 +1327,7 @@ option will not cause any commands to be executed.
another extension. Example: > another extension. Example: >
:au BufEnter *.cpp so ~/.config/nvim/init_cpp.vim :au BufEnter *.cpp so ~/.config/nvim/init_cpp.vim
:au BufEnter *.cpp doau BufEnter x.c :au BufEnter *.cpp doau BufEnter x.c
< Be careful to avoid endless loops. See < Be careful to avoid endless loops. |autocmd-nested|
|autocmd-nested|.
When the [group] argument is not given, Vim executes When the [group] argument is not given, Vim executes
the autocommands for all groups. When the [group] the autocommands for all groups. When the [group]

View File

@ -133,6 +133,7 @@ Command-line highlighting:
removed in the future). removed in the future).
Commands: Commands:
|:autocmd| accepts the `once` flag
|:checkhealth| |:checkhealth|
|:cquit| can use [count] to set the exit code |:cquit| can use [count] to set the exit code
|:drop| is always available |:drop| is always available

View File

@ -20160,7 +20160,7 @@ void ex_function(exarg_T *eap)
skip_until = vim_strsave((char_u *)"."); skip_until = vim_strsave((char_u *)".");
} }
// Check for ":python <<EOF", ":lua <<EOF", etc. // heredoc: Check for ":python <<EOF", ":lua <<EOF", etc.
arg = skipwhite(skiptowhite(p)); arg = skipwhite(skiptowhite(p));
if (arg[0] == '<' && arg[1] =='<' if (arg[0] == '<' && arg[1] =='<'
&& ((p[0] == 'p' && p[1] == 'y' && ((p[0] == 'p' && p[1] == 'y'

View File

@ -99,8 +99,9 @@
// defined and will have to be executed. // defined and will have to be executed.
// //
typedef struct AutoCmd { typedef struct AutoCmd {
char_u *cmd; // The command to be executed (NULL char_u *cmd; // Command to be executed (NULL when
// when command has been removed) // command has been removed)
bool once; // "One shot": removed after execution
char nested; // If autocommands nest here char nested; // If autocommands nest here
char last; // last command in list char last; // last command in list
scid_T scriptID; // script ID where defined scid_T scriptID; // script ID where defined
@ -121,20 +122,20 @@ typedef struct AutoPat {
char last; // last pattern for apply_autocmds() char last; // last pattern for apply_autocmds()
} AutoPat; } AutoPat;
/* ///
* struct used to keep status while executing autocommands for an event. /// Struct used to keep status while executing autocommands for an event.
*/ ///
typedef struct AutoPatCmd { typedef struct AutoPatCmd {
AutoPat *curpat; /* next AutoPat to examine */ AutoPat *curpat; // next AutoPat to examine
AutoCmd *nextcmd; /* next AutoCmd to execute */ AutoCmd *nextcmd; // next AutoCmd to execute
int group; /* group being used */ int group; // group being used
char_u *fname; /* fname to match with */ char_u *fname; // fname to match with
char_u *sfname; /* sfname to match with */ char_u *sfname; // sfname to match with
char_u *tail; /* tail of fname */ char_u *tail; // tail of fname
event_T event; /* current event */ event_T event; // current event
int arg_bufnr; /* initially equal to <abuf>, set to zero when int arg_bufnr; // initially equal to <abuf>, set to zero when
buf is deleted */ // buf is deleted
struct AutoPatCmd *next; /* chain of active apc-s for auto-invalidation*/ struct AutoPatCmd *next; // chain of active apc-s for auto-invalidation
} AutoPatCmd; } AutoPatCmd;
#define AUGROUP_DEFAULT -1 /* default autocmd group */ #define AUGROUP_DEFAULT -1 /* default autocmd group */
@ -5563,29 +5564,31 @@ static void show_autocmd(AutoPat *ap, event_T event)
} }
} }
/* // Mark an autocommand handler for deletion.
* Mark an autocommand pattern for deletion.
*/
static void au_remove_pat(AutoPat *ap) static void au_remove_pat(AutoPat *ap)
{ {
xfree(ap->pat); xfree(ap->pat);
ap->pat = NULL; ap->pat = NULL;
ap->buflocal_nr = -1; ap->buflocal_nr = -1;
au_need_clean = TRUE; au_need_clean = true;
} }
/* // Mark all commands for a pattern for deletion.
* Mark all commands for a pattern for deletion.
*/
static void au_remove_cmds(AutoPat *ap) static void au_remove_cmds(AutoPat *ap)
{ {
AutoCmd *ac; for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) {
for (ac = ap->cmds; ac != NULL; ac = ac->next) {
xfree(ac->cmd); xfree(ac->cmd);
ac->cmd = NULL; ac->cmd = NULL;
} }
au_need_clean = TRUE; au_need_clean = true;
}
// Delete one command from an autocmd pattern.
static void au_del_cmd(AutoCmd *ac)
{
xfree(ac->cmd);
ac->cmd = NULL;
au_need_clean = true;
} }
/* /*
@ -5674,18 +5677,18 @@ void aubuflocal_remove(buf_T *buf)
au_cleanup(); au_cleanup();
} }
/* // Add an autocmd group name.
* Add an autocmd group name. // Return its ID. Returns AUGROUP_ERROR (< 0) for error.
* Return it's ID. Returns AUGROUP_ERROR (< 0) for error.
*/
static int au_new_group(char_u *name) static int au_new_group(char_u *name)
{ {
int i = au_find_group(name); int i = au_find_group(name);
if (i == AUGROUP_ERROR) { /* the group doesn't exist yet, add it */ if (i == AUGROUP_ERROR) { // the group doesn't exist yet, add it.
/* First try using a free entry. */ // First try using a free entry.
for (i = 0; i < augroups.ga_len; ++i) for (i = 0; i < augroups.ga_len; i++) {
if (AUGROUP_NAME(i) == NULL) if (AUGROUP_NAME(i) == NULL) {
break; break;
}
}
if (i == augroups.ga_len) { if (i == augroups.ga_len) {
ga_grow(&augroups, 1); ga_grow(&augroups, 1);
} }
@ -5701,9 +5704,7 @@ static int au_new_group(char_u *name)
static void au_del_group(char_u *name) static void au_del_group(char_u *name)
{ {
int i; int i = au_find_group(name);
i = au_find_group(name);
if (i == AUGROUP_ERROR) { // the group doesn't exist if (i == AUGROUP_ERROR) { // the group doesn't exist
EMSG2(_("E367: No such group: \"%s\""), name); EMSG2(_("E367: No such group: \"%s\""), name);
} else if (i == current_augroup) { } else if (i == current_augroup) {
@ -5760,23 +5761,22 @@ bool au_has_group(const char_u *name)
return au_find_group(name) != AUGROUP_ERROR; return au_find_group(name) != AUGROUP_ERROR;
} }
/* /// ":augroup {name}".
* ":augroup {name}".
*/
void do_augroup(char_u *arg, int del_group) void do_augroup(char_u *arg, int del_group)
{ {
if (del_group) { if (del_group) {
if (*arg == NUL) if (*arg == NUL) {
EMSG(_(e_argreq)); EMSG(_(e_argreq));
else } else {
au_del_group(arg); au_del_group(arg);
} else if (STRICMP(arg, "end") == 0) /* ":aug end": back to group 0 */ }
} else if (STRICMP(arg, "end") == 0) { // ":aug end": back to group 0
current_augroup = AUGROUP_DEFAULT; current_augroup = AUGROUP_DEFAULT;
else if (*arg) { /* ":aug xxx": switch to group xxx */ } else if (*arg) { // ":aug xxx": switch to group xxx
int i = au_new_group(arg); int i = au_new_group(arg);
if (i != AUGROUP_ERROR) if (i != AUGROUP_ERROR)
current_augroup = i; current_augroup = i;
} else { /* ":aug": list the group names */ } else { // ":aug": list the group names
msg_start(); msg_start();
for (int i = 0; i < augroups.ga_len; ++i) { for (int i = 0; i < augroups.ga_len; ++i) {
if (AUGROUP_NAME(i) != NULL) { if (AUGROUP_NAME(i) != NULL) {
@ -5957,38 +5957,38 @@ void au_event_restore(char_u *old_ei)
} }
} }
/* // Implements :autocmd.
* do_autocmd() -- implements the :autocmd command. Can be used in the // Defines an autocmd (does not execute; cf. apply_autocmds_group).
* following ways: //
* // Can be used in the following ways:
* :autocmd <event> <pat> <cmd> Add <cmd> to the list of commands that //
* will be automatically executed for <event> // :autocmd <event> <pat> <cmd> Add <cmd> to the list of commands that
* when editing a file matching <pat>, in // will be automatically executed for <event>
* the current group. // when editing a file matching <pat>, in
* :autocmd <event> <pat> Show the autocommands associated with // the current group.
* <event> and <pat>. // :autocmd <event> <pat> Show the autocommands associated with
* :autocmd <event> Show the autocommands associated with // <event> and <pat>.
* <event>. // :autocmd <event> Show the autocommands associated with
* :autocmd Show all autocommands. // <event>.
* :autocmd! <event> <pat> <cmd> Remove all autocommands associated with // :autocmd Show all autocommands.
* <event> and <pat>, and add the command // :autocmd! <event> <pat> <cmd> Remove all autocommands associated with
* <cmd>, for the current group. // <event> and <pat>, and add the command
* :autocmd! <event> <pat> Remove all autocommands associated with // <cmd>, for the current group.
* <event> and <pat> for the current group. // :autocmd! <event> <pat> Remove all autocommands associated with
* :autocmd! <event> Remove all autocommands associated with // <event> and <pat> for the current group.
* <event> for the current group. // :autocmd! <event> Remove all autocommands associated with
* :autocmd! Remove ALL autocommands for the current // <event> for the current group.
* group. // :autocmd! Remove ALL autocommands for the current
* // group.
* Multiple events and patterns may be given separated by commas. Here are //
* some examples: // Multiple events and patterns may be given separated by commas. Here are
* :autocmd bufread,bufenter *.c,*.h set tw=0 smartindent noic // some examples:
* :autocmd bufleave * set tw=79 nosmartindent ic infercase // :autocmd bufread,bufenter *.c,*.h set tw=0 smartindent noic
* // :autocmd bufleave * set tw=79 nosmartindent ic infercase
* :autocmd * *.c show all autocommands for *.c files. //
* // :autocmd * *.c show all autocommands for *.c files.
* Mostly a {group} argument can optionally appear before <event>. //
*/ // Mostly a {group} argument can optionally appear before <event>.
void do_autocmd(char_u *arg_in, int forceit) void do_autocmd(char_u *arg_in, int forceit)
{ {
char_u *arg = arg_in; char_u *arg = arg_in;
@ -5997,6 +5997,7 @@ void do_autocmd(char_u *arg_in, int forceit)
char_u *cmd; char_u *cmd;
int need_free = false; int need_free = false;
int nested = false; int nested = false;
bool once = false;
int group; int group;
if (*arg == '|') { if (*arg == '|') {
@ -6046,6 +6047,14 @@ void do_autocmd(char_u *arg_in, int forceit)
} }
} }
// Check for "once" flag.
cmd = skipwhite(cmd);
if (*cmd != NUL && STRNCMP(cmd, "once", 4) == 0
&& ascii_iswhite(cmd[4])) {
once = true;
cmd = skipwhite(cmd + 4);
}
// Check for "nested" flag. // Check for "nested" flag.
cmd = skipwhite(cmd); cmd = skipwhite(cmd);
if (*cmd != NUL && STRNCMP(cmd, "nested", 6) == 0 if (*cmd != NUL && STRNCMP(cmd, "nested", 6) == 0
@ -6081,7 +6090,8 @@ void do_autocmd(char_u *arg_in, int forceit)
if (*arg == '*' || *arg == NUL || *arg == '|') { if (*arg == '*' || *arg == NUL || *arg == '|') {
for (event_T event = (event_T)0; (int)event < (int)NUM_EVENTS; for (event_T event = (event_T)0; (int)event < (int)NUM_EVENTS;
event = (event_T)((int)event + 1)) { event = (event_T)((int)event + 1)) {
if (do_autocmd_event(event, pat, nested, cmd, forceit, group) == FAIL) { if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
== FAIL) {
break; break;
} }
} }
@ -6089,7 +6099,8 @@ void do_autocmd(char_u *arg_in, int forceit)
while (*arg && *arg != '|' && !ascii_iswhite(*arg)) { while (*arg && *arg != '|' && !ascii_iswhite(*arg)) {
event_T event = event_name2nr(arg, &arg); event_T event = event_name2nr(arg, &arg);
assert(event < NUM_EVENTS); assert(event < NUM_EVENTS);
if (do_autocmd_event(event, pat, nested, cmd, forceit, group) == FAIL) { if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group)
== FAIL) {
break; break;
} }
} }
@ -6127,14 +6138,15 @@ static int au_get_grouparg(char_u **argp)
return group; return group;
} }
/* // do_autocmd() for one event.
* do_autocmd() for one event. // Defines an autocmd (does not execute; cf. apply_autocmds_group).
* If *pat == NUL do for all patterns. //
* If *cmd == NUL show entries. // If *pat == NUL: do for all patterns.
* If forceit == TRUE delete entries. // If *cmd == NUL: show entries.
* If group is not AUGROUP_ALL, only use this group. // If forceit == TRUE: delete entries.
*/ // If group is not AUGROUP_ALL: only use this group.
static int do_autocmd_event(event_T event, char_u *pat, int nested, char_u *cmd, int forceit, int group) static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested,
char_u *cmd, int forceit, int group)
{ {
AutoPat *ap; AutoPat *ap;
AutoPat **prev_ap; AutoPat **prev_ap;
@ -6333,6 +6345,7 @@ static int do_autocmd_event(event_T event, char_u *pat, int nested, char_u *cmd,
ac->scriptID = current_SID; ac->scriptID = current_SID;
ac->next = NULL; ac->next = NULL;
*prev_ac = ac; *prev_ac = ac;
ac->once = once;
ac->nested = nested; ac->nested = nested;
} }
} }
@ -6996,7 +7009,7 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io,
patcmd.event = event; patcmd.event = event;
patcmd.arg_bufnr = autocmd_bufnr; patcmd.arg_bufnr = autocmd_bufnr;
patcmd.next = NULL; patcmd.next = NULL;
auto_next_pat(&patcmd, FALSE); auto_next_pat(&patcmd, false);
/* found one, start executing the autocommands */ /* found one, start executing the autocommands */
if (patcmd.curpat != NULL) { if (patcmd.curpat != NULL) {
@ -7020,8 +7033,11 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io,
} }
ap->last = true; ap->last = true;
check_lnums(true); // make sure cursor and topline are valid check_lnums(true); // make sure cursor and topline are valid
// Execute the autocmd. The `getnextac` callback handles iteration.
do_cmdline(NULL, getnextac, (void *)&patcmd, do_cmdline(NULL, getnextac, (void *)&patcmd,
DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT); DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT);
if (eap != NULL) { if (eap != NULL) {
(void)set_cmdarg(NULL, save_cmdarg); (void)set_cmdarg(NULL, save_cmdarg);
set_vim_var_nr(VV_CMDBANG, save_cmdbang); set_vim_var_nr(VV_CMDBANG, save_cmdbang);
@ -7233,12 +7249,18 @@ char_u *getnextac(int c, void *cookie, int indent)
verbose_leave_scroll(); verbose_leave_scroll();
} }
retval = vim_strsave(ac->cmd); retval = vim_strsave(ac->cmd);
// Remove one-shot ("once") autocmd in anticipation of its execution.
if (ac->once) {
au_del_cmd(ac);
}
autocmd_nested = ac->nested; autocmd_nested = ac->nested;
current_SID = ac->scriptID; current_SID = ac->scriptID;
if (ac->last) if (ac->last) {
acp->nextcmd = NULL; acp->nextcmd = NULL;
else } else {
acp->nextcmd = ac->next; acp->nextcmd = ac->next;
}
return retval; return retval;
} }

View File

@ -1,15 +1,18 @@
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local dedent = helpers.dedent
local eq = helpers.eq local eq = helpers.eq
local eval = helpers.eval local eval = helpers.eval
local feed = helpers.feed
local clear = helpers.clear local clear = helpers.clear
local meths = helpers.meths local meths = helpers.meths
local funcs = helpers.funcs
local expect = helpers.expect local expect = helpers.expect
local command = helpers.command local command = helpers.command
local exc_exec = helpers.exc_exec local exc_exec = helpers.exc_exec
local curbufmeths = helpers.curbufmeths local curbufmeths = helpers.curbufmeths
describe('autocmds:', function() describe('autocmd', function()
before_each(clear) before_each(clear)
it(':tabnew triggers events in the correct order', function() it(':tabnew triggers events in the correct order', function()
@ -55,4 +58,72 @@ describe('autocmds:', function()
end of test file xx]]) end of test file xx]])
end) end)
end) end)
it('once', function() -- :help autocmd-once
--
-- ":autocmd ... once" executes its handler once, then removes the handler.
--
local expected = {
'Many1',
'Once1',
'Once2',
'Many2',
'Once3',
'Many1',
'Many2',
'Many1',
'Many2',
}
command('let g:foo = []')
command('autocmd TabNew * :call add(g:foo, "Many1")')
command('autocmd TabNew * once :call add(g:foo, "Once1")')
command('autocmd TabNew * once :call add(g:foo, "Once2")')
command('autocmd TabNew * :call add(g:foo, "Many2")')
command('autocmd TabNew * once :call add(g:foo, "Once3")')
eq(dedent([[
--- Autocommands ---
TabNew
* :call add(g:foo, "Many1")
:call add(g:foo, "Once1")
:call add(g:foo, "Once2")
:call add(g:foo, "Many2")
:call add(g:foo, "Once3")]]),
funcs.execute('autocmd Tabnew'))
command('tabnew')
command('tabnew')
command('tabnew')
eq(expected, eval('g:foo'))
eq(dedent([[
--- Autocommands ---
TabNew
* :call add(g:foo, "Many1")
:call add(g:foo, "Many2")]]),
funcs.execute('autocmd Tabnew'))
--
-- ":autocmd ... once" handlers can be deleted.
--
expected = {}
command('let g:foo = []')
command('autocmd TabNew * once :call add(g:foo, "Once1")')
command('autocmd! TabNew')
command('tabnew')
eq(expected, eval('g:foo'))
--
-- ":autocmd ... <buffer> once nested"
--
expected = {
'OptionSet-Once',
'CursorMoved-Once',
}
command('let g:foo = []')
command('autocmd OptionSet binary once nested :call add(g:foo, "OptionSet-Once")')
command('autocmd CursorMoved <buffer> once nested setlocal binary|:call add(g:foo, "CursorMoved-Once")')
command("put ='foo bar baz'")
feed('0llhlh')
eq(expected, eval('g:foo'))
end)
end) end)