diff options
Diffstat (limited to 'src/nvim/api/autocmd.c')
-rw-r--r-- | src/nvim/api/autocmd.c | 1016 |
1 files changed, 1016 insertions, 0 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..bf6402f938 --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,1016 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +#include <stdbool.h> +#include <stdio.h> + +#include "lauxlib.h" +#include "nvim/api/autocmd.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/eval/typval.h" +#include "nvim/fileio.h" +#include "nvim/lua/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.c.generated.h" +#endif + +#define AUCMD_MAX_PATTERNS 256 + +// Copy string or array of strings into an empty array. +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ + char *__next_ev; \ + event_T event_nr = \ + event_name2nr(event_str.data.string.data, &__next_ev); \ + if (event_nr >= NUM_EVENTS) { \ + api_set_error(err, kErrorTypeValidation, "unexpected event"); \ + goto goto_name; \ + } + +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int64_t next_autocmd_id = 1; + +/// Get all autocommands that match the corresponding {opts}. +/// +/// These examples will get autocommands matching ALL the given criteria: +/// <pre> +/// -- Matches all criteria +/// autocommands = vim.api.nvim_get_autocmds({ +/// group = "MyGroup", +/// event = {"BufEnter", "BufWinEnter"}, +/// pattern = {"*.c", "*.h"} +/// }) +/// +/// -- All commands from one group +/// autocommands = vim.api.nvim_get_autocmds({ +/// group = "MyGroup", +/// }) +/// </pre> +/// +/// NOTE: When multiple patterns or events are provided, it will find all the autocommands that +/// match any combination of them. +/// +/// @param opts Dictionary with at least one of the following: +/// - group (string|integer): the autocommand group name or id to match against. +/// - event (string|array): event or events to match against |autocmd-events|. +/// - pattern (string|array): pattern or patterns to match against |autocmd-pattern|. +/// @return Array of autocommands matching the criteria, with each item +/// containing the following fields: +/// - id (number): the autocommand id (only when defined with the API). +/// - group (integer): the autocommand group id. +/// - group_name (string): the autocommand group name. +/// - desc (string): the autocommand description. +/// - event (string): the autocommand event. +/// - command (string): the autocommand command. Note: this will be empty if a callback is set. +/// - callback (function|string|nil): Lua function or name of a Vim script function +/// which is executed when this autocommand is triggered. +/// - once (boolean): whether the autocommand is only run once. +/// - pattern (string): the autocommand pattern. +/// If the autocommand is buffer local |autocmd-buffer-local|: +/// - buflocal (boolean): true if the autocommand is buffer local. +/// - buffer (number): the buffer number. +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + // TODO(tjdevries): Would be cool to add nvim_get_autocmds({ id = ... }) + + Array autocmd_list = ARRAY_DICT_INIT; + char *pattern_filters[AUCMD_MAX_PATTERNS]; + char pattern_buflocal[BUFLOCAL_PAT_LEN]; + + Array buffers = ARRAY_DICT_INIT; + + bool event_set[NUM_EVENTS] = { false }; + bool check_event = false; + + int group = 0; + + switch (opts->group.type) { + case kObjectTypeNil: + break; + case kObjectTypeString: + group = augroup_find(opts->group.data.string.data); + if (group < 0) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + goto cleanup; + } + break; + case kObjectTypeInteger: + group = (int)opts->group.data.integer; + char *name = augroup_name(group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + goto cleanup; + } + break; + default: + api_set_error(err, kErrorTypeValidation, "group must be a string or an integer."); + goto cleanup; + } + + if (opts->event.type != kObjectTypeNil) { + check_event = true; + + Object v = opts->event; + if (v.type == kObjectTypeString) { + GET_ONE_EVENT(event_nr, v, cleanup); + event_set[event_nr] = true; + } else if (v.type == kObjectTypeArray) { + FOREACH_ITEM(v.data.array, event_v, { + if (event_v.type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "Every event must be a string in 'event'"); + goto cleanup; + } + + GET_ONE_EVENT(event_nr, event_v, cleanup); + event_set[event_nr] = true; + }) + } else { + api_set_error(err, + kErrorTypeValidation, + "Not a valid 'event' value. Must be a string or an array"); + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Cannot use both 'pattern' and 'buffer'"); + goto cleanup; + } + + int pattern_filter_count = 0; + if (opts->pattern.type != kObjectTypeNil) { + Object v = opts->pattern; + if (v.type == kObjectTypeString) { + pattern_filters[pattern_filter_count] = v.data.string.data; + pattern_filter_count += 1; + } else if (v.type == kObjectTypeArray) { + if (v.data.array.size > AUCMD_MAX_PATTERNS) { + api_set_error(err, kErrorTypeValidation, + "Too many patterns. Please limit yourself to %d or fewer", + AUCMD_MAX_PATTERNS); + goto cleanup; + } + + FOREACH_ITEM(v.data.array, item, { + if (item.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'pattern': must be a string"); + goto cleanup; + } + + pattern_filters[pattern_filter_count] = item.data.string.data; + pattern_filter_count += 1; + }); + } else { + api_set_error(err, kErrorTypeValidation, + "Not a valid 'pattern' value. Must be a string or an array"); + goto cleanup; + } + } + + if (opts->buffer.type == kObjectTypeInteger || opts->buffer.type == kObjectTypeBuffer) { + buf_T *buf = find_buffer_by_handle((Buffer)opts->buffer.data.integer, err); + if (ERROR_SET(err)) { + goto cleanup; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ((char *)pattern_buflocal)); + } else if (opts->buffer.type == kObjectTypeArray) { + if (opts->buffer.data.array.size > AUCMD_MAX_PATTERNS) { + api_set_error(err, + kErrorTypeValidation, + "Too many buffers. Please limit yourself to %d or fewer", AUCMD_MAX_PATTERNS); + goto cleanup; + } + + FOREACH_ITEM(opts->buffer.data.array, bufnr, { + if (bufnr.type != kObjectTypeInteger && bufnr.type != kObjectTypeBuffer) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'buffer': must be an integer"); + goto cleanup; + } + + buf_T *buf = find_buffer_by_handle((Buffer)bufnr.data.integer, err); + if (ERROR_SET(err)) { + goto cleanup; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ((char *)pattern_buflocal)); + }); + } else if (opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Invalid value for 'buffer': must be an integer or array of integers"); + goto cleanup; + } + + FOREACH_ITEM(buffers, bufnr, { + pattern_filters[pattern_filter_count] = bufnr.data.string.data; + pattern_filter_count += 1; + }); + + FOR_ALL_AUEVENTS(event) { + if (check_event && !event_set[event]) { + continue; + } + + for (AutoPat *ap = au_get_autopat_for_event(event); ap != NULL; ap = ap->next) { + if (ap->cmds == NULL) { + continue; + } + + // Skip autocmds from invalid groups if passed. + if (group != 0 && ap->group != group) { + continue; + } + + // 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]); + + char *pat = pattern_filters[i]; + int patlen = (int)STRLEN(pat); + + if (aupat_is_buflocal(pat, patlen)) { + aupat_normalize_buflocal_pat(pattern_buflocal, + pat, + patlen, + aupat_get_buflocal_nr(pat, patlen)); + + pat = pattern_buflocal; + } + + if (strequal(ap->pat, pat)) { + passed = true; + break; + } + } + + if (!passed) { + continue; + } + } + + for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { + if (aucmd_exec_is_deleted(ac->exec)) { + continue; + } + + 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((char *)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)); + } + } + } + +cleanup: + api_free_array(buffers); + return autocmd_list; +} + +/// Create an |autocommand| +/// +/// The API allows for two (mutually exclusive) types of actions to be executed when the autocommand +/// triggers: a callback function (Lua or Vimscript), or a command (like regular autocommands). +/// +/// Example using callback: +/// <pre> +/// -- Lua function +/// local myluafun = function() print("This buffer enters") end +/// +/// -- Vimscript function name (as a string) +/// local myvimfun = "g:MyVimFunction" +/// +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// callback = myluafun, -- Or myvimfun +/// }) +/// </pre> +/// +/// Lua functions receive a table with information about the autocmd event as an argument. To use +/// a function which itself accepts another (optional) parameter, wrap the function +/// in a lambda: +/// +/// <pre> +/// -- Lua function with an optional parameter. +/// -- The autocmd callback would pass a table as argument but this +/// -- function expects number|nil +/// local myluafun = function(bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() end +/// +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// callback = function() myluafun() end, +/// }) +/// </pre> +/// +/// Example using command: +/// <pre> +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// command = "echo 'Entering a C or C++ file'", +/// }) +/// </pre> +/// +/// Example values for pattern: +/// <pre> +/// pattern = "*.py" +/// pattern = { "*.py", "*.pyi" } +/// </pre> +/// +/// Example values for event: +/// <pre> +/// "BufWritePre" +/// {"CursorHold", "BufWritePre", "BufWritePost"} +/// </pre> +/// +/// @param event (string|array) The event or events to register this autocommand +/// @param opts Dictionary of autocommand options: +/// - group (string|integer) optional: the autocommand group name or +/// id to match against. +/// - pattern (string|array) optional: pattern or patterns to match +/// against |autocmd-pattern|. +/// - buffer (integer) optional: buffer number for buffer local autocommands +/// |autocmd-buflocal|. Cannot be used with {pattern}. +/// - desc (string) optional: description of the autocommand. +/// - callback (function|string) optional: if a string, the name of a Vimscript function +/// to call when this autocommand is triggered. Otherwise, a Lua function which is +/// called when this autocommand is triggered. Cannot be used with {command}. Lua +/// callbacks can return true to delete the autocommand; in addition, they accept a +/// single table argument with the following keys: +/// - id: (number) the autocommand id +/// - event: (string) the name of the event that triggered the autocommand +/// |autocmd-events| +/// - group: (number|nil) the autocommand group id, if it exists +/// - match: (string) the expanded value of |<amatch>| +/// - buf: (number) the expanded value of |<abuf>| +/// - file: (string) the expanded value of |<afile>| +/// - data: (any) arbitrary data passed to |nvim_exec_autocmds()| +/// - command (string) optional: Vim command to execute on event. Cannot be used with +/// {callback} +/// - once (boolean) optional: defaults to false. Run the autocommand +/// only once |autocmd-once|. +/// - nested (boolean) optional: defaults to false. Run nested +/// autocommands |autocmd-nested|. +/// +/// @return Integer id of the created autocommand. +/// @see |autocommand| +/// @see |nvim_del_autocmd()| +Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + int64_t autocmd_id = -1; + char *desc = NULL; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT; + Callback cb = CALLBACK_NONE; + + if (!unpack_string_or_array(&event_array, &event, "event", true, err)) { + goto cleanup; + } + + if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'callback' and 'command' for the same autocmd"); + goto cleanup; + } else if (opts->callback.type != kObjectTypeNil) { + // TODO(tjdevries): It's possible we could accept callable tables, + // but we don't do that many other places, so for the moment let's + // not do that. + + Object *callback = &opts->callback; + switch (callback->type) { + case kObjectTypeLuaRef: + if (callback->data.luaref == LUA_NOREF) { + api_set_error(err, + kErrorTypeValidation, + "must pass an actual value"); + goto cleanup; + } + + if (!nlua_ref_is_function(callback->data.luaref)) { + api_set_error(err, + kErrorTypeValidation, + "must pass a function for callback"); + goto cleanup; + } + + cb.type = kCallbackLua; + cb.data.luaref = api_new_luaref(callback->data.luaref); + break; + case kObjectTypeString: + cb.type = kCallbackFuncref; + cb.data.funcref = string_to_cstr(callback->data.string); + break; + default: + api_set_error(err, + kErrorTypeException, + "'callback' must be a lua function or name of vim function"); + goto cleanup; + } + + aucmd.type = CALLABLE_CB; + aucmd.callable.cb = cb; + } else if (opts->command.type != kObjectTypeNil) { + Object *command = &opts->command; + if (command->type == kObjectTypeString) { + aucmd.type = CALLABLE_EX; + aucmd.callable.cmd = string_to_cstr(command->data.string); + } else { + api_set_error(err, + kErrorTypeValidation, + "'command' must be a string"); + goto cleanup; + } + } else { + api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'"); + goto cleanup; + } + + bool is_once = api_object_to_bool(opts->once, "once", false, err); + bool is_nested = api_object_to_bool(opts->nested, "nested", false, err); + + int au_group = get_augroup_from_object(opts->group, err); + if (au_group == AUGROUP_ERROR) { + goto cleanup; + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + if (opts->desc.type != kObjectTypeNil) { + if (opts->desc.type == kObjectTypeString) { + desc = opts->desc.data.string.data; + } else { + api_set_error(err, + kErrorTypeValidation, + "'desc' must be a string"); + goto cleanup; + } + } + + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*"))); + } + + if (event_array.size == 0) { + api_set_error(err, kErrorTypeValidation, "'event' is a required key"); + goto cleanup; + } + + autocmd_id = next_autocmd_id++; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup); + + int retval; + + FOREACH_ITEM(patterns, pat, { + // See: TODO(sctx) + WITH_SCRIPT_CONTEXT(channel_id, { + retval = autocmd_register(autocmd_id, + event_nr, + pat.data.string.data, + (int)pat.data.string.size, + au_group, + is_once, + is_nested, + desc, + aucmd); + }); + + if (retval == FAIL) { + api_set_error(err, kErrorTypeException, "Failed to set autocmd"); + goto cleanup; + } + }) + }); + +cleanup: + aucmd_exec_free(&aucmd); + api_free_array(event_array); + api_free_array(patterns); + + return autocmd_id; +} + +/// Delete an autocommand by id. +/// +/// NOTE: Only autocommands created via the API have an id. +/// @param id Integer The id returned by nvim_create_autocmd +/// @see |nvim_create_autocmd()| +void nvim_del_autocmd(Integer id, Error *err) + FUNC_API_SINCE(9) +{ + if (id <= 0) { + api_set_error(err, kErrorTypeException, "Invalid autocmd id"); + return; + } + if (!autocmd_delete_id(id)) { + api_set_error(err, kErrorTypeException, "Failed to delete autocmd"); + } +} + +/// Clear all autocommands that match the corresponding {opts}. To delete +/// a particular autocmd, see |nvim_del_autocmd|. +/// @param opts Parameters +/// - event: (string|table) +/// Examples: +/// - event: "pat1" +/// - event: { "pat1" } +/// - event: { "pat1", "pat2", "pat3" } +/// - pattern: (string|table) +/// - pattern or patterns to match exactly. +/// - For example, if you have `*.py` as that pattern for the autocmd, +/// you must pass `*.py` exactly to clear it. `test.py` will not +/// match the pattern. +/// - defaults to clearing all patterns. +/// - NOTE: Cannot be used with {buffer} +/// - buffer: (bufnr) +/// - clear only |autocmd-buflocal| autocommands. +/// - NOTE: Cannot be used with {pattern} +/// - group: (string|int) The augroup name or id. +/// - NOTE: If not passed, will only delete autocmds *not* in any group. +/// +void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + // TODO(tjdevries): Future improvements: + // - once: (boolean) - Only clear autocmds with once. See |autocmd-once| + // - nested: (boolean) - Only clear autocmds with nested. See |autocmd-nested| + // - group: Allow passing "*" or true or something like that to force doing all + // autocmds, regardless of their group. + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + if (!unpack_string_or_array(&event_array, &opts->event, "event", false, err)) { + goto cleanup; + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Cannot use both 'pattern' and 'buffer'"); + goto cleanup; + } + + int au_group = get_augroup_from_object(opts->group, err); + if (au_group == AUGROUP_ERROR) { + goto cleanup; + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + // When we create the autocmds, we want to say that they are all matched, so that's * + // but when we clear them, we want to say that we didn't pass a pattern, so that's NUL + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + } + + // If we didn't pass any events, that means clear all events. + if (event_array.size == 0) { + FOR_ALL_AUEVENTS(event) { + FOREACH_ITEM(patterns, pat_object, { + char *pat = pat_object.data.string.data; + if (!clear_autocmd(event, (char *)pat, au_group, err)) { + goto cleanup; + } + }); + } + } else { + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup); + + FOREACH_ITEM(patterns, pat_object, { + char *pat = pat_object.data.string.data; + if (!clear_autocmd(event_nr, (char *)pat, au_group, err)) { + goto cleanup; + } + }); + }); + } + +cleanup: + api_free_array(event_array); + api_free_array(patterns); +} + +/// Create or get an autocommand group |autocmd-groups|. +/// +/// To get an existing group id, do: +/// <pre> +/// local id = vim.api.nvim_create_augroup("MyGroup", { +/// clear = false +/// }) +/// </pre> +/// +/// @param name String: The name of the group +/// @param opts Dictionary Parameters +/// - clear (bool) optional: defaults to true. Clear existing +/// commands if the group already exists |autocmd-groups|. +/// @return Integer id of the created group. +/// @see |autocmd-groups| +Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augroup) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + char *augroup_name = name.data; + bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + + int augroup = -1; + WITH_SCRIPT_CONTEXT(channel_id, { + augroup = augroup_add(augroup_name); + if (augroup == AUGROUP_ERROR) { + api_set_error(err, kErrorTypeException, "Failed to set augroup"); + return -1; + } + + if (clear_autocmds) { + FOR_ALL_AUEVENTS(event) { + aupat_del_for_event_and_group(event, augroup); + } + } + }); + + return augroup; +} + +/// Delete an autocommand group by id. +/// +/// To get a group id one can use |nvim_get_autocmds()|. +/// +/// NOTE: behavior differs from |augroup-delete|. When deleting a group, autocommands contained in +/// this group will also be deleted and cleared. This group will no longer exist. +/// @param id Integer The id of the group. +/// @see |nvim_del_augroup_by_name()| +/// @see |nvim_create_augroup()| +void nvim_del_augroup_by_id(Integer id, Error *err) + FUNC_API_SINCE(9) +{ + TRY_WRAP({ + try_start(); + char *name = augroup_name((int)id); + augroup_del(name, false); + try_end(err); + }); +} + +/// Delete an autocommand group by name. +/// +/// NOTE: behavior differs from |augroup-delete|. When deleting a group, autocommands contained in +/// this group will also be deleted and cleared. This group will no longer exist. +/// @param name String The name of the group. +/// @see |autocommand-groups| +void nvim_del_augroup_by_name(String name, Error *err) + FUNC_API_SINCE(9) +{ + TRY_WRAP({ + try_start(); + augroup_del(name.data, false); + try_end(err); + }); +} + +/// Execute all autocommands for {event} that match the corresponding +/// {opts} |autocmd-execute|. +/// @param event (String|Array) The event or events to execute +/// @param opts Dictionary of autocommand options: +/// - group (string|integer) optional: the autocommand group name or +/// id to match against. |autocmd-groups|. +/// - pattern (string|array) optional: defaults to "*" |autocmd-pattern|. Cannot be used +/// with {buffer}. +/// - buffer (integer) optional: buffer number |autocmd-buflocal|. Cannot be used with +/// {pattern}. +/// - modeline (bool) optional: defaults to true. Process the +/// modeline after the autocommands |<nomodeline>|. +/// - data (any): arbitrary data to send to the autocommand callback. See +/// |nvim_create_autocmd()| for details. +/// @see |:doautocmd| +void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int au_group = AUGROUP_ALL; + bool modeline = true; + + buf_T *buf = curbuf; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + Object *data = NULL; + + if (!unpack_string_or_array(&event_array, &event, "event", true, err)) { + goto cleanup; + } + + switch (opts->group.type) { + case kObjectTypeNil: + break; + case kObjectTypeString: + au_group = augroup_find(opts->group.data.string.data); + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeValidation, + "invalid augroup: %s", opts->group.data.string.data); + goto cleanup; + } + break; + case kObjectTypeInteger: + au_group = (int)opts->group.data.integer; + char *name = augroup_name(au_group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup: %d", au_group); + goto cleanup; + } + break; + default: + api_set_error(err, kErrorTypeValidation, "'group' must be a string or an integer."); + goto cleanup; + } + + if (opts->buffer.type != kObjectTypeNil) { + Object buf_obj = opts->buffer; + if (buf_obj.type != kObjectTypeInteger && buf_obj.type != kObjectTypeBuffer) { + api_set_error(err, kErrorTypeException, "invalid buffer: %d", buf_obj.type); + goto cleanup; + } + + buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err); + + if (ERROR_SET(err)) { + goto cleanup; + } + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + } + + if (opts->data.type != kObjectTypeNil) { + data = &opts->data; + } + + modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + + bool did_aucmd = false; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup) + + FOREACH_ITEM(patterns, pat, { + char *fname = opts->buffer.type == kObjectTypeNil ? pat.data.string.data : NULL; + did_aucmd |= + apply_autocmds_group(event_nr, fname, NULL, true, au_group, buf, NULL, data); + }) + }) + + if (did_aucmd && modeline) { + do_modelines(0); + } + +cleanup: + api_free_array(event_array); + api_free_array(patterns); +} + +static bool check_autocmd_string_array(Array arr, char *k, Error *err) +{ + FOREACH_ITEM(arr, entry, { + if (entry.type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "All entries in '%s' must be strings", + k); + return false; + } + + // Disallow newlines in the middle of the line. + const String l = entry.data.string; + if (memchr(l.data, NL, l.size)) { + api_set_error(err, kErrorTypeValidation, + "String cannot contain newlines"); + return false; + } + }) + return true; +} + +static bool unpack_string_or_array(Array *array, Object *v, char *k, bool required, Error *err) +{ + if (v->type == kObjectTypeString) { + ADD(*array, copy_object(*v)); + } else if (v->type == kObjectTypeArray) { + if (!check_autocmd_string_array(v->data.array, k, err)) { + return false; + } + *array = copy_array(v->data.array); + } else { + if (required) { + api_set_error(err, + kErrorTypeValidation, + "'%s' must be an array or a string.", + k); + return false; + } + } + + return true; +} + +// Returns AUGROUP_ERROR if there was a problem with {group} +static int get_augroup_from_object(Object group, Error *err) +{ + int au_group = AUGROUP_ERROR; + + switch (group.type) { + case kObjectTypeNil: + return AUGROUP_DEFAULT; + case kObjectTypeString: + au_group = augroup_find(group.data.string.data); + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeValidation, + "invalid augroup: %s", group.data.string.data); + + return AUGROUP_ERROR; + } + + return au_group; + case kObjectTypeInteger: + au_group = (int)group.data.integer; + char *name = augroup_name(au_group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup: %d", au_group); + return AUGROUP_ERROR; + } + + return au_group; + default: + api_set_error(err, kErrorTypeValidation, "'group' must be a string or an integer."); + return AUGROUP_ERROR; + } +} + +static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, Object buffer, + Error *err) +{ + const char pattern_buflocal[BUFLOCAL_PAT_LEN]; + + if (pattern.type != kObjectTypeNil && buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'pattern' and 'buffer' for the same autocmd"); + return false; + } else if (pattern.type != kObjectTypeNil) { + Object *v = &pattern; + + if (v->type == kObjectTypeString) { + char *pat = v->data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(*patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + } else if (v->type == kObjectTypeArray) { + if (!check_autocmd_string_array(*patterns, "pattern", err)) { + return false; + } + + Array array = v->data.array; + FOREACH_ITEM(array, entry, { + char *pat = entry.data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(*patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + }) + } else { + api_set_error(err, + kErrorTypeValidation, + "'pattern' must be a string or table"); + return false; + } + } else if (buffer.type != kObjectTypeNil) { + if (buffer.type != kObjectTypeInteger && buffer.type != kObjectTypeBuffer) { + api_set_error(err, + kErrorTypeValidation, + "'buffer' must be an integer"); + return false; + } + + buf_T *buf = find_buffer_by_handle((Buffer)buffer.data.integer, err); + if (ERROR_SET(err)) { + return false; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(*patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal))); + } + + return true; +} + +static bool clear_autocmd(event_T event, char *pat, int au_group, Error *err) +{ + if (do_autocmd_event(event, pat, false, false, "", true, au_group) == FAIL) { + api_set_error(err, kErrorTypeException, "Failed to clear autocmd"); + return false; + } + + return true; +} + +#undef GET_ONE_EVENT |