Files
neovim/src/nvim/api/autocmd.c
2022-06-26 17:57:45 +02:00

1017 lines
33 KiB
C

// 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