From 1cb60405548e79f1ec63921540e1c3ebb3ddcc01 Mon Sep 17 00:00:00 2001 From: ii14 <59243201+ii14@users.noreply.github.com> Date: Thu, 27 Apr 2023 19:25:08 +0200 Subject: [PATCH] perf(events): store autocommands in flat vectors (#23256) Instead of nested linked lists, store autocommands in a flat, contiguous kvec_t, with one kvec_t per event type. Previously patterns were stored in each node of the outer linked list, so they can be matched only once on repeating patterns. They are now reference counted and referenced in each autocommand, and matching is skipped if the pattern repeats. Speeds up creation and deletion, execution is not affected. Co-authored-by: ii14 --- src/nvim/api/autocmd.c | 167 ++-- src/nvim/autocmd.c | 982 +++++++++-------------- src/nvim/autocmd.h | 70 +- src/nvim/ex_docmd.c | 3 +- src/nvim/generators/gen_events.lua | 19 +- test/benchmark/autocmd_spec.lua | 175 ++++ test/functional/autocmd/autocmd_spec.lua | 18 + test/functional/autocmd/show_spec.lua | 41 + 8 files changed, 737 insertions(+), 738 deletions(-) create mode 100644 test/benchmark/autocmd_spec.lua diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index e606322f24..6ecbff2606 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -225,8 +225,12 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) continue; } - for (AutoPat *ap = au_get_autopat_for_event(event); ap != NULL; ap = ap->next) { - if (ap->cmds == NULL) { + AutoCmdVec *acs = au_get_autocmds_for_event(event); + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + AutoPat *const ap = ac->pat; + + if (ap == NULL) { continue; } @@ -238,19 +242,16 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) // Skip 'pattern' from invalid patterns if passed. if (pattern_filter_count > 0) { bool passed = false; - for (int i = 0; i < pattern_filter_count; i++) { - assert(i < AUCMD_MAX_PATTERNS); - assert(pattern_filters[i]); + for (int j = 0; j < pattern_filter_count; j++) { + assert(j < AUCMD_MAX_PATTERNS); + assert(pattern_filters[j]); - char *pat = pattern_filters[i]; + char *pat = pattern_filters[j]; int patlen = (int)strlen(pat); if (aupat_is_buflocal(pat, patlen)) { - aupat_normalize_buflocal_pat(pattern_buflocal, - pat, - patlen, + aupat_normalize_buflocal_pat(pattern_buflocal, pat, patlen, aupat_get_buflocal_nr(pat, patlen)); - pat = pattern_buflocal; } @@ -265,85 +266,71 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) } } - for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - if (aucmd_exec_is_deleted(ac->exec)) { - continue; - } + Dictionary autocmd_info = ARRAY_DICT_INIT; - Dictionary autocmd_info = ARRAY_DICT_INIT; - - if (ap->group != AUGROUP_DEFAULT) { - PUT(autocmd_info, "group", INTEGER_OBJ(ap->group)); - PUT(autocmd_info, "group_name", CSTR_TO_OBJ(augroup_name(ap->group))); - } - - if (ac->id > 0) { - PUT(autocmd_info, "id", INTEGER_OBJ(ac->id)); - } - - if (ac->desc != NULL) { - PUT(autocmd_info, "desc", CSTR_TO_OBJ(ac->desc)); - } - - if (ac->exec.type == CALLABLE_CB) { - PUT(autocmd_info, "command", STRING_OBJ(STRING_INIT)); - - Callback *cb = &ac->exec.callable.cb; - switch (cb->type) { - case kCallbackLua: - if (nlua_ref_is_function(cb->data.luaref)) { - PUT(autocmd_info, "callback", LUAREF_OBJ(api_new_luaref(cb->data.luaref))); - } - break; - case kCallbackFuncref: - case kCallbackPartial: - PUT(autocmd_info, "callback", STRING_OBJ(cstr_as_string(callback_to_string(cb)))); - break; - default: - abort(); - } - } else { - PUT(autocmd_info, - "command", - STRING_OBJ(cstr_as_string(xstrdup(ac->exec.callable.cmd)))); - } - - PUT(autocmd_info, - "pattern", - STRING_OBJ(cstr_to_string(ap->pat))); - - PUT(autocmd_info, - "event", - STRING_OBJ(cstr_to_string((char *)event_nr2name(event)))); - - PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); - - if (ap->buflocal_nr) { - PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); - PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); - } else { - PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); - } - - // TODO(sctx): It would be good to unify script_ctx to actually work with lua - // right now it's just super weird, and never really gives you the info that - // you would expect from this. - // - // I think we should be able to get the line number, filename, etc. from lua - // when we're executing something, and it should be easy to then save that - // info here. - // - // I think it's a big loss not getting line numbers of where options, autocmds, - // etc. are set (just getting "Sourced (lua)" or something is not that helpful. - // - // Once we do that, we can put these into the autocmd_info, but I don't think it's - // useful to do that at this time. - // - // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); - // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); - - ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); + if (ap->group != AUGROUP_DEFAULT) { + PUT(autocmd_info, "group", INTEGER_OBJ(ap->group)); + PUT(autocmd_info, "group_name", CSTR_TO_OBJ(augroup_name(ap->group))); } + + if (ac->id > 0) { + PUT(autocmd_info, "id", INTEGER_OBJ(ac->id)); + } + + if (ac->desc != NULL) { + PUT(autocmd_info, "desc", CSTR_TO_OBJ(ac->desc)); + } + + if (ac->exec.type == CALLABLE_CB) { + PUT(autocmd_info, "command", STRING_OBJ(STRING_INIT)); + + Callback *cb = &ac->exec.callable.cb; + switch (cb->type) { + case kCallbackLua: + if (nlua_ref_is_function(cb->data.luaref)) { + PUT(autocmd_info, "callback", LUAREF_OBJ(api_new_luaref(cb->data.luaref))); + } + break; + case kCallbackFuncref: + case kCallbackPartial: + PUT(autocmd_info, "callback", STRING_OBJ(cstr_as_string(callback_to_string(cb)))); + break; + default: + abort(); + } + } else { + PUT(autocmd_info, "command", STRING_OBJ(cstr_as_string(xstrdup(ac->exec.callable.cmd)))); + } + + PUT(autocmd_info, "pattern", STRING_OBJ(cstr_to_string(ap->pat))); + PUT(autocmd_info, "event", STRING_OBJ(cstr_to_string(event_nr2name(event)))); + PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); + + if (ap->buflocal_nr) { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); + PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); + } else { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); + } + + // TODO(sctx): It would be good to unify script_ctx to actually work with lua + // right now it's just super weird, and never really gives you the info that + // you would expect from this. + // + // I think we should be able to get the line number, filename, etc. from lua + // when we're executing something, and it should be easy to then save that + // info here. + // + // I think it's a big loss not getting line numbers of where options, autocmds, + // etc. are set (just getting "Sourced (lua)" or something is not that helpful. + // + // Once we do that, we can put these into the autocmd_info, but I don't think it's + // useful to do that at this time. + // + // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); + // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); + + ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); } } @@ -663,7 +650,7 @@ Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augrou if (clear_autocmds) { FOR_ALL_AUEVENTS(event) { - aupat_del_for_event_and_group(event, augroup); + aucmd_del_for_event_and_group(event, augroup); } } }); @@ -866,7 +853,7 @@ static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, Ob Object *v = &pattern; if (v->type == kObjectTypeString) { - char *pat = v->data.string.data; + const char *pat = v->data.string.data; size_t patlen = aucmd_pattern_length(pat); while (patlen) { ADD(*patterns, STRING_OBJ(cbuf_to_string(pat, patlen))); @@ -881,7 +868,7 @@ static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, Ob Array array = v->data.array; FOREACH_ITEM(array, entry, { - char *pat = entry.data.string.data; + const char *pat = entry.data.string.data; size_t patlen = aucmd_pattern_length(pat); while (patlen) { ADD(*patterns, STRING_OBJ(cbuf_to_string(pat, patlen))); diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 1d09e8f6c3..7a65f11e80 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -15,16 +15,12 @@ #include "nvim/buffer.h" #include "nvim/charset.h" #include "nvim/cursor.h" -#include "nvim/drawscreen.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/userfunc.h" #include "nvim/eval/vars.h" -#include "nvim/event/defs.h" -#include "nvim/event/loop.h" #include "nvim/ex_docmd.h" #include "nvim/ex_eval.h" -#include "nvim/ex_getln.h" #include "nvim/fileio.h" #include "nvim/garray.h" #include "nvim/getchar.h" @@ -34,7 +30,6 @@ #include "nvim/highlight_defs.h" #include "nvim/insexpand.h" #include "nvim/lua/executor.h" -#include "nvim/main.h" #include "nvim/map.h" #include "nvim/memline_defs.h" #include "nvim/memory.h" @@ -69,41 +64,13 @@ // - Groups start with augroup_ // - Events start with event_ +// The autocommands are stored in a contiguous vector for each event. // -// The autocommands are stored in a list for each event. -// Autocommands for the same pattern, that are consecutive, are joined -// together, to avoid having to match the pattern too often. -// The result is an array of Autopat lists, which point to AutoCmd lists: -// -// last_autopat[0] -----------------------------+ -// V -// first_autopat[0] --> Autopat.next --> Autopat.next --> NULL -// Autopat.cmds Autopat.cmds -// | | -// V V -// AutoCmd.next AutoCmd.next -// | | -// V V -// AutoCmd.next NULL -// | -// V -// NULL -// -// last_autopat[1] --------+ -// V -// first_autopat[1] --> Autopat.next --> NULL -// Autopat.cmds -// | -// V -// AutoCmd.next -// | -// V -// NULL -// etc. -// -// The order of AutoCmds is important, this is the order in which they were -// defined and will have to be executed. +// The order of AutoCmds is important, this is the order in which they +// were defined and will have to be executed. // +// To avoid having to match the pattern too often, patterns are reference +// counted and reused for consecutive autocommands. // Code for automatic commands. static AutoPatCmd *active_apc_list = NULL; // stack of active autocommands @@ -120,7 +87,7 @@ static int current_augroup = AUGROUP_DEFAULT; // Whether we need to delete marked patterns. // While deleting autocmds, they aren't actually remover, just marked. -static int au_need_clean = false; +static bool au_need_clean = false; static int autocmd_blocked = 0; // block all autocmds @@ -129,20 +96,16 @@ static bool autocmd_include_groups = false; static char *old_termresponse = NULL; -/// Iterates over all the AutoPats for a particular event -#define FOR_ALL_AUPATS_IN_EVENT(event, ap) \ - for (AutoPat *ap = first_autopat[event]; ap != NULL; ap = ap->next) // NOLINT - // Map of autocmd group names and ids. // name -> ID // ID -> name static Map(String, int) map_augroup_name_to_id = MAP_INIT; static Map(int, String) map_augroup_id_to_name = MAP_INIT; -static void augroup_map_del(int id, char *name) +static void augroup_map_del(int id, const char *name) { if (name != NULL) { - String key = map_key(String, int)(&map_augroup_name_to_id, cstr_as_string(name)); + String key = map_key(String, int)(&map_augroup_name_to_id, cstr_as_string((char *)name)); map_del(String, int)(&map_augroup_name_to_id, key); api_free_string(key); } @@ -161,126 +124,26 @@ static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE return deleted_augroup; } -// Show the autocommands for one AutoPat. -static void aupat_show(AutoPat *ap, event_T event, int previous_group) -{ - // Check for "got_int" (here and at various places below), which is set - // when "q" has been hit for the "--more--" prompt - if (got_int) { - return; - } - - // pattern has been removed - if (ap->pat == NULL) { - return; - } - - char *name = augroup_name(ap->group); - - msg_putchar('\n'); - if (got_int) { - return; - } - // When switching groups, we need to show the new group information. - if (ap->group != previous_group) { - // show the group name, if it's not the default group - if (ap->group != AUGROUP_DEFAULT) { - if (name == NULL) { - msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E)); - } else { - msg_puts_attr(name, HL_ATTR(HLF_T)); - } - msg_puts(" "); - } - // show the event name - msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T)); - msg_putchar('\n'); - if (got_int) { - return; - } - } - - msg_col = 4; - msg_outtrans(ap->pat); - - for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - // skip removed commands - if (aucmd_exec_is_deleted(ac->exec)) { - continue; - } - - if (msg_col >= 14) { - msg_putchar('\n'); - } - msg_col = 14; - if (got_int) { - return; - } - - char *exec_to_string = aucmd_exec_to_string(ac, ac->exec); - if (ac->desc != NULL) { - size_t msglen = 100; - char *msg = xmallocz(msglen); - if (ac->exec.type == CALLABLE_CB) { - msg_puts_attr(exec_to_string, HL_ATTR(HLF_8)); - snprintf(msg, msglen, " [%s]", ac->desc); - } else { - snprintf(msg, msglen, "%s [%s]", exec_to_string, ac->desc); - } - msg_outtrans(msg); - XFREE_CLEAR(msg); - } else if (ac->exec.type == CALLABLE_CB) { - msg_puts_attr(exec_to_string, HL_ATTR(HLF_8)); - } else { - msg_outtrans(exec_to_string); - } - XFREE_CLEAR(exec_to_string); - if (p_verbose > 0) { - last_set_msg(ac->script_ctx); - } - if (got_int) { - return; - } - if (ac->next != NULL) { - msg_putchar('\n'); - if (got_int) { - return; - } - } - } -} - -static void au_show_for_all_events(int group, char *pat) +static void au_show_for_all_events(int group, const char *pat) { FOR_ALL_AUEVENTS(event) { au_show_for_event(group, event, pat); } } -static void au_show_for_event(int group, event_T event, char *pat) +static void au_show_for_event(int group, event_T event, const char *pat) { + AutoCmdVec *const acs = &autocmds[(int)event]; // Return early if there are no autocmds for this event - if (au_event_is_empty(event)) { - return; - } - - // always need to show group information before the first pattern for the event - int previous_group = AUGROUP_ERROR; - - if (*pat == NUL) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (group == AUGROUP_ALL || ap->group == group) { - aupat_show(ap, event, previous_group); - previous_group = ap->group; - } - } + if (kv_size(*acs) == 0) { return; } char buflocal_pat[BUFLOCAL_PAT_LEN]; // for "" - // Loop through all the specified patterns. - int patlen = (int)aucmd_pattern_length(pat); - while (patlen) { + int patlen; + if (*pat != NUL) { + patlen = (int)aucmd_pattern_length(pat); + // detect special buffer-local patterns if (aupat_is_buflocal(pat, patlen)) { // normalize pat into standard "#N" form @@ -289,76 +152,163 @@ static void au_show_for_event(int group, event_T event, char *pat) patlen = (int)strlen(buflocal_pat); } + if (patlen == 0) { + return; + } assert(*pat != NUL); + } else { + pat = NULL; + patlen = 0; + } - // Find AutoPat entries with this pattern. - // always goes at or after the last one, so start at the end. - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (ap->pat != NULL) { - // Accept a pattern when: - // - a group was specified and it's that group - // - the length of the pattern matches - // - the pattern matches. - // For , this condition works because we normalize - // all buffer-local patterns. - if ((group == AUGROUP_ALL || ap->group == group) - && ap->patlen == patlen - && strncmp(pat, ap->pat, (size_t)patlen) == 0) { - // Show autocmd's for this autopat, or buflocals - aupat_show(ap, event, previous_group); - previous_group = ap->group; + // Loop through all the specified patterns. + while (true) { + AutoPat *last_ap = NULL; + int last_group = AUGROUP_ERROR; + const char *last_group_name = NULL; + + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + + // Skip deleted autocommands. + if (ac->pat == NULL) { + continue; + } + + // Accept a pattern when: + // - a group was specified and it's that group + // - the length of the pattern matches + // - the pattern matches. + // For , this condition works because we normalize + // all buffer-local patterns. + if ((group != AUGROUP_ALL && ac->pat->group != group) + || (pat != NULL + && (ac->pat->patlen != patlen || strncmp(pat, ac->pat->pat, (size_t)patlen) != 0))) { + continue; + } + + // Show event name and group only if one of them changed. + if (ac->pat->group != last_group) { + last_group = ac->pat->group; + last_group_name = augroup_name(ac->pat->group); + + if (got_int) { + return; } + + msg_putchar('\n'); + if (got_int) { + return; + } + + // When switching groups, we need to show the new group information. + // show the group name, if it's not the default group + if (ac->pat->group != AUGROUP_DEFAULT) { + if (last_group_name == NULL) { + msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E)); + } else { + msg_puts_attr(last_group_name, HL_ATTR(HLF_T)); + } + msg_puts(" "); + } + // show the event name + msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T)); + } + + // Show pattern only if it changed. + if (last_ap != ac->pat) { + last_ap = ac->pat; + + msg_putchar('\n'); + if (got_int) { + return; + } + + msg_col = 4; + msg_outtrans(ac->pat->pat); + } + + if (got_int) { + return; + } + + if (msg_col >= 14) { + msg_putchar('\n'); + } + msg_col = 14; + if (got_int) { + return; + } + + char *exec_to_string = aucmd_exec_to_string(ac, ac->exec); + if (ac->desc != NULL) { + size_t msglen = 100; + char *msg = xmallocz(msglen); + if (ac->exec.type == CALLABLE_CB) { + msg_puts_attr(exec_to_string, HL_ATTR(HLF_8)); + snprintf(msg, msglen, " [%s]", ac->desc); + } else { + snprintf(msg, msglen, "%s [%s]", exec_to_string, ac->desc); + } + msg_outtrans(msg); + XFREE_CLEAR(msg); + } else if (ac->exec.type == CALLABLE_CB) { + msg_puts_attr(exec_to_string, HL_ATTR(HLF_8)); + } else { + msg_outtrans(exec_to_string); + } + XFREE_CLEAR(exec_to_string); + if (p_verbose > 0) { + last_set_msg(ac->script_ctx); + } + + if (got_int) { + return; } } - pat = aucmd_next_pattern(pat, (size_t)patlen); - patlen = (int)aucmd_pattern_length(pat); + // If a pattern is provided, find next pattern. Otherwise exit after single iteration. + if (pat != NULL) { + pat = aucmd_next_pattern(pat, (size_t)patlen); + patlen = (int)aucmd_pattern_length(pat); + if (patlen == 0) { + break; + } + } else { + break; + } } } -// Mark an autocommand handler for deletion. -static void aupat_del(AutoPat *ap) +// Delete autocommand. +static void aucmd_del(AutoCmd *ac) { - XFREE_CLEAR(ap->pat); - ap->buflocal_nr = -1; + if (ac->pat != NULL && --ac->pat->refcount == 0) { + XFREE_CLEAR(ac->pat->pat); + vim_regfree(ac->pat->reg_prog); + xfree(ac->pat); + } + ac->pat = NULL; + aucmd_exec_free(&ac->exec); + XFREE_CLEAR(ac->desc); + au_need_clean = true; } -void aupat_del_for_event_and_group(event_T event, int group) +void aucmd_del_for_event_and_group(event_T event, int group) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (ap->group == group) { - aupat_del(ap); + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + if (ac->pat->group == group) { + aucmd_del(ac); } } au_cleanup(); } -// Mark all commands for a pattern for deletion. -static void aupat_remove_cmds(AutoPat *ap) -{ - for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - aucmd_exec_free(&ac->exec); - - if (ac->desc != NULL) { - XFREE_CLEAR(ac->desc); - } - } - au_need_clean = true; -} - -// Delete one command from an autocmd pattern. -static void aucmd_del(AutoCmd *ac) -{ - aucmd_exec_free(&ac->exec); - if (ac->desc != NULL) { - XFREE_CLEAR(ac->desc); - } - au_need_clean = true; -} - -/// Cleanup autocommands and patterns that have been deleted. +/// Cleanup autocommands that have been deleted. /// This is only done when not executing autocommands. static void au_cleanup(void) { @@ -368,72 +318,39 @@ static void au_cleanup(void) // Loop over all events. FOR_ALL_AUEVENTS(event) { - // Loop over all autocommand patterns. - AutoPat **prev_ap = &(first_autopat[(int)event]); - for (AutoPat *ap = *prev_ap; ap != NULL; ap = *prev_ap) { - bool has_cmd = false; - - // Loop over all commands for this pattern. - AutoCmd **prev_ac = &(ap->cmds); - for (AutoCmd *ac = *prev_ac; ac != NULL; ac = *prev_ac) { - // Remove the command if the pattern is to be deleted or when - // the command has been marked for deletion. - if (ap->pat == NULL || aucmd_exec_is_deleted(ac->exec)) { - *prev_ac = ac->next; - aucmd_exec_free(&ac->exec); - if (ac->desc != NULL) { - XFREE_CLEAR(ac->desc); - } - - xfree(ac); - } else { - has_cmd = true; - prev_ac = &(ac->next); - } + // Loop over all autocommands. + AutoCmdVec *const acs = &autocmds[(int)event]; + size_t nsize = 0; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + if (nsize != i) { + kv_A(*acs, nsize) = *ac; } - - if (ap->pat != NULL && !has_cmd) { - // Pattern was not marked for deletion, but all of its commands were. - // So mark the pattern for deletion. - aupat_del(ap); - } - - // Remove the pattern if it has been marked for deletion. - if (ap->pat == NULL) { - if (ap->next == NULL) { - if (prev_ap == &(first_autopat[(int)event])) { - last_autopat[(int)event] = NULL; - } else { - // this depends on the "next" field being the first in - // the struct - last_autopat[(int)event] = (AutoPat *)prev_ap; - } - } - *prev_ap = ap->next; - vim_regfree(ap->reg_prog); - xfree(ap); - } else { - prev_ap = &(ap->next); + if (ac->pat != NULL) { + nsize++; } } + if (nsize == 0) { + kv_destroy(*acs); + } else { + acs->size = nsize; + } } au_need_clean = false; } -// Get the first AutoPat for a particular event. -AutoPat *au_get_autopat_for_event(event_T event) +AutoCmdVec *au_get_autocmds_for_event(event_T event) FUNC_ATTR_PURE { - return first_autopat[(int)event]; + return &autocmds[(int)event]; } -// Called when buffer is freed, to remove/invalidate related buffer-local -// autocmds. +// Called when buffer is freed, to remove/invalidate related buffer-local autocmds. void aubuflocal_remove(buf_T *buf) { // invalidate currently executing autocommands - for (AutoPatCmd *apc = active_apc_list; apc; apc = apc->next) { + for (AutoPatCmd *apc = active_apc_list; apc != NULL; apc = apc->next) { if (buf->b_fnum == apc->arg_bufnr) { apc->arg_bufnr = 0; } @@ -441,16 +358,19 @@ void aubuflocal_remove(buf_T *buf) // invalidate buflocals looping through events FOR_ALL_AUEVENTS(event) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (ap->buflocal_nr == buf->b_fnum) { - aupat_del(ap); + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + if (ac->pat == NULL || ac->pat->buflocal_nr != buf->b_fnum) { + continue; + } - if (p_verbose >= 6) { - verbose_enter(); - smsg(_("auto-removing autocommand: %s "), - event_nr2name(event), buf->b_fnum); - verbose_leave(); - } + aucmd_del(ac); + + if (p_verbose >= 6) { + verbose_enter(); + smsg(_("auto-removing autocommand: %s "), event_nr2name(event), buf->b_fnum); + verbose_leave(); } } } @@ -459,7 +379,7 @@ void aubuflocal_remove(buf_T *buf) // Add an autocmd group name or return existing group matching name. // Return its ID. -int augroup_add(char *name) +int augroup_add(const char *name) { assert(STRICMP(name, "end") != 0); @@ -495,20 +415,21 @@ int augroup_add(char *name) /// `--- DELETED ---` groups around) void augroup_del(char *name, bool stupid_legacy_mode) { - int i = augroup_find(name); - if (i == AUGROUP_ERROR) { // the group doesn't exist + int group = augroup_find(name); + if (group == AUGROUP_ERROR) { // the group doesn't exist semsg(_("E367: No such group: \"%s\""), name); return; - } - if (i == current_augroup) { + } else if (group == current_augroup) { emsg(_("E936: Cannot delete the current group")); return; } if (stupid_legacy_mode) { FOR_ALL_AUEVENTS(event) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (ap->group == i && ap->pat != NULL) { + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoPat *const ap = kv_A(*acs, i).pat; + if (ap != NULL && ap->group == group) { give_warning(_("W19: Deleting augroup that is still in use"), true); map_put(String, int)(&map_augroup_name_to_id, cstr_as_string(name), AUGROUP_DELETED); augroup_map_del(ap->group, NULL); @@ -518,16 +439,18 @@ void augroup_del(char *name, bool stupid_legacy_mode) } } else { FOR_ALL_AUEVENTS(event) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - if (ap->group == i) { - aupat_del(ap); + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + if (ac->pat != NULL && ac->pat->group == group) { + aucmd_del(ac); } } } } // Remove the group because it's not currently in use. - augroup_map_del(i, name); + augroup_map_del(group, name); au_cleanup(); } @@ -636,14 +559,14 @@ void do_augroup(char *arg, int del_group) void free_all_autocmds(void) { FOR_ALL_AUEVENTS(event) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { - aupat_del(ap); + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + aucmd_del(&kv_A(*acs, i)); } + kv_destroy(*acs); + au_need_clean = false; } - au_need_clean = true; - au_cleanup(); - // Delete the augroup_map, including free the data String name; int id; @@ -727,8 +650,7 @@ static bool event_ignored(event_T event) while (*p != NUL) { if (STRNICMP(p, "all", 3) == 0 && (p[3] == NUL || p[3] == ',')) { return true; - } - if (event_name2nr(p, &p) == event) { + } else if (event_name2nr(p, &p) == event) { return true; } } @@ -900,7 +822,7 @@ void do_autocmd(exarg_T *eap, char *arg_in, int forceit) } } - bool is_showing = !forceit && *cmd == NUL; + const bool is_showing = !forceit && *cmd == NUL; // Print header when showing autocommands. if (is_showing) { @@ -925,8 +847,7 @@ void do_autocmd(exarg_T *eap, char *arg_in, int forceit) while (*arg && *arg != '|' && !ascii_iswhite(*arg)) { event_T event = event_name2nr(arg, &arg); assert(event < NUM_EVENTS); - if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group) - == FAIL) { + if (do_autocmd_event(event, pat, once, nested, cmd, forceit, group) == FAIL) { break; } } @@ -939,11 +860,10 @@ void do_autocmd(exarg_T *eap, char *arg_in, int forceit) xfree(envpat); } -void do_all_autocmd_events(char *pat, bool once, int nested, char *cmd, bool del, int group) +void do_all_autocmd_events(const char *pat, bool once, int nested, char *cmd, bool del, int group) { FOR_ALL_AUEVENTS(event) { - if (do_autocmd_event(event, pat, once, nested, cmd, del, group) - == FAIL) { + if (do_autocmd_event(event, pat, once, nested, cmd, del, group) == FAIL) { return; } } @@ -956,30 +876,21 @@ void do_all_autocmd_events(char *pat, bool once, int nested, char *cmd, bool del // If *cmd == NUL: show entries. // If forceit == true: delete entries. // If group is not AUGROUP_ALL: only use this group. -int do_autocmd_event(event_T event, char *pat, bool once, int nested, char *cmd, bool del, +int do_autocmd_event(event_T event, const char *pat, bool once, int nested, char *cmd, bool del, int group) FUNC_ATTR_NONNULL_ALL { // Cannot be used to show all patterns. See au_show_for_event or au_show_for_all_events assert(*pat != NUL || del); - AutoPat *ap; - AutoPat **prev_ap; - int findgroup; - int buflocal_nr; char buflocal_pat[BUFLOCAL_PAT_LEN]; // for "" bool is_adding_cmd = *cmd != NUL; - - if (group == AUGROUP_ALL) { - findgroup = current_augroup; - } else { - findgroup = group; - } + const int findgroup = group == AUGROUP_ALL ? current_augroup : group; // Delete all aupat for an event. if (*pat == NUL && del) { - aupat_del_for_event_and_group(event, findgroup); + aucmd_del_for_event_and_group(event, findgroup); return OK; } @@ -988,9 +899,8 @@ int do_autocmd_event(event_T event, char *pat, bool once, int nested, char *cmd, while (patlen) { // detect special buffer-local patterns int is_buflocal = aupat_is_buflocal(pat, patlen); - if (is_buflocal) { - buflocal_nr = aupat_get_buflocal_nr(pat, patlen); + const int buflocal_nr = aupat_get_buflocal_nr(pat, patlen); // normalize pat into standard "#N" form aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr); @@ -1002,31 +912,25 @@ int do_autocmd_event(event_T event, char *pat, bool once, int nested, char *cmd, if (del) { assert(*pat != NUL); - // Find AutoPat entries with this pattern. - prev_ap = &first_autopat[(int)event]; - while ((ap = *prev_ap) != NULL) { - if (ap->pat != NULL) { - // Accept a pattern when: - // - a group was specified and it's that group - // - the length of the pattern matches - // - the pattern matches. - // For , this condition works because we normalize - // all buffer-local patterns. - if (ap->group == findgroup - && ap->patlen == patlen - && strncmp(pat, ap->pat, (size_t)patlen) == 0) { - // Remove existing autocommands. - // If adding any new autocmd's for this AutoPat, don't - // delete the pattern from the autopat list, append to - // this list. - if (is_adding_cmd && ap->next == NULL) { - aupat_remove_cmds(ap); - break; - } - aupat_del(ap); - } + // Find existing autocommands with this pattern. + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + AutoPat *const ap = ac->pat; + // Accept a pattern when: + // - a group was specified and it's that group + // - the length of the pattern matches + // - the pattern matches. + // For , this condition works because we normalize + // all buffer-local patterns. + if (ap != NULL && ap->group == findgroup && ap->patlen == patlen + && strncmp(pat, ap->pat, (size_t)patlen) == 0) { + // Remove existing autocommands. + // If adding any new autocmd's for this AutoPat, don't + // delete the pattern from the autopat list, append to + // this list. + aucmd_del(ac); } - prev_ap = &ap->next; } } @@ -1045,32 +949,23 @@ int do_autocmd_event(event_T event, char *pat, bool once, int nested, char *cmd, return OK; } -int autocmd_register(int64_t id, event_T event, char *pat, int patlen, int group, bool once, +int autocmd_register(int64_t id, event_T event, const char *pat, int patlen, int group, bool once, bool nested, char *desc, AucmdExecutable aucmd) { // 0 is not a valid group. assert(group != 0); - AutoPat *ap; - AutoPat **prev_ap; - AutoCmd *ac; - int findgroup; - char buflocal_pat[BUFLOCAL_PAT_LEN]; // for "" - if (patlen > (int)strlen(pat)) { return FAIL; } - if (group == AUGROUP_ALL) { - findgroup = current_augroup; - } else { - findgroup = group; - } + const int findgroup = group == AUGROUP_ALL ? current_augroup : group; // detect special buffer-local patterns - int is_buflocal = aupat_is_buflocal(pat, patlen); + const int is_buflocal = aupat_is_buflocal(pat, patlen); int buflocal_nr = 0; + char buflocal_pat[BUFLOCAL_PAT_LEN]; // for "" if (is_buflocal) { buflocal_nr = aupat_get_buflocal_nr(pat, patlen); @@ -1081,67 +976,52 @@ int autocmd_register(int64_t id, event_T event, char *pat, int patlen, int group patlen = (int)strlen(buflocal_pat); } - // always goes at or after the last one, so start at the end. - if (last_autopat[(int)event] != NULL) { - prev_ap = &last_autopat[(int)event]; - } else { - prev_ap = &first_autopat[(int)event]; - } - - while ((ap = *prev_ap) != NULL) { - if (ap->pat != NULL) { - // Accept a pattern when: - // - a group was specified and it's that group - // - the length of the pattern matches - // - the pattern matches. - // For , this condition works because we normalize - // all buffer-local patterns. - if (ap->group == findgroup - && ap->patlen == patlen - && strncmp(pat, ap->pat, (size_t)patlen) == 0) { - if (ap->next == NULL) { - // Add autocmd to this autopat, if it's the last one. - break; - } - } + // Try to reuse pattern from the last existing autocommand. + AutoPat *ap = NULL; + AutoCmdVec *const acs = &autocmds[(int)event]; + for (ptrdiff_t i = (ptrdiff_t)kv_size(*acs) - 1; i >= 0; i--) { + ap = kv_A(*acs, i).pat; + if (ap == NULL) { + continue; // Skip deleted autocommands. } - prev_ap = &ap->next; + // Set result back to NULL if the last pattern doesn't match. + if (ap->group != findgroup || ap->patlen != patlen + || strncmp(pat, ap->pat, (size_t)patlen) != 0) { + ap = NULL; + } + break; } - // If the pattern we want to add a command to does appear at the - // end of the list (or not is not in the list at all), add the - // pattern at the end of the list. + // No matching pattern found, allocate a new one. if (ap == NULL) { // refuse to add buffer-local ap if buffer number is invalid - if (is_buflocal - && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) { + if (is_buflocal && (buflocal_nr == 0 || buflist_findnr(buflocal_nr) == NULL)) { semsg(_("E680: : invalid buffer number "), buflocal_nr); return FAIL; } ap = xmalloc(sizeof(AutoPat)); - ap->pat = xstrnsave(pat, (size_t)patlen); - ap->patlen = patlen; if (is_buflocal) { ap->buflocal_nr = buflocal_nr; ap->reg_prog = NULL; } else { - char *reg_pat; - ap->buflocal_nr = 0; - reg_pat = file_pat_to_reg_pat(pat, pat + patlen, &ap->allow_dirs, true); + char *reg_pat = file_pat_to_reg_pat(pat, pat + patlen, &ap->allow_dirs, true); if (reg_pat != NULL) { ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC); } xfree(reg_pat); if (reg_pat == NULL || ap->reg_prog == NULL) { - xfree(ap->pat); xfree(ap); return FAIL; } } + ap->refcount = 0; + ap->pat = xstrnsave(pat, (size_t)patlen); + ap->patlen = patlen; + // need to initialize last_mode for the first ModeChanged autocmd if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) { get_mode(last_mode); @@ -1168,53 +1048,34 @@ int autocmd_register(int64_t id, event_T event, char *pat, int patlen, int group use_tabpage(save_curtab); } - ap->cmds = NULL; - *prev_ap = ap; - last_autopat[(int)event] = ap; - ap->next = NULL; - if (group == AUGROUP_ALL) { - ap->group = current_augroup; - } else { - ap->group = group; - } + ap->group = group == AUGROUP_ALL ? current_augroup : group; } - // Add the autocmd at the end of the AutoCmd list. - AutoCmd **prev_ac = &(ap->cmds); - while ((ac = *prev_ac) != NULL) { - prev_ac = &ac->next; - } - - ac = xmalloc(sizeof(AutoCmd)); - *prev_ac = ac; + ap->refcount++; + // Add the autocmd at the end of the AutoCmd vector. + AutoCmd *ac = kv_pushp(autocmds[(int)event]); + ac->pat = ap; ac->id = id; ac->exec = aucmd_exec_copy(aucmd); ac->script_ctx = current_sctx; ac->script_ctx.sc_lnum += SOURCING_LNUM; nlua_set_sctx(&ac->script_ctx); - ac->next = NULL; ac->once = once; ac->nested = nested; - ac->desc = NULL; - - // TODO(tjdevries): What to do about :autocmd and where/how to show lua stuffs there. - // perhaps: DESCRIPTION or similar - if (desc != NULL) { - ac->desc = xstrdup(desc); - } + ac->desc = desc == NULL ? NULL : xstrdup(desc); return OK; } -size_t aucmd_pattern_length(char *pat) +size_t aucmd_pattern_length(const char *pat) FUNC_ATTR_PURE { if (*pat == NUL) { return 0; } - char *endpat; + const char *endpat; for (; *pat; pat = endpat + 1) { // Find end of the pattern. @@ -1225,8 +1086,7 @@ size_t aucmd_pattern_length(char *pat) continue; } int brace_level = 0; - for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); - endpat++) { + for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); endpat++) { if (*endpat == '{') { brace_level++; } else if (*endpat == '}') { @@ -1240,14 +1100,13 @@ size_t aucmd_pattern_length(char *pat) return strlen(pat); } -char *aucmd_next_pattern(char *pat, size_t patlen) +const char *aucmd_next_pattern(const char *pat, size_t patlen) FUNC_ATTR_PURE { pat = pat + patlen; if (*pat == ',') { pat = pat + 1; } - return pat; } @@ -1637,8 +1496,7 @@ bool apply_autocmds_retval(event_T event, char *fname, char *fname_io, bool forc return false; } - bool did_cmd = apply_autocmds_group(event, fname, fname_io, force, - AUGROUP_ALL, buf, NULL, NULL); + bool did_cmd = apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, NULL, NULL); if (did_cmd && aborting()) { *retval = FAIL; } @@ -1650,7 +1508,7 @@ bool apply_autocmds_retval(event_T event, char *fname, char *fname_io, bool forc /// @param event the autocommand to check bool has_event(event_T event) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - return first_autopat[event] != NULL; + return kv_size(autocmds[(int)event]) != 0; } /// Return true when there is a CursorHold/CursorHoldI autocommand defined for @@ -1658,7 +1516,6 @@ bool has_event(event_T event) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT bool has_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { return has_event((get_real_state() == MODE_NORMAL_BUSY ? EVENT_CURSORHOLD : EVENT_CURSORHOLDI)); - // return first_autopat[] != NULL; } /// Return true if the CursorHold/CursorHoldI event can be triggered. @@ -1692,7 +1549,6 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force char *sfname = NULL; // short file name bool retval = false; static int nesting = 0; - AutoPat *ap; char *save_cmdarg; long save_cmdbang; static int filechangeshell_busy = false; @@ -1703,8 +1559,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force // Quickly return if there are no autocommands for this event or // autocommands are blocked. - if (event == NUM_EVENTS || first_autopat[(int)event] == NULL - || is_autocmd_blocked()) { + if (event == NUM_EVENTS || kv_size(autocmds[(int)event]) == 0 || is_autocmd_blocked()) { goto BYPASS_AU; } @@ -1722,8 +1577,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force // FileChangedShell never nests, because it can create an endless loop. if (filechangeshell_busy - && (event == EVENT_FILECHANGEDSHELL - || event == EVENT_FILECHANGEDSHELLPOST)) { + && (event == EVENT_FILECHANGEDSHELL || event == EVENT_FILECHANGEDSHELLPOST)) { goto BYPASS_AU; } @@ -1742,8 +1596,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force // Check if these autocommands are disabled. Used when doing ":all" or // ":ball". if ((autocmd_no_enter && (event == EVENT_WINENTER || event == EVENT_BUFENTER)) - || (autocmd_no_leave - && (event == EVENT_WINLEAVE || event == EVENT_BUFLEAVE))) { + || (autocmd_no_leave && (event == EVENT_WINLEAVE || event == EVENT_BUFLEAVE))) { goto BYPASS_AU; } @@ -1779,11 +1632,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force } // Set the buffer number to be used for . - if (buf == NULL) { - autocmd_bufnr = 0; - } else { - autocmd_bufnr = buf->b_fnum; - } + autocmd_bufnr = buf == NULL ? 0 : buf->b_fnum; // When the file name is NULL or empty, use the file name of buffer "buf". // Always use the full path of the file name to match with, in case @@ -1886,18 +1735,24 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force // Find first autocommand that matches AutoPatCmd patcmd = { - .curpat = first_autopat[(int)event], - .group = group, + // aucmd_next will set lastpat back to NULL if there are no more autocommands left to run + .lastpat = NULL, + // current autocommand index + .auidx = 0, + // save vector size, to avoid an endless loop when more patterns + // are added when executing autocommands + .ausize = kv_size(autocmds[(int)event]), .fname = fname, .sfname = sfname, .tail = tail, + .group = group, .event = event, .arg_bufnr = autocmd_bufnr, }; - auto_next_pat(&patcmd, false); + aucmd_next(&patcmd); - // found one, start executing the autocommands - if (patcmd.curpat != NULL) { + // Found first autocommand, start executing them + if (patcmd.lastpat != NULL) { // add to active_apc_list patcmd.next = active_apc_list; active_apc_list = &patcmd; @@ -1914,12 +1769,6 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force save_cmdarg = NULL; // avoid gcc warning } retval = true; - // mark the last pattern, to avoid an endless loop when more patterns - // are added when executing autocommands - for (ap = patcmd.curpat; ap->next != NULL; ap = ap->next) { - ap->last = false; - } - ap->last = true; // Make sure cursor and topline are valid. The first time the current // values are saved, restored by reset_lnums(). When nested only the @@ -1931,8 +1780,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force } // Execute the autocmd. The `getnextac` callback handles iteration. - do_cmdline(NULL, getnextac, (void *)&patcmd, - DOCMD_NOWAIT | DOCMD_VERBOSE | DOCMD_REPEAT); + do_cmdline(NULL, getnextac, (void *)&patcmd, DOCMD_NOWAIT | DOCMD_VERBOSE | DOCMD_REPEAT); if (nesting == 1) { // restore cursor and topline, unless they were changed @@ -2038,8 +1886,7 @@ void unblock_autocmds(void) // When v:termresponse was set while autocommands were blocked, trigger // the autocommands now. Esp. useful when executing a shell command // during startup (nvim -d). - if (!is_autocmd_blocked() - && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) { + if (!is_autocmd_blocked() && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) { apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf); } } @@ -2050,77 +1897,70 @@ bool is_autocmd_blocked(void) return autocmd_blocked != 0; } -/// Find next autocommand pattern that matches. -/// stop when 'last' flag is set -void auto_next_pat(AutoPatCmd *apc, int stop_at_last) +/// Find next matching autocommand. +/// If next autocommand was not found, sets lastpat to NULL and cmdidx to SIZE_MAX on apc. +static void aucmd_next(AutoPatCmd *apc) { - AutoPat *ap; - AutoCmd *cp; - char *s; - estack_T *const entry = ((estack_T *)exestack.ga_data) + exestack.ga_len - 1; + AutoCmdVec *const acs = &autocmds[(int)apc->event]; + assert(apc->ausize <= kv_size(*acs)); + for (size_t i = apc->auidx; i < apc->ausize && !got_int; i++) { + AutoCmd *const ac = &kv_A(*acs, i); + AutoPat *const ap = ac->pat; + + // Skip deleted autocommands. + if (ap == NULL) { + continue; + } + // Skip matching if pattern didn't change. + if (ap != apc->lastpat) { + // Skip autocommands that don't match the group. + if (apc->group != AUGROUP_ALL && apc->group != ap->group) { + continue; + } + // Skip autocommands that don't match the pattern or buffer number. + if (ap->buflocal_nr == 0 + ? !match_file_pat(NULL, &ap->reg_prog, apc->fname, apc->sfname, apc->tail, ap->allow_dirs) + : ap->buflocal_nr != apc->arg_bufnr) { + continue; + } + + const char *const name = event_nr2name(apc->event); + const char *const s = _("%s Autocommands for \"%s\""); + + const size_t sourcing_name_len = strlen(s) + strlen(name) + (size_t)ap->patlen + 1; + char *const namep = xmalloc(sourcing_name_len); + snprintf(namep, sourcing_name_len, s, name, ap->pat); + if (p_verbose >= 8) { + verbose_enter(); + smsg(_("Executing %s"), namep); + verbose_leave(); + } + + // Update the exestack entry for this autocmd. + XFREE_CLEAR(entry->es_name); + entry->es_name = namep; + entry->es_info.aucmd = apc; + } + + apc->lastpat = ap; + apc->auidx = i; + + line_breakcheck(); + return; + } + // Clear the exestack entry for this ETYPE_AUCMD entry. XFREE_CLEAR(entry->es_name); entry->es_info.aucmd = NULL; - for (ap = apc->curpat; ap != NULL && !got_int; ap = ap->next) { - apc->curpat = NULL; - - // Only use a pattern when it has not been removed, has commands and - // the group matches. For buffer-local autocommands only check the - // buffer number. - if (ap->pat != NULL && ap->cmds != NULL - && (apc->group == AUGROUP_ALL || apc->group == ap->group)) { - // execution-condition - if (ap->buflocal_nr == 0 - ? match_file_pat(NULL, - &ap->reg_prog, - apc->fname, - apc->sfname, - apc->tail, - ap->allow_dirs) - : ap->buflocal_nr == apc->arg_bufnr) { - const char *const name = event_nr2name(apc->event); - s = _("%s Autocommands for \"%s\""); - - const size_t sourcing_name_len - = (strlen(s) + strlen(name) + (size_t)ap->patlen + 1); - - char *const namep = xmalloc(sourcing_name_len); - snprintf(namep, sourcing_name_len, s, name, ap->pat); - if (p_verbose >= 8) { - verbose_enter(); - smsg(_("Executing %s"), namep); - verbose_leave(); - } - - // Update the exestack entry for this autocmd. - entry->es_name = namep; - entry->es_info.aucmd = apc; - - apc->curpat = ap; - apc->nextcmd = ap->cmds; - // mark last command - for (cp = ap->cmds; cp->next != NULL; cp = cp->next) { - cp->last = false; - } - cp->last = true; - } - line_breakcheck(); - if (apc->curpat != NULL) { // found a match - break; - } - } - if (stop_at_last && ap->last) { - break; - } - } + apc->lastpat = NULL; + apc->auidx = SIZE_MAX; } static bool call_autocmd_callback(const AutoCmd *ac, const AutoPatCmd *apc) { - bool ret = false; Callback callback = ac->exec.callable.cb; if (callback.type == kCallbackLua) { Dictionary data = ARRAY_DICT_INIT; @@ -2134,7 +1974,7 @@ static bool call_autocmd_callback(const AutoCmd *ac, const AutoPatCmd *apc) PUT(data, "data", copy_object(*apc->data, NULL)); } - int group = apc->curpat->group; + int group = ac->pat->group; switch (group) { case AUGROUP_ERROR: abort(); // unreachable @@ -2152,18 +1992,19 @@ static bool call_autocmd_callback(const AutoCmd *ac, const AutoPatCmd *apc) ADD_C(args, DICTIONARY_OBJ(data)); Object result = nlua_call_ref(callback.data.luaref, NULL, args, true, NULL); + bool ret = false; if (result.type == kObjectTypeBoolean) { ret = result.data.boolean; } api_free_dictionary(data); api_free_object(result); + return ret; } else { typval_T argsin = TV_INITIAL_VALUE; typval_T rettv = TV_INITIAL_VALUE; callback_call(&callback, 0, &argsin, &rettv); + return false; } - - return ret; } /// Get next autocommand command. @@ -2176,45 +2017,16 @@ char *getnextac(int c, void *cookie, int indent, bool do_concat) (void)indent; (void)do_concat; - AutoPatCmd *acp = (AutoPatCmd *)cookie; - char *retval; + AutoPatCmd *const apc = (AutoPatCmd *)cookie; + AutoCmdVec *const acs = &autocmds[(int)apc->event]; - // Can be called again after returning the last line. - if (acp->curpat == NULL) { + aucmd_next(apc); + if (apc->lastpat == NULL) { return NULL; } - // repeat until we find an autocommand to execute - while (true) { - // skip removed commands - while (acp->nextcmd != NULL - && aucmd_exec_is_deleted(acp->nextcmd->exec)) { - if (acp->nextcmd->last) { - acp->nextcmd = NULL; - } else { - acp->nextcmd = acp->nextcmd->next; - } - } - - if (acp->nextcmd != NULL) { - break; - } - - // at end of commands, find next pattern that matches - if (acp->curpat->last) { - acp->curpat = NULL; - } else { - acp->curpat = acp->curpat->next; - } - if (acp->curpat != NULL) { - auto_next_pat(acp, true); - } - if (acp->curpat == NULL) { - return NULL; - } - } - - AutoCmd *ac = acp->nextcmd; + assert(apc->auidx < kv_size(*acs)); + AutoCmd *const ac = &kv_A(*acs, apc->auidx); bool oneshot = ac->once; if (p_verbose >= 9) { @@ -2230,10 +2042,12 @@ char *getnextac(int c, void *cookie, int indent, bool do_concat) // lua code, so that it works properly autocmd_nested = ac->nested; current_sctx = ac->script_ctx; - acp->script_ctx = current_sctx; + apc->script_ctx = current_sctx; + char *retval; if (ac->exec.type == CALLABLE_CB) { - if (call_autocmd_callback(ac, acp)) { + // Can potentially reallocate kvec_t data and invalidate the ac pointer + if (call_autocmd_callback(ac, apc)) { // If an autocommand callback returns true, delete the autocommand oneshot = true; } @@ -2248,19 +2062,20 @@ char *getnextac(int c, void *cookie, int indent, bool do_concat) // 2. make where we call do_cmdline for autocmds not have to return anything, // and instead we loop over all the matches and just execute one-by-one. // However, my expectation would be that could be expensive. - retval = xstrdup(""); + retval = xcalloc(1, 1); } else { retval = xstrdup(ac->exec.callable.cmd); } // Remove one-shot ("once") autocmd in anticipation of its execution. if (oneshot) { - aucmd_del(ac); + aucmd_del(&kv_A(*acs, apc->auidx)); } - if (ac->last) { - acp->nextcmd = NULL; + + if (apc->auidx < apc->ausize) { + apc->auidx++; } else { - acp->nextcmd = ac->next; + apc->auidx = SIZE_MAX; } return retval; @@ -2292,15 +2107,12 @@ bool has_autocmd(event_T event, char *sfname, buf_T *buf) forward_slash(fname); #endif - for (AutoPat *ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) { - if (ap->pat != NULL && ap->cmds != NULL + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoPat *const ap = kv_A(*acs, i).pat; + if (ap != NULL && (ap->buflocal_nr == 0 - ? match_file_pat(NULL, - &ap->reg_prog, - fname, - sfname, - tail, - ap->allow_dirs) + ? match_file_pat(NULL, &ap->reg_prog, fname, sfname, tail, ap->allow_dirs) : buf != NULL && ap->buflocal_nr == buf->b_fnum)) { retval = true; break; @@ -2315,13 +2127,10 @@ bool has_autocmd(event_T event, char *sfname, buf_T *buf) return retval; } -// Function given to ExpandGeneric() to obtain the list of autocommand group -// names. +// Function given to ExpandGeneric() to obtain the list of autocommand group names. char *expand_get_augroup_name(expand_T *xp, int idx) { - // Required for ExpandGeneric - (void)xp; - + (void)xp; // Required for ExpandGeneric return augroup_name(idx + 1); } @@ -2374,8 +2183,7 @@ char *set_context_in_autocmd(expand_T *xp, char *arg, int doautocmd) // Function given to ExpandGeneric() to obtain the list of event names. char *expand_get_event_name(expand_T *xp, int idx) { - // xp is a required parameter to be used with ExpandGeneric - (void)xp; + (void)xp; // xp is a required parameter to be used with ExpandGeneric // List group names char *name = augroup_name(idx + 1); @@ -2416,7 +2224,8 @@ bool autocmd_supported(const char *const event) /// exists("#Event#pat") /// /// @param arg autocommand string -bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT +bool au_exists(const char *const arg) + FUNC_ATTR_WARN_UNUSED_RESULT { buf_T *buflocal_buf = NULL; bool retval = false; @@ -2463,8 +2272,8 @@ bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT // Find the first autocommand for this event. // If there isn't any, return false; // If there is one and no pattern given, return true; - AutoPat *ap = first_autopat[(int)event]; - if (ap == NULL) { + AutoCmdVec *const acs = &autocmds[(int)event]; + if (kv_size(*acs) == 0) { goto theend; } @@ -2475,11 +2284,11 @@ bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT } // Check if there is an autocommand with the given pattern. - for (; ap != NULL; ap = ap->next) { - // only use a pattern when it has not been removed and has commands. + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoPat *const ap = kv_A(*acs, i).pat; + // Only use a pattern when it has not been removed. // For buffer-local autocommands, path_fnamecmp() works fine. - if (ap->pat != NULL && ap->cmds != NULL - && (group == AUGROUP_ALL || ap->group == group) + if ((group == AUGROUP_ALL || ap->group == group) && (pattern == NULL || (buflocal_buf == NULL ? path_fnamecmp(ap->pat, pattern) == 0 @@ -2495,15 +2304,13 @@ theend: } // Checks if a pattern is buflocal -bool aupat_is_buflocal(char *pat, int patlen) +bool aupat_is_buflocal(const char *pat, int patlen) FUNC_ATTR_PURE { - return patlen >= 8 - && strncmp(pat, "'; + return patlen >= 8 && strncmp(pat, "'; } -int aupat_get_buflocal_nr(char *pat, int patlen) +int aupat_get_buflocal_nr(const char *pat, int patlen) { assert(aupat_is_buflocal(pat, patlen)); @@ -2528,7 +2335,7 @@ int aupat_get_buflocal_nr(char *pat, int patlen) } // normalize buffer pattern -void aupat_normalize_buflocal_pat(char *dest, char *pat, int patlen, int buflocal_nr) +void aupat_normalize_buflocal_pat(char *dest, const char *pat, int patlen, int buflocal_nr) { assert(aupat_is_buflocal(pat, patlen)); @@ -2537,13 +2344,10 @@ void aupat_normalize_buflocal_pat(char *dest, char *pat, int patlen, int bufloca } // normalize pat into standard "#N" form - snprintf(dest, - BUFLOCAL_PAT_LEN, - "", - buflocal_nr); + snprintf(dest, BUFLOCAL_PAT_LEN, "", buflocal_nr); } -int autocmd_delete_event(int group, event_T event, char *pat) +int autocmd_delete_event(int group, event_T event, const char *pat) FUNC_ATTR_NONNULL_ALL { return do_autocmd_event(event, pat, false, false, "", true, group); @@ -2559,12 +2363,12 @@ bool autocmd_delete_id(int64_t id) // Note that since multiple AutoCmd objects can have the same ID, we need to do a full scan. FOR_ALL_AUEVENTS(event) { - FOR_ALL_AUPATS_IN_EVENT(event, ap) { // -V756 - for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - if (ac->id == id) { - aucmd_del(ac); - success = true; - } + AutoCmdVec *const acs = &autocmds[(int)event]; + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + if (ac->id == id) { + aucmd_del(ac); + success = true; } } } @@ -2627,25 +2431,10 @@ AucmdExecutable aucmd_exec_copy(AucmdExecutable src) abort(); } -bool aucmd_exec_is_deleted(AucmdExecutable acc) - FUNC_ATTR_PURE -{ - switch (acc.type) { - case CALLABLE_EX: - return acc.callable.cmd == NULL; - case CALLABLE_CB: - return acc.callable.cb.type == kCallbackNone; - case CALLABLE_NONE: - return true; - } - - abort(); -} - bool au_event_is_empty(event_T event) FUNC_ATTR_PURE { - return first_autopat[event] == NULL; + return kv_size(autocmds[(int)event]) == 0; } // Arg Parsing Functions @@ -2734,8 +2523,7 @@ void do_autocmd_uienter(uint64_t chanid, bool attached) assert(chanid < VARNUMBER_MAX); tv_dict_add_nr(dict, S_LEN("chan"), (varnumber_T)chanid); tv_dict_set_keys_readonly(dict); - apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE, - NULL, NULL, false, curbuf); + apply_autocmds(attached ? EVENT_UIENTER : EVENT_UILEAVE, NULL, NULL, false, curbuf); restore_v_event(dict, &save_v_event); recursive = false; diff --git a/src/nvim/autocmd.h b/src/nvim/autocmd.h index 6dbd18ba7c..9e6c534581 100644 --- a/src/nvim/autocmd.h +++ b/src/nvim/autocmd.h @@ -12,9 +12,7 @@ #include "nvim/regexp_defs.h" #include "nvim/types.h" -struct AutoCmd_S; struct AutoPatCmd_S; -struct AutoPat_S; // event_T definition #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -35,49 +33,45 @@ typedef struct { int save_State; ///< saved State } aco_save_T; -typedef struct AutoCmd_S AutoCmd; -struct AutoCmd_S { - AucmdExecutable exec; - bool once; // "One shot": removed after execution - bool nested; // If autocommands nest here - bool last; // last command in list - int64_t id; // ID used for uniquely tracking an autocmd. - sctx_T script_ctx; // script context where it is defined - char *desc; // Description for the autocmd. - AutoCmd *next; // Next AutoCmd in list -}; +typedef struct { + size_t refcount; ///< Reference count (freed when reaches zero) + char *pat; ///< Pattern as typed + regprog_T *reg_prog; ///< Compiled regprog for pattern + int group; ///< Group ID + int patlen; ///< strlen() of pat + int buflocal_nr; ///< !=0 for buffer-local AutoPat + char allow_dirs; ///< Pattern may match whole path +} AutoPat; -typedef struct AutoPat_S AutoPat; -struct AutoPat_S { - AutoPat *next; // next AutoPat in AutoPat list; MUST - // be the first entry - char *pat; // pattern as typed (NULL when pattern - // has been removed) - regprog_T *reg_prog; // compiled regprog for pattern - AutoCmd *cmds; // list of commands to do - int group; // group ID - int patlen; // strlen() of pat - int buflocal_nr; // !=0 for buffer-local AutoPat - char allow_dirs; // Pattern may match whole path - char last; // last pattern for apply_autocmds() -}; +typedef struct { + AucmdExecutable exec; ///< Command or callback function + AutoPat *pat; ///< Pattern reference (NULL when autocmd was removed) + int64_t id; ///< ID used for uniquely tracking an autocmd + char *desc; ///< Description for the autocmd + sctx_T script_ctx; ///< Script context where it is defined + bool once; ///< "One shot": removed after execution + bool nested; ///< If autocommands nest here +} AutoCmd; /// Struct used to keep status while executing autocommands for an event. typedef struct AutoPatCmd_S AutoPatCmd; struct AutoPatCmd_S { - AutoPat *curpat; // next AutoPat to examine - AutoCmd *nextcmd; // next AutoCmd to execute - int group; // group being used - char *fname; // fname to match with - char *sfname; // sfname to match with - char *tail; // tail of fname - event_T event; // current event - sctx_T script_ctx; // script context where it is defined - int arg_bufnr; // initially equal to , set to zero when buf is deleted - Object *data; // arbitrary data - AutoPatCmd *next; // chain of active apc-s for auto-invalidation + AutoPat *lastpat; ///< Last matched AutoPat + size_t auidx; ///< Current autocmd index to execute + size_t ausize; ///< Saved AutoCmd vector size + char *fname; ///< Fname to match with + char *sfname; ///< Sfname to match with + char *tail; ///< Tail of fname + int group; ///< Group being used + event_T event; ///< Current event + sctx_T script_ctx; ///< Script context where it is defined + int arg_bufnr; ///< Initially equal to , set to zero when buf is deleted + Object *data; ///< Arbitrary data + AutoPatCmd *next; ///< Chain of active apc-s for auto-invalidation }; +typedef kvec_t(AutoCmd) AutoCmdVec; + // Set by the apply_autocmds_group function if the given event is equal to // EVENT_FILETYPE. Used by the readfile function in order to determine if // EVENT_BUFREADPOST triggered the EVENT_FILETYPE. diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 8a2dabaf30..0e6d4bba1b 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -1885,8 +1885,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter // avoid that a function call in 'statusline' does this && !getline_equal(fgetline, cookie, get_func_line) // avoid that an autocommand, e.g. QuitPre, does this - && !getline_equal(fgetline, cookie, - getnextac)) { + && !getline_equal(fgetline, cookie, getnextac)) { quitmore--; } diff --git a/src/nvim/generators/gen_events.lua b/src/nvim/generators/gen_events.lua index 27cec40b03..0eb231f012 100644 --- a/src/nvim/generators/gen_events.lua +++ b/src/nvim/generators/gen_events.lua @@ -35,27 +35,24 @@ names_tgt:write('\n {0, NULL, (event_T)0},') enum_tgt:write('\n} event_T;\n') names_tgt:write('\n};\n') -local gen_autopat_events = function(name) - names_tgt:write(string.format('\nstatic AutoPat *%s[NUM_EVENTS] = {\n ', name)) +do + names_tgt:write('\nstatic AutoCmdVec autocmds[NUM_EVENTS] = {\n ') local line_len = 1 for _ = 1,((#events) - 1) do - line_len = line_len + #(' NULL,') + line_len = line_len + #(' KV_INITIAL_VALUE,') if line_len > 80 then names_tgt:write('\n ') - line_len = 1 + #(' NULL,') + line_len = 1 + #(' KV_INITIAL_VALUE,') end - names_tgt:write(' NULL,') + names_tgt:write(' KV_INITIAL_VALUE,') end - if line_len + #(' NULL') > 80 then - names_tgt:write('\n NULL') + if line_len + #(' KV_INITIAL_VALUE') > 80 then + names_tgt:write('\n KV_INITIAL_VALUE') else - names_tgt:write(' NULL') + names_tgt:write(' KV_INITIAL_VALUE') end names_tgt:write('\n};\n') end -gen_autopat_events("first_autopat") -gen_autopat_events("last_autopat") - enum_tgt:close() names_tgt:close() diff --git a/test/benchmark/autocmd_spec.lua b/test/benchmark/autocmd_spec.lua new file mode 100644 index 0000000000..f243d9c94d --- /dev/null +++ b/test/benchmark/autocmd_spec.lua @@ -0,0 +1,175 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local exec_lua = helpers.exec_lua + +local N = 7500 + +describe('autocmd perf', function() + before_each(function() + clear() + + exec_lua([[ + out = {} + function start() + ts = vim.loop.hrtime() + end + function stop(name) + out[#out+1] = ('%14.6f ms - %s'):format((vim.loop.hrtime() - ts) / 1000000, name) + end + ]]) + end) + + after_each(function() + for _, line in ipairs(exec_lua([[return out]])) do + print(line) + end + end) + + it('nvim_create_autocmd, nvim_del_autocmd (same pattern)', function() + exec_lua([[ + local N = ... + local ids = {} + + start() + for i = 1, N do + ids[i] = vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark', + command = 'eval 0', -- noop + }) + end + stop('nvim_create_autocmd') + + start() + for i = 1, N do + vim.api.nvim_del_autocmd(ids[i]) + end + stop('nvim_del_autocmd') + ]], N) + end) + + it('nvim_create_autocmd, nvim_del_autocmd (unique patterns)', function() + exec_lua([[ + local N = ... + local ids = {} + + start() + for i = 1, N do + ids[i] = vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark' .. i, + command = 'eval 0', -- noop + }) + end + stop('nvim_create_autocmd') + + start() + for i = 1, N do + vim.api.nvim_del_autocmd(ids[i]) + end + stop('nvim_del_autocmd') + ]], N) + end) + + it('nvim_create_autocmd + nvim_del_autocmd', function() + exec_lua([[ + local N = ... + + start() + for _ = 1, N do + local id = vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark', + command = 'eval 0', -- noop + }) + vim.api.nvim_del_autocmd(id) + end + stop('nvim_create_autocmd + nvim_del_autocmd') + ]], N) + end) + + it('nvim_exec_autocmds (same pattern)', function() + exec_lua([[ + local N = ... + + for i = 1, N do + vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark', + command = 'eval 0', -- noop + }) + end + + start() + vim.api.nvim_exec_autocmds('User', { pattern = 'Benchmark', modeline = false }) + stop('nvim_exec_autocmds') + ]], N) + end) + + it('nvim_del_augroup_by_id', function() + exec_lua([[ + local N = ... + local group = vim.api.nvim_create_augroup('Benchmark', {}) + + for i = 1, N do + vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark', + command = 'eval 0', -- noop + group = group, + }) + end + + start() + vim.api.nvim_del_augroup_by_id(group) + stop('nvim_del_augroup_by_id') + ]], N) + end) + + it('nvim_del_augroup_by_name', function() + exec_lua([[ + local N = ... + local group = vim.api.nvim_create_augroup('Benchmark', {}) + + for i = 1, N do + vim.api.nvim_create_autocmd('User', { + pattern = 'Benchmark', + command = 'eval 0', -- noop + group = group, + }) + end + + start() + vim.api.nvim_del_augroup_by_name('Benchmark') + stop('nvim_del_augroup_by_id') + ]], N) + end) + + it(':autocmd, :autocmd! (same pattern)', function() + exec_lua([[ + local N = ... + + start() + for i = 1, N do + vim.cmd('autocmd User Benchmark eval 0') + end + stop(':autocmd') + + start() + vim.cmd('autocmd! User Benchmark') + stop(':autocmd!') + ]], N) + end) + + it(':autocmd, :autocmd! (unique patterns)', function() + exec_lua([[ + local N = ... + + start() + for i = 1, N do + vim.cmd(('autocmd User Benchmark%d eval 0'):format(i)) + end + stop(':autocmd') + + start() + vim.cmd('autocmd! User') + stop(':autocmd!') + ]], N) + end) +end) diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index fb5bab445c..82c7f9502f 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -611,4 +611,22 @@ describe('autocmd', function() eq(4, #meths.get_autocmds { event = "BufReadCmd", group = "TestingPatterns" }) end) end) + + it('no use-after-free when adding autocommands from a callback', function() + exec_lua [[ + vim.cmd "autocmd! TabNew" + vim.g.count = 0 + vim.api.nvim_create_autocmd('TabNew', { + callback = function() + vim.g.count = vim.g.count + 1 + for _ = 1, 100 do + vim.cmd "autocmd TabNew * let g:count += 1" + end + return true + end, + }) + vim.cmd "tabnew" + ]] + eq(1, eval('g:count')) -- Added autocommands should not be executed + end) end) diff --git a/test/functional/autocmd/show_spec.lua b/test/functional/autocmd/show_spec.lua index 505bed834b..9e0a5b819a 100644 --- a/test/functional/autocmd/show_spec.lua +++ b/test/functional/autocmd/show_spec.lua @@ -180,4 +180,45 @@ describe(":autocmd", function() test_3 User B echo "B3"]]), funcs.execute('autocmd test_3 * B')) end) + + it('should skip consecutive patterns', function() + exec([[ + autocmd! BufEnter + augroup test_1 + autocmd BufEnter A echo 'A' + autocmd BufEnter A echo 'B' + autocmd BufEnter A echo 'C' + autocmd BufEnter B echo 'D' + autocmd BufEnter B echo 'E' + autocmd BufEnter B echo 'F' + augroup END + augroup test_2 + autocmd BufEnter C echo 'A' + autocmd BufEnter C echo 'B' + autocmd BufEnter C echo 'C' + autocmd BufEnter D echo 'D' + autocmd BufEnter D echo 'E' + autocmd BufEnter D echo 'F' + augroup END + + let g:output = execute('autocmd BufEnter') + ]]) + eq(dedent([[ + + --- Autocommands --- + test_1 BufEnter + A echo 'A' + echo 'B' + echo 'C' + B echo 'D' + echo 'E' + echo 'F' + test_2 BufEnter + C echo 'A' + echo 'B' + echo 'C' + D echo 'D' + echo 'E' + echo 'F']]), eval('g:output')) + end) end)