diff options
Diffstat (limited to 'src/nvim/api/autocmd.c')
-rw-r--r-- | src/nvim/api/autocmd.c | 669 |
1 files changed, 669 insertions, 0 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..deb8ec8cf3 --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,669 @@ +#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 + +// Check whether every item in the array is a kObjectTypeString +#define CHECK_STRING_ARRAY(__array, k, v, goto_name) \ + for (size_t j = 0; j < __array.size; j++) { \ + Object item = __array.items[j]; \ + if (item.type != kObjectTypeString) { \ + api_set_error(err, \ + kErrorTypeValidation, \ + "All entries in '%s' must be strings", \ + k); \ + goto goto_name; \ + } \ + } + +// Copy string or array of strings into an empty array. +#define UNPACK_STRING_OR_ARRAY(__array, k, v, goto_name) \ + if (v->type == kObjectTypeString) { \ + ADD(__array, copy_object(*v)); \ + } else if (v->type == kObjectTypeArray) { \ + CHECK_STRING_ARRAY(__array, k, v, goto_name); \ + __array = copy_array(v->data.array); \ + } else { \ + api_set_error(err, \ + kErrorTypeValidation, \ + "'%s' must be an array or a string.", \ + k); \ + goto goto_name; \ + } + +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ + char_u *__next_ev; \ + event_T event_nr = \ + event_name2nr((char_u *)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 autocmds that match the requirements passed to {opts}. +/// group +/// event +/// pattern +/// +/// -- @param {string} event - event or events to match against +/// vim.api.nvim_get_autocmds({ event = "FileType" }) +/// +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + Array autocmd_list = ARRAY_DICT_INIT; + char_u *pattern_filters[AUCMD_MAX_PATTERNS]; + char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; + + bool event_set[NUM_EVENTS] = { false }; + bool check_event = false; + + int group = 0; + + if (opts->group.type != kObjectTypeNil) { + Object v = opts->group; + if (v.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "group must be a string."); + goto cleanup; + } + + group = augroup_find(v.data.string.data); + + if (group < 0) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + 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; + } + } + + int pattern_filter_count = 0; + if (opts->pattern.type != kObjectTypeNil) { + Object v = opts->pattern; + if (v.type == kObjectTypeString) { + pattern_filters[pattern_filter_count] = (char_u *)v.data.string.data; + pattern_filter_count += 1; + } else if (v.type == kObjectTypeArray) { + FOREACH_ITEM(v.data.array, item, { + pattern_filters[pattern_filter_count] = (char_u *)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 (pattern_filter_count >= AUCMD_MAX_PATTERNS) { + api_set_error(err, + kErrorTypeValidation, + "Too many patterns. Please limit yourself to less"); + goto cleanup; + } + } + + 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 == NULL || 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_u *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((char *)ap->pat, (char *)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)); + } + + 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)); + } + + PUT(autocmd_info, + "command", + STRING_OBJ(cstr_to_string(aucmd_exec_to_string(ac, ac->exec)))); + + PUT(autocmd_info, + "pattern", + STRING_OBJ(cstr_to_string((char *)ap->pat))); + + 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: + return autocmd_list; +} + +/// Define an autocmd. +/// @param opts Dictionary +/// Required keys: +/// event: string | ArrayOf(string) +/// event = "pat1,pat2,pat3", +/// event = "pat1" +/// event = {"pat1"} +/// event = {"pat1", "pat2", "pat3"} +/// +/// +/// -- @param {string} name - augroup name +/// -- @param {string | table} event - event or events to match against +/// -- @param {string | table} pattern - pattern or patterns to match against +/// -- @param {string | function} callback - function or string to execute on autocmd +/// -- @param {string} command - optional, vimscript command +/// Eg. command = "let g:value_set = v:true" +/// -- @param {boolean} once - optional, defaults to false +/// +/// -- pattern = comma delimited list of patterns | pattern | { pattern, ... } +/// +/// pattern = "*.py,*.pyi" +/// pattern = "*.py" +/// pattern = {"*.py"} +/// pattern = { "*.py", "*.pyi" } +/// +/// -- not supported +/// pattern = {"*.py,*.pyi"} +/// +/// -- event = string | string[] +/// event = "FileType,CursorHold" +/// event = "BufPreWrite" +/// event = {"BufPostWrite"} +/// event = {"CursorHold", "BufPreWrite", "BufPostWrite"} +Integer nvim_create_autocmd(uint64_t channel_id, Dict(create_autocmd) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int64_t autocmd_id = -1; + + const char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; + int au_group = AUGROUP_DEFAULT; + char *desc = NULL; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT; + Callback cb = CALLBACK_NONE; + + 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; + if (callback->type == 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); + } else if (callback->type == kObjectTypeString) { + cb.type = kCallbackFuncref; + cb.data.funcref = vim_strsave((char_u *)callback->data.string.data); + } else { + 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 = vim_strsave((char_u *)command->data.string.data); + } 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; + } + + if (opts->event.type != kObjectTypeNil) { + UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), 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); + + // TOOD: accept number for namespace instead + if (opts->group.type != kObjectTypeNil) { + Object *v = &opts->group; + if (v->type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'group' must be a string"); + goto cleanup; + } + + au_group = augroup_find(v->data.string.data); + + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeException, + "invalid augroup: %s", v->data.string.data); + + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'pattern' and 'buffer' for the same autocmd"); + goto cleanup; + } else if (opts->pattern.type != kObjectTypeNil) { + Object *v = &opts->pattern; + + if (v->type == kObjectTypeString) { + char_u *pat = (char_u *)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) { + CHECK_STRING_ARRAY(patterns, "pattern", v, cleanup); + + Array array = v->data.array; + for (size_t i = 0; i < array.size; i++) { + char_u *pat = (char_u *)array.items[i].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"); + goto cleanup; + } + } else if (opts->buffer.type != kObjectTypeNil) { + if (opts->buffer.type != kObjectTypeInteger) { + api_set_error(err, + kErrorTypeValidation, + "'buffer' must be an integer"); + goto cleanup; + } + + 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(patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal))); + } + + if (aucmd.type == CALLABLE_NONE) { + api_set_error(err, + kErrorTypeValidation, + "'command' or 'callback' is required"); + 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; + + for (size_t i = 0; i < patterns.size; i++) { + Object pat = patterns.items[i]; + + // See: TODO(sctx) + WITH_SCRIPT_CONTEXT(channel_id, { + retval = autocmd_register(autocmd_id, + event_nr, + (char_u *)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 autocmd by ID. Autocmds only return IDs when created +/// via the API. +/// +/// @param id Integer The ID returned by nvim_create_autocmd +void nvim_del_autocmd(Integer id) + FUNC_API_SINCE(9) +{ + autocmd_delete_id(id); +} + +/// Create or get an augroup. +/// +/// To get an existing augroup ID, do: +/// <pre> +/// local id = vim.api.nvim_create_augroup({ name = name, clear = false }); +/// </pre> +/// +/// @param opts Parameters +/// - name (string): The name of the augroup +/// - clear (bool): Whether to clear existing commands or not. +// Defaults to true. +/// See |autocmd-groups| +Integer nvim_create_augroup(uint64_t channel_id, Dict(create_augroup) *opts, Error *err) + FUNC_API_SINCE(9) +{ + bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + + if (opts->name.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'name' is required and must be a string"); + return -1; + } + char *name = opts->name.data.string.data; + + int augroup = -1; + WITH_SCRIPT_CONTEXT(channel_id, { + augroup = augroup_add(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; +} + +/// NOTE: behavior differs from augroup-delete. +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_id(Integer id) + FUNC_API_SINCE(9) +{ + char *name = augroup_name((int)id); + augroup_del(name, false); +} + +/// NOTE: behavior differs from augroup-delete. +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_name(String name) + FUNC_API_SINCE(9) +{ + augroup_del(name.data, false); +} + +/// -- @param {string} group - autocmd group name +/// -- @param {number} buffer - buffer number +/// -- @param {string | table} event - event or events to match against +/// -- @param {string | table} pattern - optional, defaults to "*". +/// vim.api.nvim_do_autcmd({ group, buffer, pattern, event, modeline }) +void nvim_do_autocmd(Dict(do_autocmd) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int au_group = AUGROUP_ALL; + bool modeline = true; + + buf_T *buf = curbuf; + bool set_buf = false; + + char_u *pattern = NULL; + bool set_pattern = false; + + Array event_array = ARRAY_DICT_INIT; + + if (opts->group.type != kObjectTypeNil) { + if (opts->group.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'group' must be a string"); + goto cleanup; + } + + au_group = augroup_find(opts->group.data.string.data); + + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeException, + "invalid augroup: %s", opts->group.data.string.data); + + 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); + set_buf = true; + + if (ERROR_SET(err)) { + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil) { + if (opts->pattern.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'pattern' must be a string"); + goto cleanup; + } + + pattern = vim_strsave((char_u *)opts->pattern.data.string.data); + set_pattern = true; + } + + if (opts->event.type != kObjectTypeNil) { + UNPACK_STRING_OR_ARRAY(event_array, "event", (&opts->event), cleanup) + } + + if (opts->modeline.type != kObjectTypeNil) { + modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + } + + if (set_pattern && set_buf) { + api_set_error(err, kErrorTypeValidation, "must not set 'buffer' and 'pattern'"); + goto cleanup; + } + + bool did_aucmd = false; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup) + + did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL); + }) + + if (did_aucmd && modeline) { + do_modelines(0); + } + +cleanup: + api_free_array(event_array); + XFREE_CLEAR(pattern); +} + + +#undef UNPACK_STRING_OR_ARRAY +#undef CHECK_STRING_ARRAY +#undef GET_ONE_EVENT |