diff options
author | TJ DeVries <devries.timothyj@gmail.com> | 2021-05-28 15:45:34 -0400 |
---|---|---|
committer | bfredl <bjorn.linse@gmail.com> | 2022-02-27 22:04:55 +0100 |
commit | 991e472881bf29805982b402c1a010cde051ded3 (patch) | |
tree | 8d453a80320e49c2748f03240db8a4338384c9a1 | |
parent | 1b5767aa3480c0cdc43f7a4b78f36a14e85a182f (diff) | |
download | rneovim-991e472881bf29805982b402c1a010cde051ded3.tar.gz rneovim-991e472881bf29805982b402c1a010cde051ded3.tar.bz2 rneovim-991e472881bf29805982b402c1a010cde051ded3.zip |
feat(lua): add api and lua autocmds
38 files changed, 2878 insertions, 608 deletions
diff --git a/.gitignore b/.gitignore index 0988a51cd9..11479346c7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ compile_commands.json /.clangd/ /.cache/clangd/ /.ccls-cache/ +/.clang-tidy .DS_Store *.mo diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index 36768953f9..7b6d974181 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -95,6 +95,7 @@ CONFIG = { 'window.c', 'win_config.c', 'tabpage.c', + 'autocmd.c', 'ui.c', ], # List of files/directories for doxygen to read, separated by blanks diff --git a/scripts/uncrustify.sh b/scripts/uncrustify.sh new file mode 100755 index 0000000000..ac5d542c29 --- /dev/null +++ b/scripts/uncrustify.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +# Check that you have uncrustify +hash uncrustify + +COMMITISH="${1:-master}" +for file in $(git diff --diff-filter=d --name-only $COMMITISH | grep '\.[ch]$'); do + uncrustify -c src/uncrustify.cfg -l C --replace --no-backup "$file" +done 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 diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h new file mode 100644 index 0000000000..f9432830d9 --- /dev/null +++ b/src/nvim/api/autocmd.h @@ -0,0 +1,11 @@ +#ifndef NVIM_API_AUTOCMD_H +#define NVIM_API_AUTOCMD_H + +#include <stdint.h> + +#include "nvim/api/private/defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.h.generated.h" +#endif +#endif // NVIM_API_AUTOCMD_H diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index 45a57b9257..be71c446b1 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -110,5 +110,34 @@ return { "reverse"; "nocombine"; }; + -- Autocmds + create_autocmd = { + "buffer"; + "callback"; + "command"; + "desc"; + "event"; + "group"; + "once"; + "nested"; + "pattern"; + }; + do_autocmd = { + "buffer"; + "event"; + "group"; + "modeline"; + "pattern"; + }; + get_autocmds = { + "event"; + "group"; + "id"; + "pattern"; + }; + create_augroup = { + "clear"; + "name"; + }; } diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c index 8ab7743e01..f670f06357 100644 --- a/src/nvim/api/private/dispatch.c +++ b/src/nvim/api/private/dispatch.c @@ -6,22 +6,30 @@ #include <msgpack.h> #include <stdbool.h> -#include "nvim/api/buffer.h" #include "nvim/api/deprecated.h" -#include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/log.h" +#include "nvim/map.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/vim.h" + +// =========================================================================== +// NEW API FILES MUST GO HERE. +// +// When creating a new API file, you must include it here, +// so that the dispatcher can find the C functions that you are creating! +// =========================================================================== +#include "nvim/api/autocmd.h" +#include "nvim/api/buffer.h" +#include "nvim/api/extmark.h" #include "nvim/api/tabpage.h" #include "nvim/api/ui.h" #include "nvim/api/vim.h" #include "nvim/api/vimscript.h" #include "nvim/api/win_config.h" #include "nvim/api/window.h" -#include "nvim/log.h" -#include "nvim/map.h" -#include "nvim/msgpack_rpc/helpers.h" -#include "nvim/vim.h" static Map(String, MsgpackRpcRequestHandler) methods = MAP_INIT; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 971fa1cb0f..3d4a04f096 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -396,19 +396,14 @@ void set_option_to(uint64_t channel_id, void *to, int type, String name, Object stringval = value.data.string.data; } - const sctx_T save_current_sctx = current_sctx; - current_sctx.sc_sid = - channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; - current_sctx.sc_lnum = 0; - current_channel_id = channel_id; - - const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) - ? 0 : (type == SREQ_GLOBAL) - ? OPT_GLOBAL : OPT_LOCAL; - set_option_value_for(name.data, numval, stringval, - opt_flags, type, to, err); - - current_sctx = save_current_sctx; + WITH_SCRIPT_CONTEXT(channel_id, { + const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) + ? 0 : (type == SREQ_GLOBAL) + ? OPT_GLOBAL : OPT_LOCAL; + + set_option_value_for(name.data, numval, stringval, + opt_flags, type, to, err); + }); } buf_T *find_buffer_by_handle(Buffer buffer, Error *err) @@ -1614,3 +1609,16 @@ err: NLUA_CLEAR_REF(luaref); NLUA_CLEAR_REF(compl_luaref); } + +int find_sid(uint64_t channel_id) +{ + switch (channel_id) { + case VIML_INTERNAL_CALL: + // TODO(autocmd): Figure out what this should be + // return SID_API_CLIENT; + case LUA_INTERNAL_CALL: + return SID_LUA; + default: + return SID_API_CLIENT; + } +} diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 6d0aec9c90..6969994c3b 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -138,10 +138,27 @@ typedef struct { msg_list = saved_msg_list; /* Restore the exception context. */ \ } while (0) +// Useful macro for executing some `code` for each item in an array. +#define FOREACH_ITEM(a, __foreach_item, code) \ + for (size_t __foreach_i = 0; __foreach_i < (a).size; __foreach_i++) { \ + Object __foreach_item = (a).items[__foreach_i]; \ + code; \ + } + + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/helpers.h.generated.h" # include "keysets.h.generated.h" #endif +#define WITH_SCRIPT_CONTEXT(channel_id, code) \ + const sctx_T save_current_sctx = current_sctx; \ + current_sctx.sc_sid = \ + (channel_id) == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; \ + current_sctx.sc_lnum = 0; \ + current_channel_id = channel_id; \ + code; \ + current_sctx = save_current_sctx; + #endif // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/aucmd.c b/src/nvim/aucmd.c deleted file mode 100644 index d7f73fa4a1..0000000000 --- a/src/nvim/aucmd.c +++ /dev/null @@ -1,123 +0,0 @@ -// 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 "nvim/aucmd.h" -#include "nvim/buffer.h" -#include "nvim/eval.h" -#include "nvim/ex_docmd.h" -#include "nvim/ex_getln.h" -#include "nvim/fileio.h" -#include "nvim/main.h" -#include "nvim/os/os.h" -#include "nvim/ui.h" -#include "nvim/vim.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "aucmd.c.generated.h" -#endif - -void do_autocmd_uienter(uint64_t chanid, bool attached) -{ - static bool recursive = false; - - if (recursive) { - return; // disallow recursion - } - recursive = true; - - save_v_event_T save_v_event; - dict_T *dict = get_v_event(&save_v_event); - 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); - restore_v_event(dict, &save_v_event); - - recursive = false; -} - -void init_default_autocmds(void) -{ - // open terminals when opening files that start with term:// -#define PROTO "term://" - do_cmdline_cmd("augroup nvim_terminal"); - do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested " - "if !exists('b:term_title')|call termopen(" - // Capture the command string - "matchstr(expand(\"<amatch>\"), " - "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " - // capture the working directory - "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), " - "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})" - "|endif"); - do_cmdline_cmd("augroup END"); -#undef PROTO - - // limit syntax synchronization in the command window - do_cmdline_cmd("augroup nvim_cmdwin"); - do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1"); - do_cmdline_cmd("augroup END"); -} - -static void focusgained_event(void **argv) -{ - bool *gainedp = argv[0]; - do_autocmd_focusgained(*gainedp); - xfree(gainedp); -} -void aucmd_schedule_focusgained(bool gained) -{ - bool *gainedp = xmalloc(sizeof(*gainedp)); - *gainedp = gained; - loop_schedule_deferred(&main_loop, - event_create(focusgained_event, 1, gainedp)); -} - -static void do_autocmd_focusgained(bool gained) -{ - static bool recursive = false; - static Timestamp last_time = (time_t)0; - bool need_redraw = false; - - if (recursive) { - return; // disallow recursion - } - recursive = true; - need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST), - NULL, NULL, false, curbuf); - - // When activated: Check if any file was modified outside of Vim. - // Only do this when not done within the last two seconds as: - // 1. Some filesystems have modification time granularity in seconds. Fat32 - // has a granularity of 2 seconds. - // 2. We could get multiple notifications in a row. - if (gained && last_time + (Timestamp)2000 < os_now()) { - need_redraw = check_timestamps(true); - last_time = os_now(); - } - - if (need_redraw) { - // Something was executed, make sure the cursor is put back where it - // belongs. - need_wait_return = false; - - if (State & CMDLINE) { - redrawcmdline(); - } else if ((State & NORMAL) || (State & INSERT)) { - if (must_redraw != 0) { - update_screen(0); - } - - setcursor(); - } - - ui_flush(); - } - - if (need_maketitle) { - maketitle(); - } - - recursive = false; -} diff --git a/src/nvim/aucmd.h b/src/nvim/aucmd.h deleted file mode 100644 index 9a4dd79a78..0000000000 --- a/src/nvim/aucmd.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef NVIM_AUCMD_H -#define NVIM_AUCMD_H - -#include <stdint.h> - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "aucmd.h.generated.h" -#endif - -#endif // NVIM_AUCMD_H - diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 9117dde089..a9e1978ac0 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -2,10 +2,15 @@ // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com // autocmd.c: Autocommand related functions +#include <signal.h> +#include "nvim/autocmd.h" + +// #include "nvim/api/private/handle.h" + +#include "lauxlib.h" #include "nvim/api/private/helpers.h" #include "nvim/ascii.h" -#include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/charset.h" #include "nvim/cursor.h" @@ -13,8 +18,11 @@ #include "nvim/eval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_docmd.h" +#include "nvim/ex_getln.h" #include "nvim/fileio.h" #include "nvim/getchar.h" +#include "nvim/lua/executor.h" +#include "nvim/map.h" #include "nvim/option.h" #include "nvim/os/input.h" #include "nvim/regexp.h" @@ -28,6 +36,14 @@ # include "autocmd.c.generated.h" #endif +// Naming Conventions: +// - general autocmd behavior start with au_ +// - AutoCmd start with aucmd_ +// - Autocmd.exec stat with aucmd_exec +// - AutoPat start with aupat_ +// - Groups start with augroup_ +// - Events start with event_ + // // The autocommands are stored in a list for each event. // Autocommands for the same pattern, that are consecutive, are joined @@ -67,21 +83,20 @@ // Code for automatic commands. static AutoPatCmd *active_apc_list = NULL; // stack of active autocommands -/// List of autocmd group names -static garray_T augroups = { 0, 0, sizeof(char_u *), 10, NULL }; -#define AUGROUP_NAME(i) (((char **)augroups.ga_data)[i]) -#define BUFLOCAL_PAT_LEN 25 +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int next_augroup_id = 1; // use get_deleted_augroup() to get this static const char *deleted_augroup = NULL; -// The ID of the current group. Group 0 is the default one. +// The ID of the current group. static int current_augroup = AUGROUP_DEFAULT; -static int au_need_clean = false; // need to delete marked patterns +// Whether we need to delete marked patterns. +// While deleting autocmds, they aren't actually remover, just marked. +static int au_need_clean = false; -static event_T last_event; -static int last_group; static int autocmd_blocked = 0; // block all autocmds static bool autocmd_nested = false; @@ -89,6 +104,33 @@ static bool autocmd_include_groups = false; static char_u *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 + +/// Handles grabbing arguments from `:autocmd` such as ++once and ++nested +#define ARG_GET_FLAG(errored, cmd, flag, pattern, len) \ + if (STRNCMP(cmd, pattern, len) == 0 && ascii_iswhite((cmd)[len])) { \ + if (flag) { \ + semsg(_(e_duparg2), pattern); \ + (errored) = true; \ + } \ + (flag) = true; \ + (cmd) = skipwhite((cmd) + (len)); \ + } + +// Map of autocmd group names. +// name -> ID +static Map(String, int) augroup_map = MAP_INIT; + +static void augroup_map_del(char *name) +{ + String key = map_key(String, int)(&augroup_map, cstr_as_string(name)); + map_del(String, int)(&augroup_map, key); + api_free_string(key); +} + + static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE { if (deleted_augroup == NULL) { @@ -98,7 +140,7 @@ static inline const char *get_deleted_augroup(void) FUNC_ATTR_ALWAYS_INLINE } // Show the autocommands for one AutoPat. -static void show_autocmd(AutoPat *ap, event_T event) +static void aupat_show(AutoPat *ap) { AutoCmd *ac; @@ -107,39 +149,21 @@ static void show_autocmd(AutoPat *ap, event_T event) if (got_int) { return; } + // pattern has been removed if (ap->pat == NULL) { return; } - msg_putchar('\n'); - if (got_int) { - return; - } - if (event != last_event || ap->group != last_group) { - if (ap->group != AUGROUP_DEFAULT) { - if (AUGROUP_NAME(ap->group) == NULL) { - msg_puts_attr(get_deleted_augroup(), HL_ATTR(HLF_E)); - } else { - msg_puts_attr(AUGROUP_NAME(ap->group), HL_ATTR(HLF_T)); - } - msg_puts(" "); - } - msg_puts_attr(event_nr2name(event), HL_ATTR(HLF_T)); - last_event = event; - last_group = ap->group; - msg_putchar('\n'); - if (got_int) { - return; - } - } msg_col = 4; msg_outtrans(ap->pat); for (ac = ap->cmds; ac != NULL; ac = ac->next) { - if (ac->cmd == NULL) { // skip removed commands + // skip removed commands + if (aucmd_exec_is_deleted(ac->exec)) { continue; } + if (msg_col >= 14) { msg_putchar('\n'); } @@ -147,7 +171,7 @@ static void show_autocmd(AutoPat *ap, event_T event) if (got_int) { return; } - msg_outtrans(ac->cmd); + msg_outtrans((char_u *)aucmd_exec_to_string(ac, ac->exec)); if (p_verbose > 0) { last_set_msg(ac->script_ctx); } @@ -163,27 +187,96 @@ static void show_autocmd(AutoPat *ap, event_T event) } } +static void au_show_for_all_events(int group) +{ + FOR_ALL_AUEVENTS(event) { + au_show_for_event(group, event); + } +} + +static void au_show_for_event(int group, event_T event) +{ + // Return early if there are no autocmds for this event + if (au_event_is_empty(event)) { + return; + } + + int previous_group = AUGROUP_ERROR; + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (group != AUGROUP_ALL && group != ap->group) { + continue; + } + + char *name = augroup_name(ap->group); + + msg_putchar('\n'); + // 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; + } + + aupat_show(ap); + + previous_group = ap->group; + } +} + // Mark an autocommand handler for deletion. -static void au_remove_pat(AutoPat *ap) +static void aupat_del(AutoPat *ap) { XFREE_CLEAR(ap->pat); ap->buflocal_nr = -1; au_need_clean = true; } +void aupat_del_for_event_and_group(event_T event, int group) +{ + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == group) { + aupat_del(ap); + } + } + + au_need_clean = true; + au_cleanup(); // may really delete removed patterns/commands now +} + // Mark all commands for a pattern for deletion. -static void au_remove_cmds(AutoPat *ap) +static void aupat_remove_cmds(AutoPat *ap) { for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - XFREE_CLEAR(ac->cmd); + 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 au_del_cmd(AutoCmd *ac) +static void aucmd_del(AutoCmd *ac) { - XFREE_CLEAR(ac->cmd); + aucmd_exec_free(&ac->exec); + if (ac->desc != NULL) { + XFREE_CLEAR(ac->desc); + } au_need_clean = true; } @@ -191,16 +284,15 @@ static void au_del_cmd(AutoCmd *ac) /// This is only done when not executing autocommands. static void au_cleanup(void) { - AutoPat *ap, **prev_ap; - event_T event; + AutoPat *ap; + AutoPat **prev_ap; if (autocmd_busy || !au_need_clean) { return; } // Loop over all events. - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { + FOR_ALL_AUEVENTS(event) { // Loop over all autocommand patterns. prev_ap = &(first_autopat[(int)event]); for (ap = *prev_ap; ap != NULL; ap = *prev_ap) { @@ -211,9 +303,13 @@ static void au_cleanup(void) 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 || ac->cmd == NULL) { + if (ap->pat == NULL || aucmd_exec_is_deleted(ac->exec)) { *prev_ac = ac->next; - xfree(ac->cmd); + aucmd_exec_free(&ac->exec); + if (ac->desc != NULL) { + XFREE_CLEAR(ac->desc); + } + xfree(ac); } else { has_cmd = true; @@ -224,7 +320,7 @@ static void au_cleanup(void) if (ap->pat != NULL && !has_cmd) { // Pattern was not marked for deletion, but all of its commands were. // So mark the pattern for deletion. - au_remove_pat(ap); + aupat_del(ap); } // Remove the pattern if it has been marked for deletion. @@ -250,12 +346,17 @@ static void au_cleanup(void) au_need_clean = false; } + +// Get the first AutoPat for a particular event. +AutoPat *au_get_autopat_for_event(event_T event) +{ + return first_autopat[(int)event]; +} + // Called when buffer is freed, to remove/invalidate related buffer-local // autocmds. void aubuflocal_remove(buf_T *buf) { - AutoPat *ap; - event_T event; AutoPatCmd *apc; // invalidate currently executing autocommands @@ -266,12 +367,11 @@ void aubuflocal_remove(buf_T *buf) } // invalidate buflocals looping through events - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { - // loop over all autocommand patterns - for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) { + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { if (ap->buflocal_nr == buf->b_fnum) { - au_remove_pat(ap); + aupat_del(ap); + if (p_verbose >= 6) { verbose_enter(); smsg(_("auto-removing autocommand: %s <buffer=%d>"), @@ -284,60 +384,61 @@ void aubuflocal_remove(buf_T *buf) au_cleanup(); } -// Add an autocmd group name. +// Add an autocmd group name or return existing group matching name. // Return its ID. -static int au_new_group(char_u *name) +int augroup_add(char *name) { - int i = au_find_group(name); - if (i == AUGROUP_ERROR) { // the group doesn't exist yet, add it. - // First try using a free entry. - for (i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) == NULL) { - break; - } - } - if (i == augroups.ga_len) { - ga_grow(&augroups, 1); - } + assert(STRICMP(name, "end") != 0); - AUGROUP_NAME(i) = xstrdup((char *)name); - if (i == augroups.ga_len) { - augroups.ga_len++; - } + int existing_id = augroup_find(name); + if (existing_id > 0) { + assert(existing_id != AUGROUP_DELETED); + return existing_id; } - return i; + if (existing_id == AUGROUP_DELETED) { + augroup_map_del(name); + } + + int next_id = next_augroup_id++; + String name_copy = cstr_to_string(name); + map_put(String, int)(&augroup_map, name_copy, next_id); + + return next_id; } -static void au_del_group(char_u *name) +/// Delete the augroup that matches name. +void augroup_del(char *name, bool stupid_legacy_mode) { - int i = au_find_group(name); + int i = augroup_find(name); if (i == AUGROUP_ERROR) { // the group doesn't exist semsg(_("E367: No such group: \"%s\""), name); } else if (i == current_augroup) { emsg(_("E936: Cannot delete the current group")); } else { - event_T event; - AutoPat *ap; - int in_use = false; - - for (event = (event_T)0; (int)event < NUM_EVENTS; - event = (event_T)((int)event + 1)) { - for (ap = first_autopat[(int)event]; ap != NULL; ap = ap->next) { - if (ap->group == i && ap->pat != NULL) { - give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true); - in_use = true; - event = NUM_EVENTS; - break; + if (stupid_legacy_mode) { + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == i && ap->pat != NULL) { + give_warning((char_u *)_("W19: Deleting augroup that is still in use"), true); + map_put(String, int)(&augroup_map, cstr_as_string(name), AUGROUP_DELETED); + return; + } } } - } - xfree(AUGROUP_NAME(i)); - if (in_use) { - AUGROUP_NAME(i) = (char *)get_deleted_augroup(); } else { - AUGROUP_NAME(i) = NULL; + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + if (ap->group == i) { + aupat_del(ap); + } + } + } } + + // Remove the group because it's not currently in use. + augroup_map_del(name); + au_cleanup(); } } @@ -346,25 +447,70 @@ static void au_del_group(char_u *name) /// @param name augroup name /// /// @return the ID or AUGROUP_ERROR (< 0) for error. -static int au_find_group(const char_u *name) +int augroup_find(const char *name) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - for (int i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) != NULL && AUGROUP_NAME(i) != get_deleted_augroup() - && STRCMP(AUGROUP_NAME(i), name) == 0) { - return i; - } + int existing_id = map_get(String, int)(&augroup_map, cstr_as_string((char *)name)); + if (existing_id == AUGROUP_DELETED) { + return existing_id; + } + + if (existing_id > 0) { + return existing_id; } + return AUGROUP_ERROR; } +/// Gets the name for a particular group. +char *augroup_name(int group) +{ + assert(group != 0); + + if (group == AUGROUP_DELETED) { + return (char *)get_deleted_augroup(); + } + + if (group == AUGROUP_ALL) { + group = current_augroup; + } + + // next_augroup_id is the "source of truth" about what autocmds have existed + // + // The map_size is not the source of truth because groups can be removed from + // the map. When this happens, the map size is reduced. That's why this function + // relies on next_augroup_id instead. + + // "END" is always considered the last augroup ID. + // Used for expand_get_event_name and expand_get_augroup_name + if (group == next_augroup_id) { + return "END"; + } + + // If it's larger than the largest group, then it doesn't have a name + if (group > next_augroup_id) { + return NULL; + } + + String key; + int value; + map_foreach(&augroup_map, key, value, { + if (value == group) { + return key.data; + } + }); + + // If it's not in the map anymore, then it must have been deleted. + return (char *)get_deleted_augroup(); +} + /// Return true if augroup "name" exists. /// /// @param name augroup name -bool au_has_group(const char_u *name) +bool augroup_exists(const char *name) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - return au_find_group(name) != AUGROUP_ERROR; + return augroup_find(name) > 0; } /// ":augroup {name}". @@ -374,20 +520,27 @@ void do_augroup(char_u *arg, int del_group) if (*arg == NUL) { emsg(_(e_argreq)); } else { - au_del_group(arg); + augroup_del((char *)arg, true); } } else if (STRICMP(arg, "end") == 0) { // ":aug end": back to group 0 current_augroup = AUGROUP_DEFAULT; } else if (*arg) { // ":aug xxx": switch to group xxx - current_augroup = au_new_group(arg); + current_augroup = augroup_add((char *)arg); } else { // ":aug": list the group names msg_start(); - for (int i = 0; i < augroups.ga_len; i++) { - if (AUGROUP_NAME(i) != NULL) { - msg_puts(AUGROUP_NAME(i)); - msg_puts(" "); + + String name; + int value; + map_foreach(&augroup_map, name, value, { + if (value > 0) { + msg_puts(name.data); + } else { + msg_puts(augroup_name(value)); } - } + + msg_puts(" "); + }); + msg_clr_eos(); msg_end(); } @@ -396,25 +549,30 @@ void do_augroup(char_u *arg, int del_group) #if defined(EXITFREE) void free_all_autocmds(void) { - for (current_augroup = -1; current_augroup < augroups.ga_len; - current_augroup++) { - do_autocmd((char_u *)"", true); - } - - for (int i = 0; i < augroups.ga_len; i++) { - char *const s = ((char **)(augroups.ga_data))[i]; - if ((const char *)s != get_deleted_augroup()) { - xfree(s); + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + aupat_del(ap); } } - ga_clear(&augroups); + + au_need_clean = true; + au_cleanup(); // may really delete removed patterns/commands now + + // Delete the augroup_map, including free the data + String name; + int id; + map_foreach(&augroup_map, name, id, { + (void)id; + api_free_string(name); + }) + map_destroy(String, int)(&augroup_map); } #endif // Return the event number for event name "start". // Return NUM_EVENTS if the event name was not found. // Return a pointer to the next event name in "end". -static event_T event_name2nr(const char_u *start, char_u **end) +event_T event_name2nr(const char_u *start, char_u **end) { const char_u *p; int i; @@ -457,33 +615,6 @@ static const char *event_nr2name(event_T event) return "Unknown"; } -/// Scan over the events. "*" stands for all events. -/// true when group name was found -static char_u *find_end_event(char_u *arg, int have_group) -{ - char_u *pat; - char_u *p; - - if (*arg == '*') { - if (arg[1] && !ascii_iswhite(arg[1])) { - semsg(_("E215: Illegal character after *: %s"), arg); - return NULL; - } - pat = arg + 1; - } else { - for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) { - if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) { - if (have_group) { - semsg(_("E216: No such event: %s"), pat); - } else { - semsg(_("E216: No such group or event: %s"), pat); - } - return NULL; - } - } - } - return pat; -} /// Return true if "event" is included in 'eventignore'. /// @@ -601,12 +732,12 @@ void do_autocmd(char_u *arg_in, int forceit) group = AUGROUP_ALL; // no argument, use all groups } else { // Check for a legal group name. If not, use AUGROUP_ALL. - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); } // Scan over the events. // If we find an illegal name, return here, don't do anything. - pat = find_end_event(arg, group != AUGROUP_ALL); + pat = arg_event_skip(arg, group != AUGROUP_ALL); if (pat == NULL) { return; } @@ -643,37 +774,22 @@ void do_autocmd(char_u *arg_in, int forceit) } cmd = skipwhite(cmd); + + bool invalid_flags = false; for (size_t i = 0; i < 2; i++) { if (*cmd != NUL) { - // Check for "++once" flag. - if (STRNCMP(cmd, "++once", 6) == 0 && ascii_iswhite(cmd[6])) { - if (once) { - semsg(_(e_duparg2), "++once"); - } - once = true; - cmd = skipwhite(cmd + 6); - } + ARG_GET_FLAG(invalid_flags, cmd, once, "++once", 6); + ARG_GET_FLAG(invalid_flags, cmd, nested, "++nested", 8); - // Check for "++nested" flag. - if ((STRNCMP(cmd, "++nested", 8) == 0 && ascii_iswhite(cmd[8]))) { - if (nested) { - semsg(_(e_duparg2), "++nested"); - } - nested = true; - cmd = skipwhite(cmd + 8); - } - - // Check for the old (deprecated) "nested" flag. - if (STRNCMP(cmd, "nested", 6) == 0 && ascii_iswhite(cmd[6])) { - if (nested) { - semsg(_(e_duparg2), "nested"); - } - nested = true; - cmd = skipwhite(cmd + 6); - } + // Check the deprecated "nested" flag. + ARG_GET_FLAG(invalid_flags, cmd, nested, "nested", 6); } } + if (invalid_flags) { + return; + } + // Find the start of the commands. // Expand <sfile> in it. if (*cmd != NUL) { @@ -685,36 +801,37 @@ void do_autocmd(char_u *arg_in, int forceit) } } + bool is_showing = !forceit && *cmd == NUL; + // Print header when showing autocommands. - if (!forceit && *cmd == NUL) { + if (is_showing) { // Highlight title msg_puts_title(_("\n--- Autocommands ---")); - } - // Loop over the events. - last_event = (event_T)-1; // for listing the event name - last_group = AUGROUP_ERROR; // for listing the group name - if (*arg == '*' || *arg == NUL || *arg == '|') { - if (!forceit && *cmd != NUL) { - emsg(_(e_cannot_define_autocommands_for_all_events)); + if (*arg == '*' || *arg == '|' || *arg == NUL) { + au_show_for_all_events(group); } else { - for (event_T event = (event_T)0; event < NUM_EVENTS; - event = (event_T)(event + 1)) { + event_T event = event_name2nr(arg, &arg); + assert(event < NUM_EVENTS); + au_show_for_event(group, event); + } + } else { + if (*arg == '*' || *arg == NUL || *arg == '|') { + if (!forceit && *cmd != NUL) { + emsg(_(e_cannot_define_autocommands_for_all_events)); + } else { + do_all_autocmd_events(pat, once, nested, cmd, forceit, group); + } + } else { + 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) { break; } } } - } else { - 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) { - break; - } - } } if (need_free) { @@ -723,30 +840,14 @@ void do_autocmd(char_u *arg_in, int forceit) xfree(envpat); } -// Find the group ID in a ":autocmd" or ":doautocmd" argument. -// The "argp" argument is advanced to the following argument. -// -// Returns the group ID or AUGROUP_ALL. -static int au_get_grouparg(char_u **argp) +void do_all_autocmd_events(char_u *pat, bool once, int nested, char_u *cmd, bool delete, int group) { - char_u *group_name; - char_u *p; - char_u *arg = *argp; - int group = AUGROUP_ALL; - - for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) { - } - if (p > arg) { - group_name = vim_strnsave(arg, (size_t)(p - arg)); - group = au_find_group(group_name); - if (group == AUGROUP_ERROR) { - group = AUGROUP_ALL; // no match, use all groups - } else { - *argp = skipwhite(p); // match, skip over group name + FOR_ALL_AUEVENTS(event) { + if (do_autocmd_event(event, pat, once, nested, cmd, delete, group) + == FAIL) { + return; } - xfree(group_name); } - return group; } // do_autocmd() for one event. @@ -756,216 +857,293 @@ static int au_get_grouparg(char_u **argp) // If *cmd == NUL: show entries. // If forceit == true: delete entries. // If group is not AUGROUP_ALL: only use this group. -static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd, - int forceit, int group) +int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, char_u *cmd, bool delete, + 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 || delete); + AutoPat *ap; AutoPat **prev_ap; - AutoCmd *ac; - AutoCmd **prev_ac; - int brace_level; - char_u *endpat; int findgroup; - int allgroups; int patlen; int is_buflocal; int buflocal_nr; char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>" + bool is_adding_cmd = *cmd != NUL; + if (group == AUGROUP_ALL) { findgroup = current_augroup; } else { findgroup = group; } - allgroups = (group == AUGROUP_ALL && !forceit && *cmd == NUL); - // Show or delete all patterns for an event. - if (*pat == NUL) { - for (ap = first_autopat[event]; ap != NULL; ap = ap->next) { - if (forceit) { // delete the AutoPat, if it's in the current group - if (ap->group == findgroup) { - au_remove_pat(ap); - } - } else if (group == AUGROUP_ALL || ap->group == group) { - show_autocmd(ap, event); - } - } + // Delete all aupat for an event. + if (*pat == NUL && delete) { + aupat_del_for_event_and_group(event, findgroup); + return OK; } // Loop through all the specified patterns. - for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) { - // Find end of the pattern. - // Watch out for a comma in braces, like "*.\{obj,o\}". - endpat = pat; - // ignore single comma - if (*endpat == ',') { - continue; - } - brace_level = 0; - for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); - endpat++) { - if (*endpat == '{') { - brace_level++; - } else if (*endpat == '}') { - brace_level--; - } - } - patlen = (int)(endpat - pat); - - // detect special <buflocal[=X]> buffer-local patterns - is_buflocal = false; + patlen = (int)aucmd_pattern_length(pat); + while (patlen) { + // detect special <buffer[=X]> buffer-local patterns + is_buflocal = aupat_is_buflocal(pat, patlen); buflocal_nr = 0; - if (patlen >= 8 && STRNCMP(pat, "<buffer", 7) == 0 - && pat[patlen - 1] == '>') { - // "<buffer...>": Error will be printed only for addition. - // printing and removing will proceed silently. - is_buflocal = true; - if (patlen == 8) { - // "<buffer>" - buflocal_nr = curbuf->b_fnum; - } else if (patlen > 9 && pat[7] == '=') { - if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) { - // "<buffer=abuf>" - buflocal_nr = autocmd_bufnr; - } else if (skipdigits(pat + 8) == pat + patlen - 1) { - // "<buffer=123>" - buflocal_nr = atoi((char *)pat + 8); - } - } - } - if (is_buflocal) { + buflocal_nr = aupat_get_buflocal_nr(pat, patlen); + // normalize pat into standard "<buffer>#N" form - snprintf((char *)buflocal_pat, - BUFLOCAL_PAT_LEN, - "<buffer=%d>", - buflocal_nr); + aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr); - pat = buflocal_pat; // can modify pat and patlen - patlen = (int)STRLEN(buflocal_pat); // but not endpat + pat = buflocal_pat; + patlen = (int)STRLEN(buflocal_pat); } - // Find AutoPat entries with this pattern. When adding a command it - // always goes at or after the last one, so start at the end. - if (!forceit && *cmd != NUL && last_autopat[(int)event] != NULL) { - prev_ap = &last_autopat[(int)event]; - } else { + if (delete) { + assert(*pat != NUL); + + // Find AutoPat entries with this pattern. + // always goes at or after the last one, so start at the end. 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, or a group was - // not specified and it's the current group, or a group was - // not specified and we are listing - // - the length of the pattern matches - // - the pattern matches. - // For <buffer[=X]>, this condition works because we normalize - // all buffer-local patterns. - if ((allgroups || ap->group == findgroup) && ap->patlen == patlen - && STRNCMP(pat, ap->pat, 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 (forceit) { - if (*cmd != NUL && ap->next == NULL) { - au_remove_cmds(ap); + 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 <buffer[=X]>, this condition works because we normalize + // all buffer-local patterns. + if (ap->group == findgroup + && ap->patlen == patlen + && STRNCMP(pat, ap->pat, 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; } - au_remove_pat(ap); - } else if (*cmd == NUL) { - // Show autocmd's for this autopat, or buflocals <buffer=X> - show_autocmd(ap, event); - } else if (ap->next == NULL) { - // Add autocmd to this autopat, if it's the last one. - break; + aupat_del(ap); } } + prev_ap = &ap->next; } - prev_ap = &ap->next; } - // Add a new command. - if (*cmd != NUL) { - // 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. - 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)) { - semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr); - return FAIL; - } + if (is_adding_cmd) { + AucmdExecutable exec = AUCMD_EXECUTABLE_INIT; + exec.type = CALLABLE_EX; + exec.callable.cmd = cmd; + autocmd_register(0, event, pat, patlen, group, once, nested, NULL, exec); + } - ap = xmalloc(sizeof(AutoPat)); - ap->pat = vim_strnsave(pat, (size_t)patlen); - ap->patlen = patlen; + pat = aucmd_next_pattern(pat, (size_t)patlen); + patlen = (int)aucmd_pattern_length(pat); + } - if (is_buflocal) { - ap->buflocal_nr = buflocal_nr; - ap->reg_prog = NULL; - } else { - char_u *reg_pat; + au_cleanup(); + return OK; +} - ap->buflocal_nr = 0; - reg_pat = file_pat_to_reg_pat(pat, endpat, &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; - } - } +int autocmd_register(int64_t id, event_T event, char_u *pat, int patlen, int group, bool once, + bool nested, char *desc, AucmdExecutable aucmd) +{ + // 0 is not a valid group. + assert(group != 0); - // need to initialize last_mode for the first ModeChanged autocmd - if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) { - xfree(last_mode); - last_mode = get_mode(); - } + AutoPat *ap; + AutoPat **prev_ap; + AutoCmd *ac; + AutoCmd **prev_ac; + int is_buflocal; + int buflocal_nr; + int findgroup; + char_u buflocal_pat[BUFLOCAL_PAT_LEN]; // for "<buffer=X>" - // If the event is CursorMoved, update the last cursor position - // position to avoid immediately triggering the autocommand - if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) { - curwin->w_last_cursormoved = curwin->w_cursor; - } + if (patlen > (int)STRLEN(pat)) { + return FAIL; + } - 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; + if (group == AUGROUP_ALL) { + findgroup = current_augroup; + } else { + findgroup = group; + } + + + // detect special <buffer[=X]> buffer-local patterns + is_buflocal = aupat_is_buflocal(pat, patlen); + buflocal_nr = 0; + + if (is_buflocal) { + buflocal_nr = aupat_get_buflocal_nr(pat, patlen); + + // normalize pat into standard "<buffer>#N" form + aupat_normalize_buflocal_pat(buflocal_pat, pat, patlen, buflocal_nr); + + pat = buflocal_pat; + patlen = (int)STRLEN(buflocal_pat); + } + + 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 <buffer[=X]>, this condition works because we normalize + // all buffer-local patterns. + if (ap->group == findgroup + && ap->patlen == patlen + && STRNCMP(pat, ap->pat, patlen) == 0) { + if (ap->next == NULL) { + // Add autocmd to this autopat, if it's the last one. + break; } } + } + prev_ap = &ap->next; + } + + // 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. + 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)) { + semsg(_("E680: <buffer=%d>: invalid buffer number "), buflocal_nr); + return FAIL; + } + + ap = xmalloc(sizeof(AutoPat)); + ap->pat = vim_strnsave(pat, (size_t)patlen); + ap->patlen = patlen; + + if (is_buflocal) { + ap->buflocal_nr = buflocal_nr; + ap->reg_prog = NULL; + } else { + char_u *reg_pat; - // Add the autocmd at the end of the AutoCmd list. - prev_ac = &(ap->cmds); - while ((ac = *prev_ac) != NULL) { - prev_ac = &ac->next; + ap->buflocal_nr = 0; + 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; } - ac = xmalloc(sizeof(AutoCmd)); - ac->cmd = vim_strsave(cmd); - ac->script_ctx = current_sctx; - ac->script_ctx.sc_lnum += sourcing_lnum; - ac->next = NULL; - *prev_ac = ac; - ac->once = once; - ac->nested = nested; + } + + // need to initialize last_mode for the first ModeChanged autocmd + if (event == EVENT_MODECHANGED && !has_event(EVENT_MODECHANGED)) { + xfree(last_mode); + last_mode = get_mode(); + } + + // If the event is CursorMoved, update the last cursor position + // position to avoid immediately triggering the autocommand + if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) { + curwin->w_last_cursormoved = curwin->w_cursor; + } + + 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; } } - au_cleanup(); // may really delete removed patterns/commands now + // Add the autocmd at the end of the AutoCmd list. + prev_ac = &(ap->cmds); + while ((ac = *prev_ac) != NULL) { + prev_ac = &ac->next; + } + + ac = xmalloc(sizeof(AutoCmd)); + *prev_ac = ac; + + ac->id = id; + ac->exec = aucmd_exec_copy(aucmd); + ac->script_ctx = current_sctx; + ac->script_ctx.sc_lnum += sourcing_lnum; + 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: <lua>DESCRIPTION or similar + if (desc != NULL) { + ac->desc = xstrdup(desc); + } else { + ac->desc = aucmd_exec_default_desc(aucmd); + } + return OK; } +size_t aucmd_pattern_length(char_u *pat) +{ + if (*pat == NUL) { + return 0; + } + + char_u *endpat; + + for (; *pat; pat = (*endpat == ',' ? endpat + 1 : endpat)) { + // Find end of the pattern. + // Watch out for a comma in braces, like "*.\{obj,o\}". + endpat = pat; + // ignore single comma + if (*endpat == ',') { + continue; + } + int brace_level = 0; + for (; *endpat && (*endpat != ',' || brace_level || endpat[-1] == '\\'); + endpat++) { + if (*endpat == '{') { + brace_level++; + } else if (*endpat == '}') { + brace_level--; + } + } + + return (size_t)(endpat - pat); + } + + return STRLEN(pat); +} + +char_u *aucmd_next_pattern(char_u *pat, size_t patlen) +{ + pat = pat + patlen; + if (*pat == ',') { + pat = pat + 1; + } + + return pat; +} + /// Implementation of ":doautocmd [group] event [fname]". /// Return OK for success, FAIL for failure; /// @@ -981,7 +1159,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something) } // Check for a legal group name. If not, use AUGROUP_ALL. - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); if (*arg == '*') { emsg(_("E217: Can't execute autocommands for ALL events")); @@ -990,7 +1168,7 @@ int do_doautocmd(char_u *arg, bool do_msg, bool *did_something) // Scan over the events. // If we find an illegal name, return here, don't do anything. - fname = find_end_event(arg, group != AUGROUP_ALL); + fname = arg_event_skip(arg, group != AUGROUP_ALL); if (fname == NULL) { return FAIL; } @@ -1378,8 +1556,8 @@ bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT /// @param eap Ex command arguments /// /// @return true if some commands were executed. -static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force, - int group, buf_T *buf, exarg_T *eap) +bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, bool force, int group, + buf_T *buf, exarg_T *eap) { char_u *sfname = NULL; // short file name char_u *tail; @@ -1402,7 +1580,7 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, proftime_T wait_time; bool did_save_redobuff = false; save_redo_T save_redo; - const bool save_KeyTyped = KeyTyped; + const bool save_KeyTyped = KeyTyped; // NOLINT // Quickly return if there are no autocommands for this event or // autocommands are blocked. @@ -1814,6 +1992,11 @@ void auto_next_pat(AutoPatCmd *apc, int stop_at_last) /// @return allocated string, or NULL for end of autocommands. char_u *getnextac(int c, void *cookie, int indent, bool do_concat) { + // These arguments are required for do_cmdline. + (void)c; + (void)indent; + (void)do_concat; + AutoPatCmd *acp = (AutoPatCmd *)cookie; char_u *retval; AutoCmd *ac; @@ -1826,7 +2009,8 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat) // repeat until we find an autocommand to execute for (;;) { // skip removed commands - while (acp->nextcmd != NULL && acp->nextcmd->cmd == NULL) { + while (acp->nextcmd != NULL + && aucmd_exec_is_deleted(acp->nextcmd->exec)) { if (acp->nextcmd->last) { acp->nextcmd = NULL; } else { @@ -1856,14 +2040,32 @@ char_u *getnextac(int c, void *cookie, int indent, bool do_concat) if (p_verbose >= 9) { verbose_enter_scroll(); - smsg(_("autocommand %s"), ac->cmd); + smsg(_("autocommand %s"), aucmd_exec_to_string(ac, ac->exec)); msg_puts("\n"); // don't overwrite this either verbose_leave_scroll(); } - retval = vim_strsave(ac->cmd); + + if (ac->exec.type == CALLABLE_CB) { + typval_T argsin = TV_INITIAL_VALUE; + typval_T rettv = TV_INITIAL_VALUE; + callback_call(&ac->exec.callable.cb, 0, &argsin, &rettv); + + // Major Hack Alert: + // We just return "not-null" and continue going. + // This would be a good candidate for a refactor. You would need to refactor: + // 1. do_cmdline to accept something besides a string + // OR + // 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 woudl be that could be expensive. + retval = vim_strsave((char_u *)""); + } else { + retval = vim_strsave(ac->exec.callable.cmd); + } + // Remove one-shot ("once") autocmd in anticipation of its execution. if (ac->once) { - au_del_cmd(ac); + aucmd_del(ac); } autocmd_nested = ac->nested; current_sctx = ac->script_ctx; @@ -1928,19 +2130,12 @@ bool has_autocmd(event_T event, char_u *sfname, buf_T *buf) FUNC_ATTR_WARN_UNUSE // Function given to ExpandGeneric() to obtain the list of autocommand group // names. -char_u *get_augroup_name(expand_T *xp, int idx) +char_u *expand_get_augroup_name(expand_T *xp, int idx) { - if (idx == augroups.ga_len) { // add "END" add the end - return (char_u *)"END"; - } - if (idx >= augroups.ga_len) { // end of list - return NULL; - } - if (AUGROUP_NAME(idx) == NULL || AUGROUP_NAME(idx) == get_deleted_augroup()) { - // skip deleted entries - return (char_u *)""; - } - return (char_u *)AUGROUP_NAME(idx); + // Required for ExpandGeneric + (void)xp; + + return (char_u *)augroup_name(idx + 1); } /// @param doautocmd true for :doauto*, false for :autocmd @@ -1952,7 +2147,7 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd) // check for a group name, skip it if present autocmd_include_groups = false; p = arg; - group = au_get_grouparg(&arg); + group = arg_augroup_get(&arg); // If there only is a group name that's what we expand. if (*arg == NUL && group != AUGROUP_ALL && !ascii_iswhite(arg[-1])) { @@ -1993,16 +2188,25 @@ char_u *set_context_in_autocmd(expand_T *xp, char_u *arg, int doautocmd) } // Function given to ExpandGeneric() to obtain the list of event names. -char_u *get_event_name(expand_T *xp, int idx) +char_u *expand_get_event_name(expand_T *xp, int idx) { - if (idx < augroups.ga_len) { // First list group names, if wanted - if (!autocmd_include_groups || AUGROUP_NAME(idx) == NULL - || AUGROUP_NAME(idx) == get_deleted_augroup()) { - return (char_u *)""; // skip deleted entries + // xp is a required parameter to be used with ExpandGeneric + (void)xp; + + + // List group names + char *name = augroup_name(idx + 1); + if (name != NULL) { + // skip when not including groups or skip deleted entries + if (!autocmd_include_groups || name == get_deleted_augroup()) { + return (char_u *)""; } - return (char_u *)AUGROUP_NAME(idx); + + return (char_u *)name; } - return (char_u *)event_names[idx - augroups.ga_len].name; + + // List event names + return (char_u *)event_names[idx - next_augroup_id].name; } /// Check whether given autocommand is supported @@ -2045,7 +2249,7 @@ bool au_exists(const char *const arg) FUNC_ATTR_WARN_UNUSED_RESULT } // First, look for an autocmd group name. - group = au_find_group((char_u *)arg_save); + group = augroup_find(arg_save); char *event_name; if (group == AUGROUP_ERROR) { // Didn't match a group name, assume the first argument is an event. @@ -2109,3 +2313,346 @@ theend: xfree(arg_save); return retval; } + +// Checks if a pattern is buflocal +bool aupat_is_buflocal(char_u *pat, int patlen) +{ + return patlen >= 8 + && STRNCMP(pat, "<buffer", 7) == 0 + && (pat)[patlen - 1] == '>'; +} + +int aupat_get_buflocal_nr(char_u *pat, int patlen) +{ + assert(aupat_is_buflocal(pat, patlen)); + + // "<buffer>" + if (patlen == 8) { + return curbuf->b_fnum; + } + + if (patlen > 9 && (pat)[7] == '=') { + // "<buffer=abuf>" + if (patlen == 13 && STRNICMP(pat, "<buffer=abuf>", 13) == 0) { + return autocmd_bufnr; + } + + // "<buffer=123>" + if (skipdigits(pat + 8) == pat + patlen - 1) { + return atoi((char *)pat + 8); + } + } + + return 0; +} + +// normalize buffer pattern +void aupat_normalize_buflocal_pat(char_u *dest, char_u *pat, int patlen, int buflocal_nr) +{ + assert(aupat_is_buflocal(pat, patlen)); + + if (buflocal_nr == 0) { + buflocal_nr = curbuf->handle; + } + + // normalize pat into standard "<buffer>#N" form + snprintf((char *)dest, + BUFLOCAL_PAT_LEN, + "<buffer=%d>", + buflocal_nr); +} + +int autocmd_delete_event(int group, event_T event, char_u *pat) + FUNC_ATTR_NONNULL_ALL +{ + return do_autocmd_event(event, pat, false, false, (char_u *)"", true, group); +} + +/// Deletes an autocmd by ID. +/// Only autocmds created via the API have IDs associated with them. There +/// is no way to delete a specific autocmd created via :autocmd +void autocmd_delete_id(int64_t id) +{ + FOR_ALL_AUEVENTS(event) { + FOR_ALL_AUPATS_IN_EVENT(event, ap) { + for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { + if (ac->id == id) { + aucmd_del(ac); + } + } + } + } +} + +// =========================================================================== +// AucmdExecutable Functions +// =========================================================================== +char *aucmd_exec_default_desc(AucmdExecutable acc) +{ + size_t msglen = 100; + + switch (acc.type) { + case CALLABLE_CB: + switch (acc.callable.cb.type) { + case kCallbackLua: { + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<Lua function %d>", acc.callable.cb.data.luaref); + return msg; + } + case kCallbackFuncref: { + // TODO(tjdevries): Is this enough space for this? + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<vim function: %s>", acc.callable.cb.data.funcref); + return msg; + } + case kCallbackPartial: { + char *msg = (char *)xmallocz(msglen); + snprintf(msg, msglen, "<vim partial: %s>", acc.callable.cb.data.partial->pt_name); + return msg; + } + default: + return NULL; + } + default: + return NULL; + } + + abort(); +} +char *aucmd_exec_to_string(AutoCmd *ac, AucmdExecutable acc) +{ + switch (acc.type) { + case CALLABLE_EX: + return (char *)acc.callable.cmd; + case CALLABLE_CB: + return ac->desc; + case CALLABLE_NONE: + return "This is not possible"; + } + + abort(); +} + +void aucmd_exec_free(AucmdExecutable *acc) +{ + switch (acc->type) { + case CALLABLE_EX: + XFREE_CLEAR(acc->callable.cmd); + break; + case CALLABLE_CB: + callback_free(&acc->callable.cb); + break; + case CALLABLE_NONE: + return; + } + + acc->type = CALLABLE_NONE; +} + +AucmdExecutable aucmd_exec_copy(AucmdExecutable src) +{ + AucmdExecutable dest = AUCMD_EXECUTABLE_INIT; + + switch (src.type) { + case CALLABLE_EX: + dest.type = CALLABLE_EX; + dest.callable.cmd = vim_strsave(src.callable.cmd); + return dest; + case CALLABLE_CB: + dest.type = CALLABLE_CB; + callback_copy(&dest.callable.cb, &src.callable.cb); + return dest; + case CALLABLE_NONE: + return dest; + } + + abort(); +} + +bool aucmd_exec_is_deleted(AucmdExecutable acc) +{ + switch (acc.type) { + case CALLABLE_EX: + return acc.callable.cmd == NULL; + case CALLABLE_CB: + return callback_is_freed(acc.callable.cb); + case CALLABLE_NONE: + return true; + } + + abort(); +} + +bool au_event_is_empty(event_T event) +{ + return first_autopat[event] == NULL; +} + +// Arg Parsing Functions + +/// Scan over the events. "*" stands for all events. +/// true when group name was found +static char_u *arg_event_skip(char_u *arg, int have_group) +{ + char_u *pat; + char_u *p; + + if (*arg == '*') { + if (arg[1] && !ascii_iswhite(arg[1])) { + semsg(_("E215: Illegal character after *: %s"), arg); + return NULL; + } + pat = arg + 1; + } else { + for (pat = arg; *pat && *pat != '|' && !ascii_iswhite(*pat); pat = p) { + if ((int)event_name2nr(pat, &p) >= NUM_EVENTS) { + if (have_group) { + semsg(_("E216: No such event: %s"), pat); + } else { + semsg(_("E216: No such group or event: %s"), pat); + } + return NULL; + } + } + } + return pat; +} + +// Find the group ID in a ":autocmd" or ":doautocmd" argument. +// The "argp" argument is advanced to the following argument. +// +// Returns the group ID or AUGROUP_ALL. +static int arg_augroup_get(char_u **argp) +{ + char_u *group_name; + char_u *p; + char_u *arg = *argp; + int group = AUGROUP_ALL; + + for (p = arg; *p && !ascii_iswhite(*p) && *p != '|'; p++) { + } + if (p > arg) { + group_name = vim_strnsave(arg, (size_t)(p - arg)); + group = augroup_find((char *)group_name); + if (group == AUGROUP_ERROR) { + group = AUGROUP_ALL; // no match, use all groups + } else { + *argp = skipwhite(p); // match, skip over group name + } + xfree(group_name); + } + return group; +} + + +// UI Enter +void do_autocmd_uienter(uint64_t chanid, bool attached) +{ + static bool recursive = false; + + if (recursive) { + return; // disallow recursion + } + recursive = true; + + save_v_event_T save_v_event; + dict_T *dict = get_v_event(&save_v_event); + 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); + restore_v_event(dict, &save_v_event); + + recursive = false; +} + +// FocusGained + +static void focusgained_event(void **argv) +{ + bool *gainedp = argv[0]; + do_autocmd_focusgained(*gainedp); + xfree(gainedp); +} + +void autocmd_schedule_focusgained(bool gained) +{ + bool *gainedp = xmalloc(sizeof(*gainedp)); + *gainedp = gained; + loop_schedule_deferred(&main_loop, + event_create(focusgained_event, 1, gainedp)); +} + +static void do_autocmd_focusgained(bool gained) +{ + static bool recursive = false; + static Timestamp last_time = (time_t)0; + bool need_redraw = false; + + if (recursive) { + return; // disallow recursion + } + recursive = true; + need_redraw |= apply_autocmds((gained ? EVENT_FOCUSGAINED : EVENT_FOCUSLOST), + NULL, NULL, false, curbuf); + + // When activated: Check if any file was modified outside of Vim. + // Only do this when not done within the last two seconds as: + // 1. Some filesystems have modification time granularity in seconds. Fat32 + // has a granularity of 2 seconds. + // 2. We could get multiple notifications in a row. + if (gained && last_time + (Timestamp)2000 < os_now()) { + need_redraw = check_timestamps(true); + last_time = os_now(); + } + + if (need_redraw) { + // Something was executed, make sure the cursor is put back where it + // belongs. + need_wait_return = false; + + if (State & CMDLINE) { + redrawcmdline(); + } else if ((State & NORMAL) || (State & INSERT)) { + if (must_redraw != 0) { + update_screen(0); + } + + setcursor(); + } + + ui_flush(); + } + + if (need_maketitle) { + maketitle(); + } + + recursive = false; +} + +// initialization + +void init_default_autocmds(void) +{ + // open terminals when opening files that start with term:// +#define PROTO "term://" + do_cmdline_cmd("augroup nvim_terminal"); + do_cmdline_cmd("autocmd BufReadCmd " PROTO "* ++nested " + "if !exists('b:term_title')|call termopen(" + // Capture the command string + "matchstr(expand(\"<amatch>\"), " + "'\\c\\m" PROTO "\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " + // capture the working directory + "{'cwd': expand(get(matchlist(expand(\"<amatch>\"), " + "'\\c\\m" PROTO "\\(.\\{-}\\)//'), 1, ''))})" + "|endif"); + do_cmdline_cmd("augroup END"); +#undef PROTO + + // limit syntax synchronization in the command window + do_cmdline_cmd("augroup nvim_cmdwin"); + do_cmdline_cmd("autocmd! CmdwinEnter [:>] syntax sync minlines=1 maxlines=1"); + do_cmdline_cmd("augroup END"); +} diff --git a/src/nvim/autocmd.h b/src/nvim/autocmd.h index 63c5abd4f8..0a1f036183 100644 --- a/src/nvim/autocmd.h +++ b/src/nvim/autocmd.h @@ -4,6 +4,11 @@ #include "nvim/buffer_defs.h" #include "nvim/ex_cmds_defs.h" +// event_T definition +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "auevents_enum.generated.h" +#endif + // Struct to save values in before executing autocommands for a buffer that is // not the current buffer. typedef struct { @@ -18,22 +23,23 @@ typedef struct { } aco_save_T; typedef struct AutoCmd { - char_u *cmd; // Command to be executed (NULL when - // command has been removed) + AucmdExecutable exec; bool once; // "One shot": removed after execution bool nested; // If autocommands nest here bool last; // last command in list + int64_t id; // TODO(tjdevries): Explain sctx_T script_ctx; // script context where defined - struct AutoCmd *next; // Next AutoCmd in list + char *desc; // Description for the autocmd. + struct AutoCmd *next; // Next AutoCmd in list } AutoCmd; typedef struct AutoPat { - struct AutoPat *next; // next AutoPat in AutoPat list; MUST - // be the first entry - char_u *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 + struct AutoPat *next; // next AutoPat in AutoPat list; MUST + // be the first entry + char_u *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 @@ -41,13 +47,7 @@ typedef struct AutoPat { char last; // last pattern for apply_autocmds() } AutoPat; -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "auevents_enum.generated.h" -#endif - -/// /// Struct used to keep status while executing autocommands for an event. -/// typedef struct AutoPatCmd { AutoPat *curpat; // next AutoPat to examine AutoCmd *nextcmd; // next AutoCmd to execute @@ -75,8 +75,16 @@ EXTERN bool au_did_filetype INIT(= false); # include "autocmd.h.generated.h" #endif -#define AUGROUP_DEFAULT -1 // default autocmd group -#define AUGROUP_ERROR -2 // erroneous autocmd group -#define AUGROUP_ALL -3 // all autocmd groups +#define AUGROUP_DEFAULT (-1) // default autocmd group +#define AUGROUP_ERROR (-2) // erroneous autocmd group +#define AUGROUP_ALL (-3) // all autocmd groups +#define AUGROUP_DELETED (-4) // all autocmd groups +// #define AUGROUP_NS -5 // TODO(tjdevries): Support namespaced based augroups + +#define BUFLOCAL_PAT_LEN 25 + +/// Iterates over all the events for auto commands +#define FOR_ALL_AUEVENTS(event) \ + for (event_T event = (event_T)0; (int)event < (int)NUM_EVENTS; event = (event_T)((int)event + 1)) // NOLINT #endif // NVIM_AUTOCMD_H diff --git a/src/nvim/diff.c b/src/nvim/diff.c index 233753839b..80bd3229c6 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -85,9 +85,9 @@ typedef struct { // used for recording hunks from xdiff typedef struct { linenr_T lnum_orig; - long count_orig; + long count_orig; linenr_T lnum_new; - long count_new; + long count_new; } diffhunk_T; // two diff inputs and one result @@ -1285,7 +1285,7 @@ void ex_diffpatch(exarg_T *eap) ex_file(eap); // Do filetype detection with the new name. - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { do_cmdline_cmd(":doau filetypedetect BufRead"); } } @@ -3159,8 +3159,7 @@ static int parse_diff_unified(char_u *line, diffhunk_T *hunk) /// Callback function for the xdl_diff() function. /// Stores the diff output in a grow array. /// -static int xdiff_out(long start_a, long count_a, long start_b, long count_b, - void *priv) +static int xdiff_out(long start_a, long count_a, long start_b, long count_b, void *priv) { diffout_T *dout = (diffout_T *)priv; diffhunk_T *p = xmalloc(sizeof(*p)); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 07f0799bc4..0322898827 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -6,6 +6,7 @@ */ #include <math.h> +#include <stdlib.h> #include "auto/config.h" @@ -3258,7 +3259,7 @@ char_u *get_user_var_name(expand_T *xp, int idx) // b: variables // In cmdwin, the alternative buffer should be used. hashtab_T *ht - = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab; + = is_in_cmdwin() ? &prevwin->w_buffer->b_vars->dv_hashtab : &curbuf->b_vars->dv_hashtab; if (bdone < ht->ht_used) { if (bdone++ == 0) { hi = ht->ht_array; @@ -7746,6 +7747,7 @@ bool callback_from_typval(Callback *const callback, typval_T *const arg) callback->type = kCallbackFuncref; } } else if (nlua_is_table_from_lua(arg)) { + // TODO(tjdvries): UnifiedCallback char_u *name = nlua_register_table_as_callable(arg); if (name != NULL) { @@ -7775,6 +7777,7 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co { partial_T *partial; char_u *name; + Array args = ARRAY_DICT_INIT; switch (callback->type) { case kCallbackFuncref: name = callback->data.funcref; @@ -7786,6 +7789,13 @@ bool callback_call(Callback *const callback, const int argcount_in, typval_T *co name = partial_name(partial); break; + case kCallbackLua: + ILOG(" We tryin to call dat dang lua ref "); + nlua_call_ref(callback->data.luaref, "aucmd", args, false, NULL); + + return false; + break; + case kCallbackNone: return false; break; diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index c5b01701de..49dde537c3 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -894,6 +894,7 @@ static void f_call(typval_T *argvars, typval_T *rettv, FunPtr fptr) partial = argvars[0].vval.v_partial; func = partial_name(partial); } else if (nlua_is_table_from_lua(&argvars[0])) { + // TODO(tjdevries): UnifiedCallback func = nlua_register_table_as_callable(&argvars[0]); owned = true; } else { diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 6f8b032d41..fbda7fbc3c 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -28,11 +28,11 @@ #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/message.h" +#include "nvim/os/fileio.h" #include "nvim/os/input.h" #include "nvim/pos.h" #include "nvim/types.h" #include "nvim/vim.h" -#include "nvim/os/fileio.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "eval/typval.c.generated.h" @@ -1123,6 +1123,8 @@ bool tv_callback_equal(const Callback *cb1, const Callback *cb2) // FIXME: this is inconsistent with tv_equal but is needed for precision // maybe change dictwatcheradd to return a watcher id instead? return cb1->data.partial == cb2->data.partial; + case kCallbackLua: + return cb1->data.luaref == cb2->data.luaref; case kCallbackNone: return true; } @@ -1142,6 +1144,9 @@ void callback_free(Callback *callback) case kCallbackPartial: partial_unref(callback->data.partial); break; + case kCallbackLua: + NLUA_CLEAR_REF(callback->data.luaref); + break; case kCallbackNone: break; } @@ -1149,6 +1154,12 @@ void callback_free(Callback *callback) callback->data.funcref = NULL; } +/// Check if callback is freed +bool callback_is_freed(Callback callback) +{ + return false; +} + /// Copy a callback into a typval_T. void callback_put(Callback *cb, typval_T *tv) FUNC_ATTR_NONNULL_ALL @@ -1164,6 +1175,9 @@ void callback_put(Callback *cb, typval_T *tv) tv->vval.v_string = vim_strsave(cb->data.funcref); func_ref(cb->data.funcref); break; + case kCallbackLua: + // TODO(tjdevries): I'm not even sure if this is strictly necessary? + abort(); default: tv->v_type = VAR_SPECIAL; tv->vval.v_special = kSpecialVarNull; @@ -1185,6 +1199,9 @@ void callback_copy(Callback *dest, Callback *src) dest->data.funcref = vim_strsave(src->data.funcref); func_ref(src->data.funcref); break; + case kCallbackLua: + dest->data.luaref = api_new_luaref(src->data.luaref); + break; default: dest->data.funcref = NULL; break; diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index ad01c01499..40dc819754 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -72,15 +72,18 @@ typedef enum { kCallbackNone = 0, kCallbackFuncref, kCallbackPartial, + kCallbackLua, } CallbackType; typedef struct { union { char_u *funcref; partial_T *partial; + LuaRef luaref; } data; CallbackType type; } Callback; + #define CALLBACK_INIT { .type = kCallbackNone } #define CALLBACK_NONE ((Callback)CALLBACK_INIT) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 49bf9972b1..c7910e148d 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1943,7 +1943,7 @@ int do_write(exarg_T *eap) // If 'filetype' was empty try detecting it now. if (*curbuf->b_p_ft == NUL) { - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { (void)do_doautocmd((char_u *)"filetypedetect BufRead", true, NULL); } do_modelines(0); diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index eaf5f627b6..92675499be 100644 --- a/src/nvim/ex_cmds_defs.h +++ b/src/nvim/ex_cmds_defs.h @@ -4,6 +4,7 @@ #include <stdbool.h> #include <stdint.h> +#include "nvim/eval/typval.h" #include "nvim/normal.h" #include "nvim/pos.h" // for linenr_T #include "nvim/regexp_defs.h" @@ -91,6 +92,35 @@ typedef struct exarg exarg_T; typedef void (*ex_func_T)(exarg_T *eap); +// NOTE: These possible could be removed and changed so that +// Callback could take a "command" style string, and simply +// execute that (instead of it being a function). +// +// But it's still a bit weird to do that. +// +// Another option would be that we just make a callback reference to +// "execute($INPUT)" or something like that, so whatever the user +// sends in via autocmds is just executed via this. +// +// However, that would probably have some performance cost (probably +// very marginal, but still some cost either way). +typedef enum { + CALLABLE_NONE, + CALLABLE_EX, + CALLABLE_CB, +} AucmdExecutableType; + +typedef struct aucmd_executable_t AucmdExecutable; +struct aucmd_executable_t { + AucmdExecutableType type; + union { + char_u *cmd; + Callback cb; + } callable; +}; + +#define AUCMD_EXECUTABLE_INIT { .type = CALLABLE_NONE } + typedef char_u *(*LineGetter)(int, void *, int, bool); /// Structure for command definition. diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ed4475eb1a..30287cd6f2 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -5049,8 +5049,8 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u ** { EXPAND_SYNTAX, get_syntax_name, true, true }, { EXPAND_SYNTIME, get_syntime_arg, true, true }, { EXPAND_HIGHLIGHT, (ExpandFunc)get_highlight_name, true, true }, - { EXPAND_EVENTS, get_event_name, true, true }, - { EXPAND_AUGROUP, get_augroup_name, true, true }, + { EXPAND_EVENTS, expand_get_event_name, true, true }, + { EXPAND_AUGROUP, expand_get_augroup_name, true, true }, { EXPAND_CSCOPE, get_cscope_name, true, true }, { EXPAND_SIGN, get_sign_name, true, true }, { EXPAND_PROFILE, get_profile_name, true, true }, diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index f5824d83be..965aa8749d 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -3796,7 +3796,7 @@ static int set_rw_fname(char_u *fname, char_u *sfname) // Do filetype detection now if 'filetype' is empty. if (*curbuf->b_p_ft == NUL) { - if (au_has_group((char_u *)"filetypedetect")) { + if (augroup_exists("filetypedetect")) { (void)do_doautocmd((char_u *)"filetypedetect BufRead", false, NULL); } do_modelines(0); diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 5aa564623f..35ad57906b 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -326,16 +326,16 @@ EXTERN int want_garbage_collect INIT(= false); EXTERN int garbage_collect_at_exit INIT(= false); // Special values for current_SID. -#define SID_MODELINE -1 // when using a modeline -#define SID_CMDARG -2 // for "--cmd" argument -#define SID_CARG -3 // for "-c" argument -#define SID_ENV -4 // for sourcing environment variable -#define SID_ERROR -5 // option was reset because of an error -#define SID_NONE -6 // don't set scriptID -#define SID_WINLAYOUT -7 // changing window size -#define SID_LUA -8 // for Lua scripts/chunks -#define SID_API_CLIENT -9 // for API clients -#define SID_STR -10 // for sourcing a string with no script item +#define SID_MODELINE (-1) // when using a modeline +#define SID_CMDARG (-2) // for "--cmd" argument +#define SID_CARG (-3) // for "-c" argument +#define SID_ENV (-4) // for sourcing environment variable +#define SID_ERROR (-5) // option was reset because of an error +#define SID_NONE (-6) // don't set scriptID +#define SID_WINLAYOUT (-7) // changing window size +#define SID_LUA (-8) // for Lua scripts/chunks +#define SID_API_CLIENT (-9) // for API clients +#define SID_STR (-10) // for sourcing a string with no script item // Script CTX being sourced or was sourced to define the current function. EXTERN sctx_T current_sctx INIT(= { 0 COMMA 0 COMMA 0 }); diff --git a/src/nvim/keymap.h b/src/nvim/keymap.h index ae2ec7835e..4bf0617dfa 100644 --- a/src/nvim/keymap.h +++ b/src/nvim/keymap.h @@ -245,6 +245,7 @@ enum key_extra { KE_EVENT = 102, // event KE_LUA = 103, // lua special key KE_COMMAND = 104, // <Cmd> special key + KE_AUCMD_SPECIAL = 105, }; /* @@ -443,6 +444,8 @@ enum key_extra { #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) #define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA) +#define K_AUCMD_SPECIAL TERMCAP2KEY(KS_EXTRA, KE_AUCMD_SPECIAL) + // Bits for modifier mask // 0x01 cannot be used, because the modifier must be 0x02 or higher #define MOD_MASK_SHIFT 0x02 diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 3c1676581c..18abf04ff6 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1350,6 +1350,16 @@ Object nlua_exec(const String str, const Array args, Error *err) return nlua_pop_Object(lstate, false, err); } +bool nlua_ref_is_function(LuaRef ref) +{ + lua_State *const lstate = global_lstate; + nlua_pushref(lstate, ref); + bool is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); + lua_pop(lstate, 1); + + return is_function; +} + /// call a LuaRef as a function (or table with __call metamethod) /// /// @param ref the reference to call (not consumed) diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h index 957c61f2f9..47ac51dadb 100644 --- a/src/nvim/lua/executor.h +++ b/src/nvim/lua/executor.h @@ -24,14 +24,6 @@ typedef struct { #endif } nlua_ref_state_t; -#define set_api_error(s, err) \ - do { \ - Error *err_ = (err); \ - err_->type = kErrorTypeException; \ - err_->set = true; \ - memcpy(&err_->msg[0], s, sizeof(s)); \ - } while (0) - #define NLUA_CLEAR_REF(x) \ do { \ /* Take the address to avoid double evaluation. #1375 */ \ diff --git a/src/nvim/main.c b/src/nvim/main.c index ec64e407b2..b02ebbe030 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -9,7 +9,7 @@ #include <string.h> #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/channel.h" #include "nvim/charset.h" diff --git a/src/nvim/map.c b/src/nvim/map.c index 77ebc2a387..091d653046 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -177,6 +177,7 @@ MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(String, MsgpackRpcRequestHandler, MSGPACK_HANDLER_INITIALIZER) MAP_IMPL(HlEntry, int, DEFAULT_INITIALIZER) MAP_IMPL(String, handle_T, 0) +MAP_IMPL(String, int, DEFAULT_INITIALIZER) MAP_IMPL(ColorKey, ColorItem, COLOR_ITEM_INITIALIZER) diff --git a/src/nvim/map.h b/src/nvim/map.h index 5e56f4dd65..c9c89bf2fd 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -46,6 +46,7 @@ MAP_DECLS(handle_T, ptr_t) MAP_DECLS(String, MsgpackRpcRequestHandler) MAP_DECLS(HlEntry, int) MAP_DECLS(String, handle_T) +MAP_DECLS(String, int) MAP_DECLS(ColorKey, ColorItem) diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 6b889cf97c..b262fc6c54 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -5,7 +5,7 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/charset.h" #include "nvim/ex_docmd.h" #include "nvim/macros.h" @@ -378,7 +378,7 @@ static bool handle_focus_event(TermInput *input) bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I'; // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 3); - aucmd_schedule_focusgained(focus_gained); + autocmd_schedule_focusgained(focus_gained); return true; } return false; diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 1aadaf5c9d..31b9614c34 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -8,7 +8,7 @@ #include <string.h> #include "nvim/ascii.h" -#include "nvim/aucmd.h" +#include "nvim/autocmd.h" #include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/cursor_shape.h" diff --git a/test/functional/api/autocmd_spec.lua b/test/functional/api/autocmd_spec.lua new file mode 100644 index 0000000000..2b48ffa6f0 --- /dev/null +++ b/test/functional/api/autocmd_spec.lua @@ -0,0 +1,798 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local neq = helpers.neq +local exec_lua = helpers.exec_lua +local matches = helpers.matches +local meths = helpers.meths +local source = helpers.source + +before_each(clear) + +describe('autocmd api', function() + describe('nvim_create_autocmd', function() + it('does not allow "command" and "callback" in the same autocmd', function() + local ok, _ = pcall(meths.create_autocmd, { + event = "BufReadPost", + pattern = "*.py,*.pyi", + command = "echo 'Should Have Errored", + callback = "not allowed", + }) + + eq(false, ok) + end) + + it('doesnt leak when you use ++once', function() + eq(1, exec_lua([[ + local count = 0 + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() count = count + 1 end, + once = true + } + + vim.cmd "set filetype=txt" + vim.cmd "set filetype=python" + + return count + ]], {})) + end) + + it('allows passing buffer by key', function() + meths.set_var('called', 0) + + meths.create_autocmd { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = 0, + } + + meths.command "set filetype=txt" + eq(1, meths.get_var('called')) + + -- switch to a new buffer + meths.command "new" + meths.command "set filetype=python" + + eq(1, meths.get_var('called')) + end) + + it('does not allow passing buffer and patterns', function() + local ok = pcall(meths.create_autocmd, { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = 0, + pattern = "*.py", + }) + + eq(false, ok) + end) + + it('does not allow passing invalid buffers', function() + local ok, msg = pcall(meths.create_autocmd, { + event = "Filetype", + command = "let g:called = g:called + 1", + buffer = -1, + }) + + eq(false, ok) + matches('Invalid buffer id', msg) + end) + + it('errors on non-functions for cb', function() + eq(false, pcall(exec_lua, [[ + vim.api.nvim_create_autocmd { + event = "BufReadPost", + pattern = "*.py,*.pyi", + callback = 5, + } + ]])) + end) + + it('allow passing pattern and <buffer> in same pattern', function() + local ok = pcall(meths.create_autocmd, { + event = "BufReadPost", + pattern = "*.py,<buffer>", + command = "echo 'Should Not Error'" + }) + + eq(true, ok) + end) + + it('should handle multiple values as comma separated list', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py,*.pyi", + command = "echo 'Should Not Have Errored'" + } + + -- We should have one autocmd for *.py and one for *.pyi + eq(2, #meths.get_autocmds { event = "BufReadPost" }) + end) + + it('should handle multiple values as array', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = { "*.py", "*.pyi", }, + command = "echo 'Should Not Have Errored'" + } + + -- We should have one autocmd for *.py and one for *.pyi + eq(2, #meths.get_autocmds { event = "BufReadPost" }) + end) + + describe('desc', function() + it('can add description to one autocmd', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py", + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + eq("Can show description", meths.get_autocmds { event = "BufReadPost" }[1].desc) + end) + + it('can add description to multiple autocmd', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = {"*.py", "*.pyi"}, + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + local aus = meths.get_autocmds { event = "BufReadPost" } + eq(2, #aus) + eq("Can show description", aus[1].desc) + eq("Can show description", aus[2].desc) + end) + end) + + pending('script and verbose settings', function() + it('marks API client', function() + meths.create_autocmd { + event = "BufReadPost", + pattern = "*.py", + command = "echo 'Should Not Have Errored'", + desc = "Can show description", + } + + local aus = meths.get_autocmds { event = "BufReadPost" } + eq(1, #aus, aus) + end) + end) + end) + + describe('nvim_get_autocmds', function() + describe('events', function() + it('should return one autocmd when there is only one for an event', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + + local aus = meths.get_autocmds { event = "InsertEnter" } + eq(1, #aus) + end) + + it('should return two autocmds when there are two for an event', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local aus = meths.get_autocmds { event = "InsertEnter" } + eq(2, #aus) + end) + + it('should return the same thing if you use string or list', function() + command [[au! InsertEnter]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local string_aus = meths.get_autocmds { event = "InsertEnter" } + local array_aus = meths.get_autocmds { event = { "InsertEnter" } } + eq(string_aus, array_aus) + end) + + it('should return two autocmds when there are two for an event', function() + command [[au! InsertEnter]] + command [[au! InsertLeave]] + command [[au InsertEnter * :echo "1"]] + command [[au InsertEnter * :echo "2"]] + + local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + eq(2, #aus) + end) + + it('should return different IDs for different autocmds', function() + command [[au! InsertEnter]] + command [[au! InsertLeave]] + command [[au InsertEnter * :echo "1"]] + source [[ + call nvim_create_autocmd(#{ + \ event: "InsertLeave", + \ command: ":echo 2", + \ }) + ]] + + local aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + local first = aus[1] + eq(first.id, nil) + + -- TODO: Maybe don't have this number, just assert it's not nil + local second = aus[2] + neq(second.id, nil) + + meths.del_autocmd(second.id) + local new_aus = meths.get_autocmds { event = { "InsertEnter", "InsertLeave" } } + eq(1, #new_aus) + eq(first, new_aus[1]) + end) + end) + + describe('groups', function() + before_each(function() + command [[au! InsertEnter]] + + command [[au InsertEnter * :echo "No Group"]] + + command [[augroup GroupOne]] + command [[ au InsertEnter * :echo "GroupOne:1"]] + command [[augroup END]] + + command [[augroup GroupTwo]] + command [[ au InsertEnter * :echo "GroupTwo:2"]] + command [[ au InsertEnter * :echo "GroupTwo:3"]] + command [[augroup END]] + end) + + it('should return all groups if no group is specified', function() + local aus = meths.get_autocmds { event = "InsertEnter" } + if #aus ~= 4 then + eq({}, aus) + end + + eq(4, #aus) + end) + + it('should return only the group specified', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + group = "GroupOne", + } + + eq(1, #aus) + eq([[:echo "GroupOne:1"]], aus[1].command) + end) + + it('should return only the group specified, multiple values', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + group = "GroupTwo", + } + + eq(2, #aus) + eq([[:echo "GroupTwo:2"]], aus[1].command) + eq([[:echo "GroupTwo:3"]], aus[2].command) + end) + end) + + describe('groups: 2', function() + it('raises error for undefined augroup', function() + local success, code = unpack(meths.exec_lua([[ + return {pcall(function() + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + group = "NotDefined", + command = "echo 'hello'", + } + end)} + ]], {})) + + eq(false, success) + matches('invalid augroup: NotDefined', code) + end) + end) + + describe('patterns', function() + before_each(function() + command [[au! InsertEnter]] + + command [[au InsertEnter * :echo "No Group"]] + command [[au InsertEnter *.one :echo "GroupOne:1"]] + command [[au InsertEnter *.two :echo "GroupTwo:2"]] + command [[au InsertEnter *.two :echo "GroupTwo:3"]] + command [[au InsertEnter <buffer> :echo "Buffer"]] + end) + + it('should should return for literal match', function() + local aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "*" + } + + eq(1, #aus) + eq([[:echo "No Group"]], aus[1].command) + end) + + it('should return for multiple matches', function() + -- vim.api.nvim_get_autocmds + local aus = meths.get_autocmds { + event = "InsertEnter", + pattern = { "*.one", "*.two" }, + } + + eq(3, #aus) + eq([[:echo "GroupOne:1"]], aus[1].command) + eq([[:echo "GroupTwo:2"]], aus[2].command) + eq([[:echo "GroupTwo:3"]], aus[3].command) + end) + + it('should work for buffer autocmds', function() + local normalized_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer=1>", + } + + local raw_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer>", + } + + local zero_aus = meths.get_autocmds { + event = "InsertEnter", + pattern = "<buffer=0>", + } + + eq(normalized_aus, raw_aus) + eq(normalized_aus, zero_aus) + eq([[:echo "Buffer"]], normalized_aus[1].command) + end) + end) + end) + + describe('nvim_do_autocmd', function() + it("can trigger builtin autocmds", function() + meths.set_var("autocmd_executed", false) + + meths.create_autocmd { + event = "BufReadPost", + pattern = "*", + command = "let g:autocmd_executed = v:true", + } + + eq(false, meths.get_var("autocmd_executed")) + meths.do_autocmd { event = "BufReadPost" } + eq(true, meths.get_var("autocmd_executed")) + end) + + it("can pass the buffer", function() + meths.set_var("buffer_executed", -1) + eq(-1, meths.get_var("buffer_executed")) + + meths.create_autocmd { + event = "BufLeave", + pattern = "*", + command = 'let g:buffer_executed = +expand("<abuf>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHold", buffer = 1 } + eq(-1, meths.get_var("buffer_executed")) + + meths.do_autocmd { event = "BufLeave", buffer = 1 } + eq(1, meths.get_var("buffer_executed")) + end) + + it("can pass the filename, pattern match", function() + meths.set_var("filename_executed", 'none') + eq('none', meths.get_var("filename_executed")) + + meths.create_autocmd { + event = "BufEnter", + pattern = "*.py", + command = 'let g:filename_executed = expand("<afile>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHold", buffer = 1 } + eq('none', meths.get_var("filename_executed")) + + meths.command('edit __init__.py') + eq('__init__.py', meths.get_var("filename_executed")) + end) + + it('cannot pass buf and fname', function() + local ok = pcall(meths.do_autocmd, { pattern = "literally_cannot_error.rs", buffer = 1 }) + eq(false, ok) + end) + + it("can pass the filename, exact match", function() + meths.set_var("filename_executed", 'none') + eq('none', meths.get_var("filename_executed")) + + meths.command('edit other_file.txt') + meths.command('edit __init__.py') + eq('none', meths.get_var("filename_executed")) + + meths.create_autocmd { + event = "CursorHoldI", + pattern = "__init__.py", + command = 'let g:filename_executed = expand("<afile>")', + } + + -- Doesn't execute for other non-matching events + meths.do_autocmd { event = "CursorHoldI", buffer = 1 } + eq('none', meths.get_var("filename_executed")) + + meths.do_autocmd { event = "CursorHoldI", buffer = tonumber(meths.get_current_buf()) } + eq('__init__.py', meths.get_var("filename_executed")) + + -- Reset filename + meths.set_var("filename_executed", 'none') + + meths.do_autocmd { event = "CursorHoldI", pattern = '__init__.py' } + eq('__init__.py', meths.get_var("filename_executed")) + end) + + it("works with user autocmds", function() + meths.set_var("matched", 'none') + + meths.create_autocmd { + event = "User", + pattern = "TestCommand", + command = 'let g:matched = "matched"' + } + + meths.do_autocmd { event = "User", pattern = "OtherCommand" } + eq('none', meths.get_var('matched')) + meths.do_autocmd { event = "User", pattern = "TestCommand" } + eq('matched', meths.get_var('matched')) + end) + end) + + describe('nvim_create_augroup', function() + before_each(function() + clear() + + meths.set_var('executed', 0) + end) + + local make_counting_autocmd = function(opts) + opts = opts or {} + + local resulting = { + event = "FileType", + pattern = "*", + command = "let g:executed = g:executed + 1", + } + + resulting.group = opts.group + resulting.once = opts.once + + meths.create_autocmd(resulting) + end + + local set_ft = function(ft) + ft = ft or "txt" + source(string.format("set filetype=%s", ft)) + end + + local get_executed_count = function() + return meths.get_var('executed') + end + + it('can be added in a group', function() + local augroup = "TestGroup" + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 2) + end) + + it('works getting called multiple times', function() + make_counting_autocmd() + set_ft() + set_ft() + set_ft() + + eq(get_executed_count(), 3) + end) + + it('handles ++once', function() + make_counting_autocmd {once = true} + set_ft('txt') + set_ft('help') + set_ft('txt') + set_ft('help') + + eq(get_executed_count(), 1) + end) + + it('errors on unexpected keys', function() + local success, code = pcall(meths.create_autocmd, { + event = "FileType", + pattern = "*", + not_a_valid_key = "NotDefined", + }) + + eq(false, success) + matches('not_a_valid_key', code) + end) + + it('can execute simple callback', function() + exec_lua([[ + vim.g.executed = false + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() vim.g.executed = true end, + } + ]], {}) + + + eq(true, exec_lua([[ + vim.cmd "set filetype=txt" + return vim.g.executed + ]], {})) + end) + + it('calls multiple lua callbacks for the same autocmd execution', function() + eq(4, exec_lua([[ + local count = 0 + local counter = function() + count = count + 1 + end + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = counter, + } + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = counter, + } + + vim.cmd "set filetype=txt" + vim.cmd "set filetype=txt" + + return count + ]], {})) + end) + + it('properly releases functions with ++once', function() + exec_lua([[ + WeakTable = setmetatable({}, { __mode = "k" }) + + OnceCount = 0 + + MyVal = {} + WeakTable[MyVal] = true + + vim.api.nvim_create_autocmd { + event = "FileType", + pattern = "*", + callback = function() + OnceCount = OnceCount + 1 + MyVal = {} + end, + once = true + } + ]]) + + command [[set filetype=txt]] + eq(1, exec_lua([[return OnceCount]], {})) + + exec_lua([[collectgarbage()]], {}) + + command [[set filetype=txt]] + eq(1, exec_lua([[return OnceCount]], {})) + + eq(0, exec_lua([[ + local count = 0 + for _ in pairs(WeakTable) do + count = count + 1 + end + + return count + ]]), "Should have no keys remaining") + end) + + it('groups can be cleared', function() + local augroup = "TestGroup" + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd({ + group = augroup, + event = "FileType", + command = "let g:executed = g:executed + 1" + }) + + set_ft("txt") + set_ft("txt") + eq(2, get_executed_count(), "should only count twice") + + meths.create_augroup({ name = augroup, clear = true }) + eq({}, meths.get_autocmds { group = augroup }) + + set_ft("txt") + set_ft("txt") + eq(2, get_executed_count(), "No additional counts") + end) + + it('groups work with once', function() + local augroup = "TestGroup" + + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup, once = true } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 1) + end) + + it('autocmds can be registered multiple times.', function() + local augroup = "TestGroup" + + meths.create_augroup({ name = augroup, clear = true }) + make_counting_autocmd { group = augroup, once = false } + make_counting_autocmd { group = augroup, once = false } + make_counting_autocmd { group = augroup, once = false } + + set_ft("txt") + set_ft("python") + + eq(get_executed_count(), 3 * 2) + end) + + it('can be deleted', function() + local augroup = "WillBeDeleted" + + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd { + event = {"Filetype"}, + pattern = "*", + command = "echo 'does not matter'", + } + + -- Clears the augroup from before, which erases the autocmd + meths.create_augroup({ name = augroup, clear = true }) + + local result = #meths.get_autocmds { group = augroup } + + eq(0, result) + end) + + it('can be used for buffer local autocmds', function() + local augroup = "WillBeDeleted" + + meths.set_var("value_set", false) + + meths.create_augroup({ name = augroup, clear = true }) + meths.create_autocmd { + event = "Filetype", + pattern = "<buffer>", + command = "let g:value_set = v:true", + } + + command "new" + command "set filetype=python" + + eq(false, meths.get_var("value_set")) + end) + + it('can accept vimscript functions', function() + source [[ + let g:vimscript_executed = 0 + + function! MyVimscriptFunction() abort + let g:vimscript_executed = g:vimscript_executed + 1 + endfunction + + call nvim_create_autocmd(#{ + \ event: "Filetype", + \ pattern: ["python", "javascript"], + \ callback: "MyVimscriptFunction", + \ }) + + set filetype=txt + set filetype=python + set filetype=txt + set filetype=javascript + set filetype=txt + ]] + + eq(2, meths.get_var("vimscript_executed")) + end) + end) + + describe('augroup!', function() + it('legacy: should clear and not return any autocmds for delete groups', function() + command('augroup TEMP_A') + command(' autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + command('augroup! TEMP_A') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_A' })) + + -- For some reason, augroup! doesn't clear the autocmds themselves, which is just wild + -- but we managed to keep this behavior. + eq(1, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('legacy: remove augroups that have no autocmds', function() + command('augroup TEMP_AB') + command('augroup END') + + command('augroup! TEMP_AB') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_AB' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('legacy: multiple remove and add augroup', function() + command('augroup TEMP_ABC') + command(' au!') + command(' autocmd BufReadPost *.py echo "Hello"') + command('augroup END') + + command('augroup! TEMP_ABC') + + -- Should still have one autocmd :'( + local aus = meths.get_autocmds { event = 'BufReadPost' } + eq(1, #aus, aus) + + command('augroup TEMP_ABC') + command(' au!') + command(' autocmd BufReadPost *.py echo "Hello"') + command('augroup END') + + -- Should now have two autocmds :'( + aus = meths.get_autocmds { event = 'BufReadPost' } + eq(2, #aus, aus) + + command('augroup! TEMP_ABC') + + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABC' })) + eq(2, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('api: should clear and not return any autocmds for delete groups by id', function() + command('augroup TEMP_ABCD') + command('autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + local augroup_id = meths.create_augroup { name = "TEMP_ABCD", clear = false } + meths.del_augroup_by_id(augroup_id) + + -- For good reason, we kill all the autocmds from del_augroup, + -- so now this works as expected + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCD' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + + it('api: should clear and not return any autocmds for delete groups by name', function() + command('augroup TEMP_ABCDE') + command('autocmd! BufReadPost *.py :echo "Hello"') + command('augroup END') + + meths.del_augroup_by_name("TEMP_ABCDE") + + -- For good reason, we kill all the autocmds from del_augroup, + -- so now this works as expected + eq(false, pcall(meths.get_autocmds, { group = 'TEMP_ABCDE' })) + eq(0, #meths.get_autocmds { event = 'BufReadPost' }) + end) + end) +end) diff --git a/test/functional/autocmd/autocmd_oldtest_spec.lua b/test/functional/autocmd/autocmd_oldtest_spec.lua new file mode 100644 index 0000000000..ad3687d7b0 --- /dev/null +++ b/test/functional/autocmd/autocmd_oldtest_spec.lua @@ -0,0 +1,86 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local eq = helpers.eq +local meths = helpers.meths +local funcs = helpers.funcs + +local exec = function(str) + meths.exec(str, false) +end + +describe('oldtests', function() + before_each(clear) + + local exec_lines = function(str) + return funcs.split(funcs.execute(str), "\n") + end + + local add_an_autocmd = function() + exec [[ + augroup vimBarTest + au BufReadCmd * echo 'hello' + augroup END + ]] + + eq(3, #exec_lines('au vimBarTest')) + eq(1, #meths.get_autocmds({ group = 'vimBarTest' })) + end + + it('should recognize a bar before the {event}', function() + -- Good spacing + add_an_autocmd() + exec [[ augroup vimBarTest | au! | augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + eq({}, meths.get_autocmds({ group = 'vimBarTest' })) + + -- Sad spacing + add_an_autocmd() + exec [[ augroup vimBarTest| au!| augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + + + -- test that a bar is recognized after the {event} + add_an_autocmd() + exec [[ augroup vimBarTest| au!BufReadCmd| augroup END ]] + eq(1, #exec_lines('au vimBarTest')) + + add_an_autocmd() + exec [[ au! vimBarTest|echo 'hello' ]] + eq(1, #exec_lines('au vimBarTest')) + end) + + it('should fire on unload buf', function() + funcs.writefile({'Test file Xxx1'}, 'Xxx1') + funcs.writefile({'Test file Xxx2'}, 'Xxx2') + + local content = [[ + func UnloadAllBufs() + let i = 1 + while i <= bufnr('$') + if i != bufnr('%') && bufloaded(i) + exe i . 'bunload' + endif + let i += 1 + endwhile + endfunc + au BufUnload * call UnloadAllBufs() + au VimLeave * call writefile(['Test Finished'], 'Xout') + set nohidden + edit Xxx1 + split Xxx2 + q + ]] + + funcs.writefile(funcs.split(content, "\n"), 'Xtest') + + funcs.delete('Xout') + funcs.system(meths.get_vvar('progpath') .. ' -u NORC -i NONE -N -S Xtest') + eq(1, funcs.filereadable('Xout')) + + funcs.delete('Xxx1') + funcs.delete('Xxx2') + funcs.delete('Xtest') + funcs.delete('Xout') + end) +end) diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index 13d0103ce5..f37f04b32f 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -5,6 +5,7 @@ local assert_visible = helpers.assert_visible local assert_alive = helpers.assert_alive local dedent = helpers.dedent local eq = helpers.eq +local neq = helpers.neq local eval = helpers.eval local feed = helpers.feed local clear = helpers.clear @@ -418,4 +419,106 @@ describe('autocmd', function() :doautocmd SessionLoadPost | ]]} end) + + describe('old_tests', function() + it('vimscript: WinNew ++once', function() + source [[ + " Without ++once WinNew triggers twice + let g:did_split = 0 + augroup Testing + au! + au WinNew * let g:did_split += 1 + augroup END + split + split + call assert_equal(2, g:did_split) + call assert_true(exists('#WinNew')) + close + close + + " With ++once WinNew triggers once + let g:did_split = 0 + augroup Testing + au! + au WinNew * ++once let g:did_split += 1 + augroup END + split + split + call assert_equal(1, g:did_split) + call assert_false(exists('#WinNew')) + close + close + + call assert_fails('au WinNew * ++once ++once echo bad', 'E983:') + ]] + + meths.set_var('did_split', 0) + + source [[ + augroup Testing + au! + au WinNew * let g:did_split += 1 + augroup END + + split + split + ]] + + eq(2, meths.get_var('did_split')) + eq(1, funcs.exists('#WinNew')) + + -- Now with once + meths.set_var('did_split', 0) + + source [[ + augroup Testing + au! + au WinNew * ++once let g:did_split += 1 + augroup END + + split + split + ]] + + eq(1, meths.get_var('did_split')) + eq(0, funcs.exists('#WinNew')) + + -- call assert_fails('au WinNew * ++once ++once echo bad', 'E983:') + local ok, msg = pcall(source, [[ + au WinNew * ++once ++once echo bad + ]]) + + eq(false, ok) + eq(true, not not string.find(msg, 'E983:')) + end) + + it('should have autocmds in filetypedetect group', function() + source [[filetype on]] + neq({}, meths.get_autocmds { group = "filetypedetect" }) + end) + + it('should not access freed mem', function() + source [[ + au BufEnter,BufLeave,WinEnter,WinLeave 0 vs xxx + arg 0 + argadd + all + all + au! + bwipe xxx + ]] + end) + + it('should allow comma-separated patterns', function() + source [[ + augroup TestingPatterns + au! + autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello' + autocmd BufReadCmd *.shada,*.shada.tmp.[a-z] echo 'hello' + augroup END + ]] + + eq(4, #meths.get_autocmds { event = "BufReadCmd", group = "TestingPatterns" }) + end) + end) end) diff --git a/test/functional/autocmd/cursormoved_spec.lua b/test/functional/autocmd/cursormoved_spec.lua index 9641d4b096..85d8628d7e 100644 --- a/test/functional/autocmd/cursormoved_spec.lua +++ b/test/functional/autocmd/cursormoved_spec.lua @@ -35,6 +35,7 @@ describe('CursorMoved', function() it("is not triggered by cursor movement prior to first CursorMoved instantiation", function() source([[ let g:cursormoved = 0 + autocmd! CursorMoved autocmd CursorMoved * let g:cursormoved += 1 ]]) eq(0, eval('g:cursormoved')) diff --git a/test/functional/autocmd/show_spec.lua b/test/functional/autocmd/show_spec.lua new file mode 100644 index 0000000000..59757a7d61 --- /dev/null +++ b/test/functional/autocmd/show_spec.lua @@ -0,0 +1,35 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local dedent = helpers.dedent +local eq = helpers.eq +local funcs = helpers.funcs + +describe(":autocmd", function() + before_each(clear) + + it("should not segfault when you just do autocmd", function() + command ":autocmd" + end) + + it("should filter based on ++once", function() + command "autocmd! BufEnter" + command "autocmd BufEnter * :echo 'Hello'" + command [[augroup TestingOne]] + command [[ autocmd BufEnter * :echo "Line 1"]] + command [[ autocmd BufEnter * :echo "Line 2"]] + command [[augroup END]] + + eq(dedent([[ + + --- Autocommands --- + BufEnter + * :echo 'Hello' + TestingOne BufEnter + * :echo "Line 1" + :echo "Line 2"]]), + funcs.execute('autocmd BufEnter')) + + end) +end) diff --git a/test/functional/autocmd/termxx_spec.lua b/test/functional/autocmd/termxx_spec.lua index 1e8f981437..bc7a6d6c36 100644 --- a/test/functional/autocmd/termxx_spec.lua +++ b/test/functional/autocmd/termxx_spec.lua @@ -32,7 +32,7 @@ describe('autocmd TermClose', function() retry(nil, nil, function() eq(23, eval('g:test_termclose')) end) end) - it('kills job trapping SIGTERM', function() + pending('kills job trapping SIGTERM', function() if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') @@ -52,7 +52,7 @@ describe('autocmd TermClose', function() ok(duration <= 4000) -- Epsilon for slow CI end) - it('kills PTY job trapping SIGHUP and SIGTERM', function() + pending('kills PTY job trapping SIGHUP and SIGTERM', function() if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua index a4d78682ad..6f22f865e6 100644 --- a/test/functional/plugin/shada_spec.lua +++ b/test/functional/plugin/shada_spec.lua @@ -1,7 +1,7 @@ local helpers = require('test.functional.helpers')(after_each) local clear = helpers.clear -local eq, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf = - helpers.eq, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec, +local eq, meths, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf = + helpers.eq, helpers.meths, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec, helpers.funcs, helpers.feed, helpers.curbuf local neq = helpers.neq local read_file = helpers.read_file @@ -2162,6 +2162,10 @@ describe('plugin/shada.vim', function() wshada('\004\000\009\147\000\196\002ab\196\001a') wshada_tmp('\004\000\009\147\000\196\002ab\196\001b') + + local bufread_commands = meths.get_autocmds({ group = "ShaDaCommands", event = "BufReadCmd" }) + eq(2, #bufread_commands--[[, vim.inspect(bufread_commands) ]]) + -- Need to set nohidden so that the buffer containing 'fname' is not unloaded -- after loading 'fname_tmp', otherwise the '++opt not supported' test below -- won't work since the BufReadCmd autocmd won't be triggered. |