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)