diff options
Diffstat (limited to 'src')
232 files changed, 19398 insertions, 4420 deletions
diff --git a/src/cjson/lua_cjson.c b/src/cjson/lua_cjson.c index cf9e82c38e..b5f97bc485 100644 --- a/src/cjson/lua_cjson.c +++ b/src/cjson/lua_cjson.c @@ -776,7 +776,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg, if (has_metatable) { - nlua_pushref(l, nlua_empty_dict_ref); + nlua_pushref(l, nlua_get_empty_dict_ref(l)); if (lua_rawequal(l, -2, -1)) { as_empty_dict = true; } else { @@ -822,7 +822,7 @@ static void json_append_data(lua_State *l, json_config_t *cfg, } break; case LUA_TUSERDATA: - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); bool is_nil = lua_rawequal(l, -2, -1); lua_pop(l, 1); if (is_nil) { @@ -1285,7 +1285,7 @@ static void json_parse_object_context(lua_State *l, json_parse_t *json) /* Handle empty objects */ if (token.type == T_OBJ_END) { - nlua_pushref(l, nlua_empty_dict_ref); \ + nlua_pushref(l, nlua_get_empty_dict_ref(l)); \ lua_setmetatable(l, -2); \ json_decode_ascend(json); return; @@ -1392,7 +1392,7 @@ static void json_process_value(lua_State *l, json_parse_t *json, if (use_luanil) { lua_pushnil(l); } else { - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); } break;; default: @@ -1549,7 +1549,15 @@ int lua_cjson_new(lua_State *l) }; /* Initialise number conversions */ - fpconv_init(); + lua_getfield(l, LUA_REGISTRYINDEX, "nvim.thread"); + bool is_thread = lua_toboolean(l, -1); + lua_pop(l, 1); + + // Since fpconv_init does not need to be called multiple times and is not + // thread safe, it should only be called in the main thread. + if (!is_thread) { + fpconv_init(); + } /* Test if array metatables are in registry */ lua_pushlightuserdata(l, json_lightudata_mask(&json_empty_array)); @@ -1582,7 +1590,7 @@ int lua_cjson_new(lua_State *l) compat_luaL_setfuncs(l, reg, 1); /* Set cjson.null */ - nlua_pushref(l, nlua_nil_ref); + nlua_pushref(l, nlua_get_nil_ref(l)); lua_setfield(l, -2, "null"); /* Set cjson.empty_array_mt */ diff --git a/src/mpack/lmpack.c b/src/mpack/lmpack.c index 126f2f3824..53d7092a0c 100644 --- a/src/mpack/lmpack.c +++ b/src/mpack/lmpack.c @@ -246,7 +246,7 @@ static mpack_uint32_t lmpack_objlen(lua_State *L, int *is_array) } end: - if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1) + if ((size_t)-1 > (mpack_uint32_t)-1 && len > (mpack_uint32_t)-1) // -V560 /* msgpack spec doesn't allow lengths > 32 bits */ len = (mpack_uint32_t)-1; assert(top == lua_gettop(L)); diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 9c4b778169..a6505feb6b 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -62,6 +62,9 @@ set(LUA_SHARED_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/shared.lua) set(LUA_INSPECT_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/inspect.lua) set(LUA_F_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/F.lua) set(LUA_META_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_meta.lua) +set(LUA_FILETYPE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/filetype.lua) +set(LUA_LOAD_PACKAGE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_load_package.lua) +set(LUA_KEYMAP_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/keymap.lua) set(CHAR_BLOB_GENERATOR ${GENERATOR_DIR}/gen_char_blob.lua) set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json) set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint") @@ -334,6 +337,9 @@ add_custom_command( ${LUA_INSPECT_MODULE_SOURCE} inspect_module ${LUA_F_MODULE_SOURCE} lua_F_module ${LUA_META_MODULE_SOURCE} lua_meta_module + ${LUA_FILETYPE_MODULE_SOURCE} lua_filetype_module + ${LUA_LOAD_PACKAGE_MODULE_SOURCE} lua_load_package_module + ${LUA_KEYMAP_MODULE_SOURCE} lua_keymap_module DEPENDS ${CHAR_BLOB_GENERATOR} ${LUA_VIM_MODULE_SOURCE} @@ -341,6 +347,9 @@ add_custom_command( ${LUA_INSPECT_MODULE_SOURCE} ${LUA_F_MODULE_SOURCE} ${LUA_META_MODULE_SOURCE} + ${LUA_FILETYPE_MODULE_SOURCE} + ${LUA_LOAD_PACKAGE_MODULE_SOURCE} + ${LUA_KEYMAP_MODULE_SOURCE} VERBATIM ) @@ -490,6 +499,9 @@ add_executable(nvim ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS} ${EXTERNAL_SOURCES} ${EXTERNAL_HEADERS}) target_link_libraries(nvim ${NVIM_EXEC_LINK_LIBRARIES}) install_helper(TARGETS nvim) +if(MSVC) + install(FILES $<TARGET_PDB_FILE:nvim> DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL) +endif() set_property(TARGET nvim APPEND PROPERTY INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS}) diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..9a8eccd22c --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,707 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +#include <stdbool.h> +#include <stdio.h> + +#include "lauxlib.h" +#include "nvim/api/autocmd.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/eval/typval.h" +#include "nvim/fileio.h" +#include "nvim/lua/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.c.generated.h" +#endif + +#define AUCMD_MAX_PATTERNS 256 + +// Copy string or array of strings into an empty array. +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ + char_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}. +/// +/// @param opts Optional Parameters: +/// - event : Name or list of name of events to match against +/// - group (string): Name of group to match against +/// - pattern: Pattern or list of patterns to match against +/// +/// @return A list of autocmds that match +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + // TODO(tjdevries): Would be cool to add nvim_get_autocmds({ id = ... }) + + Array autocmd_list = ARRAY_DICT_INIT; + char_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, + "event", + STRING_OBJ(cstr_to_string((char *)event_nr2name(event)))); + + PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); + + if (ap->buflocal_nr) { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); + PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); + } else { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); + } + + // TODO(sctx): It would be good to unify script_ctx to actually work with lua + // right now it's just super weird, and never really gives you the info that + // you would expect from this. + // + // I think we should be able to get the line number, filename, etc. from lua + // when we're executing something, and it should be easy to then save that + // info here. + // + // I think it's a big loss not getting line numbers of where options, autocmds, + // etc. are set (just getting "Sourced (lua)" or something is not that helpful. + // + // Once we do that, we can put these into the autocmd_info, but I don't think it's + // useful to do that at this time. + // + // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); + // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); + + ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); + } + } + } + +cleanup: + return autocmd_list; +} + +/// Create an autocmd. +/// +/// @param event The event or events to register this autocmd +/// Required keys: +/// event: string | ArrayOf(string) +/// +/// Examples: +/// - event: "pat1,pat2,pat3", +/// - event: "pat1" +/// - event: { "pat1" } +/// - event: { "pat1", "pat2", "pat3" } +/// +/// @param opts Optional Parameters: +/// - callback: (string|function) +/// - (string): The name of the viml function to execute when triggering this autocmd +/// - (function): The lua function to execute when triggering this autocmd +/// - NOTE: Cannot be used with {command} +/// - command: (string) command +/// - vimscript command +/// - NOTE: Cannot be used with {callback} +/// Eg. command = "let g:value_set = v:true" +/// - pattern: (string|table) +/// - pattern or patterns to match against +/// - defaults to "*". +/// - NOTE: Cannot be used with {buffer} +/// - buffer: (bufnr) +/// - create a |autocmd-buflocal| autocmd. +/// - NOTE: Cannot be used with {pattern} +/// - group: (string) The augroup name +/// - once: (boolean) - See |autocmd-once| +/// - nested: (boolean) - See |autocmd-nested| +/// - desc: (string) - Description of the autocmd +/// +/// @returns opaque value to use with nvim_del_autocmd +Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + int64_t autocmd_id = -1; + + 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 (!unpack_string_or_array(&event_array, &event, "event", err)) { + goto cleanup; + } + + if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'callback' and 'command' for the same autocmd"); + goto cleanup; + } else if (opts->callback.type != kObjectTypeNil) { + // TODO(tjdevries): It's possible we could accept callable tables, + // but we don't do that many other places, so for the moment let's + // not do that. + + Object *callback = &opts->callback; + 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; + } + + bool is_once = api_object_to_bool(opts->once, "once", false, err); + bool is_nested = api_object_to_bool(opts->nested, "nested", false, err); + + // TODO(tjdevries): 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) { + if (!check_autocmd_string_array(patterns, "pattern", err)) { + goto 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. Will not error if called and no autocmds match +/// the {id}. +/// +/// @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, { +/// clear = false +/// }) +/// </pre> +/// +/// @param name String: The name of the augroup to create +/// @param opts Parameters +/// - clear (bool): Whether to clear existing commands or not. +/// Defaults to true. +/// See |autocmd-groups| +/// +/// @returns opaque value to use with nvim_del_augroup_by_id +Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augroup) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + char *augroup_name = name.data; + bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + + int augroup = -1; + WITH_SCRIPT_CONTEXT(channel_id, { + augroup = augroup_add(augroup_name); + if (augroup == AUGROUP_ERROR) { + api_set_error(err, kErrorTypeException, "Failed to set augroup"); + return -1; + } + + if (clear_autocmds) { + FOR_ALL_AUEVENTS(event) { + aupat_del_for_event_and_group(event, augroup); + } + } + }); + + return augroup; +} + +/// Delete an augroup by {id}. {id} can only be returned when augroup was +/// created with |nvim_create_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); +} + +/// Delete an augroup by {name}. +/// +/// 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); +} + +/// Do one autocmd. +/// +/// @param event The event or events to execute +/// @param opts Optional Parameters: +/// - buffer (number) - buffer number +/// - NOTE: Cannot be used with {pattern} +/// - pattern (string|table) - optional, defaults to "*". +/// - NOTE: Cannot be used with {buffer} +/// - group (string) - autocmd group name +/// - modeline (boolean) - Default true, see |<nomodeline>| +void nvim_do_autocmd(Object event, 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 (!unpack_string_or_array(&event_array, &event, "event", err)) { + goto cleanup; + } + + 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; + } + + 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); +} + +static bool check_autocmd_string_array(Array arr, char *k, Error *err) +{ + for (size_t i = 0; i < arr.size; i++) { + if (arr.items[i].type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "All entries in '%s' must be strings", + k); + return false; + } + + // Disallow newlines in the middle of the line. + const String l = arr.items[i].data.string; + if (memchr(l.data, NL, l.size)) { + api_set_error(err, kErrorTypeValidation, + "String cannot contain newlines"); + return false; + } + } + return true; +} + +static bool unpack_string_or_array(Array *array, Object *v, char *k, Error *err) +{ + if (v->type == kObjectTypeString) { + ADD(*array, copy_object(*v)); + } else if (v->type == kObjectTypeArray) { + if (!check_autocmd_string_array(v->data.array, k, err)) { + return false; + } + *array = copy_array(v->data.array); + } else { + api_set_error(err, + kErrorTypeValidation, + "'%s' must be an array or a string.", + k); + return false; + } + + return true; +} + +#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/buffer.c b/src/nvim/api/buffer.c index ce9c4e27ad..3dd647e76f 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -287,8 +287,8 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); @@ -374,15 +374,14 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); return; } - if (start > end) { api_set_error(err, kErrorTypeValidation, @@ -554,13 +553,13 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In // check range is ordered and everything! // start_row, end_row within buffer len (except add text past the end?) - start_row = normalize_index(buf, start_row, &oob); + start_row = normalize_index(buf, start_row, false, &oob); if (oob || start_row == buf->b_ml.ml_line_count + 1) { api_set_error(err, kErrorTypeValidation, "start_row out of bounds"); return; } - end_row = normalize_index(buf, end_row, &oob); + end_row = normalize_index(buf, end_row, false, &oob); if (oob || end_row == buf->b_ml.ml_line_count + 1) { api_set_error(err, kErrorTypeValidation, "end_row out of bounds"); return; @@ -757,6 +756,108 @@ end: try_end(err); } +/// Gets a range from the buffer. +/// +/// This differs from |nvim_buf_get_lines()| in that it allows retrieving only +/// portions of a line. +/// +/// Indexing is zero-based. Column indices are end-exclusive. +/// +/// Prefer |nvim_buf_get_lines()| when retrieving entire lines. +/// +/// @param channel_id +/// @param buffer Buffer handle, or 0 for current buffer +/// @param start_row First line index +/// @param start_col Starting byte offset of first line +/// @param end_row Last line index +/// @param end_col Ending byte offset of last line (exclusive) +/// @param opts Optional parameters. Currently unused. +/// @param[out] err Error details, if any +/// @return Array of lines, or empty array for unloaded buffer. +ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, + Integer start_row, Integer start_col, + Integer end_row, Integer end_col, + Dictionary opts, Error *err) + FUNC_API_SINCE(9) +{ + Array rv = ARRAY_DICT_INIT; + + if (opts.size > 0) { + api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + return rv; + } + + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return rv; + } + + // return sentinel value if the buffer isn't loaded + if (buf->b_ml.ml_mfp == NULL) { + return rv; + } + + bool oob = false; + start_row = normalize_index(buf, start_row, false, &oob); + end_row = normalize_index(buf, end_row, false, &oob); + + if (oob) { + api_set_error(err, kErrorTypeValidation, "Index out of bounds"); + return rv; + } + + // nvim_buf_get_lines doesn't care if the start row is greater than the end + // row (it will just return an empty array), but nvim_buf_get_text does in + // order to maintain symmetry with nvim_buf_set_text. + if (start_row > end_row) { + api_set_error(err, kErrorTypeValidation, "start is higher than end"); + return rv; + } + + bool replace_nl = (channel_id != VIML_INTERNAL_CALL); + + if (start_row == end_row) { + String line = buf_get_text(buf, start_row, start_col, end_col, replace_nl, err); + if (ERROR_SET(err)) { + return rv; + } + + ADD(rv, STRING_OBJ(line)); + return rv; + } + + rv.size = (size_t)(end_row - start_row) + 1; + rv.items = xcalloc(rv.size, sizeof(Object)); + + rv.items[0] = STRING_OBJ(buf_get_text(buf, start_row, start_col, MAXCOL-1, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + + if (rv.size > 2) { + Array tmp = ARRAY_DICT_INIT; + tmp.items = &rv.items[1]; + if (!buf_collect_lines(buf, rv.size - 2, start_row + 1, replace_nl, &tmp, err)) { + goto end; + } + } + + rv.items[rv.size-1] = STRING_OBJ(buf_get_text(buf, end_row, 0, end_col, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + +end: + if (ERROR_SET(err)) { + api_free_array(rv); + rv.size = 0; + rv.items = NULL; + } + + return rv; +} + /// Returns the byte offset of a line (0-indexed). |api-indexing| /// /// Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is one byte. @@ -835,7 +936,7 @@ Integer nvim_buf_get_changedtick(Buffer buffer, Error *err) /// @param[out] err Error details, if any /// @returns Array of maparg()-like dictionaries describing mappings. /// The "buffer" key holds the associated buffer handle. -ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err) +ArrayOf(Dictionary) nvim_buf_get_keymap(uint64_t channel_id, Buffer buffer, String mode, Error *err) FUNC_API_SINCE(3) { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -844,7 +945,7 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err) return (Array)ARRAY_DICT_INIT; } - return keymap_array(mode, buf); + return keymap_array(mode, buf, channel_id == LUA_INTERNAL_CALL); } /// Sets a buffer-local |mapping| for the given mode. @@ -852,11 +953,11 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err) /// @see |nvim_set_keymap()| /// /// @param buffer Buffer handle, or 0 for current buffer -void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dict(keymap) *opts, - Error *err) +void nvim_buf_set_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, String rhs, + Dict(keymap) *opts, Error *err) FUNC_API_SINCE(6) { - modify_keymap(buffer, false, mode, lhs, rhs, opts, err); + modify_keymap(channel_id, buffer, false, mode, lhs, rhs, opts, err); } /// Unmaps a buffer-local |mapping| for the given mode. @@ -864,11 +965,12 @@ void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dic /// @see |nvim_del_keymap()| /// /// @param buffer Buffer handle, or 0 for current buffer -void nvim_buf_del_keymap(Buffer buffer, String mode, String lhs, Error *err) +void nvim_buf_del_keymap(uint64_t channel_id, Buffer buffer, String mode, + String lhs, Error *err) FUNC_API_SINCE(6) { String rhs = { .data = "", .size = 0 }; - modify_keymap(buffer, true, mode, lhs, rhs, NULL, err); + modify_keymap(channel_id, buffer, true, mode, lhs, rhs, NULL, err); } /// Gets a map of buffer-local |user-commands|. @@ -1273,6 +1375,63 @@ Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err) return res; } +/// Create a new user command |user-commands| in the given buffer. +/// +/// @param buffer Buffer handle, or 0 for current buffer. +/// @param[out] err Error details, if any. +/// @see nvim_add_user_command +void nvim_buf_add_user_command(Buffer buffer, String name, Object command, + Dict(user_command) *opts, Error *err) + FUNC_API_SINCE(9) +{ + buf_T *target_buf = find_buffer_by_handle(buffer, err); + if (ERROR_SET(err)) { + return; + } + + buf_T *save_curbuf = curbuf; + curbuf = target_buf; + add_user_command(name, command, opts, UC_BUFFER, err); + curbuf = save_curbuf; +} + +/// Delete a buffer-local user-defined command. +/// +/// Only commands created with |:command-buffer| or +/// |nvim_buf_add_user_command()| can be deleted with this function. +/// +/// @param buffer Buffer handle, or 0 for current buffer. +/// @param name Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) + FUNC_API_SINCE(9) +{ + garray_T *gap; + if (buffer == -1) { + gap = &ucmds; + } else { + buf_T *buf = find_buffer_by_handle(buffer, err); + gap = &buf->b_ucmds; + } + + for (int i = 0; i < gap->ga_len; i++) { + ucmd_T *cmd = USER_CMD_GA(gap, i); + if (!STRCMP(name.data, cmd->uc_name)) { + free_ucmd(cmd); + + gap->ga_len -= 1; + + if (i < gap->ga_len) { + memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T)); + } + + return; + } + } + + api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data); +} + Dictionary nvim__buf_stats(Buffer buffer, Error *err) { Dictionary rv = ARRAY_DICT_INIT; @@ -1329,11 +1488,11 @@ static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra) } // Normalizes 0-based indexes to buffer line numbers -static int64_t normalize_index(buf_T *buf, int64_t index, bool *oob) +static int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob) { int64_t line_count = buf->b_ml.ml_line_count; // Fix if < 0 - index = index < 0 ? line_count + index +1 : index; + index = index < 0 ? line_count + index + (int)end_exclusive : index; // Check for oob if (index > line_count) { diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index 76b699800e..d2013c597c 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -22,11 +22,11 @@ /// @deprecated /// @see nvim_exec -String nvim_command_output(String command, Error *err) +String nvim_command_output(uint64_t channel_id, String command, Error *err) FUNC_API_SINCE(1) FUNC_API_DEPRECATED_SINCE(7) { - return nvim_exec(command, true, err); + return nvim_exec(channel_id, command, true, err); } /// @deprecated Use nvim_exec_lua() instead. @@ -130,7 +130,7 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A return 0; } - uint64_t ns_id = src2ns(&src_id); + uint32_t ns_id = src2ns(&src_id); int width; VirtText virt_text = parse_virt_text(chunks, err, &width); @@ -148,11 +148,12 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A return src_id; } - Decoration *decor = xcalloc(1, sizeof(*decor)); - decor->virt_text = virt_text; - decor->virt_text_width = width; + Decoration decor = DECORATION_INIT; + decor.virt_text = virt_text; + decor.virt_text_width = width; + decor.priority = 0; - extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, decor, true, + extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true, false, kExtmarkNoUndo); return src_id; } diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 742b953c2a..3a968f07ab 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -85,12 +85,12 @@ const char *describe_ns(NS ns_id) } // Is the Namespace in use? -static bool ns_initialized(uint64_t ns) +static bool ns_initialized(uint32_t ns) { if (ns < 1) { return false; } - return ns < (uint64_t)next_namespace_id; + return ns < (uint32_t)next_namespace_id; } @@ -106,22 +106,55 @@ static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict) if (add_dict) { Dictionary dict = ARRAY_DICT_INIT; + PUT(dict, "right_gravity", BOOLEAN_OBJ(extmark.right_gravity)); + if (extmark.end_row >= 0) { PUT(dict, "end_row", INTEGER_OBJ(extmark.end_row)); PUT(dict, "end_col", INTEGER_OBJ(extmark.end_col)); + PUT(dict, "end_right_gravity", BOOLEAN_OBJ(extmark.end_right_gravity)); } - if (extmark.decor) { - Decoration *decor = extmark.decor; - if (decor->hl_id) { - String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); - PUT(dict, "hl_group", STRING_OBJ(name)); + Decoration *decor = &extmark.decor; + if (decor->hl_id) { + String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); + PUT(dict, "hl_group", STRING_OBJ(name)); + PUT(dict, "hl_eol", BOOLEAN_OBJ(decor->hl_eol)); + } + if (decor->hl_mode) { + PUT(dict, "hl_mode", STRING_OBJ(cstr_to_string(hl_mode_str[decor->hl_mode]))); + } + + if (kv_size(decor->virt_text)) { + Array chunks = ARRAY_DICT_INIT; + for (size_t i = 0; i < decor->virt_text.size; i++) { + Array chunk = ARRAY_DICT_INIT; + VirtTextChunk *vtc = &decor->virt_text.items[i]; + ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); + if (vtc->hl_id > 0) { + ADD(chunk, + STRING_OBJ(cstr_to_string((const char *)syn_id2name(vtc->hl_id)))); + } + ADD(chunks, ARRAY_OBJ(chunk)); } - if (kv_size(decor->virt_text)) { + PUT(dict, "virt_text", ARRAY_OBJ(chunks)); + PUT(dict, "virt_text_hide", BOOLEAN_OBJ(decor->virt_text_hide)); + if (decor->virt_text_pos == kVTWinCol) { + PUT(dict, "virt_text_win_col", INTEGER_OBJ(decor->col)); + } + PUT(dict, "virt_text_pos", + STRING_OBJ(cstr_to_string(virt_text_pos_str[decor->virt_text_pos]))); + } + + if (kv_size(decor->virt_lines)) { + Array all_chunks = ARRAY_DICT_INIT; + bool virt_lines_leftcol = false; + for (size_t i = 0; i < decor->virt_lines.size; i++) { Array chunks = ARRAY_DICT_INIT; - for (size_t i = 0; i < decor->virt_text.size; i++) { + VirtText *vt = &decor->virt_lines.items[i].line; + virt_lines_leftcol = decor->virt_lines.items[i].left_col; + for (size_t j = 0; j < vt->size; j++) { Array chunk = ARRAY_DICT_INIT; - VirtTextChunk *vtc = &decor->virt_text.items[i]; + VirtTextChunk *vtc = &vt->items[j]; ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); if (vtc->hl_id > 0) { ADD(chunk, @@ -129,9 +162,14 @@ static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict) } ADD(chunks, ARRAY_OBJ(chunk)); } - PUT(dict, "virt_text", ARRAY_OBJ(chunks)); + ADD(all_chunks, ARRAY_OBJ(chunks)); } + PUT(dict, "virt_lines", ARRAY_OBJ(all_chunks)); + PUT(dict, "virt_lines_above", BOOLEAN_OBJ(decor->virt_lines_above)); + PUT(dict, "virt_lines_leftcol", BOOLEAN_OBJ(virt_lines_leftcol)); + } + if (decor->hl_id || kv_size(decor->virt_text)) { PUT(dict, "priority", INTEGER_OBJ(decor->priority)); } @@ -166,7 +204,7 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, return rv; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return rv; } @@ -191,7 +229,7 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, } - ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); + ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); if (extmark.row < 0) { return rv; } @@ -252,7 +290,7 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e return rv; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return rv; } @@ -310,7 +348,7 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e } - ExtmarkInfoArray marks = extmark_get(buf, (uint64_t)ns_id, l_row, l_col, + ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, u_row, u_col, (int64_t)limit, reverse); for (size_t i = 0; i < kv_size(marks); i++) { @@ -404,6 +442,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// for left). Defaults to false. /// - priority: a priority value for the highlight group. For /// example treesitter highlighting uses a value of 100. +/// - strict: boolean that indicates extmark should not be placed +/// if the line or column value is past the end of the +/// buffer or end of the line respectively. Defaults to true. +/// /// @param[out] err Error details, if any /// @return Id of the created/updated extmark Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col, @@ -417,14 +459,14 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); goto error; } - uint64_t id = 0; + uint32_t id = 0; if (opts->id.type == kObjectTypeInteger && opts->id.data.integer > 0) { - id = (uint64_t)opts->id.data.integer; + id = (uint32_t)opts->id.data.integer; } else if (HAS_KEY(opts->id)) { api_set_error(err, kErrorTypeValidation, "id is not a positive integer"); goto error; @@ -441,9 +483,18 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer opts->end_row = opts->end_line; } +#define OPTION_TO_BOOL(target, name, val) \ + target = api_object_to_bool(opts->name, #name, val, err); \ + if (ERROR_SET(err)) { \ + goto error; \ + } + + bool strict = true; + OPTION_TO_BOOL(strict, strict, true); + if (opts->end_row.type == kObjectTypeInteger) { Integer val = opts->end_row.data.integer; - if (val < 0 || val > buf->b_ml.ml_line_count) { + if (val < 0 || (val > buf->b_ml.ml_line_count && strict)) { api_set_error(err, kErrorTypeValidation, "end_row value outside range"); goto error; } else { @@ -512,12 +563,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } -#define OPTION_TO_BOOL(target, name, val) \ - target = api_object_to_bool(opts->name, #name, val, err); \ - if (ERROR_SET(err)) { \ - goto error; \ - } - OPTION_TO_BOOL(decor.virt_text_hide, virt_text_hide, false); OPTION_TO_BOOL(decor.hl_eol, hl_eol, false); @@ -596,16 +641,30 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer bool ephemeral = false; OPTION_TO_BOOL(ephemeral, ephemeral, false); - if (line < 0 || line > buf->b_ml.ml_line_count) { + if (line < 0) { api_set_error(err, kErrorTypeValidation, "line value outside range"); goto error; + } else if (line > buf->b_ml.ml_line_count) { + if (strict) { + api_set_error(err, kErrorTypeValidation, "line value outside range"); + goto error; + } else { + line = buf->b_ml.ml_line_count; + } } else if (line < buf->b_ml.ml_line_count) { len = ephemeral ? MAXCOL : STRLEN(ml_get_buf(buf, (linenr_T)line+1, false)); } if (col == -1) { col = (Integer)len; - } else if (col < -1 || col > (Integer)len) { + } else if (col > (Integer)len) { + if (strict) { + api_set_error(err, kErrorTypeValidation, "col value outside range"); + goto error; + } else { + col = (Integer)len; + } + } else if (col < -1) { api_set_error(err, kErrorTypeValidation, "col value outside range"); goto error; } @@ -621,27 +680,17 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer line2 = (int)line; } if (col2 > (Integer)len) { - api_set_error(err, kErrorTypeValidation, "end_col value outside range"); - goto error; + if (strict) { + api_set_error(err, kErrorTypeValidation, "end_col value outside range"); + goto error; + } else { + col2 = (int)len; + } } } else if (line2 >= 0) { col2 = 0; } - Decoration *d = NULL; - - if (ephemeral) { - d = &decor; - } else if (kv_size(decor.virt_text) || kv_size(decor.virt_lines) - || decor.priority != DECOR_PRIORITY_BASE - || decor.hl_eol) { - // TODO(bfredl): this is a bit sketchy. eventually we should - // have predefined decorations for both marks/ephemerals - d = xcalloc(1, sizeof(*d)); - *d = decor; - } else if (decor.hl_id) { - d = decor_hl(decor.hl_id); - } // TODO(bfredl): synergize these two branches even more if (ephemeral && decor_state.buf == buf) { @@ -652,12 +701,8 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } - extmark_set(buf, (uint64_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, - d, right_gravity, end_right_gravity, kExtmarkNoUndo); - - if (kv_size(decor.virt_lines)) { - redraw_buf_line_later(buf, MIN(buf->b_ml.ml_line_count, line+1+(decor.virt_lines_above?0:1))); - } + extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, + &decor, right_gravity, end_right_gravity, kExtmarkNoUndo); } return (Integer)id; @@ -682,23 +727,23 @@ Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *er if (!buf) { return false; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return false; } - return extmark_del(buf, (uint64_t)ns_id, (uint64_t)id); + return extmark_del(buf, (uint32_t)ns_id, (uint32_t)id); } -uint64_t src2ns(Integer *src_id) +uint32_t src2ns(Integer *src_id) { if (*src_id == 0) { *src_id = nvim_create_namespace((String)STRING_INIT); } if (*src_id < 0) { - return UINT64_MAX; + return (((uint32_t)1) << 31) - 1; } else { - return (uint64_t)(*src_id); + return (uint32_t)(*src_id); } } @@ -753,7 +798,7 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In col_end = MAXCOL; } - uint64_t ns = src2ns(&ns_id); + uint32_t ns = src2ns(&ns_id); if (!(line < buf->b_ml.ml_line_count)) { // safety check, we can't add marks outside the range @@ -773,10 +818,13 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In end_line++; } + Decoration decor = DECORATION_INIT; + decor.hl_id = hl_id; + extmark_set(buf, ns, NULL, (int)line, (colnr_T)col_start, end_line, (colnr_T)col_end, - decor_hl(hl_id), true, false, kExtmarkNoUndo); + &decor, true, false, kExtmarkNoUndo); return ns_id; } @@ -808,7 +856,7 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, if (line_end < 0 || line_end > MAXLNUM) { line_end = MAXLNUM; } - extmark_clear(buf, (ns_id < 0 ? 0 : (uint64_t)ns_id), + extmark_clear(buf, (ns_id < 0 ? 0 : (uint32_t)ns_id), (int)line_start, 0, (int)line_end-1, MAXCOL); } diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index f3e7f2f1dc..b05816f8ac 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -21,6 +21,7 @@ return { "virt_lines"; "virt_lines_above"; "virt_lines_leftcol"; + "strict"; }; keymap = { "noremap"; @@ -29,10 +30,25 @@ return { "script"; "expr"; "unique"; + "callback"; + "desc"; }; get_commands = { "builtin"; }; + user_command = { + "addr"; + "bang"; + "bar"; + "complete"; + "count"; + "desc"; + "force"; + "keepscript"; + "nargs"; + "range"; + "register"; + }; float_config = { "row"; "col"; @@ -62,5 +78,62 @@ return { option = { "scope"; }; + highlight = { + "bold"; + "standout"; + "strikethrough"; + "underline"; + "undercurl"; + "italic"; + "reverse"; + "nocombine"; + "default"; + "global"; + "cterm"; + "foreground"; "fg"; + "background"; "bg"; + "ctermfg"; + "ctermbg"; + "special"; "sp"; + "link"; + "fallback"; + "blend"; + "temp"; + }; + highlight_cterm = { + "bold"; + "standout"; + "strikethrough"; + "underline"; + "undercurl"; + "italic"; + "reverse"; + "nocombine"; + }; + -- Autocmds + create_autocmd = { + "buffer"; + "callback"; + "command"; + "desc"; + "group"; + "once"; + "nested"; + "pattern"; + }; + do_autocmd = { + "buffer"; + "group"; + "modeline"; + "pattern"; + }; + get_autocmds = { + "event"; + "group"; + "pattern"; + }; + create_augroup = { + "clear"; + }; } diff --git a/src/nvim/api/private/converter.c b/src/nvim/api/private/converter.c index 36da6c13a9..49e3cf7df7 100644 --- a/src/nvim/api/private/converter.c +++ b/src/nvim/api/private/converter.c @@ -10,6 +10,9 @@ #include "nvim/api/private/helpers.h" #include "nvim/assert.h" #include "nvim/eval/typval.h" +#include "nvim/eval/userfunc.h" +#include "nvim/lua/converter.h" +#include "nvim/lua/executor.h" /// Helper structure for vim_to_object typedef struct { @@ -54,7 +57,7 @@ typedef struct { const size_t len_ = (size_t)(len); \ const blob_T *const blob_ = (blob); \ kvi_push(edata->stack, STRING_OBJ(((String) { \ - .data = len_ != 0 ? xmemdup(blob_->bv_ga.ga_data, len_) : NULL, \ + .data = len_ != 0 ? xmemdupz(blob_->bv_ga.ga_data, len_) : xstrdup(""), \ .size = len_ \ }))); \ } while (0) @@ -228,6 +231,14 @@ static inline void typval_encode_dict_end(EncodedData *const edata) /// @return The converted value Object vim_to_object(typval_T *obj) { + if (obj->v_type == VAR_FUNC) { + ufunc_T *fp = find_func(obj->vval.v_string); + assert(fp != NULL); + if (fp->uf_cb == nlua_CFunction_func_call) { + LuaRef ref = api_new_luaref(((LuaCFunctionState *)fp->uf_cb_state)->lua_callable.func_ref); + return LUAREF_OBJ(ref); + } + } EncodedData edata; kvi_init(edata.stack); const int evo_ret = encode_vim_to_object(&edata, obj, @@ -340,6 +351,16 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) tv->vval.v_dict = dict; break; } + + case kObjectTypeLuaRef: { + LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); + state->lua_callable.func_ref = api_new_luaref(obj.data.luaref); + char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, state); + tv->v_type = VAR_FUNC; + tv->vval.v_string = vim_strsave(name); + break; + } + default: abort(); } 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 9b407eab8b..35e8589255 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -139,10 +139,10 @@ bool try_end(Error *err) got_int = false; } else if (msg_list != NULL && *msg_list != NULL) { int should_free; - char *msg = (char *)get_exception_string(*msg_list, - ET_ERROR, - NULL, - &should_free); + char *msg = get_exception_string(*msg_list, + ET_ERROR, + NULL, + &should_free); api_set_error(err, kErrorTypeException, "%s", msg); free_global_msglist(); @@ -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; + WITH_SCRIPT_CONTEXT(channel_id, { + const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) + ? 0 : (type == SREQ_GLOBAL) + ? OPT_GLOBAL : OPT_LOCAL; - 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; + set_option_value_for(name.data, numval, stringval, + opt_flags, type, to, err); + }); } @@ -591,9 +586,10 @@ Array string_to_array(const String input, bool crlf) /// @param buffer Buffer handle for a specific buffer, or 0 for the current /// buffer, or -1 to signify global behavior ("all buffers") /// @param is_unmap When true, removes the mapping that matches {lhs}. -void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String rhs, - Dict(keymap) *opts, Error *err) +void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mode, String lhs, + String rhs, Dict(keymap) *opts, Error *err) { + LuaRef lua_funcref = LUA_NOREF; bool global = (buffer == -1); if (global) { buffer = 0; @@ -604,6 +600,12 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String return; } + const sctx_T save_current_sctx = api_set_sctx(channel_id); + + if (opts != NULL && opts->callback.type == kObjectTypeLuaRef) { + lua_funcref = opts->callback.data.luaref; + opts->callback.data.luaref = LUA_NOREF; + } MapArguments parsed_args = MAP_ARGUMENTS_INIT; if (opts) { #define KEY_TO_BOOL(name) \ @@ -623,9 +625,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String parsed_args.buffer = !global; set_maparg_lhs_rhs((char_u *)lhs.data, lhs.size, - (char_u *)rhs.data, rhs.size, + (char_u *)rhs.data, rhs.size, lua_funcref, CPO_TO_CPO_FLAGS, &parsed_args); - + if (opts != NULL && opts->desc.type == kObjectTypeString) { + parsed_args.desc = xstrdup(opts->desc.data.string.data); + } else { + parsed_args.desc = NULL; + } if (parsed_args.lhs_len > MAXMAPLEN) { api_set_error(err, kErrorTypeValidation, "LHS exceeds maximum map length: %s", lhs.data); goto fail_and_free; @@ -658,7 +664,8 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String bool is_noremap = parsed_args.noremap; assert(!(is_unmap && is_noremap)); - if (!is_unmap && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) { + if (!is_unmap && lua_funcref == LUA_NOREF + && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) { if (rhs.size == 0) { // assume that the user wants RHS to be a <Nop> parsed_args.rhs_is_noop = true; } else { @@ -668,9 +675,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String api_set_error(err, kErrorTypeValidation, "Parsing of nonempty RHS failed: %s", rhs.data); goto fail_and_free; } - } else if (is_unmap && parsed_args.rhs_len) { - api_set_error(err, kErrorTypeValidation, - "Gave nonempty RHS in unmap command: %s", parsed_args.rhs); + } else if (is_unmap && (parsed_args.rhs_len || parsed_args.rhs_lua != LUA_NOREF)) { + if (parsed_args.rhs_len) { + api_set_error(err, kErrorTypeValidation, + "Gave nonempty RHS in unmap command: %s", parsed_args.rhs); + } else { + api_set_error(err, kErrorTypeValidation, "Gave nonempty RHS for unmap"); + } goto fail_and_free; } @@ -700,10 +711,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String goto fail_and_free; } // switch + parsed_args.rhs_lua = LUA_NOREF; // don't clear ref on success fail_and_free: + current_sctx = save_current_sctx; + NLUA_CLEAR_REF(parsed_args.rhs_lua); xfree(parsed_args.rhs); xfree(parsed_args.orig_rhs); - return; + XFREE_CLEAR(parsed_args.desc); } /// Collects `n` buffer lines into array `l`, optionally replacing newlines @@ -742,6 +756,52 @@ bool buf_collect_lines(buf_T *buf, size_t n, int64_t start, bool replace_nl, Arr return true; } +/// Returns a substring of a buffer line +/// +/// @param buf Buffer handle +/// @param lnum Line number (1-based) +/// @param start_col Starting byte offset into line (0-based) +/// @param end_col Ending byte offset into line (0-based, exclusive) +/// @param replace_nl Replace newlines ('\n') with null ('\0') +/// @param err Error object +/// @return The text between start_col and end_col on line lnum of buffer buf +String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col, bool replace_nl, + Error *err) +{ + String rv = STRING_INIT; + + if (lnum >= MAXLNUM) { + api_set_error(err, kErrorTypeValidation, "Line index is too high"); + return rv; + } + + const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false); + size_t line_length = strlen(bufstr); + + start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col; + end_col = end_col < 0 ? (int64_t)line_length + end_col + 1 : end_col; + + if (start_col >= MAXCOL || end_col >= MAXCOL) { + api_set_error(err, kErrorTypeValidation, "Column index is too high"); + return rv; + } + + if (start_col > end_col) { + api_set_error(err, kErrorTypeValidation, "start_col must be less than end_col"); + return rv; + } + + if ((size_t)start_col >= line_length) { + return rv; + } + + rv = cstrn_to_string(&bufstr[start_col], (size_t)(end_col - start_col)); + if (replace_nl) { + strchrsub(rv.data, '\n', '\0'); + } + + return rv; +} void api_free_string(String value) { @@ -961,6 +1021,10 @@ Object copy_object(Object obj) case kObjectTypeDictionary: return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary)); + + case kObjectTypeLuaRef: + return LUAREF_OBJ(api_new_luaref(obj.data.luaref)); + default: abort(); } @@ -969,18 +1033,16 @@ Object copy_object(Object obj) static void set_option_value_for(char *key, int numval, char *stringval, int opt_flags, int opt_type, void *from, Error *err) { - win_T *save_curwin = NULL; - tabpage_T *save_curtab = NULL; + switchwin_T switchwin; aco_save_T aco; try_start(); switch (opt_type) { case SREQ_WIN: - if (switch_win_noblock(&save_curwin, &save_curtab, (win_T *)from, - win_find_tabpage((win_T *)from), true) + if (switch_win_noblock(&switchwin, (win_T *)from, win_find_tabpage((win_T *)from), true) == FAIL) { - restore_win_noblock(save_curwin, save_curtab, true); + restore_win_noblock(&switchwin, true); if (try_end(err)) { return; } @@ -990,7 +1052,7 @@ static void set_option_value_for(char *key, int numval, char *stringval, int opt return; } set_option_value_err(key, numval, stringval, opt_flags, err); - restore_win_noblock(save_curwin, save_curtab, true); + restore_win_noblock(&switchwin, true); break; case SREQ_BUF: aucmd_prepbuf(&aco, (buf_T *)from); @@ -1048,8 +1110,9 @@ void api_set_error(Error *err, ErrorType errType, const char *format, ...) /// /// @param mode The abbreviation for the mode /// @param buf The buffer to get the mapping array. NULL for global +/// @param from_lua Whether it is called from internal lua api. /// @returns Array of maparg()-like dictionaries describing mappings -ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf) +ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf, bool from_lua) { Array mappings = ARRAY_DICT_INIT; dict_T *const dict = tv_dict_alloc(); @@ -1069,8 +1132,19 @@ ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf) // Check for correct mode if (int_mode & current_maphash->m_mode) { mapblock_fill_dict(dict, current_maphash, buffer_value, false); - ADD(mappings, vim_to_object((typval_T[]) { { .v_type = VAR_DICT, .vval.v_dict = dict } })); - + Object api_dict = vim_to_object((typval_T[]) { { .v_type = VAR_DICT, + .vval.v_dict = dict } }); + if (from_lua) { + Dictionary d = api_dict.data.dictionary; + for (size_t j = 0; j < d.size; j++) { + if (strequal("callback", d.items[j].key.data)) { + d.items[j].value.type = kObjectTypeLuaRef; + d.items[j].value.data.luaref = api_new_luaref((LuaRef)d.items[j].value.data.integer); + break; + } + } + } + ADD(mappings, api_dict); tv_dict_clear(dict); } } @@ -1108,7 +1182,7 @@ bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int return false; } - ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); + ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); if (extmark.row >= 0) { *row = extmark.row; *col = extmark.col; @@ -1342,3 +1416,230 @@ const char *get_default_stl_hl(win_T *wp) return "StatusLineNC"; } } + +void add_user_command(String name, Object command, Dict(user_command) *opts, int flags, Error *err) +{ + uint32_t argt = 0; + long def = -1; + cmd_addr_T addr_type_arg = ADDR_NONE; + int compl = EXPAND_NOTHING; + char *compl_arg = NULL; + char *rep = NULL; + LuaRef luaref = LUA_NOREF; + LuaRef compl_luaref = LUA_NOREF; + + if (!uc_validate_name(name.data)) { + api_set_error(err, kErrorTypeValidation, "Invalid command name"); + goto err; + } + + if (mb_islower(name.data[0])) { + api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter"); + goto err; + } + + if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) { + api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive"); + goto err; + } + + if (opts->nargs.type == kObjectTypeInteger) { + switch (opts->nargs.data.integer) { + case 0: + // Default value, nothing to do + break; + case 1: + argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG; + break; + default: + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + } else if (opts->nargs.type == kObjectTypeString) { + if (opts->nargs.data.string.size > 1) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + + switch (opts->nargs.data.string.data[0]) { + case '*': + argt |= EX_EXTRA; + break; + case '?': + argt |= EX_EXTRA | EX_NOSPC; + break; + case '+': + argt |= EX_EXTRA | EX_NEEDARG; + break; + default: + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + } else if (HAS_KEY(opts->nargs)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + + if (HAS_KEY(opts->complete) && !argt) { + api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'"); + goto err; + } + + if (opts->range.type == kObjectTypeBoolean) { + if (opts->range.data.boolean) { + argt |= EX_RANGE; + addr_type_arg = ADDR_LINES; + } + } else if (opts->range.type == kObjectTypeString) { + if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) { + argt |= EX_RANGE | EX_DFLALL; + addr_type_arg = ADDR_LINES; + } else { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); + goto err; + } + } else if (opts->range.type == kObjectTypeInteger) { + argt |= EX_RANGE | EX_ZEROR; + def = opts->range.data.integer; + addr_type_arg = ADDR_LINES; + } else if (HAS_KEY(opts->range)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); + goto err; + } + + if (opts->count.type == kObjectTypeBoolean) { + if (opts->count.data.boolean) { + argt |= EX_COUNT | EX_ZEROR | EX_RANGE; + addr_type_arg = ADDR_OTHER; + def = 0; + } + } else if (opts->count.type == kObjectTypeInteger) { + argt |= EX_COUNT | EX_ZEROR | EX_RANGE; + addr_type_arg = ADDR_OTHER; + def = opts->count.data.integer; + } else if (HAS_KEY(opts->count)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'"); + goto err; + } + + if (opts->addr.type == kObjectTypeString) { + if (parse_addr_type_arg((char_u *)opts->addr.data.string.data, (int)opts->addr.data.string.size, + &addr_type_arg) != OK) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); + goto err; + } + + if (addr_type_arg != ADDR_LINES) { + argt |= EX_ZEROR; + } + } else if (HAS_KEY(opts->addr)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); + goto err; + } + + if (api_object_to_bool(opts->bang, "bang", false, err)) { + argt |= EX_BANG; + } else if (ERROR_SET(err)) { + goto err; + } + + if (api_object_to_bool(opts->bar, "bar", false, err)) { + argt |= EX_TRLBAR; + } else if (ERROR_SET(err)) { + goto err; + } + + + if (api_object_to_bool(opts->register_, "register", false, err)) { + argt |= EX_REGSTR; + } else if (ERROR_SET(err)) { + goto err; + } + + if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) { + argt |= EX_KEEPSCRIPT; + } else if (ERROR_SET(err)) { + goto err; + } + + bool force = api_object_to_bool(opts->force, "force", true, err); + if (ERROR_SET(err)) { + goto err; + } + + if (opts->complete.type == kObjectTypeLuaRef) { + compl = EXPAND_USER_LUA; + compl_luaref = api_new_luaref(opts->complete.data.luaref); + } else if (opts->complete.type == kObjectTypeString) { + if (parse_compl_arg((char_u *)opts->complete.data.string.data, + (int)opts->complete.data.string.size, &compl, &argt, + (char_u **)&compl_arg) != OK) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); + goto err; + } + } else if (HAS_KEY(opts->complete)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); + goto err; + } + + switch (command.type) { + case kObjectTypeLuaRef: + luaref = api_new_luaref(command.data.luaref); + if (opts->desc.type == kObjectTypeString) { + rep = opts->desc.data.string.data; + } else { + snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref); + rep = (char *)IObuff; + } + break; + case kObjectTypeString: + rep = command.data.string.data; + break; + default: + api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function"); + goto err; + } + + if (uc_add_command((char_u *)name.data, name.size, (char_u *)rep, argt, def, flags, + compl, (char_u *)compl_arg, compl_luaref, addr_type_arg, luaref, + force) != OK) { + api_set_error(err, kErrorTypeException, "Failed to create user command"); + goto err; + } + + return; + +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; + } +} + +/// Sets sctx for API calls. +/// +/// @param channel_id api clients id. Used to determine if it's a internal +/// call or a rpc call. +/// @return returns previous value of current_sctx. To be used +/// to be used for restoring sctx to previous state. +sctx_T api_set_sctx(uint64_t channel_id) +{ + sctx_T old_current_sctx = current_sctx; + if (channel_id != VIML_INTERNAL_CALL) { + current_sctx.sc_sid = + channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; + current_sctx.sc_lnum = 0; + } + return old_current_sctx; +} diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 6d0aec9c90..bc7c2e6a60 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -138,10 +138,29 @@ 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) \ + do { \ + 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; \ + } while (0); + #endif // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index f81cdf9deb..9d0b096a36 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -19,6 +19,7 @@ #include "nvim/ascii.h" #include "nvim/buffer.h" #include "nvim/buffer_defs.h" +#include "nvim/charset.h" #include "nvim/context.h" #include "nvim/decoration.h" #include "nvim/edit.h" @@ -30,7 +31,9 @@ #include "nvim/file_search.h" #include "nvim/fileio.h" #include "nvim/getchar.h" +#include "nvim/globals.h" #include "nvim/highlight.h" +#include "nvim/highlight_defs.h" #include "nvim/lua/executor.h" #include "nvim/mark.h" #include "nvim/memline.h" @@ -121,7 +124,13 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) /// Set a highlight group. /// -/// @param ns_id number of namespace for this highlight +/// Note: unlike the `:highlight` command which can update a highlight group, +/// this function completely replaces the definition. For example: +/// `nvim_set_hl(0, 'Visual', {})` will clear the highlight group 'Visual'. +/// +/// @param ns_id number of namespace for this highlight. Use value 0 +/// to set a highlight group in the global (`:highlight`) +/// namespace. /// @param name highlight group name, like ErrorMsg /// @param val highlight definition map, like |nvim_get_hl_by_name|. /// in addition the following keys are also recognized: @@ -135,9 +144,8 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) /// same as attributes of gui color /// @param[out] err Error details, if any /// -/// TODO: ns_id = 0, should modify :highlight namespace -/// TODO val should take update vs reset flag -void nvim_set_hl(Integer ns_id, String name, Dictionary val, Error *err) +// TODO(bfredl): val should take update vs reset flag +void nvim_set_hl(Integer ns_id, String name, Dict(highlight) *val, Error *err) FUNC_API_SINCE(7) { int hl_id = syn_check_group(name.data, (int)name.size); @@ -145,7 +153,7 @@ void nvim_set_hl(Integer ns_id, String name, Dictionary val, Error *err) HlAttrs attrs = dict2hlattrs(val, true, &link_id, err); if (!ERROR_SET(err)) { - ns_hl_def((NS)ns_id, hl_id, attrs, link_id); + ns_hl_def((NS)ns_id, hl_id, attrs, link_id, val); } } @@ -169,7 +177,7 @@ void nvim__set_hl_ns(Integer ns_id, Error *err) // event path for redraws caused by "fast" events. This could tie in with // better throttling of async events causing redraws, such as non-batched // nvim_buf_set_extmark calls from async contexts. - if (!provider_active && !ns_hl_changed) { + if (!provider_active && !ns_hl_changed && must_redraw < NOT_VALID) { multiqueue_put(main_loop.events, on_redraw_event, 0); } ns_hl_changed = true; @@ -187,21 +195,23 @@ static void on_redraw_event(void **argv) /// On execution error: does not fail, but updates v:errmsg. /// /// To input sequences like <C-o> use |nvim_replace_termcodes()| (typically -/// with escape_csi=true) to replace |keycodes|, then pass the result to +/// with escape_ks=false) to replace |keycodes|, then pass the result to /// nvim_feedkeys(). /// /// Example: /// <pre> /// :let key = nvim_replace_termcodes("<C-o>", v:true, v:false, v:true) -/// :call nvim_feedkeys(key, 'n', v:true) +/// :call nvim_feedkeys(key, 'n', v:false) /// </pre> /// /// @param keys to be typed /// @param mode behavior flags, see |feedkeys()| -/// @param escape_csi If true, escape K_SPECIAL/CSI bytes in `keys` +/// @param escape_ks If true, escape K_SPECIAL bytes in `keys` +/// This should be false if you already used +/// |nvim_replace_termcodes()|, and true otherwise. /// @see feedkeys() -/// @see vim_strsave_escape_csi -void nvim_feedkeys(String keys, String mode, Boolean escape_csi) +/// @see vim_strsave_escape_ks +void nvim_feedkeys(String keys, String mode, Boolean escape_ks) FUNC_API_SINCE(1) { bool remap = true; @@ -232,10 +242,10 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi) } char *keys_esc; - if (escape_csi) { - // Need to escape K_SPECIAL and CSI before putting the string in the + if (escape_ks) { + // Need to escape K_SPECIAL before putting the string in the // typeahead buffer. - keys_esc = (char *)vim_strsave_escape_csi((char_u *)keys.data); + keys_esc = (char *)vim_strsave_escape_ks((char_u *)keys.data); } else { keys_esc = keys.data; } @@ -245,7 +255,7 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi) typebuf_was_filled = true; } - if (escape_csi) { + if (escape_ks) { xfree(keys_esc); } @@ -383,7 +393,7 @@ error: /// @param str String to be converted. /// @param from_part Legacy Vim parameter. Usually true. /// @param do_lt Also translate <lt>. Ignored if `special` is false. -/// @param special Replace |keycodes|, e.g. <CR> becomes a "\n" char. +/// @param special Replace |keycodes|, e.g. <CR> becomes a "\r" char. /// @see replace_termcodes /// @see cpoptions String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt, Boolean special) @@ -491,8 +501,12 @@ ArrayOf(String) nvim_get_runtime_file(String name, Boolean all, Error *err) int flags = DIP_DIRFILE | (all ? DIP_ALL : 0); - do_in_runtimepath((char_u *)(name.size ? name.data : ""), - flags, find_runtime_cb, &rv); + TRY_WRAP({ + try_start(); + do_in_runtimepath((char_u *)(name.size ? name.data : ""), + flags, find_runtime_cb, &rv); + try_end(err); + }); return rv; } @@ -540,20 +554,19 @@ void nvim_set_current_dir(String dir, Error *err) return; } - char string[MAXPATHL]; + char_u string[MAXPATHL]; memcpy(string, dir.data, dir.size); string[dir.size] = NUL; try_start(); - if (vim_chdir((char_u *)string)) { + if (!changedir_func(string, kCdScopeGlobal)) { if (!try_end(err)) { api_set_error(err, kErrorTypeException, "Failed to change directory"); } return; } - post_chdir(kCdScopeGlobal, true); try_end(err); } @@ -596,7 +609,19 @@ void nvim_del_current_line(Error *err) Object nvim_get_var(String name, Error *err) FUNC_API_SINCE(1) { - return dict_get_value(&globvardict, name, err); + dictitem_T *di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); + if (di == NULL) { // try to autoload script + if (!script_autoload(name.data, name.size, false) || aborting()) { + api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); + return (Object)OBJECT_INIT; + } + di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); + } + if (di == NULL) { + api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); + return (Object)OBJECT_INIT; + } + return vim_to_object(&di->di_tv); } /// Sets a global (g:) variable. @@ -662,7 +687,7 @@ Object nvim_get_option(String name, Error *err) /// /// @param name Option name /// @param opts Optional parameters -/// - scope: One of 'global' or 'local'. Analagous to +/// - scope: One of 'global' or 'local'. Analogous to /// |:setglobal| and |:setlocal|, respectively. /// @param[out] err Error details, if any /// @return Option value @@ -724,7 +749,7 @@ end: /// @param name Option name /// @param value New option value /// @param opts Optional parameters -/// - scope: One of 'global' or 'local'. Analagous to +/// - scope: One of 'global' or 'local'. Analogous to /// |:setglobal| and |:setlocal|, respectively. /// @param[out] err Error details, if any void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error *err) @@ -1163,7 +1188,7 @@ static void term_close(void *data) /// Send data to channel `id`. For a job, it writes it to the /// stdin of the process. For the stdio channel |channel-stdio|, /// it writes to Nvim's stdout. For an internal terminal instance -/// (|nvim_open_term()|) it writes directly to terimal output. +/// (|nvim_open_term()|) it writes directly to terminal output. /// See |channel-bytes| for more information. /// /// This function writes raw data, not RPC messages. If the channel @@ -1538,10 +1563,10 @@ Dictionary nvim_get_mode(void) /// @param mode Mode short-name ("n", "i", "v", ...) /// @returns Array of maparg()-like dictionaries describing mappings. /// The "buffer" key is always zero. -ArrayOf(Dictionary) nvim_get_keymap(String mode) +ArrayOf(Dictionary) nvim_get_keymap(uint64_t channel_id, String mode) FUNC_API_SINCE(3) { - return keymap_array(mode, NULL); + return keymap_array(mode, NULL, channel_id == LUA_INTERNAL_CALL); } /// Sets a global |mapping| for the given mode. @@ -1561,18 +1586,23 @@ ArrayOf(Dictionary) nvim_get_keymap(String mode) /// nmap <nowait> <Space><NL> <Nop> /// </pre> /// +/// @param channel_id /// @param mode Mode short-name (map command prefix: "n", "i", "v", "x", …) /// or "!" for |:map!|, or empty string for |:map|. /// @param lhs Left-hand-side |{lhs}| of the mapping. /// @param rhs Right-hand-side |{rhs}| of the mapping. /// @param opts Optional parameters map. Accepts all |:map-arguments| -/// as keys excluding |<buffer>| but including |noremap|. +/// as keys excluding |<buffer>| but including |noremap| and "desc". +/// "desc" can be used to give a description to keymap. +/// When called from Lua, also accepts a "callback" key that takes +/// a Lua function to call when the mapping is executed. /// Values are Booleans. Unknown key is an error. /// @param[out] err Error details, if any. -void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Error *err) +void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, Dict(keymap) *opts, + Error *err) FUNC_API_SINCE(6) { - modify_keymap(-1, false, mode, lhs, rhs, opts, err); + modify_keymap(channel_id, -1, false, mode, lhs, rhs, opts, err); } /// Unmaps a global |mapping| for the given mode. @@ -1580,10 +1610,10 @@ void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Er /// To unmap a buffer-local mapping, use |nvim_buf_del_keymap()|. /// /// @see |nvim_set_keymap()| -void nvim_del_keymap(String mode, String lhs, Error *err) +void nvim_del_keymap(uint64_t channel_id, String mode, String lhs, Error *err) FUNC_API_SINCE(6) { - nvim_buf_del_keymap(-1, mode, lhs, err); + nvim_buf_del_keymap(channel_id, -1, mode, lhs, err); } /// Gets a map of global (non-buffer-local) Ex commands. @@ -1741,7 +1771,7 @@ Array nvim_list_chans(void) /// 1. To perform several requests from an async context atomically, i.e. /// without interleaving redraws, RPC requests from other clients, or user /// interactions (however API methods may trigger autocommands or event -/// processing which have such side-effects, e.g. |:sleep| may wake timers). +/// processing which have such side effects, e.g. |:sleep| may wake timers). /// 2. To minimize RPC overhead (roundtrips) of a sequence of many requests. /// /// @param channel_id @@ -1927,7 +1957,7 @@ Dictionary nvim__stats(void) Dictionary rv = ARRAY_DICT_INIT; PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync)); PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); - PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_refcount)); + PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count())); return rv; } @@ -2101,7 +2131,7 @@ void nvim__screenshot(String path) } -/// Deletes a uppercase/file named mark. See |mark-motions|. +/// Deletes an uppercase/file named mark. See |mark-motions|. /// /// @note fails with error if a lowercase or buffer local named mark is used. /// @param name Mark name @@ -2231,7 +2261,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * Dictionary result = ARRAY_DICT_INIT; int maxwidth; - char fillchar = 0; + int fillchar = 0; Window window = 0; bool use_tabline = false; bool highlights = false; @@ -2246,12 +2276,12 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * } if (HAS_KEY(opts->fillchar)) { - if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size > 1) { - api_set_error(err, kErrorTypeValidation, "fillchar must be an ASCII character"); + if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size == 0 + || char2cells(fillchar = utf_ptr2char((char_u *)opts->fillchar.data.string.data)) != 1 + || (size_t)utf_char2len(fillchar) != opts->fillchar.data.string.size) { + api_set_error(err, kErrorTypeValidation, "fillchar must be a single-width character"); return result; } - - fillchar = opts->fillchar.data.string.data[0]; } if (HAS_KEY(opts->highlights)) { @@ -2278,11 +2308,16 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * fillchar = ' '; } else { wp = find_window_by_handle(window, err); + + if (wp == NULL) { + api_set_error(err, kErrorTypeException, "unknown winid %d", window); + return result; + } ewp = wp; if (fillchar == 0) { int attr; - fillchar = (char)fillchar_status(&attr, wp); + fillchar = fillchar_status(&attr, wp); } } @@ -2310,7 +2345,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * sizeof(buf), (char_u *)str.data, false, - (char_u)fillchar, + fillchar, maxwidth, hltab_ptr, NULL); @@ -2363,3 +2398,55 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * return result; } + +/// Create a new user command |user-commands| +/// +/// {name} is the name of the new command. The name must begin with an uppercase letter. +/// +/// {command} is the replacement text or Lua function to execute. +/// +/// Example: +/// <pre> +/// :call nvim_add_user_command('SayHello', 'echo "Hello world!"', {}) +/// :SayHello +/// Hello world! +/// </pre> +/// +/// @param name Name of the new user command. Must begin with an uppercase letter. +/// @param command Replacement command to execute when this user command is executed. When called +/// from Lua, the command can also be a Lua function. The function is called with a +/// single table argument that contains the following keys: +/// - args: (string) The args passed to the command, if any |<args>| +/// - fargs: (table) The args split by unescaped whitespace (when more than one +/// argument is allowed), if any |<f-args>| +/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| +/// - line1: (number) The starting line of the command range |<line1>| +/// - line2: (number) The final line of the command range |<line2>| +/// - range: (number) The number of items in the command range: 0, 1, or 2 |<range>| +/// - count: (number) Any count supplied |<count>| +/// - reg: (string) The optional register, if specified |<reg>| +/// - mods: (string) Command modifiers, if any |<mods>| +/// @param opts Optional command attributes. See |command-attributes| for more details. To use +/// boolean attributes (such as |:command-bang| or |:command-bar|) set the value to +/// "true". In addition to the string options listed in |:command-complete|, the +/// "complete" key also accepts a Lua function which works like the "customlist" +/// completion mode |:command-completion-customlist|. Additional parameters: +/// - desc: (string) Used for listing the command when a Lua function is used for +/// {command}. +/// - force: (boolean, default true) Override any previous definition. +/// @param[out] err Error details, if any. +void nvim_add_user_command(String name, Object command, Dict(user_command) *opts, Error *err) + FUNC_API_SINCE(9) +{ + add_user_command(name, command, opts, 0, err); +} + +/// Delete a user-defined command. +/// +/// @param name Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_del_user_command(String name, Error *err) + FUNC_API_SINCE(9) +{ + nvim_buf_del_user_command(-1, name, err); +} diff --git a/src/nvim/api/vimscript.c b/src/nvim/api/vimscript.c index 640144b234..4123674fe7 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -37,7 +37,7 @@ /// @param[out] err Error details (Vim error), if any /// @return Output (non-error, non-shell |:!|) if `output` is true, /// else empty string. -String nvim_exec(String src, Boolean output, Error *err) +String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err) FUNC_API_SINCE(7) { const int save_msg_silent = msg_silent; @@ -52,11 +52,16 @@ String nvim_exec(String src, Boolean output, Error *err) if (output) { msg_silent++; } + + const sctx_T save_current_sctx = api_set_sctx(channel_id); + do_source_str(src.data, "nvim_exec()"); if (output) { capture_ga = save_capture_ga; msg_silent = save_msg_silent; } + + current_sctx = save_current_sctx; try_end(err); if (ERROR_SET(err)) { diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 6e68c057dc..9c473ff724 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -39,7 +39,7 @@ Buffer nvim_win_get_buf(Window window, Error *err) return win->w_buffer->handle; } -/// Sets the current buffer in a window, without side-effects +/// Sets the current buffer in a window, without side effects /// /// @param window Window handle, or 0 for current window /// @param buffer Buffer handle @@ -71,6 +71,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Error *err) } /// Sets the (1,0)-indexed cursor position in the window. |api-indexing| +/// Unlike |win_execute()| this scrolls the window. /// /// @param window Window handle, or 0 for current window /// @param pos (row, col) tuple representing the new position @@ -118,6 +119,8 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err) update_topline_win(win); redraw_later(win, VALID); + redraw_for_cursorline(win); + win->w_redr_status = true; } /// Gets the window height @@ -395,7 +398,7 @@ void nvim_win_hide(Window window, Error *err) TryState tstate; try_enter(&tstate); if (tabpage == curtab) { - win_close(win, false); + win_close(win, false, false); } else { win_close_othertab(win, false, tabpage); } @@ -455,17 +458,12 @@ Object nvim_win_call(Window window, LuaRef fun, Error *err) } tabpage_T *tabpage = win_find_tabpage(win); - win_T *save_curwin; - tabpage_T *save_curtab; - try_start(); Object res = OBJECT_INIT; - if (switch_win_noblock(&save_curwin, &save_curtab, win, tabpage, true) == - OK) { + WIN_EXECUTE(win, tabpage, { Array args = ARRAY_DICT_INIT; res = nlua_call_ref(fun, NULL, args, true, err); - } - restore_win_noblock(save_curwin, save_curtab, true); + }); try_end(err); return res; } 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/auevents.lua b/src/nvim/auevents.lua index 8fe623fc96..518d0b52b2 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -40,6 +40,7 @@ return { 'DiagnosticChanged', -- diagnostics in a buffer were modified 'DiffUpdated', -- diffs have been updated 'DirChanged', -- directory changed + 'DirChangedPre', -- directory is going to change 'EncodingChanged', -- after changing the 'encoding' option 'ExitPre', -- before exiting 'FileAppendCmd', -- append to a file using command @@ -132,18 +133,14 @@ return { nvim_specific = { BufModifiedSet=true, DiagnosticChanged=true, - DirChanged=true, RecordingEnter=true, RecordingLeave=true, Signal=true, - TabClosed=true, - TabNew=true, TabNewEntered=true, TermClose=true, TermOpen=true, UIEnter=true, UILeave=true, - WinClosed=true, WinScrolled=true, }, } diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 463bd5e0e6..66222f6a6a 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -2,7 +2,9 @@ // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com // autocmd.c: Autocommand related functions +#include <signal.h> +#include "lauxlib.h" #include "nvim/api/private/helpers.h" #include "nvim/ascii.h" #include "nvim/autocmd.h" @@ -13,8 +15,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 +33,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 +80,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 +101,22 @@ 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 + +// 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 +126,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 +135,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 +157,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 +173,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 +270,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 +289,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 +306,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 +332,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 +353,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 +370,71 @@ void aubuflocal_remove(buf_T *buf) au_cleanup(); } -// Add an autocmd group name. -// Return its ID. Returns AUGROUP_ERROR (< 0) for error. -static int au_new_group(char_u *name) +// Add an autocmd group name or return existing group matching name. +// Return its ID. +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. +/// @param stupid_legacy_mode bool: This paremeter determines whether to run the augroup +/// deletion in the same fashion as `:augroup! {name}` where if there are any remaining +/// autocmds left in the augroup, it will change the name of the augroup to `--- DELETED ---` +/// but leave the autocmds existing. These are _separate_ augroups, so if you do this for +/// multiple augroups, you will have a bunch of `--- DELETED ---` augroups at the same time. +/// There is no way, as far as I could tell, how to actually delete them at this point as a user +/// +/// I did not consider this good behavior, so now when NOT in stupid_legacy_mode, we actually +/// delete these groups and their commands, like you would expect (and don't leave hanging +/// `--- DELETED ---` groups around) +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 +443,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,23 +516,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 - int i = au_new_group(arg); - if (i != AUGROUP_ERROR) { - current_augroup = i; - } + 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(); } @@ -399,25 +545,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; @@ -447,7 +598,7 @@ static event_T event_name2nr(const char_u *start, char_u **end) /// @param[in] event Event to return name for. /// /// @return Event name, static string. Returns "Unknown" for unknown events. -static const char *event_nr2name(event_T event) +const char *event_nr2name(event_T event) FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_CONST { int i; @@ -460,33 +611,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'. /// @@ -595,7 +719,7 @@ void do_autocmd(char_u *arg_in, int forceit) char_u *envpat = NULL; char_u *cmd; int need_free = false; - int nested = false; + bool nested = false; bool once = false; int group; @@ -604,12 +728,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; } @@ -646,37 +770,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); - } + invalid_flags |= arg_autocmd_flag_get(&once, &cmd, "++once", 6); + invalid_flags |= arg_autocmd_flag_get(&nested, &cmd, "++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. + invalid_flags |= arg_autocmd_flag_get(&nested, &cmd, "nested", 6); } } + if (invalid_flags) { + return; + } + // Find the start of the commands. // Expand <sfile> in it. if (*cmd != NUL) { @@ -688,36 +797,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 { + 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 { - for (event_T event = (event_T)0; event < NUM_EVENTS; - event = (event_T)(event + 1)) { + 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) { @@ -726,30 +836,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. @@ -759,216 +853,294 @@ 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; + nlua_set_sctx(&ac->script_ctx); + ac->next = NULL; + ac->once = once; + ac->nested = nested; + ac->desc = NULL; + + // TODO(tjdevries): What to do about :autocmd and where/how to show lua stuffs there. + // perhaps: <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; /// @@ -984,7 +1156,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")); @@ -993,7 +1165,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; } @@ -1069,8 +1241,6 @@ void ex_doautoall(exarg_T *eap) do_modelines(0); } } - - check_cursor(); // just in case lines got deleted } /// Check *argp for <nomodeline>. When it is present return false, otherwise @@ -1160,7 +1330,10 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf) // Prevent chdir() call in win_enter_ext(), through do_autochdir() int save_acd = p_acd; p_acd = false; + // no redrawing and don't set the window title + RedrawingDisabled++; win_enter(aucmd_win, false); + RedrawingDisabled--; p_acd = save_acd; unblock_autocmds(); curwin = aucmd_win; @@ -1168,6 +1341,10 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf) curbuf = buf; aco->new_curwin_handle = curwin->handle; set_bufref(&aco->new_curbuf, curbuf); + + // disable the Visual area, the position may be invalid in another buffer + aco->save_VIsual_active = VIsual_active; + VIsual_active = false; } /// Cleanup after executing autocommands for a (hidden) buffer. @@ -1264,6 +1441,12 @@ win_found: check_cursor(); } } + + check_cursor(); // just in case lines got deleted + VIsual_active = aco->save_VIsual_active; + if (VIsual_active) { + check_pos(curbuf, &VIsual); + } } /// Execute autocommands for "event" and file name "fname". @@ -1370,8 +1553,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; @@ -1394,7 +1577,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. @@ -1509,12 +1692,13 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, || event == EVENT_CMDLINELEAVE || event == EVENT_CMDWINENTER || event == EVENT_CMDWINLEAVE || event == EVENT_CMDUNDEFINED || event == EVENT_COLORSCHEME || event == EVENT_COLORSCHEMEPRE - || event == EVENT_DIRCHANGED || event == EVENT_FILETYPE - || event == EVENT_FUNCUNDEFINED || event == EVENT_MODECHANGED - || event == EVENT_OPTIONSET || event == EVENT_QUICKFIXCMDPOST - || event == EVENT_QUICKFIXCMDPRE || event == EVENT_REMOTEREPLY - || event == EVENT_SPELLFILEMISSING || event == EVENT_SYNTAX - || event == EVENT_SIGNAL || event == EVENT_TABCLOSED + || event == EVENT_DIRCHANGED || event == EVENT_DIRCHANGEDPRE + || event == EVENT_FILETYPE || event == EVENT_FUNCUNDEFINED + || event == EVENT_MODECHANGED || event == EVENT_OPTIONSET + || event == EVENT_QUICKFIXCMDPOST || event == EVENT_QUICKFIXCMDPRE + || event == EVENT_REMOTEREPLY || event == EVENT_SPELLFILEMISSING + || event == EVENT_SYNTAX || event == EVENT_SIGNAL + || event == EVENT_TABCLOSED || event == EVENT_USER || event == EVENT_WINCLOSED) { fname = vim_strsave(fname); } else { @@ -1805,6 +1989,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; @@ -1817,7 +2006,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 { @@ -1847,14 +2037,34 @@ 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); + + // TODO(tjdevries): + // + // 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 would 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; @@ -1919,19 +2129,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 @@ -1943,7 +2146,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])) { @@ -1984,16 +2187,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 @@ -2036,7 +2248,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. @@ -2100,3 +2312,362 @@ 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; +} + +/// Handles grabbing arguments from `:autocmd` such as ++once and ++nested +static bool arg_autocmd_flag_get(bool *flag, char_u **cmd_ptr, char *pattern, int len) +{ + if (STRNCMP(*cmd_ptr, pattern, len) == 0 && ascii_iswhite((*cmd_ptr)[len])) { + if (*flag) { + semsg(_(e_duparg2), pattern); + return true; + } + + *flag = true; + *cmd_ptr = skipwhite((*cmd_ptr) + len); + } + + return false; +} + + +// 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 ac12e2acf3..53ec7f2bb7 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 { @@ -14,25 +19,27 @@ typedef struct { handle_T save_prevwin_handle; ///< ID of saved prevwin bufref_T new_curbuf; ///< new curbuf char_u *globaldir; ///< saved value of globaldir + bool save_VIsual_active; ///< saved VIsual_active } 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; // ID used for uniquely tracking an autocmd. 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 @@ -40,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 @@ -74,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/buffer.c b/src/nvim/buffer.c index 89baea83f8..9e82b4e80b 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -846,7 +846,7 @@ void goto_buffer(exarg_T *eap, int start, int dir, int count) enter_cleanup(&cs); // Quitting means closing the split window, nothing else. - win_close(curwin, true); + win_close(curwin, true, false); swap_exists_action = SEA_NONE; swap_exists_did_quit = true; @@ -1237,7 +1237,7 @@ int do_buffer(int action, int start, int dir, int count, int forceit) while (buf == curbuf && !(curwin->w_closing || curwin->w_buffer->b_locked > 0) && (!ONE_WINDOW || first_tabpage->tp_next != NULL)) { - if (win_close(curwin, false) == FAIL) { + if (win_close(curwin, false, false) == FAIL) { break; } } @@ -1441,7 +1441,7 @@ void set_curbuf(buf_T *buf, int action) set_bufref(&prevbufref, prevbuf); set_bufref(&newbufref, buf); - // Autocommands may delete the curren buffer and/or the buffer we want to go + // Autocommands may delete the current buffer and/or the buffer we want to go // to. In those cases don't close the buffer. if (!apply_autocmds(EVENT_BUFLEAVE, NULL, NULL, false, curbuf) || (bufref_valid(&prevbufref) && bufref_valid(&newbufref) @@ -1454,7 +1454,11 @@ void set_curbuf(buf_T *buf, int action) } if (bufref_valid(&prevbufref) && !aborting()) { win_T *previouswin = curwin; - if (prevbuf == curbuf) { + + // Do not sync when in Insert mode and the buffer is open in + // another window, might be a timer doing something in another + // window. + if (prevbuf == curbuf && ((State & INSERT) == 0 || curbuf->b_nwindows <= 1)) { u_sync(false); } close_buffer(prevbuf == curwin->w_buffer ? curwin : NULL, @@ -1494,6 +1498,11 @@ void set_curbuf(buf_T *buf, int action) */ void enter_buffer(buf_T *buf) { + // Get the buffer in the current window. + curwin->w_buffer = buf; + curbuf = buf; + curbuf->b_nwindows++; + // Copy buffer and window local option values. Not for a help buffer. buf_copy_options(buf, BCO_ENTER | BCO_NOHELP); if (!buf->b_help) { @@ -1504,11 +1513,6 @@ void enter_buffer(buf_T *buf) } foldUpdateAll(curwin); // update folds (later). - // Get the buffer in the current window. - curwin->w_buffer = buf; - curbuf = buf; - curbuf->b_nwindows++; - if (curwin->w_p_diff) { diff_buf_add(curbuf); } @@ -1896,10 +1900,8 @@ void free_buf_options(buf_T *buf, int free_p_ff) clear_string_option(&buf->b_p_flp); clear_string_option(&buf->b_p_isk); clear_string_option(&buf->b_p_vsts); - xfree(buf->b_p_vsts_nopaste); - buf->b_p_vsts_nopaste = NULL; - xfree(buf->b_p_vsts_array); - buf->b_p_vsts_array = NULL; + XFREE_CLEAR(buf->b_p_vsts_nopaste); + XFREE_CLEAR(buf->b_p_vsts_array); clear_string_option(&buf->b_p_vts); XFREE_CLEAR(buf->b_p_vts_array); clear_string_option(&buf->b_p_keymap); @@ -3326,7 +3328,7 @@ void maketitle(void) len = (int)STRLEN(buf_p); if (len > 100) { len -= 100; - len += (*mb_tail_off)(buf_p, buf_p + len) + 1; + len += mb_tail_off(buf_p, buf_p + len) + 1; buf_p += len; } STRCPY(icon_str, buf_p); @@ -3418,7 +3420,7 @@ typedef enum { /// /// @return The final width of the statusline int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use_sandbox, - char_u fillchar, int maxwidth, stl_hlrec_t **hltab, StlClickRecord **tabtab) + int fillchar, int maxwidth, stl_hlrec_t **hltab, StlClickRecord **tabtab) { static size_t stl_items_len = 20; // Initial value, grows as needed. static stl_item_t *stl_items = NULL; @@ -3437,8 +3439,12 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use if (stl_items == NULL) { stl_items = xmalloc(sizeof(stl_item_t) * stl_items_len); stl_groupitems = xmalloc(sizeof(int) * stl_items_len); - stl_hltab = xmalloc(sizeof(stl_hlrec_t) * stl_items_len); - stl_tabtab = xmalloc(sizeof(StlClickRecord) * stl_items_len); + + // Allocate one more, because the last element is used to indicate the + // end of the list. + stl_hltab = xmalloc(sizeof(stl_hlrec_t) * (stl_items_len + 1)); + stl_tabtab = xmalloc(sizeof(StlClickRecord) * (stl_items_len + 1)); + stl_separator_locations = xmalloc(sizeof(int) * stl_items_len); } @@ -3461,9 +3467,6 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use if (fillchar == 0) { fillchar = ' '; - } else if (utf_char2len(fillchar) > 1) { - // Can't handle a multi-byte fill character yet. - fillchar = '-'; } // The cursor in windows other than the current one isn't always @@ -3516,8 +3519,8 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use stl_items = xrealloc(stl_items, sizeof(stl_item_t) * new_len); stl_groupitems = xrealloc(stl_groupitems, sizeof(int) * new_len); - stl_hltab = xrealloc(stl_hltab, sizeof(stl_hlrec_t) * new_len); - stl_tabtab = xrealloc(stl_tabtab, sizeof(StlClickRecord) * new_len); + stl_hltab = xrealloc(stl_hltab, sizeof(stl_hlrec_t) * (new_len + 1)); + stl_tabtab = xrealloc(stl_tabtab, sizeof(StlClickRecord) * (new_len + 1)); stl_separator_locations = xrealloc(stl_separator_locations, sizeof(int) * new_len); @@ -3661,7 +3664,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use out_p = out_p - n + 1; // Fill up space left over by half a double-wide char. while (++group_len < stl_items[stl_groupitems[groupdepth]].minwid) { - *out_p++ = fillchar; + MB_CHAR2BYTES(fillchar, out_p); } // } @@ -3684,14 +3687,14 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use if (min_group_width < 0) { min_group_width = 0 - min_group_width; while (group_len++ < min_group_width && out_p < out_end_p) { - *out_p++ = fillchar; + MB_CHAR2BYTES(fillchar, out_p); } // If the group is right-aligned, shift everything to the right and // prepend with filler characters. } else { // { Move the group to the right - memmove(t + min_group_width - group_len, t, (size_t)(out_p - t)); - group_len = min_group_width - group_len; + group_len = (min_group_width - group_len) * utf_char2len(fillchar); + memmove(t + group_len, t, (size_t)(out_p - t)); if (out_p + group_len >= (out_end_p + 1)) { group_len = (long)(out_end_p - out_p); } @@ -3705,7 +3708,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use // Prepend the fill characters for (; group_len > 0; group_len--) { - *t++ = fillchar; + MB_CHAR2BYTES(fillchar, t); } } } @@ -4001,14 +4004,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use case STL_VIRTCOL: case STL_VIRTCOL_ALT: { - // In list mode virtcol needs to be recomputed - colnr_T virtcol = wp->w_virtcol; - if (wp->w_p_list && wp->w_p_lcs_chars.tab1 == NUL) { - wp->w_p_list = false; - getvcol(wp, &wp->w_cursor, NULL, &virtcol, NULL); - wp->w_p_list = true; - } - virtcol++; + colnr_T virtcol = wp->w_virtcol + 1; // Don't display %V if it's the same as %c. if (opt == STL_VIRTCOL_ALT && (virtcol == (colnr_T)(!(State & INSERT) && empty_line @@ -4237,7 +4233,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use if (l + 1 == minwid && fillchar == '-' && ascii_isdigit(*t)) { *out_p++ = ' '; } else { - *out_p++ = fillchar; + MB_CHAR2BYTES(fillchar, out_p); } } minwid = 0; @@ -4248,20 +4244,21 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use } // { Copy the string text into the output buffer - while (*t && out_p < out_end_p) { - *out_p++ = *t++; + for (; *t && out_p < out_end_p; t++) { // Change a space by fillchar, unless fillchar is '-' and a // digit follows. - if (fillable && out_p[-1] == ' ' - && (!ascii_isdigit(*t) || fillchar != '-')) { - out_p[-1] = fillchar; + if (fillable && *t == ' ' + && (!ascii_isdigit(*(t + 1)) || fillchar != '-')) { + MB_CHAR2BYTES(fillchar, out_p); + } else { + *out_p++ = *t; } } // } // For left-aligned items, fill any remaining space with the fillchar for (; l < minwid && out_p < out_end_p; l++) { - *out_p++ = fillchar; + MB_CHAR2BYTES(fillchar, out_p); } // Otherwise if the item is a number, copy that to the output buffer. @@ -4351,7 +4348,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use // Only free the string buffer if we allocated it. // Note: This is not needed if `str` is pointing at `tmp` if (opt == STL_VIM_EXPR) { - xfree(str); + XFREE_CLEAR(str); } if (num >= 0 || (!itemisflag && str && *str)) { @@ -4454,7 +4451,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use // Fill up for half a double-wide character. while (++width < maxwidth) { - *trunc_p++ = fillchar; + MB_CHAR2BYTES(fillchar, trunc_p); *trunc_p = NUL; } // } @@ -4505,13 +4502,13 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use standard_spaces * (num_separators - 1); for (int i = 0; i < num_separators; i++) { - int dislocation = (i == (num_separators - 1)) - ? final_spaces : standard_spaces; + int dislocation = (i == (num_separators - 1)) ? final_spaces : standard_spaces; + dislocation *= utf_char2len(fillchar); char_u *start = stl_items[stl_separator_locations[i]].start; char_u *seploc = start + dislocation; STRMOVE(seploc, start); - for (char_u *s = start; s < seploc; s++) { - *s = fillchar; + for (char_u *s = start; s < seploc; ) { + MB_CHAR2BYTES(fillchar, s); } for (int item_idx = stl_separator_locations[i] + 1; @@ -4576,7 +4573,7 @@ int build_stl_str_hl(win_T *wp, char_u *out, size_t outlen, char_u *fmt, int use cur_tab_rec->def.func = NULL; } - // When inside update_screen we do not want redrawing a stausline, ruler, + // When inside update_screen we do not want redrawing a statusline, ruler, // title, etc. to trigger another redraw, it may cause an endless loop. if (updating_screen) { must_redraw = save_must_redraw; @@ -4830,7 +4827,7 @@ void do_arg_all(int count, int forceit, int keep_tabs) && (first_tabpage->tp_next == NULL || !had_tab)) { use_firstwin = true; } else { - win_close(wp, !buf_hide(buf) && !bufIsChanged(buf)); + win_close(wp, !buf_hide(buf) && !bufIsChanged(buf), false); // check if autocommands removed the next window if (!win_valid(wpnext)) { // start all over... @@ -5021,7 +5018,7 @@ void ex_buffer_all(exarg_T *eap) && !ONE_WINDOW && !(wp->w_closing || wp->w_buffer->b_locked > 0)) { - win_close(wp, false); + win_close(wp, false, false); wpnext = firstwin; // just in case an autocommand does // something strange with windows tpnext = first_tabpage; // start all over... @@ -5102,7 +5099,7 @@ void ex_buffer_all(exarg_T *eap) enter_cleanup(&cs); // User selected Quit at ATTENTION prompt; close this window. - win_close(curwin, true); + win_close(curwin, true, false); open_wins--; swap_exists_action = SEA_NONE; swap_exists_did_quit = true; @@ -5144,7 +5141,7 @@ void ex_buffer_all(exarg_T *eap) // BufWrite Autocommands made the window invalid, start over wp = lastwin; } else if (r) { - win_close(wp, !buf_hide(wp->w_buffer)); + win_close(wp, !buf_hide(wp->w_buffer), false); open_wins--; wp = lastwin; } else { @@ -5463,33 +5460,54 @@ bool find_win_for_buf(buf_T *buf, win_T **wp, tabpage_T **tp) return false; } -int buf_signcols(buf_T *buf) +static int buf_signcols_inner(buf_T *buf, int maximum) { - if (!buf->b_signcols_valid) { - sign_entry_T *sign; // a sign in the sign list - int signcols = 0; - int linesum = 0; - linenr_T curline = 0; - - FOR_ALL_SIGNS_IN_BUF(buf, sign) { - if (sign->se_lnum > curline) { - if (linesum > signcols) { - signcols = linesum; + sign_entry_T *sign; // a sign in the sign list + int signcols = 0; + int linesum = 0; + linenr_T curline = 0; + + FOR_ALL_SIGNS_IN_BUF(buf, sign) { + if (sign->se_lnum > curline) { + if (linesum > signcols) { + signcols = linesum; + if (signcols >= maximum) { + return maximum; } - curline = sign->se_lnum; - linesum = 0; - } - if (sign->se_has_text_or_icon) { - linesum++; } + curline = sign->se_lnum; + linesum = 0; } - if (linesum > signcols) { - signcols = linesum; + if (sign->se_has_text_or_icon) { + linesum++; } + } + if (linesum > signcols) { + signcols = linesum; + if (signcols >= maximum) { + return maximum; + } + } + + return signcols; +} + +int buf_signcols(buf_T *buf, int maximum) +{ + // The maximum can be determined from 'signcolumn' which is window scoped so + // need to invalidate signcols if the maximum is greater than the previous + // maximum. + if (maximum > buf->b_signcols_max) { + buf->b_signcols_valid = false; + } + + if (!buf->b_signcols_valid) { + int signcols = buf_signcols_inner(buf, maximum); // Check if we need to redraw if (signcols != buf->b_signcols) { buf->b_signcols = signcols; + buf->b_signcols_max = maximum; redraw_buf_later(buf, NOT_VALID); } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 49e527e98b..1e0c837056 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -204,6 +204,10 @@ typedef struct { #define w_p_nu w_onebuf_opt.wo_nu // 'number' int wo_rnu; #define w_p_rnu w_onebuf_opt.wo_rnu // 'relativenumber' + char_u *wo_ve; +#define w_p_ve w_onebuf_opt.wo_ve // 'virtualedit' + unsigned wo_ve_flags; +#define w_ve_flags w_onebuf_opt.wo_ve_flags // flags for 'virtualedit' long wo_nuw; #define w_p_nuw w_onebuf_opt.wo_nuw // 'numberwidth' int wo_wfh; @@ -352,6 +356,7 @@ struct mapblock { char_u *m_keys; // mapped from, lhs char_u *m_str; // mapped to, rhs char_u *m_orig_str; // rhs as entered by the user + LuaRef m_luaref; // lua function reference as rhs int m_keylen; // strlen(m_keys) int m_mode; // valid mode int m_noremap; // if non-zero no re-mapping for m_str @@ -359,6 +364,7 @@ struct mapblock { char m_nowait; // <nowait> used char m_expr; // <expr> used, m_str is an expression sctx_T m_script_ctx; // SCTX where map was defined + char *m_desc; // description of keymap }; /// Used for highlighting in the status line. @@ -581,7 +587,9 @@ struct file_buffer { // where invoked long b_mtime; // last change time of original file + long b_mtime_ns; // nanoseconds of last change time long b_mtime_read; // last change time when reading + long b_mtime_read_ns; // nanoseconds of last read time uint64_t b_orig_size; // size of original file in bytes int b_orig_mode; // mode of original file time_t b_last_used; // time when the buffer was last used; used @@ -856,6 +864,7 @@ struct file_buffer { sign_entry_T *b_signlist; // list of placed signs int b_signcols; // last calculated number of sign columns bool b_signcols_valid; // calculated sign columns is valid + int b_signcols_max; // Maximum value b_signcols is valid for. Terminal *terminal; // Terminal instance associated with the buffer @@ -864,8 +873,7 @@ struct file_buffer { int b_mapped_ctrl_c; // modes where CTRL-C is mapped MarkTree b_marktree[1]; - Map(uint64_t, ExtmarkItem) b_extmark_index[1]; - Map(uint64_t, ExtmarkNs) b_extmark_ns[1]; // extmark namespaces + Map(uint32_t, uint32_t) b_extmark_ns[1]; // extmark namespaces size_t b_virt_line_blocks; // number of virt_line blocks // array of channel_id:s which have asked to receive updates for this @@ -1346,6 +1354,7 @@ struct window_S { // recomputed int w_nrwidth; // width of 'number' and 'relativenumber' // column being used + int w_scwidth; // width of 'signcolumn' /* * === end of cached values === diff --git a/src/nvim/change.c b/src/nvim/change.c index 1dbbfff024..736867b6d3 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -223,19 +223,20 @@ static void changed_common(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra // values for the cursor. // Update the folds for this window. Can't postpone this, because // a following operator might work on the whole fold: ">>dd". - foldUpdate(wp, lnum, lnume + xtra - 1); + linenr_T last = lnume + xtra - 1; // last line after the change + foldUpdate(wp, lnum, last); // The change may cause lines above or below the change to become // included in a fold. Set lnum/lnume to the first/last line that // might be displayed differently. // Set w_cline_folded here as an efficient way to update it when - // inserting lines just above a closed fold. */ + // inserting lines just above a closed fold. bool folded = hasFoldingWin(wp, lnum, &lnum, NULL, false, NULL); if (wp->w_cursor.lnum == lnum) { wp->w_cline_folded = folded; } - folded = hasFoldingWin(wp, lnume, NULL, &lnume, false, NULL); - if (wp->w_cursor.lnum == lnume) { + folded = hasFoldingWin(wp, last, NULL, &last, false, NULL); + if (wp->w_cursor.lnum == last) { wp->w_cline_folded = folded; } @@ -789,7 +790,7 @@ int del_bytes(colnr_T count, bool fixpos_arg, bool use_delcombine) // fixpos is true, we don't want to end up positioned at the NUL, // unless "restart_edit" is set or 'virtualedit' contains "onemore". if (col > 0 && fixpos && restart_edit == 0 - && (ve_flags & VE_ONEMORE) == 0) { + && (get_ve_flags() & VE_ONEMORE) == 0) { curwin->w_cursor.col--; curwin->w_cursor.coladd = 0; curwin->w_cursor.col -= utf_head_off(oldp, oldp + curwin->w_cursor.col); @@ -952,11 +953,13 @@ int copy_indent(int size, char_u *src) /// /// "second_line_indent": indent for after ^^D in Insert mode or if flag /// OPENLINE_COM_LIST +/// "did_do_comment" is set to true when intentionally putting the comment +/// leader in fromt of the new line. /// /// @param dir FORWARD or BACKWARD /// /// @return true on success, false on failure -int open_line(int dir, int flags, int second_line_indent) +int open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) { char_u *next_line = NULL; // copy of the next line char_u *p_extra = NULL; // what goes to next line @@ -969,6 +972,7 @@ int open_line(int dir, int flags, int second_line_indent) bool retval = false; // return value int extra_len = 0; // length of p_extra string int lead_len; // length of comment leader + int comment_start = 0; // start index of the comment leader char_u *lead_flags; // position in 'comments' for comment leader char_u *leader = NULL; // copy of comment leader char_u *allocated = NULL; // allocated memory @@ -977,6 +981,7 @@ int open_line(int dir, int flags, int second_line_indent) pos_T *pos; bool do_si = (!p_paste && curbuf->b_p_si && !curbuf->b_p_cin && *curbuf->b_p_inde == NUL); + bool do_cindent; bool no_si = false; // reset did_si afterwards int first_char = NUL; // init for GCC int vreplace_mode; @@ -1189,11 +1194,30 @@ int open_line(int dir, int flags, int second_line_indent) did_ai = true; } + // May do indenting after opening a new line. + do_cindent = !p_paste && (curbuf->b_p_cin || *curbuf->b_p_inde != NUL) + && in_cinkeys(dir == FORWARD ? KEY_OPEN_FORW : KEY_OPEN_BACK, + ' ', linewhite(curwin->w_cursor.lnum)); + // Find out if the current line starts with a comment leader. // This may then be inserted in front of the new line. end_comment_pending = NUL; if (flags & OPENLINE_DO_COM) { lead_len = get_leader_len(saved_line, &lead_flags, dir == BACKWARD, true); + if (lead_len == 0 && do_cindent && dir == FORWARD) { + // Check for a line comment after code. + comment_start = check_linecomment(saved_line); + if (comment_start != MAXCOL) { + lead_len = get_leader_len(saved_line + comment_start, + &lead_flags, false, true); + if (lead_len != 0) { + lead_len += comment_start; + if (did_do_comment != NULL) { + *did_do_comment = true; + } + } + } + } } else { lead_len = 0; } @@ -1349,6 +1373,13 @@ int open_line(int dir, int flags, int second_line_indent) STRLCPY(leader, saved_line, lead_len + 1); + // TODO(vim): handle multi-byte and double width chars + for (int li = 0; li < comment_start; li++) { + if (!ascii_iswhite(leader[li])) { + leader[li] = ' '; + } + } + // Replace leader with lead_repl, right or left adjusted if (lead_repl != NULL) { int c = 0; @@ -1758,13 +1789,7 @@ int open_line(int dir, int flags, int second_line_indent) ai_col = (colnr_T)getwhitecols_curline(); } // May do indenting after opening a new line. - if (!p_paste - && (curbuf->b_p_cin - || *curbuf->b_p_inde != NUL - ) - && in_cinkeys(dir == FORWARD - ? KEY_OPEN_FORW - : KEY_OPEN_BACK, ' ', linewhite(curwin->w_cursor.lnum))) { + if (do_cindent) { do_c_expr_indent(); ai_col = (colnr_T)getwhitecols_curline(); } diff --git a/src/nvim/channel.c b/src/nvim/channel.c index cd5134fe5f..d79c0acc4a 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -613,7 +613,6 @@ static void on_channel_output(Stream *stream, Channel *chan, RBuffer *buf, size_ } else { if (chan->term) { terminal_receive(chan->term, ptr, count); - terminal_flush_output(chan->term); } rbuffer_consumed(buf, count); diff --git a/src/nvim/channel.h b/src/nvim/channel.h index 81b75e2d31..50d6b3600a 100644 --- a/src/nvim/channel.h +++ b/src/nvim/channel.h @@ -96,6 +96,8 @@ struct Channel { EXTERN PMap(uint64_t) channels INIT(= MAP_INIT); +EXTERN Callback on_print INIT(= CALLBACK_INIT); + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "channel.h.generated.h" #endif diff --git a/src/nvim/charset.c b/src/nvim/charset.c index 599d662993..f4882e57e1 100644 --- a/src/nvim/charset.c +++ b/src/nvim/charset.c @@ -217,9 +217,7 @@ int buf_init_chartab(buf_T *buf, int global) } } else if (i == 1) { // (re)set printable - // For double-byte we keep the cell width, so - // that we can detect it from the first byte. - if (((c < ' ') || (c > '~'))) { + if (c < ' ' || c > '~') { if (tilde) { g_chartab[c] = (uint8_t)((g_chartab[c] & ~CT_CELL_MASK) + ((dy_flags & DY_UHEX) ? 4 : 2)); @@ -539,7 +537,7 @@ char_u *transchar_buf(const buf_T *buf, int c) c = K_SECOND(c); } - if ((!chartab_initialized && (((c >= ' ') && (c <= '~')))) + if ((!chartab_initialized && (c >= ' ' && c <= '~')) || ((c <= 0xFF) && vim_isprintc_strict(c))) { // printable character transchar_charbuf[i] = (char_u)c; @@ -1441,7 +1439,7 @@ bool vim_isblankline(char_u *lbuf) /// @param unptr Returns the unsigned result. /// @param maxlen Max length of string to check. /// @param strict If true, fail if the number has unexpected trailing -/// alpha-numeric chars: *len is set to 0 and nothing else is +/// alphanumeric chars: *len is set to 0 and nothing else is /// returned. void vim_str2nr(const char_u *const start, int *const prep, int *const len, const int what, varnumber_T *const nptr, uvarnumber_T *const unptr, const int maxlen, @@ -1587,7 +1585,7 @@ vim_str2nr_hex: #undef PARSE_NUMBER vim_str2nr_proceed: - // Check for an alpha-numeric character immediately following, that is + // Check for an alphanumeric character immediately following, that is // most likely a typo. if (strict && ptr - (const char *)start != maxlen && ASCII_ISALNUM(*ptr)) { return; diff --git a/src/nvim/context.c b/src/nvim/context.c index 614a3ce30e..9434b4e0ea 100644 --- a/src/nvim/context.c +++ b/src/nvim/context.c @@ -256,7 +256,8 @@ static inline void ctx_save_funcs(Context *ctx, bool scriptonly) size_t cmd_len = sizeof("func! ") + STRLEN(name); char *cmd = xmalloc(cmd_len); snprintf(cmd, cmd_len, "func! %s", name); - String func_body = nvim_exec(cstr_as_string(cmd), true, &err); + String func_body = nvim_exec(VIML_INTERNAL_CALL, cstr_as_string(cmd), + true, &err); xfree(cmd); if (!ERROR_SET(&err)) { ADD(ctx->funcs, STRING_OBJ(func_body)); diff --git a/src/nvim/cursor.c b/src/nvim/cursor.c index 6e2c6232d7..55f55a46b2 100644 --- a/src/nvim/cursor.c +++ b/src/nvim/cursor.c @@ -15,6 +15,7 @@ #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/move.h" +#include "nvim/option.h" #include "nvim/plines.h" #include "nvim/screen.h" #include "nvim/state.h" @@ -110,7 +111,7 @@ static int coladvance2(pos_T *pos, bool addspaces, bool finetune, colnr_T wcol_a || (State & TERM_FOCUS) || restart_edit != NUL || (VIsual_active && *p_sel != 'o') - || ((ve_flags & VE_ONEMORE) && wcol < MAXCOL); + || ((get_ve_flags() & VE_ONEMORE) && wcol < MAXCOL); line = ml_get_buf(curbuf, pos->lnum, false); if (wcol >= MAXCOL) { @@ -366,6 +367,7 @@ void check_cursor_col_win(win_T *win) colnr_T len; colnr_T oldcol = win->w_cursor.col; colnr_T oldcoladd = win->w_cursor.col + win->w_cursor.coladd; + unsigned int cur_ve_flags = get_ve_flags(); len = (colnr_T)STRLEN(ml_get_buf(win->w_buffer, win->w_cursor.lnum, false)); if (len == 0) { @@ -377,7 +379,7 @@ void check_cursor_col_win(win_T *win) * - 'virtualedit' is set */ if ((State & INSERT) || restart_edit || (VIsual_active && *p_sel != 'o') - || (ve_flags & VE_ONEMORE) + || (cur_ve_flags & VE_ONEMORE) || virtual_active()) { win->w_cursor.col = len; } else { @@ -394,7 +396,7 @@ void check_cursor_col_win(win_T *win) // line. if (oldcol == MAXCOL) { win->w_cursor.coladd = 0; - } else if (ve_flags == VE_ALL) { + } else if (cur_ve_flags == VE_ALL) { if (oldcoladd > win->w_cursor.col) { win->w_cursor.coladd = oldcoladd - win->w_cursor.col; diff --git a/src/nvim/debugger.c b/src/nvim/debugger.c index b6e35f3047..75ebf0084e 100644 --- a/src/nvim/debugger.c +++ b/src/nvim/debugger.c @@ -268,7 +268,7 @@ void do_debug(char_u *cmd) DOCMD_VERBOSE|DOCMD_EXCRESET); debug_break_level = n; } - lines_left = (int)(Rows - 1); + lines_left = Rows - 1; } xfree(cmdline); @@ -277,7 +277,7 @@ void do_debug(char_u *cmd) redraw_all_later(NOT_VALID); need_wait_return = false; msg_scroll = save_msg_scroll; - lines_left = (int)(Rows - 1); + lines_left = Rows - 1; State = save_State; debug_mode = false; did_emsg = save_did_emsg; diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index c0f3c32f93..935b233752 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -13,8 +13,6 @@ # include "decoration.c.generated.h" #endif -static PMap(uint64_t) hl_decors; - /// Add highlighting to a buffer, bounded by two cursor positions, /// with an offset. /// @@ -33,9 +31,9 @@ void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start { colnr_T hl_start = 0; colnr_T hl_end = 0; - Decoration *decor = decor_hl(hl_id); + Decoration decor = DECORATION_INIT; + decor.hl_id = hl_id; - decor->priority = DECOR_PRIORITY_BASE; // TODO(bfredl): if decoration had blocky mode, we could avoid this loop for (linenr_T lnum = pos_start.lnum; lnum <= pos_end.lnum; lnum++) { int end_off = 0; @@ -59,40 +57,23 @@ void bufhl_add_hl_pos_offset(buf_T *buf, int src_id, int hl_id, lpos_T pos_start hl_start = pos_start.col + offset; hl_end = pos_end.col + offset; } - (void)extmark_set(buf, (uint64_t)src_id, NULL, - (int)lnum-1, hl_start, (int)lnum-1+end_off, hl_end, - decor, true, false, kExtmarkNoUndo); - } -} - -Decoration *decor_hl(int hl_id) -{ - assert(hl_id > 0); - Decoration **dp = (Decoration **)pmap_ref(uint64_t)(&hl_decors, - (uint64_t)hl_id, true); - if (*dp) { - return *dp; + extmark_set(buf, (uint32_t)src_id, NULL, + (int)lnum-1, hl_start, (int)lnum-1+end_off, hl_end, + &decor, true, false, kExtmarkNoUndo); } - - Decoration *decor = xcalloc(1, sizeof(*decor)); - decor->hl_id = hl_id; - decor->shared = true; - decor->priority = DECOR_PRIORITY_BASE; - *dp = decor; - return decor; } void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor) { - if (decor->hl_id && row2 >= row1) { + if ((!decor || decor->hl_id) && row2 >= row1) { redraw_buf_range_later(buf, row1+1, row2+1); } - if (kv_size(decor->virt_text)) { + if (decor && kv_size(decor->virt_text)) { redraw_buf_line_later(buf, row1+1); } - if (kv_size(decor->virt_lines)) { + if (decor && kv_size(decor->virt_lines)) { redraw_buf_line_later(buf, MIN(buf->b_ml.ml_line_count, row1+1+(decor->virt_lines_above?0:1))); } @@ -100,17 +81,17 @@ void decor_redraw(buf_T *buf, int row1, int row2, Decoration *decor) void decor_remove(buf_T *buf, int row, int row2, Decoration *decor) { - if (kv_size(decor->virt_lines)) { + decor_redraw(buf, row, row2, decor); + if (decor && kv_size(decor->virt_lines)) { assert(buf->b_virt_line_blocks > 0); buf->b_virt_line_blocks--; } - decor_redraw(buf, row, row2, decor); decor_free(decor); } void decor_free(Decoration *decor) { - if (decor && !decor->shared) { + if (decor) { clear_virttext(&decor->virt_text); for (size_t i = 0; i < kv_size(decor->virt_lines); i++) { clear_virttext(&kv_A(decor->virt_lines, i).line); @@ -134,17 +115,16 @@ Decoration *decor_find_virttext(buf_T *buf, int row, uint64_t ns_id) MarkTreeIter itr[1] = { 0 }; marktree_itr_get(buf->b_marktree, row, 0, itr); while (true) { - mtmark_t mark = marktree_itr_current(itr); - if (mark.row < 0 || mark.row > row) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0 || mark.pos.row > row) { break; - } else if (marktree_decor_level(mark.id) < kDecorLevelVisible) { + } else if (marktree_decor_level(mark) < kDecorLevelVisible) { goto next_mark; } - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark.id, false); - if (item && (ns_id == 0 || ns_id == item->ns_id) - && item->decor && kv_size(item->decor->virt_text)) { - return item->decor; + Decoration *decor = mark.decor_full; + if ((ns_id == 0 || ns_id == mark.ns) + && decor && kv_size(decor->virt_text)) { + return decor; } next_mark: marktree_itr_next(buf->b_marktree, itr); @@ -163,7 +143,20 @@ bool decor_redraw_reset(buf_T *buf, DecorState *state) } } kv_size(state->active) = 0; - return map_size(buf->b_extmark_index); + return buf->b_marktree->n_keys; +} + +Decoration get_decor(mtkey_t mark) +{ + if (mark.decor_full) { + return *mark.decor_full; + } else { + Decoration fake = DECORATION_INIT; + fake.hl_id = mark.hl_id; + fake.priority = mark.priority; + fake.hl_eol = (mark.flags & MT_FLAG_HL_EOL); + return fake; + } } @@ -176,42 +169,35 @@ bool decor_redraw_start(buf_T *buf, int top_row, DecorState *state) } marktree_itr_rewind(buf->b_marktree, state->itr); while (true) { - mtmark_t mark = marktree_itr_current(state->itr); - if (mark.row < 0) { // || mark.row > end_row + mtkey_t mark = marktree_itr_current(state->itr); + if (mark.pos.row < 0) { // || mark.row > end_row break; } - if ((mark.row < top_row && mark.id&MARKTREE_END_FLAG) - || marktree_decor_level(mark.id) < kDecorLevelVisible) { + if ((mark.pos.row < top_row && mt_end(mark)) + || marktree_decor_level(mark) < kDecorLevelVisible) { goto next_mark; } - uint64_t start_id = mark.id & ~MARKTREE_END_FLAG; - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - start_id, false); - if (!item || !item->decor) { - goto next_mark; - } - Decoration *decor = item->decor; + Decoration decor = get_decor(mark); - mtpos_t altpos = marktree_lookup(buf->b_marktree, - mark.id^MARKTREE_END_FLAG, NULL); + mtpos_t altpos = marktree_get_altpos(buf->b_marktree, mark, NULL); - if ((!(mark.id&MARKTREE_END_FLAG) && altpos.row < top_row - && !kv_size(decor->virt_text)) - || ((mark.id&MARKTREE_END_FLAG) && altpos.row >= top_row)) { + if ((!mt_end(mark) && altpos.row < top_row + && !kv_size(decor.virt_text)) + || (mt_end(mark) && altpos.row >= top_row)) { goto next_mark; } - if (mark.id&MARKTREE_END_FLAG) { - decor_add(state, altpos.row, altpos.col, mark.row, mark.col, - decor, false); + if (mt_end(mark)) { + decor_add(state, altpos.row, altpos.col, mark.pos.row, mark.pos.col, + &decor, false); } else { if (altpos.row == -1) { - altpos.row = mark.row; - altpos.col = mark.col; + altpos.row = mark.pos.row; + altpos.col = mark.pos.col; } - decor_add(state, mark.row, mark.col, altpos.row, altpos.col, - decor, false); + decor_add(state, mark.pos.row, mark.pos.col, altpos.row, altpos.col, + &decor, false); } next_mark: @@ -266,43 +252,36 @@ int decor_redraw_col(buf_T *buf, int col, int win_col, bool hidden, DecorState * while (true) { // TODO(bfredl): check duplicate entry in "intersection" // branch - mtmark_t mark = marktree_itr_current(state->itr); - if (mark.row < 0 || mark.row > state->row) { + mtkey_t mark = marktree_itr_current(state->itr); + if (mark.pos.row < 0 || mark.pos.row > state->row) { break; - } else if (mark.row == state->row && mark.col > col) { - state->col_until = mark.col-1; + } else if (mark.pos.row == state->row && mark.pos.col > col) { + state->col_until = mark.pos.col-1; break; } - if ((mark.id&MARKTREE_END_FLAG) - || marktree_decor_level(mark.id) < kDecorLevelVisible) { + if (mt_end(mark) + || marktree_decor_level(mark) < kDecorLevelVisible) { goto next_mark; } - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark.id, false); - if (!item || !item->decor) { - goto next_mark; - } - Decoration *decor = item->decor; + Decoration decor = get_decor(mark); - mtpos_t endpos = marktree_lookup(buf->b_marktree, - mark.id|MARKTREE_END_FLAG, NULL); + mtpos_t endpos = marktree_get_altpos(buf->b_marktree, mark, NULL); if (endpos.row == -1) { - endpos.row = mark.row; - endpos.col = mark.col; + endpos = mark.pos; } - if (endpos.row < mark.row - || (endpos.row == mark.row && endpos.col <= mark.col)) { - if (!kv_size(decor->virt_text)) { + if (endpos.row < mark.pos.row + || (endpos.row == mark.pos.row && endpos.col <= mark.pos.col)) { + if (!kv_size(decor.virt_text)) { goto next_mark; } } - decor_add(state, mark.row, mark.col, endpos.row, endpos.col, - decor, false); + decor_add(state, mark.pos.row, mark.pos.col, endpos.row, endpos.col, + &decor, false); next_mark: marktree_itr_next(buf->b_marktree, state->itr); @@ -452,18 +431,18 @@ int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines) MarkTreeIter itr[1] = { 0 }; marktree_itr_get(buf->b_marktree, row, 0, itr); while (true) { - mtmark_t mark = marktree_itr_current(itr); - if (mark.row < 0 || mark.row >= end_row) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0 || mark.pos.row >= end_row) { break; - } else if (marktree_decor_level(mark.id) < kDecorLevelVirtLine) { + } else if (marktree_decor_level(mark) < kDecorLevelVirtLine) { goto next_mark; } - bool above = mark.row > (int)(lnum - 2); - ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark.id, false); - if (item && item->decor && item->decor->virt_lines_above == above) { - virt_lines += (int)kv_size(item->decor->virt_lines); + bool above = mark.pos.row > (int)(lnum - 2); + Decoration *decor = mark.decor_full; + if (decor && decor->virt_lines_above == above) { + virt_lines += (int)kv_size(decor->virt_lines); if (lines) { - kv_splice(*lines, item->decor->virt_lines); + kv_splice(*lines, decor->virt_lines); } } next_mark: diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index 611b4223da..02472d09e4 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -17,6 +17,8 @@ typedef enum { kVTRightAlign, } VirtTextPos; +EXTERN const char *const virt_text_pos_str[] INIT(= { "eol", "overlay", "win_col", "right_align" }); + typedef enum { kHlModeUnknown, kHlModeReplace, @@ -24,6 +26,8 @@ typedef enum { kHlModeBlend, } HlMode; +EXTERN const char *const hl_mode_str[] INIT(= { "", "replace", "combine", "blend" }); + typedef kvec_t(VirtTextChunk) VirtText; #define VIRTTEXT_EMPTY ((VirtText)KV_INITIAL_VALUE) @@ -42,7 +46,6 @@ struct Decoration { // TODO(bfredl): at some point turn this into FLAGS bool virt_text_hide; bool hl_eol; - bool shared; // shared decoration, don't free bool virt_lines_above; // TODO(bfredl): style, signs, etc DecorPriority priority; @@ -50,7 +53,7 @@ struct Decoration { int virt_text_width; // width of virt_text }; #define DECORATION_INIT { KV_INITIAL_VALUE, KV_INITIAL_VALUE, 0, kVTEndOfLine, kHlModeUnknown, \ - false, false, false, false, DECOR_PRIORITY_BASE, 0, 0 } + false, false, false, DECOR_PRIORITY_BASE, 0, 0 } typedef struct { int start_row; diff --git a/src/nvim/diff.c b/src/nvim/diff.c index 0233b3a5ab..80bd3229c6 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -37,8 +37,8 @@ #include "nvim/path.h" #include "nvim/screen.h" #include "nvim/strings.h" -#include "nvim/undo.h" #include "nvim/ui.h" +#include "nvim/undo.h" #include "nvim/vim.h" #include "nvim/window.h" #include "xdiff/xdiff.h" @@ -82,6 +82,14 @@ typedef struct { garray_T dout_ga; // used for internal diff } diffout_T; +// used for recording hunks from xdiff +typedef struct { + linenr_T lnum_orig; + long count_orig; + linenr_T lnum_new; + long count_new; +} diffhunk_T; + // two diff inputs and one result typedef struct { diffin_T dio_orig; // original file input @@ -674,11 +682,11 @@ void diff_redraw(bool dofold) } if (wp_other != NULL && curwin->w_p_scb) { if (used_max_fill_curwin) { - // The current window was set to used the maximum number of filler + // The current window was set to use the maximum number of filler // lines, may need to reduce them. diff_set_topline(wp_other, curwin); } else if (used_max_fill_other) { - // The other window was set to used the maximum number of filler + // The other window was set to use the maximum number of filler // lines, may need to reduce them. diff_set_topline(curwin, wp_other); } @@ -782,9 +790,14 @@ static int diff_write(buf_T *buf, diffin_T *din) // Always use 'fileformat' set to "unix". char_u *save_ff = buf->b_p_ff; buf->b_p_ff = vim_strsave((char_u *)FF_UNIX); + const bool save_lockmarks = cmdmod.lockmarks; + // Writing the buffer is an implementation detail of performing the diff, + // so it shouldn't update the '[ and '] marks. + cmdmod.lockmarks = true; int r = buf_write(buf, din->din_fname, NULL, (linenr_T)1, buf->b_ml.ml_line_count, NULL, false, false, false, true); + cmdmod.lockmarks = save_lockmarks; free_string_option(buf->b_p_ff); buf->b_p_ff = save_ff; return r; @@ -852,7 +865,7 @@ static void diff_try_update(diffio_T *dio, int idx_orig, exarg_T *eap) } // Read the diff output and add each entry to the diff list. - diff_read(idx_orig, idx_new, &dio->dio_diff); + diff_read(idx_orig, idx_new, dio); clear_diffin(&dio->dio_new); clear_diffout(&dio->dio_diff); @@ -1078,7 +1091,7 @@ static int diff_file_internal(diffio_T *diffio) emit_cfg.ctxlen = 0; // don't need any diff_context here emit_cb.priv = &diffio->dio_diff; - emit_cb.out_line = xdiff_out; + emit_cfg.hunk_func = xdiff_out; if (xdl_diff(&diffio->dio_orig.din_mmfile, &diffio->dio_new.din_mmfile, ¶m, &emit_cfg, &emit_cb) < 0) { @@ -1272,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"); } } @@ -1508,7 +1521,7 @@ void ex_diffoff(exarg_T *eap) diff_clear(curtab); } - // Remove "hor" from from 'scrollopt' if there are no diff windows left. + // Remove "hor" from 'scrollopt' if there are no diff windows left. if (!diffwin && (vim_strchr(p_sbo, 'h') != NULL)) { do_cmdline_cmd("set sbo-=hor"); } @@ -1519,20 +1532,20 @@ void ex_diffoff(exarg_T *eap) /// @param idx_orig idx of original file /// @param idx_new idx of new file /// @dout diff output -static void diff_read(int idx_orig, int idx_new, diffout_T *dout) +static void diff_read(int idx_orig, int idx_new, diffio_T *dio) { FILE *fd = NULL; int line_idx = 0; diff_T *dprev = NULL; diff_T *dp = curtab->tp_first_diff; diff_T *dn, *dpl; + diffout_T *dout = &dio->dio_diff; char_u linebuf[LBUFLEN]; // only need to hold the diff line char_u *line; long off; int i; - linenr_T lnum_orig, lnum_new; - long count_orig, count_new; int notset = true; // block "*dp" not set yet + diffhunk_T *hunk; enum { DIFF_ED, DIFF_UNIFIED, @@ -1549,70 +1562,79 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) } } + if (!dio->dio_internal) { + hunk = xmalloc(sizeof(*hunk)); + } + for (;;) { - if (fd == NULL) { + if (dio->dio_internal) { if (line_idx >= dout->dout_ga.ga_len) { break; // did last line } - line = ((char_u **)dout->dout_ga.ga_data)[line_idx++]; + hunk = ((diffhunk_T **)dout->dout_ga.ga_data)[line_idx++]; } else { - if (vim_fgets(linebuf, LBUFLEN, fd)) { - break; // end of file - } - line = linebuf; - } - - if (diffstyle == DIFF_NONE) { - // Determine diff style. - // ed like diff looks like this: - // {first}[,{last}]c{first}[,{last}] - // {first}a{first}[,{last}] - // {first}[,{last}]d{first} - // - // unified diff looks like this: - // --- file1 2018-03-20 13:23:35.783153140 +0100 - // +++ file2 2018-03-20 13:23:41.183156066 +0100 - // @@ -1,3 +1,5 @@ - if (isdigit(*line)) { - diffstyle = DIFF_ED; - } else if ((STRNCMP(line, "@@ ", 3) == 0)) { - diffstyle = DIFF_UNIFIED; - } else if ((STRNCMP(line, "--- ", 4) == 0) // -V501 - && (vim_fgets(linebuf, LBUFLEN, fd) == 0) // -V501 - && (STRNCMP(line, "+++ ", 4) == 0) - && (vim_fgets(linebuf, LBUFLEN, fd) == 0) // -V501 - && (STRNCMP(line, "@@ ", 3) == 0)) { - diffstyle = DIFF_UNIFIED; + if (fd == NULL) { + if (line_idx >= dout->dout_ga.ga_len) { + break; // did last line + } + line = ((char_u **)dout->dout_ga.ga_data)[line_idx++]; } else { - // Format not recognized yet, skip over this line. Cygwin diff - // may put a warning at the start of the file. - continue; + if (vim_fgets(linebuf, LBUFLEN, fd)) { + break; // end of file + } + line = linebuf; } - } - if (diffstyle == DIFF_ED) { - if (!isdigit(*line)) { - continue; // not the start of a diff block - } - if (parse_diff_ed(line, &lnum_orig, &count_orig, - &lnum_new, &count_new) == FAIL) { - continue; - } - } else { - assert(diffstyle == DIFF_UNIFIED); - if (STRNCMP(line, "@@ ", 3) != 0) { - continue; // not the start of a diff block + if (diffstyle == DIFF_NONE) { + // Determine diff style. + // ed like diff looks like this: + // {first}[,{last}]c{first}[,{last}] + // {first}a{first}[,{last}] + // {first}[,{last}]d{first} + // + // unified diff looks like this: + // --- file1 2018-03-20 13:23:35.783153140 +0100 + // +++ file2 2018-03-20 13:23:41.183156066 +0100 + // @@ -1,3 +1,5 @@ + if (isdigit(*line)) { + diffstyle = DIFF_ED; + } else if ((STRNCMP(line, "@@ ", 3) == 0)) { + diffstyle = DIFF_UNIFIED; + } else if ((STRNCMP(line, "--- ", 4) == 0) // -V501 + && (vim_fgets(linebuf, LBUFLEN, fd) == 0) // -V501 + && (STRNCMP(line, "+++ ", 4) == 0) + && (vim_fgets(linebuf, LBUFLEN, fd) == 0) // -V501 + && (STRNCMP(line, "@@ ", 3) == 0)) { + diffstyle = DIFF_UNIFIED; + } else { + // Format not recognized yet, skip over this line. Cygwin diff + // may put a warning at the start of the file. + continue; + } } - if (parse_diff_unified(line, &lnum_orig, &count_orig, - &lnum_new, &count_new) == FAIL) { - continue; + + if (diffstyle == DIFF_ED) { + if (!isdigit(*line)) { + continue; // not the start of a diff block + } + if (parse_diff_ed(line, hunk) == FAIL) { + continue; + } + } else { + assert(diffstyle == DIFF_UNIFIED); + if (STRNCMP(line, "@@ ", 3) != 0) { + continue; // not the start of a diff block + } + if (parse_diff_unified(line, hunk) == FAIL) { + continue; + } } } // Go over blocks before the change, for which orig and new are equal. // Copy blocks from orig to new. while (dp != NULL - && lnum_orig > dp->df_lnum[idx_orig] + dp->df_count[idx_orig]) { + && hunk->lnum_orig > dp->df_lnum[idx_orig] + dp->df_count[idx_orig]) { if (notset) { diff_copy_entry(dprev, dp, idx_orig, idx_new); } @@ -1622,19 +1644,19 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) } if ((dp != NULL) - && (lnum_orig <= dp->df_lnum[idx_orig] + dp->df_count[idx_orig]) - && (lnum_orig + count_orig >= dp->df_lnum[idx_orig])) { + && (hunk->lnum_orig <= dp->df_lnum[idx_orig] + dp->df_count[idx_orig]) + && (hunk->lnum_orig + hunk->count_orig >= dp->df_lnum[idx_orig])) { // New block overlaps with existing block(s). // First find last block that overlaps. for (dpl = dp; dpl->df_next != NULL; dpl = dpl->df_next) { - if (lnum_orig + count_orig < dpl->df_next->df_lnum[idx_orig]) { + if (hunk->lnum_orig + hunk->count_orig < dpl->df_next->df_lnum[idx_orig]) { break; } } // If the newly found block starts before the old one, set the // start back a number of lines. - off = dp->df_lnum[idx_orig] - lnum_orig; + off = dp->df_lnum[idx_orig] - hunk->lnum_orig; if (off > 0) { for (i = idx_orig; i < idx_new; ++i) { @@ -1642,15 +1664,15 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) dp->df_lnum[i] -= off; } } - dp->df_lnum[idx_new] = lnum_new; - dp->df_count[idx_new] = count_new; + dp->df_lnum[idx_new] = hunk->lnum_new; + dp->df_count[idx_new] = hunk->count_new; } else if (notset) { // new block inside existing one, adjust new block - dp->df_lnum[idx_new] = lnum_new + off; - dp->df_count[idx_new] = count_new - off; + dp->df_lnum[idx_new] = hunk->lnum_new + off; + dp->df_count[idx_new] = hunk->count_new - off; } else { // second overlap of new block with existing block - dp->df_count[idx_new] += count_new - count_orig + dp->df_count[idx_new] += hunk->count_new - hunk->count_orig + dpl->df_lnum[idx_orig] + dpl->df_count[idx_orig] - (dp->df_lnum[idx_orig] + @@ -1659,7 +1681,7 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) // Adjust the size of the block to include all the lines to the // end of the existing block or the new diff, whatever ends last. - off = (lnum_orig + count_orig) + off = (hunk->lnum_orig + hunk->count_orig) - (dpl->df_lnum[idx_orig] + dpl->df_count[idx_orig]); if (off < 0) { @@ -1691,10 +1713,10 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) // Allocate a new diffblock. dp = diff_alloc_new(curtab, dprev, dp); - dp->df_lnum[idx_orig] = lnum_orig; - dp->df_count[idx_orig] = count_orig; - dp->df_lnum[idx_new] = lnum_new; - dp->df_count[idx_new] = count_new; + dp->df_lnum[idx_orig] = hunk->lnum_orig; + dp->df_count[idx_orig] = hunk->count_orig; + dp->df_lnum[idx_new] = hunk->lnum_new; + dp->df_count[idx_new] = hunk->count_new; // Set values for other buffers, these must be equal to the // original buffer, otherwise there would have been a change @@ -1718,6 +1740,10 @@ static void diff_read(int idx_orig, int idx_new, diffout_T *dout) notset = true; } + if (!dio->dio_internal) { + xfree(hunk); + } + if (fd != NULL) { fclose(fd); } @@ -3026,8 +3052,7 @@ linenr_T diff_lnum_win(linenr_T lnum, win_T *wp) /// Handle an ED style diff line. /// Return FAIL if the line does not contain diff info. /// -static int parse_diff_ed(char_u *line, linenr_T *lnum_orig, long *count_orig, linenr_T *lnum_new, - long *count_new) +static int parse_diff_ed(char_u *line, diffhunk_T *hunk) { char_u *p; long f1, l1, f2, l2; @@ -3061,18 +3086,18 @@ static int parse_diff_ed(char_u *line, linenr_T *lnum_orig, long *count_orig, li } if (difftype == 'a') { - *lnum_orig = f1 + 1; - *count_orig = 0; + hunk->lnum_orig = f1 + 1; + hunk->count_orig = 0; } else { - *lnum_orig = f1; - *count_orig = l1 - f1 + 1; + hunk->lnum_orig = f1; + hunk->count_orig = l1 - f1 + 1; } if (difftype == 'd') { - *lnum_new = f2 + 1; - *count_new = 0; + hunk->lnum_new = f2 + 1; + hunk->count_new = 0; } else { - *lnum_new = f2; - *count_new = l2 - f2 + 1; + hunk->lnum_new = f2; + hunk->count_new = l2 - f2 + 1; } return OK; } @@ -3081,8 +3106,7 @@ static int parse_diff_ed(char_u *line, linenr_T *lnum_orig, long *count_orig, li /// Parses unified diff with zero(!) context lines. /// Return FAIL if there is no diff information in "line". /// -static int parse_diff_unified(char_u *line, linenr_T *lnum_orig, long *count_orig, - linenr_T *lnum_new, long *count_new) +static int parse_diff_unified(char_u *line, diffhunk_T *hunk) { char_u *p; long oldline, oldcount, newline, newcount; @@ -3120,10 +3144,10 @@ static int parse_diff_unified(char_u *line, linenr_T *lnum_orig, long *count_ori newline = 1; } - *lnum_orig = oldline; - *count_orig = oldcount; - *lnum_new = newline; - *count_new = newcount; + hunk->lnum_orig = oldline; + hunk->count_orig = oldcount; + hunk->lnum_new = newline; + hunk->count_new = newcount; return OK; } @@ -3135,25 +3159,16 @@ static int parse_diff_unified(char_u *line, linenr_T *lnum_orig, long *count_ori /// Callback function for the xdl_diff() function. /// Stores the diff output in a grow array. /// -static int xdiff_out(void *priv, mmbuffer_t *mb, int nbuf) +static int xdiff_out(long start_a, long count_a, long start_b, long count_b, void *priv) { diffout_T *dout = (diffout_T *)priv; - char_u *p; - - // The header line always comes by itself, text lines in at least two - // parts. We drop the text part. - if (nbuf > 1) { - return 0; - } - - // sanity check - if (STRNCMP(mb[0].ptr, "@@ ", 3) != 0) { - return 0; - } + diffhunk_T *p = xmalloc(sizeof(*p)); ga_grow(&dout->dout_ga, 1); - - p = vim_strnsave((char_u *)mb[0].ptr, mb[0].size); - ((char_u **)dout->dout_ga.ga_data)[dout->dout_ga.ga_len++] = p; + p->lnum_orig = start_a + 1; + p->count_orig = count_a; + p->lnum_new = start_b + 1; + p->count_new = count_b; + ((diffhunk_T **)dout->dout_ga.ga_data)[dout->dout_ga.ga_len++] = p; return 0; } diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 2e3eec3642..00ffa7cba1 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -260,7 +260,7 @@ static colnr_T Insstart_blank_vcol; // vcol for first inserted blank static bool update_Insstart_orig = true; // set Insstart_orig to Insstart static char_u *last_insert = NULL; // the text of the previous insert, - // K_SPECIAL and CSI are escaped + // K_SPECIAL is escaped static int last_insert_skip; // nr of chars in front of previous insert static int new_insert_skip; // nr of chars in front of current insert static int did_restart_edit; // "restart_edit" when calling edit() @@ -663,8 +663,12 @@ static int insert_execute(VimState *state, int key) InsertState *const s = (InsertState *)state; if (stop_insert_mode) { // Insert mode ended, possibly from a callback. + if (key != K_IGNORE && key != K_NOP) { + vungetc(key); + } s->count = 0; s->nomove = true; + ins_compl_prep(ESC); return 0; } @@ -909,7 +913,7 @@ static int insert_handle_key(InsertState *s) ins_ctrl_o(); // don't move the cursor left when 'virtualedit' has "onemore". - if (ve_flags & VE_ONEMORE) { + if (get_ve_flags() & VE_ONEMORE) { ins_at_eol = false; s->nomove = true; } @@ -1076,13 +1080,21 @@ static int insert_handle_key(InsertState *s) case K_COMMAND: // some command do_cmdline(NULL, getcmdkeycmd, NULL, 0); + goto check_pum; + + case K_LUA: + map_execute_lua(); check_pum: + // nvim_select_popupmenu_item() can be called from the handling of + // K_EVENT, K_COMMAND, or K_LUA. // TODO(bfredl): Not entirely sure this indirection is necessary // but doing like this ensures using nvim_select_popupmenu_item is // equivalent to selecting the item with a typed key. if (pum_want.active) { if (pum_visible()) { + // Set this to NULL so that ins_complete() will update the message. + edit_submode_extra = NULL; insert_do_complete(s); if (pum_want.finish) { // accept the item and stop completion @@ -1600,8 +1612,8 @@ static void ins_ctrl_v(void) */ static int pc_status; #define PC_STATUS_UNSET 0 // pc_bytes was not set -#define PC_STATUS_RIGHT 1 // right halve of double-wide char -#define PC_STATUS_LEFT 2 // left halve of double-wide char +#define PC_STATUS_RIGHT 1 // right half of double-wide char +#define PC_STATUS_LEFT 2 // left half of double-wide char #define PC_STATUS_SET 3 // pc_bytes was filled static char_u pc_bytes[MB_MAXBYTES + 1]; // saved bytes static int pc_attr; @@ -3616,7 +3628,7 @@ static bool ins_compl_prep(int c) // Ignore end of Select mode mapping and mouse scroll buttons. if (c == K_SELECT || c == K_MOUSEDOWN || c == K_MOUSEUP || c == K_MOUSELEFT || c == K_MOUSERIGHT || c == K_EVENT - || c == K_COMMAND) { + || c == K_COMMAND || c == K_LUA) { return retval; } @@ -4982,7 +4994,7 @@ void ins_compl_check_keys(int frequency, int in_compl_func) */ static int ins_compl_key2dir(int c) { - if (c == K_EVENT || c == K_COMMAND) { + if (c == K_EVENT || c == K_COMMAND || c == K_LUA) { return pum_want.item < pum_selected_item ? BACKWARD : FORWARD; } if (c == Ctrl_P || c == Ctrl_L @@ -5012,7 +5024,7 @@ static int ins_compl_key2count(int c) { int h; - if (c == K_EVENT || c == K_COMMAND) { + if (c == K_EVENT || c == K_COMMAND || c == K_LUA) { int offset = pum_want.item - pum_selected_item; return abs(offset); } @@ -5046,6 +5058,7 @@ static bool ins_compl_use_match(int c) return false; case K_EVENT: case K_COMMAND: + case K_LUA: return pum_want.active && pum_want.insert; } return true; @@ -5624,8 +5637,12 @@ int get_literal(void) i = 0; for (;;) { nc = plain_vgetc(); - if (!(State & CMDLINE) - && MB_BYTE2LEN_CHECK(nc) == 1) { + if ((mod_mask & ~MOD_MASK_SHIFT) != 0) { + // A character with non-Shift modifiers should not be a valid + // character for i_CTRL-V_digit. + break; + } + if (!(State & CMDLINE) && MB_BYTE2LEN_CHECK(nc) == 1) { add_to_showcmd(nc); } if (nc == 'x' || nc == 'X') { @@ -5691,6 +5708,8 @@ int get_literal(void) --no_mapping; if (nc) { vungetc(nc); + // A character typed with i_CTRL-V_digit cannot have modifiers. + mod_mask = 0; } got_int = false; // CTRL-C typed after CTRL-V is not an interrupt return cc; @@ -6002,6 +6021,7 @@ static void internal_format(int textwidth, int second_indent, int flags, int for char_u *saved_text = NULL; colnr_T col; colnr_T end_col; + bool did_do_comment = false; virtcol = get_nolist_virtcol() + char2cells(c != NUL ? c : gchar_cursor()); @@ -6117,8 +6137,7 @@ static void internal_format(int textwidth, int second_indent, int flags, int for if (curwin->w_cursor.col <= (colnr_T)wantcol) { break; } - } else if ((cc >= 0x100 || !utf_allow_break_before(cc)) - && fo_multibyte) { + } else if ((cc >= 0x100 || !utf_allow_break_before(cc)) && fo_multibyte) { int ncc; bool allow_break; @@ -6275,11 +6294,18 @@ static void internal_format(int textwidth, int second_indent, int flags, int for + (fo_white_par ? OPENLINE_KEEPTRAIL : 0) + (do_comments ? OPENLINE_DO_COM : 0) + ((flags & INSCHAR_COM_LIST) ? OPENLINE_COM_LIST : 0), - ((flags & INSCHAR_COM_LIST) ? second_indent : old_indent)); + ((flags & INSCHAR_COM_LIST) ? second_indent : old_indent), + &did_do_comment); if (!(flags & INSCHAR_COM_LIST)) { old_indent = 0; } + // If a comment leader was inserted, may also do this on a following + // line. + if (did_do_comment) { + no_leader = false; + } + replace_offset = 0; if (first_line) { if (!(flags & INSCHAR_COM_LIST)) { @@ -6810,7 +6836,7 @@ void free_last_insert(void) /// Add character "c" to buffer "s" /// -/// Escapes the special meaning of K_SPECIAL and CSI, handles multi-byte +/// Escapes the special meaning of K_SPECIAL, handles multi-byte /// characters. /// /// @param[in] c Character to add. @@ -6824,7 +6850,7 @@ char_u *add_char2buf(int c, char_u *s) const int len = utf_char2bytes(c, temp); for (int i = 0; i < len; i++) { c = temp[i]; - // Need to escape K_SPECIAL and CSI like in the typeahead buffer. + // Need to escape K_SPECIAL like in the typeahead buffer. if (c == K_SPECIAL) { *s++ = K_SPECIAL; *s++ = KS_SPECIAL; @@ -6898,8 +6924,7 @@ int oneright(void) // move "l" bytes right, but don't end up on the NUL, unless 'virtualedit' // contains "onemore". - if (ptr[l] == NUL - && (ve_flags & VE_ONEMORE) == 0) { + if (ptr[l] == NUL && (get_ve_flags() & VE_ONEMORE) == 0) { return FAIL; } curwin->w_cursor.col += l; @@ -7106,9 +7131,7 @@ int stuff_inserted(int c, long count, int no_esc) stuffReadbuff((const char *)ptr); // A trailing "0" is inserted as "<C-V>048", "^" as "<C-V>^". if (last) { - stuffReadbuff((last == '0' - ? "\026\060\064\070" - : "\026^")); + stuffReadbuff(last == '0' ? "\026\060\064\070" : "\026^"); } } while (--count > 0); @@ -8021,7 +8044,7 @@ static bool ins_esc(long *count, int cmdchar, bool nomove) && !VIsual_active )) && !revins_on) { - if (curwin->w_cursor.coladd > 0 || ve_flags == VE_ALL) { + if (curwin->w_cursor.coladd > 0 || get_ve_flags() == VE_ALL) { oneleft(); if (restart_edit != NUL) { curwin->w_cursor.coladd++; @@ -8274,6 +8297,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) int in_indent; int oldState; int cpc[MAX_MCO]; // composing characters + bool call_fix_indent = false; // can't delete anything in an empty file // can't backup past first character in buffer @@ -8417,6 +8441,8 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) beginline(BL_WHITE); if (curwin->w_cursor.col < save_col) { mincol = curwin->w_cursor.col; + // should now fix the indent to match with the previous line + call_fix_indent = true; } curwin->w_cursor.col = save_col; } @@ -8551,6 +8577,11 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) if (curwin->w_cursor.col <= 1) { did_ai = false; } + + if (call_fix_indent) { + fix_indent(); + } + // It's a little strange to put backspaces into the redo // buffer, but it makes auto-indent a lot easier to deal // with. @@ -9165,7 +9196,7 @@ static bool ins_eol(int c) AppendToRedobuff(NL_STR); bool i = open_line(FORWARD, has_format_option(FO_RET_COMS) ? OPENLINE_DO_COM : 0, - old_indent); + old_indent, NULL); old_indent = 0; can_cindent = true; // When inserting a line the cursor line must never be in a closed fold. diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 86384bc5b2..d95b9560c2 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" @@ -69,6 +70,7 @@ static char *e_nowhitespace static char *e_invalwindow = N_("E957: Invalid window number"); static char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s"); static char *e_write2 = N_("E80: Error while writing: %s"); +static char *e_string_list_or_blob_required = N_("E1098: String, List or Blob required"); // TODO(ZyX-I): move to eval/executor static char *e_letwrong = N_("E734: Wrong variable type for %s="); @@ -112,9 +114,11 @@ typedef struct { int fi_semicolon; // TRUE if ending in '; var]' int fi_varcount; // nr of variables in the list listwatch_T fi_lw; // keep an eye on the item used. - list_T *fi_list; // list being used + list_T *fi_list; // list being used int fi_bi; // index of blob blob_T *fi_blob; // blob being used + char_u *fi_string; // copy of string being used + int fi_byte_idx; // byte index in fi_string } forinfo_T; // values for vv_flags: @@ -763,6 +767,15 @@ static int eval1_emsg(char_u **arg, typval_T *rettv, bool evaluate) return ret; } +/// @return whether a typval is a valid expression to pass to eval_expr_typval() +/// or eval_expr_to_bool(). An empty string returns false; +bool eval_expr_valid_arg(const typval_T *const tv) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_CONST +{ + return tv->v_type != VAR_UNKNOWN + && (tv->v_type != VAR_STRING || (tv->vval.v_string != NULL && *tv->vval.v_string != NUL)); +} + int eval_expr_typval(const typval_T *expr, typval_T *argv, int argc, typval_T *rettv) FUNC_ATTR_NONNULL_ARG(1, 2, 4) { @@ -2641,8 +2654,15 @@ void *eval_for_line(const char_u *arg, bool *errp, char_u **nextcmdp, int skip) fi->fi_blob = btv.vval.v_blob; } tv_clear(&tv); + } else if (tv.v_type == VAR_STRING) { + fi->fi_byte_idx = 0; + fi->fi_string = tv.vval.v_string; + tv.vval.v_string = NULL; + if (fi->fi_string == NULL) { + fi->fi_string = vim_strsave((char_u *)""); + } } else { - emsg(_(e_listblobreq)); + emsg(_(e_string_list_or_blob_required)); tv_clear(&tv); } } @@ -2679,6 +2699,22 @@ bool next_for_item(void *fi_void, char_u *arg) fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK; } + if (fi->fi_string != NULL) { + const int len = utfc_ptr2len(fi->fi_string + fi->fi_byte_idx); + if (len == 0) { + return false; + } + typval_T tv; + tv.v_type = VAR_STRING; + tv.v_lock = VAR_FIXED; + tv.vval.v_string = vim_strnsave(fi->fi_string + fi->fi_byte_idx, len); + fi->fi_byte_idx += len; + const int result + = ex_let_vars(arg, &tv, true, fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK; + xfree(tv.vval.v_string); + return result; + } + listitem_T *item = fi->fi_lw.lw_item; if (item == NULL) { return false; @@ -2698,12 +2734,16 @@ void free_for_info(void *fi_void) { forinfo_T *fi = (forinfo_T *)fi_void; - if (fi != NULL && fi->fi_list != NULL) { + if (fi == NULL) { + return; + } + if (fi->fi_list != NULL) { tv_list_watch_remove(fi->fi_list, &fi->fi_lw); tv_list_unref(fi->fi_list); - } - if (fi != NULL && fi->fi_blob != NULL) { + } else if (fi->fi_blob != NULL) { tv_blob_unref(fi->fi_blob); + } else { + xfree(fi->fi_string); } xfree(fi); } @@ -3218,9 +3258,8 @@ char_u *get_user_var_name(expand_T *xp, int idx) // b: variables // In cmdwin, the alternative buffer should be used. - hashtab_T *ht = (cmdwin_type != 0 && get_cmdline_type() == NUL) - ? &prevwin->w_buffer->b_vars->dv_hashtab - : &curbuf->b_vars->dv_hashtab; + hashtab_T *ht + = 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; @@ -3235,9 +3274,7 @@ char_u *get_user_var_name(expand_T *xp, int idx) // w: variables // In cmdwin, the alternative window should be used. - ht = (cmdwin_type != 0 && get_cmdline_type() == NUL) - ? &prevwin->w_vars->dv_hashtab - : &curwin->w_vars->dv_hashtab; + ht = is_in_cmdwin() ? &prevwin->w_vars->dv_hashtab : &curwin->w_vars->dv_hashtab; if (wdone < ht->ht_used) { if (wdone++ == 0) { hi = ht->ht_array; @@ -4402,7 +4439,7 @@ static int eval_lambda(char_u **const arg, typval_T *const rettv, const bool eva rettv->v_type = VAR_UNKNOWN; int ret = get_lambda_tv(arg, rettv, evaluate); - if (ret == NOTDONE) { + if (ret != OK) { return FAIL; } else if (**arg != '(') { if (verbose) { @@ -6468,6 +6505,10 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) if (argvars[0].v_type == VAR_DICT) { vimvars[VV_KEY].vv_type = VAR_STRING; + const VarLockStatus prev_lock = d->dv_lock; + if (map && d->dv_lock == VAR_UNLOCKED) { + d->dv_lock = VAR_LOCKED; + } ht = &d->dv_hashtab; hash_lock(ht); todo = (int)ht->ht_used; @@ -6498,6 +6539,7 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) } } hash_unlock(ht); + d->dv_lock = prev_lock; } else if (argvars[0].v_type == VAR_BLOB) { vimvars[VV_KEY].vv_type = VAR_NUMBER; @@ -6530,6 +6572,10 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) assert(argvars[0].v_type == VAR_LIST); vimvars[VV_KEY].vv_type = VAR_NUMBER; + const VarLockStatus prev_lock = tv_list_locked(l); + if (map && tv_list_locked(l) == VAR_UNLOCKED) { + tv_list_set_lock(l, VAR_LOCKED); + } for (listitem_T *li = tv_list_first(l); li != NULL;) { if (map && var_check_lock(TV_LIST_ITEM_TV(li)->v_lock, arg_errmsg, @@ -6548,6 +6594,7 @@ void filter_map(typval_T *argvars, typval_T *rettv, int map) } idx++; } + tv_list_set_lock(l, prev_lock); } restore_vimvar(VV_KEY, &save_key); @@ -6956,10 +7003,9 @@ win_T *find_tabwin(typval_T *wvp, typval_T *tvp) /// @param off 1 for gettabwinvar() void getwinvar(typval_T *argvars, typval_T *rettv, int off) { - win_T *win, *oldcurwin; + win_T *win; dictitem_T *v; tabpage_T *tp = NULL; - tabpage_T *oldtabpage = NULL; bool done = false; if (off == 1) { @@ -6979,8 +7025,8 @@ void getwinvar(typval_T *argvars, typval_T *rettv, int off) // otherwise the window is not valid. Only do this when needed, // autocommands get blocked. bool need_switch_win = tp != curtab || win != curwin; - if (!need_switch_win - || switch_win(&oldcurwin, &oldtabpage, win, tp, true) == OK) { + switchwin_T switchwin; + if (!need_switch_win || switch_win(&switchwin, win, tp, true) == OK) { if (*varname == '&') { if (varname[1] == NUL) { // get all window-local options in a dict @@ -7008,7 +7054,7 @@ void getwinvar(typval_T *argvars, typval_T *rettv, int off) if (need_switch_win) { // restore previous notion of curwin - restore_win(oldcurwin, oldtabpage, true); + restore_win(&switchwin, true); } } emsg_off--; @@ -7299,12 +7345,19 @@ void mapblock_fill_dict(dict_T *const dict, const mapblock_T *const mp, long buf noremap_value = mp->m_noremap == REMAP_SCRIPT ? 2 : !!mp->m_noremap; } - if (compatible) { - tv_dict_add_str(dict, S_LEN("rhs"), (const char *)mp->m_orig_str); + if (mp->m_luaref != LUA_NOREF) { + tv_dict_add_nr(dict, S_LEN("callback"), mp->m_luaref); } else { - tv_dict_add_allocated_str(dict, S_LEN("rhs"), - str2special_save((const char *)mp->m_str, false, - true)); + if (compatible) { + tv_dict_add_str(dict, S_LEN("rhs"), (const char *)mp->m_orig_str); + } else { + tv_dict_add_allocated_str(dict, S_LEN("rhs"), + str2special_save((const char *)mp->m_str, false, + true)); + } + } + if (mp->m_desc != NULL) { + tv_dict_add_allocated_str(dict, S_LEN("desc"), xstrdup(mp->m_desc)); } tv_dict_add_allocated_str(dict, S_LEN("lhs"), lhs); tv_dict_add_nr(dict, S_LEN("noremap"), noremap_value); @@ -7503,11 +7556,9 @@ void setwinvar(typval_T *argvars, typval_T *rettv, int off) typval_T *varp = &argvars[off + 2]; if (win != NULL && varname != NULL && varp != NULL) { - win_T *save_curwin; - tabpage_T *save_curtab; bool need_switch_win = tp != curtab || win != curwin; - if (!need_switch_win - || switch_win(&save_curwin, &save_curtab, win, tp, true) == OK) { + switchwin_T switchwin; + if (!need_switch_win || switch_win(&switchwin, win, tp, true) == OK) { if (*varname == '&') { long numval; bool error = false; @@ -7529,7 +7580,7 @@ void setwinvar(typval_T *argvars, typval_T *rettv, int off) } } if (need_switch_win) { - restore_win(save_curwin, save_curtab, true); + restore_win(&switchwin, true); } } } @@ -7696,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) { @@ -7725,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; @@ -7736,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; @@ -8140,6 +8200,66 @@ char *save_tv_as_string(typval_T *tv, ptrdiff_t *const len, bool endnl) return ret; } +/// Convert the specified byte index of line 'lnum' in buffer 'buf' to a +/// character index. Works only for loaded buffers. Returns -1 on failure. +/// The index of the first byte and the first character is zero. +int buf_byteidx_to_charidx(buf_T *buf, int lnum, int byteidx) +{ + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return -1; + } + + if (lnum > buf->b_ml.ml_line_count) { + lnum = buf->b_ml.ml_line_count; + } + + char_u *str = ml_get_buf(buf, lnum, false); + + if (*str == NUL) { + return 0; + } + + // count the number of characters + char_u *t = str; + int count; + for (count = 0; *t != NUL && t <= str + byteidx; count++) { + t += utfc_ptr2len(t); + } + + // In insert mode, when the cursor is at the end of a non-empty line, + // byteidx points to the NUL character immediately past the end of the + // string. In this case, add one to the character count. + if (*t == NUL && byteidx != 0 && t == str + byteidx) { + count++; + } + + return count - 1; +} + +/// Convert the specified character index of line 'lnum' in buffer 'buf' to a +/// byte index. Works only for loaded buffers. Returns -1 on failure. +/// The index of the first byte and the first character is zero. +int buf_charidx_to_byteidx(buf_T *buf, int lnum, int charidx) +{ + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return -1; + } + + if (lnum > buf->b_ml.ml_line_count) { + lnum = buf->b_ml.ml_line_count; + } + + char_u *str = ml_get_buf(buf, lnum, false); + + // Convert the character offset to a byte offset + char_u *t = str; + while (*t != NUL && --charidx > 0) { + t += utfc_ptr2len(t); + } + + return t - str; +} + /// Translate a VimL object into a position /// /// Accepts VAR_LIST and VAR_STRING objects. Does not give an error for invalid @@ -8148,9 +8268,11 @@ char *save_tv_as_string(typval_T *tv, ptrdiff_t *const len, bool endnl) /// @param[in] tv Object to translate. /// @param[in] dollar_lnum True when "$" is last line. /// @param[out] ret_fnum Set to fnum for marks. +/// @param[in] charcol True to return character column. /// /// @return Pointer to position or NULL in case of error (e.g. invalid type). -pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret_fnum) +pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret_fnum, + const bool charcol) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { static pos_T pos; @@ -8180,7 +8302,11 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret if (error) { return NULL; } - len = (long)STRLEN(ml_get(pos.lnum)); + if (charcol) { + len = mb_charlen(ml_get(pos.lnum)); + } else { + len = STRLEN(ml_get(pos.lnum)); + } // We accept "$" for the column number: last column. li = tv_list_find(l, 1L); @@ -8211,19 +8337,31 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret return NULL; } if (name[0] == '.') { // Cursor. - return &curwin->w_cursor; + pos = curwin->w_cursor; + if (charcol) { + pos.col = buf_byteidx_to_charidx(curbuf, pos.lnum, pos.col); + } + return &pos; } if (name[0] == 'v' && name[1] == NUL) { // Visual start. if (VIsual_active) { - return &VIsual; + pos = VIsual; + } else { + pos = curwin->w_cursor; + } + if (charcol) { + pos.col = buf_byteidx_to_charidx(curbuf, pos.lnum, pos.col); } - return &curwin->w_cursor; + return &pos; } if (name[0] == '\'') { // Mark. pp = getmark_buf_fnum(curbuf, (uint8_t)name[1], false, ret_fnum); if (pp == NULL || pp == (pos_T *)-1 || pp->lnum <= 0) { return NULL; } + if (charcol) { + pp->col = buf_byteidx_to_charidx(curbuf, pp->lnum, pp->col); + } return pp; } @@ -8249,22 +8387,24 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret pos.col = 0; } else { pos.lnum = curwin->w_cursor.lnum; - pos.col = (colnr_T)STRLEN(get_cursor_line_ptr()); + if (charcol) { + pos.col = (colnr_T)mb_charlen(get_cursor_line_ptr()); + } else { + pos.col = (colnr_T)STRLEN(get_cursor_line_ptr()); + } } return &pos; } return NULL; } -/* - * Convert list in "arg" into a position and optional file number. - * When "fnump" is NULL there is no file number, only 3 items. - * Note that the column is passed on as-is, the caller may want to decrement - * it to use 1 for the first column. - * Return FAIL when conversion is not possible, doesn't check the position for - * validity. - */ -int list2fpos(typval_T *arg, pos_T *posp, int *fnump, colnr_T *curswantp) +/// Convert list in "arg" into a position and optional file number. +/// When "fnump" is NULL there is no file number, only 3 items. +/// Note that the column is passed on as-is, the caller may want to decrement +/// it to use 1 for the first column. +/// Return FAIL when conversion is not possible, doesn't check the position for +/// validity. +int list2fpos(typval_T *arg, pos_T *posp, int *fnump, colnr_T *curswantp, bool charcol) { list_T *l; long i = 0; @@ -8300,6 +8440,15 @@ int list2fpos(typval_T *arg, pos_T *posp, int *fnump, colnr_T *curswantp) if (n < 0) { return FAIL; } + // If character position is specified, then convert to byte position + if (charcol) { + // Get the text for the specified line in a loaded buffer + buf_T *buf = buflist_findnr(fnump == NULL ? curbuf->b_fnum : *fnump); + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return FAIL; + } + n = buf_charidx_to_byteidx(buf, posp->lnum, n) + 1; + } posp->col = n; n = tv_list_find_nr(l, i, NULL); // off @@ -8939,7 +9088,7 @@ static bool tv_is_luafunc(typval_T *tv) const char *skip_luafunc_name(const char *p) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { - while (ASCII_ISALNUM(*p) || *p == '_' || *p == '.' || *p == '\'') { + while (ASCII_ISALNUM(*p) || *p == '_' || *p == '-' || *p == '.' || *p == '\'') { p++; } return p; @@ -9220,10 +9369,31 @@ static hashtab_T *find_var_ht_dict(const char *name, const size_t name_len, cons } else if (*name == 'l' && funccal != NULL) { // local variable *d = &funccal->l_vars; } else if (*name == 's' // script variable - && (current_sctx.sc_sid > 0 || current_sctx.sc_sid == SID_STR) + && (current_sctx.sc_sid > 0 || current_sctx.sc_sid == SID_STR + || current_sctx.sc_sid == SID_LUA) && current_sctx.sc_sid <= ga_scripts.ga_len) { // For anonymous scripts without a script item, create one now so script vars can be used - if (current_sctx.sc_sid == SID_STR) { + if (current_sctx.sc_sid == SID_LUA) { + // try to resolve lua filename & line no so it can be shown in lastset messages. + nlua_set_sctx(¤t_sctx); + if (current_sctx.sc_sid != SID_LUA) { + // Great we have valid location. Now here this out we'll create a new + // script context with the name and lineno of this one. why ? + // for behavioral consistency. With this different anonymous exec from + // same file can't access each others script local stuff. We need to do + // this all other cases except this will act like that otherwise. + const LastSet last_set = (LastSet){ + .script_ctx = current_sctx, + .channel_id = LUA_INTERNAL_CALL, + }; + bool should_free; + // should_free is ignored as script_sctx will be resolved to a fnmae + // & new_script_item will consume it. + char_u *sc_name = get_scriptname(last_set, &should_free); + new_script_item(sc_name, ¤t_sctx.sc_sid); + } + } + if (current_sctx.sc_sid == SID_STR || current_sctx.sc_sid == SID_LUA) { new_script_item(NULL, ¤t_sctx.sc_sid); } *d = &SCRIPT_SV(current_sctx.sc_sid)->sv_dict; @@ -10508,12 +10678,13 @@ int modify_fname(char_u *src, bool tilde_file, size_t *usedlen, char_u **fnamep, char_u *s, *p, *pbuf; char_u dirname[MAXPATHL]; int c; - int has_fullname = 0; + bool has_fullname = false; + bool has_homerelative = false; repeat: // ":p" - full path/file_name if (src[*usedlen] == ':' && src[*usedlen + 1] == 'p') { - has_fullname = 1; + has_fullname = true; valid |= VALID_PATH; *usedlen += 2; @@ -10582,8 +10753,8 @@ repeat: } pbuf = NULL; // Need full path first (use expand_env() to remove a "~/") - if (!has_fullname) { - if (c == '.' && **fnamep == '~') { + if (!has_fullname && !has_homerelative) { + if (**fnamep == '~') { p = pbuf = expand_env_save(*fnamep); } else { p = pbuf = (char_u *)FullName_save((char *)*fnamep, FALSE); @@ -10592,18 +10763,33 @@ repeat: p = *fnamep; } - has_fullname = 0; + has_fullname = false; if (p != NULL) { if (c == '.') { os_dirname(dirname, MAXPATHL); - s = path_shorten_fname(p, dirname); - if (s != NULL) { - *fnamep = s; - if (pbuf != NULL) { - xfree(*bufp); // free any allocated file name - *bufp = pbuf; - pbuf = NULL; + if (has_homerelative) { + s = vim_strsave(dirname); + home_replace(NULL, s, dirname, MAXPATHL, true); + xfree(s); + } + size_t namelen = STRLEN(dirname); + + // Do not call shorten_fname() here since it removes the prefix + // even though the path does not have a prefix. + if (fnamencmp(p, dirname, namelen) == 0) { + p += namelen; + if (vim_ispathsep(*p)) { + while (*p && vim_ispathsep(*p)) { + p++; + } + *fnamep = p; + if (pbuf != NULL) { + // free any allocated file name + xfree(*bufp); + *bufp = pbuf; + pbuf = NULL; + } } } } else { @@ -10614,6 +10800,7 @@ repeat: *fnamep = s; xfree(*bufp); *bufp = s; + has_homerelative = true; } } xfree(pbuf); @@ -10990,10 +11177,7 @@ typval_T eval_call_provider(char *provider, char *method, list_T *arguments, boo bool eval_has_provider(const char *feat) { if (!strequal(feat, "clipboard") - && !strequal(feat, "python") && !strequal(feat, "python3") - && !strequal(feat, "python_compiled") - && !strequal(feat, "python_dynamic") && !strequal(feat, "python3_compiled") && !strequal(feat, "python3_dynamic") && !strequal(feat, "perl") diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index e445a08227..05e91a658f 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -71,6 +71,7 @@ return { chanclose={args={1, 2}}, chansend={args=2}, char2nr={args={1, 2}, base=1}, + charcol={args=1, base=1}, charidx={args={2, 3}, base=1}, chdir={args=1, base=1}, cindent={args=1, base=1}, @@ -144,6 +145,7 @@ return { getchangelist={args={0, 1}, base=1}, getchar={args={0, 1}}, getcharmod={}, + getcharpos={args=1, base=1}, getcharsearch={}, getcharstr={args={0, 1}}, getcmdline={}, @@ -151,7 +153,8 @@ return { getcmdtype={}, getcmdwintype={}, getcompletion={args={2, 3}, base=1}, - getcurpos={}, + getcurpos={args={0, 1}, base=1}, + getcursorcharpos={args={0, 1}, base=1}, getcwd={args={0, 2}, base=1}, getenv={args=1, base=1}, getfontname={args={0, 1}}, @@ -246,6 +249,8 @@ return { matcharg={args=1, base=1}, matchdelete={args={1, 2}, base=1}, matchend={args={2, 4}, base=1}, + matchfuzzy={args={2, 3}, base=1}, + matchfuzzypos={args={2, 3}, base=1}, matchlist={args={2, 4}, base=1}, matchstr={args={2, 4}, base=1}, matchstrpos={args={2,4}, base=1}, @@ -259,7 +264,7 @@ return { nextnonblank={args=1, base=1}, nr2char={args={1, 2}, base=1}, ['or']={args=2, base=1}, - pathshorten={args=1, base=1}, + pathshorten={args={1, 2}, base=1}, pow={args=2, base=1}, prevnonblank={args=1, base=1}, printf={args=varargs(1), base=2}, @@ -270,12 +275,14 @@ return { pum_getpos={}, pumvisible={}, py3eval={args=1, base=1}, - pyeval={args=1, base=1}, - pyxeval={args=1, base=1}, + pyeval={args=1, base=1, func="f_py3eval"}, + pyxeval={args=1, base=1, func="f_py3eval"}, perleval={args=1, base=1}, + rand={args={0, 1}, base=1}, range={args={1, 3}, base=1}, readdir={args={1, 2}, base=1}, readfile={args={1, 3}, base=1}, + reduce={args={2, 3}, base=1}, reg_executing={}, reg_recording={}, reg_recorded={}, @@ -300,19 +307,21 @@ return { screenpos={args=3, base=1}, screenrow={}, screenstring={args=2, base=1}, - search={args={1, 4}, base=1}, + search={args={1, 5}, base=1}, searchcount={args={0, 1}, base=1}, searchdecl={args={1, 3}, base=1}, searchpair={args={3, 7}}, searchpairpos={args={3, 7}}, - searchpos={args={1, 4}, base=1}, + searchpos={args={1, 5}, base=1}, serverlist={}, serverstart={args={0, 1}}, serverstop={args=1}, setbufline={args=3, base=3}, setbufvar={args=3, base=3}, + setcharpos={args=2, base=2}, setcharsearch={args=1, base=1}, setcmdpos={args=1, base=1}, + setcursorcharpos={args={1, 3}, base=1}, setenv={args=2, base=2}, setfperm={args=2, base=1}, setline={args=2, base=2}, @@ -348,6 +357,7 @@ return { spellsuggest={args={1, 3}, base=1}, split={args={1, 3}, base=1}, sqrt={args=1, base=1, func="float_op_wrapper", data="&sqrt"}, + srand={args={0, 1}, base=1}, stdpath={args=1}, str2float={args=1, base=1}, str2list={args={1, 2}, base=1}, @@ -413,6 +423,8 @@ return { win_gotoid={args=1, base=1}, win_id2tabwin={args=1, base=1}, win_id2win={args=1, base=1}, + win_move_separator={args=2, base=1}, + win_move_statusline={args=2, base=1}, win_screenpos={args=1, base=1}, win_splitmove={args={2, 3}, base=1}, winbufnr={args=1, base=1}, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 32026282cf..49dde537c3 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -22,6 +22,7 @@ #include "nvim/eval/encode.h" #include "nvim/eval/executor.h" #include "nvim/eval/funcs.h" +#include "nvim/eval/typval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_cmds2.h" #include "nvim/ex_docmd.h" @@ -97,9 +98,9 @@ PRAGMA_DIAG_POP #endif -static char *e_listarg = N_("E686: Argument of %s must be a List"); static char *e_listblobarg = N_("E899: Argument of %s must be a List or Blob"); static char *e_invalwindow = N_("E957: Invalid window number"); +static char *e_reduceempty = N_("E998: Reduce of an empty %s with no initial value"); /// Dummy va_list for passing to vim_snprintf /// @@ -893,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 { @@ -1019,6 +1021,49 @@ static void f_char2nr(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = utf_ptr2char((const char_u *)tv_get_string(&argvars[0])); } +/// Get the current cursor column and store it in 'rettv'. If 'charcol' is true, +/// returns the character index of the column. Otherwise, returns the byte index +/// of the column. +static void get_col(typval_T *argvars, typval_T *rettv, bool charcol) +{ + colnr_T col = 0; + pos_T *fp; + int fnum = curbuf->b_fnum; + + fp = var2fpos(&argvars[0], false, &fnum, charcol); + if (fp != NULL && fnum == curbuf->b_fnum) { + if (fp->col == MAXCOL) { + // '> can be MAXCOL, get the length of the line then + if (fp->lnum <= curbuf->b_ml.ml_line_count) { + col = (colnr_T)STRLEN(ml_get(fp->lnum)) + 1; + } else { + col = MAXCOL; + } + } else { + col = fp->col + 1; + // col(".") when the cursor is on the NUL at the end of the line + // because of "coladd" can be seen as an extra column. + if (virtual_active() && fp == &curwin->w_cursor) { + char_u *p = get_cursor_pos_ptr(); + if (curwin->w_cursor.coladd >= + (colnr_T)win_chartabsize(curwin, p, curwin->w_virtcol - curwin->w_cursor.coladd)) { + int l; + if (*p != NUL && p[(l = utfc_ptr2len(p))] == NUL) { + col += l; + } + } + } + } + } + rettv->vval.v_number = col; +} + +/// "charcol()" function +static void f_charcol(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + get_col(argvars, rettv, true); +} + // "charidx()" function static void f_charidx(typval_T *argvars, typval_T *rettv, FunPtr fptr) { @@ -1147,45 +1192,10 @@ static void f_clearmatches(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } -/* - * "col(string)" function - */ +/// "col(string)" function static void f_col(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - colnr_T col = 0; - pos_T *fp; - int fnum = curbuf->b_fnum; - - fp = var2fpos(&argvars[0], FALSE, &fnum); - if (fp != NULL && fnum == curbuf->b_fnum) { - if (fp->col == MAXCOL) { - // '> can be MAXCOL, get the length of the line then - if (fp->lnum <= curbuf->b_ml.ml_line_count) { - col = (colnr_T)STRLEN(ml_get(fp->lnum)) + 1; - } else { - col = MAXCOL; - } - } else { - col = fp->col + 1; - // col(".") when the cursor is on the NUL at the end of the line - // because of "coladd" can be seen as an extra column. - if (virtual_active() && fp == &curwin->w_cursor) { - char_u *p = get_cursor_pos_ptr(); - - if (curwin->w_cursor.coladd - >= (colnr_T)win_chartabsize(curwin, p, - (curwin->w_virtcol - - curwin->w_cursor.coladd))) { - int l; - - if (*p != NUL && p[(l = utfc_ptr2len(p))] == NUL) { - col += l; - } - } - } - } - } - rettv->vval.v_number = col; + get_col(argvars, rettv, false); } /* @@ -1548,24 +1558,21 @@ static void f_ctxsize(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = ctx_size(); } -/// "cursor(lnum, col)" function, or -/// "cursor(list)" -/// -/// Moves the cursor to the specified line and column. -/// -/// @returns 0 when the position could be set, -1 otherwise. -static void f_cursor(typval_T *argvars, typval_T *rettv, FunPtr fptr) +/// Set the cursor position. +/// If 'charcol' is true, then use the column number as a character offet. +/// Otherwise use the column number as a byte offset. +static void set_cursorpos(typval_T *argvars, typval_T *rettv, bool charcol) { long line, col; long coladd = 0; bool set_curswant = true; rettv->vval.v_number = -1; - if (argvars[1].v_type == VAR_UNKNOWN) { + if (argvars[0].v_type == VAR_LIST) { pos_T pos; colnr_T curswant = -1; - if (list2fpos(argvars, &pos, NULL, &curswant) == FAIL) { + if (list2fpos(argvars, &pos, NULL, &curswant, charcol) == FAIL) { emsg(_(e_invarg)); return; } @@ -1577,16 +1584,22 @@ static void f_cursor(typval_T *argvars, typval_T *rettv, FunPtr fptr) curwin->w_curswant = curswant - 1; set_curswant = false; } - } else { + } else if ((argvars[0].v_type == VAR_NUMBER || argvars[0].v_type == VAR_STRING) + && (argvars[1].v_type == VAR_NUMBER || argvars[1].v_type == VAR_STRING)) { line = tv_get_lnum(argvars); col = (long)tv_get_number_chk(&argvars[1], NULL); + if (charcol) { + col = buf_charidx_to_byteidx(curbuf, line, col) + 1; + } if (argvars[2].v_type != VAR_UNKNOWN) { coladd = (long)tv_get_number_chk(&argvars[2], NULL); } + } else { + emsg(_(e_invarg)); + return; } - if (line < 0 || col < 0 - || coladd < 0) { - return; // type error; errmsg already given + if (line < 0 || col < 0 || coladd < 0) { + return; // type error; errmsg already given } if (line > 0) { curwin->w_cursor.lnum = line; @@ -1605,6 +1618,16 @@ static void f_cursor(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = 0; } +/// "cursor(lnum, col)" function, or +/// "cursor(list)" +/// +/// Moves the cursor to the specified line and column. +/// Returns 0 when the position could be set, -1 otherwise. +static void f_cursor(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + set_cursorpos(argvars, rettv, false); +} + // "debugbreak()" function static void f_debugbreak(typval_T *argvars, typval_T *rettv, FunPtr fptr) { @@ -2176,25 +2199,12 @@ static void f_win_execute(typval_T *argvars, typval_T *rettv, FunPtr fptr) { tabpage_T *tp; win_T *wp = win_id2wp_tp(argvars, &tp); - win_T *save_curwin; - tabpage_T *save_curtab; // Return an empty string if something fails. rettv->v_type = VAR_STRING; rettv->vval.v_string = NULL; if (wp != NULL && tp != NULL) { - pos_T curpos = wp->w_cursor; - if (switch_win_noblock(&save_curwin, &save_curtab, wp, tp, true) == - OK) { - check_cursor(); - execute_common(argvars, rettv, fptr, 1); - } - restore_win_noblock(save_curwin, save_curtab, true); - - // Update the status line if the cursor moved. - if (win_valid(wp) && !equalpos(curpos, wp->w_cursor)) { - wp->w_redr_status = true; - } + WIN_EXECUTE(wp, tp, execute_common(argvars, rettv, fptr, 1)); } } @@ -3180,7 +3190,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) if (argvars[0].v_type == VAR_UNKNOWN) { // getchar(): blocking wait. // TODO(bfredl): deduplicate shared logic with state_enter ? - if (!(char_avail() || using_script() || input_available())) { + if (!char_avail()) { (void)os_inchar(NULL, 0, -1, 0, main_loop.events); if (!multiqueue_empty(main_loop.events)) { state_handle_k_event(); @@ -3216,7 +3226,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) set_vim_var_nr(VV_MOUSE_COL, 0); rettv->vval.v_number = n; - if (IS_SPECIAL(n) || mod_mask != 0) { + if (n != 0 && (IS_SPECIAL(n) || mod_mask != 0)) { char_u temp[10]; // modifier: 3, mbyte-char: 6, NUL: 1 int i = 0; @@ -3300,6 +3310,67 @@ static void f_getcharmod(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = mod_mask; } +static void getpos_both(typval_T *argvars, typval_T *rettv, bool getcurpos, bool charcol) +{ + pos_T *fp = NULL; + pos_T pos; + win_T *wp = curwin; + int fnum = -1; + + if (getcurpos) { + if (argvars[0].v_type != VAR_UNKNOWN) { + wp = find_win_by_nr_or_id(&argvars[0]); + if (wp != NULL) { + fp = &wp->w_cursor; + } + } else { + fp = &curwin->w_cursor; + } + if (fp != NULL && charcol) { + pos = *fp; + pos.col = buf_byteidx_to_charidx(wp->w_buffer, pos.lnum, pos.col); + fp = &pos; + } + } else { + fp = var2fpos(&argvars[0], true, &fnum, charcol); + } + + list_T *const l = tv_list_alloc_ret(rettv, 4 + getcurpos); + tv_list_append_number(l, (fnum != -1) ? (varnumber_T)fnum : (varnumber_T)0); + tv_list_append_number(l, ((fp != NULL) ? (varnumber_T)fp->lnum : (varnumber_T)0)); + tv_list_append_number(l, ((fp != NULL) + ? (varnumber_T)(fp->col == MAXCOL ? MAXCOL : fp->col + 1) + : (varnumber_T)0)); + tv_list_append_number(l, (fp != NULL) ? (varnumber_T)fp->coladd : (varnumber_T)0); + if (getcurpos) { + const int save_set_curswant = curwin->w_set_curswant; + const colnr_T save_curswant = curwin->w_curswant; + const colnr_T save_virtcol = curwin->w_virtcol; + + if (wp == curwin) { + update_curswant(); + } + tv_list_append_number(l, (wp == NULL) ? 0 : ((wp->w_curswant == MAXCOL) + ? (varnumber_T)MAXCOL + : (varnumber_T)wp->w_curswant + 1)); + + // Do not change "curswant", as it is unexpected that a get + // function has a side effect. + if (wp == curwin && save_set_curswant) { + curwin->w_set_curswant = save_set_curswant; + curwin->w_curswant = save_curswant; + curwin->w_virtcol = save_virtcol; + curwin->w_valid &= ~VALID_VIRTCOL; + } + } +} + +/// "getcharpos()" function +static void f_getcharpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + getpos_both(argvars, rettv, false, true); +} + /* * "getcharsearch()" function */ @@ -3795,7 +3866,7 @@ static void f_getmatches(typval_T *argvars, typval_T *rettv, FunPtr fptr) } // "getmousepos()" function -void f_getmousepos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static void f_getmousepos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { dict_T *d; win_T *wp; @@ -3855,61 +3926,21 @@ static void f_getpid(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = os_get_pid(); } -static void getpos_both(typval_T *argvars, typval_T *rettv, bool getcurpos) +/// "getcurpos(string)" function +static void f_getcurpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - pos_T *fp; - int fnum = -1; - - if (getcurpos) { - fp = &curwin->w_cursor; - } else { - fp = var2fpos(&argvars[0], true, &fnum); - } - - list_T *const l = tv_list_alloc_ret(rettv, 4 + (!!getcurpos)); - tv_list_append_number(l, (fnum != -1) ? (varnumber_T)fnum : (varnumber_T)0); - tv_list_append_number(l, ((fp != NULL) - ? (varnumber_T)fp->lnum - : (varnumber_T)0)); - tv_list_append_number(l, ((fp != NULL) - ? (varnumber_T)(fp->col == MAXCOL ? MAXCOL : fp->col + 1) - : (varnumber_T)0)); - tv_list_append_number(l, (fp != NULL) ? (varnumber_T)fp->coladd : (varnumber_T)0); - if (getcurpos) { - const int save_set_curswant = curwin->w_set_curswant; - const colnr_T save_curswant = curwin->w_curswant; - const colnr_T save_virtcol = curwin->w_virtcol; - - update_curswant(); - tv_list_append_number(l, (curwin->w_curswant == MAXCOL - ? (varnumber_T)MAXCOL - : (varnumber_T)curwin->w_curswant + 1)); - - // Do not change "curswant", as it is unexpected that a get - // function has a side effect. - if (save_set_curswant) { - curwin->w_set_curswant = save_set_curswant; - curwin->w_curswant = save_curswant; - curwin->w_virtcol = save_virtcol; - curwin->w_valid &= ~VALID_VIRTCOL; - } - } + getpos_both(argvars, rettv, true, false); } -/* - * "getcurpos(string)" function - */ -static void f_getcurpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static void f_getcursorcharpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - getpos_both(argvars, rettv, true); + getpos_both(argvars, rettv, true, true); } -/* - * "getpos(string)" function - */ +/// "getpos(string)" function static void f_getpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - getpos_both(argvars, rettv, false); + getpos_both(argvars, rettv, false, false); } /// "getqflist()" functions @@ -3991,7 +4022,6 @@ static void f_getregtype(typval_T *argvars, typval_T *rettv, FunPtr fptr) MotionType reg_type = get_reg_type(regname, ®len); format_reg_type(reg_type, reglen, buf, ARRAY_SIZE(buf)); - rettv->v_type = VAR_STRING; rettv->vval.v_string = (char_u *)xstrdup(buf); } @@ -4032,8 +4062,6 @@ static void f_gettabinfo(typval_T *argvars, typval_T *rettv, FunPtr fptr) */ static void f_gettabvar(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - win_T *oldcurwin; - tabpage_T *oldtabpage; bool done = false; rettv->v_type = VAR_STRING; @@ -4047,7 +4075,8 @@ static void f_gettabvar(typval_T *argvars, typval_T *rettv, FunPtr fptr) win_T *const window = tp == curtab || tp->tp_firstwin == NULL ? firstwin : tp->tp_firstwin; - if (switch_win(&oldcurwin, &oldtabpage, window, tp, true) == OK) { + switchwin_T switchwin; + if (switch_win(&switchwin, window, tp, true) == OK) { // look up the variable // Let gettabvar({nr}, "") return the "t:" dictionary. const dictitem_T *const v = find_var_in_ht(&tp->tp_vars->dv_hashtab, 't', @@ -4060,7 +4089,7 @@ static void f_gettabvar(typval_T *argvars, typval_T *rettv, FunPtr fptr) } // restore previous notion of curwin - restore_win(oldcurwin, oldtabpage, true); + restore_win(&switchwin, true); } if (!done && argvars[2].v_type != VAR_UNKNOWN) { @@ -4214,7 +4243,7 @@ static void win_move_into_split(win_T *wp, win_T *targetwin, int size, int flags int height = wp->w_height; win_T *oldwin = curwin; - if (wp == targetwin) { + if (wp == targetwin || wp == aucmd_win) { return; } @@ -4434,6 +4463,12 @@ static void f_has(typval_T *argvars, typval_T *rettv, FunPtr fptr) #if defined(BSD) && !defined(__APPLE__) "bsd", #endif +#ifdef __linux__ + "linux", +#endif +#ifdef SUN_SYSTEM + "sun", +#endif #ifdef UNIX "unix", #endif @@ -4505,6 +4540,7 @@ static void f_has(typval_T *argvars, typval_T *rettv, FunPtr fptr) "mouse", "multi_byte", "multi_lang", + "nanotime", "num64", "packages", "path_extra", @@ -5882,22 +5918,20 @@ static void f_line(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (argvars[1].v_type != VAR_UNKNOWN) { tabpage_T *tp; - win_T *save_curwin; - tabpage_T *save_curtab; // use window specified in the second argument win_T *wp = win_id2wp_tp(&argvars[1], &tp); if (wp != NULL && tp != NULL) { - if (switch_win_noblock(&save_curwin, &save_curtab, wp, tp, true) - == OK) { + switchwin_T switchwin; + if (switch_win_noblock(&switchwin, wp, tp, true) == OK) { check_cursor(); - fp = var2fpos(&argvars[0], true, &fnum); + fp = var2fpos(&argvars[0], true, &fnum, false); } - restore_win_noblock(save_curwin, save_curtab, true); + restore_win_noblock(&switchwin, true); } } else { // use current window - fp = var2fpos(&argvars[0], true, &fnum); + fp = var2fpos(&argvars[0], true, &fnum, false); } if (fp != NULL) { @@ -5980,6 +6014,7 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) { char_u *keys_buf = NULL; char_u *rhs; + LuaRef rhs_lua; int mode; int abbr = FALSE; int get_dict = FALSE; @@ -6016,7 +6051,7 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) keys = replace_termcodes(keys, STRLEN(keys), &keys_buf, true, true, true, CPO_TO_CPO_FLAGS); - rhs = check_map(keys, mode, exact, false, abbr, &mp, &buffer_local); + rhs = check_map(keys, mode, exact, false, abbr, &mp, &buffer_local, &rhs_lua); xfree(keys_buf); if (!get_dict) { @@ -6027,10 +6062,15 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) } else { rettv->vval.v_string = (char_u *)str2special_save((char *)rhs, false, false); } + } else if (rhs_lua != LUA_NOREF) { + size_t msglen = 100; + char *msg = (char *)xmalloc(msglen); + snprintf(msg, msglen, "<Lua function %d>", mp->m_luaref); + rettv->vval.v_string = (char_u *)msg; } } else { tv_dict_alloc_ret(rettv); - if (rhs != NULL) { + if (mp != NULL && (rhs != NULL || rhs_lua != LUA_NOREF)) { // Return a dictionary. mapblock_fill_dict(rettv->vval.v_dict, mp, buffer_local, true); } @@ -6808,12 +6848,23 @@ static void f_or(typval_T *argvars, typval_T *rettv, FunPtr fptr) */ static void f_pathshorten(typval_T *argvars, typval_T *rettv, FunPtr fptr) { + int trim_len = 1; + + if (argvars[1].v_type != VAR_UNKNOWN) { + trim_len = (int)tv_get_number(&argvars[1]); + if (trim_len < 1) { + trim_len = 1; + } + } + rettv->v_type = VAR_STRING; - const char *const s = tv_get_string_chk(&argvars[0]); - if (!s) { - return; + const char_u *p = (char_u *)tv_get_string_chk(&argvars[0]); + if (p == NULL) { + rettv->vval.v_string = NULL; + } else { + rettv->vval.v_string = vim_strsave(p); + shorten_dir_len(rettv->vval.v_string, trim_len); } - rettv->vval.v_string = shorten_dir((char_u *)xstrdup(s)); } /* @@ -6922,7 +6973,7 @@ static void f_prompt_setinterrupt(typval_T *argvars, typval_T *rettv, FunPtr fpt } /// "prompt_getprompt({buffer})" function -void f_prompt_getprompt(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static void f_prompt_getprompt(typval_T *argvars, typval_T *rettv, FunPtr fptr) FUNC_ATTR_NONNULL_ALL { // return an empty string by default, e.g. it's not a prompt buffer @@ -6977,36 +7028,169 @@ static void f_pumvisible(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } -/* - * "pyeval()" function - */ -static void f_pyeval(typval_T *argvars, typval_T *rettv, FunPtr fptr) +/// "py3eval()" and "pyxeval()" functions (always python3) +static void f_py3eval(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - script_host_eval("python", argvars, rettv); + script_host_eval("python3", argvars, rettv); } -/* - * "py3eval()" function - */ -static void f_py3eval(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static void init_srand(uint32_t *const x) + FUNC_ATTR_NONNULL_ALL { - script_host_eval("python3", argvars, rettv); +#ifndef MSWIN + static int dev_urandom_state = NOTDONE; // FAIL or OK once tried + + if (dev_urandom_state != FAIL) { + const int fd = os_open("/dev/urandom", O_RDONLY, 0); + struct { + union { + uint32_t number; + char bytes[sizeof(uint32_t)]; + } contents; + } buf; + + // Attempt reading /dev/urandom. + if (fd == -1) { + dev_urandom_state = FAIL; + } else { + buf.contents.number = 0; + if (read(fd, buf.contents.bytes, sizeof(uint32_t)) != sizeof(uint32_t)) { + dev_urandom_state = FAIL; + } else { + dev_urandom_state = OK; + *x = buf.contents.number; + } + os_close(fd); + } + } + if (dev_urandom_state != OK) { + // Reading /dev/urandom doesn't work, fall back to time(). +#endif + *x = time(NULL); +#ifndef MSWIN + } +#endif +} + +static inline uint32_t splitmix32(uint32_t *const x) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE +{ + uint32_t z = (*x += 0x9e3779b9); + z = (z ^ (z >> 16)) * 0x85ebca6b; + z = (z ^ (z >> 13)) * 0xc2b2ae35; + return z ^ (z >> 16); +} + +static inline uint32_t shuffle_xoshiro128starstar(uint32_t *const x, uint32_t *const y, + uint32_t *const z, uint32_t *const w) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE +{ +#define ROTL(x, k) ((x << k) | (x >> (32 - k))) + const uint32_t result = ROTL(*y * 5, 7) * 9; + const uint32_t t = *y << 9; + *z ^= *x; + *w ^= *y; + *y ^= *z; + *x ^= *w; + *z ^= t; + *w = ROTL(*w, 11); +#undef ROTL + return result; +} + +/// "rand()" function +static void f_rand(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + uint32_t result; + + if (argvars[0].v_type == VAR_UNKNOWN) { + static uint32_t gx, gy, gz, gw; + static bool initialized = false; + + // When no argument is given use the global seed list. + if (!initialized) { + // Initialize the global seed list. + uint32_t x; + init_srand(&x); + + gx = splitmix32(&x); + gy = splitmix32(&x); + gz = splitmix32(&x); + gw = splitmix32(&x); + initialized = true; + } + + result = shuffle_xoshiro128starstar(&gx, &gy, &gz, &gw); + } else if (argvars[0].v_type == VAR_LIST) { + list_T *const l = argvars[0].vval.v_list; + if (tv_list_len(l) != 4) { + goto theend; + } + + typval_T *const tvx = TV_LIST_ITEM_TV(tv_list_find(l, 0L)); + typval_T *const tvy = TV_LIST_ITEM_TV(tv_list_find(l, 1L)); + typval_T *const tvz = TV_LIST_ITEM_TV(tv_list_find(l, 2L)); + typval_T *const tvw = TV_LIST_ITEM_TV(tv_list_find(l, 3L)); + if (tvx->v_type != VAR_NUMBER) { + goto theend; + } + if (tvy->v_type != VAR_NUMBER) { + goto theend; + } + if (tvz->v_type != VAR_NUMBER) { + goto theend; + } + if (tvw->v_type != VAR_NUMBER) { + goto theend; + } + uint32_t x = tvx->vval.v_number; + uint32_t y = tvy->vval.v_number; + uint32_t z = tvz->vval.v_number; + uint32_t w = tvw->vval.v_number; + + result = shuffle_xoshiro128starstar(&x, &y, &z, &w); + + tvx->vval.v_number = (varnumber_T)x; + tvy->vval.v_number = (varnumber_T)y; + tvz->vval.v_number = (varnumber_T)z; + tvw->vval.v_number = (varnumber_T)w; + } else { + goto theend; + } + + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = (varnumber_T)result; + return; + +theend: + semsg(_(e_invarg2), tv_get_string(&argvars[0])); + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = -1; } -// "pyxeval()" function -static void f_pyxeval(typval_T *argvars, typval_T *rettv, FunPtr fptr) +/// "srand()" function +static void f_srand(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - init_pyxversion(); - if (p_pyx == 2) { - f_pyeval(argvars, rettv, NULL); + uint32_t x = 0; + + tv_list_alloc_ret(rettv, 4); + if (argvars[0].v_type == VAR_UNKNOWN) { + init_srand(&x); } else { - f_py3eval(argvars, rettv, NULL); + bool error = false; + x = tv_get_number_chk(&argvars[0], &error); + if (error) { + return; + } } + + tv_list_append_number(rettv->vval.v_list, (varnumber_T)splitmix32(&x)); + tv_list_append_number(rettv->vval.v_list, (varnumber_T)splitmix32(&x)); + tv_list_append_number(rettv->vval.v_list, (varnumber_T)splitmix32(&x)); + tv_list_append_number(rettv->vval.v_list, (varnumber_T)splitmix32(&x)); } -/// /// "perleval()" function -/// static void f_perleval(typval_T *argvars, typval_T *rettv, FunPtr fptr) { script_host_eval("perl", argvars, rettv); @@ -7873,6 +8057,102 @@ static void f_reverse(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } +/// "reduce(list, { accumlator, element -> value } [, initial])" function +static void f_reduce(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + if (argvars[0].v_type != VAR_LIST && argvars[0].v_type != VAR_BLOB) { + emsg(_(e_listblobreq)); + return; + } + + const char_u *func_name; + partial_T *partial = NULL; + if (argvars[1].v_type == VAR_FUNC) { + func_name = argvars[1].vval.v_string; + } else if (argvars[1].v_type == VAR_PARTIAL) { + partial = argvars[1].vval.v_partial; + func_name = partial_name(partial); + } else { + func_name = (const char_u *)tv_get_string(&argvars[1]); + } + if (*func_name == NUL) { + return; // type error or empty name + } + + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.evaluate = true; + funcexe.partial = partial; + + typval_T initial; + typval_T argv[3]; + if (argvars[0].v_type == VAR_LIST) { + list_T *const l = argvars[0].vval.v_list; + const listitem_T *li; + + if (argvars[2].v_type == VAR_UNKNOWN) { + if (tv_list_len(l) == 0) { + semsg(_(e_reduceempty), "List"); + return; + } + const listitem_T *const first = tv_list_first(l); + initial = *TV_LIST_ITEM_TV(first); + li = TV_LIST_ITEM_NEXT(l, first); + } else { + initial = argvars[2]; + li = tv_list_first(l); + } + + tv_copy(&initial, rettv); + + if (l != NULL) { + const VarLockStatus prev_locked = tv_list_locked(l); + const int called_emsg_start = called_emsg; + + tv_list_set_lock(l, VAR_FIXED); // disallow the list changing here + for (; li != NULL; li = TV_LIST_ITEM_NEXT(l, li)) { + argv[0] = *rettv; + argv[1] = *TV_LIST_ITEM_TV(li); + rettv->v_type = VAR_UNKNOWN; + const int r = call_func(func_name, -1, rettv, 2, argv, &funcexe); + tv_clear(&argv[0]); + if (r == FAIL || called_emsg != called_emsg_start) { + break; + } + } + tv_list_set_lock(l, prev_locked); + } + } else { + const blob_T *const b = argvars[0].vval.v_blob; + int i; + + if (argvars[2].v_type == VAR_UNKNOWN) { + if (tv_blob_len(b) == 0) { + semsg(_(e_reduceempty), "Blob"); + return; + } + initial.v_type = VAR_NUMBER; + initial.vval.v_number = tv_blob_get(b, 0); + i = 1; + } else if (argvars[2].v_type != VAR_NUMBER) { + emsg(_(e_number_exp)); + return; + } else { + initial = argvars[2]; + i = 0; + } + + tv_copy(&initial, rettv); + for (; i < tv_blob_len(b); i++) { + argv[0] = *rettv; + argv[1].v_type = VAR_NUMBER; + argv[1].vval.v_number = tv_blob_get(b, i); + if (call_func(func_name, -1, rettv, 2, argv, &funcexe) == FAIL) { + return; + } + } + } +} + #define SP_NOMOVE 0x01 ///< don't move cursor #define SP_REPEAT 0x02 ///< repeat to find outer pair #define SP_RETCOUNT 0x04 ///< return matchcount @@ -7959,6 +8239,7 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp) int options = SEARCH_KEEP; int subpatnum; searchit_arg_T sia; + bool use_skip = false; const char *const pat = tv_get_string(&argvars[0]); dir = get_search_arg(&argvars[1], flagsp); // May set p_ws. @@ -7976,7 +8257,7 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp) options |= SEARCH_COL; } - // Optional arguments: line number to stop searching and timeout. + // Optional arguments: line number to stop searching, timeout and skip. if (argvars[1].v_type != VAR_UNKNOWN && argvars[2].v_type != VAR_UNKNOWN) { lnum_stop = tv_get_number_chk(&argvars[2], NULL); if (lnum_stop < 0) { @@ -7987,6 +8268,7 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp) if (time_limit < 0) { goto theend; } + use_skip = eval_expr_valid_arg(&argvars[4]); } } @@ -8006,11 +8288,46 @@ static int search_cmn(typval_T *argvars, pos_T *match_pos, int *flagsp) } pos = save_cursor = curwin->w_cursor; + pos_T firstpos = { 0 }; memset(&sia, 0, sizeof(sia)); sia.sa_stop_lnum = (linenr_T)lnum_stop; sia.sa_tm = &tm; - subpatnum = searchit(curwin, curbuf, &pos, NULL, dir, (char_u *)pat, 1, - options, RE_SEARCH, &sia); + + // Repeat until {skip} returns false. + for (;;) { + subpatnum + = searchit(curwin, curbuf, &pos, NULL, dir, (char_u *)pat, 1, options, RE_SEARCH, &sia); + // finding the first match again means there is no match where {skip} + // evaluates to zero. + if (firstpos.lnum != 0 && equalpos(pos, firstpos)) { + subpatnum = FAIL; + } + + if (subpatnum == FAIL || !use_skip) { + // didn't find it or no skip argument + break; + } + firstpos = pos; + + // If the skip expression matches, ignore this match. + { + const pos_T save_pos = curwin->w_cursor; + + curwin->w_cursor = pos; + bool err = false; + const bool do_skip = eval_expr_to_bool(&argvars[4], &err); + curwin->w_cursor = save_pos; + if (err) { + // Evaluating {skip} caused an error, break here. + subpatnum = FAIL; + break; + } + if (!do_skip) { + break; + } + } + } + if (subpatnum != FAIL) { if (flags & SP_SUBPAT) { retval = subpatnum; @@ -8470,13 +8787,9 @@ static int searchpair_cmn(typval_T *argvars, pos_T *match_pos) || argvars[4].v_type == VAR_UNKNOWN) { skip = NULL; } else { + // Type is checked later. skip = &argvars[4]; - if (skip->v_type != VAR_FUNC - && skip->v_type != VAR_PARTIAL - && skip->v_type != VAR_STRING) { - semsg(_(e_invarg2), tv_get_string(&argvars[4])); - goto theend; // Type error. - } + if (argvars[5].v_type != VAR_UNKNOWN) { lnum_stop = tv_get_number_chk(&argvars[5], NULL); if (lnum_stop < 0) { @@ -8587,10 +8900,7 @@ long do_searchpair(const char *spat, const char *mpat, const char *epat, int dir } if (skip != NULL) { - // Empty string means to not use the skip expression. - if (skip->v_type == VAR_STRING || skip->v_type == VAR_FUNC) { - use_skip = skip->vval.v_string != NULL && *skip->vval.v_string != NUL; - } + use_skip = eval_expr_valid_arg(skip); } save_cursor = curwin->w_cursor; @@ -8863,6 +9173,49 @@ static void f_setbufvar(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } +/// Set the cursor or mark position. +/// If 'charpos' is TRUE, then use the column number as a character offet. +/// Otherwise use the column number as a byte offset. +static void set_position(typval_T *argvars, typval_T *rettv, bool charpos) +{ + pos_T pos; + int fnum; + colnr_T curswant = -1; + + rettv->vval.v_number = -1; + const char *const name = tv_get_string_chk(argvars); + if (name != NULL) { + if (list2fpos(&argvars[1], &pos, &fnum, &curswant, charpos) == OK) { + if (pos.col != MAXCOL && --pos.col < 0) { + pos.col = 0; + } + if (name[0] == '.' && name[1] == NUL) { + // set cursor; "fnum" is ignored + curwin->w_cursor = pos; + if (curswant >= 0) { + curwin->w_curswant = curswant - 1; + curwin->w_set_curswant = false; + } + check_cursor(); + rettv->vval.v_number = 0; + } else if (name[0] == '\'' && name[1] != NUL && name[2] == NUL) { + // set mark + if (setmark_pos((uint8_t)name[1], &pos, fnum) == OK) { + rettv->vval.v_number = 0; + } + } else { + emsg(_(e_invarg)); + } + } + } +} + +/// "setcharpos()" function +static void f_setcharpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + set_position(argvars, rettv, true); +} + static void f_setcharsearch(typval_T *argvars, typval_T *rettv, FunPtr fptr) { dict_T *d; @@ -8905,6 +9258,12 @@ static void f_setcmdpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } +/// "setcursorcharpos" function +static void f_setcursorcharpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + set_cursorpos(argvars, rettv, true); +} + /// "setenv()" function static void f_setenv(typval_T *argvars, typval_T *rettv, FunPtr fptr) { @@ -9161,41 +9520,10 @@ static void f_setmatches(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } -/* - * "setpos()" function - */ +/// "setpos()" function static void f_setpos(typval_T *argvars, typval_T *rettv, FunPtr fptr) { - pos_T pos; - int fnum; - colnr_T curswant = -1; - - rettv->vval.v_number = -1; - const char *const name = tv_get_string_chk(argvars); - if (name != NULL) { - if (list2fpos(&argvars[1], &pos, &fnum, &curswant) == OK) { - if (pos.col != MAXCOL && --pos.col < 0) { - pos.col = 0; - } - if (name[0] == '.' && name[1] == NUL) { - // set cursor; "fnum" is ignored - curwin->w_cursor = pos; - if (curswant >= 0) { - curwin->w_curswant = curswant - 1; - curwin->w_set_curswant = false; - } - check_cursor(); - rettv->vval.v_number = 0; - } else if (name[0] == '\'' && name[1] != NUL && name[2] == NUL) { - // set mark - if (setmark_pos((uint8_t)name[1], &pos, fnum) == OK) { - rettv->vval.v_number = 0; - } - } else { - emsg(_(e_invarg)); - } - } - } + set_position(argvars, rettv, false); } /* @@ -10210,6 +10538,10 @@ static void f_stdioopen(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (!tv_dict_get_callback(opts, S_LEN("on_stdin"), &on_stdin.cb)) { return; } + if (!tv_dict_get_callback(opts, S_LEN("on_print"), &on_print)) { + return; + } + on_stdin.buffered = tv_dict_get_number(opts, "stdin_buffered"); if (on_stdin.buffered && on_stdin.cb.type == kCallbackNone) { on_stdin.self = opts; @@ -10664,7 +10996,7 @@ static void f_stridx(typval_T *argvars, typval_T *rettv, FunPtr fptr) /* * "string()" function */ -void f_string(typval_T *argvars, typval_T *rettv, FunPtr fptr) +static void f_string(typval_T *argvars, typval_T *rettv, FunPtr fptr) { rettv->v_type = VAR_STRING; rettv->vval.v_string = (char_u *)encode_tv2string(&argvars[0], NULL); @@ -11874,7 +12206,7 @@ static void f_virtcol(typval_T *argvars, typval_T *rettv, FunPtr fptr) pos_T *fp; int fnum = curbuf->b_fnum; - fp = var2fpos(&argvars[0], FALSE, &fnum); + fp = var2fpos(&argvars[0], false, &fnum, false); if (fp != NULL && fp->lnum <= curbuf->b_ml.ml_line_count && fnum == curbuf->b_fnum) { // Limit the column to a valid value, getvvcol() doesn't check. @@ -11980,6 +12312,42 @@ static void f_win_id2win(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_number = win_id2win(argvars); } +/// "win_move_separator()" function +static void f_win_move_separator(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + win_T *wp; + int offset; + + rettv->vval.v_number = false; + + wp = find_win_by_nr_or_id(&argvars[0]); + if (wp == NULL || wp->w_floating) { + return; + } + + offset = (int)tv_get_number(&argvars[1]); + win_drag_vsep_line(wp, offset); + rettv->vval.v_number = true; +} + +/// "win_move_statusline()" function +static void f_win_move_statusline(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + win_T *wp; + int offset; + + rettv->vval.v_number = false; + + wp = find_win_by_nr_or_id(&argvars[0]); + if (wp == NULL || wp->w_floating) { + return; + } + + offset = (int)tv_get_number(&argvars[1]); + win_drag_status_line(wp, offset); + rettv->vval.v_number = true; +} + /// "winbufnr(nr)" function static void f_winbufnr(typval_T *argvars, typval_T *rettv, FunPtr fptr) { diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 11bbaaed9c..44b003d106 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -8,6 +8,7 @@ #include <stdlib.h> #include <string.h> +#include "lauxlib.h" #include "nvim/ascii.h" #include "nvim/assert.h" #include "nvim/charset.h" @@ -28,11 +29,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 +1124,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 +1145,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 +1155,27 @@ void callback_free(Callback *callback) callback->data.funcref = NULL; } +/// Check if callback is freed +bool callback_is_freed(Callback callback) +{ + switch (callback.type) { + case kCallbackFuncref: + return false; + break; + case kCallbackPartial: + return false; + break; + case kCallbackLua: + return callback.data.luaref == LUA_NOREF; + break; + case kCallbackNone: + return true; + break; + } + + return true; +} + /// Copy a callback into a typval_T. void callback_put(Callback *cb, typval_T *tv) FUNC_ATTR_NONNULL_ALL @@ -1164,6 +1191,11 @@ 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): Unified Callback. + // At this point this isn't possible, but it'd be nice to put + // these handled more neatly in one place. + // So instead, we just do the default and put nil default: tv->v_type = VAR_SPECIAL; tv->vval.v_special = kSpecialVarNull; @@ -1185,6 +1217,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; @@ -2455,13 +2490,11 @@ static inline void _nothing_conv_dict_end(typval_T *const tv, dict_T **const dic #define TYPVAL_ENCODE_NAME nothing #define TYPVAL_ENCODE_FIRST_ARG_TYPE const void *const #define TYPVAL_ENCODE_FIRST_ARG_NAME ignored -#define TYPVAL_ENCODE_TRANSLATE_OBJECT_NAME #include "nvim/eval/typval_encode.c.h" #undef TYPVAL_ENCODE_SCOPE #undef TYPVAL_ENCODE_NAME #undef TYPVAL_ENCODE_FIRST_ARG_TYPE #undef TYPVAL_ENCODE_FIRST_ARG_NAME -#undef TYPVAL_ENCODE_TRANSLATE_OBJECT_NAME #undef TYPVAL_ENCODE_ALLOW_SPECIALS #undef TYPVAL_ENCODE_CONV_NIL @@ -3093,7 +3126,7 @@ linenr_T tv_get_lnum(const typval_T *const tv) linenr_T lnum = (linenr_T)tv_get_number_chk(tv, NULL); if (lnum == 0) { // No valid number, try using same function as line() does. int fnum; - pos_T *const fp = var2fpos(tv, true, &fnum); + pos_T *const fp = var2fpos(tv, true, &fnum, false); if (fp != NULL) { lnum = fp->lnum; } @@ -3207,8 +3240,9 @@ const char *tv_get_string_buf_chk(const typval_T *const tv, char *const buf) case VAR_BLOB: case VAR_UNKNOWN: emsg(_(str_errors[tv->v_type])); - return false; + return NULL; } + abort(); return NULL; } diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index d1275d6512..40dc819754 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -72,16 +72,20 @@ 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_NONE ((Callback){ .type = kCallbackNone }) + +#define CALLBACK_INIT { .type = kCallbackNone } +#define CALLBACK_NONE ((Callback)CALLBACK_INIT) /// Structure holding dictionary watcher typedef struct dict_watcher { diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index eb241eb8ae..5764f9fbd4 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -2573,6 +2573,7 @@ void ex_function(exarg_T *eap) fp->uf_calls = 0; fp->uf_script_ctx = current_sctx; fp->uf_script_ctx.sc_lnum += sourcing_lnum_top; + nlua_set_sctx(&fp->uf_script_ctx); goto ret_free; diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c index 892c46dd04..89fced59c5 100644 --- a/src/nvim/event/loop.c +++ b/src/nvim/event/loop.c @@ -75,7 +75,7 @@ bool loop_poll_events(Loop *loop, int ms) /// @note Event is queued into `fast_events`, which is processed outside of the /// primary `events` queue by loop_poll_events(). For `main_loop`, that /// means `fast_events` is NOT processed in an "editor mode" -/// (VimState.execute), so redraw and other side-effects are likely to be +/// (VimState.execute), so redraw and other side effects are likely to be /// skipped. /// @see loop_schedule_deferred void loop_schedule_fast(Loop *loop, Event event) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 4965eb9c20..c7910e148d 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -814,7 +814,11 @@ void ex_retab(exarg_T *eap) // len is actual number of white characters used len = num_spaces + num_tabs; old_len = (long)STRLEN(ptr); - long new_len = old_len - col + start_col + len + 1; + const long new_len = old_len - col + start_col + len + 1; + if (new_len <= 0 || new_len >= MAXCOL) { + emsg(_(e_resulting_text_too_long)); + break; + } new_line = xmalloc(new_len); if (start_col > 0) { @@ -847,6 +851,10 @@ void ex_retab(exarg_T *eap) break; } vcol += win_chartabsize(curwin, ptr + col, (colnr_T)vcol); + if (vcol >= MAXCOL) { + emsg(_(e_resulting_text_too_long)); + break; + } col += utfc_ptr2len(ptr + col); } if (new_line == NULL) { // out of memory @@ -980,8 +988,10 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) foldMoveRange(win, &win->w_folds, line1, line2, dest); } } - curbuf->b_op_start.lnum = dest - num_lines + 1; - curbuf->b_op_end.lnum = dest; + if (!cmdmod.lockmarks) { + curbuf->b_op_start.lnum = dest - num_lines + 1; + curbuf->b_op_end.lnum = dest; + } line_off = -num_lines; byte_off = -extent_byte; } else { @@ -991,10 +1001,14 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) foldMoveRange(win, &win->w_folds, dest + 1, line1 - 1, line2); } } - curbuf->b_op_start.lnum = dest + 1; - curbuf->b_op_end.lnum = dest + num_lines; + if (!cmdmod.lockmarks) { + curbuf->b_op_start.lnum = dest + 1; + curbuf->b_op_end.lnum = dest + num_lines; + } + } + if (!cmdmod.lockmarks) { + curbuf->b_op_start.col = curbuf->b_op_end.col = 0; } - curbuf->b_op_start.col = curbuf->b_op_end.col = 0; mark_adjust_nofold(last_line - num_lines + 1, last_line, -(last_line - dest - extra), 0L, kExtmarkNOOP); @@ -1057,9 +1071,11 @@ void ex_copy(linenr_T line1, linenr_T line2, linenr_T n) char_u *p; count = line2 - line1 + 1; - curbuf->b_op_start.lnum = n + 1; - curbuf->b_op_end.lnum = n + count; - curbuf->b_op_start.col = curbuf->b_op_end.col = 0; + if (!cmdmod.lockmarks) { + curbuf->b_op_start.lnum = n + 1; + curbuf->b_op_end.lnum = n + count; + curbuf->b_op_start.col = curbuf->b_op_end.col = 0; + } /* * there are three situations: @@ -1099,6 +1115,9 @@ void ex_copy(linenr_T line1, linenr_T line2, linenr_T n) } appended_lines_mark(n, count); + if (VIsual_active) { + check_pos(curbuf, &VIsual); + } msgmore((long)count); } @@ -1269,12 +1288,18 @@ static void do_filter(linenr_T line1, linenr_T line2, exarg_T *eap, char_u *cmd, char_u *cmd_buf; buf_T *old_curbuf = curbuf; int shell_flags = 0; + const pos_T orig_start = curbuf->b_op_start; + const pos_T orig_end = curbuf->b_op_end; const int stmp = p_stmp; if (*cmd == NUL) { // no filter command return; } + const bool save_lockmarks = cmdmod.lockmarks; + // Temporarily disable lockmarks since that's needed to propagate changed + // regions of the buffer for foldUpdate(), linecount, etc. + cmdmod.lockmarks = false; cursor_save = curwin->w_cursor; linecount = line2 - line1 + 1; @@ -1349,7 +1374,7 @@ static void do_filter(linenr_T line1, linenr_T line2, exarg_T *eap, char_u *cmd, ui_cursor_goto(Rows - 1, 0); if (do_out) { - if (u_save((line2), (linenr_T)(line2 + 1)) == FAIL) { + if (u_save(line2, (linenr_T)(line2 + 1)) == FAIL) { xfree(cmd_buf); goto error; } @@ -1455,10 +1480,15 @@ error: filterend: + cmdmod.lockmarks = save_lockmarks; if (curbuf != old_curbuf) { no_wait_return--; emsg(_("E135: *Filter* Autocommands must not change current buffer")); + } else if (cmdmod.lockmarks) { + curbuf->b_op_start = orig_start; + curbuf->b_op_end = orig_end; } + if (itmp != NULL) { os_remove((char *)itmp); } @@ -1913,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); @@ -3006,7 +3036,12 @@ void ex_append(exarg_T *eap) did_undo = true; ml_append(lnum, theline, (colnr_T)0, false); - appended_lines_mark(lnum + (empty ? 1 : 0), 1L); + if (empty) { + // there are no marks below the inserted lines + appended_lines(lnum, 1L); + } else { + appended_lines_mark(lnum, 1L); + } xfree(theline); ++lnum; @@ -3025,15 +3060,16 @@ void ex_append(exarg_T *eap) // "start" is set to eap->line2+1 unless that position is invalid (when // eap->line2 pointed to the end of the buffer and nothing was appended) // "end" is set to lnum when something has been appended, otherwise - // it is the same than "start" -- Acevedo - curbuf->b_op_start.lnum = (eap->line2 < curbuf->b_ml.ml_line_count) ? - eap->line2 + 1 : curbuf->b_ml.ml_line_count; - if (eap->cmdidx != CMD_append) { - --curbuf->b_op_start.lnum; + // it is the same as "start" -- Acevedo + if (!cmdmod.lockmarks) { + curbuf->b_op_start.lnum + = (eap->line2 < curbuf->b_ml.ml_line_count) ? eap->line2 + 1 : curbuf->b_ml.ml_line_count; + if (eap->cmdidx != CMD_append) { + curbuf->b_op_start.lnum--; + } + curbuf->b_op_end.lnum = (eap->line2 < lnum) ? lnum : curbuf->b_op_start.lnum; + curbuf->b_op_start.col = curbuf->b_op_end.col = 0; } - curbuf->b_op_end.lnum = (eap->line2 < lnum) - ? lnum : curbuf->b_op_start.lnum; - curbuf->b_op_start.col = curbuf->b_op_end.col = 0; curwin->w_cursor.lnum = lnum; check_cursor_lnum(); beginline(BL_SOL | BL_FIX); @@ -3624,8 +3660,14 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle // We do it here once to avoid it to be replaced over and over again. // But don't do it when it starts with "\=", then it's an expression. assert(sub != NULL); + + bool sub_needs_free = false; if (!(sub[0] == '\\' && sub[1] == '=')) { + char_u *source = sub; sub = regtilde(sub, p_magic); + // When previewing, the new pattern allocated by regtilde() needs to be freed + // in this function because it will not be used or freed by regtilde() later. + sub_needs_free = preview && sub != source; } // Check for a match on each line. @@ -4346,10 +4388,12 @@ skip: } if (sub_nsubs > start_nsubs) { - // Set the '[ and '] marks. - curbuf->b_op_start.lnum = eap->line1; - curbuf->b_op_end.lnum = line2; - curbuf->b_op_start.col = curbuf->b_op_end.col = 0; + if (!cmdmod.lockmarks) { + // Set the '[ and '] marks. + curbuf->b_op_start.lnum = eap->line1; + curbuf->b_op_end.lnum = line2; + curbuf->b_op_start.col = curbuf->b_op_end.col = 0; + } if (!global_busy) { // when interactive leave cursor on the match @@ -4420,6 +4464,10 @@ skip: kv_destroy(preview_lines.subresults); + if (sub_needs_free) { + xfree(sub); + } + return preview_buf; #undef ADJUST_SUB_FIRSTLNUM #undef PUSH_PREVIEW_LINES @@ -5063,8 +5111,7 @@ int find_help_tags(const char_u *arg, int *num_matches, char_u ***matches, bool && ((arg[1] != NUL && arg[2] == NUL) || (vim_strchr((char_u *)"%_z@", arg[1]) != NULL && arg[2] != NUL))) { - STRCPY(d, "/\\\\"); - STRCPY(d + 3, arg + 1); + vim_snprintf((char *)d, IOSIZE, "/\\\\%s", arg + 1); // Check for "/\\_$", should be "/\\_\$" if (d[3] == '_' && d[4] == '$') { STRCPY(d + 4, "\\$"); @@ -5817,7 +5864,7 @@ void ex_helpclose(exarg_T *eap) { FOR_ALL_WINDOWS_IN_TAB(win, curtab) { if (bt_help(win->w_buffer)) { - win_close(win, false); + win_close(win, false, eap->forceit); return; } } @@ -6102,12 +6149,14 @@ char_u *skip_vimgrep_pat(char_u *p, char_u **s, int *flags) p++; // Find the flags - while (*p == 'g' || *p == 'j') { + while (*p == 'g' || *p == 'j' || *p == 'f') { if (flags != NULL) { if (*p == 'g') { *flags |= VGR_GLOBAL; - } else { + } else if (*p == 'j') { *flags |= VGR_NOJUMP; + } else { + *flags |= VGR_FUZZY; } } p++; diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index c388373ac1..c2d40c8bb7 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2097,19 +2097,19 @@ module.cmds = { command='python', flags=bit.bor(RANGE, EXTRA, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_python', + func='ex_python3', }, { command='pydo', flags=bit.bor(RANGE, DFLALL, EXTRA, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pydo', + func='ex_pydo3', }, { command='pyfile', flags=bit.bor(RANGE, FILE1, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pyfile', + func='ex_py3file', }, { command='py3', @@ -2139,25 +2139,25 @@ module.cmds = { command='pyx', flags=bit.bor(RANGE, EXTRA, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pyx', + func='ex_python3', }, { command='pyxdo', flags=bit.bor(RANGE, DFLALL, EXTRA, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pyxdo', + func='ex_pydo3', }, { command='pythonx', flags=bit.bor(RANGE, EXTRA, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pyx', + func='ex_python3', }, { command='pyxfile', flags=bit.bor(RANGE, FILE1, NEEDARG, CMDWIN), addr_type='ADDR_LINES', - func='ex_pyxfile', + func='ex_py3file', }, { command='quit', @@ -2413,7 +2413,7 @@ module.cmds = { }, { command='set', - flags=bit.bor(TRLBAR, EXTRA, CMDWIN, SBOXOK), + flags=bit.bor(BANG, TRLBAR, EXTRA, CMDWIN, SBOXOK), addr_type='ADDR_NONE', func='ex_set', }, @@ -2425,13 +2425,13 @@ module.cmds = { }, { command='setglobal', - flags=bit.bor(TRLBAR, EXTRA, CMDWIN, SBOXOK), + flags=bit.bor(BANG, TRLBAR, EXTRA, CMDWIN, SBOXOK), addr_type='ADDR_NONE', func='ex_set', }, { command='setlocal', - flags=bit.bor(TRLBAR, EXTRA, CMDWIN, SBOXOK), + flags=bit.bor(BANG, TRLBAR, EXTRA, CMDWIN, SBOXOK), addr_type='ADDR_NONE', func='ex_set', }, diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 33f9477608..8666c7e33a 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -12,6 +12,7 @@ #include <string.h> #include "nvim/ascii.h" +#include "nvim/globals.h" #include "nvim/vim.h" #ifdef HAVE_LOCALE_H # include <locale.h> @@ -135,21 +136,6 @@ void ex_profile(exarg_T *eap) } } -void ex_python(exarg_T *eap) -{ - script_host_execute("python", eap); -} - -void ex_pyfile(exarg_T *eap) -{ - script_host_execute_file("python", eap); -} - -void ex_pydo(exarg_T *eap) -{ - script_host_do_range("python", eap); -} - void ex_ruby(exarg_T *eap) { script_host_execute("ruby", eap); @@ -1613,7 +1599,7 @@ void ex_compiler(exarg_T *eap) if (old_cur_comp != NULL) { old_cur_comp = vim_strsave(old_cur_comp); } - do_cmdline_cmd("command -nargs=* CompilerSet setlocal <args>"); + do_cmdline_cmd("command -nargs=* -keepscript CompilerSet setlocal <args>"); } do_unlet(S_LEN("g:current_compiler"), true); do_unlet(S_LEN("b:current_compiler"), true); @@ -1660,126 +1646,6 @@ void ex_options(exarg_T *eap) cmd_source((char_u *)SYS_OPTWIN_FILE, NULL); } -// Detect Python 3 or 2, and initialize 'pyxversion'. -void init_pyxversion(void) -{ - if (p_pyx == 0) { - if (eval_has_provider("python3")) { - p_pyx = 3; - } else if (eval_has_provider("python")) { - p_pyx = 2; - } - } -} - -// Does a file contain one of the following strings at the beginning of any -// line? -// "#!(any string)python2" => returns 2 -// "#!(any string)python3" => returns 3 -// "# requires python 2.x" => returns 2 -// "# requires python 3.x" => returns 3 -// otherwise return 0. -static int requires_py_version(char_u *filename) -{ - FILE *file; - int requires_py_version = 0; - int i, lines; - - lines = (int)p_mls; - if (lines < 0) { - lines = 5; - } - - file = os_fopen((char *)filename, "r"); - if (file != NULL) { - for (i = 0; i < lines; i++) { - if (vim_fgets(IObuff, IOSIZE, file)) { - break; - } - if (i == 0 && IObuff[0] == '#' && IObuff[1] == '!') { - // Check shebang. - if (strstr((char *)IObuff + 2, "python2") != NULL) { - requires_py_version = 2; - break; - } - if (strstr((char *)IObuff + 2, "python3") != NULL) { - requires_py_version = 3; - break; - } - } - IObuff[21] = '\0'; - if (STRCMP("# requires python 2.x", IObuff) == 0) { - requires_py_version = 2; - break; - } - if (STRCMP("# requires python 3.x", IObuff) == 0) { - requires_py_version = 3; - break; - } - } - fclose(file); - } - return requires_py_version; -} - - -// Source a python file using the requested python version. -static void source_pyx_file(exarg_T *eap, char_u *fname) -{ - exarg_T ex; - long int v = requires_py_version(fname); - - init_pyxversion(); - if (v == 0) { - // user didn't choose a preference, 'pyx' is used - v = p_pyx; - } - - // now source, if required python version is not supported show - // unobtrusive message. - if (eap == NULL) { - memset(&ex, 0, sizeof(ex)); - } else { - ex = *eap; - } - ex.arg = fname; - ex.cmd = (char_u *)(v == 2 ? "pyfile" : "pyfile3"); - - if (v == 2) { - ex_pyfile(&ex); - } else { - ex_py3file(&ex); - } -} - -// ":pyxfile {fname}" -void ex_pyxfile(exarg_T *eap) -{ - source_pyx_file(eap, eap->arg); -} - -// ":pyx" -void ex_pyx(exarg_T *eap) -{ - init_pyxversion(); - if (p_pyx == 2) { - ex_python(eap); - } else { - ex_python3(eap); - } -} - -// ":pyxdo" -void ex_pyxdo(exarg_T *eap) -{ - init_pyxversion(); - if (p_pyx == 2) { - ex_pydo(eap); - } else { - ex_pydo3(eap); - } -} - /// ":source [{fname}]" void ex_source(exarg_T *eap) { @@ -1957,7 +1823,9 @@ static int source_using_linegetter(void *cookie, LineGetter fgetline, const char sourcing_lnum = 0; const sctx_T save_current_sctx = current_sctx; - current_sctx.sc_sid = SID_STR; + if (current_sctx.sc_sid != SID_LUA) { + current_sctx.sc_sid = SID_STR; + } current_sctx.sc_seq = 0; current_sctx.sc_lnum = save_sourcing_lnum; funccal_entry_T entry; @@ -2038,7 +1906,6 @@ int do_source(char *fname, int check_other, int is_vimrc) char_u *fname_exp; char_u *firstline = NULL; int retval = FAIL; - static int last_current_SID_seq = 0; int save_debug_break_level = debug_break_level; scriptitem_T *si = NULL; proftime_T wait_start; @@ -2086,7 +1953,7 @@ int do_source(char *fname, int check_other, int is_vimrc) } if (cookie.fp == NULL) { - if (p_verbose > 0) { + if (p_verbose > 1) { verbose_enter(); if (sourcing_name == NULL) { smsg(_("could not source \"%s\""), fname); @@ -2162,37 +2029,8 @@ int do_source(char *fname, int check_other, int is_vimrc) funccal_entry_T funccalp_entry; save_funccal(&funccalp_entry); - // Check if this script was sourced before to finds its SID. - // If it's new, generate a new SID. - // Always use a new sequence number. const sctx_T save_current_sctx = current_sctx; - current_sctx.sc_seq = ++last_current_SID_seq; - current_sctx.sc_lnum = 0; - FileID file_id; - bool file_id_ok = os_fileid((char *)fname_exp, &file_id); - assert(script_items.ga_len >= 0); - for (current_sctx.sc_sid = script_items.ga_len; current_sctx.sc_sid > 0; - current_sctx.sc_sid--) { - si = &SCRIPT_ITEM(current_sctx.sc_sid); - // Compare dev/ino when possible, it catches symbolic links. - // Also compare file names, the inode may change when the file was edited. - bool file_id_equal = file_id_ok && si->file_id_valid - && os_fileid_equal(&(si->file_id), &file_id); - if (si->sn_name != NULL - && (file_id_equal || fnamecmp(si->sn_name, fname_exp) == 0)) { - break; - } - } - if (current_sctx.sc_sid == 0) { - si = new_script_item(fname_exp, ¤t_sctx.sc_sid); - fname_exp = vim_strsave(si->sn_name); // used for autocmd - if (file_id_ok) { - si->file_id_valid = true; - si->file_id = file_id; - } else { - si->file_id_valid = false; - } - } + si = get_current_script_id(fname_exp, ¤t_sctx); if (l_do_profiling == PROF_YES) { bool forceit = false; @@ -2225,16 +2063,14 @@ int do_source(char *fname, int check_other, int is_vimrc) firstline = p; } - if (path_with_extension((const char *)fname, "lua")) { - // TODO(shadmansaleh): Properly handle :verbose for lua - // For now change currennt_sctx before sourcing lua files - // So verbose doesn't say everything was done in line 1 since we don't know + if (path_with_extension((const char *)fname_exp, "lua")) { const sctx_T current_sctx_backup = current_sctx; const linenr_T sourcing_lnum_backup = sourcing_lnum; + current_sctx.sc_sid = SID_LUA; current_sctx.sc_lnum = 0; sourcing_lnum = 0; // Source the file as lua - nlua_exec_file((const char *)fname); + nlua_exec_file((const char *)fname_exp); current_sctx = current_sctx_backup; sourcing_lnum = sourcing_lnum_backup; } else { @@ -2307,6 +2143,52 @@ theend: } +/// Check if fname was sourced before to finds its SID. +/// If it's new, generate a new SID. +/// @param[in] fname file path of script +/// @param[out] ret_sctx sctx of this script +scriptitem_T *get_current_script_id(char_u *fname, sctx_T *ret_sctx) +{ + static int last_current_SID_seq = 0; + + sctx_T script_sctx = { .sc_seq = ++last_current_SID_seq, + .sc_lnum = 0, + .sc_sid = 0 }; + FileID file_id; + scriptitem_T *si = NULL; + + bool file_id_ok = os_fileid((char *)fname, &file_id); + assert(script_items.ga_len >= 0); + for (script_sctx.sc_sid = script_items.ga_len; script_sctx.sc_sid > 0; + script_sctx.sc_sid--) { + si = &SCRIPT_ITEM(script_sctx.sc_sid); + // Compare dev/ino when possible, it catches symbolic links. + // Also compare file names, the inode may change when the file was edited. + bool file_id_equal = file_id_ok && si->file_id_valid + && os_fileid_equal(&(si->file_id), &file_id); + if (si->sn_name != NULL + && (file_id_equal || fnamecmp(si->sn_name, fname) == 0)) { + break; + } + } + if (script_sctx.sc_sid == 0) { + si = new_script_item(vim_strsave(fname), &script_sctx.sc_sid); + if (file_id_ok) { + si->file_id_valid = true; + si->file_id = file_id; + } else { + si->file_id_valid = false; + } + } + if (ret_sctx != NULL) { + *ret_sctx = script_sctx; + } + + return si; +} + + + /// ":scriptnames" void ex_scriptnames(exarg_T *eap) { @@ -2323,9 +2205,13 @@ void ex_scriptnames(exarg_T *eap) for (int i = 1; i <= script_items.ga_len && !got_int; i++) { if (SCRIPT_ITEM(i).sn_name != NULL) { - home_replace(NULL, SCRIPT_ITEM(i).sn_name, - NameBuff, MAXPATHL, true); - smsg("%3d: %s", i, NameBuff); + home_replace(NULL, SCRIPT_ITEM(i).sn_name, NameBuff, MAXPATHL, true); + vim_snprintf((char *)IObuff, IOSIZE, "%3d: %s", i, NameBuff); + if (!message_filtered(IObuff)) { + msg_putchar('\n'); + msg_outtrans(IObuff); + line_breakcheck(); + } } } } diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index ea899b660b..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" @@ -61,6 +62,7 @@ // current buffer is locked #define EX_MODIFY 0x100000 // forbidden in non-'modifiable' buffer #define EX_FLAGS 0x200000 // allow flags after count in argument +#define EX_KEEPSCRIPT 0x4000000 // keep sctx of where command was invoked #define EX_FILES (EX_XFILE | EX_EXTRA) // multiple extra files allowed #define EX_FILE1 (EX_FILES | EX_NOSPC) // 1 file, defaults to current file #define EX_WORD1 (EX_EXTRA | EX_NOSPC) // one extra word allowed @@ -90,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. @@ -192,6 +223,7 @@ struct expand { int xp_context; // type of expansion size_t xp_pattern_len; // bytes in xp_pattern before cursor char_u *xp_arg; // completion function + LuaRef xp_luaref; // Ref to Lua completion function sctx_T xp_script_ctx; // SCTX for completion function int xp_backslash; // one of the XP_BS_ values #ifndef BACKSLASH_IN_FILENAME diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 230cbd4b60..d3203bcee8 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -67,8 +67,8 @@ #include "nvim/sign.h" #include "nvim/spell.h" #include "nvim/spellfile.h" -#include "nvim/strings.h" #include "nvim/state.h" +#include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/tag.h" #include "nvim/terminal.h" @@ -78,26 +78,14 @@ #include "nvim/vim.h" #include "nvim/window.h" +static char *e_no_such_user_defined_command_str = N_("E184: No such user-defined command: %s"); +static char *e_no_such_user_defined_command_in_current_buffer_str + = N_("E1237: No such user-defined command in current buffer: %s"); + static int quitmore = 0; static bool ex_pressedreturn = false; -typedef struct ucmd { - char_u *uc_name; // The command name - uint32_t uc_argt; // The argument type - char_u *uc_rep; // The command's replacement string - long uc_def; // The default value for a range/count - int uc_compl; // completion type - cmd_addr_T uc_addr_type; // The command's address type - sctx_T uc_script_ctx; // SCTX where the command was defined - char_u *uc_compl_arg; // completion argument if any -} ucmd_T; - -#define UC_BUFFER 1 // -buffer: local to current buffer - -static garray_T ucmds = { 0, 0, sizeof(ucmd_T), 4, NULL }; - -#define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i]) -#define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i]) +garray_T ucmds = { 0, 0, sizeof(ucmd_T), 4, NULL }; // Whether a command index indicates a user command. #define IS_USER_CMDIDX(idx) ((int)(idx) < 0) @@ -1774,7 +1762,9 @@ static char_u *do_one_cmd(char_u **cmdlinep, int flags, cstack_T *cstack, LineGe ea.regname = *ea.arg++; // for '=' register: accept the rest of the line as an expression if (ea.arg[-1] == '=' && ea.arg[0] != NUL) { - set_expr_line(vim_strsave(ea.arg)); + if (!ea.skip) { + set_expr_line(vim_strsave(ea.arg)); + } ea.arg += STRLEN(ea.arg); } ea.arg = skipwhite(ea.arg); @@ -2713,10 +2703,8 @@ static char_u *find_ucmd(exarg_T *eap, char_u *p, int *full, expand_T *xp, int * bool amb_local = false; // Found ambiguous buffer-local command, // only full match global is accepted. - /* - * Look for buffer-local user commands first, then global ones. - */ - gap = &curbuf->b_ucmds; + // Look for buffer-local user commands first, then global ones. + gap = is_in_cmdwin() ? &prevwin->w_buffer->b_ucmds : &curbuf->b_ucmds; for (;;) { for (j = 0; j < gap->ga_len; j++) { uc = USER_CMD_GA(gap, j); @@ -2761,6 +2749,7 @@ static char_u *find_ucmd(exarg_T *eap, char_u *p, int *full, expand_T *xp, int * *complp = uc->uc_compl; } if (xp != NULL) { + xp->xp_luaref = uc->uc_compl_luaref; xp->xp_arg = uc->uc_compl_arg; xp->xp_script_ctx = uc->uc_script_ctx; xp->xp_script_ctx.sc_lnum += sourcing_lnum; @@ -2906,13 +2895,17 @@ void f_fullcommand(typval_T *argvars, typval_T *rettv, FunPtr fptr) exarg_T ea; char_u *name = argvars[0].vval.v_string; - while (name[0] != NUL && name[0] == ':') { + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + if (name == NULL) { + return; + } + + while (*name == ':') { name++; } name = skip_range(name, NULL); - rettv->v_type = VAR_STRING; - ea.cmd = (*name == '2' || *name == '3') ? name + 1 : name; ea.cmdidx = (cmdidx_T)0; char_u *p = find_command(&ea, NULL); @@ -2921,7 +2914,7 @@ void f_fullcommand(typval_T *argvars, typval_T *rettv, FunPtr fptr) } rettv->vval.v_string = vim_strsave(IS_USER_CMDIDX(ea.cmdidx) - ? get_user_commands(NULL, ea.useridx) + ? get_user_command_name(ea.useridx, ea.cmdidx) : cmdnames[ea.cmdidx].cmd_name); } @@ -4148,7 +4141,11 @@ static linenr_T get_address(exarg_T *eap, char_u **ptr, cmd_addr_T addr_type, in if (!ascii_isdigit(*cmd)) { // '+' is '+1', but '+0' is not '+1' n = 1; } else { - n = getdigits(&cmd, true, 0); + n = getdigits(&cmd, false, MAXLNUM); + if (n == MAXLNUM) { + emsg(_(e_line_number_out_of_range)); + goto error; + } } if (addr_type == ADDR_TABS_RELATIVE) { @@ -4167,6 +4164,10 @@ static linenr_T get_address(exarg_T *eap, char_u **ptr, cmd_addr_T addr_type, in if (i == '-') { lnum -= n; } else { + if (n >= LONG_MAX - lnum) { + emsg(_(e_line_number_out_of_range)); + goto error; + } lnum += n; } } @@ -4385,7 +4386,7 @@ static char_u *replace_makeprg(exarg_T *eap, char_u *p, char_u **cmdlinep) ++i; } len = (int)STRLEN(p); - new_cmdline = xmalloc(STRLEN(program) + i * (len - 2) + 1); + new_cmdline = xmalloc(STRLEN(program) + (size_t)i * (len - 2) + 1); ptr = new_cmdline; while ((pos = (char_u *)strstr((char *)program, "$*")) != NULL) { i = (int)(pos - program); @@ -5166,13 +5167,32 @@ static int check_more(int message, bool forceit) char_u *get_command_name(expand_T *xp, int idx) { if (idx >= CMD_SIZE) { - return get_user_command_name(idx); + return expand_user_command_name(idx); } return cmdnames[idx].cmd_name; } -static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def, - int flags, int compl, char_u *compl_arg, cmd_addr_T addr_type, bool force) +/// Check for a valid user command name +/// +/// If the given {name} is valid, then a pointer to the end of the valid name is returned. +/// Otherwise, returns NULL. +char *uc_validate_name(char *name) +{ + if (ASCII_ISALPHA(*name)) { + while (ASCII_ISALNUM(*name)) { + name++; + } + } + if (!ends_excmd(*name) && !ascii_iswhite(*name)) { + return NULL; + } + + return name; +} + +int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t argt, long def, int flags, + int compl, char_u *compl_arg, LuaRef compl_luaref, cmd_addr_T addr_type, + LuaRef luaref, bool force) FUNC_ATTR_NONNULL_ARG(1, 3) { ucmd_T *cmd = NULL; @@ -5226,6 +5246,8 @@ static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t a XFREE_CLEAR(cmd->uc_rep); XFREE_CLEAR(cmd->uc_compl_arg); + NLUA_CLEAR_REF(cmd->uc_luaref); + NLUA_CLEAR_REF(cmd->uc_compl_luaref); break; } @@ -5255,14 +5277,19 @@ static int uc_add_command(char_u *name, size_t name_len, char_u *rep, uint32_t a cmd->uc_compl = compl; cmd->uc_script_ctx = current_sctx; cmd->uc_script_ctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&cmd->uc_script_ctx); cmd->uc_compl_arg = compl_arg; + cmd->uc_compl_luaref = compl_luaref; cmd->uc_addr_type = addr_type; + cmd->uc_luaref = luaref; return OK; fail: xfree(rep_buf); xfree(compl_arg); + NLUA_CLEAR_REF(luaref); + NLUA_CLEAR_REF(compl_luaref); return FAIL; } @@ -5301,6 +5328,7 @@ static const char *command_complete[] = [EXPAND_CSCOPE] = "cscope", [EXPAND_USER_DEFINED] = "custom", [EXPAND_USER_LIST] = "customlist", + [EXPAND_USER_LUA] = "<Lua function>", [EXPAND_DIFF_BUFFERS] = "diff_buffer", [EXPAND_DIRECTORIES] = "dir", [EXPAND_ENV_VARS] = "environment", @@ -5350,9 +5378,7 @@ static void uc_list(char_u *name, size_t name_len) uint32_t a; // In cmdwin, the alternative buffer should be used. - garray_T *gap = (cmdwin_type != 0 && get_cmdline_type() == NUL) - ? &prevwin->w_buffer->b_ucmds - : &curbuf->b_ucmds; + garray_T *gap = is_in_cmdwin() ? &prevwin->w_buffer->b_ucmds : &curbuf->b_ucmds; for (;;) { for (i = 0; i < gap->ga_len; i++) { cmd = USER_CMD_GA(gap, i); @@ -5524,6 +5550,8 @@ static int uc_scan_attr(char_u *attr, size_t len, uint32_t *argt, long *def, int *flags |= UC_BUFFER; } else if (STRNICMP(attr, "register", len) == 0) { *argt |= EX_REGSTR; + } else if (STRNICMP(attr, "keepscript", len) == 0) { + *argt |= EX_KEEPSCRIPT; } else if (STRNICMP(attr, "bar", len) == 0) { *argt |= EX_TRLBAR; } else { @@ -5678,23 +5706,18 @@ static void ex_command(exarg_T *eap) // Get the name (if any) and skip to the following argument. name = p; - if (ASCII_ISALPHA(*p)) { - while (ASCII_ISALNUM(*p)) { - p++; - } - } - if (!ends_excmd(*p) && !ascii_iswhite(*p)) { + end = (char_u *)uc_validate_name((char *)name); + if (!end) { emsg(_("E182: Invalid command name")); return; } - end = p; - name_len = (int)(end - name); + name_len = (size_t)(end - name); // If there is nothing after the name, and no attributes were specified, // we are listing commands p = skipwhite(end); if (!has_attr && ends_excmd(*p)) { - uc_list(name, end - name); + uc_list(name, name_len); } else if (!ASCII_ISUPPER(*name)) { emsg(_("E183: User defined commands must start with an uppercase letter")); } else if (name_len <= 4 && STRNCMP(name, "Next", name_len) == 0) { @@ -5702,8 +5725,8 @@ static void ex_command(exarg_T *eap) } else if (compl > 0 && (argt & EX_EXTRA) == 0) { emsg(_(e_complete_used_without_nargs)); } else { - uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg, - addr_type_arg, eap->forceit); + uc_add_command(name, name_len, p, argt, def, flags, compl, compl_arg, LUA_NOREF, + addr_type_arg, LUA_NOREF, eap->forceit); } } @@ -5717,11 +5740,13 @@ void ex_comclear(exarg_T *eap) uc_clear(&curbuf->b_ucmds); } -static void free_ucmd(ucmd_T *cmd) +void free_ucmd(ucmd_T *cmd) { xfree(cmd->uc_name); xfree(cmd->uc_rep); xfree(cmd->uc_compl_arg); + NLUA_CLEAR_REF(cmd->uc_compl_luaref); + NLUA_CLEAR_REF(cmd->uc_luaref); } /* @@ -5736,32 +5761,40 @@ static void ex_delcommand(exarg_T *eap) { int i = 0; ucmd_T *cmd = NULL; - int cmp = -1; + int res = -1; garray_T *gap; + const char_u *arg = eap->arg; + bool buffer_only = false; + + if (STRNCMP(arg, "-buffer", 7) == 0 && ascii_iswhite(arg[7])) { + buffer_only = true; + arg = skipwhite(arg + 7); + } gap = &curbuf->b_ucmds; for (;;) { for (i = 0; i < gap->ga_len; i++) { cmd = USER_CMD_GA(gap, i); - cmp = STRCMP(eap->arg, cmd->uc_name); - if (cmp <= 0) { + res = STRCMP(arg, cmd->uc_name); + if (res <= 0) { break; } } - if (gap == &ucmds || cmp == 0) { + if (gap == &ucmds || res == 0 || buffer_only) { break; } gap = &ucmds; } - if (cmp != 0) { - semsg(_("E184: No such user-defined command: %s"), eap->arg); + if (res != 0) { + semsg(_(buffer_only + ? e_no_such_user_defined_command_in_current_buffer_str + : e_no_such_user_defined_command_str), + arg); return; } - xfree(cmd->uc_name); - xfree(cmd->uc_rep); - xfree(cmd->uc_compl_arg); + free_ucmd(cmd); --gap->ga_len; @@ -5770,6 +5803,30 @@ static void ex_delcommand(exarg_T *eap) } } +/// Split a string by unescaped whitespace (space & tab), used for f-args on Lua commands callback. +/// Similar to uc_split_args(), but does not allocate, add quotes, add commas and is an iterator. +/// +/// @note If no separator is found start = 0 and end = length - 1 +/// @param[in] arg String to split +/// @param[in] iter Iteration counter +/// @param[out] start Start of the split +/// @param[out] end End of the split +/// @param[in] length Length of the string +/// @return false if it's the last split (don't call again), true otherwise (call again). +bool uc_split_args_iter(const char_u *arg, int iter, int *start, int *end, int length) +{ + int pos; + *start = *end + (iter > 1 ? 2 : 0); // Skip whitespace after the first split + for (pos = *start; pos < length - 2; pos++) { + if (arg[pos] != '\\' && ascii_iswhite(arg[pos + 1])) { + *end = pos; + return true; + } + } + *end = length - 1; + return false; +} + /* * split and quote args for <f-args> */ @@ -5843,7 +5900,7 @@ static char_u *uc_split_args(char_u *arg, size_t *lenp) return buf; } -static size_t add_cmd_modifier(char_u *buf, char *mod_str, bool *multi_mods) +static size_t add_cmd_modifier(char *buf, char *mod_str, bool *multi_mods) { size_t result = STRLEN(mod_str); if (*multi_mods) { @@ -6035,7 +6092,7 @@ static size_t uc_check_code(char_u *code, size_t len, char_u *buf, ucmd_T *cmd, break; } - case ct_MODS: { + case ct_MODS: result = quote ? 2 : 0; if (buf != NULL) { if (quote) { @@ -6044,76 +6101,13 @@ static size_t uc_check_code(char_u *code, size_t len, char_u *buf, ucmd_T *cmd, *buf = '\0'; } - bool multi_mods = false; - - // :aboveleft and :leftabove - if (cmdmod.split & WSP_ABOVE) { - result += add_cmd_modifier(buf, "aboveleft", &multi_mods); - } - // :belowright and :rightbelow - if (cmdmod.split & WSP_BELOW) { - result += add_cmd_modifier(buf, "belowright", &multi_mods); - } - // :botright - if (cmdmod.split & WSP_BOT) { - result += add_cmd_modifier(buf, "botright", &multi_mods); - } - - typedef struct { - bool *set; - char *name; - } mod_entry_T; - static mod_entry_T mod_entries[] = { - { &cmdmod.browse, "browse" }, - { &cmdmod.confirm, "confirm" }, - { &cmdmod.hide, "hide" }, - { &cmdmod.keepalt, "keepalt" }, - { &cmdmod.keepjumps, "keepjumps" }, - { &cmdmod.keepmarks, "keepmarks" }, - { &cmdmod.keeppatterns, "keeppatterns" }, - { &cmdmod.lockmarks, "lockmarks" }, - { &cmdmod.noswapfile, "noswapfile" } - }; - // the modifiers that are simple flags - for (size_t i = 0; i < ARRAY_SIZE(mod_entries); i++) { - if (*mod_entries[i].set) { - result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods); - } - } - - // TODO(vim): How to support :noautocmd? - // TODO(vim): How to support :sandbox? + result += uc_mods((char *)buf); - // :silent - if (msg_silent > 0) { - result += add_cmd_modifier(buf, emsg_silent > 0 ? "silent!" : "silent", - &multi_mods); - } - // :tab - if (cmdmod.tab > 0) { - result += add_cmd_modifier(buf, "tab", &multi_mods); - } - // :topleft - if (cmdmod.split & WSP_TOP) { - result += add_cmd_modifier(buf, "topleft", &multi_mods); - } - - // TODO(vim): How to support :unsilent? - - // :verbose - if (p_verbose > 0) { - result += add_cmd_modifier(buf, "verbose", &multi_mods); - } - // :vertical - if (cmdmod.split & WSP_VERT) { - result += add_cmd_modifier(buf, "vertical", &multi_mods); - } if (quote && buf != NULL) { buf += result - 2; *buf = '"'; } break; - } case ct_REGISTER: result = eap->regname ? 1 : 0; @@ -6152,6 +6146,76 @@ static size_t uc_check_code(char_u *code, size_t len, char_u *buf, ucmd_T *cmd, return result; } +size_t uc_mods(char *buf) +{ + size_t result = 0; + bool multi_mods = false; + + // :aboveleft and :leftabove + if (cmdmod.split & WSP_ABOVE) { + result += add_cmd_modifier(buf, "aboveleft", &multi_mods); + } + // :belowright and :rightbelow + if (cmdmod.split & WSP_BELOW) { + result += add_cmd_modifier(buf, "belowright", &multi_mods); + } + // :botright + if (cmdmod.split & WSP_BOT) { + result += add_cmd_modifier(buf, "botright", &multi_mods); + } + + typedef struct { + bool *set; + char *name; + } mod_entry_T; + static mod_entry_T mod_entries[] = { + { &cmdmod.browse, "browse" }, + { &cmdmod.confirm, "confirm" }, + { &cmdmod.hide, "hide" }, + { &cmdmod.keepalt, "keepalt" }, + { &cmdmod.keepjumps, "keepjumps" }, + { &cmdmod.keepmarks, "keepmarks" }, + { &cmdmod.keeppatterns, "keeppatterns" }, + { &cmdmod.lockmarks, "lockmarks" }, + { &cmdmod.noswapfile, "noswapfile" } + }; + // the modifiers that are simple flags + for (size_t i = 0; i < ARRAY_SIZE(mod_entries); i++) { + if (*mod_entries[i].set) { + result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods); + } + } + + // TODO(vim): How to support :noautocmd? + // TODO(vim): How to support :sandbox? + + // :silent + if (msg_silent > 0) { + result += add_cmd_modifier(buf, emsg_silent > 0 ? "silent!" : "silent", &multi_mods); + } + // :tab + if (cmdmod.tab > 0) { + result += add_cmd_modifier(buf, "tab", &multi_mods); + } + // :topleft + if (cmdmod.split & WSP_TOP) { + result += add_cmd_modifier(buf, "topleft", &multi_mods); + } + + // TODO(vim): How to support :unsilent? + + // :verbose + if (p_verbose > 0) { + result += add_cmd_modifier(buf, "verbose", &multi_mods); + } + // :vertical + if (cmdmod.split & WSP_VERT) { + result += add_cmd_modifier(buf, "vertical", &multi_mods); + } + + return result; +} + static void do_ucmd(exarg_T *eap) { char_u *buf; @@ -6174,6 +6238,11 @@ static void do_ucmd(exarg_T *eap) cmd = USER_CMD_GA(&curbuf->b_ucmds, eap->useridx); } + if (cmd->uc_luaref > 0) { + nlua_do_ucmd(cmd, eap); + return; + } + /* * Replace <> in the command by the arguments. * First round: "buf" is NULL, compute length, allocate "buf". @@ -6199,7 +6268,6 @@ static void do_ucmd(exarg_T *eap) // K_SPECIAL has been put in the buffer as K_SPECIAL // KS_SPECIAL KE_FILLER, like for mappings, but // do_cmdline() doesn't handle that, so convert it back. - // Also change K_SPECIAL KS_EXTRA KE_CSI into CSI. len = ksp - p; if (len > 0) { memmove(q, p, len); @@ -6252,15 +6320,19 @@ static void do_ucmd(exarg_T *eap) buf = xmalloc(totlen + 1); } - current_sctx.sc_sid = cmd->uc_script_ctx.sc_sid; + if ((cmd->uc_argt & EX_KEEPSCRIPT) == 0) { + current_sctx.sc_sid = cmd->uc_script_ctx.sc_sid; + } (void)do_cmdline(buf, eap->getline, eap->cookie, DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED); - current_sctx = save_current_sctx; + if ((cmd->uc_argt & EX_KEEPSCRIPT) == 0) { + current_sctx = save_current_sctx; + } xfree(buf); xfree(split_buf); } -static char_u *get_user_command_name(int idx) +static char_u *expand_user_command_name(int idx) { return get_user_commands(NULL, idx - CMD_SIZE); } @@ -6279,9 +6351,7 @@ char_u *get_user_commands(expand_T *xp FUNC_ATTR_UNUSED, int idx) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { // In cmdwin, the alternative buffer should be used. - const buf_T *const buf = (cmdwin_type != 0 && get_cmdline_type() == NUL) - ? prevwin->w_buffer - : curbuf; + const buf_T *const buf = is_in_cmdwin() ? prevwin->w_buffer : curbuf; if (idx < buf->b_ucmds.ga_len) { return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name; @@ -6293,6 +6363,24 @@ char_u *get_user_commands(expand_T *xp FUNC_ATTR_UNUSED, int idx) return NULL; } +// Get the name of user command "idx". "cmdidx" can be CMD_USER or +// CMD_USER_BUF. +// Returns NULL if the command is not found. +static char_u *get_user_command_name(int idx, int cmdidx) +{ + if (cmdidx == CMD_USER && idx < ucmds.ga_len) { + return USER_CMD(idx)->uc_name; + } + if (cmdidx == CMD_USER_BUF) { + // In cmdwin, the alternative buffer should be used. + buf_T *buf = is_in_cmdwin() ? prevwin->w_buffer : curbuf; + if (idx < buf->b_ucmds.ga_len) { + return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name; + } + } + return NULL; +} + /* * Function given to ExpandGeneric() to obtain the list of user command * attributes. @@ -6301,7 +6389,7 @@ char_u *get_user_cmd_flags(expand_T *xp, int idx) { static char *user_cmd_flags[] = { "addr", "bang", "bar", "buffer", "complete", "count", - "nargs", "range", "register" }; + "nargs", "range", "register", "keepscript" }; if (idx >= (int)ARRAY_SIZE(user_cmd_flags)) { return NULL; @@ -6577,7 +6665,7 @@ static void ex_quit(exarg_T *eap) } not_exiting(); // close window; may free buffer - win_close(wp, !buf_hide(wp->w_buffer) || eap->forceit); + win_close(wp, !buf_hide(wp->w_buffer) || eap->forceit, eap->forceit); } } @@ -6692,7 +6780,7 @@ void ex_win_close(int forceit, win_T *win, tabpage_T *tp) // free buffer when not hiding it or when it's a scratch buffer if (tp == NULL) { - win_close(win, !need_hide && !buf_hide(buf)); + win_close(win, !need_hide && !buf_hide(buf), forceit); } else { win_close_othertab(win, !need_hide && !buf_hide(buf), tp); } @@ -6856,7 +6944,7 @@ static void ex_hide(exarg_T *eap) // ":hide" or ":hide | cmd": hide current window if (!eap->skip) { if (eap->addr_count == 0) { - win_close(curwin, false); // don't free buffer + win_close(curwin, false, eap->forceit); // don't free buffer } else { int winnr = 0; win_T *win = NULL; @@ -6871,7 +6959,7 @@ static void ex_hide(exarg_T *eap) if (win == NULL) { win = lastwin; } - win_close(win, false); + win_close(win, false, eap->forceit); } } } @@ -6929,7 +7017,7 @@ static void ex_exit(exarg_T *eap) } not_exiting(); // Quit current window, may free the buffer. - win_close(curwin, !buf_hide(curwin->w_buffer)); + win_close(curwin, !buf_hide(curwin->w_buffer), eap->forceit); } } @@ -7443,7 +7531,7 @@ static void ex_edit(exarg_T *eap) do_exedit(eap, NULL); } -/// ":edit <file>" command and alikes. +/// ":edit <file>" command and alike. /// /// @param old_curwin curwin before doing a split or NULL void do_exedit(exarg_T *eap, win_T *old_curwin) @@ -7489,9 +7577,8 @@ void do_exedit(exarg_T *eap, win_T *old_curwin) if ((eap->cmdidx == CMD_new || eap->cmdidx == CMD_tabnew || eap->cmdidx == CMD_tabedit - || eap->cmdidx == CMD_vnew - ) && *eap->arg == NUL) { - // ":new" or ":tabnew" without argument: edit an new empty buffer + || eap->cmdidx == CMD_vnew) && *eap->arg == NUL) { + // ":new" or ":tabnew" without argument: edit a new empty buffer setpcmark(); (void)do_ecmd(0, NULL, NULL, eap, ECMD_ONE, ECMD_HIDE + (eap->forceit ? ECMD_FORCEIT : 0), @@ -7531,7 +7618,7 @@ void do_exedit(exarg_T *eap, win_T *old_curwin) // Reset the error/interrupt/exception state here so that // aborting() returns FALSE when closing a window. enter_cleanup(&cs); - win_close(curwin, !need_hide && !buf_hide(curbuf)); + win_close(curwin, !need_hide && !buf_hide(curbuf), false); // Restore the error/interrupt/exception state if not // discarded by a new aborting error, interrupt, or @@ -7743,7 +7830,7 @@ static char_u *get_prevdir(CdScope scope) /// Deal with the side effects of changing the current directory. /// /// @param scope Scope of the function call (global, tab or window). -void post_chdir(CdScope scope, bool trigger_dirchanged) +static void post_chdir(CdScope scope, bool trigger_dirchanged) { // Always overwrite the window-local CWD. XFREE_CLEAR(curwin->w_localdir); @@ -7784,7 +7871,7 @@ void post_chdir(CdScope scope, bool trigger_dirchanged) shorten_fnames(true); if (trigger_dirchanged) { - do_autocmd_dirchanged(cwd, scope, kCdCauseManual); + do_autocmd_dirchanged(cwd, scope, kCdCauseManual, false); } } @@ -7794,14 +7881,11 @@ void post_chdir(CdScope scope, bool trigger_dirchanged) /// @return true if the directory is successfully changed. bool changedir_func(char_u *new_dir, CdScope scope) { - char_u *tofree; - char_u *pdir = NULL; - bool retval = false; - if (new_dir == NULL || allbuf_locked()) { return false; } + char_u *pdir = NULL; // ":cd -": Change to previous directory if (STRCMP(new_dir, "-") == 0) { pdir = get_prevdir(scope); @@ -7812,26 +7896,12 @@ bool changedir_func(char_u *new_dir, CdScope scope) new_dir = pdir; } - // Free the previous directory - tofree = get_prevdir(scope); - if (os_dirname(NameBuff, MAXPATHL) == OK) { pdir = vim_strsave(NameBuff); } else { pdir = NULL; } - switch (scope) { - case kCdScopeTabpage: - curtab->tp_prevdir = pdir; - break; - case kCdScopeWindow: - curwin->w_prevdir = pdir; - break; - default: - prev_dir = pdir; - } - // For UNIX ":cd" means: go to home directory. // On other systems too if 'cdhome' is set. #if defined(UNIX) @@ -7844,17 +7914,33 @@ bool changedir_func(char_u *new_dir, CdScope scope) new_dir = NameBuff; } - bool dir_differs = new_dir == NULL || pdir == NULL - || pathcmp((char *)pdir, (char *)new_dir, -1) != 0; - if (new_dir != NULL && (!dir_differs || vim_chdir(new_dir) == 0)) { - post_chdir(scope, dir_differs); - retval = true; - } else { - emsg(_(e_failed)); + bool dir_differs = pdir == NULL || pathcmp((char *)pdir, (char *)new_dir, -1) != 0; + if (dir_differs) { + do_autocmd_dirchanged((char *)new_dir, scope, kCdCauseManual, true); + if (vim_chdir(new_dir) != 0) { + emsg(_(e_failed)); + xfree(pdir); + return false; + } } - xfree(tofree); - return retval; + char_u **pp; + switch (scope) { + case kCdScopeTabpage: + pp = &curtab->tp_prevdir; + break; + case kCdScopeWindow: + pp = &curwin->w_prevdir; + break; + default: + pp = &prev_dir; + } + xfree(*pp); + *pp = pdir; + + post_chdir(scope, dir_differs); + + return true; } /// ":cd", ":tcd", ":lcd", ":chdir", "tchdir" and ":lchdir". @@ -8102,6 +8188,7 @@ static void ex_put(exarg_T *eap) eap->forceit = TRUE; } curwin->w_cursor.lnum = eap->line2; + check_cursor_col(); do_put(eap->regname, NULL, eap->forceit ? BACKWARD : FORWARD, 1, PUT_LINE|PUT_CURSLINE); } @@ -8623,7 +8710,7 @@ static void ex_normal(exarg_T *eap) return; } - // vgetc() expects a CSI and K_SPECIAL to have been escaped. Don't do + // vgetc() expects K_SPECIAL to have been escaped. Don't do // this for the K_SPECIAL leading byte, otherwise special keys will not // work. { @@ -8632,8 +8719,7 @@ static void ex_normal(exarg_T *eap) // Count the number of characters to be escaped. for (p = eap->arg; *p != NUL; p++) { for (l = utfc_ptr2len(p) - 1; l > 0; l--) { - if (*++p == K_SPECIAL // trailbyte K_SPECIAL or CSI - ) { + if (*++p == K_SPECIAL) { // trailbyte K_SPECIAL len += 2; } } @@ -9542,23 +9628,34 @@ static void ex_filetype(exarg_T *eap) } } -/// Set all :filetype options ON if user did not explicitly set any to OFF. -void filetype_maybe_enable(void) +/// Source ftplugin.vim and indent.vim to create the necessary FileType +/// autocommands. We do this separately from filetype.vim so that these +/// autocommands will always fire first (and thus can be overriden) while still +/// allowing general filetype detection to be disabled in the user's init file. +void filetype_plugin_enable(void) { - if (filetype_detect == kNone) { - source_runtime(FILETYPE_FILE, true); - filetype_detect = kTrue; - } if (filetype_plugin == kNone) { - source_runtime(FTPLUGIN_FILE, true); + source_runtime(FTPLUGIN_FILE, DIP_ALL); filetype_plugin = kTrue; } if (filetype_indent == kNone) { - source_runtime(INDENT_FILE, true); + source_runtime(INDENT_FILE, DIP_ALL); filetype_indent = kTrue; } } +/// Enable filetype detection if the user did not explicitly disable it. +void filetype_maybe_enable(void) +{ + if (filetype_detect == kNone) { + // Normally .vim files are sourced before .lua files when both are + // supported, but we reverse the order here because we want the Lua + // autocommand to be defined first so that it runs first + source_runtime(FILETYPE_FILE, DIP_ALL); + filetype_detect = kTrue; + } +} + /// ":setfiletype [FALLBACK] {name}" static void ex_setfiletype(exarg_T *eap) { @@ -9585,18 +9682,6 @@ static void ex_digraphs(exarg_T *eap) } } -static void ex_set(exarg_T *eap) -{ - int flags = 0; - - if (eap->cmdidx == CMD_setlocal) { - flags = OPT_LOCAL; - } else if (eap->cmdidx == CMD_setglobal) { - flags = OPT_GLOBAL; - } - (void)do_set(eap->arg, flags); -} - void set_no_hlsearch(bool flag) { no_hlsearch = flag; @@ -9831,6 +9916,7 @@ Dictionary commands_array(buf_T *buf) PUT(d, "bang", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_BANG))); PUT(d, "bar", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_TRLBAR))); PUT(d, "register", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_REGSTR))); + PUT(d, "keepscript", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_KEEPSCRIPT))); switch (cmd->uc_argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) { case 0: diff --git a/src/nvim/ex_docmd.h b/src/nvim/ex_docmd.h index a302d4a3c5..abf6ec347b 100644 --- a/src/nvim/ex_docmd.h +++ b/src/nvim/ex_docmd.h @@ -32,6 +32,26 @@ typedef struct { tasave_T tabuf; } save_state_T; +typedef struct ucmd { + char_u *uc_name; // The command name + uint32_t uc_argt; // The argument type + char_u *uc_rep; // The command's replacement string + long uc_def; // The default value for a range/count + int uc_compl; // completion type + cmd_addr_T uc_addr_type; // The command's address type + sctx_T uc_script_ctx; // SCTX where the command was defined + char_u *uc_compl_arg; // completion argument if any + LuaRef uc_compl_luaref; // Reference to Lua completion function + LuaRef uc_luaref; // Reference to Lua function +} ucmd_T; + +#define UC_BUFFER 1 // -buffer: local to current buffer + +extern garray_T ucmds; + +#define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i]) +#define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i]) + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_docmd.h.generated.h" #endif diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index b1c59a607c..851828afcf 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -599,7 +599,7 @@ static void catch_exception(except_T *excp) { excp->caught = caught_stack; caught_stack = excp; - set_vim_var_string(VV_EXCEPTION, (char *)excp->value, -1); + set_vim_var_string(VV_EXCEPTION, excp->value, -1); if (*excp->throw_name != NUL) { if (excp->throw_lnum != 0) { vim_snprintf((char *)IObuff, IOSIZE, _("%s, line %" PRId64), @@ -650,7 +650,7 @@ static void finish_exception(except_T *excp) } caught_stack = caught_stack->caught; if (caught_stack != NULL) { - set_vim_var_string(VV_EXCEPTION, (char *)caught_stack->value, -1); + set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1); if (*caught_stack->throw_name != NUL) { if (caught_stack->throw_lnum != 0) { vim_snprintf((char *)IObuff, IOSIZE, @@ -733,7 +733,7 @@ static void report_pending(int action, int pending, void *value) vim_snprintf((char *)IObuff, IOSIZE, mesg, _("Exception")); mesg = (char *)concat_str(IObuff, (char_u *)": %s"); - s = (char *)((except_T *)value)->value; + s = ((except_T *)value)->value; } else if ((pending & CSTP_ERROR) && (pending & CSTP_INTERRUPT)) { s = _("Error and interrupt"); } else if (pending & CSTP_ERROR) { @@ -1620,7 +1620,7 @@ void ex_endtry(exarg_T *eap) // the finally clause. The latter case need not be tested since then // anything pending has already been discarded. bool skip = did_emsg || got_int || current_exception - || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE); + || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE); if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY)) { eap->errmsg = get_end_emsg(cstack); diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ba2238ace2..30287cd6f2 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -311,7 +311,7 @@ static char_u *get_healthcheck_names(expand_T *xp, int idx) healthchecks.last_gen = last_prompt_id; } return idx < - (int)healthchecks.names.ga_len ? ((char_u **)(healthchecks.names.ga_data))[idx] : NULL; + healthchecks.names.ga_len ? ((char_u **)(healthchecks.names.ga_data))[idx] : NULL; } /// Transform healthcheck file path into it's name. @@ -772,7 +772,7 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) ccline.cmdindent = (s->firstc > 0 ? s->indent : 0); // alloc initial ccline.cmdbuff - alloc_cmdbuff(exmode_active ? 250 : s->indent + 1); + alloc_cmdbuff(indent + 50); ccline.cmdlen = ccline.cmdpos = 0; ccline.cmdbuff[0] = NUL; @@ -1024,11 +1024,13 @@ static int command_line_execute(VimState *state, int key) CommandLineState *s = (CommandLineState *)state; s->c = key; - if (s->c == K_EVENT || s->c == K_COMMAND) { + if (s->c == K_EVENT || s->c == K_COMMAND || s->c == K_LUA) { if (s->c == K_EVENT) { state_handle_k_event(); - } else { + } else if (s->c == K_COMMAND) { do_cmdline(NULL, getcmdkeycmd, NULL, DOCMD_NOWAIT); + } else { + map_execute_lua(); } if (!cmdline_was_last_drawn) { @@ -4983,6 +4985,9 @@ static int ExpandFromContext(expand_T *xp, char_u *pat, int *num_file, char_u ** if (xp->xp_context == EXPAND_USER_LIST) { return ExpandUserList(xp, num_file, file); } + if (xp->xp_context == EXPAND_USER_LUA) { + return ExpandUserLua(xp, num_file, file); + } if (xp->xp_context == EXPAND_PACKADD) { return ExpandPackAddDir(pat, num_file, file); } @@ -5044,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 }, @@ -5411,6 +5416,35 @@ static int ExpandUserList(expand_T *xp, int *num_file, char_u ***file) return OK; } +static int ExpandUserLua(expand_T *xp, int *num_file, char_u ***file) +{ + typval_T rettv; + nlua_call_user_expand_func(xp, &rettv); + if (rettv.v_type != VAR_LIST) { + tv_clear(&rettv); + return FAIL; + } + + list_T *const retlist = rettv.vval.v_list; + + garray_T ga; + ga_init(&ga, (int)sizeof(char *), 3); + // Loop over the items in the list. + TV_LIST_ITER_CONST(retlist, li, { + if (TV_LIST_ITEM_TV(li)->v_type != VAR_STRING + || TV_LIST_ITEM_TV(li)->vval.v_string == NULL) { + continue; // Skip non-string items and empty strings. + } + + GA_APPEND(char *, &ga, xstrdup((const char *)TV_LIST_ITEM_TV(li)->vval.v_string)); + }); + tv_list_unref(retlist); + + *file = ga.ga_data; + *num_file = ga.ga_len; + return OK; +} + /// Expand color scheme, compiler or filetype names. /// Search from 'runtimepath': /// 'runtimepath'/{dirnames}/{pat}.vim @@ -6529,7 +6563,7 @@ static int open_cmdwin(void) set_bufref(&bufref, curbuf); win_goto(old_curwin); if (win_valid(wp) && wp != curwin) { - win_close(wp, true); + win_close(wp, true, false); } // win_close() may have already wiped the buffer when 'bh' is @@ -6553,6 +6587,13 @@ static int open_cmdwin(void) return cmdwin_result; } +/// @return true if in the cmdwin, not editing the command line. +bool is_in_cmdwin(void) + FUNC_ATTR_PURE +{ + return cmdwin_type != 0 && get_cmdline_type() == NUL; +} + /// Get script string /// /// Used for commands which accept either `:command script` or diff --git a/src/nvim/ex_session.c b/src/nvim/ex_session.c index a37cad9f2d..ca07174543 100644 --- a/src/nvim/ex_session.c +++ b/src/nvim/ex_session.c @@ -338,14 +338,26 @@ static int put_view(FILE *fd, win_T *wp, int add_edit, unsigned *flagp, int curr // Edit the file. Skip this when ":next" already did it. if (add_edit && (!did_next || wp->w_arg_idx_invalid)) { - char *fname_esc = - ses_escape_fname(ses_get_fname(wp->w_buffer, flagp), flagp); - // - // Load the file. - // - if (wp->w_buffer->b_ffname != NULL - && (!bt_nofile(wp->w_buffer) - || wp->w_buffer->terminal)) { + char *fname_esc = ses_escape_fname(ses_get_fname(wp->w_buffer, flagp), flagp); + if (bt_help(wp->w_buffer)) { + char *curtag = ""; + + // A help buffer needs some options to be set. + // First, create a new empty buffer with "buftype=help". + // Then ":help" will re-use both the buffer and the window and set + // the options, even when "options" is not in 'sessionoptions'. + if (0 < wp->w_tagstackidx && wp->w_tagstackidx <= wp->w_tagstacklen) { + curtag = (char *)wp->w_tagstack[wp->w_tagstackidx - 1].tagname; + } + + if (put_line(fd, "enew | setl bt=help") == FAIL + || fprintf(fd, "help %s", curtag) < 0 || put_eol(fd) == FAIL) { + return FAIL; + } + } else if (wp->w_buffer->b_ffname != NULL + && (!bt_nofile(wp->w_buffer) || wp->w_buffer->terminal)) { + // Load the file. + // Editing a file in this buffer: use ":edit file". // This may have side effects! (e.g., compressed or network file). // @@ -579,6 +591,24 @@ static int makeopens(FILE *fd, char_u *dirnow) return FAIL; } + // Put all buffers into the buffer list. + // Do it very early to preserve buffer order after loading session (which + // can be disrupted by prior `edit` or `tabedit` calls). + FOR_ALL_BUFFERS(buf) { + if (!(only_save_windows && buf->b_nwindows == 0) + && !(buf->b_help && !(ssop_flags & SSOP_HELP)) + && buf->b_fname != NULL + && buf->b_p_bl) { + if (fprintf(fd, "badd +%" PRId64 " ", + buf->b_wininfo == NULL + ? (int64_t)1L + : (int64_t)buf->b_wininfo->wi_fpos.lnum) < 0 + || ses_fname(fd, buf, &ssop_flags, true) == FAIL) { + return FAIL; + } + } + } + // the global argument list if (ses_arglist(fd, "argglobal", &global_alist.al_ga, !(ssop_flags & SSOP_CURDIR), &ssop_flags) == FAIL) { @@ -614,7 +644,10 @@ static int makeopens(FILE *fd, char_u *dirnow) // Similar to ses_win_rec() below, populate the tab pages first so // later local options won't be copied to the new tabs. FOR_ALL_TABS(tp) { - if (tp->tp_next != NULL && put_line(fd, "tabnew") == FAIL) { + // Use `bufhidden=wipe` to remove empty "placeholder" buffers once + // they are not needed. This prevents creating extra buffers (see + // cause of Vim patch 8.1.0829) + if (tp->tp_next != NULL && put_line(fd, "tabnew +setlocal\\ bufhidden=wipe") == FAIL) { return FAIL; } } @@ -792,25 +825,6 @@ static int makeopens(FILE *fd, char_u *dirnow) return FAIL; } - // Now put the remaining buffers into the buffer list. - // This is near the end, so that when 'hidden' is set we don't create extra - // buffers. If the buffer was already created with another command the - // ":badd" will have no effect. - FOR_ALL_BUFFERS(buf) { - if (!(only_save_windows && buf->b_nwindows == 0) - && !(buf->b_help && !(ssop_flags & SSOP_HELP)) - && buf->b_fname != NULL - && buf->b_p_bl) { - if (fprintf(fd, "badd +%" PRId64 " ", - buf->b_wininfo == NULL - ? (int64_t)1L - : (int64_t)buf->b_wininfo->wi_fpos.lnum) < 0 - || ses_fname(fd, buf, &ssop_flags, true) == FAIL) { - return FAIL; - } - } - } - // // Wipe out an empty unnamed buffer we started in. // diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index c4d8f75a21..48e57e20e1 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -48,28 +48,29 @@ # include "extmark.c.generated.h" #endif -static ExtmarkNs *buf_ns_ref(buf_T *buf, uint64_t ns_id, bool put) +static uint32_t *buf_ns_ref(buf_T *buf, uint32_t ns_id, bool put) { - return map_ref(uint64_t, ExtmarkNs)(buf->b_extmark_ns, ns_id, put); + return map_ref(uint32_t, uint32_t)(buf->b_extmark_ns, ns_id, put); } /// Create or update an extmark /// /// must not be used during iteration! -/// @returns the internal mark id -uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t *idp, int row, colnr_T col, int end_row, - colnr_T end_col, Decoration *decor, bool right_gravity, bool end_right_gravity, - ExtmarkOp op) +void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col, int end_row, + colnr_T end_col, Decoration *decor, bool right_gravity, bool end_right_gravity, + ExtmarkOp op) { - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); - assert(ns != NULL); - mtpos_t old_pos; - uint64_t mark = 0; - uint64_t id = idp ? *idp : 0; + uint32_t *ns = buf_ns_ref(buf, ns_id, true); + uint32_t id = idp ? *idp : 0; + bool decor_full = false; uint8_t decor_level = kDecorLevelNone; // no decor if (decor) { + if (kv_size(decor->virt_text) || kv_size(decor->virt_lines)) { + decor_full = true; + decor = xmemdup(decor, sizeof *decor); + } decor_level = kDecorLevelVisible; // decor affects redraw if (kv_size(decor->virt_lines)) { decor_level = kDecorLevelVirtLine; // decor affects horizontal size @@ -77,50 +78,64 @@ uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t *idp, int row, colnr_T } if (id == 0) { - id = ns->free_id++; + id = ++*ns; } else { - uint64_t old_mark = map_get(uint64_t, uint64_t)(ns->map, id); - if (old_mark) { - if (old_mark & MARKTREE_PAIRED_FLAG || end_row > -1) { + MarkTreeIter itr[1] = { 0 }; + mtkey_t old_mark = marktree_lookup_ns(buf->b_marktree, ns_id, id, false, itr); + if (old_mark.id) { + if (mt_paired(old_mark) || end_row > -1) { extmark_del(buf, ns_id, id); } else { - MarkTreeIter itr[1] = { 0 }; - old_pos = marktree_lookup(buf->b_marktree, old_mark, itr); + // TODO(bfredl): we need to do more if "revising" a decoration mark. assert(itr->node); - if (old_pos.row == row && old_pos.col == col) { - ExtmarkItem it = map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, - old_mark); - if (it.decor) { - decor_remove(buf, row, row, it.decor); + if (old_mark.pos.row == row && old_mark.pos.col == col) { + if (marktree_decor_level(old_mark) > kDecorLevelNone) { + decor_remove(buf, row, row, old_mark.decor_full); + old_mark.decor_full = NULL; + } + old_mark.flags = 0; + if (decor_full) { + old_mark.decor_full = decor; + } else if (decor) { + old_mark.hl_id = decor->hl_id; + // Workaround: the gcc compiler of functionaltest-lua build + // apparently incapable of handling basic integer constants. + // This can be underanged as soon as we bump minimal gcc version. + old_mark.flags = (uint16_t)(old_mark.flags + | (decor->hl_eol ? (uint16_t)MT_FLAG_HL_EOL : (uint16_t)0)); + old_mark.priority = decor->priority; } - mark = marktree_revise(buf->b_marktree, itr, decor_level); + marktree_revise(buf->b_marktree, itr, decor_level, old_mark); goto revised; } marktree_del_itr(buf->b_marktree, itr, false); } } else { - ns->free_id = MAX(ns->free_id, id+1); + *ns = MAX(*ns, id); } } - if (end_row > -1) { - mark = marktree_put_pair(buf->b_marktree, - row, col, right_gravity, - end_row, end_col, end_right_gravity, decor_level); - } else { - mark = marktree_put(buf->b_marktree, row, col, right_gravity, decor_level); + mtkey_t mark = { { row, col }, ns_id, id, 0, + mt_flags(right_gravity, decor_level), 0, NULL }; + if (decor_full) { + mark.decor_full = decor; + } else if (decor) { + mark.hl_id = decor->hl_id; + // workaround: see above + mark.flags = (uint16_t)(mark.flags | (decor->hl_eol ? (uint16_t)MT_FLAG_HL_EOL : (uint16_t)0)); + mark.priority = decor->priority; } -revised: - map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, - (ExtmarkItem){ ns_id, id, decor }); - map_put(uint64_t, uint64_t)(ns->map, id, mark); + marktree_put(buf->b_marktree, mark, end_row, end_col, end_right_gravity); +revised: if (op != kExtmarkNoUndo) { // TODO(bfredl): this doesn't cover all the cases and probably shouldn't // be done "prematurely". Any movement in undo history might necessitate - // adding new marks to old undo headers. - u_extmark_set(buf, mark, row, col); + // adding new marks to old undo headers. add a test case for this (doesn't + // fail extmark_spec.lua, and it should) + uint64_t mark_id = mt_lookup_id(ns_id, id, false); + u_extmark_set(buf, mark_id, row, col); } if (decor) { @@ -133,18 +148,17 @@ revised: if (idp) { *idp = id; } - return mark; } static bool extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col) { MarkTreeIter itr[1] = { 0 }; - mtpos_t pos = marktree_lookup(buf->b_marktree, mark, itr); - if (pos.row == -1) { + mtkey_t key = marktree_lookup(buf->b_marktree, mark, itr); + if (key.pos.row == -1) { return false; } - if (pos.row == row && pos.col == col) { + if (key.pos.row == row && key.pos.col == col) { return true; } @@ -154,45 +168,35 @@ static bool extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col) // Remove an extmark // Returns 0 on missing id -bool extmark_del(buf_T *buf, uint64_t ns_id, uint64_t id) +bool extmark_del(buf_T *buf, uint32_t ns_id, uint32_t id) { - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, false); - if (!ns) { - return false; - } - - uint64_t mark = map_get(uint64_t, uint64_t)(ns->map, id); - if (!mark) { + MarkTreeIter itr[1] = { 0 }; + mtkey_t key = marktree_lookup_ns(buf->b_marktree, ns_id, id, false, itr); + if (!key.id) { return false; } - - MarkTreeIter itr[1] = { 0 }; - mtpos_t pos = marktree_lookup(buf->b_marktree, mark, itr); - assert(pos.row >= 0); + assert(key.pos.row >= 0); marktree_del_itr(buf->b_marktree, itr, false); - ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark); - mtpos_t pos2 = pos; - if (mark & MARKTREE_PAIRED_FLAG) { - pos2 = marktree_lookup(buf->b_marktree, mark|MARKTREE_END_FLAG, itr); - assert(pos2.row >= 0); + mtkey_t key2 = key; + + if (mt_paired(key)) { + key2 = marktree_lookup_ns(buf->b_marktree, ns_id, id, true, itr); + assert(key2.pos.row >= 0); marktree_del_itr(buf->b_marktree, itr, false); } - if (item.decor) { - decor_remove(buf, pos.row, pos2.row, item.decor); + if (marktree_decor_level(key) > kDecorLevelNone) { + decor_remove(buf, key.pos.row, key2.pos.row, key.decor_full); } - map_del(uint64_t, uint64_t)(ns->map, id); - map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark); - // TODO(bfredl): delete it from current undo header, opportunistically? return true; } // Free extmarks in a ns between lines // if ns = 0, it means clear all namespaces -bool extmark_clear(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_row, colnr_T u_col) +bool extmark_clear(buf_T *buf, uint32_t ns_id, int l_row, colnr_T l_col, int u_row, colnr_T u_col) { if (!map_size(buf->b_extmark_ns)) { return false; @@ -201,68 +205,58 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_r bool marks_cleared = false; bool all_ns = (ns_id == 0); - ExtmarkNs *ns = NULL; + uint32_t *ns = NULL; if (!all_ns) { ns = buf_ns_ref(buf, ns_id, false); if (!ns) { // nothing to do return false; } - - // TODO(bfredl): if map_size(ns->map) << buf->b_marktree.n_nodes - // it could be faster to iterate over the map instead } // the value is either zero or the lnum (row+1) if highlight was present. static Map(uint64_t, ssize_t) delete_set = MAP_INIT; - typedef struct { Decoration *decor; int row1; } DecorItem; + typedef struct { int row1; } DecorItem; static kvec_t(DecorItem) decors; MarkTreeIter itr[1] = { 0 }; marktree_itr_get(buf->b_marktree, l_row, l_col, itr); while (true) { - mtmark_t mark = marktree_itr_current(itr); - if (mark.row < 0 - || mark.row > u_row - || (mark.row == u_row && mark.col > u_col)) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0 + || mark.pos.row > u_row + || (mark.pos.row == u_row && mark.pos.col > u_col)) { break; } - ssize_t *del_status = map_ref(uint64_t, ssize_t)(&delete_set, mark.id, + ssize_t *del_status = map_ref(uint64_t, ssize_t)(&delete_set, mt_lookup_key(mark), false); if (del_status) { marktree_del_itr(buf->b_marktree, itr, false); if (*del_status >= 0) { // we had a decor_id DecorItem it = kv_A(decors, *del_status); - decor_remove(buf, it.row1, mark.row, it.decor); + decor_remove(buf, it.row1, mark.pos.row, mark.decor_full); } - map_del(uint64_t, ssize_t)(&delete_set, mark.id); + map_del(uint64_t, ssize_t)(&delete_set, mt_lookup_key(mark)); continue; } - uint64_t start_id = mark.id & ~MARKTREE_END_FLAG; - ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, - start_id); - - assert(item.ns_id > 0 && item.mark_id > 0); - if (item.mark_id > 0 && (item.ns_id == ns_id || all_ns)) { + assert(mark.ns > 0 && mark.id > 0); + if (mark.ns == ns_id || all_ns) { marks_cleared = true; - if (mark.id & MARKTREE_PAIRED_FLAG) { - uint64_t other = mark.id ^ MARKTREE_END_FLAG; + if (mt_paired(mark)) { + uint64_t other = mt_lookup_id(mark.ns, mark.id, !mt_end(mark)); ssize_t decor_id = -1; - if (item.decor) { + if (marktree_decor_level(mark) > kDecorLevelNone) { // Save the decoration and the first pos. Clear the decoration // later when we know the full range. decor_id = (ssize_t)kv_size(decors); kv_push(decors, - ((DecorItem) { .decor = item.decor, .row1 = mark.row })); + ((DecorItem) { .row1 = mark.pos.row })); } map_put(uint64_t, ssize_t)(&delete_set, other, decor_id); - } else if (item.decor) { - decor_remove(buf, mark.row, mark.row, item.decor); + } else if (mark.decor_full) { + decor_remove(buf, mark.pos.row, mark.pos.row, mark.decor_full); } - ExtmarkNs *my_ns = all_ns ? buf_ns_ref(buf, item.ns_id, false) : ns; - map_del(uint64_t, uint64_t)(my_ns->map, item.mark_id); - map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, start_id); marktree_del_itr(buf->b_marktree, itr, false); } else { marktree_itr_next(buf->b_marktree, itr); @@ -271,12 +265,12 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_r uint64_t id; ssize_t decor_id; map_foreach(&delete_set, id, decor_id, { - mtpos_t pos = marktree_lookup(buf->b_marktree, id, itr); + mtkey_t mark = marktree_lookup(buf->b_marktree, id, itr); assert(itr->node); marktree_del_itr(buf->b_marktree, itr, false); if (decor_id >= 0) { DecorItem it = kv_A(decors, decor_id); - decor_remove(buf, it.row1, pos.row, it.decor); + decor_remove(buf, it.row1, mark.pos.row, mark.decor_full); } }); map_clear(uint64_t, ssize_t)(&delete_set); @@ -290,7 +284,7 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_r // will be searched to the start, or end // dir can be set to control the order of the array // amount = amount of marks to find or -1 for all -ExtmarkInfoArray extmark_get(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_col, int u_row, +ExtmarkInfoArray extmark_get(buf_T *buf, uint32_t ns_id, int l_row, colnr_T l_col, int u_row, colnr_T u_col, int64_t amount, bool reverse) { ExtmarkInfoArray array = KV_INITIAL_VALUE; @@ -300,30 +294,26 @@ ExtmarkInfoArray extmark_get(buf_T *buf, uint64_t ns_id, int l_row, colnr_T l_co itr, reverse, false, NULL); int order = reverse ? -1 : 1; while ((int64_t)kv_size(array) < amount) { - mtmark_t mark = marktree_itr_current(itr); - mtpos_t endpos = { -1, -1 }; - if (mark.row < 0 - || (mark.row - u_row) * order > 0 - || (mark.row == u_row && (mark.col - u_col) * order > 0)) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0 + || (mark.pos.row - u_row) * order > 0 + || (mark.pos.row == u_row && (mark.pos.col - u_col) * order > 0)) { break; } - if (mark.id & MARKTREE_END_FLAG) { + if (mt_end(mark)) { goto next_mark; - } else if (mark.id & MARKTREE_PAIRED_FLAG) { - endpos = marktree_lookup(buf->b_marktree, mark.id | MARKTREE_END_FLAG, - NULL); } - - ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark.id); - if (item.ns_id == ns_id) { - kv_push(array, ((ExtmarkInfo) { .ns_id = item.ns_id, - .mark_id = item.mark_id, - .row = mark.row, .col = mark.col, - .end_row = endpos.row, - .end_col = endpos.col, - .decor = item.decor })); + if (mark.ns == ns_id) { + mtkey_t end = marktree_get_alt(buf->b_marktree, mark, NULL); + kv_push(array, ((ExtmarkInfo) { .ns_id = mark.ns, + .mark_id = mark.id, + .row = mark.pos.row, .col = mark.pos.col, + .end_row = end.pos.row, + .end_col = end.pos.col, + .right_gravity = mt_right(mark), + .end_right_gravity = mt_right(end), + .decor = get_decor(mark) })); } next_mark: if (reverse) { @@ -336,36 +326,25 @@ next_mark: } // Lookup an extmark by id -ExtmarkInfo extmark_from_id(buf_T *buf, uint64_t ns_id, uint64_t id) +ExtmarkInfo extmark_from_id(buf_T *buf, uint32_t ns_id, uint32_t id) { - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, false); - ExtmarkInfo ret = { 0, 0, -1, -1, -1, -1, NULL }; - if (!ns) { + ExtmarkInfo ret = { 0, 0, -1, -1, -1, -1, false, false, DECORATION_INIT }; + mtkey_t mark = marktree_lookup_ns(buf->b_marktree, ns_id, id, false, NULL); + if (!mark.id) { return ret; } - - uint64_t mark = map_get(uint64_t, uint64_t)(ns->map, id); - if (!mark) { - return ret; - } - - mtpos_t pos = marktree_lookup(buf->b_marktree, mark, NULL); - mtpos_t endpos = { -1, -1 }; - if (mark & MARKTREE_PAIRED_FLAG) { - endpos = marktree_lookup(buf->b_marktree, mark | MARKTREE_END_FLAG, NULL); - } - assert(pos.row >= 0); - - ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, - mark); + assert(mark.pos.row >= 0); + mtkey_t end = marktree_get_alt(buf->b_marktree, mark, NULL); ret.ns_id = ns_id; ret.mark_id = id; - ret.row = pos.row; - ret.col = pos.col; - ret.end_row = endpos.row; - ret.end_col = endpos.col; - ret.decor = item.decor; + ret.row = mark.pos.row; + ret.col = mark.pos.col; + ret.end_row = end.pos.row; + ret.end_col = end.pos.col; + ret.right_gravity = mt_right(mark); + ret.end_right_gravity = mt_right(end); + ret.decor = get_decor(mark); return ret; } @@ -378,25 +357,26 @@ void extmark_free_all(buf_T *buf) return; } - uint64_t id; - ExtmarkNs ns; - ExtmarkItem item; + MarkTreeIter itr[1] = { 0 }; + marktree_itr_get(buf->b_marktree, 0, 0, itr); + while (true) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0) { + break; + } - marktree_clear(buf->b_marktree); + // don't free mark.decor_full twice for a paired mark. + if (!(mt_paired(mark) && mt_end(mark))) { + decor_free(mark.decor_full); + } - map_foreach(buf->b_extmark_ns, id, ns, { - (void)id; - map_destroy(uint64_t, uint64_t)(ns.map); - }); - map_destroy(uint64_t, ExtmarkNs)(buf->b_extmark_ns); - map_init(uint64_t, ExtmarkNs, buf->b_extmark_ns); + marktree_itr_next(buf->b_marktree, itr); + } - map_foreach(buf->b_extmark_index, id, item, { - (void)id; - decor_free(item.decor); - }); - map_destroy(uint64_t, ExtmarkItem)(buf->b_extmark_index); - map_init(uint64_t, ExtmarkItem, buf->b_extmark_index); + marktree_clear(buf->b_marktree); + + map_destroy(uint32_t, uint32_t)(buf->b_extmark_ns); + map_init(uint32_t, uint32_t, buf->b_extmark_ns); } @@ -437,16 +417,16 @@ void u_extmark_copy(buf_T *buf, int l_row, colnr_T l_col, int u_row, colnr_T u_c MarkTreeIter itr[1] = { 0 }; marktree_itr_get(buf->b_marktree, l_row, l_col, itr); while (true) { - mtmark_t mark = marktree_itr_current(itr); - if (mark.row < 0 - || mark.row > u_row - || (mark.row == u_row && mark.col > u_col)) { + mtkey_t mark = marktree_itr_current(itr); + if (mark.pos.row < 0 + || mark.pos.row > u_row + || (mark.pos.row == u_row && mark.pos.col > u_col)) { break; } ExtmarkSavePos pos; - pos.mark = mark.id; - pos.old_row = mark.row; - pos.old_col = mark.col; + pos.mark = mt_lookup_key(mark); + pos.old_row = mark.pos.row; + pos.old_col = mark.pos.col; pos.row = -1; pos.col = -1; diff --git a/src/nvim/extmark.h b/src/nvim/extmark.h index c70db9f7aa..af9526cd43 100644 --- a/src/nvim/extmark.h +++ b/src/nvim/extmark.h @@ -3,6 +3,7 @@ #include "nvim/buffer_defs.h" #include "nvim/extmark_defs.h" +#include "nvim/decoration.h" #include "nvim/marktree.h" #include "nvim/pos.h" @@ -15,7 +16,9 @@ typedef struct { colnr_T col; int end_row; colnr_T end_col; - Decoration *decor; + bool right_gravity; + bool end_right_gravity; + Decoration decor; // TODO(bfredl): CHONKY } ExtmarkInfo; typedef kvec_t(ExtmarkInfo) ExtmarkInfoArray; diff --git a/src/nvim/extmark_defs.h b/src/nvim/extmark_defs.h index bbe8504ebf..5570b5c71e 100644 --- a/src/nvim/extmark_defs.h +++ b/src/nvim/extmark_defs.h @@ -4,23 +4,11 @@ #include "nvim/lib/kvec.h" #include "nvim/types.h" -typedef struct Decoration Decoration; - typedef struct { char *text; int hl_id; } VirtTextChunk; - -typedef struct { - uint64_t ns_id; - uint64_t mark_id; - // TODO(bfredl): a lot of small allocations. Should probably use - // kvec_t(Decoration) as an arena. Alternatively, store ns_id/mark_id - // _inline_ in MarkTree and use the map only for decorations. - Decoration *decor; -} ExtmarkItem; - typedef struct undo_object ExtmarkUndoObject; typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t; diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index 1884dd49c5..d0f7a91d6c 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -483,8 +483,14 @@ void *vim_findfile_init(char_u *path, char_u *filename, char_u *stopdirs, int le int len = 0; if (p > search_ctx->ffsc_fix_path) { + // do not add '..' to the path and start upwards searching len = (int)(p - search_ctx->ffsc_fix_path) - 1; - STRNCAT(ff_expand_buffer, search_ctx->ffsc_fix_path, len); + if ((len >= 2 && STRNCMP(search_ctx->ffsc_fix_path, "..", 2) == 0) + && (len == 2 || search_ctx->ffsc_fix_path[2] == PATHSEP)) { + xfree(buf); + goto error_return; + } + STRLCAT(ff_expand_buffer, search_ctx->ffsc_fix_path, eb_len + (size_t)len + 1); add_pathsep((char *)ff_expand_buffer); } else { len = (int)STRLEN(search_ctx->ffsc_fix_path); @@ -607,7 +613,7 @@ char_u *vim_findfile(void *search_ctx_arg) for (;;) { // downward search loop for (;;) { - // check if user user wants to stop the search + // check if user wants to stop the search os_breakcheck(); if (got_int) { break; @@ -1139,7 +1145,7 @@ static int ff_check_visited(ff_visited_T **visited_list, char_u *fname, char_u * bool url = false; FileID file_id; - // For an URL we only compare the name, otherwise we compare the + // For a URL we only compare the name, otherwise we compare the // device/inode. if (path_with_url((char *)fname)) { STRLCPY(ff_expand_buffer, fname, MAXPATHL); @@ -1433,7 +1439,11 @@ char_u *find_file_in_path_option(char_u *ptr, size_t len, int options, int first rel_fname = NULL; } - if (first == TRUE) { + if (first == true) { + if (len == 0) { + return NULL; + } + // copy file name into NameBuff, expanding environment variables save_char = ptr[len]; ptr[len] = NUL; @@ -1590,11 +1600,13 @@ theend: return file_name; } -void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause) +void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause, bool pre) { static bool recursive = false; - if (recursive || !has_event(EVENT_DIRCHANGED)) { + event_T event = pre ? EVENT_DIRCHANGEDPRE : EVENT_DIRCHANGED; + + if (recursive || !has_event(event)) { // No autocommand was defined or we changed // the directory from this autocommand. return; @@ -1628,8 +1640,12 @@ void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause) new_dir = new_dir_buf; #endif + if (pre) { + tv_dict_add_str(dict, S_LEN("directory"), new_dir); + } else { + tv_dict_add_str(dict, S_LEN("cwd"), new_dir); + } tv_dict_add_str(dict, S_LEN("scope"), buf); // -V614 - tv_dict_add_str(dict, S_LEN("cwd"), new_dir); tv_dict_add_bool(dict, S_LEN("changed_window"), cause == kCdCauseWindow); tv_dict_set_keys_readonly(dict); @@ -1645,8 +1661,7 @@ void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause) abort(); } - apply_autocmds(EVENT_DIRCHANGED, (char_u *)buf, (char_u *)new_dir, false, - curbuf); + apply_autocmds(event, (char_u *)buf, (char_u *)new_dir, false, curbuf); restore_v_event(dict, &save_v_event); @@ -1672,12 +1687,16 @@ int vim_chdirfile(char_u *fname, CdCause cause) return OK; } + if (cause != kCdCauseOther) { + do_autocmd_dirchanged(dir, kCdScopeWindow, cause, true); + } + if (os_chdir(dir) != 0) { return FAIL; } if (cause != kCdCauseOther) { - do_autocmd_dirchanged(dir, kCdScopeWindow, cause); + do_autocmd_dirchanged(dir, kCdScopeWindow, cause, false); } return OK; diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index f8cf341836..965aa8749d 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -242,6 +242,7 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski bool notconverted = false; // true if conversion wanted but it wasn't possible char_u conv_rest[CONV_RESTLEN]; int conv_restlen = 0; // nr of bytes in conv_rest[] + pos_T orig_start; buf_T *old_curbuf; char_u *old_b_ffname; char_u *old_b_fname; @@ -298,14 +299,10 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski fname = sfname; #endif - /* - * The BufReadCmd and FileReadCmd events intercept the reading process by - * executing the associated commands instead. - */ + // The BufReadCmd and FileReadCmd events intercept the reading process by + // executing the associated commands instead. if (!filtering && !read_stdin && !read_buffer) { - pos_T pos; - - pos = curbuf->b_op_start; + orig_start = curbuf->b_op_start; // Set '[ mark to the line above where the lines go (line 1 if zero). curbuf->b_op_start.lnum = ((from == 0) ? 1 : from); @@ -335,7 +332,7 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski return aborting() ? FAIL : OK; } - curbuf->b_op_start = pos; + curbuf->b_op_start = orig_start; } if ((shortmess(SHM_OVER) || curbuf->b_help) && p_verbose == 0) { @@ -408,6 +405,7 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski if (os_fileinfo((char *)fname, &file_info)) { buf_store_file_info(curbuf, &file_info); curbuf->b_mtime_read = curbuf->b_mtime; + curbuf->b_mtime_read_ns = curbuf->b_mtime_ns; #ifdef UNIX /* * Use the protection bits of the original file for the swap file. @@ -424,7 +422,9 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski #endif } else { curbuf->b_mtime = 0; + curbuf->b_mtime_ns = 0; curbuf->b_mtime_read = 0; + curbuf->b_mtime_read_ns = 0; curbuf->b_orig_size = 0; curbuf->b_orig_mode = 0; } @@ -576,9 +576,8 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski ++no_wait_return; // don't wait for return yet - /* - * Set '[ mark to the line above where the lines go (line 1 if zero). - */ + // Set '[ mark to the line above where the lines go (line 1 if zero). + orig_start = curbuf->b_op_start; curbuf->b_op_start.lnum = ((from == 0) ? 1 : from); curbuf->b_op_start.col = 0; @@ -618,6 +617,7 @@ int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_ski try_mac = (vim_strchr(p_ffs, 'm') != NULL); try_dos = (vim_strchr(p_ffs, 'd') != NULL); try_unix = (vim_strchr(p_ffs, 'x') != NULL); + curbuf->b_op_start = orig_start; if (msg_scrolled == n) { msg_scroll = m; @@ -1888,13 +1888,13 @@ failed: check_cursor_lnum(); beginline(BL_WHITE | BL_FIX); // on first non-blank - /* - * Set '[ and '] marks to the newly read lines. - */ - curbuf->b_op_start.lnum = from + 1; - curbuf->b_op_start.col = 0; - curbuf->b_op_end.lnum = from + linecnt; - curbuf->b_op_end.col = 0; + if (!cmdmod.lockmarks) { + // Set '[ and '] marks to the newly read lines. + curbuf->b_op_start.lnum = from + 1; + curbuf->b_op_start.col = 0; + curbuf->b_op_end.lnum = from + linecnt; + curbuf->b_op_end.col = 0; + } } msg_scroll = msg_save; @@ -2013,10 +2013,8 @@ static linenr_T readfile_linenr(linenr_T linecnt, char_u *p, char_u *endp) return lnum; } -/* - * Fill "*eap" to force the 'fileencoding', 'fileformat' and 'binary to be - * equal to the buffer "buf". Used for calling readfile(). - */ +/// Fill "*eap" to force the 'fileencoding', 'fileformat' and 'binary' to be +/// equal to the buffer "buf". Used for calling readfile(). void prep_exarg(exarg_T *eap, const buf_T *buf) FUNC_ATTR_NONNULL_ALL { @@ -2252,6 +2250,8 @@ int buf_write(buf_T *buf, char_u *fname, char_u *sfname, linenr_T start, linenr_ int write_undo_file = FALSE; context_sha256_T sha_ctx; unsigned int bkc = get_bkc_value(buf); + const pos_T orig_start = buf->b_op_start; + const pos_T orig_end = buf->b_op_end; if (fname == NULL || *fname == NUL) { // safety check return FAIL; @@ -2432,7 +2432,13 @@ int buf_write(buf_T *buf, char_u *fname, char_u *sfname, linenr_T start, linenr_ if (buf == NULL || (buf->b_ml.ml_mfp == NULL && !empty_memline) || did_cmd || nofile_err || aborting()) { - --no_wait_return; + if (buf != NULL && cmdmod.lockmarks) { + // restore the original '[ and '] positions + buf->b_op_start = orig_start; + buf->b_op_end = orig_end; + } + + no_wait_return--; msg_scroll = msg_save; if (nofile_err) { emsg(_("E676: No matching autocommands for acwrite buffer")); @@ -2513,6 +2519,11 @@ int buf_write(buf_T *buf, char_u *fname, char_u *sfname, linenr_T start, linenr_ } } + if (cmdmod.lockmarks) { + // restore the original '[ and '] positions + buf->b_op_start = orig_start; + buf->b_op_end = orig_end; + } if (shortmess(SHM_OVER) && !exiting) { msg_scroll = FALSE; // overwrite previous file message @@ -3308,7 +3319,7 @@ restore_backup: if (end == 0 || (lnum == end && (write_bin || !buf->b_p_fixeol) - && (lnum == buf->b_no_eol_lnum + && ((write_bin && lnum == buf->b_no_eol_lnum) || (lnum == buf->b_ml.ml_line_count && !buf->b_p_eol)))) { lnum++; // written the line, count it no_eol = true; @@ -3685,11 +3696,12 @@ nofail: msg_puts_attr(_("don't quit the editor until the file is successfully written!"), attr | MSG_HIST); - /* Update the timestamp to avoid an "overwrite changed file" - * prompt when writing again. */ + // Update the timestamp to avoid an "overwrite changed file" + // prompt when writing again. if (os_fileinfo((char *)fname, &file_info_old)) { buf_store_file_info(buf, &file_info_old); buf->b_mtime_read = buf->b_mtime; + buf->b_mtime_read_ns = buf->b_mtime_ns; } } } @@ -3784,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); @@ -3853,7 +3865,7 @@ void msg_add_lines(int insert_space, long lnum, off_T nchars) *p++ = ' '; } if (shortmess(SHM_LINES)) { - vim_snprintf((char *)p, IOSIZE - (p - IObuff), "%" PRId64 "L, %" PRId64 "C", + vim_snprintf((char *)p, IOSIZE - (p - IObuff), "%" PRId64 "L, %" PRId64 "B", (int64_t)lnum, (int64_t)nchars); } else { vim_snprintf((char *)p, IOSIZE - (p - IObuff), @@ -3861,7 +3873,7 @@ void msg_add_lines(int insert_space, long lnum, off_T nchars) (int64_t)lnum); p += STRLEN(p); vim_snprintf((char *)p, IOSIZE - (p - IObuff), - NGETTEXT("%" PRId64 " character", "%" PRId64 " characters", nchars), + NGETTEXT("%" PRId64 " byte", "%" PRId64 " bytes", nchars), (int64_t)nchars); } } @@ -3883,8 +3895,7 @@ static void msg_add_eol(void) static int check_mtime(buf_T *buf, FileInfo *file_info) { if (buf->b_mtime_read != 0 - && time_differs(file_info->stat.st_mtim.tv_sec, - buf->b_mtime_read)) { + && time_differs(file_info, buf->b_mtime_read, buf->b_mtime_read_ns)) { msg_scroll = true; // Don't overwrite messages here. msg_silent = 0; // Must give this prompt. // Don't use emsg() here, don't want to flush the buffers. @@ -3898,19 +3909,17 @@ static int check_mtime(buf_T *buf, FileInfo *file_info) return OK; } -/// Return true if the times differ -/// -/// @param t1 first time -/// @param t2 second time -static bool time_differs(long t1, long t2) FUNC_ATTR_CONST +static bool time_differs(const FileInfo *file_info, long mtime, long mtime_ns) FUNC_ATTR_CONST { + return (long)file_info->stat.st_mtim.tv_nsec != mtime_ns #if defined(__linux__) || defined(MSWIN) - // On a FAT filesystem, esp. under Linux, there are only 5 bits to store - // the seconds. Since the roundoff is done when flushing the inode, the - // time may change unexpectedly by one second!!! - return t1 - t2 > 1 || t2 - t1 > 1; + // On a FAT filesystem, esp. under Linux, there are only 5 bits to store + // the seconds. Since the roundoff is done when flushing the inode, the + // time may change unexpectedly by one second!!! + || (long)file_info->stat.st_mtim.tv_sec - mtime > 1 + || mtime - (long)file_info->stat.st_mtim.tv_sec > 1; #else - return t1 != t2; + || (long)file_info->stat.st_mtim.tv_sec != mtime; #endif } @@ -4890,13 +4899,11 @@ static int move_lines(buf_T *frombuf, buf_T *tobuf) return retval; } -/* - * Check if buffer "buf" has been changed. - * Also check if the file for a new buffer unexpectedly appeared. - * return 1 if a changed buffer was found. - * return 2 if a message has been displayed. - * return 0 otherwise. - */ +/// Check if buffer "buf" has been changed. +/// Also check if the file for a new buffer unexpectedly appeared. +/// return 1 if a changed buffer was found. +/// return 2 if a message has been displayed. +/// return 0 otherwise. int buf_check_timestamp(buf_T *buf) FUNC_ATTR_NONNULL_ALL { @@ -4905,7 +4912,11 @@ int buf_check_timestamp(buf_T *buf) char *mesg = NULL; char *mesg2 = ""; bool helpmesg = false; - bool reload = false; + enum { + RELOAD_NONE, + RELOAD_NORMAL, + RELOAD_DETECT + } reload = RELOAD_NONE; bool can_reload = false; uint64_t orig_size = buf->b_orig_size; int orig_mode = buf->b_orig_mode; @@ -4933,7 +4944,7 @@ int buf_check_timestamp(buf_T *buf) if (!(buf->b_flags & BF_NOTEDITED) && buf->b_mtime != 0 && (!(file_info_ok = os_fileinfo((char *)buf->b_ffname, &file_info)) - || time_differs(file_info.stat.st_mtim.tv_sec, buf->b_mtime) + || time_differs(&file_info, buf->b_mtime, buf->b_mtime_ns) || (int)file_info.stat.st_mode != buf->b_orig_mode)) { const long prev_b_mtime = buf->b_mtime; @@ -4958,7 +4969,7 @@ int buf_check_timestamp(buf_T *buf) // If 'autoread' is set, the buffer has no changes and the file still // exists, reload the buffer. Use the buffer-local option value if it // was set, the global option value otherwise. - reload = true; + reload = RELOAD_NORMAL; } else { if (!file_info_ok) { reason = "deleted"; @@ -4989,7 +5000,9 @@ int buf_check_timestamp(buf_T *buf) } s = get_vim_var_str(VV_FCS_CHOICE); if (STRCMP(s, "reload") == 0 && *reason != 'd') { - reload = true; + reload = RELOAD_NORMAL; + } else if (STRCMP(s, "edit") == 0) { + reload = RELOAD_DETECT; } else if (STRCMP(s, "ask") == 0) { n = false; } else { @@ -5024,6 +5037,7 @@ int buf_check_timestamp(buf_T *buf) // Only timestamp changed, store it to avoid a warning // in check_mtime() later. buf->b_mtime_read = buf->b_mtime; + buf->b_mtime_read_ns = buf->b_mtime_ns; } } } @@ -5052,9 +5066,15 @@ int buf_check_timestamp(buf_T *buf) xstrlcat(tbuf, "\n", tbuf_len - 1); xstrlcat(tbuf, mesg2, tbuf_len - 1); } - if (do_dialog(VIM_WARNING, (char_u *)_("Warning"), (char_u *)tbuf, - (char_u *)_("&OK\n&Load File"), 1, NULL, true) == 2) { - reload = true; + switch (do_dialog(VIM_WARNING, (char_u *)_("Warning"), (char_u *)tbuf, + (char_u *)_("&OK\n&Load File\nLoad File &and Options"), + 1, NULL, true)) { + case 2: + reload = RELOAD_NORMAL; + break; + case 3: + reload = RELOAD_DETECT; + break; } } else if (State > NORMAL_BUSY || (State & CMDLINE) || already_warned) { if (*mesg2 != NUL) { @@ -5088,9 +5108,9 @@ int buf_check_timestamp(buf_T *buf) xfree(tbuf); } - if (reload) { + if (reload != RELOAD_NONE) { // Reload the buffer. - buf_reload(buf, orig_mode); + buf_reload(buf, orig_mode, reload == RELOAD_DETECT); if (buf->b_p_udf && buf->b_ffname != NULL) { char_u hash[UNDO_HASH_SIZE]; @@ -5108,13 +5128,11 @@ int buf_check_timestamp(buf_T *buf) return retval; } -/* - * Reload a buffer that is already loaded. - * Used when the file was changed outside of Vim. - * "orig_mode" is buf->b_orig_mode before the need for reloading was detected. - * buf->b_orig_mode may have been reset already. - */ -void buf_reload(buf_T *buf, int orig_mode) +/// Reload a buffer that is already loaded. +/// Used when the file was changed outside of Vim. +/// "orig_mode" is buf->b_orig_mode before the need for reloading was detected. +/// buf->b_orig_mode may have been reset already. +void buf_reload(buf_T *buf, int orig_mode, bool reload_options) { exarg_T ea; pos_T old_cursor; @@ -5129,11 +5147,15 @@ void buf_reload(buf_T *buf, int orig_mode) // set curwin/curbuf for "buf" and save some things aucmd_prepbuf(&aco, buf); - // We only want to read the text from the file, not reset the syntax - // highlighting, clear marks, diff status, etc. Force the fileformat and - // encoding to be the same. + // Unless reload_options is set, we only want to read the text from the + // file, not reset the syntax highlighting, clear marks, diff status, etc. + // Force the fileformat and encoding to be the same. + if (reload_options) { + memset(&ea, 0, sizeof(ea)); + } else { + prep_exarg(&ea, buf); + } - prep_exarg(&ea, buf); old_cursor = curwin->w_cursor; old_topline = curwin->w_topline; @@ -5252,6 +5274,7 @@ void buf_store_file_info(buf_T *buf, FileInfo *file_info) FUNC_ATTR_NONNULL_ALL { buf->b_mtime = file_info->stat.st_mtim.tv_sec; + buf->b_mtime_ns = file_info->stat.st_mtim.tv_nsec; buf->b_orig_size = os_fileinfo_size(file_info); buf->b_orig_mode = (int)file_info->stat.st_mode; } diff --git a/src/nvim/fold.c b/src/nvim/fold.c index b1d4321d4c..546345eeac 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -2332,7 +2332,7 @@ static linenr_T foldUpdateIEMSRecurse(garray_T *const gap, const int level, * firstlnum. */ while (!got_int) { - // set concat to 1 if it's allowed to concatenated this fold + // set concat to 1 if it's allowed to concatenate this fold // with a previous one that touches it. if (flp->start != 0 || flp->had_end <= MAX_LEVEL) { concat = 0; diff --git a/src/nvim/generators/gen_api_dispatch.lua b/src/nvim/generators/gen_api_dispatch.lua index 21f8c3855e..c6dd25154b 100644 --- a/src/nvim/generators/gen_api_dispatch.lua +++ b/src/nvim/generators/gen_api_dispatch.lua @@ -441,8 +441,8 @@ local function process_function(fn) local cparam = string.format('arg%u', j) local param_type = real_type(param[1]) local lc_param_type = real_type(param[1]):lower() - local extra = ((param_type == "Object" or param_type == "Dictionary") and "false, ") or "" - if param[1] == "DictionaryOf(LuaRef)" then + local extra = param_type == "Dictionary" and "false, " or "" + if param[1] == "Object" or param[1] == "DictionaryOf(LuaRef)" then extra = "true, " end local errshift = 0 diff --git a/src/nvim/generators/gen_keysets.lua b/src/nvim/generators/gen_keysets.lua index 63ef202fe1..633c5da184 100644 --- a/src/nvim/generators/gen_keysets.lua +++ b/src/nvim/generators/gen_keysets.lua @@ -26,6 +26,18 @@ local defspipe = io.open(defs_file, 'wb') local keysets = require'api.keysets' +local keywords = { + register = true; + default = true; +} + +local function sanitize(key) + if keywords[key] then + return key .. "_" + end + return key +end + for name, keys in pairs(keysets) do local neworder, hashfun = hashy.hashy_hash(name, keys, function (idx) return name.."_table["..idx.."].str" @@ -33,7 +45,7 @@ for name, keys in pairs(keysets) do defspipe:write("typedef struct {\n") for _, key in ipairs(neworder) do - defspipe:write(" Object "..key..";\n") + defspipe:write(" Object "..sanitize(key)..";\n") end defspipe:write("} KeyDict_"..name..";\n\n") @@ -41,7 +53,7 @@ for name, keys in pairs(keysets) do funcspipe:write("KeySetLink "..name.."_table[] = {\n") for _, key in ipairs(neworder) do - funcspipe:write(' {"'..key..'", offsetof(KeyDict_'..name..", "..key..")},\n") + funcspipe:write(' {"'..key..'", offsetof(KeyDict_'..name..", "..sanitize(key)..")},\n") end funcspipe:write(' {NULL, 0},\n') funcspipe:write("};\n\n") diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 6b1150cefa..85a5c176bb 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -15,6 +15,7 @@ #include <stdbool.h> #include <string.h> +#include "nvim/api/private/helpers.h" #include "nvim/ascii.h" #include "nvim/assert.h" #include "nvim/buffer_defs.h" @@ -58,25 +59,18 @@ static int curscript = 0; FileDescriptor *scriptin[NSCRIPT] = { NULL }; -/* - * These buffers are used for storing: - * - stuffed characters: A command that is translated into another command. - * - redo characters: will redo the last change. - * - recorded characters: for the "q" command. - * - * The bytes are stored like in the typeahead buffer: - * - K_SPECIAL introduces a special key (two more bytes follow). A literal - * K_SPECIAL is stored as K_SPECIAL KS_SPECIAL KE_FILLER. - * - CSI introduces a GUI termcap code (also when gui.in_use is FALSE, - * otherwise switching the GUI on would make mappings invalid). - * A literal CSI is stored as CSI KS_EXTRA KE_CSI. - * These translations are also done on multi-byte characters! - * - * Escaping CSI bytes is done by the system-specific input functions, called - * by ui_inchar(). - * Escaping K_SPECIAL is done by inchar(). - * Un-escaping is done by vgetc(). - */ +// These buffers are used for storing: +// - stuffed characters: A command that is translated into another command. +// - redo characters: will redo the last change. +// - recorded characters: for the "q" command. +// +// The bytes are stored like in the typeahead buffer: +// - K_SPECIAL introduces a special key (two more bytes follow). A literal +// K_SPECIAL is stored as K_SPECIAL KS_SPECIAL KE_FILLER. +// These translations are also done on multi-byte characters! +// +// Escaping K_SPECIAL is done by inchar(). +// Un-escaping is done by vgetc(). #define MINIMAL_SIZE 20 // minimal size for b_str @@ -146,7 +140,7 @@ static int KeyNoremap = 0; // remapping flags // typebuf.tb_buf has three parts: room in front (for result of mappings), the // middle for typeahead and room for new characters (which needs to be 3 * -// MAXMAPLEN) for the Amiga). +// MAXMAPLEN for the Amiga). #define TYPELEN_INIT (5 * (MAXMAPLEN + 3)) static char_u typebuf_init[TYPELEN_INIT]; // initial typebuf.tb_buf static char_u noremapbuf_init[TYPELEN_INIT]; // initial typebuf.tb_noremap @@ -169,10 +163,11 @@ void free_buff(buffheader_T *buf) xfree(p); } buf->bh_first.b_next = NULL; + buf->bh_curr = NULL; } /// Return the contents of a buffer as a single string. -/// K_SPECIAL and CSI in the returned string are escaped. +/// K_SPECIAL in the returned string is escaped. /// /// @param dozero count == zero is not an error static char_u *get_buffcont(buffheader_T *buffer, int dozero) @@ -201,11 +196,9 @@ static char_u *get_buffcont(buffheader_T *buffer, int dozero) return p; } -/* - * Return the contents of the record buffer as a single string - * and clear the record buffer. - * K_SPECIAL and CSI in the returned string are escaped. - */ +/// Return the contents of the record buffer as a single string +/// and clear the record buffer. +/// K_SPECIAL in the returned string is escaped. char_u *get_recorded(void) { char_u *p; @@ -235,10 +228,8 @@ char_u *get_recorded(void) return p; } -/* - * Return the contents of the redo buffer as a single string. - * K_SPECIAL and CSI in the returned string are escaped. - */ +/// Return the contents of the redo buffer as a single string. +/// K_SPECIAL in the returned string is escaped. char_u *get_inserted(void) { return get_buffcont(&redobuff, FALSE); @@ -246,7 +237,7 @@ char_u *get_inserted(void) /// Add string after the current block of the given buffer /// -/// K_SPECIAL and CSI should have been escaped already. +/// K_SPECIAL should have been escaped already. /// /// @param[out] buf Buffer to add to. /// @param[in] s String to add. @@ -294,9 +285,23 @@ static void add_buff(buffheader_T *const buf, const char *const s, ptrdiff_t sle } } -/* - * Add number "n" to buffer "buf". - */ +/// Delete "slen" bytes from the end of "buf". +/// Only works when it was just added. +static void delete_buff_tail(buffheader_T *buf, int slen) +{ + int len; + + if (buf->bh_curr == NULL) { + return; // nothing to delete + } + len = (int)STRLEN(buf->bh_curr->b_str); + if (len >= slen) { + buf->bh_curr->b_str[len - slen] = NUL; + buf->bh_space += (size_t)slen; + } +} + +/// Add number "n" to buffer "buf". static void add_num_buff(buffheader_T *buf, long n) { char number[32]; @@ -304,10 +309,8 @@ static void add_num_buff(buffheader_T *buf, long n) add_buff(buf, number, -1L); } -/* - * Add character 'c' to buffer "buf". - * Translates special keys, NUL, CSI, K_SPECIAL and multibyte characters. - */ +/// Add character 'c' to buffer "buf". +/// Translates special keys, NUL, K_SPECIAL and multibyte characters. static void add_char_buff(buffheader_T *buf, int c) { uint8_t bytes[MB_MAXBYTES + 1]; @@ -339,12 +342,10 @@ static void add_char_buff(buffheader_T *buf, int c) } } -/* - * Get one byte from the read buffers. Use readbuf1 one first, use readbuf2 - * if that one is empty. - * If advance == TRUE go to the next char. - * No translation is done K_SPECIAL and CSI are escaped. - */ +/// Get one byte from the read buffers. Use readbuf1 one first, use readbuf2 +/// if that one is empty. +/// If advance == TRUE go to the next char. +/// No translation is done K_SPECIAL is escaped. static int read_readbuffers(int advance) { int c; @@ -523,10 +524,8 @@ void restoreRedobuff(save_redo_T *save_redo) old_redobuff = save_redo->sr_old_redobuff; } -/* - * Append "s" to the redo buffer. - * K_SPECIAL and CSI should already have been escaped. - */ +/// Append "s" to the redo buffer. +/// K_SPECIAL should already have been escaped. void AppendToRedobuff(const char *s) { if (!block_redo) { @@ -535,7 +534,7 @@ void AppendToRedobuff(const char *s) } /// Append to Redo buffer literally, escaping special characters with CTRL-V. -/// K_SPECIAL and CSI are escaped as well. +/// K_SPECIAL is escaped as well. /// /// @param str String to append /// @param len Length of `str` or -1 for up to the NUL. @@ -583,10 +582,8 @@ void AppendToRedobuffLit(const char_u *str, int len) } } -/* - * Append a character to the redo buffer. - * Translates special keys, NUL, CSI, K_SPECIAL and multibyte characters. - */ +/// Append a character to the redo buffer. +/// Translates special keys, NUL, K_SPECIAL and multibyte characters. void AppendCharToRedobuff(int c) { if (!block_redo) { @@ -604,17 +601,15 @@ void AppendNumberToRedobuff(long n) } } -/* - * Append string "s" to the stuff buffer. - * CSI and K_SPECIAL must already have been escaped. - */ +/// Append string "s" to the stuff buffer. +/// K_SPECIAL must already have been escaped. void stuffReadbuff(const char *s) { add_buff(&readbuf1, s, -1L); } /// Append string "s" to the redo stuff buffer. -/// @remark CSI and K_SPECIAL must already have been escaped. +/// @remark K_SPECIAL must already have been escaped. void stuffRedoReadbuff(const char *s) { add_buff(&readbuf2, s, -1L); @@ -625,11 +620,9 @@ void stuffReadbuffLen(const char *s, long len) add_buff(&readbuf1, s, len); } -/* - * Stuff "s" into the stuff buffer, leaving special key codes unmodified and - * escaping other K_SPECIAL and CSI bytes. - * Change CR, LF and ESC into a space. - */ +/// Stuff "s" into the stuff buffer, leaving special key codes unmodified and +/// escaping other K_SPECIAL bytes. +/// Change CR, LF and ESC into a space. void stuffReadbuffSpec(const char *s) { while (*s != NUL) { @@ -647,10 +640,8 @@ void stuffReadbuffSpec(const char *s) } } -/* - * Append a character to the stuff buffer. - * Translates special keys, NUL, CSI, K_SPECIAL and multibyte characters. - */ +/// Append a character to the stuff buffer. +/// Translates special keys, NUL, K_SPECIAL and multibyte characters. void stuffcharReadbuff(int c) { add_char_buff(&readbuf1, c); @@ -664,12 +655,12 @@ void stuffnumReadbuff(long n) add_num_buff(&readbuf1, n); } -// Read a character from the redo buffer. Translates K_SPECIAL, CSI and -// multibyte characters. -// The redo buffer is left as it is. -// If init is true, prepare for redo, return FAIL if nothing to redo, OK -// otherwise. -// If old_redo is true, use old_redobuff instead of redobuff. +/// Read a character from the redo buffer. Translates K_SPECIAL and +/// multibyte characters. +/// The redo buffer is left as it is. +/// If init is true, prepare for redo, return FAIL if nothing to redo, OK +/// otherwise. +/// If old_redo is true, use old_redobuff instead of redobuff. static int read_redo(bool init, bool old_redo) { static buffblock_T *bp; @@ -723,9 +714,9 @@ static int read_redo(bool init, bool old_redo) return c; } -// Copy the rest of the redo buffer into the stuff buffer (in a slow way). -// If old_redo is true, use old_redobuff instead of redobuff. -// The escaped K_SPECIAL and CSI are copied without translation. +/// Copy the rest of the redo buffer into the stuff buffer (in a slow way). +/// If old_redo is true, use old_redobuff instead of redobuff. +/// The escaped K_SPECIAL is copied without translation. static void copy_redo(bool old_redo) { int c; @@ -861,7 +852,7 @@ void init_default_mappings(void) // // If noremap is REMAP_YES, new string can be mapped again. // If noremap is REMAP_NONE, new string cannot be mapped again. -// If noremap is REMAP_SKIP, fist char of new string cannot be mapped again, +// If noremap is REMAP_SKIP, first char of new string cannot be mapped again, // but abbreviations are allowed. // If noremap is REMAP_SCRIPT, new string cannot be mapped again, except for // script-local mappings. @@ -991,36 +982,34 @@ int ins_typebuf(char_u *str, int noremap, int offset, bool nottyped, bool silent return OK; } -/* - * Put character "c" back into the typeahead buffer. - * Can be used for a character obtained by vgetc() that needs to be put back. - * Uses cmd_silent, KeyTyped and KeyNoremap to restore the flags belonging to - * the char. - */ -void ins_char_typebuf(int c) +/// Put character "c" back into the typeahead buffer. +/// Can be used for a character obtained by vgetc() that needs to be put back. +/// Uses cmd_silent, KeyTyped and KeyNoremap to restore the flags belonging to +/// the char. +/// @return the length of what was inserted +int ins_char_typebuf(int c, int modifier) { - char_u buf[MB_MAXBYTES + 1]; - if (IS_SPECIAL(c)) { + char_u buf[MB_MAXBYTES * 3 + 4]; + int len = 0; + if (modifier != 0) { buf[0] = K_SPECIAL; - buf[1] = (char_u)K_SECOND(c); - buf[2] = (char_u)K_THIRD(c); + buf[1] = KS_MODIFIER; + buf[2] = (char_u)modifier; buf[3] = NUL; + len = 3; + } + if (IS_SPECIAL(c)) { + buf[len] = K_SPECIAL; + buf[len + 1] = (char_u)K_SECOND(c); + buf[len + 2] = (char_u)K_THIRD(c); + buf[len + 3] = NUL; } else { - buf[utf_char2bytes(c, buf)] = NUL; - char_u *p = buf; - while (*p) { - if ((uint8_t)(*p) == CSI || (uint8_t)(*p) == K_SPECIAL) { - bool is_csi = (uint8_t)(*p) == CSI; - memmove(p + 3, p + 1, STRLEN(p + 1) + 1); - *p++ = K_SPECIAL; - *p++ = is_csi ? KS_EXTRA : KS_SPECIAL; - *p++ = is_csi ? KE_CSI : KE_FILLER; - } else { - p++; - } - } + char_u *end = add_char2buf(c, buf + len); + *end = NUL; + len = (int)(end - buf); } (void)ins_typebuf(buf, KeyNoremap, 0, !KeyTyped, cmd_silent); + return len; } /// Return TRUE if the typeahead buffer was changed (while waiting for a @@ -1180,6 +1169,18 @@ static void gotchars(const char_u *chars, size_t len) maptick++; } +/// Undo the last gotchars() for "len" bytes. To be used when putting a typed +/// character back into the typeahead buffer, thus gotchars() will be called +/// again. +/// Only affects recorded characters. +void ungetchars(int len) +{ + if (reg_recording != 0) { + delete_buff_tail(&recordbuff, len); + last_recorded_len -= (size_t)len; + } +} + /* * Sync undo. Called when typed characters are obtained from the typeahead * buffer, or when a menu is used. @@ -1426,15 +1427,13 @@ static void updatescript(int c) } } -/* - * Get the next input character. - * Can return a special key or a multi-byte character. - * Can return NUL when called recursively, use safe_vgetc() if that's not - * wanted. - * This translates escaped K_SPECIAL and CSI bytes to a K_SPECIAL or CSI byte. - * Collects the bytes of a multibyte character into the whole character. - * Returns the modifiers in the global "mod_mask". - */ +/// Get the next input character. +/// Can return a special key or a multi-byte character. +/// Can return NUL when called recursively, use safe_vgetc() if that's not +/// wanted. +/// This translates escaped K_SPECIAL bytes to a K_SPECIAL byte. +/// Collects the bytes of a multibyte character into the whole character. +/// Returns the modifiers in the global "mod_mask". int vgetc(void) { int c, c2; @@ -1460,8 +1459,9 @@ int vgetc(void) mouse_row = old_mouse_row; mouse_col = old_mouse_col; } else { - mod_mask = 0x0; + mod_mask = 0; last_recorded_len = 0; + for (;;) { // this is done twice if there are modifiers bool did_inc = false; if (mod_mask) { // no mapping after modifier has been read @@ -1571,14 +1571,9 @@ int vgetc(void) buf[i] = (char_u)vgetorpeek(true); if (buf[i] == K_SPECIAL) { // Must be a K_SPECIAL - KS_SPECIAL - KE_FILLER sequence, - // which represents a K_SPECIAL (0x80), - // or a CSI - KS_EXTRA - KE_CSI sequence, which represents - // a CSI (0x9B), - // of a K_SPECIAL - KS_EXTRA - KE_CSI, which is CSI too. - c = vgetorpeek(true); - if (vgetorpeek(true) == KE_CSI && c == KS_EXTRA) { - buf[i] = CSI; - } + // which represents a K_SPECIAL (0x80). + (void)vgetorpeek(true); // skip KS_SPECIAL + (void)vgetorpeek(true); // skip KE_FILLER } } no_mapping--; @@ -1592,8 +1587,9 @@ int vgetc(void) if (!no_mapping && KeyTyped && !(State & TERM_FOCUS) && (mod_mask == MOD_MASK_ALT || mod_mask == MOD_MASK_META)) { mod_mask = 0; - ins_char_typebuf(c); - ins_char_typebuf(ESC); + int len = ins_char_typebuf(c, 0); + (void)ins_char_typebuf(ESC, 0); + ungetchars(len + 3); // The ALT/META modifier takes three more bytes continue; } @@ -1693,7 +1689,7 @@ typedef enum { map_result_fail, // failed, break loop map_result_get, // get a character from typeahead map_result_retry, // try to map again - map_result_nomatch // no matching mapping, get char + map_result_nomatch, // no matching mapping, get char } map_result_T; /// Handle mappings in the typeahead buffer. @@ -1714,6 +1710,15 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) int keylen = *keylenp; int i; int local_State = get_real_state(); + bool is_plug_map = false; + + // Check if typehead starts with a <Plug> mapping. + // In that case we will ignore nore flag on it. + if (typebuf.tb_buf[typebuf.tb_off] == K_SPECIAL + && typebuf.tb_buf[typebuf.tb_off+1] == KS_EXTRA + && typebuf.tb_buf[typebuf.tb_off+2] == KE_PLUG) { + is_plug_map = true; + } // Check for a mappable key sequence. // Walk through one maphash[] list until we find an entry that matches. @@ -1729,7 +1734,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) tb_c1 = typebuf.tb_buf[typebuf.tb_off]; if (no_mapping == 0 && maphash_valid && (no_zero_mapping == 0 || tb_c1 != '0') - && (typebuf.tb_maplen == 0 + && (typebuf.tb_maplen == 0 || is_plug_map || (p_remap && !(typebuf.tb_noremap[typebuf.tb_off] & (RM_NONE|RM_ABBR)))) && !(p_paste && (State & (INSERT + CMDLINE))) @@ -1817,7 +1822,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) break; } } - if (n >= 0) { + if (!is_plug_map && n >= 0) { continue; } @@ -1902,7 +1907,7 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) // complete match if (keylen >= 0 && keylen <= typebuf.tb_len) { - char_u *map_str; + char_u *map_str = NULL; int save_m_expr; int save_m_noremap; int save_m_silent; @@ -1947,20 +1952,52 @@ static int handle_mapping(int *keylenp, bool *timedout, int *mapdepth) save_m_silent = mp->m_silent; char_u *save_m_keys = NULL; // only saved when needed char_u *save_m_str = NULL; // only saved when needed + LuaRef save_m_luaref = mp->m_luaref; // Handle ":map <expr>": evaluate the {rhs} as an // expression. Also save and restore the command line // for "normal :". if (mp->m_expr) { - int save_vgetc_busy = vgetc_busy; + const int save_vgetc_busy = vgetc_busy; const bool save_may_garbage_collect = may_garbage_collect; + const int save_cursor_row = ui_current_row(); + const int save_cursor_col = ui_current_col(); + const int prev_did_emsg = did_emsg; vgetc_busy = 0; may_garbage_collect = false; save_m_keys = vim_strsave(mp->m_keys); - save_m_str = vim_strsave(mp->m_str); - map_str = eval_map_expr(save_m_str, NUL); + if (save_m_luaref == LUA_NOREF) { + save_m_str = vim_strsave(mp->m_str); + } + map_str = eval_map_expr(mp, NUL); + + // The mapping may do anything, but we expect it to take care of + // redrawing. Do put the cursor back where it was. + ui_cursor_goto(save_cursor_row, save_cursor_col); + ui_flush(); + + // If an error was displayed and the expression returns an empty + // string, generate a <Nop> to allow for a redraw. + if (prev_did_emsg != did_emsg && (map_str == NULL || *map_str == NUL)) { + char_u buf[4]; + xfree(map_str); + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = KE_IGNORE; + buf[3] = NUL; + map_str = vim_strsave(buf); + if (State & CMDLINE) { + // redraw the command below the error + msg_didout = true; + if (msg_row < cmdline_row) { + msg_row = cmdline_row; + } + redrawcmd(); + } + } + vgetc_busy = save_vgetc_busy; may_garbage_collect = save_may_garbage_collect; } else { @@ -2039,7 +2076,7 @@ void vungetc(int c) /// /// When `no_mapping` (global) is zero, checks for mappings in the current mode. /// Only returns one byte (of a multi-byte character). -/// K_SPECIAL and CSI may be escaped, need to get two more bytes then. +/// K_SPECIAL may be escaped, need to get two more bytes then. static int vgetorpeek(bool advance) { int c, c1; @@ -2294,6 +2331,10 @@ static int vgetorpeek(bool advance) c = ESC; } tc = c; + + // no chars to block abbreviations for + typebuf.tb_no_abbr_cnt = 0; + break; } @@ -2452,7 +2493,7 @@ static int vgetorpeek(bool advance) /// 1. a scriptfile /// 2. the keyboard /// -/// As much characters as we can get (up to 'maxlen') are put in "buf" and +/// As many characters as we can get (up to 'maxlen') are put in "buf" and /// NUL terminated (buffer length must be 'maxlen' + 1). /// Minimum for "maxlen" is 3!!!! /// @@ -2470,7 +2511,7 @@ static int vgetorpeek(bool advance) /// Return the number of obtained characters. /// Return -1 when end of input script reached. /// -/// @param wait_time milli seconds +/// @param wait_time milliseconds int inchar(char_u *buf, int maxlen, long wait_time) { int len = 0; // Init for GCC. @@ -2569,7 +2610,7 @@ int fix_input_buffer(char_u *buf, int len) FUNC_ATTR_NONNULL_ALL { if (!using_script()) { - // Should not escape K_SPECIAL/CSI reading input from the user because vim + // Should not escape K_SPECIAL reading input from the user because vim // key codes keys are processed in input.c/input_enqueue. buf[len] = NUL; return len; @@ -2580,9 +2621,8 @@ int fix_input_buffer(char_u *buf, int len) char_u *p = buf; // Two characters are special: NUL and K_SPECIAL. - // Replace NUL by K_SPECIAL KS_ZERO KE_FILLER + // Replace NUL by K_SPECIAL KS_ZERO KE_FILLER // Replace K_SPECIAL by K_SPECIAL KS_SPECIAL KE_FILLER - // Replace CSI by K_SPECIAL KS_EXTRA KE_CSI for (i = len; --i >= 0; ++p) { if (p[0] == NUL || (p[0] == K_SPECIAL @@ -2618,11 +2658,13 @@ int fix_input_buffer(char_u *buf, int len) /// @param[in] orig_lhs Original mapping LHS, with characters to replace. /// @param[in] orig_lhs_len `strlen` of orig_lhs. /// @param[in] orig_rhs Original mapping RHS, with characters to replace. +/// @param[in] rhs_lua Lua reference for Lua maps. /// @param[in] orig_rhs_len `strlen` of orig_rhs. /// @param[in] cpo_flags See param docs for @ref replace_termcodes. /// @param[out] mapargs MapArguments struct holding the replaced strings. -void set_maparg_lhs_rhs(const char_u *orig_lhs, const size_t orig_lhs_len, const char_u *orig_rhs, - const size_t orig_rhs_len, int cpo_flags, MapArguments *mapargs) +void set_maparg_lhs_rhs(const char_u *orig_lhs, const size_t orig_lhs_len, + const char_u *orig_rhs, const size_t orig_rhs_len, + LuaRef rhs_lua, int cpo_flags, MapArguments *mapargs) { char_u *lhs_buf = NULL; char_u *rhs_buf = NULL; @@ -2638,22 +2680,34 @@ void set_maparg_lhs_rhs(const char_u *orig_lhs, const size_t orig_lhs_len, const true, true, true, cpo_flags); mapargs->lhs_len = STRLEN(replaced); STRLCPY(mapargs->lhs, replaced, sizeof(mapargs->lhs)); + mapargs->rhs_lua = rhs_lua; - mapargs->orig_rhs_len = orig_rhs_len; - mapargs->orig_rhs = xcalloc(mapargs->orig_rhs_len + 1, sizeof(char_u)); - STRLCPY(mapargs->orig_rhs, orig_rhs, mapargs->orig_rhs_len + 1); + if (rhs_lua == LUA_NOREF) { + mapargs->orig_rhs_len = orig_rhs_len; + mapargs->orig_rhs = xcalloc(mapargs->orig_rhs_len + 1, sizeof(char_u)); + STRLCPY(mapargs->orig_rhs, orig_rhs, mapargs->orig_rhs_len + 1); - if (STRICMP(orig_rhs, "<nop>") == 0) { // "<Nop>" means nothing - mapargs->rhs = xcalloc(1, sizeof(char_u)); // single null-char - mapargs->rhs_len = 0; - mapargs->rhs_is_noop = true; + if (STRICMP(orig_rhs, "<nop>") == 0) { // "<Nop>" means nothing + mapargs->rhs = xcalloc(1, sizeof(char_u)); // single null-char + mapargs->rhs_len = 0; + mapargs->rhs_is_noop = true; + } else { + replaced = replace_termcodes(orig_rhs, orig_rhs_len, &rhs_buf, + false, true, true, cpo_flags); + mapargs->rhs_len = STRLEN(replaced); + mapargs->rhs_is_noop = false; + mapargs->rhs = xcalloc(mapargs->rhs_len + 1, sizeof(char_u)); + STRLCPY(mapargs->rhs, replaced, mapargs->rhs_len + 1); + } } else { - replaced = replace_termcodes(orig_rhs, orig_rhs_len, &rhs_buf, - false, true, true, cpo_flags); - mapargs->rhs_len = STRLEN(replaced); - mapargs->rhs_is_noop = false; - mapargs->rhs = xcalloc(mapargs->rhs_len + 1, sizeof(char_u)); - STRLCPY(mapargs->rhs, replaced, mapargs->rhs_len + 1); + char tmp_buf[64]; + // orig_rhs is not used for Lua mappings, but still needs to be a string. + mapargs->orig_rhs = xcalloc(1, sizeof(char_u)); + mapargs->orig_rhs_len = 0; + // stores <lua>ref_no<cr> in map_str + mapargs->rhs_len = (size_t)vim_snprintf(S_LEN(tmp_buf), "%c%c%c%d\r", K_SPECIAL, + (char_u)KS_EXTRA, KE_LUA, rhs_lua); + mapargs->rhs = vim_strsave((char_u *)tmp_buf); } xfree(lhs_buf); @@ -2765,7 +2819,7 @@ int str_to_mapargs(const char_u *strargs, bool is_unmap, MapArguments *mapargs) size_t orig_rhs_len = STRLEN(rhs_start); set_maparg_lhs_rhs(lhs_to_replace, orig_lhs_len, - rhs_start, orig_rhs_len, + rhs_start, orig_rhs_len, LUA_NOREF, CPO_TO_CPO_FLAGS, &parsed_args); xfree(lhs_to_replace); @@ -2827,7 +2881,7 @@ int buf_do_map(int maptype, MapArguments *args, int mode, bool is_abbrev, buf_T validate_maphash(); bool has_lhs = (args->lhs[0] != NUL); - bool has_rhs = (args->rhs[0] != NUL) || args->rhs_is_noop; + bool has_rhs = args->rhs_lua != LUA_NOREF || (args->rhs[0] != NUL) || args->rhs_is_noop; // check for :unmap without argument if (maptype == 1 && !has_lhs) { @@ -3017,10 +3071,14 @@ int buf_do_map(int maptype, MapArguments *args, int mode, bool is_abbrev, buf_T } else { // new rhs for existing entry mp->m_mode &= ~mode; // remove mode bits if (mp->m_mode == 0 && !did_it) { // reuse entry - xfree(mp->m_str); + XFREE_CLEAR(mp->m_str); + XFREE_CLEAR(mp->m_orig_str); + XFREE_CLEAR(mp->m_desc); + NLUA_CLEAR_REF(mp->m_luaref); + mp->m_str = vim_strsave(rhs); - xfree(mp->m_orig_str); mp->m_orig_str = vim_strsave(orig_rhs); + mp->m_luaref = args->rhs_lua; mp->m_noremap = noremap; mp->m_nowait = args->nowait; mp->m_silent = args->silent; @@ -3028,6 +3086,10 @@ int buf_do_map(int maptype, MapArguments *args, int mode, bool is_abbrev, buf_T mp->m_expr = args->expr; mp->m_script_ctx = current_sctx; mp->m_script_ctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&mp->m_script_ctx); + if (args->desc != NULL) { + mp->m_desc = xstrdup(args->desc); + } did_it = true; } } @@ -3096,6 +3158,7 @@ int buf_do_map(int maptype, MapArguments *args, int mode, bool is_abbrev, buf_T mp->m_keys = vim_strsave(lhs); mp->m_str = vim_strsave(rhs); mp->m_orig_str = vim_strsave(orig_rhs); + mp->m_luaref = args->rhs_lua; mp->m_keylen = (int)STRLEN(mp->m_keys); mp->m_noremap = noremap; mp->m_nowait = args->nowait; @@ -3104,6 +3167,11 @@ int buf_do_map(int maptype, MapArguments *args, int mode, bool is_abbrev, buf_T mp->m_expr = args->expr; mp->m_script_ctx = current_sctx; mp->m_script_ctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&mp->m_script_ctx); + mp->m_desc = NULL; + if (args->desc != NULL) { + mp->m_desc = xstrdup(args->desc); + } // add the new entry in front of the abbrlist or maphash[] list if (is_abbrev) { @@ -3200,8 +3268,10 @@ static void mapblock_free(mapblock_T **mpp) mp = *mpp; xfree(mp->m_keys); - xfree(mp->m_str); - xfree(mp->m_orig_str); + NLUA_CLEAR_REF(mp->m_luaref); + XFREE_CLEAR(mp->m_str); + XFREE_CLEAR(mp->m_orig_str); + XFREE_CLEAR(mp->m_desc); *mpp = mp->m_next; xfree(mp); } @@ -3392,7 +3462,8 @@ static void showmap(mapblock_T *mp, bool local) { size_t len = 1; - if (message_filtered(mp->m_keys) && message_filtered(mp->m_str)) { + if (message_filtered(mp->m_keys) && message_filtered(mp->m_str) + && (mp->m_desc == NULL || message_filtered((char_u *)mp->m_desc))) { return; } @@ -3437,19 +3508,29 @@ static void showmap(mapblock_T *mp, bool local) /* Use FALSE below if we only want things like <Up> to show up as such on * the rhs, and not M-x etc, TRUE gets both -- webb */ - if (*mp->m_str == NUL) { + if (mp->m_luaref != LUA_NOREF) { + char msg[100]; + snprintf(msg, sizeof(msg), "<Lua function %d>", mp->m_luaref); + msg_puts_attr(msg, HL_ATTR(HLF_8)); + } else if (mp->m_str[0] == NUL) { msg_puts_attr("<Nop>", HL_ATTR(HLF_8)); } else { - // Remove escaping of CSI, because "m_str" is in a format to be used + // Remove escaping of K_SPECIAL, because "m_str" is in a format to be used // as typeahead. char_u *s = vim_strsave(mp->m_str); - vim_unescape_csi(s); + vim_unescape_ks(s); msg_outtrans_special(s, false, 0); xfree(s); } + + if (mp->m_desc != NULL) { + msg_puts("\n "); // Shift line to same level as rhs. + msg_puts(mp->m_desc); + } if (p_verbose > 0) { last_set_msg(mp->m_script_ctx); } + msg_clr_eos(); ui_flush(); // show one line at a time } @@ -3532,8 +3613,7 @@ int map_to_exists_mode(const char *const rhs, const int mode, const bool abbr) mp = maphash[hash]; } for (; mp; mp = mp->m_next) { - if ((mp->m_mode & mode) - && strstr((char *)mp->m_str, rhs) != NULL) { + if ((mp->m_mode & mode) && strstr((char *)mp->m_str, rhs) != NULL) { return true; } } @@ -3818,9 +3898,9 @@ bool check_abbr(int c, char_u *ptr, int col, int mincol) int match; if (strchr((const char *)mp->m_keys, K_SPECIAL) != NULL) { - // Might have CSI escaped mp->m_keys. + // Might have K_SPECIAL escaped mp->m_keys. q = vim_strsave(mp->m_keys); - vim_unescape_csi(q); + vim_unescape_ks(q); qlen = (int)STRLEN(q); } // find entries with right mode and keys @@ -3866,7 +3946,7 @@ bool check_abbr(int c, char_u *ptr, int col, int mincol) int newlen = utf_char2bytes(c, tb + j); tb[j + newlen] = NUL; // Need to escape K_SPECIAL. - char_u *escaped = vim_strsave_escape_csi(tb + j); + char_u *escaped = vim_strsave_escape_ks(tb + j); if (escaped != NULL) { newlen = (int)STRLEN(escaped); memmove(tb + j, escaped, (size_t)newlen); @@ -3879,7 +3959,7 @@ bool check_abbr(int c, char_u *ptr, int col, int mincol) (void)ins_typebuf(tb, 1, 0, true, mp->m_silent); } if (mp->m_expr) { - s = eval_map_expr(mp->m_str, c); + s = eval_map_expr(mp, c); } else { s = mp->m_str; } @@ -3909,20 +3989,22 @@ bool check_abbr(int c, char_u *ptr, int col, int mincol) /// special characters. /// /// @param c NUL or typed character for abbreviation -static char_u *eval_map_expr(char_u *str, int c) +static char_u *eval_map_expr(mapblock_T *mp, int c) { char_u *res; - char_u *p; - char_u *expr; + char_u *p = NULL; + char_u *expr = NULL; char_u *save_cmd; pos_T save_cursor; int save_msg_col; int save_msg_row; - /* Remove escaping of CSI, because "str" is in a format to be used as - * typeahead. */ - expr = vim_strsave(str); - vim_unescape_csi(expr); + // Remove escaping of K_SPECIAL, because "str" is in a format to be used as + // typeahead. + if (mp->m_luaref == LUA_NOREF) { + expr = vim_strsave(mp->m_str); + vim_unescape_ks(expr); + } save_cmd = save_cmdline_alloc(); @@ -3934,7 +4016,22 @@ static char_u *eval_map_expr(char_u *str, int c) save_cursor = curwin->w_cursor; save_msg_col = msg_col; save_msg_row = msg_row; - p = eval_to_string(expr, NULL, false); + if (mp->m_luaref != LUA_NOREF) { + Error err = ERROR_INIT; + Array args = ARRAY_DICT_INIT; + Object ret = nlua_call_ref(mp->m_luaref, NULL, args, true, &err); + if (ret.type == kObjectTypeString) { + p = (char_u *)xstrndup(ret.data.string.data, ret.data.string.size); + } + api_free_object(ret); + if (err.type != kErrorTypeNone) { + semsg_multiline("E5108: %s", err.msg); + api_clear_error(&err); + } + } else { + p = eval_to_string(expr, NULL, false); + xfree(expr); + } textlock--; ex_normal_lock--; curwin->w_cursor = save_cursor; @@ -3942,23 +4039,20 @@ static char_u *eval_map_expr(char_u *str, int c) msg_row = save_msg_row; restore_cmdline_alloc(save_cmd); - xfree(expr); if (p == NULL) { return NULL; } - // Escape CSI in the result to be able to use the string as typeahead. - res = vim_strsave_escape_csi(p); + // Escape K_SPECIAL in the result to be able to use the string as typeahead. + res = vim_strsave_escape_ks(p); xfree(p); return res; } -/* - * Copy "p" to allocated memory, escaping K_SPECIAL and CSI so that the result - * can be put in the typeahead buffer. - */ -char_u *vim_strsave_escape_csi(char_u *p) +/// Copy "p" to allocated memory, escaping K_SPECIAL so that the result +/// can be put in the typeahead buffer. +char_u *vim_strsave_escape_ks(char_u *p) { // Need a buffer to hold up to three times as much. Four in case of an // illegal utf-8 byte: @@ -3973,7 +4067,7 @@ char_u *vim_strsave_escape_csi(char_u *p) *d++ = *s++; } else { // Add character, possibly multi-byte to destination, escaping - // CSI and K_SPECIAL. Be careful, it can be an illegal byte! + // K_SPECIAL. Be careful, it can be an illegal byte! d = add_char2buf(utf_ptr2char(s), d); s += utf_ptr2len(s); } @@ -3983,11 +4077,9 @@ char_u *vim_strsave_escape_csi(char_u *p) return res; } -/* - * Remove escaping from CSI and K_SPECIAL characters. Reverse of - * vim_strsave_escape_csi(). Works in-place. - */ -void vim_unescape_csi(char_u *p) +/// Remove escaping from K_SPECIAL characters. Reverse of +/// vim_strsave_escape_ks(). Works in-place. +void vim_unescape_ks(char_u *p) { char_u *s = p, *d = p; @@ -3995,10 +4087,6 @@ void vim_unescape_csi(char_u *p) if (s[0] == K_SPECIAL && s[1] == KS_SPECIAL && s[2] == KE_FILLER) { *d++ = K_SPECIAL; s += 3; - } else if ((s[0] == K_SPECIAL || s[0] == CSI) - && s[1] == KS_EXTRA && s[2] == (int)KE_CSI) { - *d++ = CSI; - s += 3; } else { *d++ = *s++; } @@ -4049,8 +4137,11 @@ int makemap(FILE *fd, buf_T *buf) continue; } - // skip mappings that contain a <SNR> (script-local thing), + // skip lua mappings and mappings that contain a <SNR> (script-local thing), // they probably don't work when loaded again + if (mp->m_luaref != LUA_NOREF) { + continue; + } for (p = mp->m_str; *p != NUL; p++) { if (p[0] == K_SPECIAL && p[1] == KS_EXTRA && p[2] == (int)KE_SNR) { @@ -4238,7 +4329,7 @@ int put_escstr(FILE *fd, char_u *strstart, int what) for (; *str != NUL; str++) { // Check for a multi-byte character, which may contain escaped - // K_SPECIAL and CSI bytes. + // K_SPECIAL bytes. const char *p = mb_unescape((const char **)&str); if (p != NULL) { while (*p != NUL) { @@ -4331,10 +4422,11 @@ int put_escstr(FILE *fd, char_u *strstart, int what) /// @param mp_ptr return: pointer to mapblock or NULL /// @param local_ptr return: buffer-local mapping or NULL char_u *check_map(char_u *keys, int mode, int exact, int ign_mod, int abbr, mapblock_T **mp_ptr, - int *local_ptr) + int *local_ptr, int *rhs_lua) { int len, minlen; mapblock_T *mp; + *rhs_lua = LUA_NOREF; validate_maphash(); @@ -4375,7 +4467,8 @@ char_u *check_map(char_u *keys, int mode, int exact, int ign_mod, int abbr, mapb if (local_ptr != NULL) { *local_ptr = local; } - return mp->m_str; + *rhs_lua = mp->m_luaref; + return mp->m_luaref == LUA_NOREF ? mp->m_str : NULL; } } } @@ -4560,3 +4653,47 @@ char_u *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat) return (char_u *)line_ga.ga_data; } + +bool map_execute_lua(void) +{ + garray_T line_ga; + int c1 = -1; + bool aborted = false; + + ga_init(&line_ga, 1, 32); + + no_mapping++; + + got_int = false; + while (c1 != NUL && !aborted) { + ga_grow(&line_ga, 32); + // Get one character at a time. + c1 = vgetorpeek(true); + if (got_int) { + aborted = true; + } else if (c1 == '\r' || c1 == '\n') { + c1 = NUL; // end the line + } else { + ga_append(&line_ga, (char)c1); + } + } + + no_mapping--; + + if (aborted) { + ga_clear(&line_ga); + return false; + } + + LuaRef ref = (LuaRef)atoi(line_ga.ga_data); + Error err = ERROR_INIT; + Array args = ARRAY_DICT_INIT; + nlua_call_ref(ref, NULL, args, false, &err); + if (err.type != kErrorTypeNone) { + semsg_multiline("E5108: %s", err.msg); + api_clear_error(&err); + } + + ga_clear(&line_ga); + return true; +} diff --git a/src/nvim/getchar.h b/src/nvim/getchar.h index 5950611d3f..f24a4e7c7c 100644 --- a/src/nvim/getchar.h +++ b/src/nvim/getchar.h @@ -50,14 +50,16 @@ struct map_arguments { char_u *rhs; /// The {rhs} of the mapping. size_t rhs_len; + LuaRef rhs_lua; /// lua function as rhs bool rhs_is_noop; /// True when the {orig_rhs} is <nop>. char_u *orig_rhs; /// The original text of the {rhs}. size_t orig_rhs_len; + char *desc; /// map description }; typedef struct map_arguments MapArguments; #define MAP_ARGUMENTS_INIT { false, false, false, false, false, false, false, \ - { 0 }, 0, NULL, 0, false, NULL, 0 } + { 0 }, 0, NULL, 0, LUA_NOREF, false, NULL, 0, NULL } #define KEYLEN_PART_KEY -1 // keylen value for incomplete key-code #define KEYLEN_PART_MAP -2 // keylen value for incomplete mapping diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 697d4b11a7..35ad57906b 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -28,7 +28,7 @@ #endif #ifndef FILETYPE_FILE -# define FILETYPE_FILE "filetype.vim" +# define FILETYPE_FILE "filetype.lua filetype.vim" #endif #ifndef FTPLUGIN_FILE @@ -127,7 +127,7 @@ typedef off_t off_T; // When vgetc() is called, it sets mod_mask to the set of modifiers that are // held down based on the MOD_MASK_* symbols that are read first. -EXTERN int mod_mask INIT(= 0x0); // current key modifiers +EXTERN int mod_mask INIT(= 0); // current key modifiers // Cmdline_row is the row where the command line starts, just below the @@ -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 }); @@ -524,6 +524,8 @@ EXTERN pos_T VIsual; EXTERN int VIsual_active INIT(= false); /// Whether Select mode is active. EXTERN int VIsual_select INIT(= false); +/// Register name for Select mode +EXTERN int VIsual_select_reg INIT(= 0); /// Restart Select mode when next cmd finished EXTERN int restart_VIsual_select INIT(= 0); /// Whether to restart the selection after a Select-mode mapping or menu. @@ -977,6 +979,7 @@ EXTERN char e_invalidreg[] INIT(= N_("E850: Invalid register name")); EXTERN char e_dirnotf[] INIT(= N_("E919: Directory not found in '%s': \"%s\"")); EXTERN char e_au_recursive[] INIT(= N_("E952: Autocommand caused recursive behavior")); EXTERN char e_autocmd_close[] INIT(= N_("E813: Cannot close autocmd window")); +EXTERN char e_listarg[] INIT(= N_("E686: Argument of %s must be a List")); EXTERN char e_unsupportedoption[] INIT(= N_("E519: Option not supported")); EXTERN char e_fnametoolong[] INIT(= N_("E856: Filename too long")); EXTERN char e_float_as_string[] INIT(= N_("E806: using Float as a String")); @@ -997,6 +1000,10 @@ EXTERN char e_non_empty_string_required[] INIT(= N_("E1142: Non-empty string req EXTERN char e_cannot_define_autocommands_for_all_events[] INIT(= N_("E1155: Cannot define autocommands for ALL events")); +EXTERN char e_resulting_text_too_long[] INIT(= N_("E1240: Resulting text too long")); + +EXTERN char e_line_number_out_of_range[] INIT(= N_("E1247: Line number out of range")); + EXTERN char e_highlight_group_name_too_long[] INIT(= N_("E1249: Highlight group name too long")); EXTERN char top_bot_msg[] INIT(= N_("search hit TOP, continuing at BOTTOM")); diff --git a/src/nvim/hardcopy.c b/src/nvim/hardcopy.c index 6fc70144ac..eb10c65be9 100644 --- a/src/nvim/hardcopy.c +++ b/src/nvim/hardcopy.c @@ -386,30 +386,43 @@ static uint32_t prt_get_term_color(int colorindex) return cterm_color_8[colorindex % 8]; } -static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec) +static uint32_t prt_get_color(int hl_id, int modec) { int colorindex; uint32_t fg_color; + const char *color = highlight_color(hl_id, "fg#", 'g'); + if (color != NULL) { + RgbValue rgb = name_to_color(color); + if (rgb != -1) { + return (uint32_t)rgb; + } + } + + color = highlight_color(hl_id, "fg", modec); + if (color == NULL) { + colorindex = 0; + } else { + colorindex = atoi(color); + } + + if (colorindex >= 0 && colorindex < t_colors) { + fg_color = prt_get_term_color(colorindex); + } else { + fg_color = PRCOLOR_BLACK; + } + + return fg_color; +} + +static void prt_get_attr(int hl_id, prt_text_attr_T *pattr, int modec) +{ pattr->bold = (highlight_has_attr(hl_id, HL_BOLD, modec) != NULL); pattr->italic = (highlight_has_attr(hl_id, HL_ITALIC, modec) != NULL); pattr->underline = (highlight_has_attr(hl_id, HL_UNDERLINE, modec) != NULL); pattr->undercurl = (highlight_has_attr(hl_id, HL_UNDERCURL, modec) != NULL); - { - const char *color = highlight_color(hl_id, "fg", modec); - if (color == NULL) { - colorindex = 0; - } else { - colorindex = atoi(color); - } - - if (colorindex >= 0 && colorindex < t_colors) { - fg_color = prt_get_term_color(colorindex); - } else { - fg_color = PRCOLOR_BLACK; - } - } + uint32_t fg_color = prt_get_color(hl_id, modec); if (fg_color == PRCOLOR_WHITE) { fg_color = PRCOLOR_BLACK; diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index 3050ca02de..e43a56086f 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -144,13 +144,19 @@ int hl_get_syn_attr(int ns_id, int idx, HlAttrs at_en) } } -void ns_hl_def(NS ns_id, int hl_id, HlAttrs attrs, int link_id) +void ns_hl_def(NS ns_id, int hl_id, HlAttrs attrs, int link_id, Dict(highlight) *dict) { - DecorProvider *p = get_decor_provider(ns_id, true); if ((attrs.rgb_ae_attr & HL_DEFAULT) && map_has(ColorKey, ColorItem)(&ns_hl, ColorKey(ns_id, hl_id))) { return; } + if (ns_id == 0) { + assert(dict); + // set in global (':highlight') namespace + set_hl_group(hl_id, attrs, dict, link_id); + return; + } + DecorProvider *p = get_decor_provider(ns_id, true); int attr_id = link_id > 0 ? -1 : hl_get_syn_attr(ns_id, hl_id, attrs); ColorItem it = { .attr_id = attr_id, .link_id = link_id, @@ -192,23 +198,17 @@ int ns_get_hl(NS ns_id, int hl_id, bool link, bool nodefault) int tmp = false; HlAttrs attrs = HLATTRS_INIT; if (ret.type == kObjectTypeDictionary) { - Dictionary dict = ret.data.dictionary; fallback = false; - attrs = dict2hlattrs(dict, true, &it.link_id, &err); - for (size_t i = 0; i < dict.size; i++) { - char *key = dict.items[i].key.data; - Object val = dict.items[i].value; - bool truthy = api_object_to_bool(val, key, false, &err); - - if (strequal(key, "fallback")) { - fallback = truthy; - } else if (strequal(key, "temp")) { - tmp = truthy; + Dict(highlight) dict = { 0 }; + if (api_dict_to_keydict(&dict, KeyDict_highlight_get_field, + ret.data.dictionary, &err)) { + attrs = dict2hlattrs(&dict, true, &it.link_id, &err); + fallback = api_object_to_bool(dict.fallback, "fallback", true, &err); + tmp = api_object_to_bool(dict.fallback, "tmp", false, &err); + if (it.link_id >= 0) { + fallback = true; } } - if (it.link_id >= 0) { - fallback = true; - } } it.attr_id = fallback ? -1 : hl_get_syn_attr((int)ns_id, hl_id, attrs); @@ -796,112 +796,121 @@ Dictionary hlattrs2dict(HlAttrs ae, bool use_rgb) return hl; } -HlAttrs dict2hlattrs(Dictionary dict, bool use_rgb, int *link_id, Error *err) +HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *err) { HlAttrs hlattrs = HLATTRS_INIT; - int32_t fg = -1, bg = -1, ctermfg = -1, ctermbg = -1, sp = -1; + int blend = -1; int16_t mask = 0; int16_t cterm_mask = 0; bool cterm_mask_provided = false; - for (size_t i = 0; i < dict.size; i++) { - char *key = dict.items[i].key.data; - Object val = dict.items[i].value; - - struct { - const char *name; - int16_t flag; - } flags[] = { - { "bold", HL_BOLD }, - { "standout", HL_STANDOUT }, - { "underline", HL_UNDERLINE }, - { "undercurl", HL_UNDERCURL }, - { "italic", HL_ITALIC }, - { "reverse", HL_INVERSE }, - { "default", HL_DEFAULT }, - { "global", HL_GLOBAL }, - { NULL, 0 }, - }; - - int j; - for (j = 0; flags[j].name; j++) { - if (strequal(flags[j].name, key)) { - if (api_object_to_bool(val, key, false, err)) { - mask = mask | flags[j].flag; - } - break; - } +#define CHECK_FLAG(d, m, name, extra, flag) \ + if (api_object_to_bool(d->name ## extra, #name, false, err)) { \ + m = m | flag; \ } - // Handle cterm attrs - if (strequal(key, "cterm") && val.type == kObjectTypeDictionary) { - cterm_mask_provided = true; - Dictionary cterm_dict = val.data.dictionary; - for (size_t l = 0; l < cterm_dict.size; l++) { - char *cterm_dict_key = cterm_dict.items[l].key.data; - Object cterm_dict_val = cterm_dict.items[l].value; - for (int m = 0; flags[m].name; m++) { - if (strequal(flags[m].name, cterm_dict_key)) { - if (api_object_to_bool(cterm_dict_val, cterm_dict_key, false, - err)) { - cterm_mask |= flags[m].flag; - } - break; - } - } - } + CHECK_FLAG(dict, mask, bold, , HL_BOLD); + CHECK_FLAG(dict, mask, standout, , HL_STANDOUT); + CHECK_FLAG(dict, mask, underline, , HL_UNDERLINE); + CHECK_FLAG(dict, mask, undercurl, , HL_UNDERCURL); + CHECK_FLAG(dict, mask, italic, , HL_ITALIC); + CHECK_FLAG(dict, mask, reverse, , HL_INVERSE); + CHECK_FLAG(dict, mask, strikethrough, , HL_STRIKETHROUGH); + CHECK_FLAG(dict, mask, nocombine, , HL_NOCOMBINE); + CHECK_FLAG(dict, mask, default, _, HL_DEFAULT); + CHECK_FLAG(dict, mask, global, , HL_GLOBAL); + + if (HAS_KEY(dict->fg)) { + fg = object_to_color(dict->fg, "fg", true, err); + } else if (HAS_KEY(dict->foreground)) { + fg = object_to_color(dict->foreground, "foreground", true, err); + } + if (ERROR_SET(err)) { + return hlattrs; + } + + if (HAS_KEY(dict->bg)) { + bg = object_to_color(dict->bg, "bg", true, err); + } else if (HAS_KEY(dict->background)) { + bg = object_to_color(dict->background, "background", true, err); + } + if (ERROR_SET(err)) { + return hlattrs; + } + + if (HAS_KEY(dict->sp)) { + sp = object_to_color(dict->sp, "sp", true, err); + } else if (HAS_KEY(dict->special)) { + sp = object_to_color(dict->special, "special", true, err); + } + if (ERROR_SET(err)) { + return hlattrs; + } + + if (dict->blend.type == kObjectTypeInteger) { + Integer blend0 = dict->blend.data.integer; + if (blend0 < 0 || blend0 > 100) { + api_set_error(err, kErrorTypeValidation, "'blend' is not between 0 to 100"); + } else { + blend = (int)blend0; } + } else if (HAS_KEY(dict->blend)) { + api_set_error(err, kErrorTypeValidation, "'blend' must be an integer"); + } + if (ERROR_SET(err)) { + return hlattrs; + } - struct { - const char *name; - const char *shortname; - int *dest; - } colors[] = { - { "foreground", "fg", &fg }, - { "background", "bg", &bg }, - { "ctermfg", NULL, &ctermfg }, - { "ctermbg", NULL, &ctermbg }, - { "special", "sp", &sp }, - { NULL, NULL, NULL }, - }; - - int k; - for (k = 0; (!flags[j].name) && colors[k].name; k++) { - if (strequal(colors[k].name, key) || strequal(colors[k].shortname, key)) { - if (val.type == kObjectTypeInteger) { - *colors[k].dest = (int)val.data.integer; - } else if (val.type == kObjectTypeString) { - String str = val.data.string; - // TODO(bfredl): be more fancy with "bg", "fg" etc - if (str.size) { - *colors[k].dest = name_to_color(str.data); - } - } else { - api_set_error(err, kErrorTypeValidation, - "'%s' must be string or integer", key); - } - break; + if (HAS_KEY(dict->link)) { + if (link_id) { + *link_id = object_to_hl_id(dict->link, "link", err); + if (ERROR_SET(err)) { + return hlattrs; } + } else { + api_set_error(err, kErrorTypeValidation, "Invalid Key: 'link'"); } + } - if (flags[j].name || colors[k].name) { - // handled above - } else if (link_id && strequal(key, "link")) { - if (val.type == kObjectTypeString) { - String str = val.data.string; - *link_id = syn_check_group(str.data, (int)str.size); - } else if (val.type == kObjectTypeInteger) { - // TODO(bfredl): validate range? - *link_id = (int)val.data.integer; - } else { - api_set_error(err, kErrorTypeValidation, - "'link' must be string or integer"); - } + // Handle cterm attrs + if (dict->cterm.type == kObjectTypeDictionary) { + Dict(highlight_cterm) cterm[1] = { 0 }; + if (!api_dict_to_keydict(cterm, KeyDict_highlight_cterm_get_field, + dict->cterm.data.dictionary, err)) { + return hlattrs; } + cterm_mask_provided = true; + CHECK_FLAG(cterm, cterm_mask, bold, , HL_BOLD); + CHECK_FLAG(cterm, cterm_mask, standout, , HL_STANDOUT); + CHECK_FLAG(cterm, cterm_mask, underline, , HL_UNDERLINE); + CHECK_FLAG(cterm, cterm_mask, undercurl, , HL_UNDERCURL); + CHECK_FLAG(cterm, cterm_mask, italic, , HL_ITALIC); + CHECK_FLAG(cterm, cterm_mask, reverse, , HL_INVERSE); + CHECK_FLAG(cterm, cterm_mask, strikethrough, , HL_STRIKETHROUGH); + CHECK_FLAG(cterm, cterm_mask, nocombine, , HL_NOCOMBINE); + + } else if (dict->cterm.type == kObjectTypeArray && dict->cterm.data.array.size == 0) { + // empty list from Lua API should clear all cterm attributes + // TODO(clason): handle via gen_api_dispatch + cterm_mask_provided = true; + } else if (HAS_KEY(dict->cterm)) { + api_set_error(err, kErrorTypeValidation, "'cterm' must be a Dictionary."); + } +#undef CHECK_FLAG + + if (HAS_KEY(dict->ctermfg)) { + ctermfg = object_to_color(dict->ctermfg, "ctermfg", false, err); if (ERROR_SET(err)) { - return hlattrs; // error set, caller should not use retval + return hlattrs; + } + } + + if (HAS_KEY(dict->ctermbg)) { + ctermbg = object_to_color(dict->ctermbg, "ctermbg", false, err); + if (ERROR_SET(err)) { + return hlattrs; } } @@ -914,19 +923,45 @@ HlAttrs dict2hlattrs(Dictionary dict, bool use_rgb, int *link_id, Error *err) hlattrs.rgb_bg_color = bg; hlattrs.rgb_fg_color = fg; hlattrs.rgb_sp_color = sp; - hlattrs.cterm_bg_color = - ctermbg == -1 ? cterm_normal_bg_color : ctermbg + 1; - hlattrs.cterm_fg_color = - ctermfg == -1 ? cterm_normal_fg_color : ctermfg + 1; + hlattrs.hl_blend = blend; + hlattrs.cterm_bg_color = ctermbg == -1 ? 0 : ctermbg + 1; + hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : ctermfg + 1; hlattrs.cterm_ae_attr = cterm_mask; } else { hlattrs.cterm_ae_attr = cterm_mask; - hlattrs.cterm_bg_color = bg == -1 ? cterm_normal_bg_color : bg + 1; - hlattrs.cterm_fg_color = fg == -1 ? cterm_normal_fg_color : fg + 1; + hlattrs.cterm_bg_color = bg == -1 ? 0 : bg + 1; + hlattrs.cterm_fg_color = fg == -1 ? 0 : fg + 1; } return hlattrs; } + +int object_to_color(Object val, char *key, bool rgb, Error *err) +{ + if (val.type == kObjectTypeInteger) { + return (int)val.data.integer; + } else if (val.type == kObjectTypeString) { + String str = val.data.string; + // TODO(bfredl): be more fancy with "bg", "fg" etc + if (!str.size || STRICMP(str.data, "NONE") == 0) { + return -1; + } + int color; + if (rgb) { + color = name_to_color(str.data); + } else { + color = name_to_ctermcolor(str.data); + } + if (color < 0) { + api_set_error(err, kErrorTypeValidation, "'%s' is not a valid color", str.data); + } + return color; + } else { + api_set_error(err, kErrorTypeValidation, "'%s' must be string or integer", key); + return 0; + } +} + Array hl_inspect(int attr) { Array ret = ARRAY_DICT_INIT; diff --git a/src/nvim/if_cscope.c b/src/nvim/if_cscope.c index daef8db267..9ca01137cf 100644 --- a/src/nvim/if_cscope.c +++ b/src/nvim/if_cscope.c @@ -919,8 +919,8 @@ static int cs_find(exarg_T *eap) /// Common code for cscope find, shared by cs_find() and ex_cstag(). -static bool cs_find_common(char *opt, char *pat, int forceit, int verbose, - bool use_ll, char_u *cmdline) +static bool cs_find_common(char *opt, char *pat, int forceit, int verbose, bool use_ll, + char_u *cmdline) { char *cmd; int *nummatches; @@ -1594,7 +1594,6 @@ static char *cs_pathcomponents(char *path) char *s = path + strlen(path) - 1; for (int i = 0; i < p_cspc; i++) { while (s > path && *--s != '/') { - continue; } } if ((s > path && *s == '/')) { @@ -1813,7 +1812,6 @@ static int cs_read_prompt(size_t i) static void sig_handler(int s) { // do nothing - return; } #endif diff --git a/src/nvim/indent_c.c b/src/nvim/indent_c.c index faa9b38cf7..7f483d02ab 100644 --- a/src/nvim/indent_c.c +++ b/src/nvim/indent_c.c @@ -41,9 +41,7 @@ static pos_T *ind_find_start_comment(void) // XXX pos_T *find_start_comment(int ind_maxcomment) // XXX { - pos_T *pos; - char_u *line; - char_u *p; + pos_T *pos; int64_t cur_maxcomment = ind_maxcomment; for (;; ) { @@ -55,11 +53,9 @@ pos_T *find_start_comment(int ind_maxcomment) // XXX * Check if the comment start we found is inside a string. * If it is then restrict the search to below this line and try again. */ - line = ml_get(pos->lnum); - for (p = line; *p && (colnr_T)(p - line) < pos->col; ++p) - p = skip_string(p); - if ((colnr_T)(p - line) <= pos->col) + if (!is_pos_in_string(ml_get(pos->lnum), pos->col)) { break; + } cur_maxcomment = curwin->w_cursor.lnum - pos->lnum - 1; if (cur_maxcomment <= 0) { pos = NULL; @@ -110,8 +106,6 @@ static pos_T *ind_find_start_CORS(linenr_T *is_raw) static pos_T *find_start_rawstring(int ind_maxcomment) // XXX { pos_T *pos; - char_u *line; - char_u *p; long cur_maxcomment = ind_maxcomment; for (;;) @@ -124,11 +118,9 @@ static pos_T *find_start_rawstring(int ind_maxcomment) // XXX * Check if the raw string start we found is inside a string. * If it is then restrict the search to below this line and try again. */ - line = ml_get(pos->lnum); - for (p = line; *p && (colnr_T)(p - line) < pos->col; ++p) - p = skip_string(p); - if ((colnr_T)(p - line) <= pos->col) - break; + if (!is_pos_in_string(ml_get(pos->lnum), pos->col)) { + break; + } cur_maxcomment = curwin->w_cursor.lnum - pos->lnum - 1; if (cur_maxcomment <= 0) { @@ -143,7 +135,7 @@ static pos_T *find_start_rawstring(int ind_maxcomment) // XXX * Skip to the end of a "string" and a 'c' character. * If there is no string or character, return argument unmodified. */ -static char_u *skip_string(char_u *p) +static const char_u *skip_string(const char_u *p) { int i; @@ -152,11 +144,11 @@ static char_u *skip_string(char_u *p) */ for (;; p++) { if (p[0] == '\'') { // 'c' or '\n' or '\000' - if (!p[1]) { // ' at end of line + if (p[1] == NUL) { // ' at end of line break; } i = 2; - if (p[1] == '\\') { // '\n' or '\000' + if (p[1] == '\\' && p[2] != NUL) { // '\n' or '\000' i++; while (ascii_isdigit(p[i - 1])) { // '\000' i++; @@ -178,24 +170,24 @@ static char_u *skip_string(char_u *p) continue; // continue for another string } } else if (p[0] == 'R' && p[1] == '"') { - // Raw string: R"[delim](...)[delim]" - char_u *delim = p + 2; - char_u *paren = vim_strchr(delim, '('); - - if (paren != NULL) { - const ptrdiff_t delim_len = paren - delim; - - for (p += 3; *p; ++p) - if (p[0] == ')' && STRNCMP(p + 1, delim, delim_len) == 0 - && p[delim_len + 1] == '"') - { - p += delim_len + 1; - break; - } - if (p[0] == '"') { - continue; // continue for another string - } + // Raw string: R"[delim](...)[delim]" + const char_u *delim = p + 2; + const char_u *paren = vim_strchr(delim, '('); + + if (paren != NULL) { + const ptrdiff_t delim_len = paren - delim; + + for (p += 3; *p; p++) { + if (p[0] == ')' && STRNCMP(p + 1, delim, delim_len) == 0 + && p[delim_len + 1] == '"') { + p += delim_len + 1; + break; + } + } + if (p[0] == '"') { + continue; // continue for another string } + } } break; // no string found } @@ -205,6 +197,16 @@ static char_u *skip_string(char_u *p) return p; } +/// @returns true if "line[col]" is inside a C string. +int is_pos_in_string(const char_u *line, colnr_T col) +{ + const char_u *p; + + for (p = line; *p && (colnr_T)(p - line) < col; p++) { + p = skip_string(p); + } + return !((colnr_T)(p - line) <= col); +} /* * Functions for C-indenting. @@ -218,7 +220,7 @@ static char_u *skip_string(char_u *p) /* * Return true if the string "line" starts with a word from 'cinwords'. */ -bool cin_is_cinword(char_u *line) +bool cin_is_cinword(const char_u *line) { bool retval = false; @@ -246,10 +248,10 @@ bool cin_is_cinword(char_u *line) * Skip over white space and C comments within the line. * Also skip over Perl/shell comments if desired. */ -static char_u *cin_skipcomment(char_u *s) +static const char_u *cin_skipcomment(const char_u *s) { while (*s) { - char_u *prev_s = s; + const char_u *prev_s = s; s = skipwhite(s); @@ -283,7 +285,7 @@ static char_u *cin_skipcomment(char_u *s) * Return TRUE if there is no code at *s. White space and comments are * not considered code. */ -static int cin_nocode(char_u *s) +static int cin_nocode(const char_u *s) { return *cin_skipcomment(s) == NUL; } @@ -312,9 +314,9 @@ static pos_T *find_line_comment(void) // XXX } /// Checks if `text` starts with "key:". -static bool cin_has_js_key(char_u *text) +static bool cin_has_js_key(const char_u *text) { - char_u *s = skipwhite(text); + const char_u *s = skipwhite(text); char_u quote = 0; if (*s == '\'' || *s == '"') { @@ -341,7 +343,7 @@ static bool cin_has_js_key(char_u *text) /// Checks if string matches "label:"; move to character after ':' if true. /// "*s" must point to the start of the label, if there is one. -static bool cin_islabel_skip(char_u **s) +static bool cin_islabel_skip(const char_u **s) FUNC_ATTR_NONNULL_ALL { if (!vim_isIDc(**s)) { // need at least one ID character @@ -361,7 +363,7 @@ static bool cin_islabel_skip(char_u **s) // Note: curwin->w_cursor must be where we are looking for the label. bool cin_islabel(void) // XXX { - char_u *s = cin_skipcomment(get_cursor_line_ptr()); + const char_u *s = cin_skipcomment(get_cursor_line_ptr()); // Exclude "default" from labels, since it should be indented // like a switch label. Same for C++ scope declarations. @@ -380,8 +382,8 @@ bool cin_islabel(void) // XXX * label. */ pos_T cursor_save; - pos_T *trypos; - char_u *line; + pos_T *trypos; + const char_u *line; cursor_save = curwin->w_cursor; while (curwin->w_cursor.lnum > 1) { @@ -424,8 +426,8 @@ bool cin_islabel(void) // XXX */ static int cin_isinit(void) { - char_u *s; - static char *skip[] = {"static", "public", "protected", "private"}; + const char_u *s; + static char *skip[] = { "static", "public", "protected", "private" }; s = cin_skipcomment(get_cursor_line_ptr()); @@ -460,7 +462,7 @@ static int cin_isinit(void) * Recognize a switch label: "case .*:" or "default:". */ bool cin_iscase( - char_u *s, + const char_u *s, bool strict // Allow relaxed check of case statement for JS ) { @@ -503,7 +505,7 @@ bool cin_iscase( /* * Recognize a "default" switch label. */ -static int cin_isdefault(char_u *s) +static int cin_isdefault(const char_u *s) { return STRNCMP(s, "default", 7) == 0 && *(s = cin_skipcomment(s + 7)) == ':' @@ -513,7 +515,7 @@ static int cin_isdefault(char_u *s) /* * Recognize a "public/private/protected" scope declaration label. */ -bool cin_isscopedecl(char_u *s) +bool cin_isscopedecl(const char_u *s) { int i; @@ -534,13 +536,18 @@ bool cin_isscopedecl(char_u *s) #define FIND_NAMESPACE_LIM 20 // Recognize a "namespace" scope declaration. -static bool cin_is_cpp_namespace(char_u *s) +static bool cin_is_cpp_namespace(const char_u *s) { - char_u *p; + const char_u *p; bool has_name = false; bool has_name_start = false; s = cin_skipcomment(s); + + if (STRNCMP(s, "inline", 6) == 0 && (s[6] == NUL || !vim_iswordc(s[6]))) { + s = cin_skipcomment(skipwhite(s + 6)); + } + if (STRNCMP(s, "namespace", 9) == 0 && (s[9] == NUL || !vim_iswordc(s[9]))) { p = cin_skipcomment(skipwhite(s + 9)); while (*p != NUL) { @@ -576,7 +583,7 @@ static bool cin_is_cpp_namespace(char_u *s) * case 234: a = b; * ^ */ -static char_u *after_label(char_u *l) +static const char_u *after_label(const char_u *l) { for (; *l; ++l) { if (*l == ':') { @@ -603,10 +610,10 @@ static char_u *after_label(char_u *l) */ static int get_indent_nolabel(linenr_T lnum) // XXX { - char_u *l; + const char_u *l; pos_T fp; colnr_T col; - char_u *p; + const char_u *p; l = ml_get(lnum); p = after_label(l); @@ -625,9 +632,9 @@ static int get_indent_nolabel(linenr_T lnum) // XXX * label: if (asdf && asdfasdf) * ^ */ -static int skip_label(linenr_T lnum, char_u **pp) +static int skip_label(linenr_T lnum, const char_u **pp) { - char_u *l; + const char_u *l; int amount; pos_T cursor_save; @@ -708,8 +715,8 @@ static int cin_first_id_amount(void) */ static int cin_get_equal_amount(linenr_T lnum) { - char_u *line; - char_u *s; + const char_u *line; + const char_u *s; colnr_T col; pos_T fp; @@ -747,7 +754,7 @@ static int cin_get_equal_amount(linenr_T lnum) /* * Recognize a preprocessor statement: Any line that starts with '#'. */ -static int cin_ispreproc(char_u *s) +static int cin_ispreproc(const char_u *s) { if (*skipwhite(s) == '#') return TRUE; @@ -758,9 +765,9 @@ static int cin_ispreproc(char_u *s) /// continuation line of a preprocessor statement. Decrease "*lnump" to the /// start and return the line in "*pp". /// Put the amount of indent in "*amount". -static int cin_ispreproc_cont(char_u **pp, linenr_T *lnump, int *amount) +static int cin_ispreproc_cont(const char_u **pp, linenr_T *lnump, int *amount) { - char_u *line = *pp; + const char_u *line = *pp; linenr_T lnum = *lnump; int retval = false; int candidate_amount = *amount; @@ -794,7 +801,7 @@ static int cin_ispreproc_cont(char_u **pp, linenr_T *lnump, int *amount) /* * Recognize the start of a C or C++ comment. */ -static int cin_iscomment(char_u *p) +static int cin_iscomment(const char_u *p) { return p[0] == '/' && (p[1] == '*' || p[1] == '/'); } @@ -802,7 +809,7 @@ static int cin_iscomment(char_u *p) /* * Recognize the start of a "//" comment. */ -static int cin_islinecomment(char_u *p) +static int cin_islinecomment(const char_u *p) { return p[0] == '/' && p[1] == '/'; } @@ -817,8 +824,8 @@ static int cin_islinecomment(char_u *p) * both apply in order to determine initializations). */ static char_u -cin_isterminated ( - char_u *s, +cin_isterminated( + const char_u *s, int incl_open, // include '{' at the end as terminator int incl_comma // recognize a trailing comma ) @@ -867,9 +874,9 @@ cin_isterminated ( /// lines here. /// @param[in] first_lnum Where to start looking. /// @param[in] min_lnum The line before which we will not be looking. -static int cin_isfuncdecl(char_u **sp, linenr_T first_lnum, linenr_T min_lnum) +static int cin_isfuncdecl(const char_u **sp, linenr_T first_lnum, linenr_T min_lnum) { - char_u *s; + const char_u *s; linenr_T lnum = first_lnum; linenr_T save_lnum = curwin->w_cursor.lnum; int retval = false; @@ -923,11 +930,10 @@ static int cin_isfuncdecl(char_u **sp, linenr_T first_lnum, linenr_T min_lnum) while (*s && *s != ';' && *s != '\'' && *s != '"') { if (*s == ')' && cin_nocode(s + 1)) { - /* ')' at the end: may have found a match - * Check for he previous line not to end in a backslash: - * #if defined(x) && \ - * defined(y) - */ + // ')' at the end: may have found a match + // Check for the previous line not to end in a backslash: + // #if defined(x) && {backslash} + // defined(y) lnum = first_lnum - 1; s = ml_get(lnum); if (*s == NUL || s[STRLEN(s) - 1] != '\\') @@ -971,12 +977,12 @@ done: return retval; } -static int cin_isif(char_u *p) +static int cin_isif(const char_u *p) { return STRNCMP(p, "if", 2) == 0 && !vim_isIDc(p[2]); } -static int cin_iselse(char_u *p) +static int cin_iselse(const char_u *p) { if (*p == '}') { // accept "} else" p = cin_skipcomment(p + 1); @@ -984,7 +990,7 @@ static int cin_iselse(char_u *p) return STRNCMP(p, "else", 4) == 0 && !vim_isIDc(p[4]); } -static int cin_isdo(char_u *p) +static int cin_isdo(const char_u *p) { return STRNCMP(p, "do", 2) == 0 && !vim_isIDc(p[2]); } @@ -994,7 +1000,7 @@ static int cin_isdo(char_u *p) * We only accept a "while (condition) ;", with only white space between the * ')' and ';'. The condition may be spread over several lines. */ -static int cin_iswhileofdo(char_u *p, linenr_T lnum) // XXX +static int cin_iswhileofdo(const char_u *p, linenr_T lnum) // XXX { pos_T cursor_save; pos_T *trypos; @@ -1028,7 +1034,7 @@ static int cin_iswhileofdo(char_u *p, linenr_T lnum) // XXX * Otherwise return !0 and update "*poffset" to point to the place where the * string was found. */ -static int cin_is_if_for_while_before_offset(char_u *line, int *poffset) +static int cin_is_if_for_while_before_offset(const char_u *line, int *poffset) { int offset = *poffset; @@ -1072,10 +1078,10 @@ probablyFound: */ static int cin_iswhileofdo_end(int terminated) { - char_u *line; - char_u *p; - char_u *s; - pos_T *trypos; + const char_u *line; + const char_u *p; + const char_u *s; + pos_T *trypos; int i; if (terminated != ';') { // there must be a ';' at the end @@ -1115,7 +1121,7 @@ static int cin_iswhileofdo_end(int terminated) return FALSE; } -static int cin_isbreak(char_u *p) +static int cin_isbreak(const char_u *p) { return STRNCMP(p, "break", 5) == 0 && !vim_isIDc(p[5]); } @@ -1135,10 +1141,10 @@ static int cin_isbreak(char_u *p) */ static int cin_is_cpp_baseclass(cpp_baseclass_cache_T *cached) { lpos_T *pos = &cached->lpos; // find position - char_u *s; + const char_u *s; int class_or_struct, lookfor_ctor_init, cpp_base_class; linenr_T lnum = curwin->w_cursor.lnum; - char_u *line = get_cursor_line_ptr(); + const char_u *line = get_cursor_line_ptr(); if (pos->lnum <= lnum) { return cached->found; // Use the cached result @@ -1306,10 +1312,10 @@ static int get_baseclass_amount(int col) * white space and comments. Skip strings and comments. * Ignore "ignore" after "find" if it's not NULL. */ -static int cin_ends_in(char_u *s, char_u *find, char_u *ignore) +static int cin_ends_in(const char_u *s, const char_u *find, const char_u *ignore) { - char_u *p = s; - char_u *r; + const char_u *p = s; + const char_u *r; int len = (int)STRLEN(find); while (*p != NUL) { @@ -1330,7 +1336,7 @@ static int cin_ends_in(char_u *s, char_u *find, char_u *ignore) /* * Return TRUE when "s" starts with "word" and then a non-ID character. */ -static int cin_starts_with(char_u *s, char *word) +static int cin_starts_with(const char_u *s, const char *word) { int l = (int)STRLEN(word); @@ -1338,10 +1344,10 @@ static int cin_starts_with(char_u *s, char *word) } /// Recognize a `extern "C"` or `extern "C++"` linkage specifications. -static int cin_is_cpp_extern_c(char_u *s) +static int cin_is_cpp_extern_c(const char_u *s) { - char_u *p; - int has_string_literal = false; + const char_u *p; + int has_string_literal = false; s = cin_skipcomment(s); if (STRNCMP(s, "extern", 6) == 0 && (s[6] == NUL || !vim_iswordc(s[6]))) { @@ -1380,9 +1386,9 @@ static int cin_is_cpp_extern_c(char_u *s) */ static int cin_skip2pos(pos_T *trypos) { - char_u *line; - char_u *p; - char_u *new_p; + const char_u *line; + const char_u *p; + const char_u *new_p; p = line = ml_get(trypos->lnum); while (*p && (colnr_T)(p - line) < trypos->col) { @@ -1412,8 +1418,8 @@ static int cin_skip2pos(pos_T *trypos) static pos_T *find_start_brace(void) // XXX { pos_T cursor_save; - pos_T *trypos; - pos_T *pos; + pos_T *trypos; + pos_T *pos; static pos_T pos_copy; cursor_save = curwin->w_cursor; @@ -1428,7 +1434,7 @@ static pos_T *find_start_brace(void) // XXX break; } if (pos != NULL) { - curwin->w_cursor.lnum = pos->lnum; + curwin->w_cursor = *pos; } } curwin->w_cursor = cursor_save; @@ -1525,7 +1531,7 @@ static int corr_ind_maxparen(pos_T *startpos) * Set w_cursor.col to the column number of the last unmatched ')' or '{' in * line "l". "l" must point to the start of the line. */ -static int find_last_paren(char_u *l, int start, int end) +static int find_last_paren(const char_u *l, int start, int end) { int i; int retval = FALSE; @@ -1634,8 +1640,8 @@ void parse_cino(buf_T *buf) * itself is also unclosed. */ buf->b_ind_unclosed2 = sw; - /* Suppress ignoring spaces from the indent of a line starting with an - * unclosed parentheses. */ + // Suppress ignoring spaces from the indent of a line starting with an + // unclosed parenthesis. buf->b_ind_unclosed_noignore = 0; /* If the opening paren is the last nonwhite character on the line, and @@ -1647,11 +1653,11 @@ void parse_cino(buf_T *buf) * an unclosed parentheses. */ buf->b_ind_unclosed_whiteok = 0; - /* Indent a closing parentheses under the line start of the matching - * opening parentheses. */ + // Indent a closing parenthesis under the line start of the matching + // opening parenthesis. buf->b_ind_matching_paren = 0; - // Indent a closing parentheses under the previous line. + // Indent a closing parenthesis under the previous line. buf->b_ind_paren_prev = 0; // Extra indent for comments. @@ -1797,8 +1803,8 @@ int get_c_indent(void) #define BRACE_AT_START 2 // '{' is at start of line #define BRACE_AT_END 3 // '{' is at end of line linenr_T ourscope; - char_u *l; - char_u *look; + const char_u *l; + const char_u *look; char_u terminated; int lookfor; #define LOOKFOR_INITIAL 0 @@ -1902,12 +1908,25 @@ int get_c_indent(void) * If we're inside a "//" comment and there is a "//" comment in a * previous line, lineup with that one. */ - if (cin_islinecomment(theline) - && (trypos = find_line_comment()) != NULL) { // XXX - // find how indented the line beginning the comment is - getvcol(curwin, trypos, &col, NULL, NULL); - amount = col; - goto theend; + if (cin_islinecomment(theline)) { + pos_T linecomment_pos; + + trypos = find_line_comment(); // XXX + if (trypos == NULL && curwin->w_cursor.lnum > 1) { + // There may be a statement before the comment, search from the end + // of the line for a comment start. + linecomment_pos.col = check_linecomment(ml_get(curwin->w_cursor.lnum - 1)); + if (linecomment_pos.col != MAXCOL) { + trypos = &linecomment_pos; + trypos->lnum = curwin->w_cursor.lnum - 1; + } + } + if (trypos != NULL) { + // find how indented the line beginning the comment is + getvcol(curwin, trypos, &col, NULL, NULL); + amount = col; + goto theend; + } } /* * If we're inside a comment and not looking at the start of the @@ -3598,9 +3617,9 @@ laterend: static int find_match(int lookfor, linenr_T ourscope) { - char_u *look; - pos_T *theirscope; - char_u *mightbeif; + const char_u *look; + pos_T *theirscope; + const char_u *mightbeif; int elselevel; int whilelevel; diff --git a/src/nvim/input.c b/src/nvim/input.c index 2f7c5c2c16..5fa9b8b343 100644 --- a/src/nvim/input.c +++ b/src/nvim/input.c @@ -105,7 +105,7 @@ int get_keystroke(MultiQueue *events) // terminal code to complete. n = os_inchar(buf + len, maxlen, len == 0 ? -1L : 100L, 0, events); if (n > 0) { - // Replace zero and CSI by a special key code. + // Replace zero and K_SPECIAL by a special key code. n = fix_input_buffer(buf + len, n); len += n; waited = 0; diff --git a/src/nvim/keymap.c b/src/nvim/keymap.c index abf016b832..32f2158d7b 100644 --- a/src/nvim/keymap.c +++ b/src/nvim/keymap.c @@ -158,7 +158,6 @@ static const struct key_name_entry { { ESC, "Esc" }, { ESC, "Escape" }, // Alternative name { CSI, "CSI" }, - { K_CSI, "xCSI" }, { '|', "Bar" }, { '\\', "Bslash" }, { K_DEL, "Del" }, @@ -964,7 +963,6 @@ char_u *replace_termcodes(const char_u *from, const size_t from_len, char_u **bu for (i = utfc_ptr2len_len(src, (int)(end - src) + 1); i > 0; i--) { // If the character is K_SPECIAL, replace it with K_SPECIAL // KS_SPECIAL KE_FILLER. - // If compiled with the GUI replace CSI with K_CSI. if (*src == K_SPECIAL) { result[dlen++] = K_SPECIAL; result[dlen++] = KS_SPECIAL; diff --git a/src/nvim/keymap.h b/src/nvim/keymap.h index 5ff5a38614..ae2ec7835e 100644 --- a/src/nvim/keymap.h +++ b/src/nvim/keymap.h @@ -122,8 +122,6 @@ // // Entries must be in the range 0x02-0x7f (see comment at K_SPECIAL). enum key_extra { - KE_NAME = 3, // name of this terminal entry - KE_S_UP = 4, // shift-up KE_S_DOWN = 5, // shift-down @@ -220,7 +218,7 @@ enum key_extra { KE_KINS = 79, // keypad Insert key KE_KDEL = 80, // keypad Delete key - KE_CSI = 81, // CSI typed directly + // KE_CSI = 81, // Nvim doesn't need escaping CSI KE_SNR = 82, // <SNR> KE_PLUG = 83, // <Plug> KE_CMDWIN = 84, // open command-line window from Command-line Mode @@ -245,6 +243,7 @@ enum key_extra { KE_MOUSEMOVE = 100, // mouse moved with no button down // , KE_CANCEL = 101 // return from vgetc KE_EVENT = 102, // event + KE_LUA = 103, // lua special key KE_COMMAND = 104, // <Cmd> special key }; @@ -434,7 +433,6 @@ enum key_extra { #define K_MOUSELEFT TERMCAP2KEY(KS_EXTRA, KE_MOUSELEFT) #define K_MOUSERIGHT TERMCAP2KEY(KS_EXTRA, KE_MOUSERIGHT) -#define K_CSI TERMCAP2KEY(KS_EXTRA, KE_CSI) #define K_SNR TERMCAP2KEY(KS_EXTRA, KE_SNR) #define K_PLUG TERMCAP2KEY(KS_EXTRA, KE_PLUG) #define K_CMDWIN TERMCAP2KEY(KS_EXTRA, KE_CMDWIN) @@ -443,6 +441,7 @@ enum key_extra { #define K_EVENT TERMCAP2KEY(KS_EXTRA, KE_EVENT) #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) +#define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA) // Bits for modifier mask // 0x01 cannot be used, because the modifier must be 0x02 or higher diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c index b6792a5a97..2b54e56df1 100644 --- a/src/nvim/lua/converter.c +++ b/src/nvim/lua/converter.c @@ -156,7 +156,7 @@ static LuaTableProps nlua_traverse_table(lua_State *const lstate) && ret.string_keys_num == 0)) { ret.type = kObjectTypeArray; if (tsize == 0 && lua_getmetatable(lstate, -1)) { - nlua_pushref(lstate, nlua_empty_dict_ref); + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); if (lua_rawequal(lstate, -2, -1)) { ret.type = kObjectTypeDictionary; } @@ -316,7 +316,7 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) LuaRef table_ref = LUA_NOREF; if (lua_getmetatable(lstate, -1)) { lua_pop(lstate, 1); - table_ref = nlua_ref(lstate, -1); + table_ref = nlua_ref_global(lstate, -1); } const LuaTableProps table_props = nlua_traverse_table(lstate); @@ -389,7 +389,7 @@ nlua_pop_typval_table_processing_end: } case LUA_TFUNCTION: { LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); - state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.func_ref = nlua_ref_global(lstate, -1); char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, @@ -401,7 +401,7 @@ nlua_pop_typval_table_processing_end: } case LUA_TUSERDATA: { // TODO(bfredl): check mt.__call and convert to function? - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); bool is_nil = lua_rawequal(lstate, -2, -1); lua_pop(lstate, 1); if (is_nil) { @@ -445,7 +445,7 @@ static bool typval_conv_special = false; if (typval_conv_special) { \ lua_pushnil(lstate); \ } else { \ - nlua_pushref(lstate, nlua_nil_ref); \ + nlua_pushref(lstate, nlua_global_refs->nil_ref); \ } \ } while (0) @@ -495,7 +495,7 @@ static bool typval_conv_special = false; nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary); \ } else { \ lua_createtable(lstate, 0, 0); \ - nlua_pushref(lstate, nlua_empty_dict_ref); \ + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); \ lua_setmetatable(lstate, -2); \ } \ } while (0) @@ -617,6 +617,14 @@ bool nlua_push_typval(lua_State *lstate, typval_T *const tv, bool special) semsg(_("E1502: Lua failed to grow stack to %i"), initial_size + 4); return false; } + if (tv->v_type == VAR_FUNC) { + ufunc_T *fp = find_func(tv->vval.v_string); + assert(fp != NULL); + if (fp->uf_cb == nlua_CFunction_func_call) { + nlua_pushref(lstate, ((LuaCFunctionState *)fp->uf_cb_state)->lua_callable.func_ref); + return true; + } + } if (encode_vim_to_lua(lstate, tv, "nlua_push_typval argument") == FAIL) { return false; } @@ -726,7 +734,7 @@ void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict, bool special } else { lua_createtable(lstate, 0, (int)dict.size); if (dict.size == 0 && !special) { - nlua_pushref(lstate, nlua_empty_dict_ref); + nlua_pushref(lstate, nlua_global_refs->empty_dict_ref); lua_setmetatable(lstate, -2); } } @@ -774,7 +782,7 @@ void nlua_push_Object(lua_State *lstate, const Object obj, bool special) if (special) { lua_pushnil(lstate); } else { - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); } break; case kObjectTypeLuaRef: { @@ -1191,14 +1199,14 @@ Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err) case LUA_TFUNCTION: if (ref) { - *cur.obj = LUAREF_OBJ(nlua_ref(lstate, -1)); + *cur.obj = LUAREF_OBJ(nlua_ref_global(lstate, -1)); } else { goto type_error; } break; case LUA_TUSERDATA: { - nlua_pushref(lstate, nlua_nil_ref); + nlua_pushref(lstate, nlua_global_refs->nil_ref); bool is_nil = lua_rawequal(lstate, -2, -1); lua_pop(lstate, 1); if (is_nil) { @@ -1232,7 +1240,7 @@ type_error: LuaRef nlua_pop_LuaRef(lua_State *const lstate, Error *err) { - LuaRef rv = nlua_ref(lstate, -1); + LuaRef rv = nlua_ref_global(lstate, -1); lua_pop(lstate, 1); return rv; } @@ -1242,7 +1250,12 @@ LuaRef nlua_pop_LuaRef(lua_State *const lstate, Error *err) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT \ { \ type ret; \ - ret = (type)lua_tonumber(lstate, -1); \ + if (lua_type(lstate, -1) != LUA_TNUMBER) { \ + api_set_error(err, kErrorTypeValidation, "Expected Lua number"); \ + ret = (type)-1; \ + } else { \ + ret = (type)lua_tonumber(lstate, -1); \ + } \ lua_pop(lstate, 1); \ return ret; \ } diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 107ff22913..7ac80f01f0 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -4,6 +4,7 @@ #include <lauxlib.h> #include <lua.h> #include <lualib.h> +#include <tree_sitter/api.h> #include "luv/luv.h" #include "nvim/api/private/defs.h" @@ -15,9 +16,11 @@ #include "nvim/change.h" #include "nvim/cursor.h" #include "nvim/eval/userfunc.h" +#include "nvim/eval/typval.h" #include "nvim/event/loop.h" #include "nvim/event/time.h" #include "nvim/ex_cmds2.h" +#include "nvim/ex_docmd.h" #include "nvim/ex_getln.h" #include "nvim/extmark.h" #include "nvim/func_attr.h" @@ -43,6 +46,8 @@ static int in_fast_callback = 0; // Initialized in nlua_init(). static lua_State *global_lstate = NULL; +static uv_thread_t main_thread; + typedef struct { Error err; String lua_err_str; @@ -63,11 +68,16 @@ typedef struct { } #if __has_feature(address_sanitizer) -static PMap(handle_T) nlua_ref_markers = MAP_INIT; static bool nlua_track_refs = false; # define NLUA_TRACK_REFS #endif +typedef enum luv_err_type { + kCallback, + kThread, + kThreadCallback, +} luv_err_t; + /// Convert lua error into a Vim error message /// /// @param lstate Lua interpreter state. @@ -120,8 +130,21 @@ static int nlua_nvim_version(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL static void nlua_luv_error_event(void **argv) { char *error = (char *)argv[0]; + luv_err_t type = (luv_err_t)(intptr_t)argv[1]; msg_ext_set_kind("lua_error"); - semsg_multiline("Error executing luv callback:\n%s", error); + switch (type) { + case kCallback: + semsg_multiline("Error executing luv callback:\n%s", error); + break; + case kThread: + semsg_multiline("Error in luv thread:\n%s", error); + break; + case kThreadCallback: + semsg_multiline("Error in luv callback, thread:\n%s", error); + break; + default: + break; + } xfree(error); } @@ -146,7 +169,7 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags const char *error = lua_tostring(lstate, -1); multiqueue_put(main_loop.events, nlua_luv_error_event, - 1, xstrdup(error)); + 2, xstrdup(error), (intptr_t)kCallback); lua_pop(lstate, 1); // error message retval = -status; } else { // LUA_OK @@ -160,12 +183,112 @@ static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags return retval; } +static int nlua_luv_thread_cb_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags) +{ + return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, true); +} + +static int nlua_luv_thread_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags) + FUNC_ATTR_NONNULL_ALL +{ + return nlua_luv_thread_common_cfpcall(lstate, nargs, nresult, flags, false); +} + +static int nlua_luv_thread_cfcpcall(lua_State *lstate, lua_CFunction func, + void *ud, int flags) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + lua_pushcfunction(lstate, func); + lua_pushlightuserdata(lstate, ud); + int retval = nlua_luv_thread_cfpcall(lstate, 1, 0, flags); + return retval; +} + +static int nlua_luv_thread_common_cfpcall(lua_State *lstate, int nargs, int nresult, + int flags, bool is_callback) + FUNC_ATTR_NONNULL_ALL +{ + int retval; + + int top = lua_gettop(lstate); + int status = lua_pcall(lstate, nargs, nresult, 0); + if (status) { + if (status == LUA_ERRMEM && !(flags & LUVF_CALLBACK_NOEXIT)) { + // Terminate this thread, as the main thread may be able to continue + // execution. + mch_errmsg(e_outofmem); + mch_errmsg("\n"); + lua_close(lstate); +#ifdef WIN32 + ExitThread(0); +#else + pthread_exit(0); +#endif + } + const char *error = lua_tostring(lstate, -1); + + loop_schedule_deferred(&main_loop, + event_create(nlua_luv_error_event, 2, + xstrdup(error), + is_callback + ? (intptr_t)kThreadCallback + : (intptr_t)kThread)); + lua_pop(lstate, 1); // error message + retval = -status; + } else { // LUA_OK + if (nresult == LUA_MULTRET) { + nresult = lua_gettop(lstate) - top + nargs + 1; + } + retval = nresult; + } + + return retval; +} + +static int nlua_thr_api_nvim__get_runtime(lua_State *lstate) +{ + if (lua_gettop(lstate) != 3) { + return luaL_error(lstate, "Expected 3 arguments"); + } + + luaL_checktype(lstate, -1, LUA_TTABLE); + lua_getfield(lstate, -1, "is_lua"); + if (!lua_isboolean(lstate, -1)) { + return luaL_error(lstate, "is_lua is not a boolean"); + } + bool is_lua = lua_toboolean(lstate, -1); + lua_pop(lstate, 2); + + luaL_checktype(lstate, -1, LUA_TBOOLEAN); + bool all = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + + Error err = ERROR_INIT; + const Array pat = nlua_pop_Array(lstate, &err); + if (ERROR_SET(&err)) { + luaL_where(lstate, 1); + lua_pushstring(lstate, err.msg); + api_clear_error(&err); + lua_concat(lstate, 2); + return lua_error(lstate); + } + + ArrayOf(String) ret = runtime_get_named_thread(is_lua, pat, all); + nlua_push_Array(lstate, ret, true); + api_free_array(ret); + api_free_array(pat); + + return 1; +} + static void nlua_schedule_event(void **argv) { LuaRef cb = (LuaRef)(ptrdiff_t)argv[0]; lua_State *const lstate = global_lstate; nlua_pushref(lstate, cb); - nlua_unref(lstate, cb); + nlua_unref_global(lstate, cb); if (nlua_pcall(lstate, 0, 0)) { nlua_error(lstate, _("Error executing vim.schedule lua callback: %.*s")); } @@ -182,7 +305,7 @@ static int nlua_schedule(lua_State *const lstate) return lua_error(lstate); } - LuaRef cb = nlua_ref(lstate, 1); + LuaRef cb = nlua_ref_global(lstate, 1); multiqueue_put(main_loop.events, nlua_schedule_event, 1, (void *)(ptrdiff_t)cb); @@ -300,6 +423,152 @@ static int nlua_wait(lua_State *lstate) return 2; } +static nlua_ref_state_t *nlua_new_ref_state(lua_State *lstate, bool is_thread) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = lua_newuserdata(lstate, sizeof(*ref_state)); + memset(ref_state, 0, sizeof(*ref_state)); + ref_state->nil_ref = LUA_NOREF; + ref_state->empty_dict_ref = LUA_NOREF; + if (!is_thread) { + nlua_global_refs = ref_state; + } + return ref_state; +} + +static nlua_ref_state_t *nlua_get_ref_state(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_getfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state"); + nlua_ref_state_t *ref_state = lua_touserdata(lstate, -1); + lua_pop(lstate, 1); + + return ref_state; +} + +LuaRef nlua_get_nil_ref(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + return ref_state->nil_ref; +} + +LuaRef nlua_get_empty_dict_ref(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + return ref_state->empty_dict_ref; +} + +int nlua_get_global_ref_count(void) +{ + return nlua_global_refs->ref_count; +} + +static void nlua_common_vim_init(lua_State *lstate, bool is_thread) + FUNC_ATTR_NONNULL_ARG(1) +{ + nlua_ref_state_t *ref_state = nlua_new_ref_state(lstate, is_thread); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.ref_state"); + + // vim.is_thread + lua_pushboolean(lstate, is_thread); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + lua_pushcfunction(lstate, &nlua_is_thread); + lua_setfield(lstate, -2, "is_thread"); + + // vim.NIL + lua_newuserdata(lstate, 0); + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, &nlua_nil_tostring); + lua_setfield(lstate, -2, "__tostring"); + lua_setmetatable(lstate, -2); + ref_state->nil_ref = nlua_ref(lstate, ref_state, -1); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL"); + lua_setfield(lstate, -2, "NIL"); + + // vim._empty_dict_mt + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, &nlua_empty_dict_tostring); + lua_setfield(lstate, -2, "__tostring"); + ref_state->empty_dict_ref = nlua_ref(lstate, ref_state, -1); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict"); + lua_setfield(lstate, -2, "_empty_dict_mt"); + + // vim.loop + if (is_thread) { + luv_set_callback(lstate, nlua_luv_thread_cb_cfpcall); + luv_set_thread(lstate, nlua_luv_thread_cfpcall); + luv_set_cthread(lstate, nlua_luv_thread_cfcpcall); + } else { + luv_set_loop(lstate, &main_loop.uv); + luv_set_callback(lstate, nlua_luv_cfpcall); + } + luaopen_luv(lstate); + lua_pushvalue(lstate, -1); + lua_setfield(lstate, -3, "loop"); + + // package.loaded.luv = vim.loop + // otherwise luv will be reinitialized when require'luv' + lua_getglobal(lstate, "package"); + lua_getfield(lstate, -1, "loaded"); + lua_pushvalue(lstate, -3); + lua_setfield(lstate, -2, "luv"); + lua_pop(lstate, 3); +} + +static void nlua_common_package_init(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + { + const char *code = (char *)&shared_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua") + || nlua_pcall(lstate, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n")); + return; + } + } + + { + const char *code = (char *)&lua_load_package_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_load_package_module) - 1, "@vim/_load_package.lua") + || lua_pcall(lstate, 0, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating _load_package module: %.*s")); + return; + } + } + + { + lua_getglobal(lstate, "package"); // [package] + lua_getfield(lstate, -1, "loaded"); // [package, loaded] + + const char *code = (char *)&inspect_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua") + || nlua_pcall(lstate, 0, 1)) { + nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n")); + return; + } + + // [package, loaded, inspect] + lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded] + } + + { + const char *code = (char *)&lua_F_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua") + || nlua_pcall(lstate, 0, 1)) { + nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n")); + return; + } + // [package, loaded, module] + lua_setfield(lstate, -2, "vim.F"); // [package, loaded] + + lua_pop(lstate, 2); // [] + } +} + /// Initialize lua interpreter state /// /// Called by lua interpreter itself to initialize state. @@ -360,78 +629,38 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_pushcfunction(lstate, &nlua_wait); lua_setfield(lstate, -2, "wait"); - // vim.NIL - lua_newuserdata(lstate, 0); - lua_createtable(lstate, 0, 0); - lua_pushcfunction(lstate, &nlua_nil_tostring); - lua_setfield(lstate, -2, "__tostring"); - lua_setmetatable(lstate, -2); - nlua_nil_ref = nlua_ref(lstate, -1); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.NIL"); - lua_setfield(lstate, -2, "NIL"); - - // vim._empty_dict_mt - lua_createtable(lstate, 0, 0); - lua_pushcfunction(lstate, &nlua_empty_dict_tostring); - lua_setfield(lstate, -2, "__tostring"); - nlua_empty_dict_ref = nlua_ref(lstate, -1); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, LUA_REGISTRYINDEX, "mpack.empty_dict"); - lua_setfield(lstate, -2, "_empty_dict_mt"); + nlua_common_vim_init(lstate, false); // internal vim._treesitter... API nlua_add_treesitter(lstate); - // vim.loop - luv_set_loop(lstate, &main_loop.uv); - luv_set_callback(lstate, nlua_luv_cfpcall); - luaopen_luv(lstate); - lua_pushvalue(lstate, -1); - lua_setfield(lstate, -3, "loop"); - - // package.loaded.luv = vim.loop - // otherwise luv will be reinitialized when require'luv' - lua_getglobal(lstate, "package"); - lua_getfield(lstate, -1, "loaded"); - lua_pushvalue(lstate, -3); - lua_setfield(lstate, -2, "luv"); - lua_pop(lstate, 3); - - nlua_state_add_stdlib(lstate); + nlua_state_add_stdlib(lstate, false); lua_setglobal(lstate, "vim"); - { - const char *code = (char *)&shared_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(shared_module) - 1, "@vim/shared.lua") - || nlua_pcall(lstate, 0, 0)) { - nlua_error(lstate, _("E5106: Error while creating shared module: %.*s\n")); - return 1; - } - } + nlua_common_package_init(lstate); { lua_getglobal(lstate, "package"); // [package] lua_getfield(lstate, -1, "loaded"); // [package, loaded] - const char *code = (char *)&inspect_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(inspect_module) - 1, "@vim/inspect.lua") + char *code = (char *)&lua_filetype_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_filetype_module) - 1, "@vim/filetype.lua") || nlua_pcall(lstate, 0, 1)) { - nlua_error(lstate, _("E5106: Error while creating inspect module: %.*s\n")); + nlua_error(lstate, _("E5106: Error while creating vim.filetype module: %.*s")); return 1; } - // [package, loaded, inspect] - lua_setfield(lstate, -2, "vim.inspect"); // [package, loaded] + // [package, loaded, module] + lua_setfield(lstate, -2, "vim.filetype"); // [package, loaded] - code = (char *)&lua_F_module[0]; - if (luaL_loadbuffer(lstate, code, sizeof(lua_F_module) - 1, "@vim/F.lua") + code = (char *)&lua_keymap_module[0]; + if (luaL_loadbuffer(lstate, code, sizeof(lua_keymap_module) - 1, "@vim/keymap.lua") || nlua_pcall(lstate, 0, 1)) { - nlua_error(lstate, _("E5106: Error while creating vim.F module: %.*s\n")); + nlua_error(lstate, _("E5106: Error while creating vim.keymap module: %.*s")); return 1; } // [package, loaded, module] - lua_setfield(lstate, -2, "vim.F"); // [package, loaded] + lua_setfield(lstate, -2, "vim.keymap"); // [package, loaded] lua_pop(lstate, 2); // [] } @@ -484,9 +713,60 @@ void nlua_init(void) luaL_openlibs(lstate); nlua_state_init(lstate); + luv_set_thread_cb(nlua_thread_acquire_vm, nlua_common_free_all_mem); + global_lstate = lstate; + + main_thread = uv_thread_self(); } +static lua_State *nlua_thread_acquire_vm(void) +{ + // If it is called from the main thread, it will attempt to rebuild the cache. + const uv_thread_t self = uv_thread_self(); + if (uv_thread_equal(&main_thread, &self)) { + runtime_search_path_validate(); + } + + lua_State *lstate = luaL_newstate(); + + // Add in the lua standard libraries + luaL_openlibs(lstate); + + // print + lua_pushcfunction(lstate, &nlua_print); + lua_setglobal(lstate, "print"); + + lua_pushinteger(lstate, 0); + lua_setfield(lstate, LUA_REGISTRYINDEX, "nlua.refcount"); + + // vim + lua_newtable(lstate); + + nlua_common_vim_init(lstate, true); + + nlua_state_add_stdlib(lstate, true); + + lua_setglobal(lstate, "vim"); + + nlua_common_package_init(lstate); + + lua_getglobal(lstate, "vim"); + lua_getglobal(lstate, "package"); + lua_getfield(lstate, -1, "loaded"); + lua_getfield(lstate, -1, "vim.inspect"); + lua_setfield(lstate, -4, "inspect"); + lua_pop(lstate, 3); + + lua_getglobal(lstate, "vim"); + lua_createtable(lstate, 0, 0); + lua_pushcfunction(lstate, nlua_thr_api_nvim__get_runtime); + lua_setfield(lstate, -2, "nvim__get_runtime"); + lua_setfield(lstate, -2, "api"); + lua_pop(lstate, 1); + + return lstate; +} void nlua_free_all_mem(void) { @@ -494,26 +774,30 @@ void nlua_free_all_mem(void) return; } lua_State *lstate = global_lstate; + nlua_common_free_all_mem(lstate); +} - nlua_unref(lstate, nlua_nil_ref); - nlua_unref(lstate, nlua_empty_dict_ref); +static void nlua_common_free_all_mem(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + nlua_ref_state_t *ref_state = nlua_get_ref_state(lstate); + nlua_unref(lstate, ref_state, ref_state->nil_ref); + nlua_unref(lstate, ref_state, ref_state->empty_dict_ref); #ifdef NLUA_TRACK_REFS - if (nlua_refcount) { - fprintf(stderr, "%d lua references were leaked!", nlua_refcount); + if (ref_state->ref_count) { + fprintf(stderr, "%d lua references were leaked!", ref_state->ref_count); } if (nlua_track_refs) { // in case there are leaked luarefs, leak the associated memory // to get LeakSanitizer stacktraces on exit - pmap_destroy(handle_T)(&nlua_ref_markers); + pmap_destroy(handle_T)(&ref_state->ref_markers); } #endif - nlua_refcount = 0; lua_close(lstate); } - static void nlua_print_event(void **argv) { char *str = argv[0]; @@ -591,9 +875,18 @@ static int nlua_print(lua_State *const lstate) #undef PRINT_ERROR ga_append(&msg_ga, NUL); - if (in_fast_callback) { + lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + bool is_thread = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + + if (is_thread) { + loop_schedule_deferred(&main_loop, + event_create(nlua_print_event, 2, + msg_ga.ga_data, + (intptr_t)msg_ga.ga_len)); + } else if (in_fast_callback) { multiqueue_put(main_loop.events, nlua_print_event, - 2, msg_ga.ga_data, msg_ga.ga_len); + 2, msg_ga.ga_data, (intptr_t)msg_ga.ga_len); } else { nlua_print_event((void *[]){ msg_ga.ga_data, (void *)(intptr_t)msg_ga.ga_len }); @@ -602,10 +895,12 @@ static int nlua_print(lua_State *const lstate) nlua_print_error: ga_clear(&msg_ga); + char *buff = xmalloc(IOSIZE); const char *fmt = _("E5114: Error while converting print argument #%i: %.*s"); - size_t len = (size_t)vim_snprintf((char *)IObuff, IOSIZE, fmt, curargidx, + size_t len = (size_t)vim_snprintf(buff, IOSIZE, fmt, curargidx, (int)errmsg_len, errmsg); - lua_pushlstring(lstate, (char *)IObuff, len); + lua_pushlstring(lstate, buff, len); + xfree(buff); return lua_error(lstate); } @@ -671,7 +966,7 @@ int nlua_call(lua_State *lstate) typval_T vim_args[MAX_FUNC_ARGS + 1]; int i = 0; // also used for freeing the variables for (; i < nargs; i++) { - lua_pushvalue(lstate, (int)i+2); + lua_pushvalue(lstate, i+2); if (!nlua_pop_typval(lstate, &vim_args[i])) { api_set_error(&err, kErrorTypeException, "error converting argument %d", i+1); @@ -736,7 +1031,7 @@ static int nlua_rpc(lua_State *lstate, bool request) Array args = ARRAY_DICT_INIT; for (int i = 0; i < nargs; i++) { - lua_pushvalue(lstate, (int)i+3); + lua_pushvalue(lstate, i+3); ADD(args, nlua_pop_Object(lstate, false, &err)); if (ERROR_SET(&err)) { api_free_array(args); @@ -795,40 +1090,53 @@ static int nlua_getenv(lua_State *lstate) /// add the value to the registry -LuaRef nlua_ref(lua_State *lstate, int index) +/// The current implementation does not support calls from threads. +LuaRef nlua_ref(lua_State *lstate, nlua_ref_state_t *ref_state, int index) { lua_pushvalue(lstate, index); LuaRef ref = luaL_ref(lstate, LUA_REGISTRYINDEX); if (ref > 0) { - nlua_refcount++; + ref_state->ref_count++; #ifdef NLUA_TRACK_REFS if (nlua_track_refs) { // dummy allocation to make LeakSanitizer track our luarefs - pmap_put(handle_T)(&nlua_ref_markers, ref, xmalloc(3)); + pmap_put(handle_T)(&ref_state->ref_markers, ref, xmalloc(3)); } #endif } return ref; } + +LuaRef nlua_ref_global(lua_State *lstate, int index) +{ + return nlua_ref(lstate, nlua_global_refs, index); +} + /// remove the value from the registry -void nlua_unref(lua_State *lstate, LuaRef ref) +void nlua_unref(lua_State *lstate, nlua_ref_state_t *ref_state, LuaRef ref) { if (ref > 0) { - nlua_refcount--; + ref_state->ref_count--; #ifdef NLUA_TRACK_REFS // NB: don't remove entry from map to track double-unref if (nlua_track_refs) { - xfree(pmap_get(handle_T)(&nlua_ref_markers, ref)); + xfree(pmap_get(handle_T)(&ref_state->ref_markers, ref)); } #endif luaL_unref(lstate, LUA_REGISTRYINDEX, ref); } } +void nlua_unref_global(lua_State *lstate, LuaRef ref) +{ + nlua_unref(lstate, nlua_global_refs, ref); +} + + void api_free_luaref(LuaRef ref) { - nlua_unref(global_lstate, ref); + nlua_unref_global(global_lstate, ref); } /// push a value referenced in the registry @@ -850,7 +1158,7 @@ LuaRef api_new_luaref(LuaRef original_ref) lua_State *const lstate = global_lstate; nlua_pushref(lstate, original_ref); - LuaRef new_ref = nlua_ref(lstate, -1); + LuaRef new_ref = nlua_ref_global(lstate, -1); lua_pop(lstate, 1); return new_ref; } @@ -914,6 +1222,24 @@ void nlua_typval_call(const char *str, size_t len, typval_T *const args, int arg } } +void nlua_call_user_expand_func(expand_T *xp, typval_T *ret_tv) + FUNC_ATTR_NONNULL_ALL +{ + lua_State *const lstate = global_lstate; + + nlua_pushref(lstate, xp->xp_luaref); + lua_pushstring(lstate, (char *)xp->xp_pattern); + lua_pushstring(lstate, (char *)xp->xp_line); + lua_pushinteger(lstate, xp->xp_col); + + if (nlua_pcall(lstate, 3, 1)) { + nlua_error(lstate, _("E5108: Error executing Lua function: %.*s")); + return; + } + + nlua_pop_typval(lstate, ret_tv); +} + static void nlua_typval_exec(const char *lcmd, size_t lcmd_len, const char *name, typval_T *const args, int argcount, bool special, typval_T *ret_tv) { @@ -1034,6 +1360,19 @@ 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); + + // TODO(tjdevries): This should probably check for callable tables as well. + // We should put some work maybe into simplifying how all of that works + 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) @@ -1096,11 +1435,23 @@ void ex_lua(exarg_T *const eap) FUNC_ATTR_NONNULL_ALL { size_t len; - char *const code = script_get(eap, &len); + char *code = script_get(eap, &len); if (eap->skip) { xfree(code); return; } + // When =expr is used transform it to print(vim.inspect(expr)) + if (code[0] == '=') { + len += sizeof("vim.pretty_print()") - sizeof("="); + // code_buf needs to be 1 char larger then len for null byte in the end. + // lua nlua_typval_exec doesn't expect null terminated string so len + // needs to end before null byte. + char *code_buf = xmallocz(len); + vim_snprintf(code_buf, len+1, "vim.pretty_print(%s)", code+1); + xfree(code); + code = code_buf; + } + nlua_typval_exec(code, len, ":lua", NULL, 0, false, NULL); xfree(code); @@ -1200,6 +1551,9 @@ void ex_luafile(exarg_T *const eap) /// execute lua code from a file. /// +/// Note: we call the lua global loadfile as opposed to calling luaL_loadfile +/// in case loadfile has been overridden in the users environment. +/// /// @param path path of the file /// /// @return true if everything ok, false if there was an error (echoed) @@ -1208,11 +1562,30 @@ bool nlua_exec_file(const char *path) { lua_State *const lstate = global_lstate; - if (luaL_loadfile(lstate, path)) { + lua_getglobal(lstate, "loadfile"); + lua_pushstring(lstate, path); + + if (nlua_pcall(lstate, 1, 2)) { + nlua_error(lstate, _("E5111: Error calling lua: %.*s")); + return false; + } + + // loadstring() returns either: + // 1. nil, error + // 2. chunk, nil + + if (lua_isnil(lstate, -2)) { + // 1 nlua_error(lstate, _("E5112: Error while creating lua chunk: %.*s")); + assert(lua_isnil(lstate, -1)); + lua_pop(lstate, 1); return false; } + // 2 + assert(lua_isnil(lstate, -1)); + lua_pop(lstate, 1); + if (nlua_pcall(lstate, 0, 0)) { nlua_error(lstate, _("E5113: Error while calling lua chunk: %.*s")); return false; @@ -1227,6 +1600,12 @@ int tslua_get_language_version(lua_State *L) return 1; } +int tslua_get_minimum_language_version(lua_State *L) +{ + lua_pushnumber(L, TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION); + return 1; +} + static void nlua_add_treesitter(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL { tslua_init(lstate); @@ -1248,6 +1627,9 @@ static void nlua_add_treesitter(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_pushcfunction(lstate, tslua_get_language_version); lua_setfield(lstate, -2, "_ts_get_language_version"); + + lua_pushcfunction(lstate, tslua_get_minimum_language_version); + lua_setfield(lstate, -2, "_ts_get_minimum_language_version"); } int nlua_expand_pat(expand_T *xp, char_u *pat, int *num_results, char_u ***results) @@ -1319,6 +1701,13 @@ cleanup: return ret; } +static int nlua_is_thread(lua_State *lstate) +{ + lua_getfield(lstate, LUA_REGISTRYINDEX, "nvim.thread"); + + return 1; +} + // Required functions for lua c functions as VimL callbacks int nlua_CFunction_func_call(int argcount, typval_T *argvars, typval_T *rettv, void *state) @@ -1335,7 +1724,7 @@ void nlua_CFunction_func_free(void *state) lua_State *const lstate = global_lstate; LuaCFunctionState *funcstate = (LuaCFunctionState *)state; - nlua_unref(lstate, funcstate->lua_callable.func_ref); + nlua_unref_global(lstate, funcstate->lua_callable.func_ref); xfree(funcstate); } @@ -1385,7 +1774,7 @@ char_u *nlua_register_table_as_callable(typval_T *const arg) lua_pop(lstate, 2); // [table] LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); - state->lua_callable.func_ref = nlua_ref(lstate, -1); + state->lua_callable.func_ref = nlua_ref_global(lstate, -1); char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, state); @@ -1432,3 +1821,123 @@ void nlua_execute_on_key(int c) #endif } +// Sets the editor "script context" during Lua execution. Used by :verbose. +// @param[out] current +void nlua_set_sctx(sctx_T *current) +{ + if (p_verbose <= 0 || current->sc_sid != SID_LUA) { + return; + } + lua_State *const lstate = global_lstate; + lua_Debug *info = (lua_Debug *)xmalloc(sizeof(lua_Debug)); + + // Files where internal wrappers are defined so we can ignore them + // like vim.o/opt etc are defined in _meta.lua + char *ignorelist[] = { + "vim/_meta.lua", + "vim/keymap.lua", + }; + int ignorelist_size = sizeof(ignorelist) / sizeof(ignorelist[0]); + + for (int level = 1; true; level++) { + if (lua_getstack(lstate, level, info) != 1) { + goto cleanup; + } + if (lua_getinfo(lstate, "nSl", info) == 0) { + goto cleanup; + } + + bool is_ignored = false; + if (info->what[0] == 'C' || info->source[0] != '@') { + is_ignored = true; + } else { + for (int i = 0; i < ignorelist_size; i++) { + if (strncmp(ignorelist[i], info->source+1, strlen(ignorelist[i])) == 0) { + is_ignored = true; + break; + } + } + } + if (is_ignored) { + continue; + } + break; + } + char *source_path = fix_fname(info->source + 1); + get_current_script_id((char_u *)source_path, current); + xfree(source_path); + current->sc_lnum = info->currentline; + current->sc_seq = -1; + +cleanup: + xfree(info); +} + +void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap) +{ + lua_State *const lstate = global_lstate; + + nlua_pushref(lstate, cmd->uc_luaref); + + lua_newtable(lstate); + lua_pushboolean(lstate, eap->forceit == 1); + lua_setfield(lstate, -2, "bang"); + + lua_pushinteger(lstate, eap->line1); + lua_setfield(lstate, -2, "line1"); + + lua_pushinteger(lstate, eap->line2); + lua_setfield(lstate, -2, "line2"); + + lua_newtable(lstate); // f-args table + lua_pushstring(lstate, (const char *)eap->arg); + lua_pushvalue(lstate, -1); // Reference for potential use on f-args + lua_setfield(lstate, -4, "args"); + + // Split args by unescaped whitespace |<f-args>| (nargs dependent) + if (cmd->uc_argt & EX_NOSPC) { + // Commands where nargs = 1 or "?" fargs is the same as args + lua_rawseti(lstate, -2, 1); + } else { + // Commands with more than one possible argument we split + lua_pop(lstate, 1); // Pop the reference of opts.args + int length = (int)STRLEN(eap->arg); + int start = 0; + int end = 0; + int i = 1; + bool res = true; + while (res) { + res = uc_split_args_iter(eap->arg, i, &start, &end, length); + lua_pushlstring(lstate, (const char *)eap->arg + start, (size_t)(end - start + 1)); + lua_rawseti(lstate, -2, i); + i++; + } + } + lua_setfield(lstate, -2, "fargs"); + + lua_pushstring(lstate, (const char *)&eap->regname); + lua_setfield(lstate, -2, "reg"); + + lua_pushinteger(lstate, eap->addr_count); + lua_setfield(lstate, -2, "range"); + + if (eap->addr_count > 0) { + lua_pushinteger(lstate, eap->line2); + } else { + lua_pushinteger(lstate, cmd->uc_def); + } + lua_setfield(lstate, -2, "count"); + + // The size of this buffer is chosen empirically to be large enough to hold + // every possible modifier (with room to spare). If the list of possible + // modifiers grows this may need to be updated. + char buf[200] = { 0 }; + (void)uc_mods(buf); + lua_pushstring(lstate, buf); + lua_setfield(lstate, -2, "mods"); + + if (nlua_pcall(lstate, 1, 0)) { + nlua_error(lstate, _("Error executing Lua callback: %.*s")); + } +} + diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h index a1f66bd02b..47ac51dadb 100644 --- a/src/nvim/lua/executor.h +++ b/src/nvim/lua/executor.h @@ -4,27 +4,25 @@ #include <lauxlib.h> #include <lua.h> +#include "nvim/assert.h" #include "nvim/api/private/defs.h" #include "nvim/eval/typval.h" #include "nvim/ex_cmds_defs.h" +#include "nvim/ex_docmd.h" #include "nvim/func_attr.h" #include "nvim/lua/converter.h" // Generated by msgpack-gen.lua void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL; -EXTERN LuaRef nlua_nil_ref INIT(= LUA_NOREF); -EXTERN LuaRef nlua_empty_dict_ref INIT(= LUA_NOREF); - -EXTERN int nlua_refcount INIT(= 0); - -#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) +typedef struct { + LuaRef nil_ref; + LuaRef empty_dict_ref; + int ref_count; +#if __has_feature(address_sanitizer) + PMap(handle_T) ref_markers; +#endif +} nlua_ref_state_t; #define NLUA_CLEAR_REF(x) \ do { \ @@ -38,4 +36,7 @@ EXTERN int nlua_refcount INIT(= 0); #ifdef INCLUDE_GENERATED_DECLARATIONS # include "lua/executor.h.generated.h" #endif + +EXTERN nlua_ref_state_t *nlua_global_refs INIT(= NULL); + #endif // NVIM_LUA_EXECUTOR_H diff --git a/src/nvim/lua/spell.c b/src/nvim/lua/spell.c index b84124bc19..3a63f61200 100644 --- a/src/nvim/lua/spell.c +++ b/src/nvim/lua/spell.c @@ -1,3 +1,5 @@ +// 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 <lua.h> #include <lauxlib.h> diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index 18a579ed0f..c2ce899a74 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -25,6 +25,7 @@ #include "nvim/func_attr.h" #include "nvim/garray.h" #include "nvim/getchar.h" +#include "nvim/globals.h" #include "nvim/lua/converter.h" #include "nvim/lua/executor.h" #include "nvim/lua/stdlib.h" @@ -408,6 +409,12 @@ int nlua_getvar(lua_State *lstate) const char *name = luaL_checklstring(lstate, 3, &len); dictitem_T *di = tv_dict_find(dict, name, (ptrdiff_t)len); + if (di == NULL && dict == &globvardict) { // try to autoload script + if (!script_autoload(name, len, false) || aborting()) { + return 0; // nil + } + di = tv_dict_find(dict, name, (ptrdiff_t)len); + } if (di == NULL) { return 0; // nil } @@ -464,43 +471,52 @@ static int nlua_stricmp(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL } -void nlua_state_add_stdlib(lua_State *const lstate) +void nlua_state_add_stdlib(lua_State *const lstate, bool is_thread) { - // stricmp - lua_pushcfunction(lstate, &nlua_stricmp); - lua_setfield(lstate, -2, "stricmp"); - // str_utfindex - lua_pushcfunction(lstate, &nlua_str_utfindex); - lua_setfield(lstate, -2, "str_utfindex"); - // str_byteindex - lua_pushcfunction(lstate, &nlua_str_byteindex); - lua_setfield(lstate, -2, "str_byteindex"); - // str_utf_pos - lua_pushcfunction(lstate, &nlua_str_utf_pos); - lua_setfield(lstate, -2, "str_utf_pos"); - // str_utf_start - lua_pushcfunction(lstate, &nlua_str_utf_start); - lua_setfield(lstate, -2, "str_utf_start"); - // str_utf_end - lua_pushcfunction(lstate, &nlua_str_utf_end); - lua_setfield(lstate, -2, "str_utf_end"); - // regex - lua_pushcfunction(lstate, &nlua_regex); - lua_setfield(lstate, -2, "regex"); - luaL_newmetatable(lstate, "nvim_regex"); - luaL_register(lstate, NULL, regex_meta); - - lua_pushvalue(lstate, -1); // [meta, meta] - lua_setfield(lstate, -2, "__index"); // [meta] - lua_pop(lstate, 1); // don't use metatable now - - // _getvar - lua_pushcfunction(lstate, &nlua_getvar); - lua_setfield(lstate, -2, "_getvar"); - - // _setvar - lua_pushcfunction(lstate, &nlua_setvar); - lua_setfield(lstate, -2, "_setvar"); + if (!is_thread) { + // TODO(bfredl): some of basic string functions should already be + // (or be easy to make) threadsafe + + // stricmp + lua_pushcfunction(lstate, &nlua_stricmp); + lua_setfield(lstate, -2, "stricmp"); + // str_utfindex + lua_pushcfunction(lstate, &nlua_str_utfindex); + lua_setfield(lstate, -2, "str_utfindex"); + // str_byteindex + lua_pushcfunction(lstate, &nlua_str_byteindex); + lua_setfield(lstate, -2, "str_byteindex"); + // str_utf_pos + lua_pushcfunction(lstate, &nlua_str_utf_pos); + lua_setfield(lstate, -2, "str_utf_pos"); + // str_utf_start + lua_pushcfunction(lstate, &nlua_str_utf_start); + lua_setfield(lstate, -2, "str_utf_start"); + // str_utf_end + lua_pushcfunction(lstate, &nlua_str_utf_end); + lua_setfield(lstate, -2, "str_utf_end"); + // regex + lua_pushcfunction(lstate, &nlua_regex); + lua_setfield(lstate, -2, "regex"); + luaL_newmetatable(lstate, "nvim_regex"); + luaL_register(lstate, NULL, regex_meta); + + lua_pushvalue(lstate, -1); // [meta, meta] + lua_setfield(lstate, -2, "__index"); // [meta] + lua_pop(lstate, 1); // don't use metatable now + + // _getvar + lua_pushcfunction(lstate, &nlua_getvar); + lua_setfield(lstate, -2, "_getvar"); + + // _setvar + lua_pushcfunction(lstate, &nlua_setvar); + lua_setfield(lstate, -2, "_setvar"); + + // vim.spell + luaopen_spell(lstate); + lua_setfield(lstate, -2, "spell"); + } // vim.mpack luaopen_mpack(lstate); @@ -519,10 +535,7 @@ void nlua_state_add_stdlib(lua_State *const lstate) lua_pushcfunction(lstate, &nlua_xdl_diff); lua_setfield(lstate, -2, "diff"); - // vim.spell - luaopen_spell(lstate); - lua_setfield(lstate, -2, "spell"); - + // vim.json lua_cjson_new(lstate); lua_setfield(lstate, -2, "json"); } diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index 60a000843f..f4067ad02f 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -129,6 +129,10 @@ void tslua_init(lua_State *L) build_meta(L, TS_META_QUERY, query_meta); build_meta(L, TS_META_QUERYCURSOR, querycursor_meta); build_meta(L, TS_META_TREECURSOR, treecursor_meta); + +#ifdef NVIM_TS_HAS_SET_ALLOCATOR + ts_set_allocator(xmalloc, xcalloc, xrealloc, xfree); +#endif } int tslua_has_language(lua_State *L) diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index c1a1e7f162..c0247ad996 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -40,51 +40,11 @@ assert(vim) vim.inspect = package.loaded['vim.inspect'] assert(vim.inspect) -local pathtrails = {} -vim._so_trails = {} -for s in (package.cpath..';'):gmatch('[^;]*;') do - s = s:sub(1, -2) -- Strip trailing semicolon - -- Find out path patterns. pathtrail should contain something like - -- /?.so, \?.dll. This allows not to bother determining what correct - -- suffixes are. - local pathtrail = s:match('[/\\][^/\\]*%?.*$') - if pathtrail and not pathtrails[pathtrail] then - pathtrails[pathtrail] = true - table.insert(vim._so_trails, pathtrail) - end -end - -function vim._load_package(name) - local basename = name:gsub('%.', '/') - local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"} - local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) - if #found > 0 then - local f, err = loadfile(found[1]) - return f or error(err) - end +vim.filetype = package.loaded['vim.filetype'] +assert(vim.filetype) - local so_paths = {} - for _,trail in ipairs(vim._so_trails) do - local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash - table.insert(so_paths, path) - end - - found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true}) - if #found > 0 then - -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is - -- a) strip prefix up to and including the first dash, if any - -- b) replace all dots by underscores - -- c) prepend "luaopen_" - -- So "foo-bar.baz" should result in "luaopen_bar_baz" - local dash = name:find("-", 1, true) - local modname = dash and name:sub(dash + 1) or name - local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_")) - return f or error(err) - end - return nil -end - -table.insert(package.loaders, 1, vim._load_package) +vim.keymap = package.loaded['vim.keymap'] +assert(vim.keymap) -- These are for loading runtime modules lazily since they aren't available in -- the nvim binary as specified in executor.c @@ -419,23 +379,43 @@ function vim.defer_fn(fn, timeout) end ---- Notification provider +--- Display a notification to the user. --- ---- Without a runtime, writes to :Messages ----@see :help nvim_notify ----@param msg string Content of the notification to show to the user ----@param log_level number|nil enum from vim.log.levels ----@param opts table|nil additional options (timeout, etc) -function vim.notify(msg, log_level, opts) -- luacheck: no unused - if log_level == vim.log.levels.ERROR then +--- This function can be overridden by plugins to display notifications using a +--- custom provider (such as the system notification provider). By default, +--- writes to |:messages|. +--- +---@param msg string Content of the notification to show to the user. +---@param level number|nil One of the values from |vim.log.levels|. +---@param opts table|nil Optional parameters. Unused by default. +function vim.notify(msg, level, opts) -- luacheck: no unused args + if level == vim.log.levels.ERROR then vim.api.nvim_err_writeln(msg) - elseif log_level == vim.log.levels.WARN then + elseif level == vim.log.levels.WARN then vim.api.nvim_echo({{msg, 'WarningMsg'}}, true, {}) else vim.api.nvim_echo({{msg}}, true, {}) end end +do + local notified = {} + + --- Display a notification only one time. + --- + --- Like |vim.notify()|, but subsequent calls with the same message will not + --- display a notification. + --- + ---@param msg string Content of the notification to show to the user. + ---@param level number|nil One of the values from |vim.log.levels|. + ---@param opts table|nil Optional parameters. Unused by default. + function vim.notify_once(msg, level, opts) -- luacheck: no unused args + if not notified[msg] then + vim.notify(msg, level, opts) + notified[msg] = true + end + end +end ---@private function vim.register_keystroke_callback() @@ -663,4 +643,23 @@ vim._expand_pat_get_parts = function(lua_string) return parts, search_index end +---Prints given arguments in human-readable format. +---Example: +---<pre> +--- -- Print highlight group Normal and store it's contents in a variable. +--- local hl_normal = vim.pretty_print(vim.api.nvim_get_hl_by_name("Normal", true)) +---</pre> +---@see |vim.inspect()| +---@return given arguments. +function vim.pretty_print(...) + local objects = {} + for i = 1, select('#', ...) do + local v = select(i, ...) + table.insert(objects, vim.inspect(v)) + end + + print(table.concat(objects, ' ')) + return ... +end + return module diff --git a/src/nvim/lua/xdiff.c b/src/nvim/lua/xdiff.c index b2e971f9f3..ea7a700e1e 100644 --- a/src/nvim/lua/xdiff.c +++ b/src/nvim/lua/xdiff.c @@ -184,11 +184,11 @@ static NluaXdiffMode process_xdl_diff_opts(lua_State *lstate, xdemitconf_t *cfg, if (strequal("myers", v->data.string.data)) { // default } else if (strequal("minimal", v->data.string.data)) { - cfg->flags |= XDF_NEED_MINIMAL; + params->flags |= XDF_NEED_MINIMAL; } else if (strequal("patience", v->data.string.data)) { - cfg->flags |= XDF_PATIENCE_DIFF; + params->flags |= XDF_PATIENCE_DIFF; } else if (strequal("histogram", v->data.string.data)) { - cfg->flags |= XDF_HISTOGRAM_DIFF; + params->flags |= XDF_HISTOGRAM_DIFF; } else { api_set_error(err, kErrorTypeValidation, "not a valid algorithm"); goto exit_1; diff --git a/src/nvim/macros.h b/src/nvim/macros.h index c2b2c89abf..d5611f587b 100644 --- a/src/nvim/macros.h +++ b/src/nvim/macros.h @@ -105,6 +105,9 @@ #define MB_PTR_BACK(s, p) \ (p -= utf_head_off((char_u *)s, (char_u *)p - 1) + 1) +// MB_CHAR2BYTES(): convert character to bytes and advance pointer to bytes +#define MB_CHAR2BYTES(c, b) ((b) += utf_char2bytes((c), (b))) + #define RESET_BINDING(wp) \ do { \ (wp)->w_p_scb = false; \ diff --git a/src/nvim/main.c b/src/nvim/main.c index cbd1f53727..d0b3a435c3 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" @@ -154,10 +154,10 @@ bool event_teardown(void) void early_init(mparm_T *paramp) { env_init(); - fs_init(); eval_init(); // init global variables init_path(argv0 ? argv0 : "nvim"); init_normal_cmds(); // Init the table of Normal mode commands. + runtime_init(); highlight_init(); #ifdef WIN32 @@ -230,6 +230,10 @@ int main(int argc, char **argv) // `argc` and `argv` are also copied, so that they can be changed. init_params(¶ms, argc, argv); + // Since os_open is called during the init_startuptime, we need to call + // fs_init before it. + fs_init(); + init_startuptime(¶ms); // Need to find "--clean" before actually parsing arguments. @@ -341,9 +345,11 @@ int main(int argc, char **argv) init_default_autocmds(); TIME_MSG("init default autocommands"); + bool vimrc_none = params.use_vimrc != NULL && strequal(params.use_vimrc, "NONE"); + // Reset 'loadplugins' for "-u NONE" before "--cmd" arguments. // Allows for setting 'loadplugins' there. - if (params.use_vimrc != NULL && strequal(params.use_vimrc, "NONE")) { + if (vimrc_none) { // When using --clean we still want to load plugins p_lpl = params.clean; } @@ -351,14 +357,23 @@ int main(int argc, char **argv) // Execute --cmd arguments. exe_pre_commands(¶ms); + if (!vimrc_none || params.clean) { + // Sources ftplugin.vim and indent.vim. We do this *before* the user startup scripts to ensure + // ftplugins run before FileType autocommands defined in the init file (which allows those + // autocommands to overwrite settings from ftplugins). + filetype_plugin_enable(); + } + // Source startup scripts. source_startup_scripts(¶ms); // If using the runtime (-u is not NONE), enable syntax & filetype plugins. - if (params.use_vimrc == NULL || !strequal(params.use_vimrc, "NONE")) { - // Does ":filetype plugin indent on". + if (!vimrc_none || params.clean) { + // Sources filetype.lua and filetype.vim unless the user explicitly disabled it with :filetype + // off. filetype_maybe_enable(); - // Sources syntax/syntax.vim, which calls `:filetype on`. + // Sources syntax/syntax.vim. We do this *after* the user startup scripts so that users can + // disable syntax highlighting with `:syntax off` if they wish. syn_maybe_enable(); } @@ -1549,7 +1564,7 @@ static void edit_buffers(mparm_T *parmp, char_u *cwd) // When w_arg_idx is -1 remove the window (see create_windows()). if (curwin->w_arg_idx == -1) { - win_close(curwin, true); + win_close(curwin, true, false); advance = false; } @@ -1561,7 +1576,7 @@ static void edit_buffers(mparm_T *parmp, char_u *cwd) // When w_arg_idx is -1 remove the window (see create_windows()). if (curwin->w_arg_idx == -1) { arg_idx++; - win_close(curwin, true); + win_close(curwin, true, false); advance = false; continue; } @@ -1608,7 +1623,7 @@ static void edit_buffers(mparm_T *parmp, char_u *cwd) did_emsg = FALSE; // avoid hit-enter prompt getout(1); } - win_close(curwin, true); + win_close(curwin, true, false); advance = false; } if (arg_idx == GARGCOUNT - 1) { diff --git a/src/nvim/map.c b/src/nvim/map.c index 1d9abe3ef2..091d653046 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -29,8 +29,6 @@ #define uint32_t_eq kh_int_hash_equal #define int_hash kh_int_hash_func #define int_eq kh_int_hash_equal -#define linenr_T_hash kh_int_hash_func -#define linenr_T_eq kh_int_hash_equal #define handle_T_hash kh_int_hash_func #define handle_T_eq kh_int_hash_equal @@ -173,15 +171,13 @@ MAP_IMPL(ptr_t, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(uint64_t, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(uint64_t, ssize_t, SSIZE_INITIALIZER) MAP_IMPL(uint64_t, uint64_t, DEFAULT_INITIALIZER) -#define EXTMARK_NS_INITIALIZER { { MAP_INIT }, 1 } -MAP_IMPL(uint64_t, ExtmarkNs, EXTMARK_NS_INITIALIZER) -#define EXTMARK_ITEM_INITIALIZER { 0, 0, NULL } -MAP_IMPL(uint64_t, ExtmarkItem, EXTMARK_ITEM_INITIALIZER) +MAP_IMPL(uint32_t, uint32_t, DEFAULT_INITIALIZER) MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER) #define MSGPACK_HANDLER_INITIALIZER { .fn = NULL, .fast = false } 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 dbd85a4e1f..c9c89bf2fd 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -40,20 +40,13 @@ MAP_DECLS(ptr_t, ptr_t) MAP_DECLS(uint64_t, ptr_t) MAP_DECLS(uint64_t, ssize_t) MAP_DECLS(uint64_t, uint64_t) +MAP_DECLS(uint32_t, uint32_t) -// NB: this is the only way to define a struct both containing and contained -// in a map... -typedef struct ExtmarkNs { // For namespacing extmarks - Map(uint64_t, uint64_t) map[1]; // For fast lookup - uint64_t free_id; // For automatically assigning id's -} ExtmarkNs; - -MAP_DECLS(uint64_t, ExtmarkNs) -MAP_DECLS(uint64_t, ExtmarkItem) 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/mark.c b/src/nvim/mark.c index 39f18b333d..8b29aa3676 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -217,8 +217,8 @@ void checkpcmark(void) && (equalpos(curwin->w_pcmark, curwin->w_cursor) || curwin->w_pcmark.lnum == 0)) { curwin->w_pcmark = curwin->w_prev_pcmark; - curwin->w_prev_pcmark.lnum = 0; // Show it has been checked } + curwin->w_prev_pcmark.lnum = 0; // it has been checked } /* diff --git a/src/nvim/marktree.c b/src/nvim/marktree.c index 38014ab375..918db8b76c 100644 --- a/src/nvim/marktree.c +++ b/src/nvim/marktree.c @@ -56,12 +56,6 @@ #define T MT_BRANCH_FACTOR #define ILEN (sizeof(mtnode_t)+(2 * T) * sizeof(void *)) -#define RIGHT_GRAVITY (((uint64_t)1) << 63) -#define ANTIGRAVITY(id) ((id)&(RIGHT_GRAVITY-1)) -#define IS_RIGHT(id) ((id)&RIGHT_GRAVITY) - -#define PAIRED MARKTREE_PAIRED_FLAG -#define END_FLAG MARKTREE_END_FLAG #define ID_INCR (((uint64_t)1) << 2) #define rawkey(itr) (itr->node->key[itr->i]) @@ -119,7 +113,7 @@ static int key_cmp(mtkey_t a, mtkey_t b) } // NB: keeping the events at the same pos sorted by id is actually not // necessary only make sure that START is before END etc. - return mt_generic_cmp(a.id, b.id); + return mt_generic_cmp(a.flags, b.flags); } static inline int marktree_getp_aux(const mtnode_t *x, mtkey_t k, int *r) @@ -148,7 +142,7 @@ static inline int marktree_getp_aux(const mtnode_t *x, mtkey_t k, int *r) static inline void refkey(MarkTree *b, mtnode_t *x, int i) { - pmap_put(uint64_t)(b->id2node, ANTIGRAVITY(x->key[i].id), x); + pmap_put(uint64_t)(b->id2node, mt_lookup_key(x->key[i]), x); } // put functions @@ -221,38 +215,28 @@ static inline void marktree_putp_aux(MarkTree *b, mtnode_t *x, mtkey_t k) } } -uint64_t marktree_put(MarkTree *b, int row, int col, bool right_gravity, uint8_t decor_level) +void marktree_put(MarkTree *b, mtkey_t key, int end_row, int end_col, bool end_right) { - uint64_t id = (b->next_id+=ID_INCR); - assert(decor_level < DECOR_LEVELS); - id = id | ((uint64_t)decor_level << DECOR_OFFSET); - uint64_t keyid = id; - if (right_gravity) { - // order all right gravity keys after the left ones, for effortless - // insertion (but not deletion!) - keyid |= RIGHT_GRAVITY; - } - marktree_put_key(b, row, col, keyid); - return id; -} + assert(!(key.flags & ~MT_FLAG_EXTERNAL_MASK)); + if (end_row >= 0) { + key.flags |= MT_FLAG_PAIRED; + } -uint64_t marktree_put_pair(MarkTree *b, int start_row, int start_col, bool start_right, int end_row, - int end_col, bool end_right, uint8_t decor_level) -{ - uint64_t id = (b->next_id+=ID_INCR)|PAIRED; - assert(decor_level < DECOR_LEVELS); - id = id | ((uint64_t)decor_level << DECOR_OFFSET); - uint64_t start_id = id|(start_right?RIGHT_GRAVITY:0); - uint64_t end_id = id|END_FLAG|(end_right?RIGHT_GRAVITY:0); - marktree_put_key(b, start_row, start_col, start_id); - marktree_put_key(b, end_row, end_col, end_id); - return id; + marktree_put_key(b, key); + + if (end_row >= 0) { + mtkey_t end_key = key; + end_key.flags = (uint16_t)((uint16_t)(key.flags & ~MT_FLAG_RIGHT_GRAVITY) + |(uint16_t)MT_FLAG_END + |(uint16_t)(end_right ? MT_FLAG_RIGHT_GRAVITY : 0)); + end_key.pos = (mtpos_t){ end_row, end_col }; + marktree_put_key(b, end_key); + } } -void marktree_put_key(MarkTree *b, int row, int col, uint64_t id) +void marktree_put_key(MarkTree *b, mtkey_t k) { - mtkey_t k = { .pos = { .row = row, .col = col }, .id = id }; - + k.flags |= MT_FLAG_REAL; // let's be real. if (!b->root) { b->root = (mtnode_t *)xcalloc(1, ILEN); b->n_nodes++; @@ -302,7 +286,7 @@ void marktree_del_itr(MarkTree *b, MarkTreeIter *itr, bool rev) mtnode_t *cur = itr->node; int curi = itr->i; - uint64_t id = cur->key[curi].id; + uint64_t id = mt_lookup_key(cur->key[curi]); // fprintf(stderr, "\nDELET %lu\n", id); if (itr->node->level) { @@ -364,7 +348,7 @@ void marktree_del_itr(MarkTree *b, MarkTreeIter *itr, bool rev) } b->n_keys--; - pmap_del(uint64_t)(b->id2node, ANTIGRAVITY(id)); + pmap_del(uint64_t)(b->id2node, id); // 5. bool itr_dirty = false; @@ -570,23 +554,29 @@ void marktree_free_node(mtnode_t *x) } /// NB: caller must check not pair! -uint64_t marktree_revise(MarkTree *b, MarkTreeIter *itr, uint8_t decor_level) +void marktree_revise(MarkTree *b, MarkTreeIter *itr, uint8_t decor_level, mtkey_t key) { - uint64_t old_id = rawkey(itr).id; - pmap_del(uint64_t)(b->id2node, ANTIGRAVITY(old_id)); - uint64_t new_id = (b->next_id += ID_INCR) + ((uint64_t)decor_level << DECOR_OFFSET); - rawkey(itr).id = new_id + (RIGHT_GRAVITY&old_id); - refkey(b, itr->node, itr->i); - return new_id; + // TODO(bfredl): clean up this mess and re-instantiate &= and |= forms + // once we upgrade to a non-broken version of gcc in functionaltest-lua CI + rawkey(itr).flags = (uint16_t)((uint16_t)rawkey(itr).flags & (uint16_t)~MT_FLAG_DECOR_MASK); + rawkey(itr).flags = (uint16_t)((uint16_t)rawkey(itr).flags + | (uint16_t)(decor_level << MT_FLAG_DECOR_OFFSET) + | (uint16_t)(key.flags & MT_FLAG_DECOR_MASK)); + rawkey(itr).decor_full = key.decor_full; + rawkey(itr).hl_id = key.hl_id; + rawkey(itr).priority = key.priority; } void marktree_move(MarkTree *b, MarkTreeIter *itr, int row, int col) { - uint64_t old_id = rawkey(itr).id; + mtkey_t key = rawkey(itr); // TODO(bfredl): optimize when moving a mark within a leaf without moving it // across neighbours! marktree_del_itr(b, itr, false); - marktree_put_key(b, row, col, old_id); + key.pos = (mtpos_t){ row, col }; + + + marktree_put_key(b, key); itr->node = NULL; // itr might become invalid by put } @@ -602,14 +592,15 @@ bool marktree_itr_get(MarkTree *b, int row, int col, MarkTreeIter *itr) bool marktree_itr_get_ext(MarkTree *b, mtpos_t p, MarkTreeIter *itr, bool last, bool gravity, mtpos_t *oldbase) { - mtkey_t k = { .pos = p, .id = gravity ? RIGHT_GRAVITY : 0 }; - if (last && !gravity) { - k.id = UINT64_MAX; - } if (b->n_keys == 0) { itr->node = NULL; return false; } + + mtkey_t k = { .pos = p, .flags = gravity ? MT_FLAG_RIGHT_GRAVITY : 0 }; + if (last && !gravity) { + k.flags = MT_FLAG_LAST; + } itr->pos = (mtpos_t){ 0, 0 }; itr->node = b->root; itr->lvl = 0; @@ -816,25 +807,29 @@ mtpos_t marktree_itr_pos(MarkTreeIter *itr) return pos; } -mtmark_t marktree_itr_current(MarkTreeIter *itr) +mtkey_t marktree_itr_current(MarkTreeIter *itr) { if (itr->node) { - uint64_t keyid = rawkey(itr).id; - mtpos_t pos = marktree_itr_pos(itr); - mtmark_t mark = { .row = pos.row, - .col = pos.col, - .id = ANTIGRAVITY(keyid), - .right_gravity = keyid & RIGHT_GRAVITY }; - return mark; - } - return (mtmark_t){ -1, -1, 0, false }; + mtkey_t key = rawkey(itr); + key.pos = marktree_itr_pos(itr); + return key; + } + return MT_INVALID_KEY; +} + +static bool itr_eq(MarkTreeIter *itr1, MarkTreeIter *itr2) +{ + return (&rawkey(itr1) == &rawkey(itr2)); } -static void swap_id(uint64_t *id1, uint64_t *id2) +static void itr_swap(MarkTreeIter *itr1, MarkTreeIter *itr2) { - uint64_t temp = *id1; - *id1 = *id2; - *id2 = temp; + mtkey_t key1 = rawkey(itr1); + mtkey_t key2 = rawkey(itr2); + rawkey(itr1) = key2; + rawkey(itr1).pos = key1.pos; + rawkey(itr2) = key1; + rawkey(itr2).pos = key2.pos; } bool marktree_splice(MarkTree *b, int start_line, int start_col, int old_extent_line, @@ -865,7 +860,7 @@ bool marktree_splice(MarkTree *b, int start_line, int start_col, int old_extent_ mtpos_t ipos = marktree_itr_pos(itr); if (!pos_leq(old_extent, ipos) || (old_extent.row == ipos.row && old_extent.col == ipos.col - && !IS_RIGHT(rawkey(itr).id))) { + && !mt_right(rawkey(itr)))) { marktree_itr_get_ext(b, old_extent, enditr, true, true, NULL); assert(enditr->node); // "assert" (itr <= enditr) @@ -895,13 +890,13 @@ continue_same_node: break; } - if (IS_RIGHT(rawkey(itr).id)) { - while (rawkey(itr).id != rawkey(enditr).id - && IS_RIGHT(rawkey(enditr).id)) { + if (mt_right(rawkey(itr))) { + while (!itr_eq(itr, enditr) + && mt_right(rawkey(enditr))) { marktree_itr_prev(b, enditr); } - if (!IS_RIGHT(rawkey(enditr).id)) { - swap_id(&rawkey(itr).id, &rawkey(enditr).id); + if (!mt_right(rawkey(enditr))) { + itr_swap(itr, enditr); refkey(b, itr->node, itr->i); refkey(b, enditr->node, enditr->i); } else { @@ -911,7 +906,7 @@ continue_same_node: } } - if (rawkey(itr).id == rawkey(enditr).id) { + if (itr_eq(itr, enditr)) { // actually, will be past_right after this key past_right = true; } @@ -1006,13 +1001,13 @@ void marktree_move_region(MarkTree *b, int start_row, colnr_T start_col, int ext marktree_itr_get_ext(b, start, itr, false, true, NULL); kvec_t(mtkey_t) saved = KV_INITIAL_VALUE; while (itr->node) { - mtpos_t pos = marktree_itr_pos(itr); - if (!pos_leq(pos, end) || (pos.row == end.row && pos.col == end.col - && rawkey(itr).id & RIGHT_GRAVITY)) { + mtkey_t k = marktree_itr_current(itr); + if (!pos_leq(k.pos, end) || (k.pos.row == end.row && k.pos.col == end.col + && mt_right(k))) { break; } - relative(start, &pos); - kv_push(saved, ((mtkey_t){ .pos = pos, .id = rawkey(itr).id })); + relative(start, &k.pos); + kv_push(saved, k); marktree_del_itr(b, itr, false); } @@ -1024,30 +1019,36 @@ void marktree_move_region(MarkTree *b, int start_row, colnr_T start_col, int ext for (size_t i = 0; i < kv_size(saved); i++) { mtkey_t item = kv_A(saved, i); unrelative(new, &item.pos); - marktree_put_key(b, item.pos.row, item.pos.col, item.id); + marktree_put_key(b, item); } kv_destroy(saved); } /// @param itr OPTIONAL. set itr to pos. -mtpos_t marktree_lookup(MarkTree *b, uint64_t id, MarkTreeIter *itr) +mtkey_t marktree_lookup_ns(MarkTree *b, uint32_t ns, uint32_t id, bool end, MarkTreeIter *itr) +{ + return marktree_lookup(b, mt_lookup_id(ns, id, end), itr); +} + +/// @param itr OPTIONAL. set itr to pos. +mtkey_t marktree_lookup(MarkTree *b, uint64_t id, MarkTreeIter *itr) { mtnode_t *n = pmap_get(uint64_t)(b->id2node, id); if (n == NULL) { if (itr) { itr->node = NULL; } - return (mtpos_t){ -1, -1 }; + return MT_INVALID_KEY; } int i = 0; for (i = 0; i < n->n; i++) { - if (ANTIGRAVITY(n->key[i].id) == id) { + if (mt_lookup_key(n->key[i]) == id) { goto found; } } abort(); found: {} - mtpos_t pos = n->key[i].pos; + mtkey_t key = n->key[i]; if (itr) { itr->i = i; itr->node = n; @@ -1066,14 +1067,28 @@ found_node: itr->s[b->root->level-p->level].i = i; } if (i > 0) { - unrelative(p->key[i-1].pos, &pos); + unrelative(p->key[i-1].pos, &key.pos); } n = p; } if (itr) { marktree_itr_fix_pos(b, itr); } - return pos; + return key; +} + +mtpos_t marktree_get_altpos(MarkTree *b, mtkey_t mark, MarkTreeIter *itr) +{ + return marktree_get_alt(b, mark, itr).pos; +} + +mtkey_t marktree_get_alt(MarkTree *b, mtkey_t mark, MarkTreeIter *itr) +{ + mtkey_t end = MT_INVALID_KEY; + if (mt_paired(mark)) { + end = marktree_lookup_ns(b, mark.ns, mark.id, !mt_end(mark), itr); + } + return end; } static void marktree_itr_fix_pos(MarkTree *b, MarkTreeIter *itr) @@ -1092,6 +1107,20 @@ static void marktree_itr_fix_pos(MarkTree *b, MarkTreeIter *itr) assert(x == itr->node); } +// for unit test +void marktree_put_test(MarkTree *b, uint32_t id, int row, int col, bool right_gravity) +{ + mtkey_t key = { { row, col }, UINT32_MAX, id, 0, + mt_flags(right_gravity, 0), 0, NULL }; + marktree_put(b, key, -1, -1, false); +} + +// for unit test +bool mt_right_test(mtkey_t key) +{ + return mt_right(key); +} + void marktree_check(MarkTree *b) { #ifndef NDEBUG @@ -1134,11 +1163,11 @@ static size_t check_node(MarkTree *b, mtnode_t *x, mtpos_t *last, bool *last_rig } assert(pos_leq(*last, x->key[i].pos)); if (last->row == x->key[i].pos.row && last->col == x->key[i].pos.col) { - assert(!*last_right || IS_RIGHT(x->key[i].id)); + assert(!*last_right || mt_right(x->key[i])); } - *last_right = IS_RIGHT(x->key[i].id); + *last_right = mt_right(x->key[i]); assert(x->key[i].pos.col >= 0); - assert(pmap_get(uint64_t)(b->id2node, ANTIGRAVITY(x->key[i].id)) == x); + assert(pmap_get(uint64_t)(b->id2node, mt_lookup_key(x->key[i])) == x); } if (x->level) { diff --git a/src/nvim/marktree.h b/src/nvim/marktree.h index a1dcdf5164..30f5aacebc 100644 --- a/src/nvim/marktree.h +++ b/src/nvim/marktree.h @@ -2,9 +2,12 @@ #define NVIM_MARKTREE_H #include <stdint.h> +#include <assert.h> +#include "nvim/assert.h" #include "nvim/garray.h" #include "nvim/map.h" +#include "nvim/types.h" #include "nvim/pos.h" #define MT_MAX_DEPTH 20 @@ -15,13 +18,6 @@ typedef struct { int32_t col; } mtpos_t; -typedef struct { - int32_t row; - int32_t col; - uint64_t id; - bool right_gravity; -} mtmark_t; - typedef struct mtnode_s mtnode_t; typedef struct { int oldcol; @@ -39,12 +35,75 @@ typedef struct { // Internal storage // -// NB: actual marks have id > 0, so we can use (row,col,0) pseudo-key for +// NB: actual marks have flags > 0, so we can use (row,col,0) pseudo-key for // "space before (row,col)" typedef struct { mtpos_t pos; - uint64_t id; + uint32_t ns; + uint32_t id; + int32_t hl_id; + uint16_t flags; + uint16_t priority; + Decoration *decor_full; } mtkey_t; +#define MT_INVALID_KEY (mtkey_t) { { -1, -1 }, 0, 0, 0, 0, 0, NULL } + +#define MT_FLAG_REAL (((uint16_t)1) << 0) +#define MT_FLAG_END (((uint16_t)1) << 1) +#define MT_FLAG_PAIRED (((uint16_t)1) << 2) +#define MT_FLAG_HL_EOL (((uint16_t)1) << 3) + +#define DECOR_LEVELS 4 +#define MT_FLAG_DECOR_OFFSET 4 +#define MT_FLAG_DECOR_MASK (((uint16_t)(DECOR_LEVELS-1)) << MT_FLAG_DECOR_OFFSET) + +// next flag is (((uint16_t)1) << 6) + +// These _must_ be last to preserve ordering of marks +#define MT_FLAG_RIGHT_GRAVITY (((uint16_t)1) << 14) +#define MT_FLAG_LAST (((uint16_t)1) << 15) + +#define MT_FLAG_EXTERNAL_MASK (MT_FLAG_DECOR_MASK | MT_FLAG_RIGHT_GRAVITY | MT_FLAG_HL_EOL) + +#define MARKTREE_END_FLAG (((uint64_t)1) << 63) +static inline uint64_t mt_lookup_id(uint32_t ns, uint32_t id, bool enda) +{ + return (uint64_t)ns << 32 | id | (enda?MARKTREE_END_FLAG:0); +} +#undef MARKTREE_END_FLAG + +static inline uint64_t mt_lookup_key(mtkey_t key) +{ + return mt_lookup_id(key.ns, key.id, key.flags & MT_FLAG_END); +} + +static inline bool mt_paired(mtkey_t key) +{ + return key.flags & MT_FLAG_PAIRED; +} + +static inline bool mt_end(mtkey_t key) +{ + return key.flags & MT_FLAG_END; +} + +static inline bool mt_right(mtkey_t key) +{ + return key.flags & MT_FLAG_RIGHT_GRAVITY; +} + +static inline uint8_t marktree_decor_level(mtkey_t key) +{ + return (uint8_t)((key.flags&MT_FLAG_DECOR_MASK) >> MT_FLAG_DECOR_OFFSET); +} + +static inline uint16_t mt_flags(bool right_gravity, uint8_t decor_level) +{ + assert(decor_level < DECOR_LEVELS); + return (uint16_t)((right_gravity ? MT_FLAG_RIGHT_GRAVITY : 0) + | (decor_level << MT_FLAG_DECOR_OFFSET)); +} + struct mtnode_s { int32_t n; @@ -61,7 +120,6 @@ struct mtnode_s { typedef struct { mtnode_t *root; size_t n_keys, n_nodes; - uint64_t next_id; // TODO(bfredl): the pointer to node could be part of the larger // Map(uint64_t, ExtmarkItem) essentially; PMap(uint64_t) id2node[1]; @@ -72,16 +130,4 @@ typedef struct { # include "marktree.h.generated.h" #endif -#define MARKTREE_PAIRED_FLAG (((uint64_t)1) << 1) -#define MARKTREE_END_FLAG (((uint64_t)1) << 0) - -#define DECOR_LEVELS 4 -#define DECOR_OFFSET 61 -#define DECOR_MASK (((uint64_t)(DECOR_LEVELS-1)) << DECOR_OFFSET) - -static inline uint8_t marktree_decor_level(uint64_t id) -{ - return (uint8_t)((id&DECOR_MASK) >> DECOR_OFFSET); -} - #endif // NVIM_MARKTREE_H diff --git a/src/nvim/mbyte.c b/src/nvim/mbyte.c index 5eb209a6f6..f634c7dda8 100644 --- a/src/nvim/mbyte.c +++ b/src/nvim/mbyte.c @@ -1067,7 +1067,7 @@ bool utf_printable(int c) static struct interval nonprint[] = { { 0x070f, 0x070f }, { 0x180b, 0x180e }, { 0x200b, 0x200f }, { 0x202a, 0x202e }, - { 0x206a, 0x206f }, { 0xd800, 0xdfff }, { 0xfeff, 0xfeff }, { 0xfff9, 0xfffb }, + { 0x2060, 0x206f }, { 0xd800, 0xdfff }, { 0xfeff, 0xfeff }, { 0xfff9, 0xfffb }, { 0xfffe, 0xffff } }; @@ -1317,6 +1317,12 @@ bool mb_isupper(int a) return mb_tolower(a) != a; } +bool mb_isalpha(int a) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + return mb_islower(a) || mb_isupper(a); +} + static int utf_strnicmp(const char_u *s1, const char_u *s2, size_t n1, size_t n2) { int c1, c2, cdiff; @@ -1821,12 +1827,10 @@ void mb_copy_char(const char_u **const fp, char_u **const tp) *fp += l; } -/* - * Return the offset from "p" to the first byte of a character. When "p" is - * at the start of a character 0 is returned, otherwise the offset to the next - * character. Can start anywhere in a stream of bytes. - */ -int mb_off_next(char_u *base, char_u *p) +/// Return the offset from "p" to the first byte of a character. When "p" is +/// at the start of a character 0 is returned, otherwise the offset to the next +/// character. Can start anywhere in a stream of bytes. +int mb_off_next(const char_u *base, const char_u *p) { int i; int j; @@ -1854,7 +1858,7 @@ int mb_off_next(char_u *base, char_u *p) /// Return the offset from "p" to the last byte of the character it points /// into. Can start anywhere in a stream of bytes. /// Composing characters are not included. -int mb_tail_off(char_u *base, char_u *p) +int mb_tail_off(const char_u *base, const char_u *p) { int i; int j; @@ -1882,12 +1886,13 @@ int mb_tail_off(char_u *base, char_u *p) /// Return the offset from "p" to the first byte of the character it points /// into. Can start anywhere in a stream of bytes. +/// Unlike utf_head_off() this doesn't include composing characters and returns a negative value. /// /// @param[in] base Pointer to start of string /// @param[in] p Pointer to byte for which to return the offset to the previous codepoint // /// @return 0 if invalid sequence, else offset to previous codepoint -int mb_head_off(char_u *base, char_u *p) +int mb_head_off(const char_u *base, const char_u *p) { int i; int j; @@ -2037,13 +2042,11 @@ char_u *mb_prevptr(char_u *line, char_u *p) return p; } -/* - * Return the character length of "str". Each multi-byte character (with - * following composing characters) counts as one. - */ -int mb_charlen(char_u *str) +/// Return the character length of "str". Each multi-byte character (with +/// following composing characters) counts as one. +int mb_charlen(const char_u *str) { - char_u *p = str; + const char_u *p = str; int count; if (p == NULL) { @@ -2057,12 +2060,10 @@ int mb_charlen(char_u *str) return count; } -/* - * Like mb_charlen() but for a string with specified length. - */ -int mb_charlen_len(char_u *str, int len) +/// Like mb_charlen() but for a string with specified length. +int mb_charlen_len(const char_u *str, int len) { - char_u *p = str; + const char_u *p = str; int count; for (count = 0; *p != NUL && p < str + len; count++) { @@ -2089,8 +2090,7 @@ const char *mb_unescape(const char **const pp) size_t buf_idx = 0; uint8_t *str = (uint8_t *)(*pp); - // Must translate K_SPECIAL KS_SPECIAL KE_FILLER to K_SPECIAL and CSI - // KS_EXTRA KE_CSI to CSI. + // Must translate K_SPECIAL KS_SPECIAL KE_FILLER to K_SPECIAL. // Maximum length of a utf-8 character is 4 bytes. for (size_t str_idx = 0; str[str_idx] != NUL && buf_idx < 4; str_idx++) { if (str[str_idx] == K_SPECIAL @@ -2098,11 +2098,6 @@ const char *mb_unescape(const char **const pp) && str[str_idx + 2] == KE_FILLER) { buf[buf_idx++] = (char)K_SPECIAL; str_idx += 2; - } else if ((str[str_idx] == K_SPECIAL) - && str[str_idx + 1] == KS_EXTRA - && str[str_idx + 2] == KE_CSI) { - buf[buf_idx++] = (char)CSI; - str_idx += 2; } else if (str[str_idx] == K_SPECIAL) { break; // A special key can't be a multibyte char. } else { @@ -2207,11 +2202,9 @@ char_u *enc_canonize(char_u *enc) FUNC_ATTR_NONNULL_RET return r; } -/* - * Search for an encoding alias of "name". - * Returns -1 when not found. - */ -static int enc_alias_search(char_u *name) +/// Search for an encoding alias of "name". +/// Returns -1 when not found. +static int enc_alias_search(const char_u *name) { int i; diff --git a/src/nvim/memfile.c b/src/nvim/memfile.c index 3397296b3a..2a72d1e6a0 100644 --- a/src/nvim/memfile.c +++ b/src/nvim/memfile.c @@ -247,7 +247,7 @@ bhdr_T *mf_new(memfile_T *mfp, bool negative, unsigned page_count) } else { // need to allocate memory for this block // If the number of pages matches use the bhdr_T from the free list and // allocate the data. - void *p = xmalloc(mfp->mf_page_size * page_count); + void *p = xmalloc((size_t)mfp->mf_page_size * page_count); hp = mf_rem_free(mfp); hp->bh_data = p; } @@ -269,7 +269,7 @@ bhdr_T *mf_new(memfile_T *mfp, bool negative, unsigned page_count) // Init the data to all zero, to avoid reading uninitialized data. // This also avoids that the passwd file ends up in the swap file! - (void)memset(hp->bh_data, 0, mfp->mf_page_size * page_count); + (void)memset(hp->bh_data, 0, (size_t)mfp->mf_page_size * page_count); return hp; } @@ -528,7 +528,7 @@ bool mf_release_all(void) static bhdr_T *mf_alloc_bhdr(memfile_T *mfp, unsigned page_count) { bhdr_T *hp = xmalloc(sizeof(bhdr_T)); - hp->bh_data = xmalloc(mfp->mf_page_size * page_count); + hp->bh_data = xmalloc((size_t)mfp->mf_page_size * page_count); hp->bh_page_count = page_count; return hp; } diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 08521c0dc3..004ef36b36 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -704,11 +704,14 @@ static void set_b0_fname(ZERO_BL *b0p, buf_T *buf) long_to_char((long)os_fileinfo_inode(&file_info), b0p->b0_ino); buf_store_file_info(buf, &file_info); buf->b_mtime_read = buf->b_mtime; + buf->b_mtime_read_ns = buf->b_mtime_ns; } else { long_to_char(0L, b0p->b0_mtime); long_to_char(0L, b0p->b0_ino); buf->b_mtime = 0; + buf->b_mtime_ns = 0; buf->b_mtime_read = 0; + buf->b_mtime_read_ns = 0; buf->b_orig_size = 0; buf->b_orig_mode = 0; } @@ -1032,9 +1035,9 @@ void ml_recover(bool checkext) line_count = 0; idx = 0; // start with first index in block 1 error = 0; - buf->b_ml.ml_stack_top = 0; + buf->b_ml.ml_stack_top = 0; // -V1048 buf->b_ml.ml_stack = NULL; - buf->b_ml.ml_stack_size = 0; // no stack yet + buf->b_ml.ml_stack_size = 0; // -V1048 if (curbuf->b_ffname == NULL) { cannot_open = true; @@ -1720,6 +1723,7 @@ void ml_sync_all(int check_file, int check_char, bool do_fsync) FileInfo file_info; if (!os_fileinfo((char *)buf->b_ffname, &file_info) || file_info.stat.st_mtim.tv_sec != buf->b_mtime_read + || file_info.stat.st_mtim.tv_nsec != buf->b_mtime_read_ns || os_fileinfo_size(&file_info) != buf->b_orig_size) { ml_preserve(buf, false, do_fsync); did_check_timestamps = false; @@ -4139,7 +4143,7 @@ long ml_find_line_or_offset(buf_T *buf, linenr_T lnum, long *offp, bool no_ff) || (offset != 0 && offset > size + buf->b_ml.ml_chunksize[curix].mlcs_totalsize - + ffdos * buf->b_ml.ml_chunksize[curix].mlcs_numlines))) { + + (long)ffdos * buf->b_ml.ml_chunksize[curix].mlcs_numlines))) { curline += buf->b_ml.ml_chunksize[curix].mlcs_numlines; size += buf->b_ml.ml_chunksize[curix].mlcs_totalsize; if (offset && ffdos) { diff --git a/src/nvim/menu.c b/src/nvim/menu.c index ac4d52c392..0db9d69a7e 100644 --- a/src/nvim/menu.c +++ b/src/nvim/menu.c @@ -361,7 +361,7 @@ static int add_menu_path(const char_u *const menu_path, vimmenu_T *menuarg, goto erret; } - // Not already there, so lets add it + // Not already there, so let's add it menu = xcalloc(1, sizeof(vimmenu_T)); menu->modes = modes; diff --git a/src/nvim/message.c b/src/nvim/message.c index befca8c76b..b39450cdc6 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -262,7 +262,6 @@ void msg_multiline_attr(const char *s, int attr, bool check_int, bool *need_clea if (*s != NUL) { msg_outtrans_attr((char_u *)s, attr); } - return; } @@ -329,7 +328,7 @@ bool msg_attr_keep(const char *s, int attr, bool keep, bool multiline) } retval = msg_end(); - if (keep && retval && vim_strsize((char_u *)s) < (int)(Rows - cmdline_row - 1) + if (keep && retval && vim_strsize((char_u *)s) < (Rows - cmdline_row - 1) * Columns + sc_col) { set_keep_msg((char *)s, 0); } @@ -356,10 +355,10 @@ char_u *msg_strtrunc(char_u *s, int force) len = vim_strsize(s); if (msg_scrolled != 0) { // Use all the columns. - room = (int)(Rows - msg_row) * Columns - 1; + room = (Rows - msg_row) * Columns - 1; } else { // Use up to 'showcmd' column. - room = (int)(Rows - msg_row - 1) * Columns + sc_col - 1; + room = (Rows - msg_row - 1) * Columns + sc_col - 1; } if (len > room && room > 0) { // may have up to 18 bytes per cell (6 per char, up to two @@ -873,7 +872,7 @@ char_u *msg_may_trunc(bool force, char_u *s) { int room; - room = (int)(Rows - cmdline_row - 1) * Columns + sc_col - 1; + room = (Rows - cmdline_row - 1) * Columns + sc_col - 1; if ((force || (shortmess(SHM_TRUNC) && !exmode_active)) && (int)STRLEN(s) - room > 0) { int size = vim_strsize(s); @@ -1095,6 +1094,10 @@ void wait_return(int redraw) return; } + if (headless_mode && !ui_active()) { + return; + } + /* * When inside vgetc(), we can't wait for a typed character at all. * With the global command (and some others) we only need one return at @@ -1215,7 +1218,7 @@ void wait_return(int redraw) } else if (vim_strchr((char_u *)"\r\n ", c) == NULL && c != Ctrl_C) { // Put the character back in the typeahead buffer. Don't use the // stuff buffer, because lmaps wouldn't work. - ins_char_typebuf(c); + ins_char_typebuf(c, mod_mask); do_redraw = true; // need a redraw even though there is // typeahead } @@ -1269,7 +1272,7 @@ static void hit_return_msg(void) { int save_p_more = p_more; - p_more = FALSE; // don't want see this message when scrolling back + p_more = false; // don't want to see this message when scrolling back if (msg_didout) { // start on a new line msg_putchar('\n'); } @@ -2647,6 +2650,17 @@ static void msg_puts_printf(const char *str, const ptrdiff_t maxlen) char buf[7]; char *p; + if (on_print.type != kCallbackNone) { + typval_T argv[1]; + argv[0].v_type = VAR_STRING; + argv[0].v_lock = VAR_UNLOCKED; + argv[0].vval.v_string = (char_u *)str; + typval_T rettv = TV_INITIAL_VALUE; + callback_call(&on_print, 1, argv, &rettv); + tv_clear(&rettv); + return; + } + while ((maxlen < 0 || s - str < maxlen) && *s != NUL) { int len = utf_ptr2len((const char_u *)s); if (!(silent_mode && p_verbose == 0)) { @@ -3450,6 +3464,7 @@ void msg_advance(int col) /// /// @param textfiel IObuff for inputdialog(), NULL otherwise /// @param ex_cmd when TRUE pressing : accepts default and starts Ex command +/// @returns 0 if cancelled, otherwise the nth button (1-indexed). int do_dialog(int type, char_u *title, char_u *message, char_u *buttons, int dfltbutton, char_u *textfield, int ex_cmd) { @@ -3497,7 +3512,7 @@ int do_dialog(int type, char_u *title, char_u *message, char_u *buttons, int dfl } if (c == ':' && ex_cmd) { retval = dfltbutton; - ins_char_typebuf(':'); + ins_char_typebuf(':', 0); break; } diff --git a/src/nvim/move.c b/src/nvim/move.c index 15ba6645f5..27cc2b341c 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -346,10 +346,10 @@ void update_topline(win_T *wp) */ void update_topline_win(win_T *win) { - win_T *save_curwin; - switch_win(&save_curwin, NULL, win, NULL, true); + switchwin_T switchwin; + switch_win(&switchwin, win, NULL, true); update_topline(curwin); - restore_win(save_curwin, NULL, true); + restore_win(&switchwin, true); } /* @@ -909,7 +909,7 @@ void curs_columns(win_T *wp, int may_scroll) } wp->w_skipcol = n * width; } else if (extra == 1) { - // less then 'scrolloff' lines above, decrease skipcol + // less than 'scrolloff' lines above, decrease skipcol assert(so <= INT_MAX); extra = (wp->w_skipcol + (int)so * width - wp->w_virtcol + width - 1) / width; @@ -920,7 +920,7 @@ void curs_columns(win_T *wp, int may_scroll) wp->w_skipcol -= extra * width; } } else if (extra == 2) { - // less then 'scrolloff' lines below, increase skipcol + // less than 'scrolloff' lines below, increase skipcol endcol = (n - wp->w_height_inner + 1) * width; while (endcol > wp->w_virtcol) { endcol -= width; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 3246596f16..7fe6469527 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -164,7 +164,7 @@ static const struct nv_cmd { { Ctrl_O, nv_ctrlo, 0, 0 }, { Ctrl_P, nv_up, NV_STS, false }, { Ctrl_Q, nv_visual, 0, false }, - { Ctrl_R, nv_redo, 0, 0 }, + { Ctrl_R, nv_redo_or_register, 0, 0 }, { Ctrl_S, nv_ignore, 0, 0 }, { Ctrl_T, nv_tagpop, NV_NCW, 0 }, { Ctrl_U, nv_halfpage, 0, 0 }, @@ -334,6 +334,7 @@ static const struct nv_cmd { { K_SELECT, nv_select, 0, 0 }, { K_EVENT, nv_event, NV_KEEPREG, 0 }, { K_COMMAND, nv_colon, 0, 0 }, + { K_LUA, nv_colon, 0, 0 }, }; // Number of commands in nv_cmds[]. @@ -823,15 +824,12 @@ static bool normal_get_command_count(NormalState *s) if (s->c == K_DEL || s->c == K_KDEL) { s->ca.count0 /= 10; del_from_showcmd(4); // delete the digit and ~@% + } else if (s->ca.count0 > 99999999L) { + s->ca.count0 = 999999999L; } else { s->ca.count0 = s->ca.count0 * 10 + (s->c - '0'); } - if (s->ca.count0 < 0) { - // overflow - s->ca.count0 = 999999999L; - } - // Set v:count here, when called from main() and not a stuffed // command, so that v:count can be used in an expression mapping // right after the count. Do set it for redo. @@ -960,6 +958,7 @@ normal_end: && s->oa.regname == 0) { if (restart_VIsual_select == 1) { VIsual_select = true; + VIsual_select_reg = 0; trigger_modechanged(); showmode(); restart_VIsual_select = 0; @@ -1009,7 +1008,14 @@ static int normal_execute(VimState *state, int key) // restart automatically. // Insert the typed character in the typeahead buffer, so that it can // be mapped in Insert mode. Required for ":lmap" to work. - ins_char_typebuf(s->c); + int len = ins_char_typebuf(s->c, mod_mask); + + // When recording and gotchars() was called the character will be + // recorded again, remove the previous recording. + if (KeyTyped) { + ungetchars(len); + } + if (restart_edit != 0) { s->c = 'd'; } else { @@ -1021,7 +1027,7 @@ static int normal_execute(VimState *state, int key) s->need_flushbuf = add_to_showcmd(s->c); - while (normal_get_command_count(s)) { continue; } + while (normal_get_command_count(s)) { } if (s->c == K_EVENT) { // Save the count values so that ca.opcount and ca.count0 are exactly @@ -1037,14 +1043,14 @@ static int normal_execute(VimState *state, int key) // If you give a count before AND after the operator, they are // multiplied. if (s->ca.count0) { - s->ca.count0 = (long)((uint64_t)s->ca.count0 * (uint64_t)s->ca.opcount); + if (s->ca.opcount >= 999999999L / s->ca.count0) { + s->ca.count0 = 999999999L; + } else { + s->ca.count0 *= s->ca.opcount; + } } else { s->ca.count0 = s->ca.opcount; } - if (s->ca.count0 < 0) { - // overflow - s->ca.count0 = 999999999L; - } } // Always remember the count. It will be set to zero (on the next call, @@ -3089,8 +3095,14 @@ static void nv_gd(oparg_T *oap, int nchar, int thisblock) if ((len = find_ident_under_cursor(&ptr, FIND_IDENT)) == 0 || !find_decl(ptr, len, nchar == 'd', thisblock, SEARCH_START)) { clearopbeep(oap); - } else if ((fdo_flags & FDO_SEARCH) && KeyTyped && oap->op_type == OP_NOP) { - foldOpenCursor(); + } else { + if ((fdo_flags & FDO_SEARCH) && KeyTyped && oap->op_type == OP_NOP) { + foldOpenCursor(); + } + // clear any search statistics + if (messaging() && !msg_silent && !shortmess(SHM_SEARCHCOUNT)) { + clear_cmdline = true; + } } } @@ -3280,7 +3292,7 @@ static bool nv_screengo(oparg_T *oap, int dir, long dist) int col_off1; // margin offset for first screen line int col_off2; // margin offset for wrapped screen line int width1; // text width for first screen line - int width2; // test width for wrapped screen line + int width2; // text width for wrapped screen line oap->motion_type = kMTCharWise; oap->inclusive = (curwin->w_curswant == MAXCOL); @@ -3404,6 +3416,13 @@ static bool nv_screengo(oparg_T *oap, int dir, long dist) virtcol -= vim_strsize(get_showbreak_value(curwin)); } + int c = utf_ptr2char(get_cursor_pos_ptr()); + if (dir == FORWARD && virtcol < curwin->w_curswant + && (curwin->w_curswant <= (colnr_T)width1) + && !vim_isprintc(c) && c > 255) { + oneright(); + } + if (virtcol > curwin->w_curswant && (curwin->w_curswant < (colnr_T)width1 ? (curwin->w_curswant > (colnr_T)width1 / 2) @@ -4043,21 +4062,22 @@ static void nv_regreplay(cmdarg_T *cap) } } -/// Handle a ":" command and <Cmd>. +/// Handle a ":" command and <Cmd> or Lua keymaps. static void nv_colon(cmdarg_T *cap) { int old_p_im; bool cmd_result; bool is_cmdkey = cap->cmdchar == K_COMMAND; + bool is_lua = cap->cmdchar == K_LUA; - if (VIsual_active && !is_cmdkey) { + if (VIsual_active && !is_cmdkey && !is_lua) { nv_operator(cap); } else { if (cap->oap->op_type != OP_NOP) { // Using ":" as a movement is charwise exclusive. cap->oap->motion_type = kMTCharWise; cap->oap->inclusive = false; - } else if (cap->count0 && !is_cmdkey) { + } else if (cap->count0 && !is_cmdkey && !is_lua) { // translate "count:" into ":.,.+(count - 1)" stuffcharReadbuff('.'); if (cap->count0 > 1) { @@ -4073,9 +4093,13 @@ static void nv_colon(cmdarg_T *cap) old_p_im = p_im; - // get a command line and execute it - cmd_result = do_cmdline(NULL, is_cmdkey ? getcmdkeycmd : getexline, NULL, - cap->oap->op_type != OP_NOP ? DOCMD_KEEPLINE : 0); + if (is_lua) { + cmd_result = map_execute_lua(); + } else { + // get a command line and execute it + cmd_result = do_cmdline(NULL, is_cmdkey ? getcmdkeycmd : getexline, NULL, + cap->oap->op_type != OP_NOP ? DOCMD_KEEPLINE : 0); + } // If 'insertmode' changed, enter or exit Insert mode if (p_im != old_p_im) { @@ -4431,11 +4455,7 @@ static void nv_ident(cmdarg_T *cap) // Start insert mode in terminal buffer restart_edit = 'i'; - add_map((char_u *)"<buffer> <esc> <Cmd>call jobstop(&channel)<CR>", TERM_FOCUS, true); - do_cmdline_cmd("autocmd TermClose <buffer> " - " if !v:event.status |" - " exec 'bdelete! ' .. expand('<abuf>') |" - " endif"); + add_map((char_u *)"<buffer> <esc> <Cmd>bdelete!<CR>", TERM_FOCUS, true); } } @@ -4470,8 +4490,13 @@ bool get_visual_text(cmdarg_T *cap, char_u **pp, size_t *lenp) *pp = ml_get_pos(&VIsual); *lenp = (size_t)curwin->w_cursor.col - (size_t)VIsual.col + 1; } - // Correct the length to include the whole last character. - *lenp += (size_t)(utfc_ptr2len(*pp + (*lenp - 1)) - 1); + if (**pp == NUL) { + *lenp = 0; + } + if (*lenp > 0) { + // Correct the length to include all bytes of the last character. + *lenp += (size_t)(utfc_ptr2len(*pp + (*lenp - 1)) - 1); + } } reset_VIsual_and_resel(); return true; @@ -5005,9 +5030,7 @@ static void nv_brackets(cmdarg_T *cap) * identifier "]i" "[i" "]I" "[I" "]^I" "[^I" * define "]d" "[d" "]D" "[D" "]^D" "[^D" */ - if (vim_strchr((char_u *) - "iI\011dD\004", - cap->nchar) != NULL) { + if (vim_strchr((char_u *)"iI\011dD\004", cap->nchar) != NULL) { char_u *ptr; size_t len; @@ -5961,11 +5984,8 @@ static void nv_visual(cmdarg_T *cap) * was only one -- webb */ if (resel_VIsual_mode != 'v' || resel_VIsual_line_count > 1) { - curwin->w_cursor.lnum += - resel_VIsual_line_count * cap->count0 - 1; - if (curwin->w_cursor.lnum > curbuf->b_ml.ml_line_count) { - curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; - } + curwin->w_cursor.lnum += resel_VIsual_line_count * cap->count0 - 1; + check_cursor(); } VIsual_mode = resel_VIsual_mode; if (VIsual_mode == 'v') { @@ -6044,7 +6064,7 @@ static void n_start_visual_mode(int c) // Corner case: the 0 position in a tab may change when going into // virtualedit. Recalculate curwin->w_cursor to avoid bad highlighting. // - if (c == Ctrl_V && (ve_flags & VE_BLOCK) && gchar_cursor() == TAB) { + if (c == Ctrl_V && (get_ve_flags() & VE_BLOCK) && gchar_cursor() == TAB) { validate_virtcol(); coladvance(curwin->w_virtcol); } @@ -6184,6 +6204,7 @@ static void nv_g_cmd(cmdarg_T *cap) // start Select mode. if (cap->arg) { VIsual_select = true; + VIsual_select_reg = 0; } else { may_start_select('c'); } @@ -6310,20 +6331,17 @@ static void nv_g_cmd(cmdarg_T *cap) curwin->w_set_curswant = true; break; - case 'M': { - const char_u *const ptr = get_cursor_line_ptr(); - + case 'M': oap->motion_type = kMTCharWise; oap->inclusive = false; - i = (int)mb_string2cells_len(ptr, STRLEN(ptr)); + i = linetabsize(get_cursor_line_ptr()); if (cap->count0 > 0 && cap->count0 <= 100) { coladvance((colnr_T)(i * cap->count0 / 100)); } else { coladvance((colnr_T)(i / 2)); } curwin->w_set_curswant = true; - } - break; + break; case '_': /* "g_": to the last non-blank character in the line or <count> lines @@ -6667,9 +6685,8 @@ static void n_opencmd(cmdarg_T *cap) (cap->cmdchar == 'o' ? 1 : 0)) ) && open_line(cap->cmdchar == 'O' ? BACKWARD : FORWARD, - has_format_option(FO_OPEN_COMS) - ? OPENLINE_DO_COM : 0, - 0)) { + has_format_option(FO_OPEN_COMS) ? OPENLINE_DO_COM : 0, + 0, NULL)) { if (win_cursorline_standout(curwin)) { // force redraw of cursorline curwin->w_valid &= ~VALID_CROW; @@ -6696,11 +6713,26 @@ static void nv_dot(cmdarg_T *cap) } } -/* - * CTRL-R: undo undo - */ -static void nv_redo(cmdarg_T *cap) +// CTRL-R: undo undo or specify register in select mode +static void nv_redo_or_register(cmdarg_T *cap) { + if (VIsual_select && VIsual_active) { + int reg; + // Get register name + no_mapping++; + reg = plain_vgetc(); + LANGMAP_ADJUST(reg, true); + no_mapping--; + + if (reg == '"') { + // the unnamed register is 0 + reg = 0; + } + + VIsual_select_reg = valid_yank_reg(reg, true) ? reg : 0; + return; + } + if (!checkclearopq(cap->oap)) { u_redo((int)cap->count1); curwin->w_set_curswant = true; @@ -6949,7 +6981,7 @@ static void adjust_cursor(oparg_T *oap) if (curwin->w_cursor.col > 0 && gchar_cursor() == NUL && (!VIsual_active || *p_sel == 'o') && !virtual_active() - && (ve_flags & VE_ONEMORE) == 0) { + && (get_ve_flags() & VE_ONEMORE) == 0) { curwin->w_cursor.col--; // prevent cursor from moving on the trail byte mb_adjust_cursor(); @@ -7021,6 +7053,7 @@ static void nv_select(cmdarg_T *cap) { if (VIsual_active) { VIsual_select = true; + VIsual_select_reg = 0; } else if (VIsual_reselect) { cap->nchar = 'v'; // fake "gv" command cap->arg = true; @@ -7155,7 +7188,7 @@ static void nv_esc(cmdarg_T *cap) void set_cursor_for_append_to_line(void) { curwin->w_set_curswant = true; - if (ve_flags == VE_ALL) { + if (get_ve_flags() == VE_ALL) { const int save_State = State; // Pretend Insert mode here to allow the cursor on the @@ -7473,9 +7506,9 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) // overwrites if the old contents is being put. was_visual = true; regname = cap->oap->regname; + bool save_unnamed = cap->cmdchar == 'P'; // '+' and '*' could be the same selection - bool clipoverwrite = (regname == '+' || regname == '*') - && (cb_flags & CB_UNNAMEDMASK); + bool clipoverwrite = (regname == '+' || regname == '*') && (cb_flags & CB_UNNAMEDMASK); if (regname == 0 || regname == '"' || clipoverwrite || ascii_isdigit(regname) || regname == '-') { // The delete might overwrite the register we want to put, save it first @@ -7488,6 +7521,10 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) // do_put(), which requires the visual selection to still be active. if (!VIsual_active || VIsual_mode == 'V' || regname != '.') { // Now delete the selected text. Avoid messages here. + yankreg_T *old_y_previous; + if (save_unnamed) { + old_y_previous = get_y_previous(); + } cap->cmdchar = 'd'; cap->nchar = NUL; cap->oap->regname = NUL; @@ -7497,6 +7534,10 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent) empty = (curbuf->b_ml.ml_flags & ML_EMPTY); msg_silent--; + if (save_unnamed) { + set_y_previous(old_y_previous); + } + // delete PUT_LINE_BACKWARD; cap->oap->regname = regname; } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index c6f9c5f04f..b5c7020dee 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -135,10 +135,18 @@ static char opchars[][3] = { Ctrl_X, NUL, OPF_CHANGE }, // OP_NR_SUB }; -/* - * Translate a command name into an operator type. - * Must only be called with a valid operator name! - */ +yankreg_T *get_y_previous(void) +{ + return y_previous; +} + +void set_y_previous(yankreg_T *yreg) +{ + y_previous = yreg; +} + +/// Translate a command name into an operator type. +/// Must only be called with a valid operator name! int get_op_type(int char1, int char2) { int i; @@ -267,14 +275,14 @@ void op_shift(oparg_T *oap, int curs_top, int amount) msg_attr_keep((char *)IObuff, 0, true, false); } - /* - * Set "'[" and "']" marks. - */ - curbuf->b_op_start = oap->start; - curbuf->b_op_end.lnum = oap->end.lnum; - curbuf->b_op_end.col = (colnr_T)STRLEN(ml_get(oap->end.lnum)); - if (curbuf->b_op_end.col > 0) { - curbuf->b_op_end.col--; + if (!cmdmod.lockmarks) { + // Set "'[" and "']" marks. + curbuf->b_op_start = oap->start; + curbuf->b_op_end.lnum = oap->end.lnum; + curbuf->b_op_end.col = (colnr_T)STRLEN(ml_get(oap->end.lnum)); + if (curbuf->b_op_end.col > 0) { + curbuf->b_op_end.col--; + } } changed_lines(oap->start.lnum, 0, oap->end.lnum + 1, 0L, true); @@ -381,8 +389,8 @@ static void shift_block(oparg_T *oap, int amount) } } for (; ascii_iswhite(*bd.textstart);) { - // TODO: is passing bd.textstart for start of the line OK? - incr = lbr_chartabsize_adv(bd.textstart, &bd.textstart, (bd.start_vcol)); + // TODO(fmoralesc): is passing bd.textstart for start of the line OK? + incr = lbr_chartabsize_adv(bd.textstart, &bd.textstart, bd.start_vcol); total += incr; bd.start_vcol += incr; } @@ -560,21 +568,18 @@ static void block_insert(oparg_T *oap, char_u *s, int b_insert, struct block_def } if (spaces > 0) { - int off; - - // Avoid starting halfway through a multi-byte character. - if (b_insert) { - off = utf_head_off(oldp, oldp + offset + spaces); - } else { - off = (*mb_off_next)(oldp, oldp + offset); - offset += off; - } - spaces -= off; - count -= off; + // avoid copying part of a multi-byte character + offset -= utf_head_off(oldp, oldp + offset); + } + if (spaces < 0) { // can happen when the cursor was moved + spaces = 0; } assert(count >= 0); - newp = (char_u *)xmalloc(STRLEN(oldp) + s_len + (size_t)count + 1); + // Make sure the allocated size matches what is actually copied below. + newp = xmalloc(STRLEN(oldp) + (size_t)spaces + s_len + + (spaces > 0 && !bdp->is_short ? (size_t)p_ts - (size_t)spaces : 0) + + (size_t)count + 1); // copy up to shifted part memmove(newp, oldp, (size_t)offset); @@ -589,14 +594,19 @@ static void block_insert(oparg_T *oap, char_u *s, int b_insert, struct block_def offset += (int)s_len; int skipped = 0; - if (spaces && !bdp->is_short) { - // insert post-padding - memset(newp + offset + spaces, ' ', (size_t)(p_ts - spaces)); - // We're splitting a TAB, don't copy it. - oldp++; - // We allowed for that TAB, remember this now - count++; - skipped = 1; + if (spaces > 0 && !bdp->is_short) { + if (*oldp == TAB) { + // insert post-padding + memset(newp + offset + spaces, ' ', (size_t)(p_ts - spaces)); + // We're splitting a TAB, don't copy it. + oldp++; + // We allowed for that TAB, remember this now + count++; + skipped = 1; + } else { + // Not a TAB, no extra spaces + count = spaces; + } } if (spaces > 0) { @@ -694,9 +704,11 @@ void op_reindent(oparg_T *oap, Indenter how) "%" PRId64 " lines indented ", i), (int64_t)i); } - // set '[ and '] marks - curbuf->b_op_start = oap->start; - curbuf->b_op_end = oap->end; + if (!cmdmod.lockmarks) { + // set '[ and '] marks + curbuf->b_op_start = oap->start; + curbuf->b_op_end = oap->end; + } } /* @@ -915,10 +927,29 @@ int do_record(int c) apply_autocmds(EVENT_RECORDINGENTER, NULL, NULL, false, curbuf); } } else { // stop recording - // Get the recorded key hits. K_SPECIAL and CSI will be escaped, this + save_v_event_T save_v_event; + // Set the v:event dictionary with information about the recording. + dict_T *dict = get_v_event(&save_v_event); + + // The recorded text contents. + p = get_recorded(); + if (p != NULL) { + // Remove escaping for K_SPECIAL in multi-byte chars. + vim_unescape_ks(p); + (void)tv_dict_add_str(dict, S_LEN("regcontents"), (const char *)p); + } + + // Name of requested register, or empty string for unnamed operation. + char buf[NUMBUFLEN+2]; + buf[0] = (char)regname; + buf[1] = NUL; + (void)tv_dict_add_str(dict, S_LEN("regname"), buf); + + // Get the recorded key hits. K_SPECIAL will be escaped, this // needs to be removed again to put it in a register. exec_reg then // adds the escaping back later. apply_autocmds(EVENT_RECORDINGLEAVE, NULL, NULL, false, curbuf); + restore_v_event(dict, &save_v_event); reg_recorded = reg_recording; reg_recording = 0; if (ui_has(kUIMessages)) { @@ -926,13 +957,9 @@ int do_record(int c) } else { msg(""); } - p = get_recorded(); if (p == NULL) { retval = FAIL; } else { - // Remove escaping for CSI and K_SPECIAL in multi-byte chars. - vim_unescape_csi(p); - // We don't want to change the default register here, so save and // restore the current register name. old_y_previous = y_previous; @@ -1084,7 +1111,7 @@ int do_execreg(int regname, int colon, int addcr, int silent) return FAIL; } } - escaped = vim_strsave_escape_csi(reg->y_array[i]); + escaped = vim_strsave_escape_ks(reg->y_array[i]); retval = ins_typebuf(escaped, remap, 0, true, silent); xfree(escaped); if (retval == FAIL) { @@ -1126,7 +1153,7 @@ static void put_reedit_in_typebuf(int silent) /// Insert register contents "s" into the typeahead buffer, so that it will be /// executed again. /// -/// @param esc when true then it is to be taken literally: Escape CSI +/// @param esc when true then it is to be taken literally: Escape K_SPECIAL /// characters and no remapping. /// @param colon add ':' before the line static int put_in_typebuf(char_u *s, bool esc, bool colon, int silent) @@ -1141,7 +1168,7 @@ static int put_in_typebuf(char_u *s, bool esc, bool colon, int silent) char_u *p; if (esc) { - p = vim_strsave_escape_csi(s); + p = vim_strsave_escape_ks(s); } else { p = s; } @@ -1420,6 +1447,11 @@ int op_delete(oparg_T *oap) return FAIL; } + if (VIsual_select && oap->is_VIsual) { + // Use the register given with CTRL_R, defaults to zero + oap->regname = VIsual_select_reg; + } + mb_adjust_opend(oap); /* @@ -1716,13 +1748,15 @@ int op_delete(oparg_T *oap) msgmore(curbuf->b_ml.ml_line_count - old_lcount); setmarks: - if (oap->motion_type == kMTBlockWise) { - curbuf->b_op_end.lnum = oap->end.lnum; - curbuf->b_op_end.col = oap->start.col; - } else { - curbuf->b_op_end = oap->start; + if (!cmdmod.lockmarks) { + if (oap->motion_type == kMTBlockWise) { + curbuf->b_op_end.lnum = oap->end.lnum; + curbuf->b_op_end.col = oap->start.col; + } else { + curbuf->b_op_end = oap->start; + } + curbuf->b_op_start = oap->start; } - curbuf->b_op_start = oap->start; return OK; } @@ -1927,11 +1961,14 @@ static int op_replace(oparg_T *oap, int c) while (ltoreq(curwin->w_cursor, oap->end)) { n = gchar_cursor(); if (n != NUL) { - if (utf_char2len(c) > 1 || utf_char2len(n) > 1) { + int new_byte_len = utf_char2len(c); + int old_byte_len = utfc_ptr2len(get_cursor_pos_ptr()); + + if (new_byte_len > 1 || old_byte_len > 1) { // This is slow, but it handles replacing a single-byte // with a multi-byte and the other way around. if (curwin->w_cursor.lnum == oap->end.lnum) { - oap->end.col += utf_char2len(c) - utf_char2len(n); + oap->end.col += new_byte_len - old_byte_len; } replace_character(c); } else { @@ -1987,9 +2024,11 @@ static int op_replace(oparg_T *oap, int c) check_cursor(); changed_lines(oap->start.lnum, oap->start.col, oap->end.lnum + 1, 0L, true); - // Set "'[" and "']" marks. - curbuf->b_op_start = oap->start; - curbuf->b_op_end = oap->end; + if (!cmdmod.lockmarks) { + // Set "'[" and "']" marks. + curbuf->b_op_start = oap->start; + curbuf->b_op_end = oap->end; + } return OK; } @@ -2058,11 +2097,11 @@ void op_tilde(oparg_T *oap) redraw_curbuf_later(INVERTED); } - /* - * Set '[ and '] marks. - */ - curbuf->b_op_start = oap->start; - curbuf->b_op_end = oap->end; + if (!cmdmod.lockmarks) { + // Set '[ and '] marks. + curbuf->b_op_start = oap->start; + curbuf->b_op_end = oap->end; + } if (oap->line_count > p_report) { smsg(NGETTEXT("%" PRId64 " line changed", @@ -2181,19 +2220,22 @@ void op_insert(oparg_T *oap, long count1) // doing block_prep(). When only "block" is used, virtual edit is // already disabled, but still need it when calling // coladvance_force(). + // coladvance_force() uses get_ve_flags() to get the 'virtualedit' + // state for the current window. To override that state, we need to + // set the window-local value of ve_flags rather than the global value. if (curwin->w_cursor.coladd > 0) { - unsigned old_ve_flags = ve_flags; + unsigned old_ve_flags = curwin->w_ve_flags; - ve_flags = VE_ALL; if (u_save_cursor() == FAIL) { return; } + curwin->w_ve_flags = VE_ALL; coladvance_force(oap->op_type == OP_APPEND ? oap->end_vcol + 1 : getviscol()); if (oap->op_type == OP_APPEND) { --curwin->w_cursor.col; } - ve_flags = old_ve_flags; + curwin->w_ve_flags = old_ve_flags; } // Get the info about the block before entering the text block_prep(oap, &bd, oap->start.lnum, true); @@ -2241,6 +2283,7 @@ void op_insert(oparg_T *oap, long count1) } t1 = oap->start; + const pos_T start_insert = curwin->w_cursor; (void)edit(NUL, false, (linenr_T)count1); // When a tab was inserted, and the characters in front of the tab @@ -2275,23 +2318,18 @@ void op_insert(oparg_T *oap, long count1) // The user may have moved the cursor before inserting something, try // to adjust the block for that. But only do it, if the difference // does not come from indent kicking in. - if (oap->start.lnum == curbuf->b_op_start_orig.lnum - && !bd.is_MAX - && !did_indent) { + if (oap->start.lnum == curbuf->b_op_start_orig.lnum && !bd.is_MAX && !did_indent) { + const int t = getviscol2(curbuf->b_op_start_orig.col, curbuf->b_op_start_orig.coladd); + if (oap->op_type == OP_INSERT && oap->start.col + oap->start.coladd != curbuf->b_op_start_orig.col + curbuf->b_op_start_orig.coladd) { - int t = getviscol2(curbuf->b_op_start_orig.col, - curbuf->b_op_start_orig.coladd); oap->start.col = curbuf->b_op_start_orig.col; pre_textlen -= t - oap->start_vcol; oap->start_vcol = t; } else if (oap->op_type == OP_APPEND - && oap->end.col + oap->end.coladd - >= curbuf->b_op_start_orig.col - + curbuf->b_op_start_orig.coladd) { - int t = getviscol2(curbuf->b_op_start_orig.col, - curbuf->b_op_start_orig.coladd); + && oap->start.col + oap->start.coladd + >= curbuf->b_op_start_orig.col + curbuf->b_op_start_orig.coladd) { oap->start.col = curbuf->b_op_start_orig.col; // reset pre_textlen to the value of OP_INSERT pre_textlen += bd.textlen; @@ -2339,15 +2377,27 @@ void op_insert(oparg_T *oap, long count1) firstline = ml_get(oap->start.lnum); const size_t len = STRLEN(firstline); colnr_T add = bd.textcol; + colnr_T offset = 0; // offset when cursor was moved in insert mode if (oap->op_type == OP_APPEND) { add += bd.textlen; + // account for pressing cursor in insert mode when '$' was used + if (bd.is_MAX && start_insert.lnum == Insstart.lnum && start_insert.col > Insstart.col) { + offset = start_insert.col - Insstart.col; + add -= offset; + if (oap->end_vcol > offset) { + oap->end_vcol -= offset + 1; + } else { + // moved outside of the visual block, what to do? + return; + } + } } if ((size_t)add > len) { firstline += len; // short line, point to the NUL } else { firstline += add; } - ins_len = (long)STRLEN(firstline) - pre_textlen; + ins_len = (long)STRLEN(firstline) - pre_textlen - offset; if (pre_textlen >= 0 && ins_len > 0) { ins_text = vim_strnsave(firstline, (size_t)ins_len); // block handled here @@ -2751,17 +2801,15 @@ static void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append) } } - /* - * Set "'[" and "']" marks. - */ - curbuf->b_op_start = oap->start; - curbuf->b_op_end = oap->end; - if (yank_type == kMTLineWise) { - curbuf->b_op_start.col = 0; - curbuf->b_op_end.col = MAXCOL; + if (!cmdmod.lockmarks) { + // Set "'[" and "']" marks. + curbuf->b_op_start = oap->start; + curbuf->b_op_end = oap->end; + if (yank_type == kMTLineWise) { + curbuf->b_op_start.col = 0; + curbuf->b_op_end.col = MAXCOL; + } } - - return; } // Copy a block range into a register. @@ -2786,7 +2834,7 @@ static void yank_copy_line(yankreg_T *reg, struct block_def *bd, size_t y_idx, if (exclude_trailing_space) { int s = bd->textlen + bd->endspaces; - while (ascii_iswhite(*(bd->textstart + s - 1)) && s > 0) { + while (s > 0 && ascii_iswhite(*(bd->textstart + s - 1))) { s = s - utf_head_off(bd->textstart, bd->textstart + s - 1) - 1; pnew--; } @@ -2891,6 +2939,9 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) char_u *insert_string = NULL; bool allocated = false; long cnt; + const pos_T orig_start = curbuf->b_op_start; + const pos_T orig_end = curbuf->b_op_end; + unsigned int cur_ve_flags = get_ve_flags(); if (flags & PUT_FIXINDENT) { orig_indent = get_indent(); @@ -2961,7 +3012,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) eol = (*(cursor_pos + utfc_ptr2len(cursor_pos)) == NUL); } - bool ve_allows = (ve_flags == VE_ALL || ve_flags == VE_ONEMORE); + bool ve_allows = (cur_ve_flags == VE_ALL || cur_ve_flags == VE_ONEMORE); bool eof = curbuf->b_ml.ml_line_count == curwin->w_cursor.lnum && one_past_line; if (ve_allows || !(eol || eof)) { @@ -3137,13 +3188,14 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) yanklen = (int)STRLEN(y_array[0]); - if (ve_flags == VE_ALL && y_type == kMTCharWise) { + if (cur_ve_flags == VE_ALL && y_type == kMTCharWise) { if (gchar_cursor() == TAB) { - /* Don't need to insert spaces when "p" on the last position of a - * tab or "P" on the first position. */ int viscol = getviscol(); + long ts = curbuf->b_p_ts; + // Don't need to insert spaces when "p" on the last position of a + // tab or "P" on the first position. if (dir == FORWARD - ? tabstop_padding(viscol, curbuf->b_p_ts, curbuf->b_p_vts_array) != 1 + ? tabstop_padding(viscol, ts, curbuf->b_p_vts_array) != 1 : curwin->w_cursor.coladd > 0) { coladvance_force(viscol); } else { @@ -3165,7 +3217,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) colnr_T endcol2 = 0; if (dir == FORWARD && c != NUL) { - if (ve_flags == VE_ALL) { + if (cur_ve_flags == VE_ALL) { getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2); } else { getvcol(curwin, &curwin->w_cursor, NULL, NULL, &col); @@ -3179,9 +3231,8 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) } col += curwin->w_cursor.coladd; - if (ve_flags == VE_ALL - && (curwin->w_cursor.coladd > 0 - || endcol2 == curwin->w_cursor.col)) { + if (cur_ve_flags == VE_ALL + && (curwin->w_cursor.coladd > 0 || endcol2 == curwin->w_cursor.col)) { if (dir == FORWARD && c == NUL) { col++; } @@ -3263,18 +3314,28 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) } } - // insert the new text + // Insert the new text. + // First check for multiplication overflow. + if (yanklen + spaces != 0 + && count > ((INT_MAX - (bd.startspaces + bd.endspaces)) / (yanklen + spaces))) { + emsg(_(e_resulting_text_too_long)); + break; + } + totlen = (size_t)(count * (yanklen + spaces) + bd.startspaces + bd.endspaces); int addcount = (int)totlen + lines_appended; newp = (char_u *)xmalloc(totlen + oldlen + 1); + // copy part up to cursor to new line ptr = newp; memmove(ptr, oldp, (size_t)bd.textcol); ptr += bd.textcol; + // may insert some spaces before the new text memset(ptr, ' ', (size_t)bd.startspaces); ptr += bd.startspaces; + // insert the new text for (long j = 0; j < count; j++) { memmove(ptr, y_array[i], (size_t)yanklen); @@ -3288,9 +3349,11 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) addcount -= spaces; } } + // may insert some spaces after the new text memset(ptr, ' ', (size_t)bd.endspaces); ptr += bd.endspaces; + // move the text after the cursor to the end of the line. int columns = (int)oldlen - bd.textcol - delcount + 1; assert(columns >= 0); @@ -3379,10 +3442,18 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) } } - do { + if (count == 0 || yanklen == 0) { + if (VIsual_active) { + lnum = end_lnum; + } + } else if (count > INT_MAX / yanklen) { + // multiplication overflow + emsg(_(e_resulting_text_too_long)); + } else { totlen = (size_t)(count * yanklen); - if (totlen > 0) { + do { oldp = ml_get(lnum); + oldlen = STRLEN(oldp); if (lnum > start_lnum) { pos_T pos = { .lnum = lnum, @@ -3393,11 +3464,11 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) col = MAXCOL; } } - if (VIsual_active && col > (int)STRLEN(oldp)) { + if (VIsual_active && col > (colnr_T)oldlen) { lnum++; continue; } - newp = (char_u *)xmalloc((size_t)(STRLEN(oldp) + totlen + 1)); + newp = (char_u *)xmalloc(totlen + oldlen + 1); memmove(newp, oldp, (size_t)col); ptr = newp + col; for (i = 0; i < (size_t)count; i++) { @@ -3419,14 +3490,14 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) changed_bytes(lnum, col); extmark_splice_cols(curbuf, (int)lnum-1, col, 0, (int)totlen, kExtmarkUndo); - } - if (VIsual_active) { - lnum++; - } - } while (VIsual_active && lnum <= end_lnum); + if (VIsual_active) { + lnum++; + } + } while (VIsual_active && lnum <= end_lnum); - if (VIsual_active) { // reset lnum to the last visual line - lnum--; + if (VIsual_active) { // reset lnum to the last visual line + lnum--; + } } // put '] at the first byte of the last character @@ -3440,6 +3511,9 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) curwin->w_cursor.col -= first_byte_off; } } else { + linenr_T new_lnum = new_cursor.lnum; + size_t len; + // Insert at least one line. When y_type is kMTCharWise, break the first // line in two. for (cnt = 1; cnt <= count; cnt++) { @@ -3456,6 +3530,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) STRCAT(newp, ptr); // insert second line ml_append(lnum, newp, (colnr_T)0, false); + new_lnum++; xfree(newp); oldp = ml_get(lnum); @@ -3471,10 +3546,11 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) } for (; i < y_size; i++) { - if ((y_type != kMTCharWise || i < y_size - 1) - && ml_append(lnum, y_array[i], (colnr_T)0, false) - == FAIL) { - goto error; + if ((y_type != kMTCharWise || i < y_size - 1)) { + if (ml_append(lnum, y_array[i], (colnr_T)0, false) == FAIL) { + goto error; + } + new_lnum++; } lnum++; ++nr_lines; @@ -3524,6 +3600,10 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) extmark_splice(curbuf, (int)new_cursor.lnum-1, col + 1, 0, 0, 0, (int)y_size+1, 0, totsize+2, kExtmarkUndo); } + + if (cnt == 1) { + new_lnum = lnum; + } } error: @@ -3551,11 +3631,12 @@ error: // Put the '] mark on the first byte of the last inserted character. // Correct the length for change in indent. - curbuf->b_op_end.lnum = lnum; - col = (colnr_T)STRLEN(y_array[y_size - 1]) - lendiff; + curbuf->b_op_end.lnum = new_lnum; + len = STRLEN(y_array[y_size - 1]); + col = (colnr_T)len - lendiff; if (col > 1) { curbuf->b_op_end.col = col - 1 - utf_head_off(y_array[y_size - 1], - y_array[y_size - 1] + col - 1); + y_array[y_size - 1] + len - 1); } else { curbuf->b_op_end.col = 0; } @@ -3574,8 +3655,12 @@ error: } curwin->w_cursor.col = 0; } else { - curwin->w_cursor.lnum = lnum; + curwin->w_cursor.lnum = new_lnum; curwin->w_cursor.col = col; + curbuf->b_op_end = curwin->w_cursor; + if (col > 1) { + curbuf->b_op_end.col = col - 1; + } } } else if (y_type == kMTLineWise) { // put cursor on first non-blank in first inserted line @@ -3594,6 +3679,10 @@ error: curwin->w_set_curswant = TRUE; end: + if (cmdmod.lockmarks) { + curbuf->b_op_start = orig_start; + curbuf->b_op_end = orig_end; + } if (allocated) { xfree(insert_string); } @@ -3613,14 +3702,16 @@ end: */ void adjust_cursor_eol(void) { + unsigned int cur_ve_flags = get_ve_flags(); + if (curwin->w_cursor.col > 0 && gchar_cursor() == NUL - && (ve_flags & VE_ONEMORE) == 0 + && (cur_ve_flags & VE_ONEMORE) == 0 && !(restart_edit || (State & INSERT))) { // Put the cursor on the last character in the line. dec_cursor(); - if (ve_flags == VE_ALL) { + if (cur_ve_flags == VE_ALL) { colnr_T scol, ecol; // Coladd is set to the width of the last character. @@ -3674,7 +3765,7 @@ void ex_display(exarg_T *eap) int name; char_u *arg = eap->arg; int clen; - char_u type[2]; + int type; if (arg != NULL && *arg == NUL) { arg = NULL; @@ -3687,11 +3778,11 @@ void ex_display(exarg_T *eap) name = get_register_name(i); switch (get_reg_type(name, NULL)) { case kMTLineWise: - type[0] = 'l'; break; + type = 'l'; break; case kMTCharWise: - type[0] = 'c'; break; + type = 'c'; break; default: - type[0] = 'b'; break; + type = 'b'; break; } if (arg != NULL && vim_strchr(arg, name) == NULL) { @@ -3718,88 +3809,87 @@ void ex_display(exarg_T *eap) } if (yb->y_array != NULL) { - msg_putchar('\n'); - msg_puts(" "); - msg_putchar(type[0]); - msg_puts(" "); - msg_putchar('"'); - msg_putchar(name); - msg_puts(" "); - - int n = Columns - 11; - for (size_t j = 0; j < yb->y_size && n > 1; j++) { - if (j) { - msg_puts_attr("^J", attr); - n -= 2; + bool do_show = false; + + for (size_t j = 0; !do_show && j < yb->y_size; j++) { + do_show = !message_filtered(yb->y_array[j]); + } + + if (do_show || yb->y_size == 0) { + msg_putchar('\n'); + msg_puts(" "); + msg_putchar(type); + msg_puts(" "); + msg_putchar('"'); + msg_putchar(name); + msg_puts(" "); + + int n = Columns - 11; + for (size_t j = 0; j < yb->y_size && n > 1; j++) { + if (j) { + msg_puts_attr("^J", attr); + n -= 2; + } + for (p = yb->y_array[j]; *p && (n -= ptr2cells(p)) >= 0; p++) { + clen = utfc_ptr2len(p); + msg_outtrans_len(p, clen); + p += clen - 1; + } } - for (p = yb->y_array[j]; *p && (n -= ptr2cells(p)) >= 0; p++) { // -V1019 NOLINT(whitespace/line_length) - clen = utfc_ptr2len(p); - msg_outtrans_len(p, clen); - p += clen - 1; + if (n > 1 && yb->y_type == kMTLineWise) { + msg_puts_attr("^J", attr); } + ui_flush(); // show one line at a time } - if (n > 1 && yb->y_type == kMTLineWise) { - msg_puts_attr("^J", attr); - } - ui_flush(); // show one line at a time + os_breakcheck(); } - os_breakcheck(); } - /* - * display last inserted text - */ + // display last inserted text if ((p = get_last_insert()) != NULL - && (arg == NULL || vim_strchr(arg, '.') != NULL) && !got_int) { + && (arg == NULL || vim_strchr(arg, '.') != NULL) && !got_int + && !message_filtered(p)) { msg_puts("\n c \". "); dis_msg(p, true); } - /* - * display last command line - */ + // display last command line if (last_cmdline != NULL && (arg == NULL || vim_strchr(arg, ':') != NULL) - && !got_int) { + && !got_int && !message_filtered(last_cmdline)) { msg_puts("\n c \": "); dis_msg(last_cmdline, false); } - /* - * display current file name - */ + // display current file name if (curbuf->b_fname != NULL - && (arg == NULL || vim_strchr(arg, '%') != NULL) && !got_int) { + && (arg == NULL || vim_strchr(arg, '%') != NULL) && !got_int + && !message_filtered(curbuf->b_fname)) { msg_puts("\n c \"% "); dis_msg(curbuf->b_fname, false); } - /* - * display alternate file name - */ + // display alternate file name if ((arg == NULL || vim_strchr(arg, '%') != NULL) && !got_int) { char_u *fname; linenr_T dummy; - if (buflist_name_nr(0, &fname, &dummy) != FAIL) { + if (buflist_name_nr(0, &fname, &dummy) != FAIL && !message_filtered(fname)) { msg_puts("\n c \"# "); dis_msg(fname, false); } } - /* - * display last search pattern - */ + // display last search pattern if (last_search_pat() != NULL - && (arg == NULL || vim_strchr(arg, '/') != NULL) && !got_int) { + && (arg == NULL || vim_strchr(arg, '/') != NULL) && !got_int + && !message_filtered(last_search_pat())) { msg_puts("\n c \"/ "); dis_msg(last_search_pat(), false); } - /* - * display last used expression - */ + // display last used expression if (expr_line != NULL && (arg == NULL || vim_strchr(arg, '=') != NULL) - && !got_int) { + && !got_int && !message_filtered(expr_line)) { msg_puts("\n c \"= "); dis_msg(expr_line, false); } @@ -3938,7 +4028,7 @@ int do_join(size_t count, int insert_space, int save_undo, int use_formatoptions // and setup the array of space strings lengths for (t = 0; t < (linenr_T)count; t++) { curr = curr_start = ml_get((linenr_T)(curwin->w_cursor.lnum + t)); - if (t == 0 && setmark) { + if (t == 0 && setmark && !cmdmod.lockmarks) { // Set the '[ mark. curwin->w_buffer->b_op_start.lnum = curwin->w_cursor.lnum; curwin->w_buffer->b_op_start.col = (colnr_T)STRLEN(curr); @@ -4059,7 +4149,7 @@ int do_join(size_t count, int insert_space, int save_undo, int use_formatoptions ml_replace(curwin->w_cursor.lnum, newp, false); - if (setmark) { + if (setmark && !cmdmod.lockmarks) { // Set the '] mark. curwin->w_buffer->b_op_end.lnum = curwin->w_cursor.lnum; curwin->w_buffer->b_op_end.col = sumsize; @@ -4123,7 +4213,7 @@ static int same_leader(linenr_T lnum, int leader1_len, char_u *leader1_flags, in * If first leader has 'f' flag, the lines can be joined only if the * second line does not have a leader. * If first leader has 'e' flag, the lines can never be joined. - * If fist leader has 's' flag, the lines can only be joined if there is + * If first leader has 's' flag, the lines can only be joined if there is * some text after it and the second line has the 'm' flag. */ if (leader1_flags != NULL) { @@ -4197,8 +4287,10 @@ static void op_format(oparg_T *oap, int keep_cursor) redraw_curbuf_later(INVERTED); } - // Set '[ mark at the start of the formatted area - curbuf->b_op_start = oap->start; + if (!cmdmod.lockmarks) { + // Set '[ mark at the start of the formatted area + curbuf->b_op_start = oap->start; + } // For "gw" remember the cursor position and put it back below (adjusted // for joined and split lines). @@ -4220,8 +4312,10 @@ static void op_format(oparg_T *oap, int keep_cursor) old_line_count = curbuf->b_ml.ml_line_count - old_line_count; msgmore(old_line_count); - // put '] mark on the end of the formatted area - curbuf->b_op_end = curwin->w_cursor; + if (!cmdmod.lockmarks) { + // put '] mark on the end of the formatted area + curbuf->b_op_end = curwin->w_cursor; + } if (keep_cursor) { curwin->w_cursor = saved_cursor; @@ -4308,7 +4402,7 @@ void format_lines(linenr_T line_count, int avoid_fex) int leader_len = 0; // leader len of current line int next_leader_len; // leader len of next line char_u *leader_flags = NULL; // flags for leader of current line - char_u *next_leader_flags; // flags for leader of next line + char_u *next_leader_flags = NULL; // flags for leader of next line bool advance = true; int second_indent = -1; // indent for second line (comment aware) bool first_par_line = true; @@ -4425,7 +4519,14 @@ void format_lines(linenr_T line_count, int avoid_fex) leader_len, leader_flags, next_leader_len, next_leader_flags)) { - is_end_par = true; + // Special case: If the next line starts with a line comment + // and this line has a line comment after some text, the + // paragraph doesn't really end. + if (next_leader_flags == NULL + || STRNCMP(next_leader_flags, "://", 3) != 0 + || check_linecomment(get_cursor_line_ptr()) == MAXCOL) { + is_end_par = true; + } } /* @@ -4819,7 +4920,7 @@ void op_addsub(oparg_T *oap, linenr_T Prenum1, bool g_cmd) // Set '[ mark if something changed. Keep the last end // position from do_addsub(). - if (change_cnt > 0) { + if (change_cnt > 0 && !cmdmod.lockmarks) { curbuf->b_op_start = startpos; } @@ -5173,11 +5274,13 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1) } } - // set the '[ and '] marks - curbuf->b_op_start = startpos; - curbuf->b_op_end = endpos; - if (curbuf->b_op_end.col > 0) { - curbuf->b_op_end.col--; + if (!cmdmod.lockmarks) { + // set the '[ and '] marks + curbuf->b_op_start = startpos; + curbuf->b_op_end = endpos; + if (curbuf->b_op_end.col > 0) { + curbuf->b_op_end.col--; + } } theend: @@ -5608,7 +5711,9 @@ static void str_to_reg(yankreg_T *y_ptr, MotionType yank_type, const char_u *str // When appending, copy the previous line and free it after. size_t extra = append ? STRLEN(pp[--lnum]) : 0; char_u *s = xmallocz(line_len + extra); - memcpy(s, pp[lnum], extra); + if (extra > 0) { + memcpy(s, pp[lnum], extra); + } memcpy(s + extra, start, line_len); size_t s_len = extra + line_len; @@ -5990,6 +6095,8 @@ static void op_function(const oparg_T *oap) { const TriState save_virtual_op = virtual_op; const bool save_finish_op = finish_op; + const pos_T orig_start = curbuf->b_op_start; + const pos_T orig_end = curbuf->b_op_end; if (*p_opfunc == NUL) { emsg(_("E774: 'operatorfunc' is empty")); @@ -6023,6 +6130,10 @@ static void op_function(const oparg_T *oap) virtual_op = save_virtual_op; finish_op = save_finish_op; + if (cmdmod.lockmarks) { + curbuf->b_op_start = orig_start; + curbuf->b_op_end = orig_end; + } } } diff --git a/src/nvim/option.c b/src/nvim/option.c index 65adda3c01..9068c90dd1 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -281,7 +281,7 @@ typedef struct vimoption { # include "options.generated.h" #endif -#define PARAM_COUNT ARRAY_SIZE(options) +#define OPTION_COUNT ARRAY_SIZE(options) static char *(p_ambw_values[]) = { "single", "double", NULL }; static char *(p_bg_values[]) = { "light", "dark", NULL }; @@ -931,6 +931,21 @@ void set_title_defaults(void) } } +void ex_set(exarg_T *eap) +{ + int flags = 0; + + if (eap->cmdidx == CMD_setlocal) { + flags = OPT_LOCAL; + } else if (eap->cmdidx == CMD_setglobal) { + flags = OPT_GLOBAL; + } + if (eap->forceit) { + flags |= OPT_ONECOLUMN; + } + (void)do_set(eap->arg, flags); +} + /// Parse 'arg' for option settings. /// /// 'arg' may be IObuff, but only when no errors can be present and option @@ -1345,7 +1360,7 @@ int do_set(char_u *arg, int opt_flags) if (nextchar == '&') { // set to default val newval = options[opt_idx].def_val; - // expand environment variables and ~ (since the + // expand environment variables and ~ since the // default value was already expanded, only // required when an environment variable was set // later @@ -1766,7 +1781,7 @@ static char *illegal_char(char *errbuf, size_t errbuflen, int c) if (errbuf == NULL) { return ""; } - vim_snprintf((char *)errbuf, errbuflen, _("E539: Illegal character <%s>"), + vim_snprintf(errbuf, errbuflen, _("E539: Illegal character <%s>"), (char *)transchar(c)); return errbuf; } @@ -1969,10 +1984,9 @@ static void didset_options(void) (void)did_set_spell_option(true); // set cedit_key (void)check_cedit(); - briopt_check(curwin); // initialize the table for 'breakat'. fill_breakat_flags(); - fill_culopt_flags(NULL, curwin); + didset_window_options(curwin); } // More side effects of setting options. @@ -1993,9 +2007,9 @@ static void didset_options2(void) // Parse default for 'wildmode'. check_opt_wim(); xfree(curbuf->b_p_vsts_array); - tabstop_set(curbuf->b_p_vsts, &curbuf->b_p_vsts_array); + (void)tabstop_set(curbuf->b_p_vsts, &curbuf->b_p_vsts_array); xfree(curbuf->b_p_vts_array); - tabstop_set(curbuf->b_p_vts, &curbuf->b_p_vts_array); + (void)tabstop_set(curbuf->b_p_vts, &curbuf->b_p_vts_array); } /// Check for string options that are NULL (normally only termcap options). @@ -2269,12 +2283,12 @@ static char *set_string_option(const int opt_idx, const char *const value, const *varp = s; char *const saved_oldval = xstrdup(oldval); - char *const saved_oldval_l = (oldval_l != NULL) ? xstrdup((char *)oldval_l) : 0; - char *const saved_oldval_g = (oldval_g != NULL) ? xstrdup((char *)oldval_g) : 0; + char *const saved_oldval_l = (oldval_l != NULL) ? xstrdup(oldval_l) : 0; + char *const saved_oldval_g = (oldval_g != NULL) ? xstrdup(oldval_g) : 0; char *const saved_newval = xstrdup(s); int value_checked = false; - char *const r = did_set_string_option(opt_idx, (char_u **)varp, (int)true, + char *const r = did_set_string_option(opt_idx, (char_u **)varp, true, (char_u *)oldval, NULL, 0, opt_flags, &value_checked); if (r == NULL) { @@ -2762,7 +2776,7 @@ ambw_end: if (!ascii_isdigit(*(s - 1))) { if (errbuf != NULL) { - vim_snprintf((char *)errbuf, errbuflen, + vim_snprintf(errbuf, errbuflen, _("E526: Missing number after <%s>"), transchar_byte(*(s - 1))); errmsg = errbuf; @@ -2953,7 +2967,7 @@ ambw_end: } } else { if (errbuf != NULL) { - vim_snprintf((char *)errbuf, errbuflen, + vim_snprintf(errbuf, errbuflen, _("E535: Illegal character after <%c>"), *--s); errmsg = errbuf; @@ -3084,14 +3098,27 @@ ambw_end: if (foldmethodIsIndent(curwin)) { foldUpdateAll(curwin); } - } else if (varp == &p_ve) { // 'virtualedit' - if (opt_strings_flags(p_ve, p_ve_values, &ve_flags, true) != OK) { - errmsg = e_invarg; - } else if (STRCMP(p_ve, oldval) != 0) { - // Recompute cursor position in case the new 've' setting - // changes something. - validate_virtcol(); - coladvance(curwin->w_virtcol); + } else if (gvarp == &p_ve) { // 'virtualedit' + char_u *ve = p_ve; + unsigned int *flags = &ve_flags; + + if (opt_flags & OPT_LOCAL) { + ve = curwin->w_p_ve; + flags = &curwin->w_ve_flags; + } + + if ((opt_flags & OPT_LOCAL) && *ve == NUL) { + // make the local value empty: use the global value + *flags = 0; + } else { + if (opt_strings_flags(ve, p_ve_values, flags, true) != OK) { + errmsg = e_invarg; + } else if (STRCMP(p_ve, oldval) != 0) { + // Recompute cursor position in case the new 've' setting + // changes something. + validate_virtcol(); + coladvance(curwin->w_virtcol); + } } } else if (varp == &p_csqf) { if (p_csqf != NULL) { @@ -3150,10 +3177,7 @@ ambw_end: char_u *cp; if (!(*varp)[0] || ((*varp)[0] == '0' && !(*varp)[1])) { - if (curbuf->b_p_vsts_array) { - xfree(curbuf->b_p_vsts_array); - curbuf->b_p_vsts_array = 0; - } + XFREE_CLEAR(curbuf->b_p_vsts_array); } else { for (cp = *varp; *cp; cp++) { if (ascii_isdigit(*cp)) { @@ -3178,10 +3202,7 @@ ambw_end: char_u *cp; if (!(*varp)[0] || ((*varp)[0] == '0' && !(*varp)[1])) { - if (curbuf->b_p_vts_array) { - xfree(curbuf->b_p_vts_array); - curbuf->b_p_vts_array = NULL; - } + XFREE_CLEAR(curbuf->b_p_vts_array); } else { for (cp = *varp; *cp; cp++) { if (ascii_isdigit(*cp)) { @@ -3857,6 +3878,7 @@ static void set_option_sctx_idx(int opt_idx, int opt_flags, sctx_T script_ctx) { int both = (opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0; int indir = (int)options[opt_idx].indir; + nlua_set_sctx(&script_ctx); const LastSet last_set = { .script_ctx = { script_ctx.sc_sid, @@ -4132,7 +4154,7 @@ static char *set_bool_option(const int opt_idx, char_u *const varp, const int va } } - // Arabic requires a utf-8 encoding, inform the user if its not + // Arabic requires a utf-8 encoding, inform the user if it's not // set. if (STRCMP(p_enc, "utf-8") != 0) { static char *w_arabic = N_("W17: Arabic requires UTF-8, do ':set encoding=utf-8'"); @@ -4256,7 +4278,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } // Save the global value before changing anything. This is needed as for - // a global-only option setting the "local value" infact sets the global + // a global-only option setting the "local value" in fact sets the global // value (since there is only one value). if ((opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0) { old_global_value = *(long *)get_varp_scope(&(options[opt_idx]), OPT_GLOBAL); @@ -4321,6 +4343,12 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } else if (value > 10000) { errmsg = e_invarg; } + } else if (pp == &p_pyx) { + if (value == 0) { + value = 3; + } else if (value != 3) { + errmsg = e_invarg; + } } else if (pp == &p_re) { if (value < 0 || value > 2) { errmsg = e_invarg; @@ -4386,6 +4414,8 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } else if (pp == &curbuf->b_p_ts || pp == &p_ts) { if (value < 1) { errmsg = e_positive; + } else if (value > TABSTOP_MAX) { + errmsg = e_invarg; } } else if (pp == &curbuf->b_p_tw || pp == &p_tw) { if (value < 0) { @@ -4399,7 +4429,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, // Don't change the value and return early if validation failed. if (errmsg != NULL) { - return (char *)errmsg; + return errmsg; } *pp = value; @@ -4495,10 +4525,6 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, if (pum_drawn()) { pum_redraw(); } - } else if (pp == &p_pyx) { - if (p_pyx != 0 && p_pyx != 2 && p_pyx != 3) { - errmsg = e_invarg; - } } else if (pp == &p_ul || pp == &curbuf->b_p_ul) { // sync undo before 'undolevels' changes // use the old value, otherwise u_sync() may not work properly @@ -4527,7 +4553,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, // Check the (new) bounds for Rows and Columns here. if (p_lines < min_rows() && full_screen) { if (errbuf != NULL) { - vim_snprintf((char *)errbuf, errbuflen, + vim_snprintf(errbuf, errbuflen, _("E593: Need at least %d lines"), min_rows()); errmsg = errbuf; } @@ -4535,7 +4561,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } if (p_columns < MIN_COLUMNS && full_screen) { if (errbuf != NULL) { - vim_snprintf((char *)errbuf, errbuflen, + vim_snprintf(errbuf, errbuflen, _("E594: Need at least %d columns"), MIN_COLUMNS); errmsg = errbuf; } @@ -4651,7 +4677,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, } check_redraw(options[opt_idx].flags); - return (char *)errmsg; + return errmsg; } /// Trigger the OptionSet autocommand. @@ -5184,7 +5210,7 @@ static void showoptions(int all, int opt_flags) #define INC 20 #define GAP 3 - vimoption_T **items = xmalloc(sizeof(vimoption_T *) * PARAM_COUNT); + vimoption_T **items = xmalloc(sizeof(vimoption_T *) * OPTION_COUNT); // Highlight title if (opt_flags & OPT_GLOBAL) { @@ -5198,6 +5224,7 @@ static void showoptions(int all, int opt_flags) // Do the loop two times: // 1. display the short items // 2. display the long items (only strings and numbers) + // When "opt_flags" has OPT_ONECOLUMN do everything in run 2. for (run = 1; run <= 2 && !got_int; run++) { // collect the items in items[] item_count = 0; @@ -5208,7 +5235,7 @@ static void showoptions(int all, int opt_flags) } varp = NULL; - if (opt_flags != 0) { + if ((opt_flags & (OPT_LOCAL | OPT_GLOBAL)) != 0) { if (p->indir != PV_NONE) { varp = get_varp_scope(p, opt_flags); } @@ -5217,8 +5244,10 @@ static void showoptions(int all, int opt_flags) } if (varp != NULL && (all == 1 || (all == 0 && !optval_default(p, varp)))) { - if (p->flags & P_BOOL) { - len = 1; // a toggle option fits always + if (opt_flags & OPT_ONECOLUMN) { + len = Columns; + } else if (p->flags & P_BOOL) { + len = 1; // a toggle option fits always } else { option_value2string(p, opt_flags); len = (int)STRLEN(p->fullname) + vim_strsize(NameBuff) + 1; @@ -5748,6 +5777,10 @@ void unset_global_local_option(char *name, void *from) set_chars_option((win_T *)from, &((win_T *)from)->w_p_fcs, true); redraw_later((win_T *)from, NOT_VALID); break; + case PV_VE: + clear_string_option(&((win_T *)from)->w_p_ve); + ((win_T *)from)->w_ve_flags = 0; + break; } } @@ -5814,6 +5847,8 @@ static char_u *get_varp_scope(vimoption_T *p, int opt_flags) return (char_u *)&(curwin->w_p_fcs); case PV_LCS: return (char_u *)&(curwin->w_p_lcs); + case PV_VE: + return (char_u *)&(curwin->w_p_ve); } return NULL; // "cannot happen" } @@ -5908,6 +5943,9 @@ static char_u *get_varp(vimoption_T *p) case PV_LCS: return *curwin->w_p_lcs != NUL ? (char_u *)&(curwin->w_p_lcs) : p->var; + case PV_VE: + return *curwin->w_p_ve != NUL + ? (char_u *)&curwin->w_p_ve : p->var; case PV_ARAB: return (char_u *)&(curwin->w_p_arab); @@ -6136,6 +6174,7 @@ void win_copy_options(win_T *wp_from, win_T *wp_to) { copy_winopt(&wp_from->w_onebuf_opt, &wp_to->w_onebuf_opt); copy_winopt(&wp_from->w_allbuf_opt, &wp_to->w_allbuf_opt); + didset_window_options(wp_to); } /// Copy the options from one winopt_T to another. @@ -6148,6 +6187,8 @@ void copy_winopt(winopt_T *from, winopt_T *to) to->wo_list = from->wo_list; to->wo_nu = from->wo_nu; to->wo_rnu = from->wo_rnu; + to->wo_ve = vim_strsave(from->wo_ve); + to->wo_ve_flags = from->wo_ve_flags; to->wo_nuw = from->wo_nuw; to->wo_rl = from->wo_rl; to->wo_rlc = vim_strsave(from->wo_rlc); @@ -6192,6 +6233,9 @@ void copy_winopt(winopt_T *from, winopt_T *to) to->wo_fcs = vim_strsave(from->wo_fcs); to->wo_lcs = vim_strsave(from->wo_lcs); to->wo_winbl = from->wo_winbl; + + // Copy the script context so that we know were the value was last set. + memmove(to->wo_script_ctx, from->wo_script_ctx, sizeof(to->wo_script_ctx)); check_winopt(to); // don't want NULL pointers } @@ -6224,6 +6268,7 @@ static void check_winopt(winopt_T *wop) check_string_option(&wop->wo_winhl); check_string_option(&wop->wo_fcs); check_string_option(&wop->wo_lcs); + check_string_option(&wop->wo_ve); } /// Free the allocated memory inside a winopt_T. @@ -6248,6 +6293,7 @@ void clear_winopt(winopt_T *wop) clear_string_option(&wop->wo_winhl); clear_string_option(&wop->wo_fcs); clear_string_option(&wop->wo_lcs); + clear_string_option(&wop->wo_ve); } void didset_window_options(win_T *wp) @@ -6262,11 +6308,30 @@ void didset_window_options(win_T *wp) wp->w_grid_alloc.blending = wp->w_p_winbl > 0; } +/// Index into the options table for a buffer-local option enum. +static int buf_opt_idx[BV_COUNT]; +#define COPY_OPT_SCTX(buf, bv) buf->b_p_script_ctx[bv] = options[buf_opt_idx[bv]].last_set + +/// Initialize buf_opt_idx[] if not done already. +static void init_buf_opt_idx(void) +{ + static int did_init_buf_opt_idx = false; + + if (did_init_buf_opt_idx) { + return; + } + did_init_buf_opt_idx = true; + for (int i = 0; options[i].fullname != NULL; i++) { + if (options[i].indir & PV_BUF) { + buf_opt_idx[options[i].indir & PV_MASK] = i; + } + } +} /// Copy global option values to local options for one buffer. /// Used when creating a new buffer and sometimes when entering a buffer. /// flags: -/// BCO_ENTER We will enter the buf buffer. +/// BCO_ENTER We will enter the buffer "buf". /// BCO_ALWAYS Always copy the options, but only set b_p_initialized when /// appropriate. /// BCO_NOHELP Don't copy the values to a help buffer. @@ -6302,11 +6367,12 @@ void buf_copy_options(buf_T *buf, int flags) } if (should_copy || (flags & BCO_ALWAYS)) { - /* Don't copy the options specific to a help buffer when - * BCO_NOHELP is given or the options were initialized already - * (jumping back to a help file with CTRL-T or CTRL-O) */ - dont_do_help = ((flags & BCO_NOHELP) && buf->b_help) - || buf->b_p_initialized; + memset(buf->b_p_script_ctx, 0, sizeof(buf->b_p_script_ctx)); + init_buf_opt_idx(); + // Don't copy the options specific to a help buffer when + // BCO_NOHELP is given or the options were initialized already + // (jumping back to a help file with CTRL-T or CTRL-O) + dont_do_help = ((flags & BCO_NOHELP) && buf->b_help) || buf->b_p_initialized; if (dont_do_help) { // don't free b_p_isk save_p_isk = buf->b_p_isk; buf->b_p_isk = NULL; @@ -6338,80 +6404,129 @@ void buf_copy_options(buf_T *buf, int flags) } buf->b_p_ai = p_ai; + COPY_OPT_SCTX(buf, BV_AI); buf->b_p_ai_nopaste = p_ai_nopaste; buf->b_p_sw = p_sw; + COPY_OPT_SCTX(buf, BV_SW); buf->b_p_scbk = p_scbk; + COPY_OPT_SCTX(buf, BV_SCBK); buf->b_p_tw = p_tw; + COPY_OPT_SCTX(buf, BV_TW); buf->b_p_tw_nopaste = p_tw_nopaste; buf->b_p_tw_nobin = p_tw_nobin; buf->b_p_wm = p_wm; + COPY_OPT_SCTX(buf, BV_WM); buf->b_p_wm_nopaste = p_wm_nopaste; buf->b_p_wm_nobin = p_wm_nobin; buf->b_p_bin = p_bin; + COPY_OPT_SCTX(buf, BV_BIN); buf->b_p_bomb = p_bomb; + COPY_OPT_SCTX(buf, BV_BOMB); buf->b_p_et = p_et; + COPY_OPT_SCTX(buf, BV_ET); buf->b_p_fixeol = p_fixeol; + COPY_OPT_SCTX(buf, BV_FIXEOL); buf->b_p_et_nobin = p_et_nobin; buf->b_p_et_nopaste = p_et_nopaste; buf->b_p_ml = p_ml; + COPY_OPT_SCTX(buf, BV_ML); buf->b_p_ml_nobin = p_ml_nobin; buf->b_p_inf = p_inf; - buf->b_p_swf = cmdmod.noswapfile ? false : p_swf; + COPY_OPT_SCTX(buf, BV_INF); + if (cmdmod.noswapfile) { + buf->b_p_swf = false; + } else { + buf->b_p_swf = p_swf; + COPY_OPT_SCTX(buf, BV_SWF); + } buf->b_p_cpt = vim_strsave(p_cpt); + COPY_OPT_SCTX(buf, BV_CPT); #ifdef BACKSLASH_IN_FILENAME buf->b_p_csl = vim_strsave(p_csl); + COPY_OPT_SCTX(buf, BV_CSL); #endif buf->b_p_cfu = vim_strsave(p_cfu); + COPY_OPT_SCTX(buf, BV_CFU); buf->b_p_ofu = vim_strsave(p_ofu); + COPY_OPT_SCTX(buf, BV_OFU); buf->b_p_tfu = vim_strsave(p_tfu); + COPY_OPT_SCTX(buf, BV_TFU); buf->b_p_sts = p_sts; + COPY_OPT_SCTX(buf, BV_STS); buf->b_p_sts_nopaste = p_sts_nopaste; buf->b_p_vsts = vim_strsave(p_vsts); + COPY_OPT_SCTX(buf, BV_VSTS); if (p_vsts && p_vsts != empty_option) { - tabstop_set(p_vsts, &buf->b_p_vsts_array); + (void)tabstop_set(p_vsts, &buf->b_p_vsts_array); } else { - buf->b_p_vsts_array = 0; + buf->b_p_vsts_array = NULL; } buf->b_p_vsts_nopaste = p_vsts_nopaste ? vim_strsave(p_vsts_nopaste) : NULL; buf->b_p_com = vim_strsave(p_com); + COPY_OPT_SCTX(buf, BV_COM); buf->b_p_cms = vim_strsave(p_cms); + COPY_OPT_SCTX(buf, BV_CMS); buf->b_p_fo = vim_strsave(p_fo); + COPY_OPT_SCTX(buf, BV_FO); buf->b_p_flp = vim_strsave(p_flp); + COPY_OPT_SCTX(buf, BV_FLP); buf->b_p_nf = vim_strsave(p_nf); + COPY_OPT_SCTX(buf, BV_NF); buf->b_p_mps = vim_strsave(p_mps); + COPY_OPT_SCTX(buf, BV_MPS); buf->b_p_si = p_si; + COPY_OPT_SCTX(buf, BV_SI); buf->b_p_channel = 0; buf->b_p_ci = p_ci; + COPY_OPT_SCTX(buf, BV_CI); buf->b_p_cin = p_cin; + COPY_OPT_SCTX(buf, BV_CIN); buf->b_p_cink = vim_strsave(p_cink); + COPY_OPT_SCTX(buf, BV_CINK); buf->b_p_cino = vim_strsave(p_cino); + COPY_OPT_SCTX(buf, BV_CINO); // Don't copy 'filetype', it must be detected buf->b_p_ft = empty_option; buf->b_p_pi = p_pi; + COPY_OPT_SCTX(buf, BV_PI); buf->b_p_cinw = vim_strsave(p_cinw); + COPY_OPT_SCTX(buf, BV_CINW); buf->b_p_lisp = p_lisp; + COPY_OPT_SCTX(buf, BV_LISP); // Don't copy 'syntax', it must be set buf->b_p_syn = empty_option; buf->b_p_smc = p_smc; + COPY_OPT_SCTX(buf, BV_SMC); buf->b_s.b_syn_isk = empty_option; buf->b_s.b_p_spc = vim_strsave(p_spc); + COPY_OPT_SCTX(buf, BV_SPC); (void)compile_cap_prog(&buf->b_s); buf->b_s.b_p_spf = vim_strsave(p_spf); + COPY_OPT_SCTX(buf, BV_SPF); buf->b_s.b_p_spl = vim_strsave(p_spl); + COPY_OPT_SCTX(buf, BV_SPL); buf->b_s.b_p_spo = vim_strsave(p_spo); + COPY_OPT_SCTX(buf, BV_SPO); buf->b_p_inde = vim_strsave(p_inde); + COPY_OPT_SCTX(buf, BV_INDE); buf->b_p_indk = vim_strsave(p_indk); + COPY_OPT_SCTX(buf, BV_INDK); buf->b_p_fp = empty_option; buf->b_p_fex = vim_strsave(p_fex); + COPY_OPT_SCTX(buf, BV_FEX); buf->b_p_sua = vim_strsave(p_sua); + COPY_OPT_SCTX(buf, BV_SUA); buf->b_p_keymap = vim_strsave(p_keymap); + COPY_OPT_SCTX(buf, BV_KMAP); buf->b_kmap_state |= KEYMAP_INIT; // This isn't really an option, but copying the langmap and IME // state from the current buffer is better than resetting it. buf->b_p_iminsert = p_iminsert; + COPY_OPT_SCTX(buf, BV_IMI); buf->b_p_imsearch = p_imsearch; + COPY_OPT_SCTX(buf, BV_IMS); // options that are normally global but also have a local value // are not copied, start using the global value @@ -6431,11 +6546,14 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_p_def = empty_option; buf->b_p_inc = empty_option; buf->b_p_inex = vim_strsave(p_inex); + COPY_OPT_SCTX(buf, BV_INEX); buf->b_p_dict = empty_option; buf->b_p_tsr = empty_option; buf->b_p_tsrfu = empty_option; buf->b_p_qe = vim_strsave(p_qe); + COPY_OPT_SCTX(buf, BV_QE); buf->b_p_udf = p_udf; + COPY_OPT_SCTX(buf, BV_UDF); buf->b_p_lw = empty_option; buf->b_p_menc = empty_option; @@ -6448,17 +6566,20 @@ void buf_copy_options(buf_T *buf, int flags) if (dont_do_help) { buf->b_p_isk = save_p_isk; if (p_vts && p_vts != empty_option && !buf->b_p_vts_array) { - tabstop_set(p_vts, &buf->b_p_vts_array); + (void)tabstop_set(p_vts, &buf->b_p_vts_array); } else { buf->b_p_vts_array = NULL; } } else { buf->b_p_isk = vim_strsave(p_isk); + COPY_OPT_SCTX(buf, BV_ISK); did_isk = true; buf->b_p_ts = p_ts; + COPY_OPT_SCTX(buf, BV_TS); buf->b_p_vts = vim_strsave(p_vts); + COPY_OPT_SCTX(buf, BV_VTS); if (p_vts && p_vts != empty_option && !buf->b_p_vts_array) { - tabstop_set(p_vts, &buf->b_p_vts_array); + (void)tabstop_set(p_vts, &buf->b_p_vts_array); } else { buf->b_p_vts_array = NULL; } @@ -6467,6 +6588,7 @@ void buf_copy_options(buf_T *buf, int flags) clear_string_option(&buf->b_p_bt); } buf->b_p_ma = p_ma; + COPY_OPT_SCTX(buf, BV_MA); } } @@ -7107,10 +7229,7 @@ static void paste_option_changed(void) free_string_option(buf->b_p_vsts); } buf->b_p_vsts = empty_option; - if (buf->b_p_vsts_array) { - xfree(buf->b_p_vsts_array); - } - buf->b_p_vsts_array = 0; + XFREE_CLEAR(buf->b_p_vsts_array); } // set global options @@ -7147,13 +7266,11 @@ static void paste_option_changed(void) buf->b_p_vsts = buf->b_p_vsts_nopaste ? vim_strsave(buf->b_p_vsts_nopaste) : empty_option; - if (buf->b_p_vsts_array) { - xfree(buf->b_p_vsts_array); - } + xfree(buf->b_p_vsts_array); if (buf->b_p_vsts && buf->b_p_vsts != empty_option) { - tabstop_set(buf->b_p_vsts, &buf->b_p_vsts_array); + (void)tabstop_set(buf->b_p_vsts, &buf->b_p_vsts_array); } else { - buf->b_p_vsts_array = 0; + buf->b_p_vsts_array = NULL; } } @@ -7469,6 +7586,7 @@ int check_ff_value(char_u *p) // Set the integer values corresponding to the string setting of 'vartabstop'. // "array" will be set, caller must free it if needed. +// Return false for an error. bool tabstop_set(char_u *var, long **array) { long valcount = 1; @@ -7488,7 +7606,7 @@ bool tabstop_set(char_u *var, long **array) if (cp != end) { emsg(_(e_positive)); } else { - emsg(_(e_invarg)); + semsg(_(e_invarg2), cp); } return false; } @@ -7501,7 +7619,7 @@ bool tabstop_set(char_u *var, long **array) valcount++; continue; } - emsg(_(e_invarg)); + semsg(_(e_invarg2), var); return false; } @@ -7510,7 +7628,15 @@ bool tabstop_set(char_u *var, long **array) t = 1; for (cp = var; *cp != NUL;) { - (*array)[t++] = atoi((char *)cp); + int n = atoi((char *)cp); + + // Catch negative values, overflow and ridiculous big values. + if (n <= 0 || n > TABSTOP_MAX) { + semsg(_(e_invarg2), cp); + XFREE_CLEAR(*array); + return false; + } + (*array)[t++] = n; while (*cp != NUL && *cp != ',') { cp++; } @@ -7815,6 +7941,12 @@ unsigned int get_bkc_value(buf_T *buf) return buf->b_bkc_flags ? buf->b_bkc_flags : bkc_flags; } +/// Get the local or global value of the 'virtualedit' flags. +unsigned int get_ve_flags(void) +{ + return (curwin->w_ve_flags ? curwin->w_ve_flags : ve_flags) & ~(VE_NONE | VE_NONEU); +} + /// Get the local or global value of 'showbreak'. /// /// @param win If not NULL, the window to get the local option from; global @@ -7994,7 +8126,6 @@ int win_signcol_count(win_T *wp) /// Return the number of requested sign columns, based on user / configuration. int win_signcol_configured(win_T *wp, int *is_fixed) { - int minimum = 0, maximum = 1, needed_signcols; const char *scl = (const char *)wp->w_p_scl; if (is_fixed) { @@ -8007,7 +8138,6 @@ int win_signcol_configured(win_T *wp, int *is_fixed) && (wp->w_p_nu || wp->w_p_rnu)))) { return 0; } - needed_signcols = buf_signcols(wp->w_buffer); // yes or yes if (!strncmp(scl, "yes:", 4)) { @@ -8023,6 +8153,8 @@ int win_signcol_configured(win_T *wp, int *is_fixed) *is_fixed = 0; } + int minimum = 0, maximum = 1; + if (!strncmp(scl, "auto:", 5)) { // Variable depending on a configuration maximum = scl[5] - '0'; @@ -8033,6 +8165,7 @@ int win_signcol_configured(win_T *wp, int *is_fixed) } } + int needed_signcols = buf_signcols(wp->w_buffer, maximum); int ret = MAX(minimum, MIN(maximum, needed_signcols)); assert(ret <= SIGN_SHOW_MAX); return ret; diff --git a/src/nvim/option.h b/src/nvim/option.h index f7dbaafeec..9321dd5454 100644 --- a/src/nvim/option.h +++ b/src/nvim/option.h @@ -13,16 +13,16 @@ /// When OPT_GLOBAL and OPT_LOCAL are both missing, set both local and global /// values, get local value. typedef enum { - OPT_FREE = 1, ///< Free old value if it was allocated. - OPT_GLOBAL = 2, ///< Use global value. - OPT_LOCAL = 4, ///< Use local value. - OPT_MODELINE = 8, ///< Option in modeline. - OPT_WINONLY = 16, ///< Only set window-local options. - OPT_NOWIN = 32, ///< Don’t set window-local options. - OPT_ONECOLUMN = 64, ///< list options one per line - OPT_NO_REDRAW = 128, ///< ignore redraw flags on option - OPT_SKIPRTP = 256, ///< "skiprtp" in 'sessionoptions' - OPT_CLEAR = 512, ///< Clear local value of an option. + OPT_FREE = 0x01, ///< Free old value if it was allocated. + OPT_GLOBAL = 0x02, ///< Use global value. + OPT_LOCAL = 0x04, ///< Use local value. + OPT_MODELINE = 0x08, ///< Option in modeline. + OPT_WINONLY = 0x10, ///< Only set window-local options. + OPT_NOWIN = 0x20, ///< Don’t set window-local options. + OPT_ONECOLUMN = 0x40, ///< list options one per line + OPT_NO_REDRAW = 0x80, ///< ignore redraw flags on option + OPT_SKIPRTP = 0x100, ///< "skiprtp" in 'sessionoptions' + OPT_CLEAR = 0x200, ///< Clear local value of an option. } OptionFlags; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 09c3bf3800..d88cd6b9b9 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -705,12 +705,14 @@ EXTERN int p_vb; ///< 'visualbell' EXTERN char_u *p_ve; ///< 'virtualedit' EXTERN unsigned ve_flags; #ifdef IN_OPTION_C -static char *(p_ve_values[]) = { "block", "insert", "all", "onemore", NULL }; +static char *(p_ve_values[]) = { "block", "insert", "all", "onemore", "none", "NONE", NULL }; #endif -#define VE_BLOCK 5 // includes "all" -#define VE_INSERT 6 // includes "all" -#define VE_ALL 4 -#define VE_ONEMORE 8 +#define VE_BLOCK 5U // includes "all" +#define VE_INSERT 6U // includes "all" +#define VE_ALL 4U +#define VE_ONEMORE 8U +#define VE_NONE 16U // "none" +#define VE_NONEU 32U // "NONE" EXTERN long p_verbose; // 'verbose' #ifdef IN_OPTION_C char_u *p_vfile = (char_u *)""; // used before options are initialized @@ -869,6 +871,7 @@ enum { WV_LBR, WV_NU, WV_RNU, + WV_VE, WV_NUW, WV_PVW, WV_RL, @@ -900,6 +903,8 @@ enum { #define SB_MAX 100000 // Maximum 'scrollback' value. +#define TABSTOP_MAX 9999 + /// Stores an identifier of a script or channel that last set an option. typedef struct { sctx_T script_ctx; /// script context where the option was last set diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 28b4eb9fe2..e665ffd346 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -411,7 +411,7 @@ return { }, { full_name='compatible', abbreviation='cp', - short_desc=N_("No description"), + short_desc=N_("No description"), type='bool', scope={'global'}, redraw={'all_windows'}, varname='p_force_off', @@ -665,14 +665,14 @@ return { }, { full_name='edcompatible', abbreviation='ed', - short_desc=N_("No description"), + short_desc=N_("No description"), type='bool', scope={'global'}, varname='p_force_off', defaults={if_true=false} }, { full_name='emoji', abbreviation='emo', - short_desc=N_("No description"), + short_desc=N_("No description"), type='bool', scope={'global'}, redraw={'all_windows', 'ui_option'}, varname='p_emoji', @@ -1184,7 +1184,7 @@ return { }, { full_name='inccommand', abbreviation='icm', - short_desc=N_("Live preview of substitution"), + short_desc=N_("Live preview of substitution"), type='string', scope={'global'}, redraw={'all_windows'}, varname='p_icm', @@ -1846,7 +1846,7 @@ return { type='number', scope={'global'}, secure=true, varname='p_pyx', - defaults={if_true=0} + defaults={if_true=3} }, { full_name='quickfixtextfunc', abbreviation='qftf', @@ -2499,7 +2499,7 @@ return { }, { full_name='termencoding', abbreviation='tenc', - short_desc=N_("Terminal encodig"), + short_desc=N_("Terminal encoding"), type='string', scope={'global'}, defaults={if_true=""} }, @@ -2622,7 +2622,7 @@ return { }, { full_name='ttyfast', abbreviation='tf', - short_desc=N_("No description"), + short_desc=N_("No description"), type='bool', scope={'global'}, no_mkrc=true, varname='p_force_on', @@ -2736,7 +2736,7 @@ return { { full_name='virtualedit', abbreviation='ve', short_desc=N_("when to use virtual editing"), - type='string', list='onecomma', scope={'global'}, + type='string', list='onecomma', scope={'global', 'window'}, deny_duplicates=true, redraw={'curswant'}, varname='p_ve', diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index e9f44d2775..e9868d6b61 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -1111,10 +1111,9 @@ size_t home_replace(const buf_T *const buf, const char_u *src, char_u *const dst *dst_p++ = '~'; } - // If it's just the home directory, add "/". - if (!vim_ispathsep(src[0]) && --dstlen > 0) { - *dst_p++ = '/'; - } + // Do not add directory separator into dst, because dst is + // expected to just return the directory name without the + // directory separator '/'. break; } if (p == homedir_env_mod) { diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 24c7678633..daf974ee74 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -40,8 +40,10 @@ bool did_try_to_free = false; \ uv_call_start: {} \ uv_fs_t req; \ + fs_loop_lock(); \ ret = func(&fs_loop, &req, __VA_ARGS__); \ uv_fs_req_cleanup(&req); \ + fs_loop_unlock(); \ if (ret == UV_ENOMEM && !did_try_to_free) { \ try_to_free_memory(); \ did_try_to_free = true; \ @@ -52,12 +54,27 @@ uv_call_start: {} \ // Many fs functions from libuv return that value on success. static const int kLibuvSuccess = 0; static uv_loop_t fs_loop; +static uv_mutex_t fs_loop_mutex; // Initialize the fs module void fs_init(void) { uv_loop_init(&fs_loop); + uv_mutex_init_recursive(&fs_loop_mutex); +} + +/// TODO(bfredl): some of these operations should +/// be possible to do the private libuv loop of the +/// thread, instead of contending the global fs loop +void fs_loop_lock(void) +{ + uv_mutex_lock(&fs_loop_mutex); +} + +void fs_loop_unlock(void) +{ + uv_mutex_unlock(&fs_loop_mutex); } @@ -98,9 +115,12 @@ bool os_isrealdir(const char *name) FUNC_ATTR_NONNULL_ALL { uv_fs_t request; + fs_loop_lock(); if (uv_fs_lstat(&fs_loop, &request, name, NULL) != kLibuvSuccess) { + fs_loop_unlock(); return false; } + fs_loop_unlock(); if (S_ISLNK(request.statbuf.st_mode)) { return false; } else { @@ -738,7 +758,9 @@ static int os_stat(const char *name, uv_stat_t *statbuf) return UV_EINVAL; } uv_fs_t request; + fs_loop_lock(); int result = uv_fs_stat(&fs_loop, &request, name, NULL); + fs_loop_unlock(); if (result == kLibuvSuccess) { *statbuf = request.statbuf; } @@ -935,9 +957,11 @@ int os_mkdtemp(const char *template, char *path) FUNC_ATTR_NONNULL_ALL { uv_fs_t request; + fs_loop_lock(); int result = uv_fs_mkdtemp(&fs_loop, &request, template, NULL); + fs_loop_unlock(); if (result == kLibuvSuccess) { - STRNCPY(path, request.path, TEMP_FILE_PATH_MAXLEN); + xstrlcpy(path, request.path, TEMP_FILE_PATH_MAXLEN); } uv_fs_req_cleanup(&request); return result; @@ -962,7 +986,9 @@ int os_rmdir(const char *path) bool os_scandir(Directory *dir, const char *path) FUNC_ATTR_NONNULL_ALL { + fs_loop_lock(); int r = uv_fs_scandir(&fs_loop, &dir->request, path, 0, NULL); + fs_loop_unlock(); if (r < 0) { os_closedir(dir); } @@ -1023,7 +1049,9 @@ bool os_fileinfo_link(const char *path, FileInfo *file_info) return false; } uv_fs_t request; + fs_loop_lock(); bool ok = uv_fs_lstat(&fs_loop, &request, path, NULL) == kLibuvSuccess; + fs_loop_unlock(); if (ok) { file_info->stat = request.statbuf; } @@ -1041,6 +1069,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info) { uv_fs_t request; memset(file_info, 0, sizeof(*file_info)); + fs_loop_lock(); bool ok = uv_fs_fstat(&fs_loop, &request, file_descriptor, @@ -1049,6 +1078,7 @@ bool os_fileinfo_fd(int file_descriptor, FileInfo *file_info) file_info->stat = request.statbuf; } uv_fs_req_cleanup(&request); + fs_loop_unlock(); return ok; } @@ -1165,6 +1195,7 @@ char *os_realpath(const char *name, char *buf) FUNC_ATTR_NONNULL_ARG(1) { uv_fs_t request; + fs_loop_lock(); int result = uv_fs_realpath(&fs_loop, &request, name, NULL); if (result == kLibuvSuccess) { if (buf == NULL) { @@ -1173,6 +1204,7 @@ char *os_realpath(const char *name, char *buf) xstrlcpy(buf, request.ptr, MAXPATHL + 1); } uv_fs_req_cleanup(&request); + fs_loop_unlock(); return result == kLibuvSuccess ? buf : NULL; } diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 3790eba212..54cfaee80a 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -234,9 +234,9 @@ size_t input_enqueue(String keys) while (rbuffer_space(input_buffer) >= 19 && ptr < end) { // A "<x>" form occupies at least 1 characters, and produces up // to 19 characters (1 + 5 * 3 for the char and 3 for a modifier). - // In the case of K_SPECIAL(0x80) or CSI(0x9B), 3 bytes are escaped and - // needed, but since the keys are UTF-8, so the first byte cannot be - // K_SPECIAL(0x80) or CSI(0x9B). + // In the case of K_SPECIAL(0x80), 3 bytes are escaped and needed, + // but since the keys are UTF-8, so the first byte cannot be + // K_SPECIAL(0x80). uint8_t buf[19] = { 0 }; unsigned int new_size = trans_special((const uint8_t **)&ptr, (size_t)(end - ptr), buf, true, @@ -263,12 +263,8 @@ size_t input_enqueue(String keys) continue; } - // copy the character, escaping CSI and K_SPECIAL - if ((uint8_t)*ptr == CSI) { - rbuffer_write(input_buffer, (char *)&(uint8_t){ K_SPECIAL }, 1); - rbuffer_write(input_buffer, (char *)&(uint8_t){ KS_EXTRA }, 1); - rbuffer_write(input_buffer, (char *)&(uint8_t){ KE_CSI }, 1); - } else if ((uint8_t)*ptr == K_SPECIAL) { + // copy the character, escaping K_SPECIAL + if ((uint8_t)(*ptr) == K_SPECIAL) { rbuffer_write(input_buffer, (char *)&(uint8_t){ K_SPECIAL }, 1); rbuffer_write(input_buffer, (char *)&(uint8_t){ KS_SPECIAL }, 1); rbuffer_write(input_buffer, (char *)&(uint8_t){ KE_FILLER }, 1); diff --git a/src/nvim/path.c b/src/nvim/path.c index 674d67e21a..d3aa5e5bf2 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -269,16 +269,17 @@ int vim_ispathlistsep(int c) #endif } -/* - * Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" - * It's done in-place. - */ -char_u *shorten_dir(char_u *str) +/// Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" +/// "trim_len" specifies how many characters to keep for each directory. +/// Must be 1 or more. +/// It's done in-place. +void shorten_dir_len(char_u *str, int trim_len) { char_u *tail = path_tail(str); char_u *d = str; bool skip = false; - for (char_u *s = str;; ++s) { + int dirchunk_len = 0; + for (char_u *s = str;; s++) { if (s >= tail) { // copy the whole tail *d++ = *s; if (*s == NUL) { @@ -287,10 +288,16 @@ char_u *shorten_dir(char_u *str) } else if (vim_ispathsep(*s)) { // copy '/' and next char *d++ = *s; skip = false; + dirchunk_len = 0; } else if (!skip) { *d++ = *s; // copy next char if (*s != '~' && *s != '.') { // and leading "~" and "." - skip = true; + dirchunk_len++; // only count word chars for the size + // keep copying chars until we have our preferred length (or + // until the above if/else branches move us along) + if (dirchunk_len >= trim_len) { + skip = true; + } } int l = utfc_ptr2len(s); while (--l > 0) { @@ -298,7 +305,13 @@ char_u *shorten_dir(char_u *str) } } } - return str; +} + +/// Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" +/// It's done in-place. +void shorten_dir(char_u *str) +{ + shorten_dir_len(str, 1); } /* @@ -629,8 +642,7 @@ static size_t do_path_expand(garray_T *gap, const char_u *path, size_t wildoff, } else if (path_end >= path + wildoff && (vim_strchr((char_u *)"*?[{~$", *path_end) != NULL #ifndef WIN32 - || (!p_fic && (flags & EW_ICASE) - && isalpha(utf_ptr2char(path_end))) + || (!p_fic && (flags & EW_ICASE) && mb_isalpha(utf_ptr2char(path_end))) #endif )) { e = p; @@ -1508,7 +1520,7 @@ void simplify_filename(char_u *filename) p = filename; #ifdef BACKSLASH_IN_FILENAME - if (p[1] == ':') { // skip "x:" + if (p[0] != NUL && p[1] == ':') { // skip "x:" p += 2; } #endif @@ -1682,6 +1694,10 @@ char_u *find_file_name_in_path(char_u *ptr, size_t len, int options, long count, char_u *file_name; char_u *tofree = NULL; + if (len == 0) { + return NULL; + } + if ((options & FNAME_INCL) && *curbuf->b_p_inex != NUL) { tofree = (char_u *)eval_includeexpr((char *)ptr, len); if (tofree != NULL) { @@ -1743,14 +1759,32 @@ int path_is_url(const char *p) return 0; } -/// Check if "fname" starts with "name://". Return URL_SLASH if it does. +/// Check if "fname" starts with "name://" or "name:\\". /// /// @param fname is the filename to test -/// @return URL_BACKSLASH for "name:\\", zero otherwise. +/// @return URL_SLASH for "name://", URL_BACKSLASH for "name:\\", zero otherwise. int path_with_url(const char *fname) { const char *p; - for (p = fname; isalpha(*p); p++) {} + + // We accept alphabetic characters and a dash in scheme part. + // RFC 3986 allows for more, but it increases the risk of matching + // non-URL text. + + // first character must be alpha + if (!isalpha(*fname)) { + return 0; + } + + // check body: alpha or dash + for (p = fname; (isalpha(*p) || (*p == '-')); p++) {} + + // check last char is not a dash + if (p[-1] == '-') { + return 0; + } + + // "://" or ":\\" must follow return path_is_url(p); } @@ -2367,9 +2401,11 @@ static int path_to_absolute(const char_u *fname, char_u *buf, size_t len, int fo int path_is_absolute(const char_u *fname) { #ifdef WIN32 + if (*fname == NUL) { + return false; + } // A name like "d:/foo" and "//server/share" is absolute - return ((isalpha(fname[0]) && fname[1] == ':' - && vim_ispathsep_nocolon(fname[2])) + return ((isalpha(fname[0]) && fname[1] == ':' && vim_ispathsep_nocolon(fname[2])) || (vim_ispathsep_nocolon(fname[0]) && fname[0] == fname[1])); #else // UNIX: This just checks if the file name starts with '/' or '~'. diff --git a/src/nvim/plines.c b/src/nvim/plines.c index a061f76f34..a572f747df 100644 --- a/src/nvim/plines.c +++ b/src/nvim/plines.c @@ -83,7 +83,7 @@ int plines_win_nofill(win_T *wp, linenr_T lnum, bool winheight) return 1; } - // A folded lines is handled just like an empty line. + // Folded lines are handled just like an empty line. if (lineFolded(wp, lnum)) { return 1; } diff --git a/src/nvim/po/ja.euc-jp.po b/src/nvim/po/ja.euc-jp.po index 5dda7c59f5..9633bec9f2 100644 --- a/src/nvim/po/ja.euc-jp.po +++ b/src/nvim/po/ja.euc-jp.po @@ -1644,15 +1644,6 @@ msgstr " [w]" msgid " written" msgstr " " -msgid "E205: Patchmode: can't save original file" -msgstr "E205: patchmode: ܥե¸Ǥޤ" - -msgid "E206: patchmode: can't touch empty original file" -msgstr "E206: patchmode: θܥեtouchǤޤ" - -msgid "E207: Can't delete backup file" -msgstr "E207: Хååץեäޤ" - msgid "" "\n" "WARNING: Original file may be lost or damaged\n" diff --git a/src/nvim/po/ja.po b/src/nvim/po/ja.po index a169bd3589..c363c00fa6 100644 --- a/src/nvim/po/ja.po +++ b/src/nvim/po/ja.po @@ -1644,15 +1644,6 @@ msgstr " [w]" msgid " written" msgstr " 書込み" -msgid "E205: Patchmode: can't save original file" -msgstr "E205: patchmode: 原本ファイルを保存できません" - -msgid "E206: patchmode: can't touch empty original file" -msgstr "E206: patchmode: 空の原本ファイルをtouchできません" - -msgid "E207: Can't delete backup file" -msgstr "E207: バックアップファイルを消せません" - msgid "" "\n" "WARNING: Original file may be lost or damaged\n" diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index da2ada791f..d7726409b5 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -291,7 +291,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } else { assert(Columns - pum_col - pum_scrollbar >= INT_MIN && Columns - pum_col - pum_scrollbar <= INT_MAX); - pum_width = (int)(Columns - pum_col - pum_scrollbar); + pum_width = Columns - pum_col - pum_scrollbar; } if ((pum_width > max_width + pum_kind_width + pum_extra_width + 1) @@ -352,12 +352,12 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i // not enough room, will use what we have if (pum_rl) { assert(Columns - 1 >= INT_MIN); - pum_col = (int)(Columns - 1); + pum_col = Columns - 1; } else { pum_col = 0; } assert(Columns - 1 >= INT_MIN); - pum_width = (int)(Columns - 1); + pum_width = Columns - 1; } else { if (max_width > p_pw) { // truncate @@ -369,7 +369,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } else { assert(Columns - max_width >= INT_MIN && Columns - max_width <= INT_MAX); - pum_col = (int)(Columns - max_width); + pum_col = Columns - max_width; } pum_width = max_width - pum_scrollbar; } diff --git a/src/nvim/pos.h b/src/nvim/pos.h index d17e27906e..51991ed314 100644 --- a/src/nvim/pos.h +++ b/src/nvim/pos.h @@ -16,7 +16,9 @@ typedef int colnr_T; /// Maximal (invalid) line number enum { MAXLNUM = 0x7fffffff, }; /// Maximal column number -enum { MAXCOL = INT_MAX, }; +/// MAXCOL used to be INT_MAX, but with 64 bit ints that results in running +/// out of memory when trying to allocate a very long line. +enum { MAXCOL = 0x7fffffff, }; // Minimum line number enum { MINLNUM = 1, }; // minimum column number diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 32d0ebe8eb..7e29aed51b 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -131,7 +131,7 @@ struct qf_info_S { static qf_info_T ql_info; // global quickfix list static unsigned last_qf_id = 0; // Last Used quickfix list id -#define FMT_PATTERNS 11 // maximum number of % recognized +#define FMT_PATTERNS 13 // maximum number of % recognized // Structure used to hold the info of one part of 'errorformat' typedef struct efm_S efm_T; @@ -209,6 +209,17 @@ typedef struct { bool valid; } qffields_T; +/// :vimgrep command arguments +typedef struct vgr_args_S { + long tomatch; ///< maximum number of matches to find + char_u *spat; ///< search pattern + int flags; ///< search modifier + char_u **fnames; ///< list of files to search + int fcount; ///< number of files + regmmatch_T regmatch; ///< compiled search pattern + char_u *qf_title; ///< quickfix list title +} vgr_args_T; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "quickfix.c.generated.h" #endif @@ -321,22 +332,27 @@ int qf_init(win_T *wp, const char_u *restrict efile, char_u *restrict errorforma // Maximum number of bytes allowed per line while reading an errorfile. static const size_t LINE_MAXLEN = 4096; +/// Patterns used. Keep in sync with qf_parse_fmt[]. static struct fmtpattern { char_u convchar; char *pattern; } fmt_pat[FMT_PATTERNS] = { - { 'f', ".\\+" }, // only used when at end - { 'n', "\\d\\+" }, - { 'l', "\\d\\+" }, - { 'c', "\\d\\+" }, - { 't', "." }, - { 'm', ".\\+" }, - { 'r', ".*" }, - { 'p', "[- .]*"}, // NOLINT(whitespace/tab) - { 'v', "\\d\\+" }, - { 's', ".\\+" }, - { 'o', ".\\+" } + { 'f', ".\\+" }, // only used when at end + { 'n', "\\d\\+" }, // 1 + { 'l', "\\d\\+" }, // 2 + { 'e', "\\d\\+" }, // 3 + { 'c', "\\d\\+" }, // 4 + { 'k', "\\d\\+" }, // 5 + { 't', "." }, // 6 +#define FMT_PATTERN_M 7 + { 'm', ".\\+" }, // 7 +#define FMT_PATTERN_R 8 + { 'r', ".*" }, // 8 + { 'p', "[- \t.]*" }, // 9 + { 'v', "\\d\\+" }, // 10 + { 's', ".\\+" }, // 11 + { 'o', ".\\+" } // 12 }; /// Convert an errorformat pattern to a regular expression pattern. @@ -352,9 +368,9 @@ static char_u *efmpat_to_regpat(const char_u *efmpat, char_u *regpat, efm_T *efm semsg(_("E372: Too many %%%c in format string"), *efmpat); return NULL; } - if ((idx && idx < 6 + if ((idx && idx < FMT_PATTERN_R && vim_strchr((char_u *)"DXOPQ", efminfo->prefix) != NULL) - || (idx == 6 + || (idx == FMT_PATTERN_R && vim_strchr((char_u *)"OPQ", efminfo->prefix) == NULL)) { semsg(_("E373: Unexpected %%%c in format string"), *efmpat); return NULL; @@ -1277,7 +1293,7 @@ static int qf_parse_fmt_n(regmatch_T *rmp, int midx, qffields_T *fields) return QF_OK; } -/// Parse the match for line number (%l') pattern in regmatch. +/// Parse the match for line number ('%l') pattern in regmatch. /// Return the matched value in "fields->lnum". static int qf_parse_fmt_l(regmatch_T *rmp, int midx, qffields_T *fields) { @@ -1288,6 +1304,17 @@ static int qf_parse_fmt_l(regmatch_T *rmp, int midx, qffields_T *fields) return QF_OK; } +/// Parse the match for end line number ('%e') pattern in regmatch. +/// Return the matched value in "fields->end_lnum". +static int qf_parse_fmt_e(regmatch_T *rmp, int midx, qffields_T *fields) +{ + if (rmp->startp[midx] == NULL) { + return QF_FAIL; + } + fields->end_lnum = atol((char *)rmp->startp[midx]); + return QF_OK; +} + /// Parse the match for column number ('%c') pattern in regmatch. /// Return the matched value in "fields->col". static int qf_parse_fmt_c(regmatch_T *rmp, int midx, qffields_T *fields) @@ -1299,6 +1326,17 @@ static int qf_parse_fmt_c(regmatch_T *rmp, int midx, qffields_T *fields) return QF_OK; } +/// Parse the match for end line number ('%e') pattern in regmatch. +/// Return the matched value in "fields->end_lnum". +static int qf_parse_fmt_k(regmatch_T *rmp, int midx, qffields_T *fields) +{ + if (rmp->startp[midx] == NULL) { + return QF_FAIL; + } + fields->end_col = (int)atol((char *)rmp->startp[midx]); + return QF_OK; +} + /// Parse the match for error type ('%t') pattern in regmatch. /// Return the matched value in "fields->type". static int qf_parse_fmt_t(regmatch_T *rmp, int midx, qffields_T *fields) @@ -1431,14 +1469,17 @@ static int qf_parse_fmt_o(regmatch_T *rmp, int midx, qffields_T *fields) /// 'errorformat' format pattern parser functions. /// The '%f' and '%r' formats are parsed differently from other formats. /// See qf_parse_match() for details. +/// Keep in sync with fmt_pat[]. static int (*qf_parse_fmt[FMT_PATTERNS])(regmatch_T *, int, qffields_T *) = { - NULL, + NULL, // %f qf_parse_fmt_n, qf_parse_fmt_l, + qf_parse_fmt_e, qf_parse_fmt_c, + qf_parse_fmt_k, qf_parse_fmt_t, qf_parse_fmt_m, - NULL, + NULL, // %r qf_parse_fmt_p, qf_parse_fmt_v, qf_parse_fmt_s, @@ -1474,13 +1515,13 @@ static int qf_parse_match(char_u *linebuf, size_t linelen, efm_T *fmt_ptr, regma midx = (int)fmt_ptr->addr[i]; if (i == 0 && midx > 0) { // %f status = qf_parse_fmt_f(regmatch, midx, fields, idx); - } else if (i == 5) { + } else if (i == FMT_PATTERN_M) { if (fmt_ptr->flags == '+' && !qf_multiscan) { // %+ qf_parse_fmt_plus(linebuf, linelen, fields); } else if (midx > 0) { // %m status = qf_parse_fmt_m(regmatch, midx, fields); } - } else if (i == 6 && midx > 0) { // %r + } else if (i == FMT_PATTERN_R && midx > 0) { // %r status = qf_parse_fmt_r(regmatch, midx, tail); } else if (midx > 0) { // others status = (qf_parse_fmt[i])(regmatch, midx, fields); @@ -1625,10 +1666,16 @@ static int qf_parse_multiline_pfx(int idx, qf_list_T *qfl, qffields_T *fields) if (!qfprev->qf_lnum) { qfprev->qf_lnum = fields->lnum; } + if (!qfprev->qf_end_lnum) { + qfprev->qf_end_lnum = fields->end_lnum; + } if (!qfprev->qf_col) { qfprev->qf_col = fields->col; qfprev->qf_viscol = fields->use_viscol; } + if (!qfprev->qf_end_col) { + qfprev->qf_end_col = fields->end_col; + } if (!qfprev->qf_fnum) { qfprev->qf_fnum = qf_get_fnum(qfl, qfl->qf_directory, *fields->namebuf || qfl->qf_directory @@ -2975,7 +3022,7 @@ static void qf_jump_newwin(qf_info_T *qi, int dir, int errornr, int forceit, boo if (retval != OK) { if (opened_window) { - win_close(curwin, true); // Close opened window + win_close(curwin, true, false); // Close opened window } if (qf_ptr != NULL && qf_ptr->qf_fnum != 0) { // Couldn't open file, so put index back where it was. This could @@ -3530,7 +3577,7 @@ void ex_cclose(exarg_T *eap) // Find existing quickfix window and close it. win = qf_find_win(qi); if (win != NULL) { - win_close(win, false); + win_close(win, false, false); } } @@ -3604,7 +3651,7 @@ static int qf_open_new_cwindow(qf_info_T *qi, int height) // win_split, so add a check to ensure that the win is still here if (IS_LL_STACK(qi) && !win_valid(win)) { // close the window that was supposed to be for the loclist - win_close(curwin, false); + win_close(curwin, false, false); return FAIL; } @@ -3947,7 +3994,7 @@ static int qf_buf_add_line(qf_list_T *qfl, buf_T *buf, linenr_T lnum, const qfli int len; buf_T *errbuf; - // If the 'quickfixtextfunc' function returned an non-empty custom string + // If the 'quickfixtextfunc' function returned a non-empty custom string // for this entry, then use it. if (qftf_str != NULL && *qftf_str != NUL) { STRLCPY(IObuff, qftf_str, IOSIZE); @@ -4849,11 +4896,12 @@ static qfline_T *qf_find_closest_entry(qf_list_T *qfl, int bnr, const pos_T *pos /// Get the nth quickfix entry below the specified entry. Searches forward in /// the list. If linewise is true, then treat multiple entries on a single line /// as one. -static void qf_get_nth_below_entry(qfline_T *entry, linenr_T n, bool linewise, int *errornr) +static void qf_get_nth_below_entry(qfline_T *entry_arg, linenr_T n, bool linewise, int *errornr) FUNC_ATTR_NONNULL_ALL { + qfline_T *entry = entry_arg; + while (n-- > 0 && !got_int) { - // qfline_T *first_entry = entry; int first_errornr = *errornr; if (linewise) { @@ -4864,9 +4912,6 @@ static void qf_get_nth_below_entry(qfline_T *entry, linenr_T n, bool linewise, i if (entry->qf_next == NULL || entry->qf_next->qf_fnum != entry->qf_fnum) { if (linewise) { - // If multiple entries are on the same line, then use the first - // entry - // entry = first_entry; *errornr = first_errornr; } break; @@ -5194,49 +5239,93 @@ static bool vgr_qflist_valid(win_T *wp, qf_info_T *qi, unsigned qfid, char_u *ti /// Search for a pattern in all the lines in a buffer and add the matching lines /// to a quickfix list. -static bool vgr_match_buflines(qf_list_T *qfl, char_u *fname, buf_T *buf, regmmatch_T *regmatch, - long *tomatch, int duplicate_name, int flags) - FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5) +static bool vgr_match_buflines(qf_list_T *qfl, char_u *fname, buf_T *buf, char_u *spat, + regmmatch_T *regmatch, long *tomatch, int duplicate_name, int flags) + FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5, 6) { bool found_match = false; for (long lnum = 1; lnum <= buf->b_ml.ml_line_count && *tomatch > 0; lnum++) { colnr_T col = 0; - while (vim_regexec_multi(regmatch, curwin, buf, lnum, col, NULL, - NULL) > 0) { - // Pass the buffer number so that it gets used even for a - // dummy buffer, unless duplicate_name is set, then the - // buffer will be wiped out below. - if (qf_add_entry(qfl, - NULL, // dir - fname, - NULL, - duplicate_name ? 0 : buf->b_fnum, - ml_get_buf(buf, regmatch->startpos[0].lnum + lnum, - false), - regmatch->startpos[0].lnum + lnum, - regmatch->endpos[0].lnum + lnum, - regmatch->startpos[0].col + 1, - regmatch->endpos[0].col + 1, - false, // vis_col - NULL, // search pattern - 0, // nr - 0, // type - true) // valid - == QF_FAIL) { - got_int = true; - break; - } - found_match = true; - if (--*tomatch == 0) { - break; - } - if ((flags & VGR_GLOBAL) == 0 || regmatch->endpos[0].lnum > 0) { - break; + if (!(flags & VGR_FUZZY)) { + // Regular expression match + while (vim_regexec_multi(regmatch, curwin, buf, lnum, col, NULL, NULL) > 0) { + // Pass the buffer number so that it gets used even for a + // dummy buffer, unless duplicate_name is set, then the + // buffer will be wiped out below. + if (qf_add_entry(qfl, + NULL, // dir + fname, + NULL, + duplicate_name ? 0 : buf->b_fnum, + ml_get_buf(buf, regmatch->startpos[0].lnum + lnum, false), + regmatch->startpos[0].lnum + lnum, + regmatch->endpos[0].lnum + lnum, + regmatch->startpos[0].col + 1, + regmatch->endpos[0].col + 1, + false, // vis_col + NULL, // search pattern + 0, // nr + 0, // type + true) // valid + == QF_FAIL) { + got_int = true; + break; + } + found_match = true; + if (--*tomatch == 0) { + break; + } + if ((flags & VGR_GLOBAL) == 0 || regmatch->endpos[0].lnum > 0) { + break; + } + col = regmatch->endpos[0].col + (col == regmatch->endpos[0].col); + if (col > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) { + break; + } } - col = regmatch->endpos[0].col + (col == regmatch->endpos[0].col); - if (col > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) { - break; + } else { + const size_t pat_len = STRLEN(spat); + char_u *const str = ml_get_buf(buf, lnum, false); + int score; + uint32_t matches[MAX_FUZZY_MATCHES]; + const size_t sz = sizeof(matches) / sizeof(matches[0]); + + // Fuzzy string match + while (fuzzy_match(str + col, spat, false, &score, matches, (int)sz) > 0) { + // Pass the buffer number so that it gets used even for a + // dummy buffer, unless duplicate_name is set, then the + // buffer will be wiped out below. + if (qf_add_entry(qfl, + NULL, // dir + fname, + NULL, + duplicate_name ? 0 : buf->b_fnum, + str, + lnum, + 0, + (colnr_T)matches[0] + col + 1, + 0, + false, // vis_col + NULL, // search pattern + 0, // nr + 0, // type + true) // valid + == QF_FAIL) { + got_int = true; + break; + } + found_match = true; + if (--*tomatch == 0) { + break; + } + if ((flags & VGR_GLOBAL) == 0) { + break; + } + col = (colnr_T)matches[pat_len - 1] + col + 1; + if (col > (colnr_T)STRLEN(str)) { + break; + } } } line_breakcheck(); @@ -5249,7 +5338,7 @@ static bool vgr_match_buflines(qf_list_T *qfl, char_u *fname, buf_T *buf, regmma } /// Jump to the first match and update the directory. -static void vgr_jump_to_match(qf_info_T *qi, int forceit, int *redraw_for_dummy, +static void vgr_jump_to_match(qf_info_T *qi, int forceit, bool *redraw_for_dummy, buf_T *first_match_buf, char_u *target_dir) { buf_T *buf = curbuf; @@ -5284,104 +5373,72 @@ static bool existing_swapfile(const buf_T *buf) return false; } -// ":vimgrep {pattern} file(s)" -// ":vimgrepadd {pattern} file(s)" -// ":lvimgrep {pattern} file(s)" -// ":lvimgrepadd {pattern} file(s)" -void ex_vimgrep(exarg_T *eap) +/// Process :vimgrep command arguments. The command syntax is: +/// +/// :{count}vimgrep /{pattern}/[g][j] {file} ... +static int vgr_process_args(exarg_T *eap, vgr_args_T *args) { - regmmatch_T regmatch; - int fcount; - char_u **fnames; - char_u *fname; - char_u *s; - char_u *p; - int fi; - qf_list_T *qfl; - win_T *wp = NULL; - buf_T *buf; - int duplicate_name = FALSE; - int using_dummy; - int redraw_for_dummy = FALSE; - int found_match; - buf_T *first_match_buf = NULL; - time_t seconds = 0; - aco_save_T aco; - int flags = 0; - long tomatch; - char_u *dirname_start = NULL; - char_u *dirname_now = NULL; - char_u *target_dir = NULL; - char_u *au_name = NULL; - - au_name = vgr_get_auname(eap->cmdidx); - if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, - curbuf->b_fname, true, curbuf)) { - if (aborting()) { - return; - } - } + memset(args, 0, sizeof(*args)); - qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp); + args->regmatch.regprog = NULL; + args->qf_title = vim_strsave(qf_cmdtitle(*eap->cmdlinep)); if (eap->addr_count > 0) { - tomatch = eap->line2; + args->tomatch = eap->line2; } else { - tomatch = MAXLNUM; + args->tomatch = MAXLNUM; } // Get the search pattern: either white-separated or enclosed in // - regmatch.regprog = NULL; - char_u *title = vim_strsave(qf_cmdtitle(*eap->cmdlinep)); - p = skip_vimgrep_pat(eap->arg, &s, &flags); + char_u *p = skip_vimgrep_pat(eap->arg, &args->spat, &args->flags); if (p == NULL) { emsg(_(e_invalpat)); - goto theend; + return FAIL; } - vgr_init_regmatch(®match, s); - if (regmatch.regprog == NULL) { - goto theend; + vgr_init_regmatch(&args->regmatch, args->spat); + if (args->regmatch.regprog == NULL) { + return FAIL; } p = skipwhite(p); if (*p == NUL) { emsg(_("E683: File name missing or invalid pattern")); - goto theend; - } - - if ((eap->cmdidx != CMD_grepadd && eap->cmdidx != CMD_lgrepadd - && eap->cmdidx != CMD_vimgrepadd && eap->cmdidx != CMD_lvimgrepadd) - || qf_stack_empty(qi)) { - // make place for a new list - qf_new_list(qi, title); + return FAIL; } // Parse the list of arguments, wildcards have already been expanded. - if (get_arglist_exp(p, &fcount, &fnames, true) == FAIL) { - goto theend; + if (get_arglist_exp(p, &args->fcount, &args->fnames, true) == FAIL) { + return FAIL; } - if (fcount == 0) { + if (args->fcount == 0) { emsg(_(e_nomatch)); - goto theend; + return FAIL; } - dirname_start = xmalloc(MAXPATHL); - dirname_now = xmalloc(MAXPATHL); + return OK; +} + +/// Search for a pattern in a list of files and populate the quickfix list with +/// the matches. +static int vgr_process_files(win_T *wp, qf_info_T *qi, vgr_args_T *cmd_args, + bool *redraw_for_dummy, buf_T **first_match_buf, + char_u **target_dir) +{ + int status = FAIL; + unsigned save_qfid = qf_get_curlist(qi)->qf_id; + bool duplicate_name = false; + + char_u *dirname_start = xmalloc(MAXPATHL); + char_u *dirname_now = xmalloc(MAXPATHL); // Remember the current directory, because a BufRead autocommand that does // ":lcd %:p:h" changes the meaning of short path names. os_dirname(dirname_start, MAXPATHL); - incr_quickfix_busy(); - - // Remember the current quickfix list identifier, so that we can check for - // autocommands changing the current quickfix list. - unsigned save_qfid = qf_get_curlist(qi)->qf_id; - - seconds = (time_t)0; - for (fi = 0; fi < fcount && !got_int && tomatch > 0; fi++) { - fname = path_try_shorten_fname(fnames[fi]); + time_t seconds = (time_t)0; + for (int fi = 0; fi < cmd_args->fcount && !got_int && cmd_args->tomatch > 0; fi++) { + char_u *fname = path_try_shorten_fname(cmd_args->fnames[fi]); if (time(NULL) > seconds) { // Display the file name every second or so, show the user we are // working on it. @@ -5389,13 +5446,13 @@ void ex_vimgrep(exarg_T *eap) vgr_display_fname(fname); } - buf = buflist_findname_exp(fnames[fi]); + buf_T *buf = buflist_findname_exp(cmd_args->fnames[fi]); + bool using_dummy; if (buf == NULL || buf->b_ml.ml_mfp == NULL) { // Remember that a buffer with this name already exists. duplicate_name = (buf != NULL); - using_dummy = TRUE; - redraw_for_dummy = TRUE; - + using_dummy = true; + *redraw_for_dummy = true; buf = vgr_load_dummy_buf(fname, dirname_start, dirname_now); } else { // Use existing, loaded buffer. @@ -5404,11 +5461,10 @@ void ex_vimgrep(exarg_T *eap) // Check whether the quickfix list is still valid. When loading a // buffer above, autocommands might have changed the quickfix list. - if (!vgr_qflist_valid(wp, qi, save_qfid, *eap->cmdlinep)) { - FreeWild(fcount, fnames); - decr_quickfix_busy(); + if (!vgr_qflist_valid(wp, qi, save_qfid, cmd_args->qf_title)) { goto theend; } + save_qfid = qf_get_curlist(qi)->qf_id; if (buf == NULL) { @@ -5418,13 +5474,18 @@ void ex_vimgrep(exarg_T *eap) } else { // Try for a match in all lines of the buffer. // For ":1vimgrep" look for first match only. - found_match = vgr_match_buflines(qf_get_curlist(qi), - fname, buf, ®match, &tomatch, - duplicate_name, flags); + bool found_match = vgr_match_buflines(qf_get_curlist(qi), + fname, + buf, + cmd_args->spat, + &cmd_args->regmatch, + &cmd_args->tomatch, + duplicate_name, + cmd_args->flags); if (using_dummy) { - if (found_match && first_match_buf == NULL) { - first_match_buf = buf; + if (found_match && *first_match_buf == NULL) { + *first_match_buf = buf; } if (duplicate_name) { // Never keep a dummy buffer if there is another buffer @@ -5444,8 +5505,8 @@ void ex_vimgrep(exarg_T *eap) if (!found_match) { wipe_dummy_buffer(buf, dirname_start); buf = NULL; - } else if (buf != first_match_buf - || (flags & VGR_NOJUMP) + } else if (buf != *first_match_buf + || (cmd_args->flags & VGR_NOJUMP) || existing_swapfile(buf)) { unload_dummy_buffer(buf, dirname_start); // Keeping the buffer, remove the dummy flag. @@ -5460,16 +5521,17 @@ void ex_vimgrep(exarg_T *eap) // If the buffer is still loaded we need to use the // directory we jumped to below. - if (buf == first_match_buf - && target_dir == NULL + if (buf == *first_match_buf + && *target_dir == NULL && STRCMP(dirname_start, dirname_now) != 0) { - target_dir = vim_strsave(dirname_now); + *target_dir = vim_strsave(dirname_now); } // The buffer is still loaded, the Filetype autocommands // need to be done now, in that buffer. And the modelines // need to be done (again). But not the window-local // options! + aco_save_T aco; aucmd_prepbuf(&aco, buf); apply_autocmds(EVENT_FILETYPE, buf->b_p_ft, buf->b_fname, true, buf); do_modelines(OPT_NOWIN); @@ -5479,9 +5541,58 @@ void ex_vimgrep(exarg_T *eap) } } - FreeWild(fcount, fnames); + status = OK; - qfl = qf_get_curlist(qi); +theend: + xfree(dirname_now); + xfree(dirname_start); + return status; +} + +/// ":vimgrep {pattern} file(s)" +/// ":vimgrepadd {pattern} file(s)" +/// ":lvimgrep {pattern} file(s)" +/// ":lvimgrepadd {pattern} file(s)" +void ex_vimgrep(exarg_T *eap) +{ + char_u *au_name = vgr_get_auname(eap->cmdidx); + if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name, + curbuf->b_fname, true, curbuf)) { + if (aborting()) { + return; + } + } + + win_T *wp = NULL; + qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp); + char_u *target_dir = NULL; + vgr_args_T args; + if (vgr_process_args(eap, &args) == FAIL) { + goto theend; + } + + if ((eap->cmdidx != CMD_grepadd && eap->cmdidx != CMD_lgrepadd + && eap->cmdidx != CMD_vimgrepadd && eap->cmdidx != CMD_lvimgrepadd) + || qf_stack_empty(qi)) { + // make place for a new list + qf_new_list(qi, args.qf_title); + } + + incr_quickfix_busy(); + + bool redraw_for_dummy = false; + buf_T *first_match_buf = NULL; + int status = vgr_process_files(wp, qi, &args, &redraw_for_dummy, &first_match_buf, &target_dir); + + if (status != OK) { + FreeWild(args.fcount, args.fnames); + decr_quickfix_busy(); + goto theend; + } + + FreeWild(args.fcount, args.fnames); + + qf_list_T *qfl = qf_get_curlist(qi); qfl->qf_nonevalid = false; qfl->qf_ptr = qfl->qf_start; qfl->qf_index = 1; @@ -5489,26 +5600,28 @@ void ex_vimgrep(exarg_T *eap) qf_update_buffer(qi, NULL); + // Remember the current quickfix list identifier, so that we can check for + // autocommands changing the current quickfix list. + unsigned save_qfid = qf_get_curlist(qi)->qf_id; + if (au_name != NULL) { apply_autocmds(EVENT_QUICKFIXCMDPOST, au_name, curbuf->b_fname, true, curbuf); } // The QuickFixCmdPost autocmd may free the quickfix list. Check the list // is still valid. - if (!qflist_valid(wp, save_qfid) - || qf_restore_list(qi, save_qfid) == FAIL) { + if (!qflist_valid(wp, save_qfid) || qf_restore_list(qi, save_qfid) == FAIL) { decr_quickfix_busy(); goto theend; } // Jump to first match. if (!qf_list_empty(qf_get_curlist(qi))) { - if ((flags & VGR_NOJUMP) == 0) { - vgr_jump_to_match(qi, eap->forceit, &redraw_for_dummy, first_match_buf, - target_dir); + if ((args.flags & VGR_NOJUMP) == 0) { + vgr_jump_to_match(qi, eap->forceit, &redraw_for_dummy, first_match_buf, target_dir); } } else { - semsg(_(e_nomatch2), s); + semsg(_(e_nomatch2), args.spat); } decr_quickfix_busy(); @@ -5520,11 +5633,9 @@ void ex_vimgrep(exarg_T *eap) } theend: - xfree(title); - xfree(dirname_now); - xfree(dirname_start); + xfree(args.qf_title); xfree(target_dir); - vim_regfree(regmatch.regprog); + vim_regfree(args.regmatch.regprog); } // Restore current working directory to "dirname_start" if they differ, taking @@ -5656,7 +5767,7 @@ static void wipe_dummy_buffer(buf_T *buf, char_u *dirname_start) if (firstwin->w_next != NULL) { for (win_T *wp = firstwin; wp != NULL; wp = wp->w_next) { if (wp->w_buffer == buf) { - if (win_close(wp, false) == OK) { + if (win_close(wp, false, false) == OK) { did_one = true; } break; diff --git a/src/nvim/quickfix.h b/src/nvim/quickfix.h index f5178e332a..0da43e436c 100644 --- a/src/nvim/quickfix.h +++ b/src/nvim/quickfix.h @@ -7,6 +7,7 @@ // flags for skip_vimgrep_pat() #define VGR_GLOBAL 1 #define VGR_NOJUMP 2 +#define VGR_FUZZY 4 #ifdef INCLUDE_GENERATED_DECLARATIONS # include "quickfix.h.generated.h" diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 45e580dbee..6a6c915094 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -2901,18 +2901,14 @@ static int peekchr(void) { int c = regparse[1]; - if (c == NUL) - curchr = '\\'; /* trailing '\' */ - else if ( - c <= '~' && META_flags[c] - ) { - /* - * META contains everything that may be magic sometimes, - * except ^ and $ ("\^" and "\$" are only magic after - * "\V"). We now fetch the next character and toggle its - * magicness. Therefore, \ is so meta-magic that it is - * not in META. - */ + if (c == NUL) { + curchr = '\\'; // trailing '\' + } else if (c <= '~' && META_flags[c]) { + // META contains everything that may be magic sometimes, + // except ^ and $ ("\^" and "\$" are only magic after + // "\V"). We now fetch the next character and toggle its + // magicness. Therefore, \ is so meta-magic that it is + // not in META. curchr = -1; prev_at_start = at_start; at_start = false; // be able to say "/\*ptr" @@ -3232,7 +3228,7 @@ typedef struct { // The current match-position is remembered with these variables: linenr_T lnum; ///< line number, relative to first line char_u *line; ///< start of current line - char_u *input; ///< current input, points into "regline" + char_u *input; ///< current input, points into "line" int need_clear_subexpr; ///< subexpressions still need to be cleared int need_clear_zsubexpr; ///< extmatch subexpressions still need to be @@ -6538,11 +6534,16 @@ char_u *regtilde(char_u *source, int magic) } } - xfree(reg_prev_sub); - if (newsub != source) /* newsub was allocated, just keep it */ - reg_prev_sub = newsub; - else /* no ~ found, need to save newsub */ - reg_prev_sub = vim_strsave(newsub); + // Only change reg_prev_sub when not previewing. + if (!(State & CMDPREVIEW)) { + xfree(reg_prev_sub); + if (newsub != source) { // newsub was allocated, just keep it + reg_prev_sub = newsub; + } else { // no ~ found, need to save newsub + reg_prev_sub = vim_strsave(newsub); + } + } + return newsub; } diff --git a/src/nvim/regexp_nfa.c b/src/nvim/regexp_nfa.c index 41c927eaa6..133858f113 100644 --- a/src/nvim/regexp_nfa.c +++ b/src/nvim/regexp_nfa.c @@ -2013,7 +2013,7 @@ static int nfa_regpiece(void) // will emit NFA_STAR. // Bail out if we can use the other engine, but only, when the // pattern does not need the NFA engine like (e.g. [[:upper:]]\{2,\} - // does not work with with characters > 8 bit with the BT engine) + // does not work with characters > 8 bit with the BT engine) if ((nfa_re_flags & RE_AUTO) && (maxval > 500 || maxval > minval + 200) && (maxval != MAX_LIMIT && minval < 200) @@ -4367,7 +4367,7 @@ static regsubs_T *addstate_here( // First add the state(s) at the end, so that we know how many there are. // Pass the listidx as offset (avoids adding another argument to - // addstate(). + // addstate()). regsubs_T *r = addstate(l, state, subs, pim, -listidx - ADDSTATE_HERE_OFFSET); if (r == NULL) { return NULL; @@ -6071,8 +6071,15 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, case NFA_MARK_GT: case NFA_MARK_LT: { + size_t col = rex.input - rex.line; pos_T *pos = getmark_buf(rex.reg_buf, t->state->val, false); + // Line may have been freed, get it again. + if (REG_MULTI) { + rex.line = reg_getline(rex.lnum); + rex.input = rex.line + col; + } + // Compare the mark position to the match position, if the mark // exists and mark is set in reg_buf. if (pos != NULL && pos->lnum > 0) { diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 1c04cb16b3..1102096b32 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -24,6 +24,13 @@ static bool runtime_search_path_valid = false; static int *runtime_search_path_ref = NULL; static RuntimeSearchPath runtime_search_path; +static RuntimeSearchPath runtime_search_path_thread; +static uv_mutex_t runtime_search_path_mutex; + +void runtime_init(void) +{ + uv_mutex_init(&runtime_search_path_mutex); +} /// ":runtime [what] {name}" void ex_runtime(exarg_T *eap) @@ -146,7 +153,7 @@ int do_in_path(char_u *path, char *name, int flags, DoInRuntimepathCB callback, if (flags & DIP_ERR) { semsg(_(e_dirnotf), basepath, name); - } else if (p_verbose > 0) { + } else if (p_verbose > 1) { verbose_enter(); smsg(_("not found in '%s': \"%s\""), basepath, name); verbose_leave(); @@ -172,6 +179,17 @@ RuntimeSearchPath runtime_search_path_get_cached(int *ref) return runtime_search_path; } +RuntimeSearchPath copy_runtime_search_path(const RuntimeSearchPath src) +{ + RuntimeSearchPath dst = KV_INITIAL_VALUE; + for (size_t j = 0; j < kv_size(src); j++) { + SearchPathItem src_item = kv_A(src, j); + kv_push(dst, ((SearchPathItem){ xstrdup(src_item.path), src_item.after, src_item.has_lua })); + } + + return dst; +} + void runtime_search_path_unref(RuntimeSearchPath path, int *ref) FUNC_ATTR_NONNULL_ALL { @@ -268,7 +286,7 @@ int do_in_cached_path(char_u *name, int flags, DoInRuntimepathCB callback, void if (!did_one && name != NULL) { if (flags & DIP_ERR) { semsg(_(e_dirnotf), "runtime path", name); - } else if (p_verbose > 0) { + } else if (p_verbose > 1) { verbose_enter(); smsg(_("not found in runtime path: \"%s\""), name); verbose_leave(); @@ -302,15 +320,35 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) { int ref; RuntimeSearchPath path = runtime_search_path_get_cached(&ref); - ArrayOf(String) rv = ARRAY_DICT_INIT; static char buf[MAXPATHL]; + ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, path, buf, sizeof buf); + + runtime_search_path_unref(path, &ref); + return rv; +} + +ArrayOf(String) runtime_get_named_thread(bool lua, Array pat, bool all) +{ + // TODO(bfredl): avoid contention between multiple worker threads? + uv_mutex_lock(&runtime_search_path_mutex); + static char buf[MAXPATHL]; + ArrayOf(String) rv = runtime_get_named_common(lua, pat, all, runtime_search_path_thread, + buf, sizeof buf); + uv_mutex_unlock(&runtime_search_path_mutex); + return rv; +} + +ArrayOf(String) runtime_get_named_common(bool lua, Array pat, bool all, + RuntimeSearchPath path, char *buf, size_t buf_len) +{ + ArrayOf(String) rv = ARRAY_DICT_INIT; for (size_t i = 0; i < kv_size(path); i++) { SearchPathItem *item = &kv_A(path, i); if (lua) { if (item->has_lua == kNone) { - size_t size = (size_t)snprintf(buf, sizeof buf, "%s/lua/", item->path); - item->has_lua = (size < sizeof buf && os_isdir((char_u *)buf)) ? kTrue : kFalse; + size_t size = (size_t)snprintf(buf, buf_len, "%s/lua/", item->path); + item->has_lua = (size < buf_len && os_isdir((char_u *)buf)); } if (item->has_lua == kFalse) { continue; @@ -320,9 +358,9 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) for (size_t j = 0; j < pat.size; j++) { Object pat_item = pat.items[j]; if (pat_item.type == kObjectTypeString) { - size_t size = (size_t)snprintf(buf, sizeof buf, "%s/%s", + size_t size = (size_t)snprintf(buf, buf_len, "%s/%s", item->path, pat_item.data.string.data); - if (size < sizeof buf) { + if (size < buf_len) { if (os_file_is_readable(buf)) { ADD(rv, STRING_OBJ(cstr_to_string(buf))); if (!all) { @@ -333,9 +371,7 @@ ArrayOf(String) runtime_get_named(bool lua, Array pat, bool all) } } } - done: - runtime_search_path_unref(path, &ref); return rv; } @@ -569,6 +605,10 @@ void runtime_search_path_validate(void) runtime_search_path = runtime_search_path_build(); runtime_search_path_valid = true; runtime_search_path_ref = NULL; // initially unowned + uv_mutex_lock(&runtime_search_path_mutex); + runtime_search_path_free(runtime_search_path_thread); + runtime_search_path_thread = copy_runtime_search_path(runtime_search_path); + uv_mutex_unlock(&runtime_search_path_mutex); } } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index e62e3ca7bc..7fafe3dd6e 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -326,6 +326,18 @@ void redraw_buf_status_later(buf_T *buf) } } +void redraw_win_signcol(win_T *wp) +{ + // If we can compute a change in the automatic sizing of the sign column + // under 'signcolumn=auto:X' and signs currently placed in the buffer, better + // figuring it out here so we can redraw the entire screen for it. + int scwidth = wp->w_scwidth; + wp->w_scwidth = win_signcol_count(wp); + if (wp->w_scwidth != scwidth) { + changed_line_abv_curs_win(wp); + } +} + /// Redraw the parts of the screen that is marked for redraw. /// /// Most code shouldn't call this directly, rather use redraw_later() and @@ -790,12 +802,6 @@ static void win_update(win_T *wp, Providers *providers) linenr_T mod_bot = 0; int save_got_int; - - // If we can compute a change in the automatic sizing of the sign column - // under 'signcolumn=auto:X' and signs currently placed in the buffer, better - // figuring it out here so we can redraw the entire screen for it. - buf_signcols(buf); - type = wp->w_redr_type; if (type >= NOT_VALID) { @@ -817,6 +823,8 @@ static void win_update(win_T *wp, Providers *providers) return; } + redraw_win_signcol(wp); + init_search_hl(wp); /* Force redraw when width of 'number' or 'relativenumber' column @@ -1215,19 +1223,40 @@ static void win_update(win_T *wp, Providers *providers) */ if (VIsual_mode == Ctrl_V) { colnr_T fromc, toc; - int save_ve_flags = ve_flags; + unsigned int save_ve_flags = curwin->w_ve_flags; if (curwin->w_p_lbr) { - ve_flags = VE_ALL; + curwin->w_ve_flags = VE_ALL; } getvcols(wp, &VIsual, &curwin->w_cursor, &fromc, &toc); - ve_flags = save_ve_flags; toc++; + curwin->w_ve_flags = save_ve_flags; // Highlight to the end of the line, unless 'virtualedit' has // "block". - if (curwin->w_curswant == MAXCOL && !(ve_flags & VE_BLOCK)) { - toc = MAXCOL; + if (curwin->w_curswant == MAXCOL) { + if (get_ve_flags() & VE_BLOCK) { + pos_T pos; + int cursor_above = curwin->w_cursor.lnum < VIsual.lnum; + + // Need to find the longest line. + toc = 0; + pos.coladd = 0; + for (pos.lnum = curwin->w_cursor.lnum; + cursor_above ? pos.lnum <= VIsual.lnum : pos.lnum >= VIsual.lnum; + pos.lnum += cursor_above ? 1 : -1) { + colnr_T t; + + pos.col = STRLEN(ml_get_buf(wp->w_buffer, pos.lnum, false)); + getvvcol(wp, &pos, NULL, NULL, &t); + if (toc < t) { + toc = t; + } + } + toc++; + } else { + toc = MAXCOL; + } } if (fromc != wp->w_old_cursor_fcol @@ -1825,7 +1854,7 @@ static void win_draw_end(win_T *wp, int c1, int c2, bool draw_margin, int row, i win_hl_attr(wp, HLF_FC)); } // draw the sign column - int count = win_signcol_count(wp); + int count = wp->w_scwidth; if (count > 0) { n = win_fill_end(wp, ' ', ' ', n, win_signcol_width(wp) * count, row, endrow, win_hl_attr(wp, HLF_SC)); @@ -1960,7 +1989,7 @@ static size_t fill_foldcolumn(char_u *p, win_T *wp, foldinfo_T foldinfo, linenr_ level = foldinfo.fi_level; // If the column is too narrow, we start at the lowest level that - // fits and use numbers to indicated the depth. + // fits and use numbers to indicate the depth. first_level = level - fdc - closed + 1; if (first_level < 1) { first_level = 1; @@ -2771,10 +2800,9 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc draw_state = WL_SIGN; /* Show the sign column when there are any signs in this * buffer or when using Netbeans. */ - int count = win_signcol_count(wp); - if (count > 0) { + if (wp->w_scwidth > 0) { get_sign_display_info(false, wp, lnum, sattrs, row, - startrow, filler_lines, filler_todo, count, + startrow, filler_lines, filler_todo, &c_extra, &c_final, extra, sizeof(extra), &p_extra, &n_extra, &char_attr, &draw_state, &sign_idx); @@ -2792,10 +2820,9 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc // in 'lnum', then display the sign instead of the line // number. if (*wp->w_p_scl == 'n' && *(wp->w_p_scl + 1) == 'u' - && num_signs > 0) { - int count = win_signcol_count(wp); + && num_signs > 0 && sign_get_attr(SIGN_TEXT, sattrs, 0, 1)) { get_sign_display_info(true, wp, lnum, sattrs, row, - startrow, filler_lines, filler_todo, count, + startrow, filler_lines, filler_todo, &c_extra, &c_final, extra, sizeof(extra), &p_extra, &n_extra, &char_attr, &draw_state, &sign_idx); @@ -3720,10 +3747,13 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc tab_len += n_extra - tab_len; } - // if n_extra > 0, it gives the number of chars + // If n_extra > 0, it gives the number of chars // to use for a tab, else we need to calculate the width - // for a tab + // for a tab. int len = (tab_len * utf_char2len(wp->w_p_lcs_chars.tab2)); + if (wp->w_p_lcs_chars.tab3) { + len += utf_char2len(wp->w_p_lcs_chars.tab3); + } if (n_extra > 0) { len += n_extra - tab_len; } @@ -3740,13 +3770,11 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc } int lcs = wp->w_p_lcs_chars.tab2; - // if tab3 is given, need to change the char - // for tab + // if tab3 is given, use it for the last char if (wp->w_p_lcs_chars.tab3 && i == tab_len - 1) { lcs = wp->w_p_lcs_chars.tab3; } - utf_char2bytes(lcs, p); - p += utf_char2len(lcs); + p += utf_char2bytes(lcs, p); n_extra += utf_char2len(lcs) - (saved_nextra > 0 ? 1 : 0); } p_extra = p_extra_free; @@ -3771,7 +3799,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc // Make sure, the highlighting for the tab char will be // correctly set further below (effectively reverts the - // FIX_FOR_BOGSUCOLS macro. + // FIX_FOR_BOGSUCOLS macro). if (n_extra == tab_len + vc_saved && wp->w_p_list && wp->w_p_lcs_chars.tab1) { tab_len += vc_saved; @@ -4130,7 +4158,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc if (((wp->w_p_cuc && (int)wp->w_virtcol >= VCOL_HLC - eol_hl_off && (int)wp->w_virtcol < - grid->Columns * (row - startrow + 1) + v + (long)grid->Columns * (row - startrow + 1) + v && lnum != wp->w_cursor.lnum) || draw_color_col || line_attr_lowprio || line_attr || diff_hlf != (hlf_T)0 || has_virttext)) { @@ -4232,6 +4260,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc // Show "extends" character from 'listchars' if beyond the line end and // 'list' is set. if (wp->w_p_lcs_chars.ext != NUL + && draw_state == WL_LINE && wp->w_p_list && !wp->w_p_wrap && filler_todo <= 0 @@ -4294,7 +4323,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc // Store the character. // if (wp->w_p_rl && utf_char2cells(mb_c) > 1) { - // A double-wide character is: put first halve in left cell. + // A double-wide character is: put first half in left cell. off--; col--; } @@ -4427,7 +4456,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc */ if ((wp->w_p_rl ? (col < 0) : (col >= grid->Columns)) && foldinfo.fi_lines == 0 - && (*ptr != NUL + && (draw_state != WL_LINE + || *ptr != NUL || filler_todo > 0 || (wp->w_p_list && wp->w_p_lcs_chars.eol != NUL && p_extra != at_end_str) @@ -4637,8 +4667,8 @@ void screen_adjust_grid(ScreenGrid **grid, int *row_off, int *col_off) static bool use_cursor_line_sign(win_T *wp, linenr_T lnum) { return wp->w_p_cul - && lnum == wp->w_cursor.lnum - && (wp->w_p_culopt_flags & CULOPT_NBR); + && lnum == wp->w_cursor.lnum + && (wp->w_p_culopt_flags & CULOPT_NBR); } // Get information needed to display the sign in line 'lnum' in window 'wp'. @@ -4650,10 +4680,11 @@ static bool use_cursor_line_sign(win_T *wp, linenr_T lnum) // @param[in, out] sign_idxp Index of the displayed sign static void get_sign_display_info(bool nrcol, win_T *wp, linenr_T lnum, sign_attrs_T sattrs[], int row, int startrow, int filler_lines, int filler_todo, - int count, int *c_extrap, int *c_finalp, char_u *extra, + int *c_extrap, int *c_finalp, char_u *extra, size_t extra_size, char_u **pp_extra, int *n_extrap, int *char_attrp, int *draw_statep, int *sign_idxp) { + int count = wp->w_scwidth; // Draw cells with the sign value or blank. *c_extrap = ' '; *c_finalp = NUL; @@ -4834,9 +4865,9 @@ static void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, end_dirty = col + char_cells; // When writing a single-width character over a double-width // character and at the end of the redrawn text, need to clear out - // the right halve of the old character. - // Also required when writing the right halve of a double-width - // char over the left halve of an existing one + // the right half of the old character. + // Also required when writing the right half of a double-width + // char over the left half of an existing one if (col + char_cells == endcol && ((char_cells == 1 && grid_off2cells(grid, off_to, max_off_to) > 1) @@ -5887,8 +5918,8 @@ void grid_puts_len(ScreenGrid *grid, char_u *text, int textlen, int row, int col } off = grid->line_offset[row] + col; - /* When drawing over the right halve of a double-wide char clear out the - * left halve. Only needed in a terminal. */ + // When drawing over the right half of a double-wide char clear out the + // left half. Only needed in a terminal. if (grid != &default_grid && col == 0 && grid_invalid_row(grid, row)) { // redraw the previous cell, make it empty put_dirty_first = -1; @@ -5950,9 +5981,9 @@ void grid_puts_len(ScreenGrid *grid, char_u *text, int textlen, int row, int col if (need_redraw) { // When at the end of the text and overwriting a two-cell // character with a one-cell character, need to clear the next - // cell. Also when overwriting the left halve of a two-cell char - // with the right halve of a two-cell char. Do this only once - // (utf8_off2cells() may return 2 on the right halve). + // cell. Also when overwriting the left half of a two-cell char + // with the right half of a two-cell char. Do this only once + // (utf8_off2cells() may return 2 on the right half). if (clear_next_cell) { clear_next_cell = false; } else if ((len < 0 ? ptr[mbyte_blen] == NUL @@ -6351,9 +6382,9 @@ void grid_fill(ScreenGrid *grid, int start_row, int end_row, int start_col, int } for (int row = start_row; row < end_row; row++) { - // When drawing over the right halve of a double-wide char clear - // out the left halve. When drawing over the left halve of a - // double wide-char clear out the right halve. Only needed in a + // When drawing over the right half of a double-wide char clear + // out the left half. When drawing over the left half of a + // double wide-char clear out the right half. Only needed in a // terminal. if (start_col > 0 && grid_fix_col(grid, start_col, row) != start_col) { grid_puts_len(grid, (char_u *)" ", 1, row, start_col - 1, 0); @@ -6762,7 +6793,7 @@ void grid_clear_line(ScreenGrid *grid, unsigned off, int width, bool valid) void grid_invalidate(ScreenGrid *grid) { - (void)memset(grid->attrs, -1, grid->Rows * grid->Columns * sizeof(sattr_T)); + (void)memset(grid->attrs, -1, sizeof(sattr_T) * grid->Rows * grid->Columns); } bool grid_invalid_row(ScreenGrid *grid, int row) @@ -6891,8 +6922,6 @@ void grid_ins_lines(ScreenGrid *grid, int row, int line_count, int end, int col, if (!grid->throttled) { ui_call_grid_scroll(grid->handle, row, end, col, col+width, -line_count, 0); } - - return; } /// delete lines on the screen and move lines up. @@ -6943,8 +6972,6 @@ void grid_del_lines(ScreenGrid *grid, int row, int line_count, int end, int col, if (!grid->throttled) { ui_call_grid_scroll(grid->handle, row, end, col, col+width, line_count, 0); } - - return; } @@ -7326,7 +7353,7 @@ void draw_tabline(void) if (room > 0) { // Get buffer name in NameBuff[] get_trans_bufname(cwp->w_buffer); - (void)shorten_dir(NameBuff); + shorten_dir(NameBuff); len = vim_strsize(NameBuff); p = NameBuff; while (len > room) { diff --git a/src/nvim/search.c b/src/nvim/search.c index 906c9a6f47..682fa417a9 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -26,6 +26,7 @@ #include "nvim/func_attr.h" #include "nvim/getchar.h" #include "nvim/indent.h" +#include "nvim/indent_c.h" #include "nvim/main.h" #include "nvim/mark.h" #include "nvim/mbyte.h" @@ -2313,12 +2314,9 @@ pos_T *findmatchlimit(oparg_T *oap, int initc, int flags, int64_t maxtravel) return (pos_T *)NULL; // never found it } -/* - * Check if line[] contains a / / comment. - * Return MAXCOL if not, otherwise return the column. - * TODO: skip strings. - */ -static int check_linecomment(const char_u *line) +/// Check if line[] contains a / / comment. +/// @returns MAXCOL if not, otherwise return the column. +int check_linecomment(const char_u *line) { const char_u *p = line; // scan from start // skip Lispish one-line comments @@ -2338,7 +2336,8 @@ static int check_linecomment(const char_u *line) in_str = true; } } else if (!in_str && ((p - line) < 2 - || (*(p - 1) != '\\' && *(p - 2) != '#'))) { + || (*(p - 1) != '\\' && *(p - 2) != '#')) + && !is_pos_in_string(line, (colnr_T)(p - line))) { break; // found! } p++; @@ -2348,9 +2347,11 @@ static int check_linecomment(const char_u *line) } } else { while ((p = vim_strchr(p, '/')) != NULL) { - // accept a double /, unless it's preceded with * and followed by *, - // because * / / * is an end and start of a C comment - if (p[1] == '/' && (p == line || p[-1] != '*' || p[2] != '*')) { + // Accept a double /, unless it's preceded with * and followed by *, + // because * / / * is an end and start of a C comment. + // Only accept the position if it is not inside a string. + if (p[1] == '/' && (p == line || p[-1] != '*' || p[2] != '*') + && !is_pos_in_string(line, (colnr_T)(p - line))) { break; } ++p; @@ -4763,6 +4764,536 @@ the_end: restore_last_search_pattern(); } +/// Fuzzy string matching +/// +/// Ported from the lib_fts library authored by Forrest Smith. +/// https://github.com/forrestthewoods/lib_fts/tree/master/code +/// +/// The following blog describes the fuzzy matching algorithm: +/// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/ +/// +/// Each matching string is assigned a score. The following factors are checked: +/// - Matched letter +/// - Unmatched letter +/// - Consecutively matched letters +/// - Proximity to start +/// - Letter following a separator (space, underscore) +/// - Uppercase letter following lowercase (aka CamelCase) +/// +/// Matched letters are good. Unmatched letters are bad. Matching near the start +/// is good. Matching the first letter in the middle of a phrase is good. +/// Matching the uppercase letters in camel case entries is good. +/// +/// The score assigned for each factor is explained below. +/// File paths are different from file names. File extensions may be ignorable. +/// Single words care about consecutive matches but not separators or camel +/// case. +/// Score starts at 100 +/// Matched letter: +0 points +/// Unmatched letter: -1 point +/// Consecutive match bonus: +15 points +/// First letter bonus: +15 points +/// Separator bonus: +30 points +/// Camel case bonus: +30 points +/// Unmatched leading letter: -5 points (max: -15) +/// +/// There is some nuance to this. Scores don’t have an intrinsic meaning. The +/// score range isn’t 0 to 100. It’s roughly [50, 150]. Longer words have a +/// lower minimum score due to unmatched letter penalty. Longer search patterns +/// have a higher maximum score due to match bonuses. +/// +/// Separator and camel case bonus is worth a LOT. Consecutive matches are worth +/// quite a bit. +/// +/// There is a penalty if you DON’T match the first three letters. Which +/// effectively rewards matching near the start. However there’s no difference +/// in matching between the middle and end. +/// +/// There is not an explicit bonus for an exact match. Unmatched letters receive +/// a penalty. So shorter strings and closer matches are worth more. +typedef struct { + int idx; ///< used for stable sort + listitem_T *item; + int score; + list_T *lmatchpos; +} fuzzyItem_T; + +/// bonus for adjacent matches; this is higher than SEPARATOR_BONUS so that +/// matching a whole word is preferred. +#define SEQUENTIAL_BONUS 40 +/// bonus if match occurs after a path separator +#define PATH_SEPARATOR_BONUS 30 +/// bonus if match occurs after a word separator +#define WORD_SEPARATOR_BONUS 25 +/// bonus if match is uppercase and prev is lower +#define CAMEL_BONUS 30 +/// bonus if the first letter is matched +#define FIRST_LETTER_BONUS 15 +/// penalty applied for every letter in str before the first match +#define LEADING_LETTER_PENALTY -5 +/// maximum penalty for leading letters +#define MAX_LEADING_LETTER_PENALTY -15 +/// penalty for every letter that doesn't match +#define UNMATCHED_LETTER_PENALTY -1 +/// penalty for gap in matching positions (-2 * k) +#define GAP_PENALTY -2 +/// Score for a string that doesn't fuzzy match the pattern +#define SCORE_NONE -9999 + +#define FUZZY_MATCH_RECURSION_LIMIT 10 + +/// Compute a score for a fuzzy matched string. The matching character locations +/// are in 'matches'. +static int fuzzy_match_compute_score(const char_u *const str, const int strSz, + const uint32_t *const matches, const int numMatches) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + assert(numMatches > 0); // suppress clang "result of operation is garbage" + // Initialize score + int score = 100; + + // Apply leading letter penalty + int penalty = LEADING_LETTER_PENALTY * matches[0]; + if (penalty < MAX_LEADING_LETTER_PENALTY) { + penalty = MAX_LEADING_LETTER_PENALTY; + } + score += penalty; + + // Apply unmatched penalty + const int unmatched = strSz - numMatches; + score += UNMATCHED_LETTER_PENALTY * unmatched; + + // Apply ordering bonuses + for (int i = 0; i < numMatches; i++) { + const uint32_t currIdx = matches[i]; + + if (i > 0) { + const uint32_t prevIdx = matches[i - 1]; + + // Sequential + if (currIdx == prevIdx + 1) { + score += SEQUENTIAL_BONUS; + } else { + score += GAP_PENALTY * (currIdx - prevIdx); + } + } + + // Check for bonuses based on neighbor character value + if (currIdx > 0) { + // Camel case + const char_u *p = str; + int neighbor; + + for (uint32_t sidx = 0; sidx < currIdx; sidx++) { + neighbor = utf_ptr2char(p); + MB_PTR_ADV(p); + } + const int curr = utf_ptr2char(p); + + if (mb_islower(neighbor) && mb_isupper(curr)) { + score += CAMEL_BONUS; + } + + // Bonus if the match follows a separator character + if (neighbor == '/' || neighbor == '\\') { + score += PATH_SEPARATOR_BONUS; + } else if (neighbor == ' ' || neighbor == '_') { + score += WORD_SEPARATOR_BONUS; + } + } else { + // First letter + score += FIRST_LETTER_BONUS; + } + } + return score; +} + +/// Perform a recursive search for fuzzy matching 'fuzpat' in 'str'. +/// @return the number of matching characters. +static int fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, uint32_t strIdx, + int *const outScore, const char_u *const strBegin, + const int strLen, const uint32_t *const srcMatches, + uint32_t *const matches, const int maxMatches, int nextMatch, + int *const recursionCount) + FUNC_ATTR_NONNULL_ARG(1, 2, 4, 5, 8, 11) FUNC_ATTR_WARN_UNUSED_RESULT +{ + // Recursion params + bool recursiveMatch = false; + uint32_t bestRecursiveMatches[MAX_FUZZY_MATCHES]; + int bestRecursiveScore = 0; + + // Count recursions + (*recursionCount)++; + if (*recursionCount >= FUZZY_MATCH_RECURSION_LIMIT) { + return 0; + } + + // Detect end of strings + if (*fuzpat == NUL || *str == NUL) { + return 0; + } + + // Loop through fuzpat and str looking for a match + bool first_match = true; + while (*fuzpat != NUL && *str != NUL) { + const int c1 = utf_ptr2char(fuzpat); + const int c2 = utf_ptr2char(str); + + // Found match + if (mb_tolower(c1) == mb_tolower(c2)) { + // Supplied matches buffer was too short + if (nextMatch >= maxMatches) { + return 0; + } + + // "Copy-on-Write" srcMatches into matches + if (first_match && srcMatches != NULL) { + memcpy(matches, srcMatches, nextMatch * sizeof(srcMatches[0])); + first_match = false; + } + + // Recursive call that "skips" this match + uint32_t recursiveMatches[MAX_FUZZY_MATCHES]; + int recursiveScore = 0; + const char_u *const next_char = str + utfc_ptr2len(str); + if (fuzzy_match_recursive(fuzpat, next_char, strIdx + 1, &recursiveScore, strBegin, strLen, + matches, recursiveMatches, + sizeof(recursiveMatches) / sizeof(recursiveMatches[0]), nextMatch, + recursionCount)) { + // Pick best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + memcpy(bestRecursiveMatches, recursiveMatches, + MAX_FUZZY_MATCHES * sizeof(recursiveMatches[0])); + bestRecursiveScore = recursiveScore; + } + recursiveMatch = true; + } + + // Advance + matches[nextMatch++] = strIdx; + MB_PTR_ADV(fuzpat); + } + MB_PTR_ADV(str); + strIdx++; + } + + // Determine if full fuzpat was matched + const bool matched = *fuzpat == NUL; + + // Calculate score + if (matched) { + *outScore = fuzzy_match_compute_score(strBegin, strLen, matches, nextMatch); + } + + // Return best result + if (recursiveMatch && (!matched || bestRecursiveScore > *outScore)) { + // Recursive score is better than "this" + memcpy(matches, bestRecursiveMatches, maxMatches * sizeof(matches[0])); + *outScore = bestRecursiveScore; + return nextMatch; + } else if (matched) { + return nextMatch; // "this" score is better than recursive + } + + return 0; // no match +} + +/// fuzzy_match() +/// +/// Performs exhaustive search via recursion to find all possible matches and +/// match with highest score. +/// Scores values have no intrinsic meaning. Possible score range is not +/// normalized and varies with pattern. +/// Recursion is limited internally (default=10) to prevent degenerate cases +/// (pat_arg="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"). +/// Uses char_u for match indices. Therefore patterns are limited to +/// MAX_FUZZY_MATCHES characters. +/// +/// @return true if 'pat_arg' matches 'str'. Also returns the match score in +/// 'outScore' and the matching character positions in 'matches'. +bool fuzzy_match(char_u *const str, const char_u *const pat_arg, const bool matchseq, + int *const outScore, uint32_t *const matches, const int maxMatches) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const int len = mb_charlen(str); + bool complete = false; + int numMatches = 0; + + *outScore = 0; + + char_u *const save_pat = vim_strsave(pat_arg); + char_u *pat = save_pat; + char_u *p = pat; + + // Try matching each word in 'pat_arg' in 'str' + while (true) { + if (matchseq) { + complete = true; + } else { + // Extract one word from the pattern (separated by space) + p = skipwhite(p); + if (*p == NUL) { + break; + } + pat = p; + while (*p != NUL && !ascii_iswhite(utf_ptr2char(p))) { + MB_PTR_ADV(p); + } + if (*p == NUL) { // processed all the words + complete = true; + } + *p = NUL; + } + + int score = 0; + int recursionCount = 0; + const int matchCount + = fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL, matches + numMatches, + maxMatches - numMatches, 0, &recursionCount); + if (matchCount == 0) { + numMatches = 0; + break; + } + + // Accumulate the match score and the number of matches + *outScore += score; + numMatches += matchCount; + + if (complete) { + break; + } + + // try matching the next word + p++; + } + + xfree(save_pat); + return numMatches != 0; +} + +/// Sort the fuzzy matches in the descending order of the match score. +/// For items with same score, retain the order using the index (stable sort) +static int fuzzy_match_item_compare(const void *const s1, const void *const s2) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + const int v1 = ((const fuzzyItem_T *)s1)->score; + const int v2 = ((const fuzzyItem_T *)s2)->score; + const int idx1 = ((const fuzzyItem_T *)s1)->idx; + const int idx2 = ((const fuzzyItem_T *)s2)->idx; + + return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1; +} + +/// Fuzzy search the string 'str' in a list of 'items' and return the matching +/// strings in 'fmatchlist'. +/// If 'matchseq' is true, then for multi-word search strings, match all the +/// words in sequence. +/// If 'items' is a list of strings, then search for 'str' in the list. +/// If 'items' is a list of dicts, then either use 'key' to lookup the string +/// for each item or use 'item_cb' Funcref function to get the string. +/// If 'retmatchpos' is true, then return a list of positions where 'str' +/// matches for each item. +static void fuzzy_match_in_list(list_T *const items, char_u *const str, const bool matchseq, + const char_u *const key, Callback *const item_cb, + const bool retmatchpos, list_T *const fmatchlist) + FUNC_ATTR_NONNULL_ARG(2, 5, 7) +{ + const long len = tv_list_len(items); + if (len == 0) { + return; + } + + fuzzyItem_T *const ptrs = xcalloc(len, sizeof(fuzzyItem_T)); + long i = 0; + bool found_match = false; + uint32_t matches[MAX_FUZZY_MATCHES]; + + // For all the string items in items, get the fuzzy matching score + TV_LIST_ITER(items, li, { + ptrs[i].idx = i; + ptrs[i].item = li; + ptrs[i].score = SCORE_NONE; + char_u *itemstr = NULL; + typval_T rettv; + rettv.v_type = VAR_UNKNOWN; + const typval_T *const tv = TV_LIST_ITEM_TV(li); + if (tv->v_type == VAR_STRING) { // list of strings + itemstr = tv->vval.v_string; + } else if (tv->v_type == VAR_DICT && (key != NULL || item_cb->type != kCallbackNone)) { + // For a dict, either use the specified key to lookup the string or + // use the specified callback function to get the string. + if (key != NULL) { + itemstr = (char_u *)tv_dict_get_string(tv->vval.v_dict, (const char *)key, false); + } else { + typval_T argv[2]; + + // Invoke the supplied callback (if any) to get the dict item + tv->vval.v_dict->dv_refcount++; + argv[0].v_type = VAR_DICT; + argv[0].vval.v_dict = tv->vval.v_dict; + argv[1].v_type = VAR_UNKNOWN; + if (callback_call(item_cb, 1, argv, &rettv)) { + if (rettv.v_type == VAR_STRING) { + itemstr = rettv.vval.v_string; + } + } + tv_dict_unref(tv->vval.v_dict); + } + } + + int score; + if (itemstr != NULL && fuzzy_match(itemstr, str, matchseq, &score, matches, + sizeof(matches) / sizeof(matches[0]))) { + // Copy the list of matching positions in itemstr to a list, if + // 'retmatchpos' is set. + if (retmatchpos) { + ptrs[i].lmatchpos = tv_list_alloc(kListLenMayKnow); + int j = 0; + const char_u *p = str; + while (*p != NUL) { + if (!ascii_iswhite(utf_ptr2char(p))) { + tv_list_append_number(ptrs[i].lmatchpos, matches[j]); + j++; + } + MB_PTR_ADV(p); + } + } + ptrs[i].score = score; + found_match = true; + } + i++; + tv_clear(&rettv); + }); + + if (found_match) { + // Sort the list by the descending order of the match score + qsort(ptrs, len, sizeof(fuzzyItem_T), fuzzy_match_item_compare); + + // For matchfuzzy(), return a list of matched strings. + // ['str1', 'str2', 'str3'] + // For matchfuzzypos(), return a list with three items. + // The first item is a list of matched strings. The second item + // is a list of lists where each list item is a list of matched + // character positions. The third item is a list of matching scores. + // [['str1', 'str2', 'str3'], [[1, 3], [1, 3], [1, 3]]] + list_T *l; + if (retmatchpos) { + const listitem_T *const li = tv_list_find(fmatchlist, 0); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + } else { + l = fmatchlist; + } + + // Copy the matching strings with a valid score to the return list + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_tv(l, TV_LIST_ITEM_TV(ptrs[i].item)); + } + + // next copy the list of matching positions + if (retmatchpos) { + const listitem_T *li = tv_list_find(fmatchlist, -2); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_list(l, ptrs[i].lmatchpos); + } + + // copy the matching scores + li = tv_list_find(fmatchlist, -1); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_number(l, ptrs[i].score); + } + } + } + xfree(ptrs); +} + +/// Do fuzzy matching. Returns the list of matched strings in 'rettv'. +/// If 'retmatchpos' is true, also returns the matching character positions. +static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv, + const bool retmatchpos) + FUNC_ATTR_NONNULL_ALL +{ + // validate and get the arguments + if (argvars[0].v_type != VAR_LIST || argvars[0].vval.v_list == NULL) { + semsg(_(e_listarg), retmatchpos ? "matchfuzzypos()" : "matchfuzzy()"); + return; + } + if (argvars[1].v_type != VAR_STRING || argvars[1].vval.v_string == NULL) { + semsg(_(e_invarg2), tv_get_string(&argvars[1])); + return; + } + + Callback cb = CALLBACK_NONE; + const char_u *key = NULL; + bool matchseq = false; + if (argvars[2].v_type != VAR_UNKNOWN) { + if (argvars[2].v_type != VAR_DICT || argvars[2].vval.v_dict == NULL) { + emsg(_(e_dictreq)); + return; + } + + // To search a dict, either a callback function or a key can be + // specified. + dict_T *const d = argvars[2].vval.v_dict; + const dictitem_T *const di = tv_dict_find(d, "key", -1); + if (di != NULL) { + if (di->di_tv.v_type != VAR_STRING || di->di_tv.vval.v_string == NULL + || *di->di_tv.vval.v_string == NUL) { + semsg(_(e_invarg2), tv_get_string(&di->di_tv)); + return; + } + key = (const char_u *)tv_get_string(&di->di_tv); + } else if (!tv_dict_get_callback(d, "text_cb", -1, &cb)) { + semsg(_(e_invargval), "text_cb"); + return; + } + if (tv_dict_find(d, "matchseq", -1) != NULL) { + matchseq = true; + } + } + + // get the fuzzy matches + tv_list_alloc_ret(rettv, retmatchpos ? 3 : kListLenUnknown); + if (retmatchpos) { + // For matchfuzzypos(), a list with three items are returned. First + // item is a list of matching strings, the second item is a list of + // lists with matching positions within each string and the third item + // is the list of scores of the matches. + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + } + + fuzzy_match_in_list(argvars[0].vval.v_list, (char_u *)tv_get_string(&argvars[1]), matchseq, key, + &cb, retmatchpos, rettv->vval.v_list); + callback_free(&cb); +} + +/// "matchfuzzy()" function +void f_matchfuzzy(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + do_fuzzymatch(argvars, rettv, false); +} + +/// "matchfuzzypos()" function +void f_matchfuzzypos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + do_fuzzymatch(argvars, rettv, true); +} + /// Find identifiers or defines in included files. /// If p_ic && (compl_cont_status & CONT_SOL) then ptr must be in lowercase. /// diff --git a/src/nvim/search.h b/src/nvim/search.h index 15b8d41f39..53059cc1ea 100644 --- a/src/nvim/search.h +++ b/src/nvim/search.h @@ -55,6 +55,9 @@ #define SEARCH_STAT_DEF_MAX_COUNT 99 #define SEARCH_STAT_BUF_LEN 12 +/// Maximum number of characters that can be fuzzy matched +#define MAX_FUZZY_MATCHES 256 + /// Structure containing offset definition for the last search pattern /// /// @note Only offset for the last search pattern is used, not for the last diff --git a/src/nvim/shada.c b/src/nvim/shada.c index e75a244031..b909888783 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -831,7 +831,7 @@ static int shada_read_file(const char *const file, const int flags) ShaDaReadDef sd_reader; const int of_ret = open_shada_file_for_reading(fname, &sd_reader); - if (p_verbose > 0) { + if (p_verbose > 1) { verbose_enter(); smsg(_("Reading ShaDa file \"%s\"%s%s%s%s"), fname, @@ -3036,7 +3036,7 @@ shada_write_file_nomerge: {} return FAIL; } - if (p_verbose > 0) { + if (p_verbose > 1) { verbose_enter(); smsg(_("Writing ShaDa file \"%s\""), fname); verbose_leave(); diff --git a/src/nvim/sign.c b/src/nvim/sign.c index a308df07d1..8b41781c98 100644 --- a/src/nvim/sign.c +++ b/src/nvim/sign.c @@ -1501,28 +1501,28 @@ static void sign_getinfo(sign_T *sp, dict_T *retdict) if (p == NULL) { p = "NONE"; } - tv_dict_add_str(retdict, S_LEN("linehl"), (char *)p); + tv_dict_add_str(retdict, S_LEN("linehl"), p); } if (sp->sn_text_hl > 0) { p = get_highlight_name_ext(NULL, sp->sn_text_hl - 1, false); if (p == NULL) { p = "NONE"; } - tv_dict_add_str(retdict, S_LEN("texthl"), (char *)p); + tv_dict_add_str(retdict, S_LEN("texthl"), p); } if (sp->sn_cul_hl > 0) { p = get_highlight_name_ext(NULL, sp->sn_cul_hl - 1, false); if (p == NULL) { p = "NONE"; } - tv_dict_add_str(retdict, S_LEN("culhl"), (char *)p); + tv_dict_add_str(retdict, S_LEN("culhl"), p); } if (sp->sn_num_hl > 0) { p = get_highlight_name_ext(NULL, sp->sn_num_hl - 1, false); if (p == NULL) { p = "NONE"; } - tv_dict_add_str(retdict, S_LEN("numhl"), (char *)p); + tv_dict_add_str(retdict, S_LEN("numhl"), p); } } diff --git a/src/nvim/spell.c b/src/nvim/spell.c index bd31e98faa..1296d410f6 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -219,7 +219,7 @@ typedef struct { #define SCORE_THRES3 100 // word count threshold for COMMON3 // When trying changed soundfold words it becomes slow when trying more than -// two changes. With less then two changes it's slightly faster but we miss a +// two changes. With less than two changes it's slightly faster but we miss a // few good suggestions. In rare cases we need to try three of four changes. #define SCORE_SFMAX1 200 // maximum score for first try #define SCORE_SFMAX2 300 // maximum score for second try @@ -1628,7 +1628,7 @@ size_t spell_move_to(win_T *wp, int dir, bool allwords, bool curline, hlf_T *att } // For spell checking: concatenate the start of the following line "line" into -// "buf", blanking-out special characters. Copy less then "maxlen" bytes. +// "buf", blanking-out special characters. Copy less than "maxlen" bytes. // Keep the blanks at the start of the next line, this is used in win_line() // to skip those bytes if the word was OK. void spell_cat_line(char_u *buf, char_u *line, int maxlen) @@ -4082,7 +4082,7 @@ static void suggest_trie_walk(suginfo_T *su, langp_T *lp, char_u *fword, bool so // char, e.g., "thes," -> "these". p = fword + sp->ts_fidx; MB_PTR_BACK(fword, p); - if (!spell_iswordp(p, curwin)) { + if (!spell_iswordp(p, curwin) && *preword != NUL) { p = preword + STRLEN(preword); MB_PTR_BACK(preword, p); if (spell_iswordp(p, curwin)) { @@ -6106,7 +6106,7 @@ static void spell_soundfold_wsal(slang_T *slang, char_u *inword, char_u *res) for (; ((ws = smp[n].sm_lead_w)[0] & 0xff) == (c & 0xff) && ws[0] != NUL; ++n) { // Quickly skip entries that don't match the word. Most - // entries are less then three chars, optimize for that. + // entries are less than three chars, optimize for that. if (c != ws[0]) { continue; } @@ -7057,7 +7057,7 @@ void spell_dump_compl(char_u *pat, int ic, Direction *dir, int dumpflags_arg) arridx[depth] = idxs[n]; curi[depth] = 1; - // Check if this characters matches with the pattern. + // Check if this character matches with the pattern. // If not skip the whole tree below it. // Always ignore case here, dump_word() will check // proper case later. This isn't exactly right when diff --git a/src/nvim/spellfile.c b/src/nvim/spellfile.c index 42bb3c61a5..d7b220b3f6 100644 --- a/src/nvim/spellfile.c +++ b/src/nvim/spellfile.c @@ -4395,7 +4395,7 @@ static int write_vim_spell(spellinfo_T *spin, char_u *fname) // // The table with character flags and the table for case folding. // This makes sure the same characters are recognized as word characters - // when generating an when using a spell file. + // when generating and when using a spell file. // Skip this for ASCII, the table may conflict with the one used for // 'encoding'. // Also skip this for an .add.spl file, the main spell file must contain @@ -5122,7 +5122,7 @@ static int sug_filltable(spellinfo_T *spin, wordnode_T *node, int startwordnr, g wordnr++; // Remove extra NUL entries, we no longer need them. We don't - // bother freeing the nodes, the won't be reused anyway. + // bother freeing the nodes, they won't be reused anyway. while (p->wn_sibling != NULL && p->wn_sibling->wn_byte == NUL) { p->wn_sibling = p->wn_sibling->wn_sibling; } @@ -5654,7 +5654,7 @@ void spell_add_word(char_u *word, int len, SpellAddType what, int idx, bool undo // If the .add file is edited somewhere, reload it. if (buf != NULL) { - buf_reload(buf, buf->b_orig_mode); + buf_reload(buf, buf->b_orig_mode, false); } redraw_all_later(SOME_VALID); diff --git a/src/nvim/state.c b/src/nvim/state.c index 68bc76660d..f9a3aaab7f 100644 --- a/src/nvim/state.c +++ b/src/nvim/state.c @@ -12,6 +12,7 @@ #include "nvim/lib/kvec.h" #include "nvim/log.h" #include "nvim/main.h" +#include "nvim/option.h" #include "nvim/option_defs.h" #include "nvim/os/input.h" #include "nvim/state.h" @@ -38,10 +39,16 @@ void state_enter(VimState *s) int key; getkey: - if (char_avail() || using_script() || input_available()) { - // Don't block for events if there's a character already available for - // processing. Characters can come from mappings, scripts and other - // sources, so this scenario is very common. + // Expand mappings first by calling vpeekc() directly. + // - If vpeekc() returns non-NUL, there is a character already available for processing, so + // don't block for events. vgetc() may still block, in case of an incomplete UTF-8 sequence. + // - If vpeekc() returns NUL, vgetc() will block, and there are three cases: + // - There is no input available. + // - All of available input maps to an empty string. + // - There is an incomplete mapping. + // A blocking wait for a character should only be done in the third case, which is the only + // case of the three where typebuf.tb_len > 0 after vpeekc() returns NUL. + if (vpeekc() != NUL || typebuf.tb_len > 0) { key = safe_vgetc(); } else if (!multiqueue_empty(main_loop.events)) { // Event was made available after the last multiqueue_process_events call @@ -54,9 +61,11 @@ getkey: // mapping engine. (void)os_inchar(NULL, 0, -1, 0, main_loop.events); // If an event was put into the queue, we send K_EVENT directly. - key = !multiqueue_empty(main_loop.events) - ? K_EVENT - : safe_vgetc(); + if (!multiqueue_empty(main_loop.events)) { + key = K_EVENT; + } else { + goto getkey; + } } if (key == K_EVENT) { @@ -107,15 +116,17 @@ void state_handle_k_event(void) /// Return true if in the current mode we need to use virtual. bool virtual_active(void) { + unsigned int cur_ve_flags = get_ve_flags(); + // While an operator is being executed we return "virtual_op", because // VIsual_active has already been reset, thus we can't check for "block" // being used. if (virtual_op != kNone) { return virtual_op; } - return ve_flags == VE_ALL - || ((ve_flags & VE_BLOCK) && VIsual_active && VIsual_mode == Ctrl_V) - || ((ve_flags & VE_INSERT) && (State & INSERT)); + return cur_ve_flags == VE_ALL + || ((cur_ve_flags & VE_BLOCK) && VIsual_active && VIsual_mode == Ctrl_V) + || ((cur_ve_flags & VE_INSERT) && (State & INSERT)); } /// VISUAL, SELECTMODE and OP_PENDING State are never set, they are equal to @@ -180,7 +191,7 @@ char *get_mode(void) buf[1] = 'x'; } } - } else if (State & CMDLINE) { + } else if ((State & CMDLINE) || exmode_active) { buf[0] = 'c'; if (exmode_active) { buf[1] = 'v'; diff --git a/src/nvim/strings.c b/src/nvim/strings.c index e2a8108c45..291138ef23 100644 --- a/src/nvim/strings.c +++ b/src/nvim/strings.c @@ -1001,22 +1001,20 @@ int vim_vsnprintf_typval(char *str, size_t str_m, const char *fmt, va_list ap, t - str_arg); } if (fmt_spec == 'S') { - if (min_field_width != 0) { - min_field_width += (strlen(str_arg) - - mb_string2cells((char_u *)str_arg)); - } - if (precision) { - char_u *p1; - size_t i = 0; - - for (p1 = (char_u *)str_arg; *p1; - p1 += utfc_ptr2len(p1)) { - i += (size_t)utf_ptr2cells(p1); - if (i > precision) { - break; - } + char_u *p1; + size_t i; + + for (i = 0, p1 = (char_u *)str_arg; *p1; p1 += utfc_ptr2len(p1)) { + size_t cell = (size_t)utf_ptr2cells(p1); + if (precision_specified && i + cell > precision) { + break; } - str_arg_l = (size_t)(p1 - (char_u *)str_arg); + i += cell; + } + + str_arg_l = (size_t)(p1 - (char_u *)str_arg); + if (min_field_width != 0) { + min_field_width += str_arg_l - i; } } break; diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index a9447165c2..db10b71d38 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -27,6 +27,7 @@ #include "nvim/highlight.h" #include "nvim/indent_c.h" #include "nvim/keymap.h" +#include "nvim/lua/executor.h" #include "nvim/macros.h" #include "nvim/mbyte.h" #include "nvim/memline.h" @@ -3112,9 +3113,9 @@ static void syn_cmd_conceal(exarg_T *eap, int syncing) next = skiptowhite(arg); if (*arg == NUL) { if (curwin->w_s->b_syn_conceal) { - msg(_("syntax conceal on")); + msg("syntax conceal on"); } else { - msg(_("syntax conceal off")); + msg("syntax conceal off"); } } else if (STRNICMP(arg, "on", 2) == 0 && next - arg == 2) { curwin->w_s->b_syn_conceal = true; @@ -3141,9 +3142,9 @@ static void syn_cmd_case(exarg_T *eap, int syncing) next = skiptowhite(arg); if (*arg == NUL) { if (curwin->w_s->b_syn_ic) { - msg(_("syntax case ignore")); + msg("syntax case ignore"); } else { - msg(_("syntax case match")); + msg("syntax case match"); } } else if (STRNICMP(arg, "match", 5) == 0 && next - arg == 5) { curwin->w_s->b_syn_ic = false; @@ -3168,9 +3169,9 @@ static void syn_cmd_foldlevel(exarg_T *eap, int syncing) if (*arg == NUL) { switch (curwin->w_s->b_syn_foldlevel) { case SYNFLD_START: - msg(_("syntax foldlevel start")); break; + msg("syntax foldlevel start"); break; case SYNFLD_MINIMUM: - msg(_("syntax foldlevel minimum")); break; + msg("syntax foldlevel minimum"); break; default: break; } @@ -3209,11 +3210,11 @@ static void syn_cmd_spell(exarg_T *eap, int syncing) next = skiptowhite(arg); if (*arg == NUL) { if (curwin->w_s->b_syn_spell == SYNSPL_TOP) { - msg(_("syntax spell toplevel")); + msg("syntax spell toplevel"); } else if (curwin->w_s->b_syn_spell == SYNSPL_NOTOP) { - msg(_("syntax spell notoplevel")); + msg("syntax spell notoplevel"); } else { - msg(_("syntax spell default")); + msg("syntax spell default"); } } else if (STRNICMP(arg, "toplevel", 8) == 0 && next - arg == 8) { curwin->w_s->b_syn_spell = SYNSPL_TOP; @@ -3245,7 +3246,7 @@ static void syn_cmd_iskeyword(exarg_T *eap, int syncing) if (*arg == NUL) { msg_puts("\n"); if (curwin->w_s->b_syn_isk != empty_option) { - msg_puts(_("syntax iskeyword ")); + msg_puts("syntax iskeyword "); msg_outtrans(curwin->w_s->b_syn_isk); } else { msg_outtrans((char_u *)_("syntax iskeyword not set")); @@ -6714,6 +6715,100 @@ int lookup_color(const int idx, const bool foreground, TriState *const boldp) return color; } +void set_hl_group(int id, HlAttrs attrs, Dict(highlight) *dict, int link_id) +{ + int idx = id - 1; // Index is ID minus one. + + bool is_default = attrs.rgb_ae_attr & HL_DEFAULT; + + // Return if "default" was used and the group already has settings + if (is_default && hl_has_settings(idx, true)) { + return; + } + + HlGroup *g = &HL_TABLE()[idx]; + + if (link_id > 0) { + g->sg_cleared = false; + g->sg_link = link_id; + g->sg_script_ctx = current_sctx; + g->sg_script_ctx.sc_lnum += sourcing_lnum; + g->sg_set |= SG_LINK; + if (is_default) { + g->sg_deflink = link_id; + g->sg_deflink_sctx = current_sctx; + g->sg_deflink_sctx.sc_lnum += sourcing_lnum; + } + return; + } + + g->sg_cleared = false; + g->sg_link = 0; + g->sg_gui = attrs.rgb_ae_attr; + + g->sg_rgb_fg = attrs.rgb_fg_color; + g->sg_rgb_bg = attrs.rgb_bg_color; + g->sg_rgb_sp = attrs.rgb_sp_color; + + struct { + char **dest; RgbValue val; Object name; + } cattrs[] = { + { &g->sg_rgb_fg_name, g->sg_rgb_fg, HAS_KEY(dict->fg) ? dict->fg : dict->foreground }, + { &g->sg_rgb_bg_name, g->sg_rgb_bg, HAS_KEY(dict->bg) ? dict->bg : dict->background }, + { &g->sg_rgb_sp_name, g->sg_rgb_sp, HAS_KEY(dict->sp) ? dict->sp : dict->special }, + { NULL, -1, NIL }, + }; + + char hex_name[8]; + char *name; + + for (int j = 0; cattrs[j].dest; j++) { + if (cattrs[j].val < 0) { + XFREE_CLEAR(*cattrs[j].dest); + continue; + } + + if (cattrs[j].name.type == kObjectTypeString && cattrs[j].name.data.string.size) { + name = cattrs[j].name.data.string.data; + } else { + snprintf(hex_name, sizeof(hex_name), "#%06x", cattrs[j].val); + name = hex_name; + } + + if (!*cattrs[j].dest + || STRCMP(*cattrs[j].dest, name) != 0) { + xfree(*cattrs[j].dest); + *cattrs[j].dest = xstrdup(name); + } + } + + g->sg_cterm = attrs.cterm_ae_attr; + g->sg_cterm_bg = attrs.cterm_bg_color; + g->sg_cterm_fg = attrs.cterm_fg_color; + g->sg_cterm_bold = g->sg_cterm & HL_BOLD; + g->sg_blend = attrs.hl_blend; + + g->sg_script_ctx = current_sctx; + g->sg_script_ctx.sc_lnum += sourcing_lnum; + + // 'Normal' is special + if (STRCMP(g->sg_name_u, "NORMAL") == 0) { + cterm_normal_fg_color = g->sg_cterm_fg; + cterm_normal_bg_color = g->sg_cterm_bg; + normal_fg = g->sg_rgb_fg; + normal_bg = g->sg_rgb_bg; + normal_sp = g->sg_rgb_sp; + ui_default_colors_set(); + } else { + g->sg_attr = hl_get_syn_attr(0, id, attrs); + + // a cursor style uses this syn_id, make sure its attribute is updated. + if (cursor_mode_uses_syn_id(id)) { + ui_mode_info_set(); + } + } +} + /// Handle ":highlight" command /// @@ -6825,6 +6920,7 @@ void do_highlight(const char *line, const bool forceit, const bool init) hlgroup->sg_deflink = to_id; hlgroup->sg_deflink_sctx = current_sctx; hlgroup->sg_deflink_sctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&hlgroup->sg_deflink_sctx); } } @@ -6845,6 +6941,7 @@ void do_highlight(const char *line, const bool forceit, const bool init) hlgroup->sg_link = to_id; hlgroup->sg_script_ctx = current_sctx; hlgroup->sg_script_ctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&hlgroup->sg_script_ctx); hlgroup->sg_cleared = false; redraw_all_later(SOME_VALID); @@ -7225,6 +7322,7 @@ void do_highlight(const char *line, const bool forceit, const bool init) } HL_TABLE()[idx].sg_script_ctx = current_sctx; HL_TABLE()[idx].sg_script_ctx.sc_lnum += sourcing_lnum; + nlua_set_sctx(&HL_TABLE()[idx].sg_script_ctx); } xfree(key); xfree(arg); @@ -8765,6 +8863,22 @@ RgbValue name_to_color(const char *name) return -1; } +int name_to_ctermcolor(const char *name) +{ + int i; + int off = TOUPPER_ASC(*name); + for (i = ARRAY_SIZE(color_names); --i >= 0;) { + if (off == color_names[i][0] + && STRICMP(name+1, color_names[i]+1) == 0) { + break; + } + } + if (i < 0) { + return -1; + } + TriState bold = kNone; + return lookup_color(i, false, &bold); +} /************************************** * End of Highlighting stuff * diff --git a/src/nvim/tag.c b/src/nvim/tag.c index a10a2a0c32..32d72218c8 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -2960,7 +2960,7 @@ static int jumpto_tag(const char_u *lbuf_arg, int forceit, int keep_help) } else { RedrawingDisabled--; if (postponed_split) { // close the window - win_close(curwin, false); + win_close(curwin, false, false); postponed_split = 0; } } @@ -3408,7 +3408,7 @@ static void tagstack_push_items(win_T *wp, list_T *l) if ((di = tv_dict_find(itemdict, "from", -1)) == NULL) { continue; } - if (list2fpos(&di->di_tv, &mark, &fnum, NULL) != OK) { + if (list2fpos(&di->di_tv, &mark, &fnum, NULL, false) != OK) { continue; } if ((tagname = (char_u *)tv_dict_get_string(itemdict, "tagname", true)) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 6add541ad4..1c26e46a21 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -135,7 +135,6 @@ struct terminal { int row, col; bool visible; } cursor; - int pressed_button; // which mouse button is pressed bool pending_resize; // pending width/height bool color_set[16]; @@ -173,6 +172,11 @@ void terminal_teardown(void) pmap_init(ptr_t, &invalidated_terminals); } +static void term_output_callback(const char *s, size_t len, void *user_data) +{ + terminal_send((Terminal *)user_data, (char *)s, len); +} + // public API {{{ Terminal *terminal_open(buf_T *buf, TerminalOptions opts) @@ -196,6 +200,7 @@ Terminal *terminal_open(buf_T *buf, TerminalOptions opts) vterm_screen_set_callbacks(rv->vts, &vterm_screen_callbacks, rv); vterm_screen_set_damage_merge(rv->vts, VTERM_DAMAGE_SCROLL); vterm_screen_reset(rv->vts, 1); + vterm_output_set_callback(rv->vt, term_output_callback, rv); // force a initial refresh of the screen to ensure the buffer will always // have as many lines as screen rows when refresh_scrollback is called rv->invalid_start = 0; @@ -529,6 +534,10 @@ static int terminal_execute(VimState *state, int key) do_cmdline(NULL, getcmdkeycmd, NULL, 0); break; + case K_LUA: + map_execute_lua(); + break; + case Ctrl_N: if (s->got_bsl) { return 0; @@ -633,7 +642,6 @@ void terminal_paste(long count, char_u **y_array, size_t y_size) return; } vterm_keyboard_start_paste(curbuf->terminal->vt); - terminal_flush_output(curbuf->terminal); size_t buff_len = STRLEN(y_array[0]); char_u *buff = xmalloc(buff_len); for (int i = 0; i < count; i++) { // -V756 @@ -664,14 +672,6 @@ void terminal_paste(long count, char_u **y_array, size_t y_size) } xfree(buff); vterm_keyboard_end_paste(curbuf->terminal->vt); - terminal_flush_output(curbuf->terminal); -} - -void terminal_flush_output(Terminal *term) -{ - size_t len = vterm_output_read(term->vt, term->textbuf, - sizeof(term->textbuf)); - terminal_send(term, term->textbuf, len); } void terminal_send_key(Terminal *term, int c) @@ -690,8 +690,6 @@ void terminal_send_key(Terminal *term, int c) } else { vterm_keyboard_unichar(term->vt, (uint32_t)c, mod); } - - terminal_flush_output(term); } void terminal_receive(Terminal *term, char *data, size_t len) @@ -1209,21 +1207,12 @@ static VTermKey convert_key(int key, VTermModifier *statep) } } -static void mouse_action(Terminal *term, int button, int row, int col, bool drag, VTermModifier mod) +static void mouse_action(Terminal *term, int button, int row, int col, bool pressed, + VTermModifier mod) { - if (term->pressed_button && (term->pressed_button != button || !drag)) { - // release the previous button - vterm_mouse_button(term->vt, term->pressed_button, 0, mod); - term->pressed_button = 0; - } - - // move the mouse vterm_mouse_move(term->vt, row, col, mod); - - if (!term->pressed_button) { - // press the button if not already pressed - vterm_mouse_button(term->vt, button, 1, mod); - term->pressed_button = button; + if (button) { + vterm_mouse_button(term->vt, button, pressed, mod); } } @@ -1242,35 +1231,35 @@ static bool send_mouse_event(Terminal *term, int c) // event in the terminal window and mouse events was enabled by the // program. translate and forward the event int button; - bool drag = false; + bool pressed = false; switch (c) { case K_LEFTDRAG: - drag = true; FALLTHROUGH; case K_LEFTMOUSE: + pressed = true; FALLTHROUGH; + case K_LEFTRELEASE: button = 1; break; case K_MOUSEMOVE: - drag = true; button = 0; break; + button = 0; break; case K_MIDDLEDRAG: - drag = true; FALLTHROUGH; case K_MIDDLEMOUSE: + pressed = true; FALLTHROUGH; + case K_MIDDLERELEASE: button = 2; break; case K_RIGHTDRAG: - drag = true; FALLTHROUGH; case K_RIGHTMOUSE: + pressed = true; FALLTHROUGH; + case K_RIGHTRELEASE: button = 3; break; case K_MOUSEDOWN: - button = 4; break; + pressed = true; button = 4; break; case K_MOUSEUP: - button = 5; break; + pressed = true; button = 5; break; default: return false; } - mouse_action(term, button, row, col - offset, drag, 0); - size_t len = vterm_output_read(term->vt, term->textbuf, - sizeof(term->textbuf)); - terminal_send(term, term->textbuf, len); + mouse_action(term, button, row, col - offset, pressed, 0); return false; } @@ -1302,7 +1291,7 @@ static bool send_mouse_event(Terminal *term, int c) } end: - ins_char_typebuf(c); + ins_char_typebuf(c, mod_mask); return true; } diff --git a/src/nvim/testdir/check.vim b/src/nvim/testdir/check.vim index 14bab33a2f..883f036fe1 100644 --- a/src/nvim/testdir/check.vim +++ b/src/nvim/testdir/check.vim @@ -12,9 +12,9 @@ endfunc " Command to check for the absence of a feature. command -nargs=1 CheckNotFeature call CheckNotFeature(<f-args>) func CheckNotFeature(name) - if !has(a:name, 1) - throw 'Checking for non-existent feature ' .. a:name - endif + " if !has(a:name, 1) + " throw 'Checking for non-existent feature ' .. a:name + " endif if has(a:name) throw 'Skipped: ' .. a:name .. ' feature present' endif @@ -63,6 +63,15 @@ func CheckUnix() endif endfunc +" Command to check for not running on a BSD system. +" TODO: using this checks should not be needed +command CheckNotBSD call CheckNotBSD() +func CheckNotBSD() + if has('bsd') + throw 'Skipped: does not work on BSD' + endif +endfunc + " Command to check that making screendumps is supported. " Caller must source screendump.vim command CheckScreendump call CheckScreendump() @@ -104,6 +113,14 @@ func CheckNotGui() endif endfunc +" Command to check that test is not running as root +command CheckNotRoot call CheckNotRoot() +func CheckNotRoot() + if IsRoot() + throw 'Skipped: cannot run test as root' + endif +endfunc + " Command to check that the current language is English command CheckEnglish call CheckEnglish() func CheckEnglish() diff --git a/src/nvim/testdir/runnvim.sh b/src/nvim/testdir/runnvim.sh index 25cb8437b4..72d88f9f93 100755 --- a/src/nvim/testdir/runnvim.sh +++ b/src/nvim/testdir/runnvim.sh @@ -65,9 +65,6 @@ main() {( fi fi fi - if test "$FAILED" = 1 ; then - ci_fold start "$NVIM_TEST_CURRENT_SUITE/$test_name" - fi valgrind_check . if test -n "$LOG_DIR" ; then check_sanitizer "$LOG_DIR" @@ -78,9 +75,6 @@ main() {( fi rm -f "$tlog" if test "$FAILED" = 1 ; then - ci_fold end "$NVIM_TEST_CURRENT_SUITE/$test_name" - fi - if test "$FAILED" = 1 ; then echo "Test $test_name failed, see output above and summary for more details" >> test.log # When Neovim crashed/aborted it might not have created messages. # test.log itself is used as an indicator to exit non-zero in the Makefile. diff --git a/src/nvim/testdir/setup.vim b/src/nvim/testdir/setup.vim index fdae0697c3..15e3b31498 100644 --- a/src/nvim/testdir/setup.vim +++ b/src/nvim/testdir/setup.vim @@ -10,6 +10,7 @@ let s:did_load = 1 set backspace= set directory^=. set fillchars=vert:\|,fold:- +set fsync set laststatus=1 set listchars=eol:$ set joinspaces diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim index f456ff4250..c2809844ac 100644 --- a/src/nvim/testdir/shared.vim +++ b/src/nvim/testdir/shared.vim @@ -343,6 +343,15 @@ func RunVimPiped(before, after, arguments, pipecmd) return 1 endfunc +func IsRoot() + if !has('unix') + return v:false + elseif $USER == 'root' || system('id -un') =~ '\<root\>' + return v:true + endif + return v:false +endfunc + " Get all messages but drop the maintainer entry. func GetMessages() redir => result diff --git a/src/nvim/testdir/test_alot.vim b/src/nvim/testdir/test_alot.vim index cc767a9bcf..5a3d1d56bb 100644 --- a/src/nvim/testdir/test_alot.vim +++ b/src/nvim/testdir/test_alot.vim @@ -27,6 +27,7 @@ source test_join.vim source test_jumps.vim source test_fileformat.vim source test_filetype.vim +source test_filetype_lua.vim source test_lambda.vim source test_menu.vim source test_messages.vim @@ -38,7 +39,8 @@ source test_put.vim source test_rename.vim source test_scroll_opt.vim source test_shift.vim -source test_sort.vim +" Test fails on windows CI when using the MSVC compiler. +" source test_sort.vim source test_sha256.vim source test_suspend.vim source test_syn_attr.vim diff --git a/src/nvim/testdir/test_alot_utf8.vim b/src/nvim/testdir/test_alot_utf8.vim index 70f14320a6..77f5ede4c8 100644 --- a/src/nvim/testdir/test_alot_utf8.vim +++ b/src/nvim/testdir/test_alot_utf8.vim @@ -6,7 +6,6 @@ source test_charsearch_utf8.vim source test_expr_utf8.vim -source test_matchadd_conceal_utf8.vim source test_mksession_utf8.vim source test_regexp_utf8.vim source test_source_utf8.vim diff --git a/src/nvim/testdir/test_assert.vim b/src/nvim/testdir/test_assert.vim index 52f243aaea..28c5948142 100644 --- a/src/nvim/testdir/test_assert.vim +++ b/src/nvim/testdir/test_assert.vim @@ -1,5 +1,55 @@ " Test that the methods used for testing work. +func Test_assert_false() + call assert_equal(0, assert_false(0)) + call assert_equal(0, assert_false(v:false)) + call assert_equal(0, v:false->assert_false()) + + call assert_equal(1, assert_false(123)) + call assert_match("Expected False but got 123", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, 123->assert_false()) + call assert_match("Expected False but got 123", v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_assert_true() + call assert_equal(0, assert_true(1)) + call assert_equal(0, assert_true(123)) + call assert_equal(0, assert_true(v:true)) + call assert_equal(0, v:true->assert_true()) + + call assert_equal(1, assert_true(0)) + call assert_match("Expected True but got 0", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, 0->assert_true()) + call assert_match("Expected True but got 0", v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_assert_equal() + let s = 'foo' + call assert_equal(0, assert_equal('foo', s)) + let n = 4 + call assert_equal(0, assert_equal(4, n)) + let l = [1, 2, 3] + call assert_equal(0, assert_equal([1, 2, 3], l)) + call assert_equal(v:_null_list, v:_null_list) + call assert_equal(v:_null_list, []) + call assert_equal([], v:_null_list) + + let s = 'foo' + call assert_equal(1, assert_equal('bar', s)) + call assert_match("Expected 'bar' but got 'foo'", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal('XxxxxxxxxxxxxxxxxxxxxxX', 'XyyyyyyyyyyyyyyyyyyyyyyyyyX') + call assert_match("Expected 'X\\\\\\[x occurs 21 times]X' but got 'X\\\\\\[y occurs 25 times]X'", v:errors[0]) + call remove(v:errors, 0) +endfunc + func Test_assert_equalfile() call assert_equal(1, assert_equalfile('abcabc', 'xyzxyz')) call assert_match("E485: Can't read file abcabc", v:errors[0]) @@ -46,17 +96,129 @@ func Test_assert_equalfile() call delete('Xtwo') endfunc +func Test_assert_notequal() + let n = 4 + call assert_equal(0, assert_notequal('foo', n)) + let s = 'foo' + call assert_equal(0, assert_notequal([1, 2, 3], s)) + + call assert_equal(1, assert_notequal('foo', s)) + call assert_match("Expected not equal to 'foo'", v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_assert_report() + call assert_equal(1, assert_report('something is wrong')) + call assert_match('something is wrong', v:errors[0]) + call remove(v:errors, 0) + call assert_equal(1, 'also wrong'->assert_report()) + call assert_match('also wrong', v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_assert_exception() + try + nocommand + catch + call assert_equal(0, assert_exception('E492:')) + endtry + + try + nocommand + catch + try + " illegal argument, get NULL for error + call assert_equal(1, assert_exception([])) + catch + call assert_equal(0, assert_exception('E730:')) + endtry + endtry +endfunc + +func Test_wrong_error_type() + let save_verrors = v:errors + let v:['errors'] = {'foo': 3} + call assert_equal('yes', 'no') + let verrors = v:errors + let v:errors = save_verrors + call assert_equal(type([]), type(verrors)) +endfunc + +func Test_match() + call assert_equal(0, assert_match('^f.*b.*r$', 'foobar')) + + call assert_equal(1, assert_match('bar.*foo', 'foobar')) + call assert_match("Pattern 'bar.*foo' does not match 'foobar'", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, assert_match('bar.*foo', 'foobar', 'wrong')) + call assert_match('wrong', v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, 'foobar'->assert_match('bar.*foo', 'wrong')) + call assert_match('wrong', v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_notmatch() + call assert_equal(0, assert_notmatch('foo', 'bar')) + call assert_equal(0, assert_notmatch('^foobar$', 'foobars')) + + call assert_equal(1, assert_notmatch('foo', 'foobar')) + call assert_match("Pattern 'foo' does match 'foobar'", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, 'foobar'->assert_notmatch('foo')) + call assert_match("Pattern 'foo' does match 'foobar'", v:errors[0]) + call remove(v:errors, 0) +endfunc + +func Test_assert_fail_fails() + call assert_equal(1, assert_fails('xxx', 'E12345')) + call assert_match("Expected 'E12345' but got 'E492:", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, assert_fails('xxx', 'E9876', 'stupid')) + call assert_match("stupid: Expected 'E9876' but got 'E492:", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, assert_fails('echo', '', 'echo command')) + call assert_match("command did not fail: echo command", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(1, 'echo'->assert_fails('', 'echo command')) + call assert_match("command did not fail: echo command", v:errors[0]) + call remove(v:errors, 0) +endfunc + func Test_assert_fails_in_try_block() try call assert_equal(0, assert_fails('throw "error"')) endtry endfunc +func Test_assert_beeps() + new + call assert_equal(0, assert_beeps('normal h')) + + call assert_equal(1, assert_beeps('normal 0')) + call assert_match("command did not beep: normal 0", v:errors[0]) + call remove(v:errors, 0) + + call assert_equal(0, 'normal h'->assert_beeps()) + call assert_equal(1, 'normal 0'->assert_beeps()) + call assert_match("command did not beep: normal 0", v:errors[0]) + call remove(v:errors, 0) + + bwipe +endfunc + func Test_assert_inrange() call assert_equal(0, assert_inrange(7, 7, 7)) call assert_equal(0, assert_inrange(5, 7, 5)) call assert_equal(0, assert_inrange(5, 7, 6)) call assert_equal(0, assert_inrange(5, 7, 7)) + call assert_equal(1, assert_inrange(5, 7, 4)) call assert_match("Expected range 5 - 7, but got 4", v:errors[0]) call remove(v:errors, 0) @@ -64,6 +226,12 @@ func Test_assert_inrange() call assert_match("Expected range 5 - 7, but got 8", v:errors[0]) call remove(v:errors, 0) + call assert_equal(0, 5->assert_inrange(5, 7)) + call assert_equal(0, 7->assert_inrange(5, 7)) + call assert_equal(1, 8->assert_inrange(5, 7)) + call assert_match("Expected range 5 - 7, but got 8", v:errors[0]) + call remove(v:errors, 0) + call assert_fails('call assert_inrange(1, 1)', 'E119:') if has('float') @@ -83,6 +251,12 @@ func Test_assert_inrange() endif endfunc +func Test_assert_with_msg() + call assert_equal('foo', 'bar', 'testing') + call assert_match("testing: Expected 'foo' but got 'bar'", v:errors[0]) + call remove(v:errors, 0) +endfunc + " Must be last. func Test_zz_quit_detected() " Verify that if a test function ends Vim the test script detects this. diff --git a/src/nvim/testdir/test_autochdir.vim b/src/nvim/testdir/test_autochdir.vim index 9ad727241e..53ed4617f7 100644 --- a/src/nvim/testdir/test_autochdir.vim +++ b/src/nvim/testdir/test_autochdir.vim @@ -64,4 +64,14 @@ func Test_verbose_pwd() call delete('Xautodir', 'rf') endfunc +func Test_multibyte() + " using an invalid character should not cause a crash + set wic + " Except on Windows, E472 is also thrown last, but v8.1.1183 isn't ported yet + " call assert_fails('tc *', has('win32') ? 'E480:' : 'E344:') + call assert_fails('tc *', has('win32') ? 'E480:' : 'E472:') + set nowic +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 45285b69a1..c39546b9ea 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -4,7 +4,7 @@ source shared.vim source check.vim source term_util.vim -func! s:cleanup_buffers() abort +func s:cleanup_buffers() abort for bnr in range(1, bufnr('$')) if bufloaded(bnr) && bufnr('%') != bnr execute 'bd! ' . bnr @@ -502,7 +502,7 @@ func Test_autocmd_bufwipe_in_SessLoadPost() [CODE] call writefile(content, 'Xvimrc') - call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq') + call system(GetVimCommand('Xvimrc') .. ' --headless --noplugins -S Session.vim -c cq') let errors = join(readfile('Xerrors')) call assert_match('E814', errors) @@ -562,7 +562,7 @@ func Test_autocmd_bufwipe_in_SessLoadPost2() [CODE] call writefile(content, 'Xvimrc') - call system(v:progpath. ' --headless -i NONE -u Xvimrc --noplugins -S Session.vim -c cq') + call system(GetVimCommand('Xvimrc') .. ' --headless --noplugins -S Session.vim -c cq') let errors = join(readfile('Xerrors')) " This probably only ever matches on unix. call assert_notmatch('Caught deadly signal SEGV', errors) @@ -1506,7 +1506,7 @@ func Test_bufunload_all() call writefile(content, 'Xtest') call delete('Xout') - call system(v:progpath. ' -u NORC -i NONE -N -S Xtest') + call system(GetVimCommandClean() .. ' -N --headless -S Xtest') call assert_true(filereadable('Xout')) call delete('Xxx1') @@ -1827,6 +1827,14 @@ func Test_autocommand_all_events() call assert_fails('au * x bwipe', 'E1155:') endfunc +func Test_autocmd_user() + au User MyEvent let s:res = [expand("<afile>"), expand("<amatch>")] + doautocmd User MyEvent + call assert_equal(['MyEvent', 'MyEvent'], s:res) + au! User + unlet s:res +endfunc + function s:Before_test_dirchanged() augroup test_dirchanged autocmd! @@ -1850,14 +1858,16 @@ endfunc function Test_dirchanged_global() call s:Before_test_dirchanged() + autocmd test_dirchanged DirChangedPre global call add(s:li, expand("<amatch>") .. " pre cd " .. v:event.directory) autocmd test_dirchanged DirChanged global call add(s:li, "cd:") autocmd test_dirchanged DirChanged global call add(s:li, expand("<afile>")) call chdir(s:dir_foo) - call assert_equal(["cd:", s:dir_foo], s:li) + let expected = ["global pre cd " .. s:dir_foo, "cd:", s:dir_foo] + call assert_equal(expected, s:li) call chdir(s:dir_foo) - call assert_equal(["cd:", s:dir_foo], s:li) + call assert_equal(expected, s:li) exe 'lcd ' .. fnameescape(s:dir_bar) - call assert_equal(["cd:", s:dir_foo], s:li) + call assert_equal(expected, s:li) call s:After_test_dirchanged() endfunc @@ -1879,6 +1889,7 @@ function Test_dirchanged_auto() CheckOption autochdir call s:Before_test_dirchanged() call test_autochdir() + autocmd test_dirchanged DirChangedPre auto call add(s:li, "pre cd " .. v:event.directory) autocmd test_dirchanged DirChanged auto call add(s:li, "auto:") autocmd test_dirchanged DirChanged auto call add(s:li, expand("<afile>")) set acd @@ -1886,7 +1897,8 @@ function Test_dirchanged_auto() call assert_equal([], s:li) exe 'edit ' . s:dir_foo . '/Xfile' call assert_equal(s:dir_foo, getcwd()) - call assert_equal(["auto:", s:dir_foo], s:li) + let expected = ["pre cd " .. s:dir_foo, "auto:", s:dir_foo] + call assert_equal(expected, s:li) set noacd bwipe! call s:After_test_dirchanged() @@ -2380,6 +2392,40 @@ func Test_autocmd_was_using_freed_memory() pclose endfunc +func Test_BufWrite_lockmarks() + edit! Xtest + call setline(1, ['a', 'b', 'c', 'd']) + + " :lockmarks preserves the marks + call SetChangeMarks(2, 3) + lockmarks write + call assert_equal([2, 3], [line("'["), line("']")]) + + " *WritePre autocmds get the correct line range, but lockmarks preserves the + " original values for the user + augroup lockmarks + au! + au BufWritePre,FilterWritePre * call assert_equal([1, 4], [line("'["), line("']")]) + au FileWritePre * call assert_equal([3, 4], [line("'["), line("']")]) + augroup END + + lockmarks write + call assert_equal([2, 3], [line("'["), line("']")]) + + if executable('cat') + lockmarks %!cat + call assert_equal([2, 3], [line("'["), line("']")]) + endif + + lockmarks 3,4write Xtest2 + call assert_equal([2, 3], [line("'["), line("']")]) + + au! lockmarks + augroup! lockmarks + call delete('Xtest') + call delete('Xtest2') +endfunc + " FileChangedShell tested in test_filechanged.vim func LogACmd() @@ -2509,6 +2555,16 @@ func Test_close_autocmd_tab() %bwipe! endfunc +func Test_Visual_doautoall_redraw() + call setline(1, ['a', 'b']) + new + wincmd p + call feedkeys("G\<C-V>", 'txn') + autocmd User Explode ++once redraw + doautoall User Explode + %bwipe! +endfunc + func Test_autocmd_closes_window() au BufNew,BufWinLeave * e %e file yyy diff --git a/src/nvim/testdir/test_blockedit.vim b/src/nvim/testdir/test_blockedit.vim index 38978ef689..7b56b1554f 100644 --- a/src/nvim/testdir/test_blockedit.vim +++ b/src/nvim/testdir/test_blockedit.vim @@ -81,4 +81,52 @@ func Test_blockinsert_delete() bwipe! endfunc +func Test_blockappend_eol_cursor() + new + " Test 1 Move 1 char left + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "norm! gg$\<c-v>2jA\<left>x\<esc>" + call assert_equal(['aaxa', 'bbxb', 'ccxc'], getline(1, '$')) + " Test 2 Move 2 chars left + sil %d + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "norm! gg$\<c-v>2jA\<left>\<left>x\<esc>" + call assert_equal(['axaa', 'bxbb', 'cxcc'], getline(1, '$')) + " Test 3 Move 3 chars left (outside of the visual selection) + sil %d + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "norm! ggl$\<c-v>2jA\<left>\<left>\<left>x\<esc>" + call assert_equal(['xaaa', 'bbb', 'ccc'], getline(1, '$')) + bw! +endfunc + +func Test_blockappend_eol_cursor2() + new + " Test 1 Move 1 char left + call setline(1, ['aaaaa', 'bbb', 'ccccc']) + exe "norm! gg\<c-v>$2jA\<left>x\<esc>" + call assert_equal(['aaaaxa', 'bbbx', 'ccccxc'], getline(1, '$')) + " Test 2 Move 2 chars left + sil %d + call setline(1, ['aaaaa', 'bbb', 'ccccc']) + exe "norm! gg\<c-v>$2jA\<left>\<left>x\<esc>" + call assert_equal(['aaaxaa', 'bbbx', 'cccxcc'], getline(1, '$')) + " Test 3 Move 3 chars left (to the beginning of the visual selection) + sil %d + call setline(1, ['aaaaa', 'bbb', 'ccccc']) + exe "norm! gg\<c-v>$2jA\<left>\<left>\<left>x\<esc>" + call assert_equal(['aaxaaa', 'bbxb', 'ccxccc'], getline(1, '$')) + " Test 4 Move 3 chars left (outside of the visual selection) + sil %d + call setline(1, ['aaaaa', 'bbb', 'ccccc']) + exe "norm! ggl\<c-v>$2jA\<left>\<left>\<left>x\<esc>" + call assert_equal(['aaxaaa', 'bbxb', 'ccxccc'], getline(1, '$')) + " Test 5 Move 4 chars left (outside of the visual selection) + sil %d + call setline(1, ['aaaaa', 'bbb', 'ccccc']) + exe "norm! ggl\<c-v>$2jA\<left>\<left>\<left>\<left>x\<esc>" + call assert_equal(['axaaaa', 'bxbb', 'cxcccc'], getline(1, '$')) + bw! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_breakindent.vim b/src/nvim/testdir/test_breakindent.vim index 8d592f21ea..438edb0257 100644 --- a/src/nvim/testdir/test_breakindent.vim +++ b/src/nvim/testdir/test_breakindent.vim @@ -1,7 +1,7 @@ " Test for breakindent " " Note: if you get strange failures when adding new tests, it might be that -" while the test is run, the breakindent cacheing gets in its way. +" while the test is run, the breakindent caching gets in its way. " It helps to change the tabstop setting and force a redraw (e.g. see " Test_breakindent08()) if !exists('+breakindent') @@ -20,7 +20,7 @@ func s:screen_lines2(lnums, lnume, width) abort return ScreenLines([a:lnums, a:lnume], a:width) endfunc -func! s:compare_lines(expect, actual) +func s:compare_lines(expect, actual) call assert_equal(join(a:expect, "\n"), join(a:actual, "\n")) endfunc diff --git a/src/nvim/testdir/test_buffer.vim b/src/nvim/testdir/test_buffer.vim index 40111fdf06..a31cdbb49a 100644 --- a/src/nvim/testdir/test_buffer.vim +++ b/src/nvim/testdir/test_buffer.vim @@ -1,5 +1,7 @@ " Tests for Vim buffer +source check.vim + func Test_buffer_error() new foo1 new foo2 @@ -30,4 +32,33 @@ func Test_balt() call assert_equal('OtherBuffer', bufname()) endfunc +" Test for buffer match URL(scheme) check +" scheme is alpha and inner hyphen only. +func Test_buffer_scheme() + CheckMSWindows + + set noshellslash + %bwipe! + let bufnames = [ + \ #{id: 'b0', name: 'test://xyz/foo/b0' , match: 1}, + \ #{id: 'b1', name: 'test+abc://xyz/foo/b1', match: 0}, + \ #{id: 'b2', name: 'test_abc://xyz/foo/b2', match: 0}, + \ #{id: 'b3', name: 'test-abc://xyz/foo/b3', match: 1}, + \ #{id: 'b4', name: '-test://xyz/foo/b4' , match: 0}, + \ #{id: 'b5', name: 'test-://xyz/foo/b5' , match: 0}, + \] + for buf in bufnames + new `=buf.name` + if buf.match + call assert_equal(buf.name, getbufinfo(buf.id)[0].name) + else + " slashes will have become backslashes + call assert_notequal(buf.name, getbufinfo(buf.id)[0].name) + endif + bwipe + endfor + + set shellslash& +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_cd.vim b/src/nvim/testdir/test_cd.vim index 76a2620be0..c364babd65 100644 --- a/src/nvim/testdir/test_cd.vim +++ b/src/nvim/testdir/test_cd.vim @@ -44,6 +44,15 @@ func Test_cd_minus() cd - call assert_equal(path, getcwd()) + " Test for :cd - after a failed :cd + " v8.2.1183 is not ported yet + " call assert_fails('cd /nonexistent', 'E344:') + call assert_fails('cd /nonexistent', 'E472:') + call assert_equal(path, getcwd()) + cd - + call assert_equal(path_dotdot, getcwd()) + cd - + " Test for :cd - without a previous directory let lines =<< trim [SCRIPT] call assert_fails('cd -', 'E186:') diff --git a/src/nvim/testdir/test_cdo.vim b/src/nvim/testdir/test_cdo.vim new file mode 100644 index 0000000000..aa2e4f1a8c --- /dev/null +++ b/src/nvim/testdir/test_cdo.vim @@ -0,0 +1,205 @@ +" Tests for the :cdo, :cfdo, :ldo and :lfdo commands + +if !has('quickfix') + throw 'Skipped: quickfix feature missing' +endif + +" Create the files used by the tests +function SetUp() + call writefile(["Line1", "Line2", "Line3"], 'Xtestfile1') + call writefile(["Line1", "Line2", "Line3"], 'Xtestfile2') + call writefile(["Line1", "Line2", "Line3"], 'Xtestfile3') +endfunction + +" Remove the files used by the tests +function TearDown() + call delete('Xtestfile1') + call delete('Xtestfile2') + call delete('Xtestfile3') +endfunction + +" Returns the current line in '<filename> <linenum>L <column>C' format +function GetRuler() + return expand('%') . ' ' . line('.') . 'L' . ' ' . col('.') . 'C' +endfunction + +" Tests for the :cdo and :ldo commands +function XdoTests(cchar) + enew + + " Shortcuts for calling the cdo and ldo commands + let Xdo = a:cchar . 'do' + let Xgetexpr = a:cchar . 'getexpr' + let Xprev = a:cchar. 'prev' + let XdoCmd = Xdo . ' call add(l, GetRuler())' + + " Try with an empty list + let l = [] + exe XdoCmd + call assert_equal([], l) + + " Populate the list and then try + exe Xgetexpr . " ['non-error 1', 'Xtestfile1:1:3:Line1', 'non-error 2', 'Xtestfile2:2:2:Line2', 'non-error 3', 'Xtestfile3:3:1:Line3']" + + let l = [] + exe XdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 3L 1C'], l) + + " Run command only on selected error lines + let l = [] + enew + exe "2,3" . XdoCmd + call assert_equal(['Xtestfile2 2L 2C', 'Xtestfile3 3L 1C'], l) + + " Boundary condition tests + let l = [] + enew + exe "1,1" . XdoCmd + call assert_equal(['Xtestfile1 1L 3C'], l) + + let l = [] + enew + exe "3" . XdoCmd + call assert_equal(['Xtestfile3 3L 1C'], l) + + " Range test commands + let l = [] + enew + exe "%" . XdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 3L 1C'], l) + + let l = [] + enew + exe "1,$" . XdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 3L 1C'], l) + + let l = [] + enew + exe Xprev + exe "." . XdoCmd + call assert_equal(['Xtestfile2 2L 2C'], l) + + let l = [] + enew + exe "+" . XdoCmd + call assert_equal(['Xtestfile3 3L 1C'], l) + + " Invalid error lines test + let l = [] + enew + exe "silent! 27" . XdoCmd + exe "silent! 4,5" . XdoCmd + call assert_equal([], l) + + " Run commands from an unsaved buffer + let v:errmsg='' + let l = [] + enew + setlocal modified + exe "silent! 2,2" . XdoCmd + if v:errmsg !~# 'No write since last change' + call add(v:errors, 'Unsaved file change test failed') + endif + + " If the executed command fails, then the operation should be aborted + enew! + let subst_count = 0 + exe "silent!" . Xdo . " s/Line/xLine/ | let subst_count += 1" + if subst_count != 1 || getline('.') != 'xLine1' + call add(v:errors, 'Abort command on error test failed') + endif + + let l = [] + exe "2,2" . Xdo . "! call add(l, GetRuler())" + call assert_equal(['Xtestfile2 2L 2C'], l) + + " List with no valid error entries + let l = [] + edit! +2 Xtestfile1 + exe Xgetexpr . " ['non-error 1', 'non-error 2', 'non-error 3']" + exe XdoCmd + call assert_equal([], l) + exe "silent! 2" . XdoCmd + call assert_equal([], l) + let v:errmsg='' + exe "%" . XdoCmd + exe "1,$" . XdoCmd + exe "." . XdoCmd + call assert_equal('', v:errmsg) + + " List with only one valid entry + let l = [] + exe Xgetexpr . " ['Xtestfile3:3:1:Line3']" + exe XdoCmd + call assert_equal(['Xtestfile3 3L 1C'], l) + +endfunction + +" Tests for the :cfdo and :lfdo commands +function XfdoTests(cchar) + enew + + " Shortcuts for calling the cfdo and lfdo commands + let Xfdo = a:cchar . 'fdo' + let Xgetexpr = a:cchar . 'getexpr' + let XfdoCmd = Xfdo . ' call add(l, GetRuler())' + let Xpfile = a:cchar. 'pfile' + + " Clear the quickfix/location list + exe Xgetexpr . " []" + + " Try with an empty list + let l = [] + exe XfdoCmd + call assert_equal([], l) + + " Populate the list and then try + exe Xgetexpr . " ['non-error 1', 'Xtestfile1:1:3:Line1', 'Xtestfile1:2:1:Line2', 'non-error 2', 'Xtestfile2:2:2:Line2', 'non-error 3', 'Xtestfile3:2:3:Line2', 'Xtestfile3:3:1:Line3']" + + let l = [] + exe XfdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 2L 3C'], l) + + " Run command only on selected error lines + let l = [] + exe "2,3" . XfdoCmd + call assert_equal(['Xtestfile2 2L 2C', 'Xtestfile3 2L 3C'], l) + + " Boundary condition tests + let l = [] + exe "3" . XfdoCmd + call assert_equal(['Xtestfile3 2L 3C'], l) + + " Range test commands + let l = [] + exe "%" . XfdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 2L 3C'], l) + + let l = [] + exe "1,$" . XfdoCmd + call assert_equal(['Xtestfile1 1L 3C', 'Xtestfile2 2L 2C', 'Xtestfile3 2L 3C'], l) + + let l = [] + exe Xpfile + exe "." . XfdoCmd + call assert_equal(['Xtestfile2 2L 2C'], l) + + " List with only one valid entry + let l = [] + exe Xgetexpr . " ['Xtestfile2:2:5:Line2']" + exe XfdoCmd + call assert_equal(['Xtestfile2 2L 5C'], l) + +endfunction + +" Tests for cdo and cfdo +function Test_cdo() + call XdoTests('c') + call XfdoTests('c') +endfunction + +" Tests for ldo and lfdo +function Test_ldo() + call XdoTests('l') + call XfdoTests('l') +endfunction diff --git a/src/nvim/testdir/test_charsearch_utf8.vim b/src/nvim/testdir/test_charsearch_utf8.vim index 09341a90b0..82a807ac5b 100644 --- a/src/nvim/testdir/test_charsearch_utf8.vim +++ b/src/nvim/testdir/test_charsearch_utf8.vim @@ -1,7 +1,7 @@ " Tests for related f{char} and t{char} using utf-8. " Test for t,f,F,T movement commands -function! Test_search_cmds() +func Test_search_cmds() new! call setline(1, "・最初から最後まで最強のVimは最高") 1 diff --git a/src/nvim/testdir/test_cindent.vim b/src/nvim/testdir/test_cindent.vim index 562867f548..4b702bf2b8 100644 --- a/src/nvim/testdir/test_cindent.vim +++ b/src/nvim/testdir/test_cindent.vim @@ -1,6 +1,5 @@ " Test for cinoptions and cindent " -" TODO: rewrite test3.in into this new style test func Test_cino_hash() " Test that curbuf->b_ind_hash_comment is correctly reset @@ -128,12 +127,5187 @@ func Test_cindent_func() bwipe! endfunc +func Test_cindent_1() + new + setl cindent ts=4 sw=4 + setl cino& sts& + + let code =<< trim [CODE] + /* start of AUTO matically checked vim: set ts=4 : */ + { + if (test) + cmd1; + cmd2; + } + + { + if (test) + cmd1; + else + cmd2; + } + + { + if (test) + { + cmd1; + cmd2; + } + } + + { + if (test) + { + cmd1; + else + } + } + + { + while (this) + if (test) + cmd1; + cmd2; + } + + { + while (this) + if (test) + cmd1; + else + cmd2; + } + + { + if (test) + { + cmd; + } + + if (test) + cmd; + } + + { + if (test) { + cmd; + } + + if (test) cmd; + } + + { + cmd1; + for (blah) + while (this) + if (test) + cmd2; + cmd3; + } + + { + cmd1; + for (blah) + while (this) + if (test) + cmd2; + cmd3; + + if (test) + { + cmd1; + cmd2; + cmd3; + } + } + + + /* Test for 'cindent' do/while mixed with if/else: */ + + { + do + if (asdf) + asdfasd; + while (cond); + + do + if (asdf) + while (asdf) + asdf; + while (asdf); + } + + /* Test for 'cindent' with two ) on a continuation line */ + { + if (asdfasdf;asldkfj asdlkfj as;ldkfj sal;d + aal;sdkjf ( ;asldfkja;sldfk + al;sdjfka ;slkdf ) sa;ldkjfsa dlk;) + line up here; + } + + + /* C++ tests: */ + + // foo() these three lines should remain in column 0 + // { + // } + + /* Test for continuation and unterminated lines: */ + { + i = 99 + 14325 + + 21345 + + 21345 + + 21345 + ( 21345 + + 21345) + + 2345 + + 1234; + c = 1; + } + + /* + testje for indent with empty line + + here */ + + { + if (testing && + not a joke || + line up here) + hay; + if (testing && + (not a joke || testing + )line up here) + hay; + if (testing && + (not a joke || testing + line up here)) + hay; + } + + + { + switch (c) + { + case xx: + do + if (asdf) + do + asdfasdf; + while (asdf); + else + asdfasdf; + while (cond); + case yy: + case xx: + case zz: + testing; + } + } + + { + if (cond) { + foo; + } + else + { + bar; + } + } + + { + if (alskdfj ;alsdkfjal;skdjf (;sadlkfsa ;dlkf j;alksdfj ;alskdjf + alsdkfj (asldk;fj + awith cino=(0 ;lf this one goes to below the paren with == + ;laksjfd ;lsakdjf ;alskdf asd) + asdfasdf;))) + asdfasdf; + } + + int + func(a, b) + int a; + int c; + { + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3) + ) + } + + { + while (asd) + { + if (asdf) + if (test) + if (that) + { + if (asdf) + do + cdasd; + while (as + df); + } + else + if (asdf) + asdf; + else + asdf; + asdf; + } + } + + { + s = "/*"; b = ';' + s = "/*"; b = ';'; + a = b; + } + + { + switch (a) + { + case a: + switch (t) + { + case 1: + cmd; + break; + case 2: + cmd; + break; + } + cmd; + break; + case b: + { + int i; + cmd; + } + break; + case c: { + int i; + cmd; + } + case d: if (cond && + test) { /* this line doesn't work right */ + int i; + cmd; + } + break; + } + } + + { + if (!(vim_strchr(p_cpo, CPO_BUFOPTGLOB) != NULL && entering) && + (bp_to->b_p_initialized || + (!entering && vim_strchr(p_cpo, CPO_BUFOPT) != NULL))) + return; + label : + asdf = asdf ? + asdf : asdf; + asdf = asdf ? + asdf: asdf; + } + + /* Special Comments : This function has the added complexity (compared */ + /* : to addtolist) of having to check for a detail */ + /* : texture and add that to the list first. */ + + char *(array[100]) = { + "testje", + "foo", + "bar", + } + + enum soppie + { + yes = 0, + no, + maybe + }; + + typedef enum soppie + { + yes = 0, + no, + maybe + }; + + static enum + { + yes = 0, + no, + maybe + } soppie; + + public static enum + { + yes = 0, + no, + maybe + } soppie; + + static private enum + { + yes = 0, + no, + maybe + } soppie; + + { + int a, + b; + } + + { + struct Type + { + int i; + char *str; + } var[] = + { + 0, "zero", + 1, "one", + 2, "two", + 3, "three" + }; + + float matrix[3][3] = + { + { + 0, + 1, + 2 + }, + { + 3, + 4, + 5 + }, + { + 6, + 7, + 8 + } + }; + } + + { + /* blah ( blah */ + /* where does this go? */ + + /* blah ( blah */ + cmd; + + func(arg1, + /* comment */ + arg2); + a; + { + b; + { + c; /* Hey, NOW it indents?! */ + } + } + + { + func(arg1, + arg2, + arg3); + /* Hey, what am I doing here? Is this coz of the ","? */ + } + } + + main () + { + if (cond) + { + a = b; + } + if (cond) { + a = c; + } + if (cond) + a = d; + return; + } + + { + case 2: if (asdf && + asdfasdf) + aasdf; + a = 9; + case 3: if (asdf) + aasdf; + a = 9; + case 4: x = 1; + y = 2; + + label: if (asdf) + here; + + label: if (asdf && + asdfasdf) + { + } + + label: if (asdf && + asdfasdf) { + there; + } + + label: if (asdf && + asdfasdf) + there; + } + + { + /* + hello with ":set comments= cino=c5" + */ + + /* + hello with ":set comments= cino=" + */ + } + + + { + if (a < b) { + a = a + 1; + } else + a = a + 2; + + if (a) + do { + testing; + } while (asdfasdf); + a = b + 1; + asdfasdf + } + + { + for ( int i = 0; + i < 10; i++ ) + { + } + i = 0; + } + + class bob + { + int foo() {return 1;} + int bar; + } + + main() + { + while(1) + if (foo) + { + bar; + } + else { + asdf; + } + misplacedline; + } + + { + if (clipboard.state == SELECT_DONE + && ((row == clipboard.start.lnum + && col >= clipboard.start.col) + || row > clipboard.start.lnum)) + } + + { + if (1) {i += 4;} + where_am_i; + return 0; + } + + { + { + } // sdf(asdf + if (asdf) + asd; + } + + { + label1: + label2: + } + + { + int fooRet = foo(pBar1, false /*fKB*/, + true /*fPTB*/, 3 /*nT*/, false /*fDF*/); + f() { + for ( i = 0; + i < m; + /* c */ i++ ) { + a = b; + } + } + } + + { + f1(/*comment*/); + f2(); + } + + { + do { + if (foo) { + } else + ; + } while (foo); + foo(); // was wrong + } + + int x; // no extra indent because of the ; + void func() + { + } + + char *tab[] = {"aaa", + "};", /* }; */ NULL} + int indented; + {} + + char *a[] = {"aaa", "bbb", + "ccc", NULL}; + // here + + char *tab[] = {"aaa", + "xx", /* xx */}; /* asdf */ + int not_indented; + + { + do { + switch (bla) + { + case 1: if (foo) + bar; + } + } while (boo); + wrong; + } + + int foo, + bar; + int foo; + + #if defined(foo) \ + && defined(bar) + char * xx = "asdf\ + foo\ + bor"; + int x; + + char *foo = "asdf\ + asdf\ + asdf", + *bar; + + void f() + { + #if defined(foo) \ + && defined(bar) + char *foo = "asdf\ + asdf\ + asdf", + *bar; + { + int i; + char *foo = "asdf\ + asdf\ + asdf", + *bar; + } + #endif + } + #endif + + int y; // comment + // comment + + // comment + + { + Constructor(int a, + int b ) : BaseClass(a) + { + } + } + + void foo() + { + char one, + two; + struct bla piet, + jan; + enum foo kees, + jannie; + static unsigned sdf, + krap; + unsigned int piet, + jan; + int + kees, + jan; + } + + { + t(int f, + int d); // ) + d(); + } + + Constructor::Constructor(int a, + int b + ) : + BaseClass(a, + b, + c), + mMember(b), + { + } + + Constructor::Constructor(int a, + int b ) : + BaseClass(a) + { + } + + Constructor::Constructor(int a, + int b ) /*x*/ : /*x*/ BaseClass(a), + member(b) + { + } + + A::A(int a, int b) + : aa(a), + bb(b), + cc(c) + { + } + + class CAbc : + public BaseClass1, + protected BaseClass2 + { + int Test() { return FALSE; } + int Test1() { return TRUE; } + + CAbc(int a, int b ) : + BaseClass(a) + { + switch(xxx) + { + case abc: + asdf(); + break; + + case 999: + baer(); + break; + } + } + + public: // <-- this was incorrectly indented before!! + void testfall(); + protected: + void testfall(); + }; + + class CAbc : public BaseClass1, + protected BaseClass2 + { + }; + + static struct + { + int a; + int b; + } variable[COUNT] = + { + { + 123, + 456 + }, + { + 123, + 456 + } + }; + + static struct + { + int a; + int b; + } variable[COUNT] = + { + { 123, 456 }, + { 123, 456 } + }; + + void asdf() /* ind_maxparen may cause trouble here */ + { + if ((0 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1)) break; + } + + foo() + { + a = cond ? foo() : asdf + + asdf; + + a = cond ? + foo() : asdf + + asdf; + } + + int main(void) + { + if (a) + if (b) + 2; + else 3; + next_line_of_code(); + } + + barry() + { + Foo::Foo (int one, + int two) + : something(4) + {} + } + + barry() + { + Foo::Foo (int one, int two) + : something(4) + {} + } + + Constructor::Constructor(int a, + int b + ) : + BaseClass(a, + b, + c), + mMember(b) + { + } + int main () + { + if (lala) + do + ++(*lolo); + while (lili + && lele); + lulu; + } + + int main () + { + switch (c) + { + case 'c': if (cond) + { + } + } + } + + main() + { + (void) MyFancyFuasdfadsfnction( + argument); + } + + main() + { + char foo[] = "/*"; + /* as + df */ + hello + } + + /* valid namespaces with normal indent */ + namespace + { + { + 111111111111; + } + } + namespace /* test */ + { + 11111111111111111; + } + namespace // test + { + 111111111111111111; + } + namespace + { + 111111111111111111; + } + namespace test + { + 111111111111111111; + } + namespace{ + 111111111111111111; + } + namespace test{ + 111111111111111111; + } + namespace { + 111111111111111111; + } + namespace test { + 111111111111111111; + namespace test2 { + 22222222222222222; + } + } + inline namespace { + 111111111111111111; + } + inline /* test */ namespace { + 111111111111111111; + } + inline/* test */namespace { + 111111111111111111; + } + + /* invalid namespaces use block indent */ + namespace test test2 { + 111111111111111111111; + } + namespace11111111111 { + 111111111111; + } + namespace() { + 1111111111111; + } + namespace() + { + 111111111111111111; + } + namespace test test2 + { + 1111111111111111111; + } + namespace111111111 + { + 111111111111111111; + } + inlinenamespace { + 111111111111111111; + } + + void getstring() { + /* Raw strings */ + const char* s = R"( + test { + # comment + field: 123 + } + )"; + } + + void getstring() { + const char* s = R"foo( + test { + # comment + field: 123 + } + )foo"; + } + + { + int a[4] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + }; + } + + { + a = b[2] + + 3; + } + + { + if (1) + /* aaaaa + * bbbbb + */ + a = 1; + } + + void func() + { + switch (foo) + { + case (bar): + if (baz()) + quux(); + break; + case (shmoo): + if (!bar) + { + } + case (foo1): + switch (bar) + { + case baz: + baz_f(); + break; + } + break; + default: + baz(); + baz(); + break; + } + } + + /* end of AUTO */ + [CODE] + + call append(0, code) + normal gg + call search('start of AUTO') + exe "normal =/end of AUTO\<CR>" + + let expected =<< trim [CODE] + /* start of AUTO matically checked vim: set ts=4 : */ + { + if (test) + cmd1; + cmd2; + } + + { + if (test) + cmd1; + else + cmd2; + } + + { + if (test) + { + cmd1; + cmd2; + } + } + + { + if (test) + { + cmd1; + else + } + } + + { + while (this) + if (test) + cmd1; + cmd2; + } + + { + while (this) + if (test) + cmd1; + else + cmd2; + } + + { + if (test) + { + cmd; + } + + if (test) + cmd; + } + + { + if (test) { + cmd; + } + + if (test) cmd; + } + + { + cmd1; + for (blah) + while (this) + if (test) + cmd2; + cmd3; + } + + { + cmd1; + for (blah) + while (this) + if (test) + cmd2; + cmd3; + + if (test) + { + cmd1; + cmd2; + cmd3; + } + } + + + /* Test for 'cindent' do/while mixed with if/else: */ + + { + do + if (asdf) + asdfasd; + while (cond); + + do + if (asdf) + while (asdf) + asdf; + while (asdf); + } + + /* Test for 'cindent' with two ) on a continuation line */ + { + if (asdfasdf;asldkfj asdlkfj as;ldkfj sal;d + aal;sdkjf ( ;asldfkja;sldfk + al;sdjfka ;slkdf ) sa;ldkjfsa dlk;) + line up here; + } + + + /* C++ tests: */ + + // foo() these three lines should remain in column 0 + // { + // } + + /* Test for continuation and unterminated lines: */ + { + i = 99 + 14325 + + 21345 + + 21345 + + 21345 + ( 21345 + + 21345) + + 2345 + + 1234; + c = 1; + } + + /* + testje for indent with empty line + + here */ + + { + if (testing && + not a joke || + line up here) + hay; + if (testing && + (not a joke || testing + )line up here) + hay; + if (testing && + (not a joke || testing + line up here)) + hay; + } + + + { + switch (c) + { + case xx: + do + if (asdf) + do + asdfasdf; + while (asdf); + else + asdfasdf; + while (cond); + case yy: + case xx: + case zz: + testing; + } + } + + { + if (cond) { + foo; + } + else + { + bar; + } + } + + { + if (alskdfj ;alsdkfjal;skdjf (;sadlkfsa ;dlkf j;alksdfj ;alskdjf + alsdkfj (asldk;fj + awith cino=(0 ;lf this one goes to below the paren with == + ;laksjfd ;lsakdjf ;alskdf asd) + asdfasdf;))) + asdfasdf; + } + + int + func(a, b) + int a; + int c; + { + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3) + ) + } + + { + while (asd) + { + if (asdf) + if (test) + if (that) + { + if (asdf) + do + cdasd; + while (as + df); + } + else + if (asdf) + asdf; + else + asdf; + asdf; + } + } + + { + s = "/*"; b = ';' + s = "/*"; b = ';'; + a = b; + } + + { + switch (a) + { + case a: + switch (t) + { + case 1: + cmd; + break; + case 2: + cmd; + break; + } + cmd; + break; + case b: + { + int i; + cmd; + } + break; + case c: { + int i; + cmd; + } + case d: if (cond && + test) { /* this line doesn't work right */ + int i; + cmd; + } + break; + } + } + + { + if (!(vim_strchr(p_cpo, CPO_BUFOPTGLOB) != NULL && entering) && + (bp_to->b_p_initialized || + (!entering && vim_strchr(p_cpo, CPO_BUFOPT) != NULL))) + return; + label : + asdf = asdf ? + asdf : asdf; + asdf = asdf ? + asdf: asdf; + } + + /* Special Comments : This function has the added complexity (compared */ + /* : to addtolist) of having to check for a detail */ + /* : texture and add that to the list first. */ + + char *(array[100]) = { + "testje", + "foo", + "bar", + } + + enum soppie + { + yes = 0, + no, + maybe + }; + + typedef enum soppie + { + yes = 0, + no, + maybe + }; + + static enum + { + yes = 0, + no, + maybe + } soppie; + + public static enum + { + yes = 0, + no, + maybe + } soppie; + + static private enum + { + yes = 0, + no, + maybe + } soppie; + + { + int a, + b; + } + + { + struct Type + { + int i; + char *str; + } var[] = + { + 0, "zero", + 1, "one", + 2, "two", + 3, "three" + }; + + float matrix[3][3] = + { + { + 0, + 1, + 2 + }, + { + 3, + 4, + 5 + }, + { + 6, + 7, + 8 + } + }; + } + + { + /* blah ( blah */ + /* where does this go? */ + + /* blah ( blah */ + cmd; + + func(arg1, + /* comment */ + arg2); + a; + { + b; + { + c; /* Hey, NOW it indents?! */ + } + } + + { + func(arg1, + arg2, + arg3); + /* Hey, what am I doing here? Is this coz of the ","? */ + } + } + + main () + { + if (cond) + { + a = b; + } + if (cond) { + a = c; + } + if (cond) + a = d; + return; + } + + { + case 2: if (asdf && + asdfasdf) + aasdf; + a = 9; + case 3: if (asdf) + aasdf; + a = 9; + case 4: x = 1; + y = 2; + + label: if (asdf) + here; + + label: if (asdf && + asdfasdf) + { + } + + label: if (asdf && + asdfasdf) { + there; + } + + label: if (asdf && + asdfasdf) + there; + } + + { + /* + hello with ":set comments= cino=c5" + */ + + /* + hello with ":set comments= cino=" + */ + } + + + { + if (a < b) { + a = a + 1; + } else + a = a + 2; + + if (a) + do { + testing; + } while (asdfasdf); + a = b + 1; + asdfasdf + } + + { + for ( int i = 0; + i < 10; i++ ) + { + } + i = 0; + } + + class bob + { + int foo() {return 1;} + int bar; + } + + main() + { + while(1) + if (foo) + { + bar; + } + else { + asdf; + } + misplacedline; + } + + { + if (clipboard.state == SELECT_DONE + && ((row == clipboard.start.lnum + && col >= clipboard.start.col) + || row > clipboard.start.lnum)) + } + + { + if (1) {i += 4;} + where_am_i; + return 0; + } + + { + { + } // sdf(asdf + if (asdf) + asd; + } + + { + label1: + label2: + } + + { + int fooRet = foo(pBar1, false /*fKB*/, + true /*fPTB*/, 3 /*nT*/, false /*fDF*/); + f() { + for ( i = 0; + i < m; + /* c */ i++ ) { + a = b; + } + } + } + + { + f1(/*comment*/); + f2(); + } + + { + do { + if (foo) { + } else + ; + } while (foo); + foo(); // was wrong + } + + int x; // no extra indent because of the ; + void func() + { + } + + char *tab[] = {"aaa", + "};", /* }; */ NULL} + int indented; + {} + + char *a[] = {"aaa", "bbb", + "ccc", NULL}; + // here + + char *tab[] = {"aaa", + "xx", /* xx */}; /* asdf */ + int not_indented; + + { + do { + switch (bla) + { + case 1: if (foo) + bar; + } + } while (boo); + wrong; + } + + int foo, + bar; + int foo; + + #if defined(foo) \ + && defined(bar) + char * xx = "asdf\ + foo\ + bor"; + int x; + + char *foo = "asdf\ + asdf\ + asdf", + *bar; + + void f() + { + #if defined(foo) \ + && defined(bar) + char *foo = "asdf\ + asdf\ + asdf", + *bar; + { + int i; + char *foo = "asdf\ + asdf\ + asdf", + *bar; + } + #endif + } + #endif + + int y; // comment + // comment + + // comment + + { + Constructor(int a, + int b ) : BaseClass(a) + { + } + } + + void foo() + { + char one, + two; + struct bla piet, + jan; + enum foo kees, + jannie; + static unsigned sdf, + krap; + unsigned int piet, + jan; + int + kees, + jan; + } + + { + t(int f, + int d); // ) + d(); + } + + Constructor::Constructor(int a, + int b + ) : + BaseClass(a, + b, + c), + mMember(b), + { + } + + Constructor::Constructor(int a, + int b ) : + BaseClass(a) + { + } + + Constructor::Constructor(int a, + int b ) /*x*/ : /*x*/ BaseClass(a), + member(b) + { + } + + A::A(int a, int b) + : aa(a), + bb(b), + cc(c) + { + } + + class CAbc : + public BaseClass1, + protected BaseClass2 + { + int Test() { return FALSE; } + int Test1() { return TRUE; } + + CAbc(int a, int b ) : + BaseClass(a) + { + switch(xxx) + { + case abc: + asdf(); + break; + + case 999: + baer(); + break; + } + } + + public: // <-- this was incorrectly indented before!! + void testfall(); + protected: + void testfall(); + }; + + class CAbc : public BaseClass1, + protected BaseClass2 + { + }; + + static struct + { + int a; + int b; + } variable[COUNT] = + { + { + 123, + 456 + }, + { + 123, + 456 + } + }; + + static struct + { + int a; + int b; + } variable[COUNT] = + { + { 123, 456 }, + { 123, 456 } + }; + + void asdf() /* ind_maxparen may cause trouble here */ + { + if ((0 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1 + && 1)) break; + } + + foo() + { + a = cond ? foo() : asdf + + asdf; + + a = cond ? + foo() : asdf + + asdf; + } + + int main(void) + { + if (a) + if (b) + 2; + else 3; + next_line_of_code(); + } + + barry() + { + Foo::Foo (int one, + int two) + : something(4) + {} + } + + barry() + { + Foo::Foo (int one, int two) + : something(4) + {} + } + + Constructor::Constructor(int a, + int b + ) : + BaseClass(a, + b, + c), + mMember(b) + { + } + int main () + { + if (lala) + do + ++(*lolo); + while (lili + && lele); + lulu; + } + + int main () + { + switch (c) + { + case 'c': if (cond) + { + } + } + } + + main() + { + (void) MyFancyFuasdfadsfnction( + argument); + } + + main() + { + char foo[] = "/*"; + /* as + df */ + hello + } + + /* valid namespaces with normal indent */ + namespace + { + { + 111111111111; + } + } + namespace /* test */ + { + 11111111111111111; + } + namespace // test + { + 111111111111111111; + } + namespace + { + 111111111111111111; + } + namespace test + { + 111111111111111111; + } + namespace{ + 111111111111111111; + } + namespace test{ + 111111111111111111; + } + namespace { + 111111111111111111; + } + namespace test { + 111111111111111111; + namespace test2 { + 22222222222222222; + } + } + inline namespace { + 111111111111111111; + } + inline /* test */ namespace { + 111111111111111111; + } + inline/* test */namespace { + 111111111111111111; + } + + /* invalid namespaces use block indent */ + namespace test test2 { + 111111111111111111111; + } + namespace11111111111 { + 111111111111; + } + namespace() { + 1111111111111; + } + namespace() + { + 111111111111111111; + } + namespace test test2 + { + 1111111111111111111; + } + namespace111111111 + { + 111111111111111111; + } + inlinenamespace { + 111111111111111111; + } + + void getstring() { + /* Raw strings */ + const char* s = R"( + test { + # comment + field: 123 + } + )"; + } + + void getstring() { + const char* s = R"foo( + test { + # comment + field: 123 + } + )foo"; + } + + { + int a[4] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + }; + } + + { + a = b[2] + + 3; + } + + { + if (1) + /* aaaaa + * bbbbb + */ + a = 1; + } + + void func() + { + switch (foo) + { + case (bar): + if (baz()) + quux(); + break; + case (shmoo): + if (!bar) + { + } + case (foo1): + switch (bar) + { + case baz: + baz_f(); + break; + } + break; + default: + baz(); + baz(); + break; + } + } + + /* end of AUTO */ + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_2() + new + setl cindent ts=4 sw=4 + setl tw=0 noai fo=croq + let &wm = &columns - 20 + + let code =<< trim [CODE] + { + + /* this is + * a real serious important big + * comment + */ + /* insert " about life, the universe, and the rest" after "serious" */ + } + [CODE] + + call append(0, code) + normal gg + call search('serious', 'e') + normal a about life, the universe, and the rest + + let expected =<< trim [CODE] + { + + /* this is + * a real serious + * about life, the + * universe, and the + * rest important big + * comment + */ + /* insert " about life, the universe, and the rest" after "serious" */ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + set wm& + enew! | close +endfunc + +func Test_cindent_3() + new + setl nocindent ts=4 sw=4 + + let code =<< trim [CODE] + { + /* + * Testing for comments, without 'cin' set + */ + + /* + * what happens here? + */ + + /* + the end of the comment, try inserting a line below */ + + /* how about + this one */ + } + [CODE] + + call append(0, code) + normal gg + call search('comments') + normal joabout life + call search('happens') + normal jothere + call search('below') + normal oline + call search('this') + normal Ohello + + let expected =<< trim [CODE] + { + /* + * Testing for comments, without 'cin' set + */ + about life + + /* + * what happens here? + */ + there + + /* + the end of the comment, try inserting a line below */ + line + + /* how about + hello + this one */ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_4() + new + setl cindent ts=4 sw=4 + + let code =<< trim [CODE] + { + var = this + that + vec[0] * vec[0] + + vec[1] * vec[1] + + vec2[2] * vec[2]; + } + [CODE] + + call append(0, code) + normal gg + call search('vec2') + normal == + + let expected =<< trim [CODE] + { + var = this + that + vec[0] * vec[0] + + vec[1] * vec[1] + + vec2[2] * vec[2]; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_5() + new + setl cindent ts=4 sw=4 + setl cino=}4 + + let code =<< trim [CODE] + { + asdf asdflkajds f; + if (tes & ting) { + asdf asdf asdf ; + asdfa sdf asdf; + } + testing1; + if (tes & ting) + { + asdf asdf asdf ; + asdfa sdf asdf; + } + testing2; + } + [CODE] + + call append(0, code) + normal gg + call search('testing1') + exe "normal k2==/testing2\<CR>" + normal k2== + + let expected =<< trim [CODE] + { + asdf asdflkajds f; + if (tes & ting) { + asdf asdf asdf ; + asdfa sdf asdf; + } + testing1; + if (tes & ting) + { + asdf asdf asdf ; + asdfa sdf asdf; + } + testing2; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_6() + new + setl cindent ts=4 sw=4 + setl cino=(0,)20 + + let code =<< trim [CODE] + main ( int first_par, /* + * Comment for + * first par + */ + int second_par /* + * Comment for + * second par + */ + ) + { + func( first_par, /* + * Comment for + * first par + */ + second_par /* + * Comment for + * second par + */ + ); + + } + [CODE] + + call append(0, code) + normal gg + call search('main') + normal =][ + + let expected =<< trim [CODE] + main ( int first_par, /* + * Comment for + * first par + */ + int second_par /* + * Comment for + * second par + */ + ) + { + func( first_par, /* + * Comment for + * first par + */ + second_par /* + * Comment for + * second par + */ + ); + + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_7() + new + setl cindent ts=4 sw=4 + setl cino=es,n0s + + let code =<< trim [CODE] + main(void) + { + /* Make sure that cino=X0s is not parsed like cino=Xs. */ + if (cond) + foo(); + else + { + bar(); + } + } + [CODE] + + call append(0, code) + normal gg + call search('main') + normal =][ + + let expected =<< trim [CODE] + main(void) + { + /* Make sure that cino=X0s is not parsed like cino=Xs. */ + if (cond) + foo(); + else + { + bar(); + } + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_8() + new + setl cindent ts=4 sw=4 + setl cino= + + let code =<< trim [CODE] + + { + do + { + if () + { + if () + asdf; + else + asdf; + } + } while (); + cmd; /* this should go under the } */ + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + + { + do + { + if () + { + if () + asdf; + else + asdf; + } + } while (); + cmd; /* this should go under the } */ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_9() + new + setl cindent ts=4 sw=4 + + let code =<< trim [CODE] + + void f() + { + if ( k() ) { + l(); + + } else { /* Start (two words) end */ + m(); + } + + n(); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + + void f() + { + if ( k() ) { + l(); + + } else { /* Start (two words) end */ + m(); + } + + n(); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_10() + new + setl cindent ts=4 sw=4 + setl cino={s,e-s + + let code =<< trim [CODE] + + void f() + { + if ( k() ) + { + l(); + } else { /* Start (two words) end */ + m(); + } + n(); /* should be under the if () */ + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + + void f() + { + if ( k() ) + { + l(); + } else { /* Start (two words) end */ + m(); + } + n(); /* should be under the if () */ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_11() + new + setl cindent ts=4 sw=4 + setl cino={s,fs + + let code =<< trim [CODE] + void bar(void) + { + static array[2][2] = + { + { 1, 2 }, + { 3, 4 }, + } + + while (a) + { + foo(&a); + } + + { + int a; + { + a = a + 1; + } + } + b = a; + } + + void func(void) + { + a = 1; + { + b = 2; + } + c = 3; + d = 4; + } + /* foo */ + [CODE] + + call append(0, code) + normal gg + exe "normal ]]=/ foo\<CR>" + + let expected =<< trim [CODE] + void bar(void) + { + static array[2][2] = + { + { 1, 2 }, + { 3, 4 }, + } + + while (a) + { + foo(&a); + } + + { + int a; + { + a = a + 1; + } + } + b = a; + } + + void func(void) + { + a = 1; + { + b = 2; + } + c = 3; + d = 4; + } + /* foo */ + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_12() + new + setl cindent ts=4 sw=4 + setl cino= + + let code =<< trim [CODE] + a() + { + do { + a = a + + a; + } while ( a ); /* add text under this line */ + if ( a ) + a; + } + [CODE] + + call append(0, code) + normal gg + call search('while') + normal ohere + + let expected =<< trim [CODE] + a() + { + do { + a = a + + a; + } while ( a ); /* add text under this line */ + here + if ( a ) + a; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_13() + new + setl cindent ts=4 sw=4 + setl cino= com= + + let code =<< trim [CODE] + a() + { + label1: + /* hmm */ + // comment + } + [CODE] + + call append(0, code) + normal gg + call search('comment') + exe "normal olabel2: b();\rlabel3 /* post */:\r/* pre */ label4:\r" . + \ "f(/*com*/);\rif (/*com*/)\rcmd();" + + let expected =<< trim [CODE] + a() + { + label1: + /* hmm */ + // comment + label2: b(); + label3 /* post */: + /* pre */ label4: + f(/*com*/); + if (/*com*/) + cmd(); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_14() + new + setl cindent ts=4 sw=4 + setl comments& comments^=s:/*,m:**,ex:*/ + + let code =<< trim [CODE] + /* + * A simple comment + */ + + /* + ** A different comment + */ + [CODE] + + call append(0, code) + normal gg + call search('simple') + normal =5j + + let expected =<< trim [CODE] + /* + * A simple comment + */ + + /* + ** A different comment + */ + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_15() + new + setl cindent ts=4 sw=4 + setl cino=c0 + setl comments& comments-=s1:/* comments^=s0:/* + + let code =<< trim [CODE] + void f() + { + + /********* + A comment. + *********/ + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + + /********* + A comment. + *********/ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_16() + new + setl cindent ts=4 sw=4 + setl cino=c0,C1 + setl comments& comments-=s1:/* comments^=s0:/* + + let code =<< trim [CODE] + void f() + { + + /********* + A comment. + *********/ + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + + /********* + A comment. + *********/ + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_17() + new + setl cindent ts=4 sw=4 + setl cino= + + let code =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_18() + new + setl cindent ts=4 sw=4 + setl cino=(s + + let code =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_19() + new + setl cindent ts=4 sw=4 + set cino=(s,U1 + + let code =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + c = c1 && + ( + c2 || + c3 + ) && c4; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_20() + new + setl cindent ts=4 sw=4 + setl cino=(0 + + let code =<< trim [CODE] + void f() + { + if ( c1 + && ( c2 + || c3)) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + if ( c1 + && ( c2 + || c3)) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_21() + new + setl cindent ts=4 sw=4 + setl cino=(0,w1 + + let code =<< trim [CODE] + void f() + { + if ( c1 + && ( c2 + || c3)) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + if ( c1 + && ( c2 + || c3)) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_22() + new + setl cindent ts=4 sw=4 + setl cino=(s + + let code =<< trim [CODE] + void f() + { + c = c1 && ( + c2 || + c3 + ) && c4; + if ( + c1 && c2 + ) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + c = c1 && ( + c2 || + c3 + ) && c4; + if ( + c1 && c2 + ) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_23() + new + setl cindent ts=4 sw=4 + setl cino=(s,m1 + + let code =<< trim [CODE] + void f() + { + c = c1 && ( + c2 || + c3 + ) && c4; + if ( + c1 && c2 + ) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + c = c1 && ( + c2 || + c3 + ) && c4; + if ( + c1 && c2 + ) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_24() + new + setl cindent ts=4 sw=4 + setl cino=b1 + + let code =<< trim [CODE] + void f() + { + switch (x) + { + case 1: + a = b; + break; + default: + a = 0; + break; + } + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + switch (x) + { + case 1: + a = b; + break; + default: + a = 0; + break; + } + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_25() + new + setl cindent ts=4 sw=4 + setl cino=(0,W5 + + let code =<< trim [CODE] + void f() + { + invokeme( + argu, + ment); + invokeme( + argu, + ment + ); + invokeme(argu, + ment + ); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + invokeme( + argu, + ment); + invokeme( + argu, + ment + ); + invokeme(argu, + ment + ); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_26() + new + setl cindent ts=4 sw=4 + setl cino=/6 + + let code =<< trim [CODE] + void f() + { + statement; + // comment 1 + // comment 2 + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void f() + { + statement; + // comment 1 + // comment 2 + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_27() + new + setl cindent ts=4 sw=4 + setl cino= + + let code =<< trim [CODE] + void f() + { + statement; + // comment 1 + // comment 2 + } + [CODE] + + call append(0, code) + normal gg + exe "normal ]]/comment 1/+1\<CR>==" + + let expected =<< trim [CODE] + void f() + { + statement; + // comment 1 + // comment 2 + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_28() + new + setl cindent ts=4 sw=4 + setl cino=g0 + + let code =<< trim [CODE] + class CAbc + { + int Test() { return FALSE; } + + public: // comment + void testfall(); + protected: + void testfall(); + }; + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + class CAbc + { + int Test() { return FALSE; } + + public: // comment + void testfall(); + protected: + void testfall(); + }; + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_29() + new + setl cindent ts=4 sw=4 + setl cino=(0,gs,hs + + let code =<< trim [CODE] + class Foo : public Bar + { + public: + virtual void method1(void) = 0; + virtual void method2(int arg1, + int arg2, + int arg3) = 0; + }; + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + class Foo : public Bar + { + public: + virtual void method1(void) = 0; + virtual void method2(int arg1, + int arg2, + int arg3) = 0; + }; + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_30() + new + setl cindent ts=4 sw=4 + setl cino=+20 + + let code =<< [CODE] + void +foo() +{ + if (a) + { + } else + asdf; +} +[CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< [CODE] + void +foo() +{ + if (a) + { + } else + asdf; +} + +[CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_31() + new + setl cindent ts=4 sw=4 + setl cino=(0,W2s + + let code =<< trim [CODE] + + { + averylongfunctionnamelongfunctionnameaverylongfunctionname()->asd( + asdasdf, + func(asdf, + asdfadsf), + asdfasdf + ); + + /* those are ugly, but consequent */ + + func()->asd(asdasdf, + averylongfunctionname( + abc, + dec)->averylongfunctionname( + asdfadsf, + asdfasdf, + asdfasdf, + ), + func(asdfadf, + asdfasdf + ), + asdasdf + ); + + averylongfunctionnameaverylongfunctionnameavery()->asd(fasdf( + abc, + dec)->asdfasdfasdf( + asdfadsf, + asdfasdf, + asdfasdf, + ), + func(asdfadf, + asdfasdf), + asdasdf + ); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + + { + averylongfunctionnamelongfunctionnameaverylongfunctionname()->asd( + asdasdf, + func(asdf, + asdfadsf), + asdfasdf + ); + + /* those are ugly, but consequent */ + + func()->asd(asdasdf, + averylongfunctionname( + abc, + dec)->averylongfunctionname( + asdfadsf, + asdfasdf, + asdfasdf, + ), + func(asdfadf, + asdfasdf + ), + asdasdf + ); + + averylongfunctionnameaverylongfunctionnameavery()->asd(fasdf( + abc, + dec)->asdfasdfasdf( + asdfadsf, + asdfasdf, + asdfasdf, + ), + func(asdfadf, + asdfasdf), + asdasdf + ); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_32() + new + setl cindent ts=4 sw=4 + setl cino=M1 + + let code =<< trim [CODE] + int main () + { + if (cond1 && + cond2 + ) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + int main () + { + if (cond1 && + cond2 + ) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_33() + new + setl cindent ts=4 sw=4 + setl cino=(0,ts + + let code =<< trim [CODE] + void func(int a + #if defined(FOO) + , int b + , int c + #endif + ) + { + } + [CODE] + + call append(0, code) + normal gg + normal 2j=][ + + let expected =<< trim [CODE] + void func(int a + #if defined(FOO) + , int b + , int c + #endif + ) + { + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_34() + new + setl cindent ts=4 sw=4 + setl cino=(0 + + let code =<< trim [CODE] + + void + func(int a + #if defined(FOO) + , int b + , int c + #endif + ) + { + } + [CODE] + + call append(0, code) + normal gg + normal =][ + + let expected =<< trim [CODE] + + void + func(int a + #if defined(FOO) + , int b + , int c + #endif + ) + { + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_35() + new + setl cindent ts=4 sw=4 + setl cino& + + let code =<< trim [CODE] + void func(void) + { + if(x==y) + if(y==z) + foo=1; + else { bar=1; + baz=2; + } + printf("Foo!\n"); + } + + void func1(void) + { + char* tab[] = {"foo", "bar", + "baz", "quux", + "this line used", "to be indented incorrectly"}; + foo(); + } + + void func2(void) + { + int tab[] = + {1, 2, + 3, 4, + 5, 6}; + + printf("This line used to be indented incorrectly.\n"); + } + + int foo[] + #ifdef BAR + + = { 1, 2, 3, + 4, 5, 6 } + + #endif + ; + int baz; + + void func3(void) + { + int tab[] = { + 1, 2, + 3, 4, + 5, 6}; + + printf("Don't you dare indent this line incorrectly!\n"); + } + + void + func4(a, b, + c) + int a; + int b; + int c; + { + } + + void + func5( + int a, + int b) + { + } + + void + func6( + int a) + { + } + [CODE] + + call append(0, code) + normal gg + normal ]]=7][ + + let expected =<< trim [CODE] + void func(void) + { + if(x==y) + if(y==z) + foo=1; + else { bar=1; + baz=2; + } + printf("Foo!\n"); + } + + void func1(void) + { + char* tab[] = {"foo", "bar", + "baz", "quux", + "this line used", "to be indented incorrectly"}; + foo(); + } + + void func2(void) + { + int tab[] = + {1, 2, + 3, 4, + 5, 6}; + + printf("This line used to be indented incorrectly.\n"); + } + + int foo[] + #ifdef BAR + + = { 1, 2, 3, + 4, 5, 6 } + + #endif + ; + int baz; + + void func3(void) + { + int tab[] = { + 1, 2, + 3, 4, + 5, 6}; + + printf("Don't you dare indent this line incorrectly!\n"); + } + + void + func4(a, b, + c) + int a; + int b; + int c; + { + } + + void + func5( + int a, + int b) + { + } + + void + func6( + int a) + { + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_36() + new + setl cindent ts=4 sw=4 + setl cino& + setl cino+=l1 + + let code =<< trim [CODE] + void func(void) + { + int tab[] = + { + 1, 2, 3, + 4, 5, 6}; + + printf("Indent this line correctly!\n"); + + switch (foo) + { + case bar: + printf("bar"); + break; + case baz: { + printf("baz"); + break; + } + case quux: + printf("But don't break the indentation of this instruction\n"); + break; + } + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + int tab[] = + { + 1, 2, 3, + 4, 5, 6}; + + printf("Indent this line correctly!\n"); + + switch (foo) + { + case bar: + printf("bar"); + break; + case baz: { + printf("baz"); + break; + } + case quux: + printf("But don't break the indentation of this instruction\n"); + break; + } + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_37() + new + setl cindent ts=4 sw=4 + setl cino& + + let code =<< trim [CODE] + void func(void) + { + cout << "a" + << "b" + << ") :" + << "c"; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + cout << "a" + << "b" + << ") :" + << "c"; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_38() + new + setl cindent ts=4 sw=4 + setl com=s1:/*,m:*,ex:*/ + + let code =<< trim [CODE] + void func(void) + { + /* + * This is a comment. + */ + } + [CODE] + + call append(0, code) + normal gg + normal ]]3jofoo(); + + let expected =<< trim [CODE] + void func(void) + { + /* + * This is a comment. + */ + foo(); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_39() + new + setl cindent ts=4 sw=4 + setl cino& + + let code =<< trim [CODE] + void func(void) + { + for (int i = 0; i < 10; ++i) + if (i & 1) { + foo(1); + } else + foo(0); + baz(); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + for (int i = 0; i < 10; ++i) + if (i & 1) { + foo(1); + } else + foo(0); + baz(); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_40() + new + setl cindent ts=4 sw=4 + setl cino=k2s,(0 + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_41() + new + setl cindent ts=4 sw=4 + setl cino=k2s,(s + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_42() + new + setl cindent ts=4 sw=4 + setl cino=k2s,(s,U1 + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + c = c1 && + ( + c2 || + c3 + ) && c4; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + c = c1 && + ( + c2 || + c3 + ) && c4; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_43() + new + setl cindent ts=4 sw=4 + setl cino=k2s,(0,W4 + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + if ( c1 + && ( c2 + || c3)) + foo; + + a_long_line( + argument, + argument); + a_short_line(argument, + argument); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + if ( c1 + && ( c2 + || c3)) + foo; + + a_long_line( + argument, + argument); + a_short_line(argument, + argument); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_44() + new + setl cindent ts=4 sw=4 + setl cino=k2s,u2 + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_45() + new + setl cindent ts=4 sw=4 + setl cino=k2s,(0,w1 + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + if (c123456789 + && (c22345 + || c3)) + printf("foo\n"); + + if ( c1 + && ( c2 + || c3)) + foo; + func( c1 + && ( c2 + || c3)) + foo; + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_46() + new + setl cindent ts=4 sw=4 + setl cino=k2,(s + + let code =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + } + [CODE] + + call append(0, code) + normal gg + normal ]]=][ + + let expected =<< trim [CODE] + void func(void) + { + if (condition1 + && condition2) + action(); + function(argument1 + && argument2); + + if (c1 && (c2 || + c3)) + foo; + if (c1 && + (c2 || c3)) + { + } + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_47() + new + setl cindent ts=4 sw=4 + setl cino=N-s + + let code =<< trim [CODE] + NAMESPACESTART + /* valid namespaces with normal indent */ + namespace + { + { + 111111111111; + } + } + namespace /* test */ + { + 11111111111111111; + } + namespace // test + { + 111111111111111111; + } + namespace + { + 111111111111111111; + } + namespace test + { + 111111111111111111; + } + namespace test::cpp17 + { + 111111111111111111; + } + namespace ::incorrectcpp17 + { + 111111111111111111; + } + namespace test::incorrectcpp17:: + { + 111111111111111111; + } + namespace test:incorrectcpp17 + { + 111111111111111111; + } + namespace test:::incorrectcpp17 + { + 111111111111111111; + } + namespace{ + 111111111111111111; + } + namespace test{ + 111111111111111111; + } + namespace { + 111111111111111111; + } + namespace test { + 111111111111111111; + namespace test2 { + 22222222222222222; + } + } + inline namespace { + 111111111111111111; + } + inline /* test */ namespace { + 111111111111111111; + } + inline/* test */namespace { + 111111111111111111; + } + + /* invalid namespaces use block indent */ + namespace test test2 { + 111111111111111111111; + } + namespace11111111111 { + 111111111111; + } + namespace() { + 1111111111111; + } + namespace() + { + 111111111111111111; + } + namespace test test2 + { + 1111111111111111111; + } + namespace111111111 + { + 111111111111111111; + } + inlinenamespace { + 111111111111111111; + } + NAMESPACEEND + [CODE] + + call append(0, code) + normal gg + call search('^NAMESPACESTART') + exe "normal =/^NAMESPACEEND\n" + + let expected =<< trim [CODE] + NAMESPACESTART + /* valid namespaces with normal indent */ + namespace + { + { + 111111111111; + } + } + namespace /* test */ + { + 11111111111111111; + } + namespace // test + { + 111111111111111111; + } + namespace + { + 111111111111111111; + } + namespace test + { + 111111111111111111; + } + namespace test::cpp17 + { + 111111111111111111; + } + namespace ::incorrectcpp17 + { + 111111111111111111; + } + namespace test::incorrectcpp17:: + { + 111111111111111111; + } + namespace test:incorrectcpp17 + { + 111111111111111111; + } + namespace test:::incorrectcpp17 + { + 111111111111111111; + } + namespace{ + 111111111111111111; + } + namespace test{ + 111111111111111111; + } + namespace { + 111111111111111111; + } + namespace test { + 111111111111111111; + namespace test2 { + 22222222222222222; + } + } + inline namespace { + 111111111111111111; + } + inline /* test */ namespace { + 111111111111111111; + } + inline/* test */namespace { + 111111111111111111; + } + + /* invalid namespaces use block indent */ + namespace test test2 { + 111111111111111111111; + } + namespace11111111111 { + 111111111111; + } + namespace() { + 1111111111111; + } + namespace() + { + 111111111111111111; + } + namespace test test2 + { + 1111111111111111111; + } + namespace111111111 + { + 111111111111111111; + } + inlinenamespace { + 111111111111111111; + } + NAMESPACEEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_48() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + var bar = { + foo: { + that: this, + some: ok, + }, + "bar":{ + a : 2, + b: "123abc", + x: 4, + "y": 5 + } + } + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + var bar = { + foo: { + that: this, + some: ok, + }, + "bar":{ + a : 2, + b: "123abc", + x: 4, + "y": 5 + } + } + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_49() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + var foo = [ + 1, + 2, + 3 + ]; + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + var foo = [ + 1, + 2, + 3 + ]; + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_50() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + function bar() { + var foo = [ + 1, + 2, + 3 + ]; + } + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + function bar() { + var foo = [ + 1, + 2, + 3 + ]; + } + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_51() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + (function($){ + + if (cond && + cond) { + stmt; + } + window.something.left = + (width - 50 + offset) + "px"; + var class_name='myclass'; + + function private_method() { + } + + var public_method={ + method: function(options,args){ + private_method(); + } + } + + function init(options) { + + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + + $.fn[class_name]=function() { + + var _arguments=arguments; + return this.each(function(){ + + var options=$(this).data(class_name+'_public'); + if (!options) { + init.apply(this,_arguments); + + } else { + var method=public_method[_arguments[0]]; + + if (typeof(method)!='function') { + console.log(class_name+' has no method "'+_arguments[0]+'"'); + return false; + } + _arguments[0]=options; + method.apply(this,_arguments); + } + }); + } + + })(jQuery); + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + (function($){ + + if (cond && + cond) { + stmt; + } + window.something.left = + (width - 50 + offset) + "px"; + var class_name='myclass'; + + function private_method() { + } + + var public_method={ + method: function(options,args){ + private_method(); + } + } + + function init(options) { + + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + + $.fn[class_name]=function() { + + var _arguments=arguments; + return this.each(function(){ + + var options=$(this).data(class_name+'_public'); + if (!options) { + init.apply(this,_arguments); + + } else { + var method=public_method[_arguments[0]]; + + if (typeof(method)!='function') { + console.log(class_name+' has no method "'+_arguments[0]+'"'); + return false; + } + _arguments[0]=options; + method.apply(this,_arguments); + } + }); + } + + })(jQuery); + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_52() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + function init(options) { + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + function init(options) { + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_53() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1 + + let code =<< trim [CODE] + JSSTART + (function($){ + function init(options) { + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + })(jQuery); + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + (function($){ + function init(options) { + $(this).data(class_name+'_public',$.extend({},{ + foo: 'bar', + bar: 2, + foobar: [ + 1, + 2, + 3 + ], + callback: function(){ + return true; + } + }, options||{})); + } + })(jQuery); + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_54() + new + setl cindent ts=4 sw=4 + setl cino=j1,J1,+2 + + let code =<< trim [CODE] + JSSTART + // Results of JavaScript indent + // 1 + (function(){ + var a = [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 2 + (function(){ + var a = [ + 0 + + 5 * + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 3 + (function(){ + var a = [ + 0 + + // comment 1 + 5 * + /* comment 2 */ + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 4 + { + var a = [ + 0, + 1 + ]; + var b; + var c; + } + + // 5 + { + var a = [ + [ + 0 + ], + 2, + 3 + ]; + } + + // 6 + { + var a = [ + [ + 0, + 1 + ], + 2, + 3 + ]; + } + + // 7 + { + var a = [ + // [ + 0, + // 1 + // ], + 2, + 3 + ]; + } + + // 8 + var x = [ + (function(){ + var a, + b, + c, + d, + e, + f, + g, + h, + i; + }) + ]; + + // 9 + var a = [ + 0 + + 5 * + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + + // 10 + var a, + b, + c, + d, + e, + f, + g, + h, + i; + JSEND + [CODE] + + call append(0, code) + normal gg + call search('^JSSTART') + exe "normal =/^JSEND\n" + + let expected =<< trim [CODE] + JSSTART + // Results of JavaScript indent + // 1 + (function(){ + var a = [ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 2 + (function(){ + var a = [ + 0 + + 5 * + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 3 + (function(){ + var a = [ + 0 + + // comment 1 + 5 * + /* comment 2 */ + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + }()) + + // 4 + { + var a = [ + 0, + 1 + ]; + var b; + var c; + } + + // 5 + { + var a = [ + [ + 0 + ], + 2, + 3 + ]; + } + + // 6 + { + var a = [ + [ + 0, + 1 + ], + 2, + 3 + ]; + } + + // 7 + { + var a = [ + // [ + 0, + // 1 + // ], + 2, + 3 + ]; + } + + // 8 + var x = [ + (function(){ + var a, + b, + c, + d, + e, + f, + g, + h, + i; + }) + ]; + + // 9 + var a = [ + 0 + + 5 * + 9 * + 'a', + 'b', + 0 + + 5 * + 9 * + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i' + ]; + + // 10 + var a, + b, + c, + d, + e, + f, + g, + h, + i; + JSEND + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_55() + new + setl cindent ts=4 sw=4 + setl cino& + + let code =<< trim [CODE] + /* start of define */ + { + } + #define AAA \ + BBB\ + CCC + + #define CNT \ + 1 + \ + 2 + \ + 4 + /* end of define */ + [CODE] + + call append(0, code) + normal gg + call search('start of define') + exe "normal =/end of define\n" + + let expected =<< trim [CODE] + /* start of define */ + { + } + #define AAA \ + BBB\ + CCC + + #define CNT \ + 1 + \ + 2 + \ + 4 + /* end of define */ + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + +func Test_cindent_56() + new + setl cindent ts=4 sw=4 + setl cino& + + let code =<< trim [CODE] + { + a = second/*bug*/*line; + } + [CODE] + + call append(0, code) + normal gg + call search('a = second') + normal ox + + let expected =<< trim [CODE] + { + a = second/*bug*/*line; + x + } + + [CODE] + + call assert_equal(expected, getline(1, '$')) + enew! | close +endfunc + " this was going beyond the end of the line. func Test_cindent_case() new - call setline(1, "case x: // x") + call setline(1, 'case x: // x') set cindent norm! f:a: + call assert_equal('case x:: // x', getline(1)) + + set cindent& bwipe! endfunc @@ -173,4 +5347,23 @@ func Test_cindent_pragma() enew! | close endfunc +func Test_backslash_at_end_of_line() + new + exe "norm v>O'\\\<C-m>-" + exe "norm \<C-q>=" + bwipe! +endfunc + +func Test_find_brace_backwards() + " this was looking beyond the end of the line + new + norm R/* + norm o0{ + norm o// + norm V{= + call assert_equal(['/*', ' 0{', '//'], getline(1, 3)) + bwipe! +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim index 1672b0e840..ff4cbe544c 100644 --- a/src/nvim/testdir/test_cmdline.vim +++ b/src/nvim/testdir/test_cmdline.vim @@ -500,8 +500,16 @@ func Test_fullcommand() for [in, want] in items(tests) call assert_equal(want, fullcommand(in)) endfor + call assert_equal('', fullcommand(v:_null_string)) call assert_equal('syntax', 'syn'->fullcommand()) + + command -buffer BufferLocalCommand : + command GlobalCommand : + call assert_equal('GlobalCommand', fullcommand('GlobalCom')) + call assert_equal('BufferLocalCommand', fullcommand('BufferL')) + delcommand BufferLocalCommand + delcommand GlobalCommand endfunc func Test_shellcmd_completion() diff --git a/src/nvim/testdir/test_compiler.vim b/src/nvim/testdir/test_compiler.vim index aaa2301bca..c0c572ce65 100644 --- a/src/nvim/testdir/test_compiler.vim +++ b/src/nvim/testdir/test_compiler.vim @@ -19,6 +19,9 @@ func Test_compiler() call assert_equal('perl', b:current_compiler) call assert_fails('let g:current_compiler', 'E121:') + let verbose_efm = execute('verbose set efm') + call assert_match('Last set from .*[/\\]compiler[/\\]perl.vim ', verbose_efm) + call setline(1, ['#!/usr/bin/perl -w', 'use strict;', 'my $foo=1']) w! call feedkeys(":make\<CR>\<CR>", 'tx') diff --git a/src/nvim/testdir/test_conceal.vim b/src/nvim/testdir/test_conceal.vim index 1306dbe5cf..bffc2f49d3 100644 --- a/src/nvim/testdir/test_conceal.vim +++ b/src/nvim/testdir/test_conceal.vim @@ -4,10 +4,10 @@ source check.vim CheckFeature conceal source screendump.vim -" CheckScreendump func Test_conceal_two_windows() CheckScreendump + let code =<< trim [CODE] let lines = ["one one one one one", "two |hidden| here", "three |hidden| three"] call setline(1, lines) @@ -111,6 +111,7 @@ endfunc func Test_conceal_with_cursorline() CheckScreendump + " Opens a help window, where 'conceal' is set, switches to the other window " where 'cursorline' needs to be updated when the cursor moves. let code =<< trim [CODE] @@ -139,6 +140,7 @@ endfunc func Test_conceal_resize_term() CheckScreendump + let code =<< trim [CODE] call setline(1, '`one` `two` `three` `four` `five`, the backticks should be concealed') setl cocu=n cole=3 diff --git a/src/nvim/testdir/test_cscope.vim b/src/nvim/testdir/test_cscope.vim index cc6154af69..faf37485cd 100644 --- a/src/nvim/testdir/test_cscope.vim +++ b/src/nvim/testdir/test_cscope.vim @@ -102,7 +102,7 @@ func Test_cscopeWithCscopeConnections() for cmd in ['cs find f Xmemfile_test.c', 'cs find 7 Xmemfile_test.c'] enew let a = execute(cmd) - call assert_true(a =~ '"Xmemfile_test.c" \d\+L, \d\+C') + call assert_true(a =~ '"Xmemfile_test.c" \d\+L, \d\+B') call assert_equal('Xmemfile_test.c', @%) endfor @@ -112,7 +112,7 @@ func Test_cscopeWithCscopeConnections() let a = execute(cmd) let alines = split(a, '\n', 1) call assert_equal('', alines[0]) - call assert_true(alines[1] =~ '"Xmemfile_test.c" \d\+L, \d\+C') + call assert_true(alines[1] =~ '"Xmemfile_test.c" \d\+L, \d\+B') call assert_equal('(1 of 1): <<global>> #include <assert.h>', alines[2]) call assert_equal('#include <assert.h>', getline('.')) endfor diff --git a/src/nvim/testdir/test_cursor_func.vim b/src/nvim/testdir/test_cursor_func.vim index e8c4a952ee..57825b4551 100644 --- a/src/nvim/testdir/test_cursor_func.vim +++ b/src/nvim/testdir/test_cursor_func.vim @@ -1,4 +1,4 @@ -" Tests for cursor(). +" Tests for cursor() and other functions that get/set the cursor position func Test_wrong_arguments() call assert_fails('call cursor(1. 3)', 'E474:') @@ -24,6 +24,9 @@ func Test_move_cursor() " below last line goes to last line call cursor(9, 1) call assert_equal([4, 1, 0, 1], getcurpos()[1:]) + " pass string arguments + call cursor('3', '3') + call assert_equal([3, 3, 0, 3], getcurpos()[1:]) call setline(1, ["\<TAB>"]) call cursor(1, 1, 1) @@ -72,7 +75,6 @@ func Test_curswant_with_cursorline() endfunc func Test_screenpos() - throw 'skipped: TODO: ' rightbelow new rightbelow 20vsplit call setline(1, ["\tsome text", "long wrapping line here", "next line"]) @@ -100,9 +102,10 @@ func Test_screenpos() bwipe! call assert_equal({'col': 1, 'row': 1, 'endcol': 1, 'curscol': 1}, screenpos(win_getid(), 1, 1)) - nmenu WinBar.TEST : - call assert_equal({'col': 1, 'row': 2, 'endcol': 1, 'curscol': 1}, screenpos(win_getid(), 1, 1)) - nunmenu WinBar.TEST + " Needs WinBar + " nmenu WinBar.TEST : + " call assert_equal({'col': 1, 'row': 2, 'endcol': 1, 'curscol': 1}, screenpos(win_getid(), 1, 1)) + " nunmenu WinBar.TEST endfunc func Test_screenpos_number() @@ -119,3 +122,247 @@ func Test_screenpos_number() close bwipe! endfunc + +" Save the visual start character position +func SaveVisualStartCharPos() + call add(g:VisualStartPos, getcharpos('v')) + return '' +endfunc + +" Save the current cursor character position in insert mode +func SaveInsertCurrentCharPos() + call add(g:InsertCurrentPos, getcharpos('.')) + return '' +endfunc + +" Test for the getcharpos() function +func Test_getcharpos() + call assert_fails('call getcharpos({})', 'E731:') + call assert_equal([0, 0, 0, 0], getcharpos(0)) + new + call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) + + " Test for '.' and '$' + normal 1G + call assert_equal([0, 1, 1, 0], getcharpos('.')) + call assert_equal([0, 4, 1, 0], getcharpos('$')) + normal 2G6l + call assert_equal([0, 2, 7, 0], getcharpos('.')) + normal 3G$ + call assert_equal([0, 3, 1, 0], getcharpos('.')) + normal 4G$ + call assert_equal([0, 4, 9, 0], getcharpos('.')) + + " Test for a mark + normal 2G7lmmgg + call assert_equal([0, 2, 8, 0], getcharpos("'m")) + delmarks m + call assert_equal([0, 0, 0, 0], getcharpos("'m")) + + " Test for the visual start column + vnoremap <expr> <F3> SaveVisualStartCharPos() + let g:VisualStartPos = [] + exe "normal 2G6lv$\<F3>ohh\<F3>o\<F3>" + call assert_equal([[0, 2, 7, 0], [0, 2, 10, 0], [0, 2, 5, 0]], g:VisualStartPos) + call assert_equal([0, 2, 9, 0], getcharpos('v')) + let g:VisualStartPos = [] + exe "normal 3Gv$\<F3>o\<F3>" + call assert_equal([[0, 3, 1, 0], [0, 3, 2, 0]], g:VisualStartPos) + let g:VisualStartPos = [] + exe "normal 1Gv$\<F3>o\<F3>" + call assert_equal([[0, 1, 1, 0], [0, 1, 1, 0]], g:VisualStartPos) + vunmap <F3> + + " Test for getting the position in insert mode with the cursor after the + " last character in a line + inoremap <expr> <F3> SaveInsertCurrentCharPos() + let g:InsertCurrentPos = [] + exe "normal 1GA\<F3>" + exe "normal 2GA\<F3>" + exe "normal 3GA\<F3>" + exe "normal 4GA\<F3>" + exe "normal 2G6li\<F3>" + call assert_equal([[0, 1, 1, 0], [0, 2, 10, 0], [0, 3, 2, 0], [0, 4, 10, 0], + \ [0, 2, 7, 0]], g:InsertCurrentPos) + iunmap <F3> + + %bw! +endfunc + +" Test for the setcharpos() function +func Test_setcharpos() + call assert_equal(-1, setcharpos('.', v:_null_list)) + new + call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) + call setcharpos('.', [0, 1, 1, 0]) + call assert_equal([1, 1], [line('.'), col('.')]) + call setcharpos('.', [0, 2, 7, 0]) + call assert_equal([2, 9], [line('.'), col('.')]) + call setcharpos('.', [0, 3, 4, 0]) + call assert_equal([3, 1], [line('.'), col('.')]) + call setcharpos('.', [0, 3, 1, 0]) + call assert_equal([3, 1], [line('.'), col('.')]) + call setcharpos('.', [0, 4, 0, 0]) + call assert_equal([4, 1], [line('.'), col('.')]) + call setcharpos('.', [0, 4, 20, 0]) + call assert_equal([4, 9], [line('.'), col('.')]) + + " Test for mark + delmarks m + call setcharpos("'m", [0, 2, 9, 0]) + normal `m + call assert_equal([2, 11], [line('.'), col('.')]) + " unload the buffer and try to set the mark + let bnr = bufnr() + enew! + call assert_equal(-1, setcharpos("'m", [bnr, 2, 2, 0])) + + %bw! + call assert_equal(-1, setcharpos('.', [10, 3, 1, 0])) +endfunc + +func SaveVisualStartCharCol() + call add(g:VisualStartCol, charcol('v')) + return '' +endfunc + +func SaveInsertCurrentCharCol() + call add(g:InsertCurrentCol, charcol('.')) + return '' +endfunc + +" Test for the charcol() function +func Test_charcol() + call assert_fails('call charcol({})', 'E731:') + call assert_equal(0, charcol(0)) + new + call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) + + " Test for '.' and '$' + normal 1G + call assert_equal(1, charcol('.')) + call assert_equal(1, charcol('$')) + normal 2G6l + call assert_equal(7, charcol('.')) + call assert_equal(10, charcol('$')) + normal 3G$ + call assert_equal(1, charcol('.')) + call assert_equal(2, charcol('$')) + normal 4G$ + call assert_equal(9, charcol('.')) + call assert_equal(10, charcol('$')) + + " Test for [lnum, '$'] + call assert_equal(1, charcol([1, '$'])) + call assert_equal(10, charcol([2, '$'])) + call assert_equal(2, charcol([3, '$'])) + call assert_equal(0, charcol([5, '$'])) + + " Test for a mark + normal 2G7lmmgg + call assert_equal(8, charcol("'m")) + delmarks m + call assert_equal(0, charcol("'m")) + + " Test for the visual start column + vnoremap <expr> <F3> SaveVisualStartCharCol() + let g:VisualStartCol = [] + exe "normal 2G6lv$\<F3>ohh\<F3>o\<F3>" + call assert_equal([7, 10, 5], g:VisualStartCol) + call assert_equal(9, charcol('v')) + let g:VisualStartCol = [] + exe "normal 3Gv$\<F3>o\<F3>" + call assert_equal([1, 2], g:VisualStartCol) + let g:VisualStartCol = [] + exe "normal 1Gv$\<F3>o\<F3>" + call assert_equal([1, 1], g:VisualStartCol) + vunmap <F3> + + " Test for getting the column number in insert mode with the cursor after + " the last character in a line + inoremap <expr> <F3> SaveInsertCurrentCharCol() + let g:InsertCurrentCol = [] + exe "normal 1GA\<F3>" + exe "normal 2GA\<F3>" + exe "normal 3GA\<F3>" + exe "normal 4GA\<F3>" + exe "normal 2G6li\<F3>" + call assert_equal([1, 10, 2, 10, 7], g:InsertCurrentCol) + iunmap <F3> + + %bw! +endfunc + +func SaveInsertCursorCharPos() + call add(g:InsertCursorPos, getcursorcharpos('.')) + return '' +endfunc + +" Test for getcursorcharpos() +func Test_getcursorcharpos() + call assert_equal(getcursorcharpos(), getcursorcharpos(0)) + call assert_equal([0, 0, 0, 0, 0], getcursorcharpos(-1)) + call assert_equal([0, 0, 0, 0, 0], getcursorcharpos(1999)) + + new + call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) + normal 1G9l + call assert_equal([0, 1, 1, 0, 1], getcursorcharpos()) + normal 2G9l + call assert_equal([0, 2, 9, 0, 14], getcursorcharpos()) + normal 3G9l + call assert_equal([0, 3, 1, 0, 1], getcursorcharpos()) + normal 4G9l + call assert_equal([0, 4, 9, 0, 9], getcursorcharpos()) + + " Test for getting the cursor position in insert mode with the cursor after + " the last character in a line + inoremap <expr> <F3> SaveInsertCursorCharPos() + let g:InsertCursorPos = [] + exe "normal 1GA\<F3>" + exe "normal 2GA\<F3>" + exe "normal 3GA\<F3>" + exe "normal 4GA\<F3>" + exe "normal 2G6li\<F3>" + call assert_equal([[0, 1, 1, 0, 1], [0, 2, 10, 0, 15], [0, 3, 2, 0, 2], + \ [0, 4, 10, 0, 10], [0, 2, 7, 0, 12]], g:InsertCursorPos) + iunmap <F3> + + let winid = win_getid() + normal 2G5l + wincmd w + call assert_equal([0, 2, 6, 0, 11], getcursorcharpos(winid)) + %bw! +endfunc + +" Test for setcursorcharpos() +func Test_setcursorcharpos() + call assert_fails('call setcursorcharpos(v:_null_list)', 'E474:') + call assert_fails('call setcursorcharpos([1])', 'E474:') + call assert_fails('call setcursorcharpos([1, 1, 1, 1, 1])', 'E474:') + new + call setline(1, ['', "01\tà4è678", 'Ⅵ', '012345678']) + normal G + call setcursorcharpos([1, 1]) + call assert_equal([1, 1], [line('.'), col('.')]) + call setcursorcharpos([2, 7, 0]) + call assert_equal([2, 9], [line('.'), col('.')]) + call setcursorcharpos(3, 4) + call assert_equal([3, 1], [line('.'), col('.')]) + call setcursorcharpos([3, 1]) + call assert_equal([3, 1], [line('.'), col('.')]) + call setcursorcharpos([4, 0, 0, 0]) + call assert_equal([4, 1], [line('.'), col('.')]) + call setcursorcharpos([4, 20]) + call assert_equal([4, 9], [line('.'), col('.')]) + normal 1G + call setcursorcharpos([100, 100, 100, 100]) + call assert_equal([4, 9], [line('.'), col('.')]) + normal 1G + call setcursorcharpos('$', 1) + call assert_equal([4, 1], [line('.'), col('.')]) + + %bw! +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_diffmode.vim b/src/nvim/testdir/test_diffmode.vim index 3a0c615cf6..482d39056f 100644 --- a/src/nvim/testdir/test_diffmode.vim +++ b/src/nvim/testdir/test_diffmode.vim @@ -1146,6 +1146,35 @@ func Test_diff_followwrap() bwipe! endfunc +func Test_diff_maintains_change_mark() + func DiffMaintainsChangeMark() + enew! + call setline(1, ['a', 'b', 'c', 'd']) + diffthis + new + call setline(1, ['a', 'b', 'c', 'e']) + " Set '[ and '] marks + 2,3yank + call assert_equal([2, 3], [line("'["), line("']")]) + " Verify they aren't affected by the implicit diff + diffthis + call assert_equal([2, 3], [line("'["), line("']")]) + " Verify they aren't affected by an explicit diff + diffupdate + call assert_equal([2, 3], [line("'["), line("']")]) + bwipe! + bwipe! + endfunc + + set diffopt-=internal + call DiffMaintainsChangeMark() + set diffopt+=internal + call DiffMaintainsChangeMark() + + set diffopt& + delfunc DiffMaintainsChangeMark +endfunc + func Test_diff_rnu() CheckScreendump diff --git a/src/nvim/testdir/test_digraph.vim b/src/nvim/testdir/test_digraph.vim index d23748a3e3..5965ee48ef 100644 --- a/src/nvim/testdir/test_digraph.vim +++ b/src/nvim/testdir/test_digraph.vim @@ -81,7 +81,7 @@ func Test_digraphs() call Put_Dig(".e") call Put_Dig("a.") " not defined call assert_equal(['ḃ', 'ė', '.'], getline(line('.')-2,line('.'))) - " Diaresis + " Diaeresis call Put_Dig("a:") call Put_Dig(":u") call Put_Dig("b:") " not defined @@ -288,7 +288,7 @@ func Test_digraphs_option() call Put_Dig_BS(".","e") call Put_Dig_BS("a",".") " not defined call assert_equal(['ḃ', 'ė', '.'], getline(line('.')-2,line('.'))) - " Diaresis + " Diaeresis call Put_Dig_BS("a",":") call Put_Dig_BS(":","u") call Put_Dig_BS("b",":") " not defined diff --git a/src/nvim/testdir/test_display.vim b/src/nvim/testdir/test_display.vim index c2a9683f7c..9f74d0a38a 100644 --- a/src/nvim/testdir/test_display.vim +++ b/src/nvim/testdir/test_display.vim @@ -41,7 +41,7 @@ func Test_display_foldcolumn() quit! endfunc -func! Test_display_foldtext_mbyte() +func Test_display_foldtext_mbyte() CheckFeature folding call NewWindow(10, 40) diff --git a/src/nvim/testdir/test_edit.vim b/src/nvim/testdir/test_edit.vim index fc4e80f0d6..360b3aaaa0 100644 --- a/src/nvim/testdir/test_edit.vim +++ b/src/nvim/testdir/test_edit.vim @@ -213,7 +213,7 @@ func Test_edit_07() bw! endfunc -func! Test_edit_08() +func Test_edit_08() throw 'skipped: moved to test/functional/legacy/edit_spec.lua' " reset insertmode from i_ctrl-r_= let g:bufnr = bufnr('%') @@ -417,7 +417,7 @@ func Test_edit_13() bwipe! endfunc -func! Test_edit_CR() +func Test_edit_CR() " Test for <CR> in insert mode " basically only in quickfix mode ist tested, the rest " has been taken care of by other tests @@ -450,7 +450,7 @@ func! Test_edit_CR() call delete('Xqflist.txt') endfunc -func! Test_edit_CTRL_() +func Test_edit_CTRL_() " disabled for Windows builds, why? if !has("rightleft") || has("win32") return @@ -590,7 +590,7 @@ func Test_edit_CTRL_K() call feedkeys("A\<c-x>\<c-k>\<down>\<down>\<down>\<down>\<cr>\<esc>", 'tnix') call assert_equal(['AA'], getline(1, '$')) - " press an unexecpted key after dictionary completion + " press an unexpected key after dictionary completion %d call setline(1, 'A') call cursor(1, 1) @@ -734,7 +734,7 @@ func Test_edit_CTRL_O() bw! endfunc -func! Test_edit_CTRL_R() +func Test_edit_CTRL_R() " Insert Register new " call test_override("ALL", 1) @@ -1006,16 +1006,14 @@ func Test_edit_DROP() endfunc func Test_edit_CTRL_V() - if has("ebcdic") - return - endif new call setline(1, ['abc']) call cursor(2, 1) + " force some redraws set showmode showcmd - "call test_override_char_avail(1) - " call test_override('ALL', 1) + " call test_override('char_avail', 1) + call feedkeys("A\<c-v>\<c-n>\<c-v>\<c-l>\<c-v>\<c-b>\<esc>", 'tnix') call assert_equal(["abc\x0e\x0c\x02"], getline(1, '$')) @@ -1028,8 +1026,19 @@ func Test_edit_CTRL_V() set norl endif - " call test_override('ALL', 0) set noshowmode showcmd + " call test_override('char_avail', 0) + + " No modifiers should be applied to the char typed using i_CTRL-V_digit. + call feedkeys(":append\<CR>\<C-V>76c\<C-V>76\<C-F2>\<C-V>u3c0j\<C-V>u3c0\<M-F3>\<CR>.\<CR>", 'tnix') + call assert_equal('LcL<C-F2>πjπ<M-F3>', getline(2)) + + if has('osx') + " A char with a modifier should not be a valid char for i_CTRL-V_digit. + call feedkeys("o\<C-V>\<D-j>\<C-V>\<D-1>\<C-V>\<D-o>\<C-V>\<D-x>\<C-V>\<D-u>", 'tnix') + call assert_equal('<D-j><D-1><D-o><D-x><D-u>', getline(3)) + endif + bw! endfunc @@ -1550,11 +1559,7 @@ endfunc func Test_edit_special_chars() new - if has("ebcdic") - let t = "o\<C-V>193\<C-V>xc2\<C-V>o303 \<C-V>90a\<C-V>xfg\<C-V>o578\<Esc>" - else - let t = "o\<C-V>65\<C-V>x42\<C-V>o103 \<C-V>33a\<C-V>xfg\<C-V>o78\<Esc>" - endif + let t = "o\<C-V>65\<C-V>x42\<C-V>o103 \<C-V>33a\<C-V>xfg\<C-V>o78\<Esc>" exe "normal " . t call assert_equal("ABC !a\<C-O>g\<C-G>8", getline(2)) diff --git a/src/nvim/testdir/test_eval_stuff.vim b/src/nvim/testdir/test_eval_stuff.vim index 883ba5de3d..95eccde35c 100644 --- a/src/nvim/testdir/test_eval_stuff.vim +++ b/src/nvim/testdir/test_eval_stuff.vim @@ -65,11 +65,9 @@ func Test_E963() endfunc func Test_for_invalid() - " Vim gives incorrect emsg here until v8.2.3284, but the exact emsg from that - " patch cannot be used until v8.2.2658 is ported (for loop over Strings) - call assert_fails("for x in 99", 'E897:') - call assert_fails("for x in function('winnr')", 'E897:') - call assert_fails("for x in {'a': 9}", 'E897:') + call assert_fails("for x in 99", 'E1098:') + call assert_fails("for x in function('winnr')", 'E1098:') + call assert_fails("for x in {'a': 9}", 'E1098:') if 0 /1/5/2/s/\n diff --git a/src/nvim/testdir/test_ex_mode.vim b/src/nvim/testdir/test_ex_mode.vim index 92e0559618..dcec5f7cc6 100644 --- a/src/nvim/testdir/test_ex_mode.vim +++ b/src/nvim/testdir/test_ex_mode.vim @@ -29,12 +29,11 @@ endfunc " Test editing line in Ex mode (both Q and gQ) func Test_ex_mode() - throw 'skipped: TODO: ' + throw 'Skipped: Nvim only supports Vim Ex mode' let encoding_save = &encoding set sw=2 - " for e in ['utf8', 'latin1'] - for e in ['utf8'] + for e in ['utf8', 'latin1'] exe 'set encoding=' . e call assert_equal(['bar', 'bar'], Ex("foo bar\<C-u>bar"), e) @@ -98,4 +97,14 @@ func Test_ex_mode_count_overflow() call delete('Xexmodescript') endfunc +func Test_ex_mode_large_indent() + new + set ts=500 ai + call setline(1, "\t") + exe "normal gQi\<CR>." + set ts=8 noai + bwipe! +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_excmd.vim b/src/nvim/testdir/test_excmd.vim index 2d01cbba83..8055a51a11 100644 --- a/src/nvim/testdir/test_excmd.vim +++ b/src/nvim/testdir/test_excmd.vim @@ -1,6 +1,8 @@ " Tests for various Ex commands. source check.vim +source shared.vim +source term_util.vim func Test_ex_delete() new @@ -122,6 +124,27 @@ func Test_append_cmd() close! endfunc +func Test_append_cmd_empty_buf() + CheckRunVimInTerminal + let lines =<< trim END + func Timer(timer) + append + aaaaa + bbbbb + . + endfunc + call timer_start(10, 'Timer') + END + call writefile(lines, 'Xtest_append_cmd_empty_buf') + let buf = RunVimInTerminal('-S Xtest_append_cmd_empty_buf', {'rows': 6}) + call WaitForAssert({-> assert_equal('bbbbb', term_getline(buf, 2))}) + call WaitForAssert({-> assert_equal('aaaaa', term_getline(buf, 1))}) + + " clean up + call StopVimInTerminal(buf) + call delete('Xtest_append_cmd_empty_buf') +endfunc + " Test for the :insert command func Test_insert_cmd() set noautoindent " test assumes noautoindent, but it's on by default in Nvim @@ -151,6 +174,27 @@ func Test_insert_cmd() close! endfunc +func Test_insert_cmd_empty_buf() + CheckRunVimInTerminal + let lines =<< trim END + func Timer(timer) + insert + aaaaa + bbbbb + . + endfunc + call timer_start(10, 'Timer') + END + call writefile(lines, 'Xtest_insert_cmd_empty_buf') + let buf = RunVimInTerminal('-S Xtest_insert_cmd_empty_buf', {'rows': 6}) + call WaitForAssert({-> assert_equal('bbbbb', term_getline(buf, 2))}) + call WaitForAssert({-> assert_equal('aaaaa', term_getline(buf, 1))}) + + " clean up + call StopVimInTerminal(buf) + call delete('Xtest_insert_cmd_empty_buf') +endfunc + " Test for the :change command func Test_change_cmd() set noautoindent " test assumes noautoindent, but it's on by default in Nvim @@ -356,3 +400,27 @@ func Test_winsize_cmd() call assert_fails('win_getid(1)', 'E475: Invalid argument: _getid(1)') " Actually changing the window size would be flaky. endfunc + +func Test_not_break_expression_register() + call setreg('=', '1+1') + if 0 + put =1 + endif + call assert_equal('1+1', getreg('=', 1)) +endfunc + +func Test_address_line_overflow() + throw 'Skipped: v:sizeoflong is N/A' " use legacy/excmd_spec.lua instead + + if v:sizeoflong < 8 + throw 'Skipped: only works with 64 bit long ints' + endif + new + call setline(1, 'text') + call assert_fails('|.44444444444444444444444', 'E1247:') + call assert_fails('|.9223372036854775806', 'E1247:') + bwipe! +endfunc + + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_exec_while_if.vim b/src/nvim/testdir/test_exec_while_if.vim index 3da2784d77..3f13b09945 100644 --- a/src/nvim/testdir/test_exec_while_if.vim +++ b/src/nvim/testdir/test_exec_while_if.vim @@ -6,11 +6,7 @@ func Test_exec_while_if() let i = 0 while i < 12 let i = i + 1 - if has("ebcdic") - execute "normal o" . i . "\047" - else - execute "normal o" . i . "\033" - endif + execute "normal o" . i . "\033" if i % 2 normal Ax if i == 9 @@ -21,21 +17,13 @@ func Test_exec_while_if() else let j = 9 while j > 0 - if has("ebcdic") - execute "normal" j . "a" . j . "\x27" - else - execute "normal" j . "a" . j . "\x1b" - endif + execute "normal" j . "a" . j . "\x1b" let j = j - 1 endwhile endif endif if i == 9 - if has("ebcdic") - execute "normal Az\047" - else - execute "normal Az\033" - endif + execute "normal Az\033" endif endwhile unlet i j diff --git a/src/nvim/testdir/test_execute_func.vim b/src/nvim/testdir/test_execute_func.vim index 2cb6d73407..16cc20e9a7 100644 --- a/src/nvim/testdir/test_execute_func.vim +++ b/src/nvim/testdir/test_execute_func.vim @@ -147,3 +147,30 @@ func Test_win_execute_other_tab() tabclose unlet xyz endfunc + +func Test_win_execute_visual_redraw() + call setline(1, ['a', 'b', 'c']) + new + wincmd p + " start Visual in current window, redraw in other window with fewer lines + call feedkeys("G\<C-V>", 'txn') + call win_execute(winnr('#')->win_getid(), 'redraw') + call feedkeys("\<Esc>", 'txn') + bwipe! + bwipe! + + enew + new + call setline(1, ['a', 'b', 'c']) + let winid = win_getid() + wincmd p + " start Visual in current window, extend it in other window with more lines + call feedkeys("\<C-V>", 'txn') + call win_execute(winid, 'call feedkeys("G\<C-V>", ''txn'')') + redraw + + bwipe! + bwipe! +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_expand.vim b/src/nvim/testdir/test_expand.vim new file mode 100644 index 0000000000..48dce25bb3 --- /dev/null +++ b/src/nvim/testdir/test_expand.vim @@ -0,0 +1,83 @@ +" Test for expanding file names + +func Test_with_directories() + call mkdir('Xdir1') + call mkdir('Xdir2') + call mkdir('Xdir3') + cd Xdir3 + call mkdir('Xdir4') + cd .. + + split Xdir1/file + call setline(1, ['a', 'b']) + w + w Xdir3/Xdir4/file + close + + next Xdir?/*/file + call assert_equal('Xdir3/Xdir4/file', expand('%')) + if has('unix') + next! Xdir?/*/nofile + call assert_equal('Xdir?/*/nofile', expand('%')) + endif + " Edit another file, on MS-Windows the swap file would be in use and can't + " be deleted. + edit foo + + call assert_equal(0, delete('Xdir1', 'rf')) + call assert_equal(0, delete('Xdir2', 'rf')) + call assert_equal(0, delete('Xdir3', 'rf')) +endfunc + +func Test_with_tilde() + let dir = getcwd() + call mkdir('Xdir ~ dir') + call assert_true(isdirectory('Xdir ~ dir')) + cd Xdir\ ~\ dir + call assert_true(getcwd() =~ 'Xdir \~ dir') + call chdir(dir) + call delete('Xdir ~ dir', 'd') + call assert_false(isdirectory('Xdir ~ dir')) +endfunc + +func Test_expand_tilde_filename() + split ~ + call assert_equal('~', expand('%')) + call assert_notequal(expand('%:p'), expand('~/')) + call assert_match('\~', expand('%:p')) + bwipe! +endfunc + +func Test_expandcmd() + let $FOO = 'Test' + call assert_equal('e x/Test/y', expandcmd('e x/$FOO/y')) + unlet $FOO + + new + edit Xfile1 + call assert_equal('e Xfile1', expandcmd('e %')) + edit Xfile2 + edit Xfile1 + call assert_equal('e Xfile2', 'e #'->expandcmd()) + edit Xfile2 + edit Xfile3 + edit Xfile4 + let bnum = bufnr('Xfile2') + call assert_equal('e Xfile2', expandcmd('e #' . bnum)) + call setline('.', 'Vim!@#') + call assert_equal('e Vim', expandcmd('e <cword>')) + call assert_equal('e Vim!@#', expandcmd('e <cWORD>')) + enew! + edit Xfile.java + call assert_equal('e Xfile.py', expandcmd('e %:r.py')) + call assert_equal('make abc.java', expandcmd('make abc.%:e')) + call assert_equal('make Xabc.java', expandcmd('make %:s?file?abc?')) + edit a1a2a3.rb + call assert_equal('make b1b2b3.rb a1a2a3 Xfile.o', expandcmd('make %:gs?a?b? %< #<.o')) + + call assert_fails('call expandcmd("make <afile>")', 'E495:') + call assert_fails('call expandcmd("make <afile>")', 'E495:') + enew + call assert_fails('call expandcmd("make %")', 'E499:') + close +endfunc diff --git a/src/nvim/testdir/test_expr.vim b/src/nvim/testdir/test_expr.vim index 1d7fd3e385..5b10e691e5 100644 --- a/src/nvim/testdir/test_expr.vim +++ b/src/nvim/testdir/test_expr.vim @@ -283,6 +283,71 @@ function Test_printf_misc() call assert_equal('🐍', printf('%.2S', '🐍🐍')) call assert_equal('', printf('%.1S', '🐍🐍')) + call assert_equal('[ あいう]', printf('[%10.6S]', 'あいうえお')) + call assert_equal('[ あいうえ]', printf('[%10.8S]', 'あいうえお')) + call assert_equal('[あいうえお]', printf('[%10.10S]', 'あいうえお')) + call assert_equal('[あいうえお]', printf('[%10.12S]', 'あいうえお')) + + call assert_equal('あいう', printf('%S', 'あいう')) + call assert_equal('あいう', printf('%#S', 'あいう')) + + call assert_equal('あb', printf('%2S', 'あb')) + call assert_equal('あb', printf('%.4S', 'あb')) + call assert_equal('あ', printf('%.2S', 'あb')) + call assert_equal(' あb', printf('%4S', 'あb')) + call assert_equal('0あb', printf('%04S', 'あb')) + call assert_equal('あb ', printf('%-4S', 'あb')) + call assert_equal('あ ', printf('%-4.2S', 'あb')) + + call assert_equal('aい', printf('%2S', 'aい')) + call assert_equal('aい', printf('%.4S', 'aい')) + call assert_equal('a', printf('%.2S', 'aい')) + call assert_equal(' aい', printf('%4S', 'aい')) + call assert_equal('0aい', printf('%04S', 'aい')) + call assert_equal('aい ', printf('%-4S', 'aい')) + call assert_equal('a ', printf('%-4.2S', 'aい')) + + call assert_equal('[あいう]', printf('[%05S]', 'あいう')) + call assert_equal('[あいう]', printf('[%06S]', 'あいう')) + call assert_equal('[0あいう]', printf('[%07S]', 'あいう')) + + call assert_equal('[あiう]', printf('[%05S]', 'あiう')) + call assert_equal('[0あiう]', printf('[%06S]', 'あiう')) + call assert_equal('[00あiう]', printf('[%07S]', 'あiう')) + + call assert_equal('[0あい]', printf('[%05.4S]', 'あいう')) + call assert_equal('[00あい]', printf('[%06.4S]', 'あいう')) + call assert_equal('[000あい]', printf('[%07.4S]', 'あいう')) + + call assert_equal('[00あi]', printf('[%05.4S]', 'あiう')) + call assert_equal('[000あi]', printf('[%06.4S]', 'あiう')) + call assert_equal('[0000あi]', printf('[%07.4S]', 'あiう')) + + call assert_equal('[0あい]', printf('[%05.5S]', 'あいう')) + call assert_equal('[00あい]', printf('[%06.5S]', 'あいう')) + call assert_equal('[000あい]', printf('[%07.5S]', 'あいう')) + + call assert_equal('[あiう]', printf('[%05.5S]', 'あiう')) + call assert_equal('[0あiう]', printf('[%06.5S]', 'あiう')) + call assert_equal('[00あiう]', printf('[%07.5S]', 'あiう')) + + call assert_equal('[0000000000]', printf('[%010.0S]', 'あいう')) + call assert_equal('[0000000000]', printf('[%010.1S]', 'あいう')) + call assert_equal('[00000000あ]', printf('[%010.2S]', 'あいう')) + call assert_equal('[00000000あ]', printf('[%010.3S]', 'あいう')) + call assert_equal('[000000あい]', printf('[%010.4S]', 'あいう')) + call assert_equal('[000000あい]', printf('[%010.5S]', 'あいう')) + call assert_equal('[0000あいう]', printf('[%010.6S]', 'あいう')) + call assert_equal('[0000あいう]', printf('[%010.7S]', 'あいう')) + + call assert_equal('[0000000000]', printf('[%010.1S]', 'あiう')) + call assert_equal('[00000000あ]', printf('[%010.2S]', 'あiう')) + call assert_equal('[0000000あi]', printf('[%010.3S]', 'あiう')) + call assert_equal('[0000000あi]', printf('[%010.4S]', 'あiう')) + call assert_equal('[00000あiう]', printf('[%010.5S]', 'あiう')) + call assert_equal('[00000あiう]', printf('[%010.6S]', 'あiう')) + call assert_equal('[00000あiう]', printf('[%010.7S]', 'あiう')) + call assert_equal('1%', printf('%d%%', 1)) endfunc diff --git a/src/nvim/testdir/test_feedkeys.vim b/src/nvim/testdir/test_feedkeys.vim index 70500f2bb5..f343b0174c 100644 --- a/src/nvim/testdir/test_feedkeys.vim +++ b/src/nvim/testdir/test_feedkeys.vim @@ -12,3 +12,15 @@ func Test_feedkeys_x_with_empty_string() call assert_equal('foo', getline('.')) quit! endfunc + +func Test_feedkeys_with_abbreviation() + new + inoreabbrev trigger value + call feedkeys("atrigger ", 'x') + call feedkeys("atrigger ", 'x') + call assert_equal('value value ', getline(1)) + bwipe! + iunabbrev trigger +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_filechanged.vim b/src/nvim/testdir/test_filechanged.vim index b95cd5faf8..06ccd6e85f 100644 --- a/src/nvim/testdir/test_filechanged.vim +++ b/src/nvim/testdir/test_filechanged.vim @@ -1,9 +1,10 @@ " Tests for when a file was changed outside of Vim. +source check.vim + func Test_FileChangedShell_reload() - if !has('unix') - return - endif + CheckUnix + augroup testreload au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'reload' augroup END @@ -90,11 +91,107 @@ func Test_FileChangedShell_reload() call delete('Xchanged_r') endfunc +func Test_FileChangedShell_edit() + CheckUnix + + new Xchanged_r + call setline(1, 'reload this') + set fileformat=unix + write + + " File format changed, reload (content only, no 'ff' etc) + augroup testreload + au! + au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'reload' + augroup END + call assert_equal(&fileformat, 'unix') + sleep 10m " make the test less flaky in Nvim + call writefile(["line1\r", "line2\r"], 'Xchanged_r') + let g:reason = '' + checktime + call assert_equal('changed', g:reason) + call assert_equal(&fileformat, 'unix') + call assert_equal("line1\r", getline(1)) + call assert_equal("line2\r", getline(2)) + %s/\r + write + + " File format changed, reload with 'ff', etc + augroup testreload + au! + au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'edit' + augroup END + call assert_equal(&fileformat, 'unix') + sleep 10m " make the test less flaky in Nvim + call writefile(["line1\r", "line2\r"], 'Xchanged_r') + let g:reason = '' + checktime + call assert_equal('changed', g:reason) + call assert_equal(&fileformat, 'dos') + call assert_equal('line1', getline(1)) + call assert_equal('line2', getline(2)) + set fileformat=unix + write + + au! testreload + bwipe! + call delete(undofile('Xchanged_r')) + call delete('Xchanged_r') +endfunc + +func Test_FileChangedShell_edit_dialog() + throw 'Skipped: requires a UI to be active' + CheckNotGui + + new Xchanged_r + call setline(1, 'reload this') + set fileformat=unix + write + + " File format changed, reload (content only) via prompt + augroup testreload + au! + au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'ask' + augroup END + call assert_equal(&fileformat, 'unix') + call writefile(["line1\r", "line2\r"], 'Xchanged_r') + let g:reason = '' + call feedkeys('L', 'L') " load file content only + checktime + call assert_equal('changed', g:reason) + call assert_equal(&fileformat, 'unix') + call assert_equal("line1\r", getline(1)) + call assert_equal("line2\r", getline(2)) + %s/\r + write + + " File format changed, reload (file and options) via prompt + augroup testreload + au! + au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'ask' + augroup END + call assert_equal(&fileformat, 'unix') + call writefile(["line1\r", "line2\r"], 'Xchanged_r') + let g:reason = '' + call feedkeys('a', 'L') " load file content and options + checktime + call assert_equal('changed', g:reason) + call assert_equal(&fileformat, 'dos') + call assert_equal("line1", getline(1)) + call assert_equal("line2", getline(2)) + set fileformat=unix + write + + au! testreload + bwipe! + call delete(undofile('Xchanged_r')) + call delete('Xchanged_r') +endfunc + func Test_file_changed_dialog() - throw 'skipped: TODO: ' - if !has('unix') || has('gui_running') - return - endif + throw 'Skipped: requires a UI to be active' + CheckUnix + CheckNotGui au! FileChangedShell new Xchanged_d diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index 1ee23bb646..5f4a7dac6e 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -76,10 +76,12 @@ let s:filename_checks = { \ 'ave': ['file.ave'], \ 'awk': ['file.awk', 'file.gawk'], \ 'b': ['file.mch', 'file.ref', 'file.imp'], + \ 'basic': ['file.bas', 'file.bi', 'file.bm'], \ 'bzl': ['file.bazel', 'file.bzl', 'WORKSPACE'], \ 'bc': ['file.bc'], \ 'bdf': ['file.bdf'], \ 'bib': ['file.bib'], + \ 'bicep': ['file.bicep'], \ 'beancount': ['file.beancount'], \ 'bindzone': ['named.root', '/bind/db.file', '/named/db.file', 'any/bind/db.file', 'any/named/db.file'], \ 'blank': ['file.bl'], @@ -130,6 +132,7 @@ let s:filename_checks = { \ 'cvs': ['cvs123'], \ 'cvsrc': ['.cvsrc'], \ 'cynpp': ['file.cyn'], + \ 'd': ['file.d'], \ 'dart': ['file.dart', 'file.drt'], \ 'datascript': ['file.ds'], \ 'dcd': ['file.dcd'], @@ -146,12 +149,13 @@ let s:filename_checks = { \ 'diff': ['file.diff', 'file.rej'], \ 'dircolors': ['.dir_colors', '.dircolors', '/etc/DIR_COLORS', 'any/etc/DIR_COLORS'], \ 'dnsmasq': ['/etc/dnsmasq.conf', '/etc/dnsmasq.d/file', 'any/etc/dnsmasq.conf', 'any/etc/dnsmasq.d/file'], - \ 'dockerfile': ['Containerfile', 'Dockerfile', 'file.Dockerfile'], + \ 'dockerfile': ['Containerfile', 'Dockerfile', 'file.Dockerfile', 'Dockerfile.debian', 'Containerfile.something'], \ 'dosbatch': ['file.bat', 'file.sys'], \ 'dosini': ['.editorconfig', '/etc/pacman.conf', '/etc/yum.conf', 'file.ini', 'npmrc', '.npmrc', 'php.ini', 'php.ini-5', 'php.ini-file', '/etc/yum.repos.d/file', 'any/etc/pacman.conf', 'any/etc/yum.conf', 'any/etc/yum.repos.d/file', 'file.wrap'], \ 'dot': ['file.dot', 'file.gv'], \ 'dracula': ['file.drac', 'file.drc', 'filelvs', 'filelpe', 'drac.file', 'lpe', 'lvs', 'some-lpe', 'some-lvs'], \ 'dtd': ['file.dtd'], + \ 'dtrace': ['/usr/lib/dtrace/io.d'], \ 'dts': ['file.dts', 'file.dtsi'], \ 'dune': ['jbuild', 'dune', 'dune-project', 'dune-workspace'], \ 'dylan': ['file.dylan'], @@ -182,47 +186,59 @@ let s:filename_checks = { \ 'fgl': ['file.4gl', 'file.4gh', 'file.m4gl'], \ 'fish': ['file.fish'], \ 'focexec': ['file.fex', 'file.focexec'], + \ 'form': ['file.frm'], \ 'forth': ['file.ft', 'file.fth'], \ 'fortran': ['file.f', 'file.for', 'file.fortran', 'file.fpp', 'file.ftn', 'file.f77', 'file.f90', 'file.f95', 'file.f03', 'file.f08'], \ 'fpcmake': ['file.fpc'], \ 'framescript': ['file.fsl'], - \ 'freebasic': ['file.fb', 'file.bi'], + \ 'freebasic': ['file.fb'], \ 'fsharp': ['file.fs', 'file.fsi', 'file.fsx'], \ 'fstab': ['fstab', 'mtab'], + \ 'fusion': ['file.fusion'], \ 'fvwm': ['/.fvwm/file', 'any/.fvwm/file'], \ 'gdb': ['.gdbinit', 'gdbinit'], + \ 'gdresource': ['file.tscn', 'file.tres'], + \ 'gdscript': ['file.gd'], \ 'gdmo': ['file.mo', 'file.gdmo'], \ 'gedcom': ['file.ged', 'lltxxxxx.txt', '/tmp/lltmp', '/tmp/lltmp-file', 'any/tmp/lltmp', 'any/tmp/lltmp-file'], \ 'gemtext': ['file.gmi', 'file.gemini'], \ 'gift': ['file.gift'], - \ 'gitcommit': ['COMMIT_EDITMSG', 'MERGE_MSG', 'TAG_EDITMSG'], - \ 'gitconfig': ['file.git/config', '.gitconfig', '.gitmodules', 'file.git/modules//config', '/.config/git/config', '/etc/gitconfig', '/etc/gitconfig.d/file', '/.gitconfig.d/file', 'any/.config/git/config', 'any/.gitconfig.d/file', 'some.git/config', 'some.git/modules/any/config'], + \ 'gitcommit': ['COMMIT_EDITMSG', 'MERGE_MSG', 'TAG_EDITMSG', 'NOTES_EDITMSG', 'EDIT_DESCRIPTION'], + \ 'gitconfig': ['file.git/config', 'file.git/config.worktree', 'file.git/worktrees/x/config.worktree', '.gitconfig', '.gitmodules', 'file.git/modules//config', '/.config/git/config', '/etc/gitconfig', '/usr/local/etc/gitconfig', '/etc/gitconfig.d/file', '/.gitconfig.d/file', 'any/.config/git/config', 'any/.gitconfig.d/file', 'some.git/config', 'some.git/modules/any/config'], \ 'gitolite': ['gitolite.conf', '/gitolite-admin/conf/file', 'any/gitolite-admin/conf/file'], \ 'gitrebase': ['git-rebase-todo'], \ 'gitsendemail': ['.gitsendemail.msg.xxxxxx'], \ 'gkrellmrc': ['gkrellmrc', 'gkrellmrc_x'], + \ 'glsl': ['file.glsl'], \ 'gnash': ['gnashrc', '.gnashrc', 'gnashpluginrc', '.gnashpluginrc'], - \ 'gnuplot': ['file.gpi'], + \ 'gnuplot': ['file.gpi', '.gnuplot'], \ 'go': ['file.go'], \ 'gomod': ['go.mod'], + \ 'gowork': ['go.work'], \ 'gp': ['file.gp', '.gprc'], \ 'gpg': ['/.gnupg/options', '/.gnupg/gpg.conf', '/usr/any/gnupg/options.skel', 'any/.gnupg/gpg.conf', 'any/.gnupg/options', 'any/usr/any/gnupg/options.skel'], \ 'grads': ['file.gs'], + \ 'graphql': ['file.graphql', 'file.graphqls', 'file.gql'], \ 'gretl': ['file.gretl'], \ 'groovy': ['file.gradle', 'file.groovy'], \ 'group': ['any/etc/group', 'any/etc/group-', 'any/etc/group.edit', 'any/etc/gshadow', 'any/etc/gshadow-', 'any/etc/gshadow.edit', 'any/var/backups/group.bak', 'any/var/backups/gshadow.bak', '/etc/group', '/etc/group-', '/etc/group.edit', '/etc/gshadow', '/etc/gshadow-', '/etc/gshadow.edit', '/var/backups/group.bak', '/var/backups/gshadow.bak'], \ 'grub': ['/boot/grub/menu.lst', '/boot/grub/grub.conf', '/etc/grub.conf', 'any/boot/grub/grub.conf', 'any/boot/grub/menu.lst', 'any/etc/grub.conf'], \ 'gsp': ['file.gsp'], \ 'gtkrc': ['.gtkrc', 'gtkrc', '.gtkrc-file', 'gtkrc-file'], + \ 'hack': ['file.hack', 'file.hackpartial'], \ 'haml': ['file.haml'], \ 'hamster': ['file.hsm'], + \ 'handlebars': ['file.hbs'], \ 'haskell': ['file.hs', 'file.hsc', 'file.hs-boot', 'file.hsig'], \ 'haste': ['file.ht'], \ 'hastepreproc': ['file.htpp'], \ 'hb': ['file.hb'], + \ 'hcl': ['file.hcl'], \ 'hercules': ['file.vc', 'file.ev', 'file.sum', 'file.errsum'], + \ 'heex': ['file.heex'], \ 'hex': ['file.hex', 'file.h32'], \ 'hgcommit': ['hg-editor-file.txt'], + \ 'hjson': ['file.hjson'], \ 'hog': ['file.hog', 'snort.conf', 'vision.conf'], \ 'hollywood': ['file.hws'], \ 'hostconf': ['/etc/host.conf', 'any/etc/host.conf'], @@ -258,12 +274,14 @@ let s:filename_checks = { \ 'java': ['file.java', 'file.jav'], \ 'javacc': ['file.jj', 'file.jjt'], \ 'javascript': ['file.js', 'file.javascript', 'file.es', 'file.mjs', 'file.cjs'], + \ 'javascript.glimmer': ['file.gjs'], \ 'javascriptreact': ['file.jsx'], \ 'jess': ['file.clp'], \ 'jgraph': ['file.jgr'], \ 'jovial': ['file.jov', 'file.j73', 'file.jovial'], \ 'jproperties': ['file.properties', 'file.properties_xx', 'file.properties_xx_xx', 'some.properties_xx_xx_file'], \ 'json': ['file.json', 'file.jsonp', 'file.json-patch', 'file.webmanifest', 'Pipfile.lock', 'file.ipynb', '.babelrc', '.eslintrc', '.prettierrc', '.firebaserc', 'file.slnf'], + \ 'json5': ['file.json5'], \ 'jsonc': ['file.jsonc'], \ 'jsp': ['file.jsp'], \ 'julia': ['file.jl'], @@ -277,6 +295,7 @@ let s:filename_checks = { \ 'latte': ['file.latte', 'file.lte'], \ 'ld': ['file.ld'], \ 'ldif': ['file.ldif'], + \ 'ledger': ['file.ldg', 'file.ledger', 'file.journal'], \ 'less': ['file.less'], \ 'lex': ['file.lex', 'file.l', 'file.lxx', 'file.l++'], \ 'lftp': ['lftp.conf', '.lftprc', 'anylftp/rc', 'lftp/rc', 'some-lftp/rc'], @@ -286,7 +305,7 @@ let s:filename_checks = { \ 'lilo': ['lilo.conf', 'lilo.conf-file'], \ 'limits': ['/etc/limits', '/etc/anylimits.conf', '/etc/anylimits.d/file.conf', '/etc/limits.conf', '/etc/limits.d/file.conf', '/etc/some-limits.conf', '/etc/some-limits.d/file.conf', 'any/etc/limits', 'any/etc/limits.conf', 'any/etc/limits.d/file.conf', 'any/etc/some-limits.conf', 'any/etc/some-limits.d/file.conf'], \ 'liquid': ['file.liquid'], - \ 'lisp': ['file.lsp', 'file.lisp', 'file.el', 'file.cl', '.emacs', '.sawfishrc', 'sbclrc', '.sbclrc'], + \ 'lisp': ['file.lsp', 'file.lisp', 'file.asd', 'file.el', 'file.cl', '.emacs', '.sawfishrc', 'sbclrc', '.sbclrc'], \ 'lite': ['file.lite', 'file.lt'], \ 'litestep': ['/LiteStep/any/file.rc', 'any/LiteStep/any/file.rc'], \ 'loginaccess': ['/etc/login.access', 'any/etc/login.access'], @@ -350,6 +369,7 @@ let s:filename_checks = { \ 'netrc': ['.netrc'], \ 'nginx': ['file.nginx', 'nginxfile.conf', 'filenginx.conf', 'any/etc/nginx/file', 'any/usr/local/nginx/conf/file', 'any/nginx/file.conf'], \ 'ninja': ['file.ninja'], + \ 'nix': ['file.nix'], \ 'nqc': ['file.nqc'], \ 'nroff': ['file.tr', 'file.nr', 'file.roff', 'file.tmac', 'file.mom', 'tmac.file'], \ 'nsis': ['file.nsi', 'file.nsh'], @@ -392,6 +412,7 @@ let s:filename_checks = { \ 'ppd': ['file.ppd'], \ 'ppwiz': ['file.it', 'file.ih'], \ 'privoxy': ['file.action'], + \ 'prisma': ['file.prisma'], \ 'proc': ['file.pc'], \ 'procmail': ['.procmail', '.procmailrc'], \ 'prolog': ['file.pdb'], @@ -402,10 +423,12 @@ let s:filename_checks = { \ 'ps1xml': ['file.ps1xml'], \ 'psf': ['file.psf'], \ 'psl': ['file.psl'], + \ 'pug': ['file.pug'], \ 'puppet': ['file.pp'], \ 'pyret': ['file.arr'], \ 'pyrex': ['file.pyx', 'file.pxd'], \ 'python': ['file.py', 'file.pyw', '.pythonstartup', '.pythonrc', 'file.ptl', 'file.pyi', 'SConstruct'], + \ 'ql': ['file.ql', 'file.qll'], \ 'quake': ['anybaseq2/file.cfg', 'anyid1/file.cfg', 'quake3/file.cfg', 'baseq2/file.cfg', 'id1/file.cfg', 'quake1/file.cfg', 'some-baseq2/file.cfg', 'some-id1/file.cfg', 'some-quake1/file.cfg'], \ 'radiance': ['file.rad', 'file.mat'], \ 'raku': ['file.pm6', 'file.p6', 'file.t6', 'file.pod6', 'file.raku', 'file.rakumod', 'file.rakudoc', 'file.rakutest'], @@ -416,6 +439,7 @@ let s:filename_checks = { \ 'readline': ['.inputrc', 'inputrc'], \ 'remind': ['.reminders', 'file.remind', 'file.rem', '.reminders-file'], \ 'rego': ['file.rego'], + \ 'rescript': ['file.res', 'file.resi'], \ 'resolv': ['resolv.conf'], \ 'reva': ['file.frt'], \ 'rexx': ['file.rex', 'file.orx', 'file.rxo', 'file.rxj', 'file.jrexx', 'file.rexxj', 'file.rexx', 'file.testGroup', 'file.testUnit'], @@ -436,7 +460,7 @@ let s:filename_checks = { \ 'sather': ['file.sa'], \ 'sbt': ['file.sbt'], \ 'scala': ['file.scala', 'file.sc'], - \ 'scheme': ['file.scm', 'file.ss', 'file.rkt', 'file.rktd', 'file.rktl'], + \ 'scheme': ['file.scm', 'file.ss', 'file.sld', 'file.rkt', 'file.rktd', 'file.rktl'], \ 'scilab': ['file.sci', 'file.sce'], \ 'screen': ['.screenrc', 'screenrc'], \ 'sexplib': ['file.sexp'], @@ -446,7 +470,7 @@ let s:filename_checks = { \ 'sdc': ['file.sdc'], \ 'sdl': ['file.sdl', 'file.pr'], \ 'sed': ['file.sed'], - \ 'sensors': ['/etc/sensors.conf', '/etc/sensors3.conf', 'any/etc/sensors.conf', 'any/etc/sensors3.conf'], + \ 'sensors': ['/etc/sensors.conf', '/etc/sensors3.conf', '/etc/sensors.d/file', 'any/etc/sensors.conf', 'any/etc/sensors3.conf', 'any/etc/sensors.d/file'], \ 'services': ['/etc/services', 'any/etc/services'], \ 'setserial': ['/etc/serial.conf', 'any/etc/serial.conf'], \ 'sh': ['.bashrc', 'file.bash', '/usr/share/doc/bash-completion/filter.sh','/etc/udev/cdsymlinks.conf', 'any/etc/udev/cdsymlinks.conf'], @@ -457,6 +481,7 @@ let s:filename_checks = { \ 'skill': ['file.il', 'file.ils', 'file.cdf'], \ 'slang': ['file.sl'], \ 'slice': ['file.ice'], + \ 'solidity': ['file.sol'], \ 'solution': ['file.sln'], \ 'slpconf': ['/etc/slp.conf', 'any/etc/slp.conf'], \ 'slpreg': ['/etc/slp.reg', 'any/etc/slp.reg'], @@ -481,12 +506,13 @@ let s:filename_checks = { \ 'squid': ['squid.conf'], \ 'squirrel': ['file.nut'], \ 'srec': ['file.s19', 'file.s28', 'file.s37', 'file.mot', 'file.srec'], - \ 'sshconfig': ['ssh_config', '/.ssh/config', '/etc/ssh/ssh_config.d/file.conf', 'any/etc/ssh/ssh_config.d/file.conf', 'any/.ssh/config'], + \ 'sshconfig': ['ssh_config', '/.ssh/config', '/etc/ssh/ssh_config.d/file.conf', 'any/etc/ssh/ssh_config.d/file.conf', 'any/.ssh/config', 'any/.ssh/file.conf'], \ 'sshdconfig': ['sshd_config', '/etc/ssh/sshd_config.d/file.conf', 'any/etc/ssh/sshd_config.d/file.conf'], \ 'st': ['file.st'], \ 'stata': ['file.ado', 'file.do', 'file.imata', 'file.mata'], \ 'stp': ['file.stp'], \ 'sudoers': ['any/etc/sudoers', 'sudoers.tmp', '/etc/sudoers', 'any/etc/sudoers.d/file'], + \ 'surface': ['file.sface'], \ 'svg': ['file.svg'], \ 'svn': ['svn-commitfile.tmp', 'svn-commit-file.tmp', 'svn-commit.tmp'], \ 'swift': ['file.swift'], @@ -500,8 +526,10 @@ let s:filename_checks = { \ 'taskdata': ['pending.data', 'completed.data', 'undo.data'], \ 'taskedit': ['file.task'], \ 'tcl': ['file.tcl', 'file.tm', 'file.tk', 'file.itcl', 'file.itk', 'file.jacl', '.tclshrc', 'tclsh.rc', '.wishrc'], + \ 'teal': ['file.tl'], \ 'teraterm': ['file.ttl'], \ 'terminfo': ['file.ti'], + \ 'terraform': ['file.tfvars'], \ 'tex': ['file.latex', 'file.sty', 'file.dtx', 'file.ltx', 'file.bbl'], \ 'texinfo': ['file.texinfo', 'file.texi', 'file.txi'], \ 'texmf': ['texmf.cnf'], @@ -509,6 +537,7 @@ let s:filename_checks = { \ 'tf': ['file.tf', '.tfrc', 'tfrc'], \ 'tidy': ['.tidyrc', 'tidyrc', 'tidy.conf'], \ 'tilde': ['file.t.html'], + \ 'tla': ['file.tla'], \ 'tli': ['file.tli'], \ 'tmux': ['tmuxfile.conf', '.tmuxfile.conf', '.tmux-file.conf', '.tmux.conf', 'tmux-file.conf', 'tmux.conf', 'tmux.conf.local'], \ 'toml': ['file.toml', 'Gopkg.lock', 'Pipfile', '/home/user/.cargo/config'], @@ -520,6 +549,7 @@ let s:filename_checks = { \ 'tssgm': ['file.tssgm'], \ 'tssop': ['file.tssop'], \ 'twig': ['file.twig'], + \ 'typescript.glimmer': ['file.gts'], \ 'typescriptreact': ['file.tsx'], \ 'uc': ['file.uc'], \ 'udevconf': ['/etc/udev/udev.conf', 'any/etc/udev/udev.conf'], @@ -534,6 +564,7 @@ let s:filename_checks = { \ 'usserverlog': ['usserver.log', 'USSERVER.LOG', 'usserver.file.log', 'USSERVER.FILE.LOG', 'file.usserver.log', 'FILE.USSERVER.LOG'], \ 'usw2kagtlog': ['usw2kagt.log', 'USW2KAGT.LOG', 'usw2kagt.file.log', 'USW2KAGT.FILE.LOG', 'file.usw2kagt.log', 'FILE.USW2KAGT.LOG'], \ 'vb': ['file.sba', 'file.vb', 'file.vbs', 'file.dsm', 'file.ctl'], + \ 'vala': ['file.vala'], \ 'vera': ['file.vr', 'file.vri', 'file.vrh'], \ 'verilog': ['file.v'], \ 'verilogams': ['file.va', 'file.vams'], @@ -569,6 +600,7 @@ let s:filename_checks = { \ 'xslt': ['file.xsl', 'file.xslt'], \ 'yacc': ['file.yy', 'file.yxx', 'file.y++'], \ 'yaml': ['file.yaml', 'file.yml'], + \ 'yang': ['file.yang'], \ 'raml': ['file.raml'], \ 'z8a': ['file.z8a'], \ 'zig': ['file.zig'], @@ -653,7 +685,7 @@ let s:script_checks = { \ ['#!/path/nodejs'], \ ['#!/path/rhino']], \ 'bc': [['#!/path/bc']], - \ 'sed': [['#!/path/sed']], + \ 'sed': [['#!/path/sed'], ['#n'], ['#n comment']], \ 'ocaml': [['#!/path/ocaml']], \ 'awk': [['#!/path/awk'], \ ['#!/path/gawk']], @@ -706,83 +738,162 @@ func Test_setfiletype_completion() call assert_equal('"setfiletype java javacc javascript javascriptreact', @:) endfunc -func Test_hook_file() +""""""""""""""""""""""""""""""""""""""""""""""""" +" Tests for specific extentions and filetypes. +" Keep sorted. +""""""""""""""""""""""""""""""""""""""""""""""""" + +func Test_bas_file() filetype on - call writefile(['[Trigger]', 'this is pacman config'], 'Xfile.hook') - split Xfile.hook - call assert_equal('dosini', &filetype) + call writefile(['looks like BASIC'], 'Xfile.bas') + split Xfile.bas + call assert_equal('basic', &filetype) bwipe! - call writefile(['not pacman'], 'Xfile.hook') - split Xfile.hook - call assert_notequal('dosini', &filetype) + " Test dist#ft#FTbas() + + let g:filetype_bas = 'freebasic' + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! + unlet g:filetype_bas - call delete('Xfile.hook') - filetype off -endfunc + " FreeBASIC -func Test_ts_file() - filetype on + call writefile(["/' FreeBASIC multiline comment '/"], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) + bwipe! - call writefile(['<?xml version="1.0" encoding="utf-8"?>'], 'Xfile.ts') - split Xfile.ts - call assert_equal('xml', &filetype) + call writefile(['#define TESTING'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! - call writefile(['// looks like Typescript'], 'Xfile.ts') - split Xfile.ts - call assert_equal('typescript', &filetype) + call writefile(['option byval'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! - call delete('Xfile.hook') + call writefile(['extern "C"'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) + bwipe! + + " QB64 + + call writefile(['$LET TESTING = 1'], 'Xfile.bas') + split Xfile.bas + call assert_equal('qb64', &filetype) + bwipe! + + call writefile(['OPTION _EXPLICIT'], 'Xfile.bas') + split Xfile.bas + call assert_equal('qb64', &filetype) + bwipe! + + " Visual Basic + + call writefile(['Attribute VB_NAME = "Testing"'], 'Xfile.bas') + split Xfile.bas + call assert_equal('vb', &filetype) + bwipe! + + call delete('Xfile.bas') filetype off endfunc -func Test_ttl_file() +func Test_d_file() filetype on - call writefile(['@base <http://example.org/> .'], 'Xfile.ttl') - split Xfile.ttl - call assert_equal('turtle', &filetype) + call writefile(['looks like D'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) bwipe! - call writefile(['looks like Tera Term Language'], 'Xfile.ttl') - split Xfile.ttl - call assert_equal('teraterm', &filetype) + call writefile(['#!/some/bin/dtrace'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile(['#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile([':some:thing:'], 'Xfile.d') + split Xfile.d + call assert_equal('dtrace', &filetype) + bwipe! + + call writefile(['module this', '#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) + bwipe! + + call writefile(['import that', '#pragma D option'], 'Xfile.d') + split Xfile.d + call assert_equal('d', &filetype) bwipe! - call delete('Xfile.ttl') filetype off endfunc -func Test_pp_file() +func Test_dep3patch_file() filetype on - call writefile(['looks like puppet'], 'Xfile.pp') - split Xfile.pp - call assert_equal('puppet', &filetype) + call assert_true(mkdir('debian/patches', 'p')) + + " series files are not patches + call writefile(['Description: some awesome patch'], 'debian/patches/series') + split debian/patches/series + call assert_notequal('dep3patch', &filetype) bwipe! - let g:filetype_pp = 'pascal' - split Xfile.pp - call assert_equal('pascal', &filetype) + " diff/patch files without the right headers should still show up as ft=diff + call writefile([], 'debian/patches/foo.diff') + split debian/patches/foo.diff + call assert_equal('diff', &filetype) bwipe! - unlet g:filetype_pp - " Test dist#ft#FTpp() - call writefile(['{ pascal comment'], 'Xfile.pp') - split Xfile.pp - call assert_equal('pascal', &filetype) + " Files with the right headers are detected as dep3patch, even if they don't + " have a diff/patch extension + call writefile(['Subject: dep3patches'], 'debian/patches/bar') + split debian/patches/bar + call assert_equal('dep3patch', &filetype) bwipe! - call writefile(['procedure pascal'], 'Xfile.pp') - split Xfile.pp - call assert_equal('pascal', &filetype) + " Files in sub-directories are detected + call assert_true(mkdir('debian/patches/s390x', 'p')) + call writefile(['Subject: dep3patches'], 'debian/patches/s390x/bar') + split debian/patches/s390x/bar + call assert_equal('dep3patch', &filetype) bwipe! - call delete('Xfile.pp') + " The detection stops when seeing the "header end" marker + call writefile(['---', 'Origin: the cloud'], 'debian/patches/baz') + split debian/patches/baz + call assert_notequal('dep3patch', &filetype) + bwipe! + + call delete('debian', 'rf') +endfunc + +func Test_dsl_file() + filetype on + + call writefile([' <!doctype dsssl-spec ['], 'dslfile.dsl') + split dslfile.dsl + call assert_equal('dsl', &filetype) + bwipe! + + call writefile(['workspace {'], 'dslfile.dsl') + split dslfile.dsl + call assert_equal('structurizr', &filetype) + bwipe! + + call delete('dslfile.dsl') filetype off endfunc @@ -823,20 +934,183 @@ func Test_ex_file() filetype off endfunc -func Test_dsl_file() +func Test_foam_file() filetype on + call assert_true(mkdir('0', 'p')) + call assert_true(mkdir('0.orig', 'p')) - call writefile([' <!doctype dsssl-spec ['], 'dslfile.dsl') - split dslfile.dsl - call assert_equal('dsl', &filetype) + call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict') + split Xfile1Dict + call assert_equal('foam', &filetype) bwipe! - call writefile(['workspace {'], 'dslfile.dsl') - split dslfile.dsl - call assert_equal('structurizr', &filetype) + call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict.something') + split Xfile1Dict.something + call assert_equal('foam', &filetype) bwipe! - call delete('dslfile.dsl') + call writefile(['FoamFile {', ' object something;'], 'XfileProperties') + split XfileProperties + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') + split XfileProperties.something + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties') + split XfileProperties + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') + split XfileProperties.something + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], '0/Xfile') + split 0/Xfile + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], '0.orig/Xfile') + split 0.orig/Xfile + call assert_equal('foam', &filetype) + bwipe! + + call delete('0', 'rf') + call delete('0.orig', 'rf') + call delete('Xfile1Dict') + call delete('Xfile1Dict.something') + call delete('XfileProperties') + call delete('XfileProperties.something') + filetype off +endfunc + +func Test_frm_file() + filetype on + + call writefile(['looks like FORM'], 'Xfile.frm') + split Xfile.frm + call assert_equal('form', &filetype) + bwipe! + + " Test dist#ft#FTfrm() + + let g:filetype_frm = 'form' + split Xfile.frm + call assert_equal('form', &filetype) + bwipe! + unlet g:filetype_frm + + " Visual Basic + + call writefile(['Begin VB.Form Form1'], 'Xfile.frm') + split Xfile.frm + call assert_equal('vb', &filetype) + bwipe! + + call delete('Xfile.frm') + filetype off +endfunc + +func Test_fs_file() + filetype on + + call writefile(['looks like F#'], 'Xfile.fs') + split Xfile.fs + call assert_equal('fsharp', &filetype) + bwipe! + + let g:filetype_fs = 'forth' + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + unlet g:filetype_fs + + " Test dist#ft#FTfs() + + " Forth (Gforth) + + call writefile(['( Forth inline comment )'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['.( Forth displayed inline comment )'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['\ Forth line comment'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + " empty line comment - no space required + call writefile(['\'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['\G Forth documentation comment '], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile([': squared ( n -- n^2 )', 'dup * ;'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call delete('Xfile.fs') + filetype off +endfunc + +func Test_git_file() + filetype on + + call assert_true(mkdir('Xrepo.git', 'p')) + + call writefile([], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('', &filetype) + bwipe! + + call writefile(['0000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call writefile(['0000000000000000000000000000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call writefile(['ref: refs/heads/master'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call delete('Xrepo.git', 'rf') + filetype off +endfunc + +func Test_hook_file() + filetype on + + call writefile(['[Trigger]', 'this is pacman config'], 'Xfile.hook') + split Xfile.hook + call assert_equal('dosini', &filetype) + bwipe! + + call writefile(['not pacman'], 'Xfile.hook') + split Xfile.hook + call assert_notequal('dosini', &filetype) + bwipe! + + call delete('Xfile.hook') filetype off endfunc @@ -940,109 +1214,154 @@ func Test_m_file() filetype off endfunc -func Test_xpm_file() +func Test_patch_file() filetype on - call writefile(['this is XPM2'], 'file.xpm') - split file.xpm - call assert_equal('xpm2', &filetype) + call writefile([], 'Xfile.patch') + split Xfile.patch + call assert_equal('diff', &filetype) bwipe! - call delete('file.xpm') + call writefile(['From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001'], 'Xfile.patch') + split Xfile.patch + call assert_equal('gitsendemail', &filetype) + bwipe! + + call writefile(['From 0000000000000000000000000000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001'], 'Xfile.patch') + split Xfile.patch + call assert_equal('gitsendemail', &filetype) + bwipe! + + call delete('Xfile.patch') filetype off endfunc -func Test_fs_file() +func Test_perl_file() filetype on - call writefile(['looks like F#'], 'Xfile.fs') - split Xfile.fs - call assert_equal('fsharp', &filetype) - bwipe! + " only tests one case, should do more + let lines =<< trim END - let g:filetype_fs = 'forth' - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - unlet g:filetype_fs + use a + END + call writefile(lines, "Xfile.t") + split Xfile.t + call assert_equal('perl', &filetype) + bwipe - " Test dist#ft#FTfs() + call delete('Xfile.t') + filetype off +endfunc - " Forth (Gforth) +func Test_pp_file() + filetype on - call writefile(['( Forth inline comment )'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + call writefile(['looks like puppet'], 'Xfile.pp') + split Xfile.pp + call assert_equal('puppet', &filetype) bwipe! - call writefile(['.( Forth displayed inline comment )'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + let g:filetype_pp = 'pascal' + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! + unlet g:filetype_pp - call writefile(['\ Forth line comment'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + " Test dist#ft#FTpp() + call writefile(['{ pascal comment'], 'Xfile.pp') + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! - " empty line comment - no space required - call writefile(['\'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + call writefile(['procedure pascal'], 'Xfile.pp') + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! - call writefile(['\G Forth documentation comment '], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + call delete('Xfile.pp') + filetype off +endfunc + +func Test_tex_file() + filetype on + + " only tests one case, should do more + let lines =<< trim END + % This is a sentence. + + This is a sentence. + END + call writefile(lines, "Xfile.tex") + split Xfile.tex + call assert_equal('plaintex', &filetype) + bwipe + + call delete('Xfile.tex') + filetype off +endfunc + +func Test_tf_file() + filetype on + + call writefile([';;; TF MUD client is super duper cool'], 'Xfile.tf') + split Xfile.tf + call assert_equal('tf', &filetype) bwipe! - call writefile([': squared ( n -- n^2 )', 'dup * ;'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) + call writefile(['provider "azurerm" {'], 'Xfile.tf') + split Xfile.tf + call assert_equal('terraform', &filetype) bwipe! - call delete('Xfile.fs') + call delete('Xfile.tf') filetype off endfunc -func Test_dep3patch_file() +func Test_ts_file() filetype on - call assert_true(mkdir('debian/patches', 'p')) - - " series files are not patches - call writefile(['Description: some awesome patch'], 'debian/patches/series') - split debian/patches/series - call assert_notequal('dep3patch', &filetype) + call writefile(['<?xml version="1.0" encoding="utf-8"?>'], 'Xfile.ts') + split Xfile.ts + call assert_equal('xml', &filetype) bwipe! - " diff/patch files without the right headers should still show up as ft=diff - call writefile([], 'debian/patches/foo.diff') - split debian/patches/foo.diff - call assert_equal('diff', &filetype) + call writefile(['// looks like Typescript'], 'Xfile.ts') + split Xfile.ts + call assert_equal('typescript', &filetype) bwipe! - " Files with the right headers are detected as dep3patch, even if they don't - " have a diff/patch extension - call writefile(['Subject: dep3patches'], 'debian/patches/bar') - split debian/patches/bar - call assert_equal('dep3patch', &filetype) + call delete('Xfile.ts') + filetype off +endfunc + +func Test_ttl_file() + filetype on + + call writefile(['@base <http://example.org/> .'], 'Xfile.ttl') + split Xfile.ttl + call assert_equal('turtle', &filetype) bwipe! - " Files in sub-directories are detected - call assert_true(mkdir('debian/patches/s390x', 'p')) - call writefile(['Subject: dep3patches'], 'debian/patches/s390x/bar') - split debian/patches/s390x/bar - call assert_equal('dep3patch', &filetype) + call writefile(['looks like Tera Term Language'], 'Xfile.ttl') + split Xfile.ttl + call assert_equal('teraterm', &filetype) bwipe! - " The detection stops when seeing the "header end" marker - call writefile(['---', 'Origin: the cloud'], 'debian/patches/baz') - split debian/patches/baz - call assert_notequal('dep3patch', &filetype) + call delete('Xfile.ttl') + filetype off +endfunc + +func Test_xpm_file() + filetype on + + call writefile(['this is XPM2'], 'file.xpm') + split file.xpm + call assert_equal('xpm2', &filetype) bwipe! - call delete('debian/patches', 'rf') + call delete('file.xpm') + filetype off endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_filetype_lua.vim b/src/nvim/testdir/test_filetype_lua.vim new file mode 100644 index 0000000000..f73e4ca33f --- /dev/null +++ b/src/nvim/testdir/test_filetype_lua.vim @@ -0,0 +1,2 @@ +let g:do_filetype_lua = 1 +source test_filetype.vim diff --git a/src/nvim/testdir/test_filter_cmd.vim b/src/nvim/testdir/test_filter_cmd.vim index 0c45db049b..d465e48c7b 100644 --- a/src/nvim/testdir/test_filter_cmd.vim +++ b/src/nvim/testdir/test_filter_cmd.vim @@ -145,3 +145,38 @@ func Test_filter_commands() bwipe! file.h bwipe! file.hs endfunc + +func Test_filter_display() + edit Xdoesnotmatch + let @a = '!!willmatch' + let @b = '!!doesnotmatch' + let @c = "oneline\ntwoline\nwillmatch\n" + let @/ = '!!doesnotmatch' + call feedkeys(":echo '!!doesnotmatch:'\<CR>", 'ntx') + let lines = map(split(execute('filter /willmatch/ display'), "\n"), 'v:val[5:6]') + + call assert_true(index(lines, '"a') >= 0) + call assert_false(index(lines, '"b') >= 0) + call assert_true(index(lines, '"c') >= 0) + call assert_false(index(lines, '"/') >= 0) + call assert_false(index(lines, '":') >= 0) + call assert_false(index(lines, '"%') >= 0) + + let lines = map(split(execute('filter /doesnotmatch/ display'), "\n"), 'v:val[5:6]') + call assert_true(index(lines, '"a') < 0) + call assert_false(index(lines, '"b') < 0) + call assert_true(index(lines, '"c') < 0) + call assert_false(index(lines, '"/') < 0) + call assert_false(index(lines, '":') < 0) + call assert_false(index(lines, '"%') < 0) + + bwipe! +endfunc + +func Test_filter_scriptnames() + let lines = split(execute('filter /test_filter_cmd/ scriptnames'), "\n") + call assert_equal(1, len(lines)) + call assert_match('filter_cmd', lines[0]) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_filter_map.vim b/src/nvim/testdir/test_filter_map.vim index a52a66ac2f..1cd3a2287b 100644 --- a/src/nvim/testdir/test_filter_map.vim +++ b/src/nvim/testdir/test_filter_map.vim @@ -88,4 +88,14 @@ func Test_map_filter_fails() call assert_fails("let l = filter('abc', '\"> \" . v:val')", 'E896:') endfunc +func Test_map_and_modify() + let l = ["abc"] + " cannot change the list halfway a map() + call assert_fails('call map(l, "remove(l, 0)[0]")', 'E741:') + + let d = #{a: 1, b: 2, c: 3} + call assert_fails('call map(d, "remove(d, v:key)[0]")', 'E741:') + call assert_fails('echo map(d, {k,v -> remove(d, k)})', 'E741:') +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_findfile.vim b/src/nvim/testdir/test_findfile.vim index 5a20475d3d..1684c5d30a 100644 --- a/src/nvim/testdir/test_findfile.vim +++ b/src/nvim/testdir/test_findfile.vim @@ -226,4 +226,26 @@ func Test_find_cmd() call assert_fails('tabfind', 'E471:') endfunc +func Test_find_non_existing_path() + new + let save_path = &path + let save_dir = getcwd() + call mkdir('dir1/dir2', 'p') + call writefile([], 'dir1/file.txt') + call writefile([], 'dir1/dir2/base.txt') + call chdir('dir1/dir2') + e base.txt + set path=../include + + call assert_fails(':find file.txt', 'E345:') + + call chdir(save_dir) + bw! + call delete('dir1/dir2/base.txt', 'rf') + call delete('dir1/dir2', 'rf') + call delete('dir1/file.txt', 'rf') + call delete('dir1', 'rf') + let &path = save_path +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_fnamemodify.vim b/src/nvim/testdir/test_fnamemodify.vim index 411f7ebbb3..5ae2a5ee17 100644 --- a/src/nvim/testdir/test_fnamemodify.vim +++ b/src/nvim/testdir/test_fnamemodify.vim @@ -3,8 +3,10 @@ func Test_fnamemodify() let save_home = $HOME let save_shell = &shell + let save_shellslash = &shellslash let $HOME = fnamemodify('.', ':p:h:h') set shell=sh + set shellslash call assert_equal('/', fnamemodify('.', ':p')[-1:]) call assert_equal('r', fnamemodify('.', ':p:h')[-1:]) @@ -27,6 +29,21 @@ func Test_fnamemodify() call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e')) call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e:e')) call assert_equal('tar', fnamemodify('abc.fb2.tar.gz', ':e:e:r')) + call assert_equal(getcwd(), fnamemodify('', ':p:h')) + + let cwd = getcwd() + call chdir($HOME) + call assert_equal('foobar', fnamemodify('~/foobar', ':~:.')) + call chdir(cwd) + call mkdir($HOME . '/XXXXXXXX/a', 'p') + call mkdir($HOME . '/XXXXXXXX/b', 'p') + call chdir($HOME . '/XXXXXXXX/a/') + call assert_equal('foo', fnamemodify($HOME . '/XXXXXXXX/a/foo', ':p:~:.')) + call assert_equal('~/XXXXXXXX/b/foo', fnamemodify($HOME . '/XXXXXXXX/b/foo', ':p:~:.')) + call mkdir($HOME . '/XXXXXXXX/a.ext', 'p') + call assert_equal('~/XXXXXXXX/a.ext/foo', fnamemodify($HOME . '/XXXXXXXX/a.ext/foo', ':p:~:.')) + call chdir(cwd) + call delete($HOME . '/XXXXXXXX', 'rf') call assert_equal('''abc def''', fnamemodify('abc def', ':S')) call assert_equal('''abc" "def''', fnamemodify('abc" "def', ':S')) @@ -44,6 +61,7 @@ func Test_fnamemodify() let $HOME = save_home let &shell = save_shell + let &shellslash = save_shellslash endfunc func Test_fnamemodify_er() @@ -73,6 +91,7 @@ func Test_fnamemodify_er() call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e')) call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e:e')) + call assert_equal('', fnamemodify('', ':p:t')) call assert_equal('', fnamemodify(v:_null_string, v:_null_string)) endfunc diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim index 0edbeb420a..994d74601a 100644 --- a/src/nvim/testdir/test_functions.vim +++ b/src/nvim/testdir/test_functions.vim @@ -332,37 +332,6 @@ func Test_simplify() call assert_fails('call simplify(1.2)', 'E806:') endfunc -func Test_setbufvar_options() - " This tests that aucmd_prepbuf() and aucmd_restbuf() properly restore the - " window layout. - call assert_equal(1, winnr('$')) - split dummy_preview - resize 2 - set winfixheight winfixwidth - let prev_id = win_getid() - - wincmd j - let wh = winheight(0) - let dummy_buf = bufnr('dummy_buf1', v:true) - call setbufvar(dummy_buf, '&buftype', 'nofile') - execute 'belowright vertical split #' . dummy_buf - call assert_equal(wh, winheight(0)) - let dum1_id = win_getid() - - wincmd h - let wh = winheight(0) - let dummy_buf = bufnr('dummy_buf2', v:true) - eval 'nofile'->setbufvar(dummy_buf, '&buftype') - execute 'belowright vertical split #' . dummy_buf - call assert_equal(wh, winheight(0)) - - bwipe! - call win_gotoid(prev_id) - bwipe! - call win_gotoid(dum1_id) - bwipe! -endfunc - func Test_pathshorten() call assert_equal('', pathshorten('')) call assert_equal('foo', pathshorten('foo')) @@ -376,6 +345,25 @@ func Test_pathshorten() call assert_equal('~.f/bar', pathshorten('~.foo/bar')) call assert_equal('.~f/bar', pathshorten('.~foo/bar')) call assert_equal('~/f/bar', pathshorten('~/foo/bar')) + call assert_fails('call pathshorten([])', 'E730:') + + " test pathshorten with optional variable to set preferred size of shortening + call assert_equal('', pathshorten('', 2)) + call assert_equal('foo', pathshorten('foo', 2)) + call assert_equal('/foo', pathshorten('/foo', 2)) + call assert_equal('fo/', pathshorten('foo/', 2)) + call assert_equal('fo/bar', pathshorten('foo/bar', 2)) + call assert_equal('fo/ba/foobar', pathshorten('foo/bar/foobar', 2)) + call assert_equal('/fo/ba/foobar', pathshorten('/foo/bar/foobar', 2)) + call assert_equal('.fo/bar', pathshorten('.foo/bar', 2)) + call assert_equal('~fo/bar', pathshorten('~foo/bar', 2)) + call assert_equal('~.fo/bar', pathshorten('~.foo/bar', 2)) + call assert_equal('.~fo/bar', pathshorten('.~foo/bar', 2)) + call assert_equal('~/fo/bar', pathshorten('~/foo/bar', 2)) + call assert_fails('call pathshorten([],2)', 'E730:') + call assert_notequal('~/fo/bar', pathshorten('~/foo/bar', 3)) + call assert_equal('~/foo/bar', pathshorten('~/foo/bar', 3)) + call assert_equal('~/f/bar', pathshorten('~/foo/bar', 0)) endfunc func Test_strpart() @@ -1274,6 +1262,37 @@ func Test_shellescape() let &shell = save_shell endfunc +func Test_setbufvar_options() + " This tests that aucmd_prepbuf() and aucmd_restbuf() properly restore the + " window layout. + call assert_equal(1, winnr('$')) + split dummy_preview + resize 2 + set winfixheight winfixwidth + let prev_id = win_getid() + + wincmd j + let wh = winheight(0) + let dummy_buf = bufnr('dummy_buf1', v:true) + call setbufvar(dummy_buf, '&buftype', 'nofile') + execute 'belowright vertical split #' . dummy_buf + call assert_equal(wh, winheight(0)) + let dum1_id = win_getid() + + wincmd h + let wh = winheight(0) + let dummy_buf = bufnr('dummy_buf2', v:true) + eval 'nofile'->setbufvar(dummy_buf, '&buftype') + execute 'belowright vertical split #' . dummy_buf + call assert_equal(wh, winheight(0)) + + bwipe! + call win_gotoid(prev_id) + bwipe! + call win_gotoid(dum1_id) + bwipe! +endfunc + func Test_redo_in_nested_functions() nnoremap g. :set opfunc=Operator<CR>g@ function Operator( type, ... ) @@ -1331,68 +1350,6 @@ func Test_trim() call assert_equal("x", trim(chars . "x" . chars)) endfunc -func EditAnotherFile() - let word = expand('<cword>') - edit Xfuncrange2 -endfunc - -func Test_func_range_with_edit() - " Define a function that edits another buffer, then call it with a range that - " is invalid in that buffer. - call writefile(['just one line'], 'Xfuncrange2') - new - eval 10->range()->setline(1) - write Xfuncrange1 - call assert_fails('5,8call EditAnotherFile()', 'E16:') - - call delete('Xfuncrange1') - call delete('Xfuncrange2') - bwipe! -endfunc - -func Test_func_exists_on_reload() - call writefile(['func ExistingFunction()', 'echo "yes"', 'endfunc'], 'Xfuncexists') - call assert_equal(0, exists('*ExistingFunction')) - source Xfuncexists - call assert_equal(1, '*ExistingFunction'->exists()) - " Redefining a function when reloading a script is OK. - source Xfuncexists - call assert_equal(1, exists('*ExistingFunction')) - - " But redefining in another script is not OK. - call writefile(['func ExistingFunction()', 'echo "yes"', 'endfunc'], 'Xfuncexists2') - call assert_fails('source Xfuncexists2', 'E122:') - - delfunc ExistingFunction - call assert_equal(0, exists('*ExistingFunction')) - call writefile([ - \ 'func ExistingFunction()', 'echo "yes"', 'endfunc', - \ 'func ExistingFunction()', 'echo "no"', 'endfunc', - \ ], 'Xfuncexists') - call assert_fails('source Xfuncexists', 'E122:') - call assert_equal(1, exists('*ExistingFunction')) - - call delete('Xfuncexists2') - call delete('Xfuncexists') - delfunc ExistingFunction -endfunc - -sandbox function Fsandbox() - normal ix -endfunc - -func Test_func_sandbox() - sandbox let F = {-> 'hello'} - call assert_equal('hello', F()) - - sandbox let F = {-> "normal ix\<Esc>"->execute()} - call assert_fails('call F()', 'E48:') - unlet F - - call assert_fails('call Fsandbox()', 'E48:') - delfunc Fsandbox -endfunc - " Test for reg_recording() and reg_executing() func Test_reg_executing_and_recording() let s:reg_stat = '' @@ -1494,6 +1451,10 @@ func Test_getchar() call assert_equal('', getcharstr(0)) call assert_equal('', getcharstr(1)) + call feedkeys("\<M-F2>", '') + call assert_equal("\<M-F2>", getchar(0)) + call assert_equal(0, getchar(0)) + call setline(1, 'xxxx') " call test_setmouse(1, 3) " let v:mouse_win = 9 @@ -1519,24 +1480,31 @@ func Test_libcall_libcallnr() let libc = 'msvcrt.dll' elseif has('mac') let libc = 'libSystem.B.dylib' - elseif system('uname -s') =~ 'SunOS' - " Set the path to libc.so according to the architecture. - let test_bits = system('file ' . GetVimProg()) - let test_arch = system('uname -p') - if test_bits =~ '64-bit' && test_arch =~ 'sparc' - let libc = '/usr/lib/sparcv9/libc.so' - elseif test_bits =~ '64-bit' && test_arch =~ 'i386' - let libc = '/usr/lib/amd64/libc.so' + elseif executable('ldd') + let libc = matchstr(split(system('ldd ' . GetVimProg())), '/libc\.so\>') + endif + if get(l:, 'libc', '') ==# '' + " On Unix, libc.so can be in various places. + if has('linux') + " There is not documented but regarding the 1st argument of glibc's + " dlopen an empty string and nullptr are equivalent, so using an empty + " string for the 1st argument of libcall allows to call functions. + let libc = '' + elseif has('sun') + " Set the path to libc.so according to the architecture. + let test_bits = system('file ' . GetVimProg()) + let test_arch = system('uname -p') + if test_bits =~ '64-bit' && test_arch =~ 'sparc' + let libc = '/usr/lib/sparcv9/libc.so' + elseif test_bits =~ '64-bit' && test_arch =~ 'i386' + let libc = '/usr/lib/amd64/libc.so' + else + let libc = '/usr/lib/libc.so' + endif else - let libc = '/usr/lib/libc.so' + " Unfortunately skip this test until a good way is found. + return endif - elseif system('uname -s') =~ 'OpenBSD' - let libc = 'libc.so' - else - " On Unix, libc.so can be in various places. - " Interestingly, using an empty string for the 1st argument of libcall - " allows to call functions from libc which is not documented. - let libc = '' endif if has('win32') @@ -1559,47 +1527,96 @@ func Test_libcall_libcallnr() call assert_fails("call libcallnr('Xdoesnotexist_', 'strlen', 'abcd')", 'E364:') endfunc -func Test_bufadd_bufload() - call assert_equal(0, bufexists('someName')) - let buf = bufadd('someName') - call assert_notequal(0, buf) - call assert_equal(1, bufexists('someName')) - call assert_equal(0, getbufvar(buf, '&buflisted')) - call assert_equal(0, bufloaded(buf)) - call bufload(buf) - call assert_equal(1, bufloaded(buf)) - call assert_equal([''], getbufline(buf, 1, '$')) +sandbox function Fsandbox() + normal ix +endfunc - let curbuf = bufnr('') - eval ['some', 'text']->writefile('XotherName') - let buf = 'XotherName'->bufadd() - call assert_notequal(0, buf) - eval 'XotherName'->bufexists()->assert_equal(1) - call assert_equal(0, getbufvar(buf, '&buflisted')) - call assert_equal(0, bufloaded(buf)) - eval buf->bufload() - call assert_equal(1, bufloaded(buf)) - call assert_equal(['some', 'text'], getbufline(buf, 1, '$')) - call assert_equal(curbuf, bufnr('')) +func Test_func_sandbox() + sandbox let F = {-> 'hello'} + call assert_equal('hello', F()) - let buf1 = bufadd('') - let buf2 = bufadd('') - call assert_notequal(0, buf1) - call assert_notequal(0, buf2) - call assert_notequal(buf1, buf2) - call assert_equal(1, bufexists(buf1)) - call assert_equal(1, bufexists(buf2)) - call assert_equal(0, bufloaded(buf1)) - exe 'bwipe ' .. buf1 - call assert_equal(0, bufexists(buf1)) - call assert_equal(1, bufexists(buf2)) - exe 'bwipe ' .. buf2 - call assert_equal(0, bufexists(buf2)) + sandbox let F = {-> "normal ix\<Esc>"->execute()} + call assert_fails('call F()', 'E48:') + unlet F - bwipe someName - bwipe XotherName - call assert_equal(0, bufexists('someName')) - call delete('XotherName') + call assert_fails('call Fsandbox()', 'E48:') + delfunc Fsandbox +endfunc + +func EditAnotherFile() + let word = expand('<cword>') + edit Xfuncrange2 +endfunc + +func Test_func_range_with_edit() + " Define a function that edits another buffer, then call it with a range that + " is invalid in that buffer. + call writefile(['just one line'], 'Xfuncrange2') + new + eval 10->range()->setline(1) + write Xfuncrange1 + call assert_fails('5,8call EditAnotherFile()', 'E16:') + + call delete('Xfuncrange1') + call delete('Xfuncrange2') + bwipe! +endfunc + +func Test_func_exists_on_reload() + call writefile(['func ExistingFunction()', 'echo "yes"', 'endfunc'], 'Xfuncexists') + call assert_equal(0, exists('*ExistingFunction')) + source Xfuncexists + call assert_equal(1, '*ExistingFunction'->exists()) + " Redefining a function when reloading a script is OK. + source Xfuncexists + call assert_equal(1, exists('*ExistingFunction')) + + " But redefining in another script is not OK. + call writefile(['func ExistingFunction()', 'echo "yes"', 'endfunc'], 'Xfuncexists2') + call assert_fails('source Xfuncexists2', 'E122:') + + delfunc ExistingFunction + call assert_equal(0, exists('*ExistingFunction')) + call writefile([ + \ 'func ExistingFunction()', 'echo "yes"', 'endfunc', + \ 'func ExistingFunction()', 'echo "no"', 'endfunc', + \ ], 'Xfuncexists') + call assert_fails('source Xfuncexists', 'E122:') + call assert_equal(1, exists('*ExistingFunction')) + + call delete('Xfuncexists2') + call delete('Xfuncexists') + delfunc ExistingFunction +endfunc + +func Test_platform_name() + " The system matches at most only one name. + let names = ['amiga', 'beos', 'bsd', 'hpux', 'linux', 'mac', 'qnx', 'sun', 'vms', 'win32', 'win32unix'] + call assert_inrange(0, 1, len(filter(copy(names), 'has(v:val)'))) + + " Is Unix? + call assert_equal(has('beos'), has('beos') && has('unix')) + call assert_equal(has('bsd'), has('bsd') && has('unix')) + call assert_equal(has('hpux'), has('hpux') && has('unix')) + call assert_equal(has('linux'), has('linux') && has('unix')) + call assert_equal(has('mac'), has('mac') && has('unix')) + call assert_equal(has('qnx'), has('qnx') && has('unix')) + call assert_equal(has('sun'), has('sun') && has('unix')) + call assert_equal(has('win32'), has('win32') && !has('unix')) + call assert_equal(has('win32unix'), has('win32unix') && has('unix')) + + if has('unix') && executable('uname') + let uname = system('uname') + call assert_equal(uname =~? 'BeOS', has('beos')) + " GNU userland on BSD kernels (e.g., GNU/kFreeBSD) don't have BSD defined + call assert_equal(uname =~? '\%(GNU/k\w\+\)\@<!BSD\|DragonFly', has('bsd')) + call assert_equal(uname =~? 'HP-UX', has('hpux')) + call assert_equal(uname =~? 'Linux', has('linux')) + call assert_equal(uname =~? 'Darwin', has('mac')) + call assert_equal(uname =~? 'QNX', has('qnx')) + call assert_equal(uname =~? 'SunOS', has('sun')) + call assert_equal(uname =~? 'CYGWIN\|MSYS', has('win32unix')) + endif endfunc func Test_readdir() @@ -1658,6 +1675,49 @@ func Test_eventhandler() call assert_equal(0, eventhandler()) endfunc +func Test_bufadd_bufload() + call assert_equal(0, bufexists('someName')) + let buf = bufadd('someName') + call assert_notequal(0, buf) + call assert_equal(1, bufexists('someName')) + call assert_equal(0, getbufvar(buf, '&buflisted')) + call assert_equal(0, bufloaded(buf)) + call bufload(buf) + call assert_equal(1, bufloaded(buf)) + call assert_equal([''], getbufline(buf, 1, '$')) + + let curbuf = bufnr('') + eval ['some', 'text']->writefile('XotherName') + let buf = 'XotherName'->bufadd() + call assert_notequal(0, buf) + eval 'XotherName'->bufexists()->assert_equal(1) + call assert_equal(0, getbufvar(buf, '&buflisted')) + call assert_equal(0, bufloaded(buf)) + eval buf->bufload() + call assert_equal(1, bufloaded(buf)) + call assert_equal(['some', 'text'], getbufline(buf, 1, '$')) + call assert_equal(curbuf, bufnr('')) + + let buf1 = bufadd('') + let buf2 = bufadd('') + call assert_notequal(0, buf1) + call assert_notequal(0, buf2) + call assert_notequal(buf1, buf2) + call assert_equal(1, bufexists(buf1)) + call assert_equal(1, bufexists(buf2)) + call assert_equal(0, bufloaded(buf1)) + exe 'bwipe ' .. buf1 + call assert_equal(0, bufexists(buf1)) + call assert_equal(1, bufexists(buf2)) + exe 'bwipe ' .. buf2 + call assert_equal(0, bufexists(buf2)) + + bwipe someName + bwipe XotherName + call assert_equal(0, bufexists('someName')) + call delete('XotherName') +endfunc + " Test for the eval() function func Test_eval() call assert_fails("call eval('5 a')", 'E488:') @@ -1675,6 +1735,33 @@ func Test_nr2char() call assert_equal("\x80\xfc\b\xfd\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX", eval('"\<M-' .. nr2char(0x40000000) .. '>"')) endfunc +" Test for getcurpos() and setpos() +func Test_getcurpos_setpos() + new + call setline(1, ['012345678', '012345678']) + normal gg6l + let sp = getcurpos() + normal 0 + call setpos('.', sp) + normal jyl + call assert_equal('6', @") + call assert_equal(-1, setpos('.', v:_null_list)) + call assert_equal(-1, setpos('.', {})) + + let winid = win_getid() + normal G$ + let pos = getcurpos() + wincmd w + call assert_equal(pos, getcurpos(winid)) + + wincmd w + close! + + call assert_equal(getcurpos(), getcurpos(0)) + call assert_equal([0, 0, 0, 0, 0], getcurpos(-1)) + call assert_equal([0, 0, 0, 0, 0], getcurpos(1999)) +endfunc + func HasDefault(msg = 'msg') return a:msg endfunc diff --git a/src/nvim/testdir/test_gf.vim b/src/nvim/testdir/test_gf.vim index 43efd6248e..589899f532 100644 --- a/src/nvim/testdir/test_gf.vim +++ b/src/nvim/testdir/test_gf.vim @@ -19,11 +19,7 @@ func Test_gf_url() call search("^second") call search("URL") call assert_equal("URL://machine.name/tmp/vimtest2b", expand("<cfile>")) - if has("ebcdic") - set isf=@,240-249,/,.,-,_,+,,,$,:,~,\ - else - set isf=@,48-57,/,.,-,_,+,,,$,~,\ - endif + set isf=@,48-57,/,.,-,_,+,,,$,~,\ call search("^third") call search("name") call assert_equal("URL:\\\\machine.name\\vimtest2c", expand("<cfile>")) @@ -76,11 +72,7 @@ endfunc " Test for invoking 'gf' on a ${VAR} variable func Test_gf() - if has("ebcdic") - set isfname=@,240-249,/,.,-,_,+,,,$,:,~,{,} - else - set isfname=@,48-57,/,.,-,_,+,,,$,:,~,{,} - endif + set isfname=@,48-57,/,.,-,_,+,,,$,:,~,{,} call writefile(["Test for gf command"], "Xtest1") if has("unix") diff --git a/src/nvim/testdir/test_global.vim b/src/nvim/testdir/test_global.vim index 8edc9c2608..ad561baf4a 100644 --- a/src/nvim/testdir/test_global.vim +++ b/src/nvim/testdir/test_global.vim @@ -36,6 +36,36 @@ func Test_global_error() call assert_fails('g/\(/y', 'E476:') endfunc +" Test for printing lines using :g with different search patterns +func Test_global_print() + new + call setline(1, ['foo', 'bar', 'foo', 'foo']) + let @/ = 'foo' + let t = execute("g/")->trim()->split("\n") + call assert_equal(['foo', 'foo', 'foo'], t) + + " Test for Vi compatible patterns + let @/ = 'bar' + let t = execute('g\/')->trim()->split("\n") + call assert_equal(['bar'], t) + + normal gg + s/foo/foo/ + let t = execute('g\&')->trim()->split("\n") + call assert_equal(['foo', 'foo', 'foo'], t) + + let @/ = 'bar' + let t = execute('g?')->trim()->split("\n") + call assert_equal(['bar'], t) + + " Test for the 'Pattern found in every line' message + let v:statusmsg = '' + v/foo\|bar/p + call assert_notequal('', v:statusmsg) + + close! +endfunc + func Test_wrong_delimiter() call assert_fails('g x^bxd', 'E146:') endfunc diff --git a/src/nvim/testdir/test_help.vim b/src/nvim/testdir/test_help.vim index 8e59efd22d..b2d943be00 100644 --- a/src/nvim/testdir/test_help.vim +++ b/src/nvim/testdir/test_help.vim @@ -1,4 +1,3 @@ - " Tests for :help func Test_help_restore_snapshot() @@ -13,6 +12,18 @@ endfunc func Test_help_errors() call assert_fails('help doesnotexist', 'E149:') call assert_fails('help!', 'E478:') + if has('multi_lang') + call assert_fails('help help@xy', 'E661:') + endif + + let save_hf = &helpfile + set helpfile=help_missing + help + call assert_equal(1, winnr('$')) + call assert_notequal('help', &buftype) + let &helpfile = save_hf + + call assert_fails('help ' . repeat('a', 1048), 'E149:') new set keywordprg=:help @@ -101,4 +112,13 @@ func Test_helptag_cmd() call delete('Xdir', 'rf') endfunc +func Test_help_long_argument() + try + exe 'help \%' .. repeat('0', 1021) + catch + call assert_match("E149:", v:exception) + endtry +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_help_tagjump.vim b/src/nvim/testdir/test_help_tagjump.vim index a6494c531c..a43889b57e 100644 --- a/src/nvim/testdir/test_help_tagjump.vim +++ b/src/nvim/testdir/test_help_tagjump.vim @@ -23,6 +23,11 @@ func Test_help_tagjump() call assert_true(getline('.') =~ '\*bar\*') helpclose + help " + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*quote\*') + helpclose + help "* call assert_equal("help", &filetype) call assert_true(getline('.') =~ '\*quotestar\*') @@ -86,11 +91,40 @@ func Test_help_tagjump() call assert_true(getline('.') =~ '\*i_^_CTRL-D\*') helpclose + help i^x^y + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*i_CTRL-X_CTRL-Y\*') + helpclose + + exe "help i\<C-\>\<C-G>" + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*i_CTRL-\\_CTRL-G\*') + helpclose + exec "help \<C-V>" call assert_equal("help", &filetype) call assert_true(getline('.') =~ '\*CTRL-V\*') helpclose + help /\| + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*/\\bar\*') + helpclose + + help CTRL-\_CTRL-N + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*CTRL-\\_CTRL-N\*') + helpclose + + help `:pwd`, + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*:pwd\*') + helpclose + + help `:ls`. + call assert_equal("help", &filetype) + call assert_true(getline('.') =~ '\*:ls\*') + helpclose exec "help! ('textwidth'" call assert_equal("help", &filetype) @@ -122,6 +156,15 @@ func Test_help_tagjump() call assert_true(getline('.') =~ '\*{address}\*') helpclose + " Use special patterns in the help tag + for h in ['/\w', '/\%^', '/\%(', '/\zs', '/\@<=', '/\_$', '[++opt]', '/\{'] + exec "help! " . h + call assert_equal("help", &filetype) + let pat = '\*' . escape(h, '\$[') . '\*' + call assert_true(getline('.') =~ pat, pat) + helpclose + endfor + exusage call assert_equal("help", &filetype) call assert_true(getline('.') =~ '\*:index\*') diff --git a/src/nvim/testdir/test_highlight.vim b/src/nvim/testdir/test_highlight.vim index 899eb530ec..6971ecd357 100644 --- a/src/nvim/testdir/test_highlight.vim +++ b/src/nvim/testdir/test_highlight.vim @@ -597,6 +597,31 @@ func Test_cursorline_with_visualmode() call delete('Xtest_cursorline_with_visualmode') endfunc +func Test_colorcolumn() + CheckScreendump + + " check that setting 'colorcolumn' when entering a buffer works + let lines =<< trim END + split + edit X + call setline(1, ["1111111111","22222222222","3333333333"]) + set nomodified + set colorcolumn=3,9 + set number cursorline cursorlineopt=number + wincmd w + buf X + END + call writefile(lines, 'Xtest_colorcolumn') + let buf = RunVimInTerminal('-S Xtest_colorcolumn', {'rows': 10}) + call term_sendkeys(buf, ":\<CR>") + call term_wait(buf) + call VerifyScreenDump(buf, 'Test_colorcolumn_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('Xtest_colorcolumn') +endfunc + func Test_colorcolumn_bri() CheckScreendump diff --git a/src/nvim/testdir/test_ins_complete.vim b/src/nvim/testdir/test_ins_complete.vim index ce75799551..186fa8871f 100644 --- a/src/nvim/testdir/test_ins_complete.vim +++ b/src/nvim/testdir/test_ins_complete.vim @@ -109,7 +109,7 @@ func s:CompleteDone_CompleteFuncNone( findstart, base ) return v:none endfunc -function! s:CompleteDone_CompleteFuncDict( findstart, base ) +func s:CompleteDone_CompleteFuncDict( findstart, base ) if a:findstart return 0 endif @@ -126,7 +126,7 @@ function! s:CompleteDone_CompleteFuncDict( findstart, base ) \ } \ ] \ } -endfunction +endfunc func s:CompleteDone_CheckCompletedItemNone() let s:called_completedone = 1 @@ -341,6 +341,14 @@ func Test_compl_feedkeys() set completeopt& endfunc +func s:ComplInCmdwin_GlobalCompletion(a, l, p) + return 'global' +endfunc + +func s:ComplInCmdwin_LocalCompletion(a, l, p) + return 'local' +endfunc + func Test_compl_in_cmdwin() set wildmenu wildchar=<Tab> com! -nargs=1 -complete=command GetInput let input = <q-args> @@ -376,6 +384,47 @@ func Test_compl_in_cmdwin() call feedkeys("q::GetInput b:test_\<Tab>\<CR>:q\<CR>", 'tx!') call assert_equal('b:test_', input) + + " Argument completion of buffer-local command + func s:ComplInCmdwin_GlobalCompletionList(a, l, p) + return ['global'] + endfunc + + func s:ComplInCmdwin_LocalCompletionList(a, l, p) + return ['local'] + endfunc + + func s:ComplInCmdwin_CheckCompletion(arg) + call assert_equal('local', a:arg) + endfunc + + com! -nargs=1 -complete=custom,<SID>ComplInCmdwin_GlobalCompletion + \ TestCommand call s:ComplInCmdwin_CheckCompletion(<q-args>) + com! -buffer -nargs=1 -complete=custom,<SID>ComplInCmdwin_LocalCompletion + \ TestCommand call s:ComplInCmdwin_CheckCompletion(<q-args>) + call feedkeys("q:iTestCommand \<Tab>\<CR>", 'tx!') + + com! -nargs=1 -complete=customlist,<SID>ComplInCmdwin_GlobalCompletionList + \ TestCommand call s:ComplInCmdwin_CheckCompletion(<q-args>) + com! -buffer -nargs=1 -complete=customlist,<SID>ComplInCmdwin_LocalCompletionList + \ TestCommand call s:ComplInCmdwin_CheckCompletion(<q-args>) + + call feedkeys("q:iTestCommand \<Tab>\<CR>", 'tx!') + + func! s:ComplInCmdwin_CheckCompletion(arg) + call assert_equal('global', a:arg) + endfunc + new + call feedkeys("q:iTestCommand \<Tab>\<CR>", 'tx!') + quit + + delfunc s:ComplInCmdwin_GlobalCompletion + delfunc s:ComplInCmdwin_LocalCompletion + delfunc s:ComplInCmdwin_GlobalCompletionList + delfunc s:ComplInCmdwin_LocalCompletionList + delfunc s:ComplInCmdwin_CheckCompletion + + delcom -buffer TestCommand delcom TestCommand delcom GetInput unlet w:test_winvar @@ -445,6 +494,28 @@ func Test_issue_7021() set completeslash= endfunc +func Test_pum_stopped_by_timer() + CheckScreendump + + let lines =<< trim END + call setline(1, ['hello', 'hullo', 'heeee', '']) + func StartCompl() + call timer_start(100, { -> execute('stopinsert') }) + call feedkeys("Gah\<C-N>") + endfunc + END + + call writefile(lines, 'Xpumscript') + let buf = RunVimInTerminal('-S Xpumscript', #{rows: 12}) + call term_sendkeys(buf, ":call StartCompl()\<CR>") + call TermWait(buf, 200) + call term_sendkeys(buf, "k") + call VerifyScreenDump(buf, 'Test_pum_stopped_by_timer', {}) + + call StopVimInTerminal(buf) + call delete('Xpumscript') +endfunc + func Test_pum_with_folds_two_tabs() CheckScreendump diff --git a/src/nvim/testdir/test_lambda.vim b/src/nvim/testdir/test_lambda.vim index 63bb4ae1ef..c1fe47d1c9 100644 --- a/src/nvim/testdir/test_lambda.vim +++ b/src/nvim/testdir/test_lambda.vim @@ -1,24 +1,24 @@ " Test for lambda and closure -function! Test_lambda_feature() +func Test_lambda_feature() call assert_equal(1, has('lambda')) -endfunction +endfunc -function! Test_lambda_with_filter() +func Test_lambda_with_filter() let s:x = 2 call assert_equal([2, 3], filter([1, 2, 3], {i, v -> v >= s:x})) -endfunction +endfunc -function! Test_lambda_with_map() +func Test_lambda_with_map() let s:x = 1 call assert_equal([2, 3, 4], map([1, 2, 3], {i, v -> v + s:x})) -endfunction +endfunc -function! Test_lambda_with_sort() +func Test_lambda_with_sort() call assert_equal([1, 2, 3, 4, 7], sort([3,7,2,1,4], {a, b -> a - b})) -endfunction +endfunc -function! Test_lambda_with_timer() +func Test_lambda_with_timer() if !has('timers') return endif @@ -54,10 +54,10 @@ function! Test_lambda_with_timer() call assert_true(s:n > m) endfunc -function! Test_lambda_with_partial() +func Test_lambda_with_partial() let l:Cb = function({... -> ['zero', a:1, a:2, a:3]}, ['one', 'two']) call assert_equal(['zero', 'one', 'two', 'three'], l:Cb('three')) -endfunction +endfunc function Test_lambda_fails() call assert_equal(3, {a, b -> a + b}(1, 2)) @@ -70,59 +70,59 @@ func Test_not_lambda() call assert_equal('foo', x['>']) endfunc -function! Test_lambda_capture_by_reference() +func Test_lambda_capture_by_reference() let v = 1 let l:F = {x -> x + v} let v = 2 call assert_equal(12, l:F(10)) -endfunction +endfunc -function! Test_lambda_side_effect() - function! s:update_and_return(arr) +func Test_lambda_side_effect() + func! s:update_and_return(arr) let a:arr[1] = 5 return a:arr - endfunction + endfunc - function! s:foo(arr) + func! s:foo(arr) return {-> s:update_and_return(a:arr)} - endfunction + endfunc let arr = [3,2,1] call assert_equal([3, 5, 1], s:foo(arr)()) -endfunction +endfunc -function! Test_lambda_refer_local_variable_from_other_scope() - function! s:foo(X) +func Test_lambda_refer_local_variable_from_other_scope() + func! s:foo(X) return a:X() " refer l:x in s:bar() - endfunction + endfunc - function! s:bar() + func! s:bar() let x = 123 return s:foo({-> x}) - endfunction + endfunc call assert_equal(123, s:bar()) -endfunction +endfunc -function! Test_lambda_do_not_share_local_variable() - function! s:define_funcs() +func Test_lambda_do_not_share_local_variable() + func! s:define_funcs() let l:One = {-> split(execute("let a = 'abc' | echo a"))[0]} let l:Two = {-> exists("a") ? a : "no"} return [l:One, l:Two] - endfunction + endfunc let l:F = s:define_funcs() call assert_equal('no', l:F[1]()) call assert_equal('abc', l:F[0]()) call assert_equal('no', l:F[1]()) -endfunction +endfunc -function! Test_lambda_closure_counter() - function! s:foo() +func Test_lambda_closure_counter() + func! s:foo() let x = 0 return {-> [execute("let x += 1"), x][-1]} - endfunction + endfunc let l:F = s:foo() call garbagecollect() @@ -130,52 +130,52 @@ function! Test_lambda_closure_counter() call assert_equal(2, l:F()) call assert_equal(3, l:F()) call assert_equal(4, l:F()) -endfunction +endfunc -function! Test_lambda_with_a_var() - function! s:foo() +func Test_lambda_with_a_var() + func! s:foo() let x = 2 return {... -> a:000 + [x]} - endfunction - function! s:bar() + endfunc + func! s:bar() return s:foo()(1) - endfunction + endfunc call assert_equal([1, 2], s:bar()) -endfunction +endfunc -function! Test_lambda_call_lambda_from_lambda() - function! s:foo(x) +func Test_lambda_call_lambda_from_lambda() + func! s:foo(x) let l:F1 = {-> {-> a:x}} return {-> l:F1()} - endfunction + endfunc let l:F = s:foo(1) call assert_equal(1, l:F()()) -endfunction +endfunc -function! Test_lambda_delfunc() - function! s:gen() +func Test_lambda_delfunc() + func! s:gen() let pl = l: let l:Foo = {-> get(pl, "Foo", get(pl, "Bar", {-> 0}))} let l:Bar = l:Foo delfunction l:Foo return l:Bar - endfunction + endfunc let l:F = s:gen() call assert_fails(':call l:F()', 'E933:') -endfunction +endfunc -function! Test_lambda_scope() - function! s:NewCounter() +func Test_lambda_scope() + func! s:NewCounter() let c = 0 return {-> [execute('let c += 1'), c][-1]} - endfunction + endfunc - function! s:NewCounter2() + func! s:NewCounter2() return {-> [execute('let c += 100'), c][-1]} - endfunction + endfunc let l:C = s:NewCounter() let l:D = s:NewCounter2() @@ -183,37 +183,37 @@ function! Test_lambda_scope() call assert_equal(1, l:C()) call assert_fails(':call l:D()', 'E121:') call assert_equal(2, l:C()) -endfunction +endfunc -function! Test_lambda_share_scope() - function! s:New() +func Test_lambda_share_scope() + func! s:New() let c = 0 let l:Inc0 = {-> [execute('let c += 1'), c][-1]} let l:Dec0 = {-> [execute('let c -= 1'), c][-1]} return [l:Inc0, l:Dec0] - endfunction + endfunc let [l:Inc, l:Dec] = s:New() call assert_equal(1, l:Inc()) call assert_equal(2, l:Inc()) call assert_equal(1, l:Dec()) -endfunction +endfunc -function! Test_lambda_circular_reference() - function! s:Foo() +func Test_lambda_circular_reference() + func! s:Foo() let d = {} let d.f = {-> d} return d.f - endfunction + endfunc call s:Foo() call garbagecollect() let i = 0 | while i < 10000 | call s:Foo() | let i+= 1 | endwhile call garbagecollect() -endfunction +endfunc -function! Test_lambda_combination() +func Test_lambda_combination() call assert_equal(2, {x -> {x -> x}}(1)(2)) call assert_equal(10, {y -> {x -> x(y)(10)}({y -> y})}({z -> z})) call assert_equal(5.0, {x -> {y -> x / y}}(10)(2.0)) @@ -226,17 +226,17 @@ function! Test_lambda_combination() let Z = {f -> {x -> f({y -> x(x)(y)})}({x -> f({y -> x(x)(y)})})} let Fact = {f -> {x -> x == 0 ? 1 : x * f(x - 1)}} call assert_equal(120, Z(Fact)(5)) -endfunction +endfunc -function! Test_closure_counter() - function! s:foo() +func Test_closure_counter() + func! s:foo() let x = 0 - function! s:bar() closure + func! s:bar() closure let x += 1 return x - endfunction + endfunc return function('s:bar') - endfunction + endfunc let l:F = s:foo() call garbagecollect() @@ -244,30 +244,30 @@ function! Test_closure_counter() call assert_equal(2, l:F()) call assert_equal(3, l:F()) call assert_equal(4, l:F()) -endfunction +endfunc -function! Test_closure_unlet() - function! s:foo() +func Test_closure_unlet() + func! s:foo() let x = 1 - function! s:bar() closure + func! s:bar() closure unlet x - endfunction + endfunc call s:bar() return l: - endfunction + endfunc call assert_false(has_key(s:foo(), 'x')) call garbagecollect() -endfunction +endfunc -function! LambdaFoo() +func LambdaFoo() let x = 0 - function! LambdaBar() closure + func! LambdaBar() closure let x += 1 return x - endfunction + endfunc return function('LambdaBar') -endfunction +endfunc func Test_closure_refcount() let g:Count = LambdaFoo() @@ -303,3 +303,8 @@ func Test_lambda_with_index() let Extract = {-> function(List, ['foobar'])()[0]} call assert_equal('foobar', Extract()) endfunc + +func Test_lambda_error() + " This was causing a crash + call assert_fails('ec{@{->{d->()()', 'E15') +endfunc diff --git a/src/nvim/testdir/test_listchars.vim b/src/nvim/testdir/test_listchars.vim index 0bcbd9c4a5..c6e2ebd406 100644 --- a/src/nvim/testdir/test_listchars.vim +++ b/src/nvim/testdir/test_listchars.vim @@ -1,6 +1,8 @@ " Tests for 'listchars' display with 'list' and :list +source check.vim source view_util.vim +source screendump.vim func Test_listchars() enew! @@ -517,4 +519,34 @@ func Test_listchars_window_local() set list& listchars& endfunc +func Test_listchars_foldcolumn() + CheckScreendump + + let lines =<< trim END + call setline(1, ['aaa', '', 'a', 'aaaaaa']) + vsplit + vsplit + windo set signcolumn=yes foldcolumn=1 winminwidth=0 nowrap list listchars=extends:>,precedes:< + END + call writefile(lines, 'XTest_listchars') + + let buf = RunVimInTerminal('-S XTest_listchars', {'rows': 10, 'cols': 60}) + + call term_sendkeys(buf, "13\<C-W>>") + call VerifyScreenDump(buf, 'Test_listchars_01', {}) + call term_sendkeys(buf, "\<C-W>>") + call VerifyScreenDump(buf, 'Test_listchars_02', {}) + call term_sendkeys(buf, "\<C-W>>") + call VerifyScreenDump(buf, 'Test_listchars_03', {}) + call term_sendkeys(buf, "\<C-W>>") + call VerifyScreenDump(buf, 'Test_listchars_04', {}) + call term_sendkeys(buf, "\<C-W>>") + call VerifyScreenDump(buf, 'Test_listchars_05', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XTest_listchars') +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_listdict.vim b/src/nvim/testdir/test_listdict.vim index f6c404d390..aa66d86af1 100644 --- a/src/nvim/testdir/test_listdict.vim +++ b/src/nvim/testdir/test_listdict.vim @@ -516,22 +516,22 @@ func Test_dict_lock_operator() endfunc " No remove() of write-protected scope-level variable -func! Tfunc(this_is_a_long_parameter_name) +func Tfunc1(this_is_a_long_parameter_name) call assert_fails("call remove(a:, 'this_is_a_long_parameter_name')", 'E742') -endfun +endfunc func Test_dict_scope_var_remove() - call Tfunc('testval') + call Tfunc1('testval') endfunc " No extend() of write-protected scope-level variable func Test_dict_scope_var_extend() call assert_fails("call extend(a:, {'this_is_a_long_parameter_name': 1234})", 'E742') endfunc -func! Tfunc(this_is_a_long_parameter_name) +func Tfunc2(this_is_a_long_parameter_name) call assert_fails("call extend(a:, {'this_is_a_long_parameter_name': 1234})", 'E742') endfunc func Test_dict_scope_var_extend_overwrite() - call Tfunc('testval') + call Tfunc2('testval') endfunc " No :unlet of variable in locked scope @@ -620,6 +620,49 @@ func Test_reverse_sort_uniq() call assert_fails('call reverse("")', 'E899:') endfunc +" reduce a list or a blob +func Test_reduce() + call assert_equal(1, reduce([], { acc, val -> acc + val }, 1)) + call assert_equal(10, reduce([1, 3, 5], { acc, val -> acc + val }, 1)) + call assert_equal(2 * (2 * ((2 * 1) + 2) + 3) + 4, reduce([2, 3, 4], { acc, val -> 2 * acc + val }, 1)) + call assert_equal('a x y z', ['x', 'y', 'z']->reduce({ acc, val -> acc .. ' ' .. val}, 'a')) + call assert_equal(#{ x: 1, y: 1, z: 1 }, ['x', 'y', 'z']->reduce({ acc, val -> extend(acc, { val: 1 }) }, {})) + call assert_equal([0, 1, 2, 3], reduce([1, 2, 3], function('add'), [0])) + + let l = ['x', 'y', 'z'] + call assert_equal(42, reduce(l, function('get'), #{ x: #{ y: #{ z: 42 } } })) + call assert_equal(['x', 'y', 'z'], l) + + call assert_equal(1, reduce([1], { acc, val -> acc + val })) + call assert_equal('x y z', reduce(['x', 'y', 'z'], { acc, val -> acc .. ' ' .. val })) + call assert_equal(120, range(1, 5)->reduce({ acc, val -> acc * val })) + call assert_fails("call reduce([], { acc, val -> acc + val })", 'E998: Reduce of an empty List with no initial value') + + call assert_equal(1, reduce(0z, { acc, val -> acc + val }, 1)) + call assert_equal(1 + 0xaf + 0xbf + 0xcf, reduce(0zAFBFCF, { acc, val -> acc + val }, 1)) + call assert_equal(2 * (2 * 1 + 0xaf) + 0xbf, 0zAFBF->reduce({ acc, val -> 2 * acc + val }, 1)) + + call assert_equal(0xff, reduce(0zff, { acc, val -> acc + val })) + call assert_equal(2 * (2 * 0xaf + 0xbf) + 0xcf, reduce(0zAFBFCF, { acc, val -> 2 * acc + val })) + call assert_fails("call reduce(0z, { acc, val -> acc + val })", 'E998: Reduce of an empty Blob with no initial value') + + call assert_fails("call reduce({}, { acc, val -> acc + val }, 1)", 'E897:') + call assert_fails("call reduce(0, { acc, val -> acc + val }, 1)", 'E897:') + call assert_fails("call reduce('', { acc, val -> acc + val }, 1)", 'E897:') + + let g:lut = [1, 2, 3, 4] + func EvilRemove() + call remove(g:lut, 1) + return 1 + endfunc + call assert_fails("call reduce(g:lut, { acc, val -> EvilRemove() }, 1)", 'E742:') + unlet g:lut + delfunc EvilRemove + + call assert_equal(42, reduce(v:_null_list, function('add'), 42)) + call assert_equal(42, reduce(v:_null_blob, function('add'), 42)) +endfunc + " splitting a string to a List func Test_str_split() call assert_equal(['aa', 'bb'], split(' aa bb ')) diff --git a/src/nvim/testdir/test_listlbr_utf8.vim b/src/nvim/testdir/test_listlbr_utf8.vim index c38e0c5f3c..1f100d6244 100644 --- a/src/nvim/testdir/test_listlbr_utf8.vim +++ b/src/nvim/testdir/test_listlbr_utf8.vim @@ -69,6 +69,16 @@ func Test_nolinebreak_with_list() call s:close_windows() endfunc +" this was causing a crash +func Test_linebreak_with_list_and_tabs() + set linebreak list listchars=tab:⇤\ ⇥ tabstop=100 + new + call setline(1, "\t\t\ttext") + redraw + bwipe! + set nolinebreak nolist listchars&vim tabstop=8 +endfunc + func Test_linebreak_with_nolist() call s:test_windows('setl nolist') call setline(1, "\t*mask = nil;") diff --git a/src/nvim/testdir/test_mapping.vim b/src/nvim/testdir/test_mapping.vim index f88e8cf843..1080a3c85b 100644 --- a/src/nvim/testdir/test_mapping.vim +++ b/src/nvim/testdir/test_mapping.vim @@ -1,6 +1,8 @@ " Tests for mappings and abbreviations source shared.vim +source check.vim +source screendump.vim func Test_abbreviation() " abbreviation with 0x80 should work @@ -451,6 +453,82 @@ func Test_expr_map_gets_cursor() nunmap ! endfunc +func Test_expr_map_restore_cursor() + CheckScreendump + + let lines =<< trim END + call setline(1, ['one', 'two', 'three']) + 2 + set ls=2 + hi! link StatusLine ErrorMsg + noremap <expr> <C-B> Func() + func Func() + let g:on = !get(g:, 'on', 0) + redraws + return '' + endfunc + func Status() + return get(g:, 'on', 0) ? '[on]' : '' + endfunc + set stl=%{Status()} + END + call writefile(lines, 'XtestExprMap') + let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10}) + call term_sendkeys(buf, "\<C-B>") + call VerifyScreenDump(buf, 'Test_map_expr_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestExprMap') +endfunc + +func Test_map_listing() + CheckScreendump + + let lines =<< trim END + nmap a b + END + call writefile(lines, 'XtestMapList') + let buf = RunVimInTerminal('-S XtestMapList', #{rows: 6}) + call term_sendkeys(buf, ": nmap a\<CR>") + call VerifyScreenDump(buf, 'Test_map_list_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestMapList') +endfunc + +func Test_expr_map_error() + CheckScreendump + + let lines =<< trim END + func Func() + throw 'test' + return '' + endfunc + + nnoremap <expr> <F2> Func() + cnoremap <expr> <F2> Func() + + call test_override('ui_delay', 10) + END + call writefile(lines, 'XtestExprMap') + let buf = RunVimInTerminal('-S XtestExprMap', #{rows: 10}) + call term_sendkeys(buf, "\<F2>") + call TermWait(buf) + call term_sendkeys(buf, "\<CR>") + call VerifyScreenDump(buf, 'Test_map_expr_2', {}) + + call term_sendkeys(buf, ":abc\<F2>") + call VerifyScreenDump(buf, 'Test_map_expr_3', {}) + call term_sendkeys(buf, "\<Esc>0") + call VerifyScreenDump(buf, 'Test_map_expr_4', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XtestExprMap') +endfunc + " Test for mapping errors func Test_map_error() call assert_fails('unmap', 'E474:') diff --git a/src/nvim/testdir/test_marks.vim b/src/nvim/testdir/test_marks.vim index b3035d73ce..00ee8f6d6a 100644 --- a/src/nvim/testdir/test_marks.vim +++ b/src/nvim/testdir/test_marks.vim @@ -1,6 +1,6 @@ " Test that a deleted mark is restored after delete-undo-redo-undo. -function! Test_Restore_DelMark() +func Test_Restore_DelMark() enew! call append(0, [" textline A", " textline B", " textline C"]) normal! 2gg @@ -11,10 +11,10 @@ function! Test_Restore_DelMark() call assert_equal(2, pos[1]) call assert_equal(1, pos[2]) enew! -endfunction +endfunc " Test that CTRL-A and CTRL-X updates last changed mark '[, ']. -function! Test_Incr_Marks() +func Test_Incr_Marks() enew! call append(0, ["123 123 123", "123 123 123", "123 123 123"]) normal! gg @@ -23,7 +23,17 @@ function! Test_Incr_Marks() call assert_equal("123 XXXXXXX", getline(2)) call assert_equal("XXX 123 123", getline(3)) enew! -endfunction +endfunc + +func Test_previous_jump_mark() + new + call setline(1, ['']->repeat(6)) + normal Ggg + call assert_equal(6, getpos("''")[1]) + normal jjjjj + call assert_equal(6, getpos("''")[1]) + bwipe! +endfunc func Test_setpos() new Xone @@ -207,6 +217,21 @@ func Test_mark_error() call assert_fails('mark _', 'E191:') endfunc +" Test for :lockmarks when pasting content +func Test_lockmarks_with_put() + new + call append(0, repeat(['sky is blue'], 4)) + normal gg + 1,2yank r + put r + normal G + lockmarks put r + call assert_equal(2, line("'[")) + call assert_equal(3, line("']")) + + bwipe! +endfunc + " Test for the getmarklist() function func Test_getmarklist() new @@ -231,3 +256,5 @@ func Test_getmarklist() call assert_equal([], {}->getmarklist()) close! endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_matchadd_conceal.vim b/src/nvim/testdir/test_matchadd_conceal.vim index 2cbaf5cb76..29a2c30b0d 100644 --- a/src/nvim/testdir/test_matchadd_conceal.vim +++ b/src/nvim/testdir/test_matchadd_conceal.vim @@ -7,7 +7,7 @@ source shared.vim source term_util.vim source view_util.vim -function! Test_simple_matchadd() +func Test_simple_matchadd() new 1put='# This is a Test' @@ -333,7 +333,7 @@ func Test_matchadd_and_syn_conceal() call assert_notequal(screenattr(1, 10) , screenattr(1, 11)) call assert_notequal(screenattr(1, 11) , screenattr(1, 12)) call assert_equal(screenattr(1, 11) , screenattr(1, 32)) -endfunction +endfunc func Test_cursor_column_in_concealed_line_after_window_scroll() CheckRunVimInTerminal diff --git a/src/nvim/testdir/test_matchadd_conceal_utf8.vim b/src/nvim/testdir/test_matchadd_conceal_utf8.vim index 7bfac13ad8..1d0c740734 100644 --- a/src/nvim/testdir/test_matchadd_conceal_utf8.vim +++ b/src/nvim/testdir/test_matchadd_conceal_utf8.vim @@ -3,19 +3,19 @@ if !has('conceal') finish endif -function! s:screenline(lnum) abort +func s:screenline(lnum) abort let line = [] for c in range(1, winwidth(0)) call add(line, nr2char(a:lnum->screenchar(c))) endfor return s:trim(join(line, '')) -endfunction +endfunc -function! s:trim(str) abort +func s:trim(str) abort return matchstr(a:str,'^\s*\zs.\{-}\ze\s*$') -endfunction +endfunc -function! Test_match_using_multibyte_conceal_char() +func Test_match_using_multibyte_conceal_char() new setlocal concealcursor=n conceallevel=1 diff --git a/src/nvim/testdir/test_matchfuzzy.vim b/src/nvim/testdir/test_matchfuzzy.vim new file mode 100644 index 0000000000..abcc9b40c1 --- /dev/null +++ b/src/nvim/testdir/test_matchfuzzy.vim @@ -0,0 +1,248 @@ +" Tests for fuzzy matching + +source shared.vim +source check.vim + +" Test for matchfuzzy() +func Test_matchfuzzy() + call assert_fails('call matchfuzzy(10, "abc")', 'E686:') + " Needs v8.2.1183; match the final error that's thrown for now + " call assert_fails('call matchfuzzy(["abc"], [])', 'E730:') + call assert_fails('call matchfuzzy(["abc"], [])', 'E475:') + call assert_fails("let x = matchfuzzy(v:_null_list, 'foo')", 'E686:') + call assert_fails('call matchfuzzy(["abc"], v:_null_string)', 'E475:') + call assert_equal([], matchfuzzy([], 'abc')) + call assert_equal([], matchfuzzy(['abc'], '')) + call assert_equal(['abc'], matchfuzzy(['abc', 10], 'ac')) + call assert_equal([], matchfuzzy([10, 20], 'ac')) + call assert_equal(['abc'], matchfuzzy(['abc'], 'abc')) + call assert_equal(['crayon', 'camera'], matchfuzzy(['camera', 'crayon'], 'cra')) + call assert_equal(['aabbaa', 'aaabbbaaa', 'aaaabbbbaaaa', 'aba'], matchfuzzy(['aba', 'aabbaa', 'aaabbbaaa', 'aaaabbbbaaaa'], 'aa')) + call assert_equal(['one'], matchfuzzy(['one', 'two'], 'one')) + call assert_equal(['oneTwo', 'onetwo'], matchfuzzy(['onetwo', 'oneTwo'], 'oneTwo')) + call assert_equal(['onetwo', 'one_two'], matchfuzzy(['onetwo', 'one_two'], 'oneTwo')) + call assert_equal(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], matchfuzzy(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], 'aa')) + call assert_equal(256, matchfuzzy([repeat('a', 256)], repeat('a', 256))[0]->len()) + call assert_equal([], matchfuzzy([repeat('a', 300)], repeat('a', 257))) + " matches with same score should not be reordered + let l = ['abc1', 'abc2', 'abc3'] + call assert_equal(l, l->matchfuzzy('abc')) + + " Tests for match preferences + " preference for camel case match + call assert_equal(['oneTwo', 'onetwo'], ['onetwo', 'oneTwo']->matchfuzzy('onetwo')) + " preference for match after a separator (_ or space) + call assert_equal(['onetwo', 'one_two', 'one two'], ['onetwo', 'one_two', 'one two']->matchfuzzy('onetwo')) + " preference for leading letter match + call assert_equal(['onetwo', 'xonetwo'], ['xonetwo', 'onetwo']->matchfuzzy('onetwo')) + " preference for sequential match + call assert_equal(['onetwo', 'oanbectdweo'], ['oanbectdweo', 'onetwo']->matchfuzzy('onetwo')) + " non-matching leading letter(s) penalty + call assert_equal(['xonetwo', 'xxonetwo'], ['xxonetwo', 'xonetwo']->matchfuzzy('onetwo')) + " total non-matching letter(s) penalty + call assert_equal(['one', 'onex', 'onexx'], ['onexx', 'one', 'onex']->matchfuzzy('one')) + " prefer complete matches over separator matches + call assert_equal(['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c'], ['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c']->matchfuzzy('vimrc')) + " gap penalty + call assert_equal(['xxayybxxxx', 'xxayyybxxx', 'xxayyyybxx'], ['xxayyyybxx', 'xxayyybxxx', 'xxayybxxxx']->matchfuzzy('ab')) + " path separator vs word separator + call assert_equal(['color/setup.vim', 'color\\setup.vim', 'color setup.vim', 'color_setup.vim', 'colorsetup.vim'], matchfuzzy(['colorsetup.vim', 'color setup.vim', 'color/setup.vim', 'color_setup.vim', 'color\\setup.vim'], 'setup.vim')) + + " match multiple words (separated by space) + call assert_equal(['foo bar baz'], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('baz foo')) + call assert_equal([], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('one two')) + call assert_equal([], ['foo bar']->matchfuzzy(" \t ")) + + " test for matching a sequence of words + call assert_equal(['bar foo'], ['foo bar', 'bar foo', 'foobar', 'barfoo']->matchfuzzy('bar foo', {'matchseq' : 1})) + call assert_equal([#{text: 'two one'}], [#{text: 'one two'}, #{text: 'two one'}]->matchfuzzy('two one', #{key: 'text', matchseq: v:true})) + + %bw! + eval ['somebuf', 'anotherone', 'needle', 'yetanotherone']->map({_, v -> bufadd(v) + bufload(v)}) + let l = getbufinfo()->map({_, v -> v.name})->matchfuzzy('ndl') + call assert_equal(1, len(l)) + call assert_match('needle', l[0]) + + " Test for fuzzy matching dicts + let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}] + call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'text_cb' : {v -> v.val}})) + call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'key' : 'val'})) + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> v.val}})) + call assert_equal([], matchfuzzy(l, 'day', {'key' : 'val'})) + call assert_fails("let x = matchfuzzy(l, 'cam', 'random')", 'E715:') + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> []}})) + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> 1}})) + call assert_fails("let x = matchfuzzy(l, 'day', {'text_cb' : {a, b -> 1}})", 'E119:') + call assert_equal([], matchfuzzy(l, 'cam')) + " Nvim's callback implementation is different, so E6000 is expected instead, + " but we need v8.2.1183 to assert it + " call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E921:') + " call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E6000:') + call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E475:') + " call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : []})", 'E730:') + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : []})", 'E475:') + call assert_fails("let x = matchfuzzy(l, 'cam', v:_null_dict)", 'E715:') + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : v:_null_string})", 'E475:') + " Nvim doesn't have null functions + " call assert_fails("let x = matchfuzzy(l, 'foo', {'text_cb' : test_null_function()})", 'E475:') + " matches with same score should not be reordered + let l = [#{text: 'abc', id: 1}, #{text: 'abc', id: 2}, #{text: 'abc', id: 3}] + call assert_equal(l, l->matchfuzzy('abc', #{key: 'text'})) + + let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : 'name'})", 'E730:') + + " Test in latin1 encoding + let save_enc = &encoding + " Nvim supports utf-8 encoding only + " set encoding=latin1 + call assert_equal(['abc'], matchfuzzy(['abc'], 'abc')) + let &encoding = save_enc +endfunc + +" Test for the matchfuzzypos() function +func Test_matchfuzzypos() + call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'curl'], 'rl')) + call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'one', 'curl'], 'rl')) + call assert_equal([['hello', 'hello world hello world'], + \ [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [275, 257]], + \ matchfuzzypos(['hello world hello world', 'hello', 'world'], 'hello')) + call assert_equal([['aaaaaaa'], [[0, 1, 2]], [191]], matchfuzzypos(['aaaaaaa'], 'aaa')) + call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b')) + call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b')) + call assert_equal([['a b'], [[0]], [112]], matchfuzzypos(['a b'], ' a ')) + call assert_equal([[], [], []], matchfuzzypos(['a b'], ' ')) + call assert_equal([[], [], []], matchfuzzypos(['world', 'curl'], 'ab')) + let x = matchfuzzypos([repeat('a', 256)], repeat('a', 256)) + call assert_equal(range(256), x[1][0]) + call assert_equal([[], [], []], matchfuzzypos([repeat('a', 300)], repeat('a', 257))) + call assert_equal([[], [], []], matchfuzzypos([], 'abc')) + + " match in a long string + call assert_equal([[repeat('x', 300) .. 'abc'], [[300, 301, 302]], [-135]], + \ matchfuzzypos([repeat('x', 300) .. 'abc'], 'abc')) + + " preference for camel case match + call assert_equal([['xabcxxaBc'], [[6, 7, 8]], [189]], matchfuzzypos(['xabcxxaBc'], 'abc')) + " preference for match after a separator (_ or space) + call assert_equal([['xabx_ab'], [[5, 6]], [145]], matchfuzzypos(['xabx_ab'], 'ab')) + " preference for leading letter match + call assert_equal([['abcxabc'], [[0, 1]], [150]], matchfuzzypos(['abcxabc'], 'ab')) + " preference for sequential match + call assert_equal([['aobncedone'], [[7, 8, 9]], [158]], matchfuzzypos(['aobncedone'], 'one')) + " best recursive match + call assert_equal([['xoone'], [[2, 3, 4]], [168]], matchfuzzypos(['xoone'], 'one')) + + " match multiple words (separated by space) + call assert_equal([['foo bar baz'], [[8, 9, 10, 0, 1, 2]], [369]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo')) + call assert_equal([[], [], []], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('one two')) + call assert_equal([[], [], []], ['foo bar']->matchfuzzypos(" \t ")) + call assert_equal([['grace'], [[1, 2, 3, 4, 2, 3, 4, 0, 1, 2, 3, 4]], [657]], ['grace']->matchfuzzypos('race ace grace')) + + let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}] + call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]], + \ matchfuzzypos(l, 'cam', {'text_cb' : {v -> v.val}})) + call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]], + \ matchfuzzypos(l, 'cam', {'key' : 'val'})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> v.val}})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'key' : 'val'})) + call assert_fails("let x = matchfuzzypos(l, 'cam', 'random')", 'E715:') + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> []}})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> 1}})) + call assert_fails("let x = matchfuzzypos(l, 'day', {'text_cb' : {a, b -> 1}})", 'E119:') + call assert_equal([[], [], []], matchfuzzypos(l, 'cam')) + " Nvim's callback implementation is different, so E6000 is expected instead, + " but we need v8.2.1183 to assert it + " call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E921:') + " call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E6000:') + call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E475:') + " call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : []})", 'E730:') + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : []})", 'E475:') + call assert_fails("let x = matchfuzzypos(l, 'cam', v:_null_dict)", 'E715:') + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : v:_null_string})", 'E475:') + " Nvim doesn't have null functions + " call assert_fails("let x = matchfuzzypos(l, 'foo', {'text_cb' : test_null_function()})", 'E475:') + + let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : 'name'})", 'E730:') +endfunc + +" Test for matchfuzzy() with multibyte characters +func Test_matchfuzzy_mbyte() + CheckFeature multi_lang + call assert_equal(['ンヹㄇヺヴ'], matchfuzzy(['ンヹㄇヺヴ'], 'ヹヺ')) + " reverse the order of characters + call assert_equal([], matchfuzzy(['ンヹㄇヺヴ'], 'ヺヹ')) + call assert_equal(['αβΩxxx', 'xαxβxΩx'], + \ matchfuzzy(['αβΩxxx', 'xαxβxΩx'], 'αβΩ')) + call assert_equal(['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'], + \ matchfuzzy(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ')) + + " match multiple words (separated by space) + call assert_equal(['세 마리의 작은 돼지'], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('돼지 마리의')) + call assert_equal([], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('파란 하늘')) + + " preference for camel case match + call assert_equal(['oneĄwo', 'oneąwo'], + \ ['oneąwo', 'oneĄwo']->matchfuzzy('oneąwo')) + " preference for complete match then match after separator (_ or space) + call assert_equal(['ⅠⅡabㄟㄠ'] + sort(['ⅠⅡa_bㄟㄠ', 'ⅠⅡa bㄟㄠ']), + \ ['ⅠⅡabㄟㄠ', 'ⅠⅡa bㄟㄠ', 'ⅠⅡa_bㄟㄠ']->matchfuzzy('ⅠⅡabㄟㄠ')) + " preference for match after a separator (_ or space) + call assert_equal(['ㄓㄔabㄟㄠ', 'ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ'], + \ ['ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ', 'ㄓㄔabㄟㄠ']->matchfuzzy('ㄓㄔabㄟㄠ')) + " preference for leading letter match + call assert_equal(['ŗŝţũŵż', 'xŗŝţũŵż'], + \ ['xŗŝţũŵż', 'ŗŝţũŵż']->matchfuzzy('ŗŝţũŵż')) + " preference for sequential match + call assert_equal(['ㄞㄡㄤfffifl', 'ㄞaㄡbㄤcffdfiefl'], + \ ['ㄞaㄡbㄤcffdfiefl', 'ㄞㄡㄤfffifl']->matchfuzzy('ㄞㄡㄤfffifl')) + " non-matching leading letter(s) penalty + call assert_equal(['xㄞㄡㄤfffifl', 'xxㄞㄡㄤfffifl'], + \ ['xxㄞㄡㄤfffifl', 'xㄞㄡㄤfffifl']->matchfuzzy('ㄞㄡㄤfffifl')) + " total non-matching letter(s) penalty + call assert_equal(['ŗŝţ', 'ŗŝţx', 'ŗŝţxx'], + \ ['ŗŝţxx', 'ŗŝţ', 'ŗŝţx']->matchfuzzy('ŗŝţ')) +endfunc + +" Test for matchfuzzypos() with multibyte characters +func Test_matchfuzzypos_mbyte() + CheckFeature multi_lang + call assert_equal([['こんにちは世界'], [[0, 1, 2, 3, 4]], [273]], + \ matchfuzzypos(['こんにちは世界'], 'こんにちは')) + call assert_equal([['ンヹㄇヺヴ'], [[1, 3]], [88]], matchfuzzypos(['ンヹㄇヺヴ'], 'ヹヺ')) + " reverse the order of characters + call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇヺヴ'], 'ヺヹ')) + call assert_equal([['αβΩxxx', 'xαxβxΩx'], [[0, 1, 2], [1, 3, 5]], [222, 113]], + \ matchfuzzypos(['αβΩxxx', 'xαxβxΩx'], 'αβΩ')) + call assert_equal([['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'], + \ [[0, 1], [0, 1], [0, 1], [0, 2]], [151, 148, 145, 110]], + \ matchfuzzypos(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ')) + call assert_equal([['ααααααα'], [[0, 1, 2]], [191]], + \ matchfuzzypos(['ααααααα'], 'ααα')) + + call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇ', 'ŗŝţ'], 'fffifl')) + let x = matchfuzzypos([repeat('Ψ', 256)], repeat('Ψ', 256)) + call assert_equal(range(256), x[1][0]) + call assert_equal([[], [], []], matchfuzzypos([repeat('✓', 300)], repeat('✓', 257))) + + " match multiple words (separated by space) + call assert_equal([['세 마리의 작은 돼지'], [[9, 10, 2, 3, 4]], [328]], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('돼지 마리의')) + call assert_equal([[], [], []], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('파란 하늘')) + + " match in a long string + call assert_equal([[repeat('ぶ', 300) .. 'ẼẼẼ'], [[300, 301, 302]], [-135]], + \ matchfuzzypos([repeat('ぶ', 300) .. 'ẼẼẼ'], 'ẼẼẼ')) + " preference for camel case match + call assert_equal([['xѳѵҁxxѳѴҁ'], [[6, 7, 8]], [189]], matchfuzzypos(['xѳѵҁxxѳѴҁ'], 'ѳѵҁ')) + " preference for match after a separator (_ or space) + call assert_equal([['xちだx_ちだ'], [[5, 6]], [145]], matchfuzzypos(['xちだx_ちだ'], 'ちだ')) + " preference for leading letter match + call assert_equal([['ѳѵҁxѳѵҁ'], [[0, 1]], [150]], matchfuzzypos(['ѳѵҁxѳѵҁ'], 'ѳѵ')) + " preference for sequential match + call assert_equal([['aンbヹcㄇdンヹㄇ'], [[7, 8, 9]], [158]], matchfuzzypos(['aンbヹcㄇdンヹㄇ'], 'ンヹㄇ')) + " best recursive match + call assert_equal([['xффйд'], [[2, 3, 4]], [168]], matchfuzzypos(['xффйд'], 'фйд')) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_messages.vim b/src/nvim/testdir/test_messages.vim index e0286548d9..9c84d77dd2 100644 --- a/src/nvim/testdir/test_messages.vim +++ b/src/nvim/testdir/test_messages.vim @@ -40,7 +40,7 @@ endfunc " indicator (e.g., "-- INSERT --") when ":stopinsert" is invoked. Message " output could then be disturbed when 'cmdheight' was greater than one. " This test ensures that the bugfix for this issue remains in place. -function! Test_stopinsert_does_not_break_message_output() +func Test_stopinsert_does_not_break_message_output() set cmdheight=2 redraw! @@ -55,7 +55,7 @@ function! Test_stopinsert_does_not_break_message_output() redraw! set cmdheight& -endfunction +endfunc func Test_message_completion() call feedkeys(":message \<C-A>\<C-B>\"\<CR>", 'tx') diff --git a/src/nvim/testdir/test_mksession.vim b/src/nvim/testdir/test_mksession.vim index 057895047d..a8e50af510 100644 --- a/src/nvim/testdir/test_mksession.vim +++ b/src/nvim/testdir/test_mksession.vim @@ -309,6 +309,31 @@ func Test_mksession_buffer_count() set nohidden endfunc +func Test_mksession_buffer_order() + %bwipe! + e Xfoo | e Xbar | e Xbaz | e Xqux + bufdo write + mksession! Xtest_mks.out + + " Verify that loading the session preserves order of buffers + %bwipe! + source Xtest_mks.out + + let s:buf_info = getbufinfo() + call assert_true(s:buf_info[0]['name'] =~# 'Xfoo$') + call assert_true(s:buf_info[1]['name'] =~# 'Xbar$') + call assert_true(s:buf_info[2]['name'] =~# 'Xbaz$') + call assert_true(s:buf_info[3]['name'] =~# 'Xqux$') + + " Clean up. + call delete('Xfoo') + call delete('Xbar') + call delete('Xbaz') + call delete('Xqux') + call delete('Xtest_mks.out') + %bwipe! +endfunc + if has('extra_search') func Test_mksession_hlsearch() @@ -696,6 +721,36 @@ func Test_mksession_foldopt() set sessionoptions& endfunc +" Test for mksession with "help" but not "options" in 'sessionoptions' +func Test_mksession_help_noopt() + set sessionoptions-=options + set sessionoptions+=help + help + let fname = expand('%') + mksession! Xtest_mks.out + bwipe + + source Xtest_mks.out + call assert_equal('help', &buftype) + call assert_equal('help', &filetype) + call assert_equal(fname, expand('%')) + call assert_false(&modifiable) + call assert_true(&readonly) + + helpclose + help index + let fname = expand('%') + mksession! Xtest_mks.out + bwipe + + source Xtest_mks.out + call assert_equal('help', &buftype) + call assert_equal(fname, expand('%')) + + call delete('Xtest_mks.out') + set sessionoptions& +endfunc + " Test for mksession with window position func Test_mksession_winpos() if !has('gui_running') diff --git a/src/nvim/testdir/test_normal.vim b/src/nvim/testdir/test_normal.vim index aff22f5d01..f45cd96733 100644 --- a/src/nvim/testdir/test_normal.vim +++ b/src/nvim/testdir/test_normal.vim @@ -1118,7 +1118,7 @@ func Test_normal20_exmode() endif call writefile(['1a', 'foo', 'bar', '.', 'w! Xfile2', 'q!'], 'Xscript') call writefile(['1', '2'], 'Xfile') - call system(v:progpath .' -e -s < Xscript Xfile') + call system(GetVimCommand() .. ' -e -s < Xscript Xfile') let a=readfile('Xfile2') call assert_equal(['1', 'foo', 'bar', '2'], a) @@ -1171,13 +1171,13 @@ func Test_normal22_zet() endfor call writefile(['1', '2'], 'Xfile_Test_normal22_zet') - let args = ' --headless -u NONE -N -U NONE -i NONE --noplugins' - call system(v:progpath . args . ' -c "%d" -c ":norm! ZZ" Xfile_Test_normal22_zet') + let args = ' -N -i NONE --noplugins -X --headless' + call system(GetVimCommand() .. args .. ' -c "%d" -c ":norm! ZZ" Xfile_Test_normal22_zet') let a = readfile('Xfile_Test_normal22_zet') call assert_equal([], a) " Test for ZQ call writefile(['1', '2'], 'Xfile_Test_normal22_zet') - call system(v:progpath . args . ' -c "%d" -c ":norm! ZQ" Xfile_Test_normal22_zet') + call system(GetVimCommand() . args . ' -c "%d" -c ":norm! ZQ" Xfile_Test_normal22_zet') let a = readfile('Xfile_Test_normal22_zet') call assert_equal(['1', '2'], a) @@ -1812,7 +1812,15 @@ fun! Test_normal33_g_cmd2() call assert_equal(87, col('.')) call assert_equal('E', getreg(0)) + " Test for gM with Tab characters + call setline('.', "\ta\tb\tc\td\te\tf") + norm! gMyl + call assert_equal(6, col('.')) + call assert_equal("c", getreg(0)) + " Test for g Ctrl-G + call setline('.', lineC) + norm! 60gMyl set ff=unix let a=execute(":norm! g\<c-g>") call assert_match('Col 87 of 144; Line 2 of 2; Word 1 of 1; Byte 88 of 146', a) @@ -2759,4 +2767,37 @@ func Test_normal_count_after_operator() bw! endfunc +func Test_normal_gj_on_extra_wide_char() + new | 25vsp + let text='1 foooooooo ar e inszwe1 foooooooo inszwei' . + \ ' i drei vier fünf sechs sieben acht un zehn elf zwöfl' . + \ ' dreizehn v ierzehn fünfzehn' + put =text + call cursor(2,1) + norm! gj + call assert_equal([0,2,25,0], getpos('.')) + bw! +endfunc + +func Test_normal_count_out_of_range() + new + call setline(1, 'text') + normal 44444444444| + call assert_equal(999999999, v:count) + normal 444444444444| + call assert_equal(999999999, v:count) + normal 4444444444444| + call assert_equal(999999999, v:count) + normal 4444444444444444444| + call assert_equal(999999999, v:count) + + normal 9y99999999| + call assert_equal(899999991, v:count) + normal 10y99999999| + call assert_equal(999999999, v:count) + normal 44444444444y44444444444| + call assert_equal(999999999, v:count) + bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_number.vim b/src/nvim/testdir/test_number.vim index d737ebe9f0..dfbdc0bffd 100644 --- a/src/nvim/testdir/test_number.vim +++ b/src/nvim/testdir/test_number.vim @@ -284,10 +284,10 @@ func Test_relativenumber_colors() " Default colors call VerifyScreenDump(buf, 'Test_relnr_colors_1', {}) - call term_sendkeys(buf, ":hi LineNrAbove ctermfg=blue\<CR>") + call term_sendkeys(buf, ":hi LineNrAbove ctermfg=blue\<CR>:\<CR>") call VerifyScreenDump(buf, 'Test_relnr_colors_2', {}) - call term_sendkeys(buf, ":hi LineNrBelow ctermfg=green\<CR>") + call term_sendkeys(buf, ":hi LineNrBelow ctermfg=green\<CR>:\<CR>") call VerifyScreenDump(buf, 'Test_relnr_colors_3', {}) call term_sendkeys(buf, ":hi clear LineNrAbove\<CR>") diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim index 5946732937..8612b7013b 100644 --- a/src/nvim/testdir/test_options.vim +++ b/src/nvim/testdir/test_options.vim @@ -22,16 +22,16 @@ func Test_whichwrap() call assert_equal('h', &whichwrap) set whichwrap& -endfunction +endfunc -function! Test_isfname() +func Test_isfname() " This used to cause Vim to access uninitialized memory. set isfname= call assert_equal("~X", expand("~X")) set isfname& -endfunction +endfunc -function Test_wildchar() +func Test_wildchar() " Empty 'wildchar' used to access invalid memory. call assert_fails('set wildchar=', 'E521:') call assert_fails('set wildchar=abc', 'E521:') @@ -42,7 +42,7 @@ function Test_wildchar() let a=execute('set wildchar?') call assert_equal("\n wildchar=<Esc>", a) set wildchar& -endfunction +endfunc func Test_wildoptions() set wildoptions= @@ -51,7 +51,7 @@ func Test_wildoptions() call assert_equal('tagfile', &wildoptions) endfunc -function! Test_options() +func Test_options_command() let caught = 'ok' try options @@ -88,9 +88,9 @@ function! Test_options() " close option-window close -endfunction +endfunc -function! Test_path_keep_commas() +func Test_path_keep_commas() " Test that changing 'path' keeps two commas. set path=foo,,bar set path-=bar @@ -98,7 +98,7 @@ function! Test_path_keep_commas() call assert_equal('foo,,bar', &path) set path& -endfunction +endfunc func Test_filetype_valid() set ft=valid_name @@ -259,6 +259,8 @@ func Test_set_errors() call assert_fails('set shiftwidth=-1', 'E487:') call assert_fails('set sidescroll=-1', 'E487:') call assert_fails('set tabstop=-1', 'E487:') + call assert_fails('set tabstop=10000', 'E474:') + call assert_fails('set tabstop=5500000000', 'E474:') call assert_fails('set textwidth=-1', 'E487:') call assert_fails('set timeoutlen=-1', 'E487:') call assert_fails('set updatecount=-1', 'E487:') @@ -312,21 +314,50 @@ func Test_set_errors() set modifiable& endfunc +func CheckWasSet(name) + let verb_cm = execute('verbose set ' .. a:name .. '?') + call assert_match('Last set from.*test_options.vim', verb_cm) +endfunc +func CheckWasNotSet(name) + let verb_cm = execute('verbose set ' .. a:name .. '?') + call assert_notmatch('Last set from', verb_cm) +endfunc + " Must be executed before other tests that set 'term'. func Test_000_term_option_verbose() if has('nvim') || has('gui_running') return endif - let verb_cm = execute('verbose set t_cm') - call assert_notmatch('Last set from', verb_cm) + + call CheckWasNotSet('t_cm') let term_save = &term set term=ansi - let verb_cm = execute('verbose set t_cm') - call assert_match('Last set from.*test_options.vim', verb_cm) + call CheckWasSet('t_cm') let &term = term_save endfunc +func Test_copy_context() + setlocal list + call CheckWasSet('list') + split + call CheckWasSet('list') + quit + setlocal nolist + + set ai + call CheckWasSet('ai') + set filetype=perl + call CheckWasSet('filetype') + set fo=tcroq + call CheckWasSet('fo') + + split Xsomebuf + call CheckWasSet('ai') + call CheckWasNotSet('filetype') + call CheckWasSet('fo') +endfunc + func Test_set_ttytype() " Nvim does not support 'ttytype'. if !has('nvim') && !has('gui_running') && has('unix') @@ -368,6 +399,13 @@ func Test_set_all() set tw& iskeyword& splitbelow& endfunc +func Test_set_one_column() + let out_mult = execute('set all')->split("\n") + let out_one = execute('set! all')->split("\n") + " one column should be two to four times as many lines + call assert_inrange(len(out_mult) * 2, len(out_mult) * 4, len(out_one)) +endfunc + func Test_set_values() " The file is only generated when running "make test" in the src directory. if filereadable('opt_test.vim') @@ -647,6 +685,19 @@ func Test_buftype() close! endfunc +" Test for the 'shellquote' option +func Test_shellquote() + CheckUnix + set shellquote=# + set verbose=20 + redir => v + silent! !echo Hello + redir END + set verbose& + set shellquote& + call assert_match(': "#echo Hello#"', v) +endfunc + " Test for setting option values using v:false and v:true func Test_opt_boolean() set number& diff --git a/src/nvim/testdir/test_packadd.vim b/src/nvim/testdir/test_packadd.vim new file mode 100644 index 0000000000..fcb8b8033b --- /dev/null +++ b/src/nvim/testdir/test_packadd.vim @@ -0,0 +1,361 @@ +" Tests for 'packpath' and :packadd + + +func SetUp() + let s:topdir = getcwd() . '/Xdir' + exe 'set packpath=' . s:topdir + let s:plugdir = s:topdir . '/pack/mine/opt/mytest' +endfunc + +func TearDown() + call delete(s:topdir, 'rf') +endfunc + +func Test_packadd() + if !exists('s:plugdir') + echomsg 'when running this test manually, call SetUp() first' + return + endif + + call mkdir(s:plugdir . '/plugin/also', 'p') + call mkdir(s:plugdir . '/ftdetect', 'p') + call mkdir(s:plugdir . '/after', 'p') + set rtp& + let rtp = &rtp + filetype on + + let rtp_entries = split(rtp, ',') + for entry in rtp_entries + if entry =~? '\<after\>' + let first_after_entry = entry + break + endif + endfor + + exe 'split ' . s:plugdir . '/plugin/test.vim' + call setline(1, 'let g:plugin_works = 42') + wq + + exe 'split ' . s:plugdir . '/plugin/also/loaded.vim' + call setline(1, 'let g:plugin_also_works = 77') + wq + + exe 'split ' . s:plugdir . '/ftdetect/test.vim' + call setline(1, 'let g:ftdetect_works = 17') + wq + + packadd mytest + + call assert_equal(42, g:plugin_works) + call assert_equal(77, g:plugin_also_works) + call assert_equal(17, g:ftdetect_works) + call assert_true(len(&rtp) > len(rtp)) + call assert_match('/testdir/Xdir/pack/mine/opt/mytest\($\|,\)', &rtp) + + let new_after = match(&rtp, '/testdir/Xdir/pack/mine/opt/mytest/after,') + let forwarded = substitute(first_after_entry, '\\', '[/\\\\]', 'g') + let old_after = match(&rtp, ',' . forwarded . '\>') + call assert_true(new_after > 0, 'rtp is ' . &rtp) + call assert_true(old_after > 0, 'match ' . forwarded . ' in ' . &rtp) + call assert_true(new_after < old_after, 'rtp is ' . &rtp) + + " NOTE: '/.../opt/myte' forwardly matches with '/.../opt/mytest' + call mkdir(fnamemodify(s:plugdir, ':h') . '/myte', 'p') + let rtp = &rtp + packadd myte + + " Check the path of 'myte' is added + call assert_true(len(&rtp) > len(rtp)) + call assert_match('/testdir/Xdir/pack/mine/opt/myte\($\|,\)', &rtp) + + " Check exception + call assert_fails("packadd directorynotfound", 'E919:') + call assert_fails("packadd", 'E471:') +endfunc + +func Test_packadd_start() + let plugdir = s:topdir . '/pack/mine/start/other' + call mkdir(plugdir . '/plugin', 'p') + set rtp& + let rtp = &rtp + filetype on + + exe 'split ' . plugdir . '/plugin/test.vim' + call setline(1, 'let g:plugin_works = 24') + wq + + packadd other + + call assert_equal(24, g:plugin_works) + call assert_true(len(&rtp) > len(rtp)) + call assert_match('/testdir/Xdir/pack/mine/start/other\($\|,\)', &rtp) +endfunc + +func Test_packadd_noload() + call mkdir(s:plugdir . '/plugin', 'p') + call mkdir(s:plugdir . '/syntax', 'p') + set rtp& + let rtp = &rtp + + exe 'split ' . s:plugdir . '/plugin/test.vim' + call setline(1, 'let g:plugin_works = 42') + wq + let g:plugin_works = 0 + + packadd! mytest + + call assert_true(len(&rtp) > len(rtp)) + call assert_match('testdir/Xdir/pack/mine/opt/mytest\($\|,\)', &rtp) + call assert_equal(0, g:plugin_works) + + " check the path is not added twice + let new_rtp = &rtp + packadd! mytest + call assert_equal(new_rtp, &rtp) +endfunc + +func Test_packadd_symlink_dir() + if !has('unix') + return + endif + let top2_dir = s:topdir . '/Xdir2' + let real_dir = s:topdir . '/Xsym' + call mkdir(real_dir, 'p') + exec "silent !ln -s Xsym" top2_dir + let &rtp = top2_dir . ',' . top2_dir . '/after' + let &packpath = &rtp + + let s:plugdir = top2_dir . '/pack/mine/opt/mytest' + call mkdir(s:plugdir . '/plugin', 'p') + + exe 'split ' . s:plugdir . '/plugin/test.vim' + call setline(1, 'let g:plugin_works = 44') + wq + let g:plugin_works = 0 + + packadd mytest + + " Must have been inserted in the middle, not at the end + call assert_match('/pack/mine/opt/mytest,', &rtp) + call assert_equal(44, g:plugin_works) + + " No change when doing it again. + let rtp_before = &rtp + packadd mytest + call assert_equal(rtp_before, &rtp) + + set rtp& + let rtp = &rtp + exec "silent !rm" top2_dir +endfunc + +func Test_packadd_symlink_dir2() + if !has('unix') + return + endif + let top2_dir = s:topdir . '/Xdir2' + let real_dir = s:topdir . '/Xsym/pack' + call mkdir(top2_dir, 'p') + call mkdir(real_dir, 'p') + let &rtp = top2_dir . ',' . top2_dir . '/after' + let &packpath = &rtp + + exec "silent !ln -s ../Xsym/pack" top2_dir . '/pack' + let s:plugdir = top2_dir . '/pack/mine/opt/mytest' + call mkdir(s:plugdir . '/plugin', 'p') + + exe 'split ' . s:plugdir . '/plugin/test.vim' + call setline(1, 'let g:plugin_works = 48') + wq + let g:plugin_works = 0 + + packadd mytest + + " Must have been inserted in the middle, not at the end + call assert_match('/Xdir2/pack/mine/opt/mytest,', &rtp) + call assert_equal(48, g:plugin_works) + + " No change when doing it again. + let rtp_before = &rtp + packadd mytest + call assert_equal(rtp_before, &rtp) + + set rtp& + let rtp = &rtp + exec "silent !rm" top2_dir . '/pack' + exec "silent !rmdir" top2_dir +endfunc + +" Check command-line completion for 'packadd' +func Test_packadd_completion() + let optdir1 = &packpath . '/pack/mine/opt' + let optdir2 = &packpath . '/pack/candidate/opt' + + call mkdir(optdir1 . '/pluginA', 'p') + call mkdir(optdir1 . '/pluginC', 'p') + call mkdir(optdir2 . '/pluginB', 'p') + call mkdir(optdir2 . '/pluginC', 'p') + + let li = [] + call feedkeys(":packadd \<Tab>')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":packadd " . repeat("\<Tab>", 2) . "')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":packadd " . repeat("\<Tab>", 3) . "')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":packadd " . repeat("\<Tab>", 4) . "')\<C-B>call add(li, '\<CR>", 'tx') + call assert_equal("packadd pluginA", li[0]) + call assert_equal("packadd pluginB", li[1]) + call assert_equal("packadd pluginC", li[2]) + call assert_equal("packadd ", li[3]) +endfunc + +func Test_packloadall() + " plugin foo with an autoload directory + let fooplugindir = &packpath . '/pack/mine/start/foo/plugin' + call mkdir(fooplugindir, 'p') + call writefile(['let g:plugin_foo_number = 1234', + \ 'let g:plugin_foo_auto = bbb#value', + \ 'let g:plugin_extra_auto = extra#value'], fooplugindir . '/bar.vim') + let fooautodir = &packpath . '/pack/mine/start/foo/autoload' + call mkdir(fooautodir, 'p') + call writefile(['let bar#value = 77'], fooautodir . '/bar.vim') + + " plugin aaa with an autoload directory + let aaaplugindir = &packpath . '/pack/mine/start/aaa/plugin' + call mkdir(aaaplugindir, 'p') + call writefile(['let g:plugin_aaa_number = 333', + \ 'let g:plugin_aaa_auto = bar#value'], aaaplugindir . '/bbb.vim') + let aaaautodir = &packpath . '/pack/mine/start/aaa/autoload' + call mkdir(aaaautodir, 'p') + call writefile(['let bbb#value = 55'], aaaautodir . '/bbb.vim') + + " plugin extra with only an autoload directory + let extraautodir = &packpath . '/pack/mine/start/extra/autoload' + call mkdir(extraautodir, 'p') + call writefile(['let extra#value = 99'], extraautodir . '/extra.vim') + + packloadall + call assert_equal(1234, g:plugin_foo_number) + call assert_equal(55, g:plugin_foo_auto) + call assert_equal(99, g:plugin_extra_auto) + call assert_equal(333, g:plugin_aaa_number) + call assert_equal(77, g:plugin_aaa_auto) + + " only works once + call writefile(['let g:plugin_bar_number = 4321'], fooplugindir . '/bar2.vim') + packloadall + call assert_false(exists('g:plugin_bar_number')) + + " works when ! used + packloadall! + call assert_equal(4321, g:plugin_bar_number) +endfunc + +func Test_helptags() + let docdir1 = &packpath . '/pack/mine/start/foo/doc' + let docdir2 = &packpath . '/pack/mine/start/bar/doc' + call mkdir(docdir1, 'p') + call mkdir(docdir2, 'p') + call writefile(['look here: *look-here*'], docdir1 . '/bar.txt') + call writefile(['look away: *look-away*'], docdir2 . '/foo.txt') + exe 'set rtp=' . &packpath . '/pack/mine/start/foo,' . &packpath . '/pack/mine/start/bar' + + helptags ALL + + let tags1 = readfile(docdir1 . '/tags') + call assert_match('look-here', tags1[0]) + let tags2 = readfile(docdir2 . '/tags') + call assert_match('look-away', tags2[0]) + + call assert_fails('helptags abcxyz', 'E150:') +endfunc + +func Test_colorscheme() + let colordirrun = &packpath . '/runtime/colors' + let colordirstart = &packpath . '/pack/mine/start/foo/colors' + let colordiropt = &packpath . '/pack/mine/opt/bar/colors' + call mkdir(colordirrun, 'p') + call mkdir(colordirstart, 'p') + call mkdir(colordiropt, 'p') + call writefile(['let g:found_one = 1'], colordirrun . '/one.vim') + call writefile(['let g:found_two = 1'], colordirstart . '/two.vim') + call writefile(['let g:found_three = 1'], colordiropt . '/three.vim') + exe 'set rtp=' . &packpath . '/runtime' + + colorscheme one + call assert_equal(1, g:found_one) + colorscheme two + call assert_equal(1, g:found_two) + colorscheme three + call assert_equal(1, g:found_three) +endfunc + +func Test_colorscheme_completion() + let colordirrun = &packpath . '/runtime/colors' + let colordirstart = &packpath . '/pack/mine/start/foo/colors' + let colordiropt = &packpath . '/pack/mine/opt/bar/colors' + call mkdir(colordirrun, 'p') + call mkdir(colordirstart, 'p') + call mkdir(colordiropt, 'p') + call writefile(['let g:found_one = 1'], colordirrun . '/one.vim') + call writefile(['let g:found_two = 1'], colordirstart . '/two.vim') + call writefile(['let g:found_three = 1'], colordiropt . '/three.vim') + exe 'set rtp=' . &packpath . '/runtime' + + let li=[] + call feedkeys(":colorscheme " . repeat("\<Tab>", 1) . "')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":colorscheme " . repeat("\<Tab>", 2) . "')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":colorscheme " . repeat("\<Tab>", 3) . "')\<C-B>call add(li, '\<CR>", 't') + call feedkeys(":colorscheme " . repeat("\<Tab>", 4) . "')\<C-B>call add(li, '\<CR>", 'tx') + call assert_equal("colorscheme one", li[0]) + call assert_equal("colorscheme three", li[1]) + call assert_equal("colorscheme two", li[2]) + call assert_equal("colorscheme ", li[3]) +endfunc + +func Test_runtime() + let rundir = &packpath . '/runtime/extra' + let startdir = &packpath . '/pack/mine/start/foo/extra' + let optdir = &packpath . '/pack/mine/opt/bar/extra' + call mkdir(rundir, 'p') + call mkdir(startdir, 'p') + call mkdir(optdir, 'p') + call writefile(['let g:sequence .= "run"'], rundir . '/bar.vim') + call writefile(['let g:sequence .= "start"'], startdir . '/bar.vim') + call writefile(['let g:sequence .= "foostart"'], startdir . '/foo.vim') + call writefile(['let g:sequence .= "opt"'], optdir . '/bar.vim') + call writefile(['let g:sequence .= "xxxopt"'], optdir . '/xxx.vim') + exe 'set rtp=' . &packpath . '/runtime' + + let g:sequence = '' + runtime extra/bar.vim + call assert_equal('run', g:sequence) + let g:sequence = '' + runtime START extra/bar.vim + call assert_equal('start', g:sequence) + let g:sequence = '' + runtime OPT extra/bar.vim + call assert_equal('opt', g:sequence) + let g:sequence = '' + runtime PACK extra/bar.vim + call assert_equal('start', g:sequence) + let g:sequence = '' + runtime! PACK extra/bar.vim + call assert_equal('startopt', g:sequence) + let g:sequence = '' + runtime PACK extra/xxx.vim + call assert_equal('xxxopt', g:sequence) + + let g:sequence = '' + runtime ALL extra/bar.vim + call assert_equal('run', g:sequence) + let g:sequence = '' + runtime ALL extra/foo.vim + call assert_equal('foostart', g:sequence) + let g:sequence = '' + runtime! ALL extra/xxx.vim + call assert_equal('xxxopt', g:sequence) + let g:sequence = '' + runtime! ALL extra/bar.vim + call assert_equal('runstartopt', g:sequence) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_profile.vim b/src/nvim/testdir/test_profile.vim index 4b0097617e..fdb6f13e2b 100644 --- a/src/nvim/testdir/test_profile.vim +++ b/src/nvim/testdir/test_profile.vim @@ -1,8 +1,9 @@ " Test Vim profiler -if !has('profile') - finish -endif +source check.vim +CheckFeature profile + +source shared.vim source screendump.vim func Test_profile_func() @@ -37,7 +38,7 @@ func Test_profile_func() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath + call system(GetVimCommand() \ . ' -es --clean' \ . ' -c "so Xprofile_func.vim"' \ . ' -c "qall!"') @@ -124,8 +125,8 @@ func Test_profile_func_with_ifelse() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommand() + \ . ' -es -i NONE --noplugin' \ . ' -c "profile start Xprofile_func.log"' \ . ' -c "profile func Foo*"' \ . ' -c "so Xprofile_func.vim"' @@ -237,8 +238,8 @@ func Test_profile_func_with_trycatch() [CODE] call writefile(lines, 'Xprofile_func.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommand() + \ . ' -es -i NONE --noplugin' \ . ' -c "profile start Xprofile_func.log"' \ . ' -c "profile func Foo*"' \ . ' -c "so Xprofile_func.vim"' @@ -324,8 +325,8 @@ func Test_profile_file() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath - \ . ' -es --clean' + call system(GetVimCommandClean() + \ . ' -es' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' \ . ' -c "so Xprofile_file.vim"' @@ -369,8 +370,8 @@ func Test_profile_file_with_cont() \ ] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath - \ . ' -es -u NONE -U NONE -i NONE --noplugin' + call system(GetVimCommandClean() + \ . ' -es' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' \ . ' -c "so Xprofile_file.vim"' @@ -427,7 +428,7 @@ func Test_profile_truncate_mbyte() \ ] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath + call system(GetVimCommandClean() \ . ' -es --cmd "set enc=utf-8"' \ . ' -c "profile start Xprofile_file.log"' \ . ' -c "profile file Xprofile_file.vim"' @@ -474,7 +475,7 @@ func Test_profdel_func() call Foo3() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q') + call system(GetVimCommandClean() . ' -es -c "so Xprofile_file.vim" -c q') call assert_equal(0, v:shell_error) let lines = readfile('Xprofile_file.log') @@ -509,7 +510,7 @@ func Test_profdel_star() call Foo() [CODE] call writefile(lines, 'Xprofile_file.vim') - call system(v:progpath . ' -es --clean -c "so Xprofile_file.vim" -c q') + call system(GetVimCommandClean() . ' -es -c "so Xprofile_file.vim" -c q') call assert_equal(0, v:shell_error) let lines = readfile('Xprofile_file.log') diff --git a/src/nvim/testdir/test_put.vim b/src/nvim/testdir/test_put.vim index f42b177c50..65232175c6 100644 --- a/src/nvim/testdir/test_put.vim +++ b/src/nvim/testdir/test_put.vim @@ -1,5 +1,7 @@ " Tests for put commands, e.g. ":put", "p", "gp", "P", "gP", etc. +source check.vim + func Test_put_block() new call feedkeys("i\<C-V>u2500\<CR>x\<ESC>", 'x') @@ -112,15 +114,92 @@ func Test_put_p_indent_visual() bwipe! endfunc +func Test_gp_with_count_leaves_cursor_at_end() + new + call setline(1, '<---->') + call setreg('@', "foo\nbar", 'c') + normal 1G3|3gp + call assert_equal([0, 4, 4, 0], getpos(".")) + call assert_equal(['<--foo', 'barfoo', 'barfoo', 'bar-->'], getline(1, '$')) + call assert_equal([0, 4, 3, 0], getpos("']")) + + bwipe! +endfunc + +func Test_p_with_count_leaves_mark_at_end() + new + call setline(1, '<---->') + call setreg('@', "start\nend", 'c') + normal 1G3|3p + call assert_equal([0, 1, 4, 0], getpos(".")) + call assert_equal(['<--start', 'endstart', 'endstart', 'end-->'], getline(1, '$')) + call assert_equal([0, 4, 3, 0], getpos("']")) + + bwipe! +endfunc + +func Test_very_large_count() + new + " total put-length (21474837 * 100) brings 32 bit int overflow + let @" = repeat('x', 100) + call assert_fails('norm 21474837p', 'E1240:') + bwipe! +endfunc + +func Test_very_large_count_64bit() + throw 'Skipped: v:sizeoflong is N/A' " use legacy/put_spec.lua instead + + if v:sizeoflong < 8 + throw 'Skipped: only works with 64 bit long ints' + endif + + new + let @" = repeat('x', 100) + call assert_fails('norm 999999999p', 'E1240:') + bwipe! +endfunc + +func Test_very_large_count_block() + new + " total put-length (21474837 * 100) brings 32 bit int overflow + call setline(1, repeat('x', 100)) + exe "norm \<C-V>99ly" + call assert_fails('norm 21474837p', 'E1240:') + bwipe! +endfunc + +func Test_very_large_count_block_64bit() + throw 'Skipped: v:sizeoflong is N/A' " use legacy/put_spec.lua instead + + if v:sizeoflong < 8 + throw 'Skipped: only works with 64 bit long ints' + endif + + new + call setline(1, repeat('x', 100)) + exe "norm \<C-V>$y" + call assert_fails('norm 999999999p', 'E1240:') + bwipe! +endfunc + +func Test_put_above_first_line() + new + let @" = 'text' + silent! normal 0o00 + 0put + call assert_equal('text', getline(1)) + bwipe! +endfunc + func Test_multibyte_op_end_mark() - new - call setline(1, 'тест') - normal viwdp - call assert_equal([0, 1, 7, 0], getpos("'>")) - call assert_equal([0, 1, 7, 0], getpos("']")) - - normal Vyp - call assert_equal([0, 1, 2147483647, 0], getpos("'>")) - call assert_equal([0, 2, 7, 0], getpos("']")) - bwipe! - endfunc + new + call setline(1, 'тест') + normal viwdp + call assert_equal([0, 1, 7, 0], getpos("'>")) + call assert_equal([0, 1, 7, 0], getpos("']")) + + normal Vyp + call assert_equal([0, 1, 2147483647, 0], getpos("'>")) + call assert_equal([0, 2, 7, 0], getpos("']")) + bwipe! +endfunc diff --git a/src/nvim/testdir/test_python3.vim b/src/nvim/testdir/test_python3.vim index 54da3d2eba..f6a1942e24 100644 --- a/src/nvim/testdir/test_python3.vim +++ b/src/nvim/testdir/test_python3.vim @@ -69,6 +69,7 @@ func Test_vim_function() endfunc func Test_skipped_python3_command_does_not_affect_pyxversion() + throw 'skipped: Nvim hardcodes pyxversion=3' set pyxversion=0 if 0 python3 import vim diff --git a/src/nvim/testdir/test_pyx2.vim b/src/nvim/testdir/test_pyx2.vim index b6ed80f842..6a8ebf3da0 100644 --- a/src/nvim/testdir/test_pyx2.vim +++ b/src/nvim/testdir/test_pyx2.vim @@ -1,9 +1,9 @@ " Test for pyx* commands and functions with Python 2. -set pyx=2 if !has('python') finish endif +set pyx=2 let s:py2pattern = '^2\.[0-7]\.\d\+' let s:py3pattern = '^3\.\d\+\.\d\+' diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim index 6db679c5f9..6852f53ea8 100644 --- a/src/nvim/testdir/test_quickfix.vim +++ b/src/nvim/testdir/test_quickfix.vim @@ -32,7 +32,7 @@ func s:setup_commands(cchar) command! -count -nargs=* -bang Xnfile <mods><count>cnfile<bang> <args> command! -nargs=* -bang Xpfile <mods>cpfile<bang> <args> command! -nargs=* Xexpr <mods>cexpr <args> - command! -count -nargs=* Xvimgrep <mods> <count>vimgrep <args> + command! -count=999 -nargs=* Xvimgrep <mods> <count>vimgrep <args> command! -nargs=* Xvimgrepadd <mods> vimgrepadd <args> command! -nargs=* Xgrep <mods> grep <args> command! -nargs=* Xgrepadd <mods> grepadd <args> @@ -69,7 +69,7 @@ func s:setup_commands(cchar) command! -count -nargs=* -bang Xnfile <mods><count>lnfile<bang> <args> command! -nargs=* -bang Xpfile <mods>lpfile<bang> <args> command! -nargs=* Xexpr <mods>lexpr <args> - command! -count -nargs=* Xvimgrep <mods> <count>lvimgrep <args> + command! -count=999 -nargs=* Xvimgrep <mods> <count>lvimgrep <args> command! -nargs=* Xvimgrepadd <mods> lvimgrepadd <args> command! -nargs=* Xgrep <mods> lgrep <args> command! -nargs=* Xgrepadd <mods> lgrepadd <args> @@ -1384,6 +1384,29 @@ func Test_efm_error_type() let &efm = save_efm endfunc +" Test for end_lnum ('%e') and end_col ('%k') fields in 'efm' +func Test_efm_end_lnum_col() + let save_efm = &efm + + " single line + set efm=%f:%l-%e:%c-%k:%t:%m + cexpr ["Xfile1:10-20:1-2:E:msg1", "Xfile1:20-30:2-3:W:msg2",] + let output = split(execute('clist'), "\n") + call assert_equal([ + \ ' 1 Xfile1:10-20 col 1-2 error: msg1', + \ ' 2 Xfile1:20-30 col 2-3 warning: msg2'], output) + + " multiple lines + set efm=%A%n)%m,%Z%f:%l-%e:%c-%k + cexpr ["1)msg1", "Xfile1:14-24:1-2", + \ "2)msg2", "Xfile1:24-34:3-4"] + let output = split(execute('clist'), "\n") + call assert_equal([ + \ ' 1 Xfile1:14-24 col 1-2 error 1: msg1', + \ ' 2 Xfile1:24-34 col 3-4 error 2: msg2'], output) + let &efm = save_efm +endfunc + func XquickfixChangedByAutocmd(cchar) call s:setup_commands(a:cchar) if a:cchar == 'c' @@ -1914,6 +1937,7 @@ func Test_switchbuf() " If opening a file changes 'switchbuf', then the new value should be " retained. + set modeline&vim call writefile(["vim: switchbuf=split"], 'Xqftestfile1') enew | only set switchbuf&vim @@ -2715,7 +2739,7 @@ func Test_cwindow_jump() call assert_true(winnr('$') == 2) call assert_true(winnr() == 1) - " Jumping to a file from the location list window should find a usuable + " Jumping to a file from the location list window should find a usable " window by wrapping around the window list. enew | only call setloclist(0, [], 'f') @@ -5027,6 +5051,52 @@ func Test_qfbuf_update() call Xqfbuf_update('l') endfunc +" Test for the :vimgrep 'f' flag (fuzzy match) +func Xvimgrep_fuzzy_match(cchar) + call s:setup_commands(a:cchar) + + Xvimgrep /three one/f Xfile* + let l = g:Xgetlist() + call assert_equal(2, len(l)) + call assert_equal(['Xfile1', 1, 9, 'one two three'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile2', 2, 1, 'three one two'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + + Xvimgrep /the/f Xfile* + let l = g:Xgetlist() + call assert_equal(3, len(l)) + call assert_equal(['Xfile1', 1, 9, 'one two three'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile2', 2, 1, 'three one two'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + call assert_equal(['Xfile2', 4, 4, 'aaathreeaaa'], + \ [bufname(l[2].bufnr), l[2].lnum, l[2].col, l[2].text]) + + Xvimgrep /aaa/fg Xfile* + let l = g:Xgetlist() + call assert_equal(4, len(l)) + call assert_equal(['Xfile1', 2, 1, 'aaaaaa'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile1', 2, 4, 'aaaaaa'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + call assert_equal(['Xfile2', 4, 1, 'aaathreeaaa'], + \ [bufname(l[2].bufnr), l[2].lnum, l[2].col, l[2].text]) + call assert_equal(['Xfile2', 4, 9, 'aaathreeaaa'], + \ [bufname(l[3].bufnr), l[3].lnum, l[3].col, l[3].text]) + + call assert_fails('Xvimgrep /xyz/fg Xfile*', 'E480:') +endfunc + +func Test_vimgrep_fuzzy_match() + call writefile(['one two three', 'aaaaaa'], 'Xfile1') + call writefile(['one', 'three one two', 'two', 'aaathreeaaa'], 'Xfile2') + call Xvimgrep_fuzzy_match('c') + call Xvimgrep_fuzzy_match('l') + call delete('Xfile1') + call delete('Xfile2') +endfunc + " Test for getting a specific item from a quickfix list func Xtest_getqflist_by_idx(cchar) call s:setup_commands(a:cchar) diff --git a/src/nvim/testdir/test_random.vim b/src/nvim/testdir/test_random.vim new file mode 100644 index 0000000000..6d3f7dcfd9 --- /dev/null +++ b/src/nvim/testdir/test_random.vim @@ -0,0 +1,51 @@ +" Tests for srand() and rand() + +func Test_Rand() + let r = srand(123456789) + call assert_equal([1573771921, 319883699, 2742014374, 1324369493], r) + call assert_equal(4284103975, rand(r)) + call assert_equal(1001954530, rand(r)) + call assert_equal(2701803082, rand(r)) + call assert_equal(2658065534, rand(r)) + call assert_equal(3104308804, rand(r)) + + " Nvim does not support test_settime + " call test_settime(12341234) + let s = srand() + if !has('win32') && filereadable('/dev/urandom') + " using /dev/urandom + call assert_notequal(s, srand()) + " else + " " using time() + " call assert_equal(s, srand()) + " call test_settime(12341235) + " call assert_notequal(s, srand()) + endif + + " Nvim does not support test_srand_seed + " call test_srand_seed(123456789) + " call assert_equal(4284103975, rand()) + " call assert_equal(1001954530, rand()) + " call test_srand_seed() + + if has('float') + call assert_fails('echo srand(1.2)', 'E805:') + endif + call assert_fails('echo srand([1])', 'E745:') + call assert_fails('echo rand("burp")', 'E475:') + call assert_fails('echo rand([1, 2, 3])', 'E475:') + call assert_fails('echo rand([[1], 2, 3, 4])', 'E475:') + call assert_fails('echo rand([1, [2], 3, 4])', 'E475:') + call assert_fails('echo rand([1, 2, [3], 4])', 'E475:') + call assert_fails('echo rand([1, 2, 3, [4]])', 'E475:') + + " call test_settime(0) +endfunc + +func Test_issue_5587() + call rand() + call garbagecollect() + call rand() +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_regexp_latin.vim b/src/nvim/testdir/test_regexp_latin.vim index 712f1e6025..a92f7e1192 100644 --- a/src/nvim/testdir/test_regexp_latin.vim +++ b/src/nvim/testdir/test_regexp_latin.vim @@ -787,4 +787,12 @@ func Test_regexp_error() set re& endfunc +func Test_using_mark_position() + " this was using freed memory + new + norm O0 + call assert_fails("s/\\%')", 'E486:') + bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_regexp_utf8.vim b/src/nvim/testdir/test_regexp_utf8.vim index d8d5797dcf..b640a6d043 100644 --- a/src/nvim/testdir/test_regexp_utf8.vim +++ b/src/nvim/testdir/test_regexp_utf8.vim @@ -152,9 +152,6 @@ func s:classes_test() if has('win32') let identchars_ok = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
¡¢£¤¥¦§µÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' let kwordchars_ok = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzµÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' - elseif has('ebcdic') - let identchars_ok = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz¬®µº¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' - let kwordchars_ok = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz¬®µº¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' else let identchars_ok = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzµÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' let kwordchars_ok = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzµÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' @@ -166,8 +163,6 @@ func s:classes_test() let fnamechars_ok = '$+,-./0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~ ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' elseif has('vms') let fnamechars_ok = '#$%+,-./0123456789:;<>ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~ ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' - elseif has('ebcdic') - let fnamechars_ok = '#$%+,-./=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~ ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' else let fnamechars_ok = '#$%+,-./0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~ ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' endif @@ -590,4 +585,12 @@ func Test_match_char_class_upper() bwipe! endfunc +func Test_match_invalid_byte() + call writefile(0z630a.765d30aa0a.2e0a.790a.4030, 'Xinvalid') + new + source Xinvalid + bwipe! + call delete('Xinvalid') +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_registers.vim b/src/nvim/testdir/test_registers.vim index 84a5aca3d5..23e39eba35 100644 --- a/src/nvim/testdir/test_registers.vim +++ b/src/nvim/testdir/test_registers.vim @@ -1,6 +1,4 @@ -" " Tests for register operations -" source check.vim source view_util.vim @@ -13,6 +11,8 @@ func Test_aaa_empty_reg_test() call assert_fails('normal @!', 'E354:') call assert_fails('normal @:', 'E30:') call assert_fails('normal @.', 'E29:') + call assert_fails('put /', 'E35:') + call assert_fails('put .', 'E29:') endfunc func Test_yank_shows_register() @@ -119,6 +119,17 @@ func Test_recording_esc_sequence() endif endfunc +func Test_recording_with_select_mode() + new + call feedkeys("qacc12345\<Esc>gH98765\<Esc>q", "tx") + call assert_equal("98765", getline(1)) + call assert_equal("cc12345\<Esc>gH98765\<Esc>", @a) + call setline(1, 'asdf') + normal! @a + call assert_equal("98765", getline(1)) + bwipe! +endfunc + " Test for executing the last used register (@) func Test_last_used_exec_reg() " Test for the @: command @@ -141,6 +152,14 @@ func Test_last_used_exec_reg() normal @@ call assert_equal('EditEdit', a) + " Test for repeating the last command-line in visual mode + call append(0, 'register') + normal gg + let @r = '' + call feedkeys("v:yank R\<CR>", 'xt') + call feedkeys("v@:", 'xt') + call assert_equal("\nregister\nregister\n", @r) + enew! endfunc @@ -164,6 +183,19 @@ func Test_get_register() call assert_equal('', getregtype('!')) + " Test for inserting an invalid register content + call assert_beeps('exe "normal i\<C-R>!"') + + " Test for inserting a register with multiple lines + call deletebufline('', 1, '$') + call setreg('r', ['a', 'b']) + exe "normal i\<C-R>r" + call assert_equal(['a', 'b', ''], getline(1, '$')) + + " Test for inserting a multi-line register in the command line + call feedkeys(":\<C-R>r\<Esc>", 'xt') + call assert_equal("a\rb", histget(':', -1)) " Modified because of #6137 + enew! endfunc @@ -187,9 +219,200 @@ func Test_set_register() call setreg('=', 'b', 'a') call assert_equal('regwrite', getreg('=')) + " Test for setting a list of lines to special registers + call setreg('/', []) + call assert_equal('', @/) + call setreg('=', []) + call assert_equal('', @=) + call assert_fails("call setreg('/', ['a', 'b'])", 'E883:') + call assert_fails("call setreg('=', ['a', 'b'])", 'E883:') + call assert_equal(0, setreg('_', ['a', 'b'])) + + " Test for recording to a invalid register + call assert_beeps('normal q$') + + " Appending to a register when recording + call append(0, "text for clipboard test") + normal gg + call feedkeys('qrllq', 'xt') + call feedkeys('qRhhq', 'xt') + call assert_equal('llhh', getreg('r')) + + " Appending a list of characters to a register from different lines + let @r = '' + call append(0, ['abcdef', '123456']) + normal gg"ry3l + call cursor(2, 4) + normal "Ry3l + call assert_equal('abc456', @r) + + " Test for gP with multiple lines selected using characterwise motion + %delete + call append(0, ['vim editor', 'vim editor']) + let @r = '' + exe "normal ggwy/vim /e\<CR>gP" + call assert_equal(['vim editor', 'vim editor', 'vim editor'], getline(1, 3)) + + " Test for gP with . register + %delete + normal iabc + normal ".gp + call assert_equal('abcabc', getline(1)) + normal 0".gP + call assert_equal('abcabcabc', getline(1)) + enew! endfunc +" Test for clipboard registers (* and +) +func Test_clipboard_regs() + throw 'skipped: needs clipboard=autoselect,autoselectplus' + + CheckNotGui + CheckFeature clipboard_working + + new + call append(0, "text for clipboard test") + normal gg"*yiw + call assert_equal('text', getreg('*')) + normal gg2w"+yiw + call assert_equal('clipboard', getreg('+')) + + " Test for replacing the clipboard register contents + set clipboard=unnamed + let @* = 'food' + normal ggviw"*p + call assert_equal('text', getreg('*')) + call assert_equal('food for clipboard test', getline(1)) + normal ggviw"*p + call assert_equal('food', getreg('*')) + call assert_equal('text for clipboard test', getline(1)) + + " Test for replacing the selection register contents + set clipboard=unnamedplus + let @+ = 'food' + normal ggviw"+p + call assert_equal('text', getreg('+')) + call assert_equal('food for clipboard test', getline(1)) + normal ggviw"+p + call assert_equal('food', getreg('+')) + call assert_equal('text for clipboard test', getline(1)) + + " Test for auto copying visually selected text to clipboard register + call setline(1, "text for clipboard test") + let @* = '' + set clipboard=autoselect + normal ggwwviwy + call assert_equal('clipboard', @*) + + " Test for auto copying visually selected text to selection register + let @+ = '' + set clipboard=autoselectplus + normal ggwviwy + call assert_equal('for', @+) + + set clipboard&vim + bwipe! +endfunc + +" Test for restarting the current mode (insert or virtual replace) after +" executing the contents of a register +func Test_put_reg_restart_mode() + new + call append(0, 'editor') + normal gg + let @r = "ivim \<Esc>" + call feedkeys("i\<C-O>@r\<C-R>=mode()\<CR>", 'xt') + call assert_equal('vimi editor', getline(1)) + + call setline(1, 'editor') + normal gg + call feedkeys("gR\<C-O>@r\<C-R>=mode()\<CR>", 'xt') + call assert_equal('vimReditor', getline(1)) + + bwipe! +endfunc + +" Test for getting register info +func Test_get_reginfo() + enew + call setline(1, ['foo', 'bar']) + + exe 'norm! "zyy' + let info = getreginfo('"') + call assert_equal('z', info.points_to) + call setreg('y', 'baz') + call assert_equal('z', getreginfo('').points_to) + call setreg('y', { 'isunnamed': v:true }) + call assert_equal('y', getreginfo('"').points_to) + + exe '$put' + call assert_equal(getreg('y'), getline(3)) + call setreg('', 'qux') + call assert_equal('0', getreginfo('').points_to) + call setreg('x', 'quux') + call assert_equal('0', getreginfo('').points_to) + + let info = getreginfo('') + call assert_equal(getreg('', 1, 1), info.regcontents) + call assert_equal(getregtype(''), info.regtype) + + exe "norm! 0\<c-v>e" .. '"zy' + let info = getreginfo('z') + call assert_equal(getreg('z', 1, 1), info.regcontents) + call assert_equal(getregtype('z'), info.regtype) + call assert_equal(1, +info.isunnamed) + + let info = getreginfo('"') + call assert_equal('z', info.points_to) + + bwipe! +endfunc + +" Test for restoring register with dict from getreginfo +func Test_set_register_dict() + enew! + + call setreg('"', #{ regcontents: ['one', 'two'], + \ regtype: 'V', points_to: 'z' }) + call assert_equal(['one', 'two'], getreg('"', 1, 1)) + let info = getreginfo('"') + call assert_equal('z', info.points_to) + call assert_equal('V', info.regtype) + call assert_equal(1, +getreginfo('z').isunnamed) + + call setreg('x', #{ regcontents: ['three', 'four'], + \ regtype: 'v', isunnamed: v:true }) + call assert_equal(['three', 'four'], getreg('"', 1, 1)) + let info = getreginfo('"') + call assert_equal('x', info.points_to) + call assert_equal('v', info.regtype) + call assert_equal(1, +getreginfo('x').isunnamed) + + call setreg('y', #{ regcontents: 'five', + \ regtype: "\<c-v>", isunnamed: v:false }) + call assert_equal("\<c-v>4", getreginfo('y').regtype) + call assert_equal(0, +getreginfo('y').isunnamed) + call assert_equal(['three', 'four'], getreg('"', 1, 1)) + call assert_equal('x', getreginfo('"').points_to) + + call setreg('"', #{ regcontents: 'six' }) + call assert_equal('0', getreginfo('"').points_to) + call assert_equal(1, +getreginfo('0').isunnamed) + call assert_equal(['six'], getreginfo('0').regcontents) + call assert_equal(['six'], getreginfo('"').regcontents) + + let @x = 'one' + call setreg('x', {}) + call assert_equal(1, len(split(execute('reg x'), '\n'))) + + call assert_fails("call setreg('0', #{regtype: 'V'}, 'v')", 'E118:') + call assert_fails("call setreg('0', #{regtype: 'X'})", 'E475:') + call assert_fails("call setreg('0', #{regtype: 'vy'})", 'E475:') + + bwipe! +endfunc + func Test_v_register() enew call setline(1, 'nothing') @@ -288,84 +511,25 @@ func Test_insert_small_delete() bwipe! endfunc -" Test for getting register info -func Test_get_reginfo() - enew - call setline(1, ['foo', 'bar']) - - exe 'norm! "zyy' - let info = getreginfo('"') - call assert_equal('z', info.points_to) - call setreg('y', 'baz') - call assert_equal('z', getreginfo('').points_to) - call setreg('y', { 'isunnamed': v:true }) - call assert_equal('y', getreginfo('"').points_to) - - exe '$put' - call assert_equal(getreg('y'), getline(3)) - call setreg('', 'qux') - call assert_equal('0', getreginfo('').points_to) - call setreg('x', 'quux') - call assert_equal('0', getreginfo('').points_to) - - let info = getreginfo('') - call assert_equal(getreg('', 1, 1), info.regcontents) - call assert_equal(getregtype(''), info.regtype) - - exe "norm! 0\<c-v>e" .. '"zy' - let info = getreginfo('z') - call assert_equal(getreg('z', 1, 1), info.regcontents) - call assert_equal(getregtype('z'), info.regtype) - call assert_equal(1, +info.isunnamed) - - let info = getreginfo('"') - call assert_equal('z', info.points_to) +func Test_record_in_select_mode() + new + call setline(1, 'text') + sil norm q00 + sil norm q + call assert_equal('0ext', getline(1)) + + %delete + let @r = '' + call setline(1, ['abc', 'abc', 'abc']) + smap <F2> <Right><Right>, + call feedkeys("qrgh\<F2>Dk\<Esc>q", 'xt') + call assert_equal("gh\<F2>Dk\<Esc>", @r) + norm j0@rj0@@ + call assert_equal([',Dk', ',Dk', ',Dk'], getline(1, 3)) + sunmap <F2> bwipe! endfunc -" Test for restoring register with dict from getreginfo -func Test_set_register_dict() - enew! - - call setreg('"', #{ regcontents: ['one', 'two'], - \ regtype: 'V', points_to: 'z' }) - call assert_equal(['one', 'two'], getreg('"', 1, 1)) - let info = getreginfo('"') - call assert_equal('z', info.points_to) - call assert_equal('V', info.regtype) - call assert_equal(1, +getreginfo('z').isunnamed) - - call setreg('x', #{ regcontents: ['three', 'four'], - \ regtype: 'v', isunnamed: v:true }) - call assert_equal(['three', 'four'], getreg('"', 1, 1)) - let info = getreginfo('"') - call assert_equal('x', info.points_to) - call assert_equal('v', info.regtype) - call assert_equal(1, +getreginfo('x').isunnamed) - - call setreg('y', #{ regcontents: 'five', - \ regtype: "\<c-v>", isunnamed: v:false }) - call assert_equal("\<c-v>4", getreginfo('y').regtype) - call assert_equal(0, +getreginfo('y').isunnamed) - call assert_equal(['three', 'four'], getreg('"', 1, 1)) - call assert_equal('x', getreginfo('"').points_to) - - call setreg('"', #{ regcontents: 'six' }) - call assert_equal('0', getreginfo('"').points_to) - call assert_equal(1, +getreginfo('0').isunnamed) - call assert_equal(['six'], getreginfo('0').regcontents) - call assert_equal(['six'], getreginfo('"').regcontents) - - let @x = 'one' - call setreg('x', {}) - call assert_equal(1, len(split(execute('reg x'), '\n'))) - - call assert_fails("call setreg('0', #{regtype: 'V'}, 'v')", 'E118:') - call assert_fails("call setreg('0', #{regtype: 'X'})", 'E475:') - call assert_fails("call setreg('0', #{regtype: 'vy'})", 'E475:') - - bwipe! -endfunc " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_rename.vim b/src/nvim/testdir/test_rename.vim index 3887fcfabf..5359b84923 100644 --- a/src/nvim/testdir/test_rename.vim +++ b/src/nvim/testdir/test_rename.vim @@ -1,5 +1,7 @@ " Test rename() +source shared.vim + func Test_rename_file_to_file() call writefile(['foo'], 'Xrename1') @@ -81,7 +83,7 @@ func Test_rename_copy() call assert_equal(0, rename('Xrenamedir/Xrenamefile', 'Xrenamefile')) - if !has('win32') + if !has('win32') && !IsRoot() " On Windows, the source file is removed despite " its directory being made not writable. call assert_equal(['foo'], readfile('Xrenamedir/Xrenamefile')) diff --git a/src/nvim/testdir/test_retab.vim b/src/nvim/testdir/test_retab.vim index f11a32bade..1650a03876 100644 --- a/src/nvim/testdir/test_retab.vim +++ b/src/nvim/testdir/test_retab.vim @@ -69,9 +69,33 @@ func Test_retab() call assert_equal(" a b c ", Retab('!', 3)) call assert_equal(" a b c ", Retab('', 5)) call assert_equal(" a b c ", Retab('!', 5)) + + set tabstop& expandtab& endfunc func Test_retab_error() call assert_fails('retab -1', 'E487:') call assert_fails('retab! -1', 'E487:') + call assert_fails('ret -1000', 'E487:') + call assert_fails('ret 10000', 'E475:') + call assert_fails('ret 80000000000000000000', 'E475:') endfunc + +func Test_retab_endless() + new + call setline(1, "\t0\t") + let caught = 'no' + try + while 1 + set ts=4000 + retab 4 + endwhile + catch /E1240/ + let caught = 'yes' + endtry + bwipe! + set tabstop& +endfunc + + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_scriptnames.vim b/src/nvim/testdir/test_scriptnames.vim index fc6c910bfa..44ec146666 100644 --- a/src/nvim/testdir/test_scriptnames.vim +++ b/src/nvim/testdir/test_scriptnames.vim @@ -23,4 +23,10 @@ func Test_scriptnames() bwipe call delete('Xscripting') + + let msgs = execute('messages') + scriptnames + call assert_equal(msgs, execute('messages')) endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_search.vim b/src/nvim/testdir/test_search.vim index c796f1f676..a3d5ca96a1 100644 --- a/src/nvim/testdir/test_search.vim +++ b/src/nvim/testdir/test_search.vim @@ -292,15 +292,84 @@ endfunc func Test_searchpair() new - call setline(1, ['other code here', '', '[', '" cursor here', ']']) + call setline(1, ['other code', 'here [', ' [', ' " cursor here', ' ]]']) + + 4 + call assert_equal(3, searchpair('\[', '', ']', 'bW')) + call assert_equal([0, 3, 2, 0], getpos('.')) + 4 + call assert_equal(2, searchpair('\[', '', ']', 'bWr')) + call assert_equal([0, 2, 6, 0], getpos('.')) + 4 + call assert_equal(1, searchpair('\[', '', ']', 'bWm')) + call assert_equal([0, 3, 2, 0], getpos('.')) + 4|norm ^ + call assert_equal(5, searchpair('\[', '', ']', 'Wn')) + call assert_equal([0, 4, 2, 0], getpos('.')) + 4 + call assert_equal(2, searchpair('\[', '', ']', 'bW', + \ 'getline(".") =~ "^\\s*\["')) + call assert_equal([0, 2, 6, 0], getpos('.')) + set nomagic + 4 + call assert_equal(3, searchpair('\[', '', ']', 'bW')) + call assert_equal([0, 3, 2, 0], getpos('.')) + set magic + 4|norm ^ + call assert_equal(0, searchpair('{', '', '}', 'bW')) + call assert_equal([0, 4, 2, 0], getpos('.')) + + %d + call setline(1, ['if 1', ' if 2', ' else', ' endif 2', 'endif 1']) + + /\<if 1 + call assert_equal(5, searchpair('\<if\>', '\<else\>', '\<endif\>', 'W')) + call assert_equal([0, 5, 1, 0], getpos('.')) + /\<if 2 + call assert_equal(3, searchpair('\<if\>', '\<else\>', '\<endif\>', 'W')) + call assert_equal([0, 3, 3, 0], getpos('.')) + + q! +endfunc + +func Test_searchpairpos() + new + call setline(1, ['other code', 'here [', ' [', ' " cursor here', ' ]]']) + + 4 + call assert_equal([3, 2], searchpairpos('\[', '', ']', 'bW')) + call assert_equal([0, 3, 2, 0], getpos('.')) 4 - let a = searchpair('\[','',']','bW') - call assert_equal(3, a) + call assert_equal([2, 6], searchpairpos('\[', '', ']', 'bWr')) + call assert_equal([0, 2, 6, 0], getpos('.')) + 4|norm ^ + call assert_equal([5, 2], searchpairpos('\[', '', ']', 'Wn')) + call assert_equal([0, 4, 2, 0], getpos('.')) + 4 + call assert_equal([2, 6], searchpairpos('\[', '', ']', 'bW', + \ 'getline(".") =~ "^\\s*\["')) + call assert_equal([0, 2, 6, 0], getpos('.')) + 4 + call assert_equal([2, 6], searchpairpos('\[', '', ']', 'bWr')) + call assert_equal([0, 2, 6, 0], getpos('.')) set nomagic 4 - let a = searchpair('\[','',']','bW') - call assert_equal(3, a) + call assert_equal([3, 2], searchpairpos('\[', '', ']', 'bW')) + call assert_equal([0, 3, 2, 0], getpos('.')) set magic + 4|norm ^ + call assert_equal([0, 0], searchpairpos('{', '', '}', 'bW')) + call assert_equal([0, 4, 2, 0], getpos('.')) + + %d + call setline(1, ['if 1', ' if 2', ' else', ' endif 2', 'endif 1']) + /\<if 1 + call assert_equal([5, 1], searchpairpos('\<if\>', '\<else\>', '\<endif\>', 'W')) + call assert_equal([0, 5, 1, 0], getpos('.')) + /\<if 2 + call assert_equal([3, 3], searchpairpos('\<if\>', '\<else\>', '\<endif\>', 'W')) + call assert_equal([0, 3, 3, 0], getpos('.')) + q! endfunc @@ -309,17 +378,29 @@ func Test_searchpair_errors() call assert_fails("call searchpair('start', {-> 0}, 'end', 'bW', 'skip', 99, 100)", 'E729: using Funcref as a String') call assert_fails("call searchpair('start', 'middle', {'one': 1}, 'bW', 'skip', 99, 100)", 'E731: using Dictionary as a String') call assert_fails("call searchpair('start', 'middle', 'end', 'flags', 'skip', 99, 100)", 'E475: Invalid argument: flags') - call assert_fails("call searchpair('start', 'middle', 'end', 'bW', 0, 99, 100)", 'E475: Invalid argument: 0') call assert_fails("call searchpair('start', 'middle', 'end', 'bW', 'func', -99, 100)", 'E475: Invalid argument: -99') call assert_fails("call searchpair('start', 'middle', 'end', 'bW', 'func', 99, -100)", 'E475: Invalid argument: -100') + call assert_fails("call searchpair('start', 'middle', 'end', 'e')", 'E475: Invalid argument: e') + call assert_fails("call searchpair('start', 'middle', 'end', 'sn')", 'E475: Invalid argument: sn') +endfunc + +func Test_searchpairpos_errors() + call assert_fails("call searchpairpos([0], 'middle', 'end', 'bW', 'skip', 99, 100)", 'E730: using List as a String') + call assert_fails("call searchpairpos('start', {-> 0}, 'end', 'bW', 'skip', 99, 100)", 'E729: using Funcref as a String') + call assert_fails("call searchpairpos('start', 'middle', {'one': 1}, 'bW', 'skip', 99, 100)", 'E731: using Dictionary as a String') + call assert_fails("call searchpairpos('start', 'middle', 'end', 'flags', 'skip', 99, 100)", 'E475: Invalid argument: flags') + call assert_fails("call searchpairpos('start', 'middle', 'end', 'bW', 'func', -99, 100)", 'E475: Invalid argument: -99') + call assert_fails("call searchpairpos('start', 'middle', 'end', 'bW', 'func', 99, -100)", 'E475: Invalid argument: -100') + call assert_fails("call searchpairpos('start', 'middle', 'end', 'e')", 'E475: Invalid argument: e') + call assert_fails("call searchpairpos('start', 'middle', 'end', 'sn')", 'E475: Invalid argument: sn') endfunc func Test_searchpair_skip() func Zero() - return 0 + return 0 endfunc func Partial(x) - return a:x + return a:x endfunc new call setline(1, ['{', 'foo', 'foo', 'foo', '}']) @@ -1192,7 +1273,7 @@ endfunc " This was causing E874. Also causes an invalid read? func Test_look_behind() new - call setline(1, '0\|\&\n\@<=') + call setline(1, '0\|\&\n\@<=') call search(getline(".")) bwipe! endfunc @@ -1236,11 +1317,11 @@ endfunc func Test_search_Ctrl_L_combining() " Make sure, that Ctrl-L works correctly with combining characters. " It uses an artificial example of an 'a' with 4 combining chars: - " 'a' U+0061 Dec:97 LATIN SMALL LETTER A a /\%u61\Z "\u0061" + " 'a' U+0061 Dec:97 LATIN SMALL LETTER A a /\%u61\Z "\u0061" " ' ̀' U+0300 Dec:768 COMBINING GRAVE ACCENT ̀ /\%u300\Z "\u0300" " ' ́' U+0301 Dec:769 COMBINING ACUTE ACCENT ́ /\%u301\Z "\u0301" " ' ̇' U+0307 Dec:775 COMBINING DOT ABOVE ̇ /\%u307\Z "\u0307" - " ' ̣' U+0323 Dec:803 COMBINING DOT BELOW ̣ /\%u323 "\u0323" + " ' ̣' U+0323 Dec:803 COMBINING DOT BELOW ̣ /\%u323 "\u0323" " Those should also appear on the commandline CheckOption incsearch diff --git a/src/nvim/testdir/test_search_stat.vim b/src/nvim/testdir/test_search_stat.vim index 335a51268d..d950626615 100644 --- a/src/nvim/testdir/test_search_stat.vim +++ b/src/nvim/testdir/test_search_stat.vim @@ -1,14 +1,15 @@ " Tests for search_stats, when "S" is not in 'shortmess' -source screendump.vim source check.vim +source screendump.vim func Test_search_stat() new set shortmess-=S " Append 50 lines with text to search for, "foobar" appears 20 times call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 10)) - call nvim_win_set_cursor(0, [1, 0]) + + call cursor(1, 1) " searchcount() returns an empty dictionary when previous pattern was not set call assert_equal({}, searchcount(#{pattern: ''})) @@ -45,7 +46,6 @@ func Test_search_stat() \ searchcount(#{pattern: 'fooooobar', maxcount: 1})) " match at second line - call cursor(1, 1) let messages_before = execute('messages') let @/ = 'fo*\(bar\?\)\?' let g:a = execute(':unsilent :norm! n') @@ -73,7 +73,6 @@ func Test_search_stat() let stat = '\[2/50\]' let g:a = execute(':unsilent :norm! n') call assert_notmatch(pat .. stat, g:a) - call writefile(getline(1, '$'), 'sample.txt') " n does not update search stat call assert_equal( \ #{current: 50, exact_match: 1, total: 50, incomplete: 0, maxcount: 99}, @@ -263,6 +262,34 @@ func Test_searchcount_fails() call assert_fails('echo searchcount("boo!")', 'E715:') endfunc +func Test_searchcount_in_statusline() + CheckScreendump + + let lines =<< trim END + set shortmess-=S + call append(0, 'this is something') + function TestSearchCount() abort + let search_count = searchcount() + if !empty(search_count) + return '[' . search_count.current . '/' . search_count.total . ']' + else + return '' + endif + endfunction + set hlsearch + set laststatus=2 statusline+=%{TestSearchCount()} + END + call writefile(lines, 'Xsearchstatusline') + let buf = RunVimInTerminal('-S Xsearchstatusline', #{rows: 10}) + call TermWait(buf) + call term_sendkeys(buf, "/something") + call VerifyScreenDump(buf, 'Test_searchstat_4', {}) + + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) + call delete('Xsearchstatusline') +endfunc + func Test_search_stat_foldopen() CheckScreendump @@ -320,30 +347,29 @@ func! Test_search_stat_screendump() call delete('Xsearchstat') endfunc -func Test_searchcount_in_statusline() +func Test_search_stat_then_gd() CheckScreendump let lines =<< trim END + call setline(1, ['int cat;', 'int dog;', 'cat = dog;']) set shortmess-=S - call append(0, 'this is something') - function TestSearchCount() abort - let search_count = searchcount() - if !empty(search_count) - return '[' . search_count.current . '/' . search_count.total . ']' - else - return '' - endif - endfunction set hlsearch - set laststatus=2 statusline+=%{TestSearchCount()} END - call writefile(lines, 'Xsearchstatusline') - let buf = RunVimInTerminal('-S Xsearchstatusline', #{rows: 10}) + call writefile(lines, 'Xsearchstatgd') + + let buf = RunVimInTerminal('-S Xsearchstatgd', #{rows: 10}) + call term_sendkeys(buf, "/dog\<CR>") call TermWait(buf) - call term_sendkeys(buf, "/something") - call VerifyScreenDump(buf, 'Test_searchstat_4', {}) + call VerifyScreenDump(buf, 'Test_searchstatgd_1', {}) + + call term_sendkeys(buf, "G0gD") + call TermWait(buf) + call VerifyScreenDump(buf, 'Test_searchstatgd_2', {}) - call term_sendkeys(buf, "\<Esc>") call StopVimInTerminal(buf) - call delete('Xsearchstatusline') + call delete('Xsearchstatgd') endfunc + + + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_selectmode.vim b/src/nvim/testdir/test_selectmode.vim new file mode 100644 index 0000000000..b483841060 --- /dev/null +++ b/src/nvim/testdir/test_selectmode.vim @@ -0,0 +1,57 @@ +" Test for Select-mode + +source shared.vim + +" Test for selecting a register with CTRL-R +func Test_selectmode_register() + new + + " Default behavior: use unnamed register + call setline(1, 'foo') + call setreg('"', 'bar') + call setreg('a', 'baz') + exe ":norm! v\<c-g>a" + call assert_equal(getline('.'), 'aoo') + call assert_equal('f', getreg('"')) + call assert_equal('baz', getreg('a')) + + " Use the black hole register + call setline(1, 'foo') + call setreg('"', 'bar') + call setreg('a', 'baz') + exe ":norm! v\<c-g>\<c-r>_a" + call assert_equal(getline('.'), 'aoo') + call assert_equal('bar', getreg('"')) + call assert_equal('baz', getreg('a')) + + " Invalid register: use unnamed register + call setline(1, 'foo') + call setreg('"', 'bar') + call setreg('a', 'baz') + exe ":norm! v\<c-g>\<c-r>?a" + call assert_equal(getline('.'), 'aoo') + call assert_equal('f', getreg('"')) + call assert_equal('baz', getreg('a')) + + " Use unnamed register + call setline(1, 'foo') + call setreg('"', 'bar') + call setreg('a', 'baz') + exe ":norm! v\<c-g>\<c-r>\"a" + call assert_equal(getline('.'), 'aoo') + call assert_equal('f', getreg('"')) + call assert_equal('baz', getreg('a')) + + " use specicifed register, unnamed register is also written + call setline(1, 'foo') + call setreg('"', 'bar') + call setreg('a', 'baz') + exe ":norm! v\<c-g>\<c-r>aa" + call assert_equal(getline('.'), 'aoo') + call assert_equal('f', getreg('"')) + call assert_equal('f', getreg('a')) + + bw! +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_source_utf8.vim b/src/nvim/testdir/test_source_utf8.vim index e93ea29dff..66fabe0442 100644 --- a/src/nvim/testdir/test_source_utf8.vim +++ b/src/nvim/testdir/test_source_utf8.vim @@ -1,4 +1,5 @@ " Test the :source! command +source check.vim func Test_source_utf8() " check that sourcing a script with 0x80 as second byte works @@ -31,24 +32,24 @@ endfunc " Test for sourcing a file with CTRL-V's at the end of the line func Test_source_ctrl_v() - call writefile(['map __1 afirst', - \ 'map __2 asecond', - \ 'map __3 athird', - \ 'map __4 afourth', - \ 'map __5 afifth', - \ "map __1 asd\<C-V>", - \ "map __2 asd\<C-V>\<C-V>", - \ "map __3 asd\<C-V>\<C-V>", - \ "map __4 asd\<C-V>\<C-V>\<C-V>", - \ "map __5 asd\<C-V>\<C-V>\<C-V>", - \ ], 'Xtestfile') + call writefile(['map __1 afirst', + \ 'map __2 asecond', + \ 'map __3 athird', + \ 'map __4 afourth', + \ 'map __5 afifth', + \ "map __1 asd\<C-V>", + \ "map __2 asd\<C-V>\<C-V>", + \ "map __3 asd\<C-V>\<C-V>", + \ "map __4 asd\<C-V>\<C-V>\<C-V>", + \ "map __5 asd\<C-V>\<C-V>\<C-V>", + \ ], 'Xtestfile') source Xtestfile enew! exe "normal __1\<Esc>\<Esc>__2\<Esc>__3\<Esc>\<Esc>__4\<Esc>__5\<Esc>" exe "%s/\<C-J>/0/g" call assert_equal(['sd', - \ "map __2 asd\<Esc>secondsd\<Esc>sd0map __5 asd0fifth"], - \ getline(1, 2)) + \ "map __2 asd\<Esc>secondsd\<Esc>sd0map __5 asd0fifth"], + \ getline(1, 2)) enew! call delete('Xtestfile') diff --git a/src/nvim/testdir/test_spell.vim b/src/nvim/testdir/test_spell.vim index cf0faeee31..1ecb5c8070 100644 --- a/src/nvim/testdir/test_spell.vim +++ b/src/nvim/testdir/test_spell.vim @@ -768,6 +768,14 @@ func Test_spell_screendump() call delete('XtestSpell') endfunc +func Test_spell_single_word() + new + silent! norm 0R00 + spell! + silent 0norm 0r$ Dvz= + bwipe! +endfunc + let g:test_data_aff1 = [ \"SET ISO8859-1", \"TRY esianrtolcdugmphbyfvkwjkqxz-\xEB\xE9\xE8\xEA\xEF\xEE\xE4\xE0\xE2\xF6\xFC\xFB'ESIANRTOLCDUGMPHBYFVKWJKQXZ", diff --git a/src/nvim/testdir/test_stat.vim b/src/nvim/testdir/test_stat.vim index 170358e023..d3059664e9 100644 --- a/src/nvim/testdir/test_stat.vim +++ b/src/nvim/testdir/test_stat.vim @@ -1,11 +1,13 @@ " Tests for stat functions and checktime +source check.vim + func CheckFileTime(doSleep) let fnames = ['Xtest1.tmp', 'Xtest2.tmp', 'Xtest3.tmp'] let times = [] let result = 0 - " Use three files istead of localtim(), with a network filesystem the file + " Use three files instead of localtim(), with a network filesystem the file " times may differ at bit let fl = ['Hello World!'] for fname in fnames @@ -74,6 +76,44 @@ func Test_checktime() call delete(fname) endfunc +func Test_checktime_fast() + CheckFeature nanotime + + let fname = 'Xtest.tmp' + + let fl = ['Hello World!'] + call writefile(fl, fname) + set autoread + exec 'e' fname + let fl = readfile(fname) + let fl[0] .= ' - checktime' + sleep 10m " make test less flaky in Nvim + call writefile(fl, fname) + checktime + call assert_equal(fl[0], getline(1)) + + call delete(fname) +endfunc + +func Test_autoread_fast() + CheckFeature nanotime + + " this is timing sensitive + let g:test_is_flaky = 1 + + new Xautoread + setlocal autoread + call setline(1, 'foo') + w! + sleep 10m + call writefile(['bar'], 'Xautoread') + sleep 10m + checktime + call assert_equal('bar', trim(getline(1))) + + call delete('Xautoread') +endfunc + func Test_autoread_file_deleted() new Xautoread set autoread diff --git a/src/nvim/testdir/test_statusline.vim b/src/nvim/testdir/test_statusline.vim index a3e4dcdd25..fad13e3340 100644 --- a/src/nvim/testdir/test_statusline.vim +++ b/src/nvim/testdir/test_statusline.vim @@ -186,7 +186,16 @@ func Test_statusline() set virtualedit=all norm 10| call assert_match('^10,-10\s*$', s:get_statusline()) + set list + call assert_match('^10,-10\s*$', s:get_statusline()) set virtualedit& + exe "norm A\<Tab>\<Tab>a\<Esc>" + " In list mode a <Tab> is shown as "^I", which is 2-wide. + call assert_match('^9,-9\s*$', s:get_statusline()) + set list& + " Now the second <Tab> ends at the 16th screen column. + call assert_match('^17,-17\s*$', s:get_statusline()) + undo " %w: Preview window flag, text is "[Preview]". " %W: Preview window flag, text is ",PRV". @@ -498,5 +507,20 @@ func Test_statusline_after_split_vsplit() set ls& stl& endfunc +" Test using a multibyte character for 'stl' and 'stlnc' items in 'fillchars' +" with a custom 'statusline' +func Test_statusline_mbyte_fillchar() + only + set laststatus=2 + set fillchars=vert:\|,fold:-,stl:━,stlnc:═ + set statusline=a%=b + call assert_match('^a\+━\+b$', s:get_statusline()) + vnew + call assert_match('^a\+━\+b━a\+═\+b$', s:get_statusline()) + wincmd w + call assert_match('^a\+═\+b═a\+━\+b$', s:get_statusline()) + set statusline& fillchars& laststatus& + %bw! +endfunc " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_substitute.vim b/src/nvim/testdir/test_substitute.vim index 20b760ac15..dbb792d2b0 100644 --- a/src/nvim/testdir/test_substitute.vim +++ b/src/nvim/testdir/test_substitute.vim @@ -51,10 +51,12 @@ func Test_substitute_variants() \ { 'cmd': ':s/t/r/cg', 'exp': 'Tesring srring', 'prompt': 'a' }, \ { 'cmd': ':s/t/r/ci', 'exp': 'resting string', 'prompt': 'y' }, \ { 'cmd': ':s/t/r/cI', 'exp': 'Tesring string', 'prompt': 'y' }, + \ { 'cmd': ':s/t/r/c', 'exp': 'Testing string', 'prompt': 'n' }, \ { 'cmd': ':s/t/r/cn', 'exp': ln }, \ { 'cmd': ':s/t/r/cp', 'exp': 'Tesring string', 'prompt': 'y' }, \ { 'cmd': ':s/t/r/cl', 'exp': 'Tesring string', 'prompt': 'y' }, \ { 'cmd': ':s/t/r/gc', 'exp': 'Tesring srring', 'prompt': 'a' }, + \ { 'cmd': ':s/i/I/gc', 'exp': 'TestIng string', 'prompt': 'l' }, \ { 'cmd': ':s/foo/bar/ge', 'exp': ln }, \ { 'cmd': ':s/t/r/g', 'exp': 'Tesring srring' }, \ { 'cmd': ':s/t/r/gi', 'exp': 'resring srring' }, @@ -86,6 +88,7 @@ func Test_substitute_variants() \ { 'cmd': ':s//r/rp', 'exp': 'Testr string' }, \ { 'cmd': ':s//r/rl', 'exp': 'Testr string' }, \ { 'cmd': ':s//r/r', 'exp': 'Testr string' }, + \ { 'cmd': ':s/i/I/gc', 'exp': 'Testing string', 'prompt': 'q' }, \] for var in variants @@ -105,7 +108,7 @@ func Test_substitute_variants() call assert_equal(var.exp, getline('.'), msg) endfor endfor -endfunction +endfunc " Test the l, p, # flags. func Test_substitute_flags_lp() @@ -384,6 +387,10 @@ func Test_substitute_join() call assert_equal(["foo\tbarbar\<C-H>foo"], getline(1, '$')) call assert_equal('\n', histget("search", -1)) + call setline(1, ['foo', 'bar', 'baz', 'qux']) + call execute('1,2s/\n//') + call assert_equal(['foobarbaz', 'qux'], getline(1, '$')) + bwipe! endfunc @@ -398,6 +405,11 @@ func Test_substitute_count() call assert_fails('s/foo/bar/0', 'E939:') + call setline(1, ['foo foo', 'foo foo', 'foo foo', 'foo foo', 'foo foo']) + 2,4s/foo/bar/ 10 + call assert_equal(['foo foo', 'foo foo', 'foo foo', 'bar foo', 'bar foo'], + \ getline(1, '$')) + bwipe! endfunc @@ -416,6 +428,10 @@ func Test_substitute_flag_n() " No substitution should have been done. call assert_equal(lines, getline(1, '$')) + %delete _ + call setline(1, ['A', 'Bar', 'Baz']) + call assert_equal("\n1 match on 1 line", execute('s/\nB\@=//gn')) + bwipe! endfunc @@ -749,6 +765,45 @@ func Test_sub_beyond_end() bwipe! endfunc +" Test for repeating last substitution using :~ and :&r +func Test_repeat_last_sub() + new + call setline(1, ['blue green yellow orange white']) + s/blue/red/ + let @/ = 'yellow' + ~ + let @/ = 'white' + :&r + let @/ = 'green' + s//gray + call assert_equal('red gray red orange red', getline(1)) + close! +endfunc + +" Test for Vi compatible substitution: +" \/{string}/, \?{string}? and \&{string}& +func Test_sub_vi_compatibility() + new + call setline(1, ['blue green yellow orange blue']) + let @/ = 'orange' + s\/white/ + let @/ = 'blue' + s\?amber? + let @/ = 'white' + s\&green& + call assert_equal('amber green yellow white green', getline(1)) + close! +endfunc + +" Test for substitute with the new text longer than the original text +func Test_sub_expand_text() + new + call setline(1, 'abcabcabcabcabcabcabcabc') + s/b/\=repeat('B', 10)/g + call assert_equal(repeat('aBBBBBBBBBBc', 8), getline(1)) + close! +endfunc + func Test_submatch_list_concatenate() let pat = 'A\(.\)' let Rep = {-> string([submatch(0, 1)] + [[submatch(1)]])} diff --git a/src/nvim/testdir/test_suspend.vim b/src/nvim/testdir/test_suspend.vim index 4b3bd5eadf..bf88bd4453 100644 --- a/src/nvim/testdir/test_suspend.vim +++ b/src/nvim/testdir/test_suspend.vim @@ -26,8 +26,8 @@ func Test_suspend() " Wait for shell prompt. call WaitForAssert({-> assert_match('[$#] $', term_getline(buf, '.'))}) - call term_sendkeys(buf, v:progpath - \ . " --clean -X" + call term_sendkeys(buf, GetVimCommandClean() + \ . " -X" \ . " -c 'set nu'" \ . " -c 'call setline(1, \"foo\")'" \ . " Xfoo\<CR>") diff --git a/src/nvim/testdir/test_swap.vim b/src/nvim/testdir/test_swap.vim index b3018b2b0d..b180f27685 100644 --- a/src/nvim/testdir/test_swap.vim +++ b/src/nvim/testdir/test_swap.vim @@ -1,6 +1,7 @@ " Tests for the swap feature source check.vim +source shared.vim func s:swapname() return trim(execute('swapname')) @@ -198,14 +199,17 @@ func Test_swapfile_delete() quit call assert_equal(fnamemodify(swapfile_name, ':t'), fnamemodify(s:swapname, ':t')) - " Write the swapfile with a modified PID, now it will be automatically - " deleted. Process one should never be Vim. - let swapfile_bytes[24:27] = 0z01000000 - call writefile(swapfile_bytes, swapfile_name) - let s:swapname = '' - split XswapfileText - quit - call assert_equal('', s:swapname) + " This test won't work as root because root can successfully run kill(1, 0) + if !IsRoot() + " Write the swapfile with a modified PID, now it will be automatically + " deleted. Process one should never be Vim. + let swapfile_bytes[24:27] = 0z01000000 + call writefile(swapfile_bytes, swapfile_name) + let s:swapname = '' + split XswapfileText + quit + call assert_equal('', s:swapname) + endif " Now set the modified flag, the swap file will not be deleted let swapfile_bytes[28 + 80 + 899] = 0x55 diff --git a/src/nvim/testdir/test_syntax.vim b/src/nvim/testdir/test_syntax.vim index 757866f5dc..b047b53b6f 100644 --- a/src/nvim/testdir/test_syntax.vim +++ b/src/nvim/testdir/test_syntax.vim @@ -728,6 +728,56 @@ func Test_syntax_foldlevel() quit! endfunc +func Test_search_syntax_skip() + new + let lines =<< trim END + + /* This is VIM */ + Another Text for VIM + let a = "VIM" + END + call setline(1, lines) + syntax on + syntax match Comment "^/\*.*\*/" + syntax match String '".*"' + + " Skip argument using string evaluation. + 1 + call search('VIM', 'w', '', 0, 'synIDattr(synID(line("."), col("."), 1), "name") =~? "comment"') + call assert_equal('Another Text for VIM', getline('.')) + 1 + call search('VIM', 'w', '', 0, 'synIDattr(synID(line("."), col("."), 1), "name") !~? "string"') + call assert_equal(' let a = "VIM"', getline('.')) + + " Skip argument using Lambda. + 1 + call search('VIM', 'w', '', 0, { -> synIDattr(synID(line("."), col("."), 1), "name") =~? "comment"}) + call assert_equal('Another Text for VIM', getline('.')) + + 1 + call search('VIM', 'w', '', 0, { -> synIDattr(synID(line("."), col("."), 1), "name") !~? "string"}) + call assert_equal(' let a = "VIM"', getline('.')) + + " Skip argument using funcref. + func InComment() + return synIDattr(synID(line("."), col("."), 1), "name") =~? "comment" + endfunc + func InString() + return synIDattr(synID(line("."), col("."), 1), "name") !~? "string" + endfunc + 1 + call search('VIM', 'w', '', 0, function('InComment')) + call assert_equal('Another Text for VIM', getline('.')) + + 1 + call search('VIM', 'w', '', 0, function('InString')) + call assert_equal(' let a = "VIM"', getline('.')) + + delfunc InComment + delfunc InString + bwipe! +endfunc + func Test_syn_include_contains_TOP() let l:case = "TOP in included syntax means its group list name" new diff --git a/src/nvim/testdir/test_system.vim b/src/nvim/testdir/test_system.vim index 1858b48807..18692f42c9 100644 --- a/src/nvim/testdir/test_system.vim +++ b/src/nvim/testdir/test_system.vim @@ -46,15 +46,15 @@ func Test_System() bwipe! call assert_fails('call system("wc -l", 99999)', 'E86:') -endfunction +endfunc -function! Test_system_exmode() +func Test_system_exmode() if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "source Xscript" +q; echo "result=$?"' + let cmd = ' -es -c "source Xscript" +q; echo "result=$?"' " Need to put this in a script, "catch" isn't found after an unknown " function. call writefile(['try', 'call doesnotexist()', 'catch', 'endtry'], 'Xscript') - let a = system(v:progpath . cmd) + let a = system(GetVimCommand() . cmd) call assert_match('result=0', a) call assert_equal(0, v:shell_error) endif @@ -62,33 +62,33 @@ function! Test_system_exmode() " Error before try does set error flag. call writefile(['call nosuchfunction()', 'try', 'call doesnotexist()', 'catch', 'endtry'], 'Xscript') if has('unix') " echo $? only works on Unix - let a = system(v:progpath . cmd) + let a = system(GetVimCommand() . cmd) call assert_notequal('0', a[0]) endif - let cmd = ' -es --headless -u NONE -c "source Xscript" +q' - let a = system(v:progpath . cmd) + let cmd = ' -es -c "source Xscript" +q' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, v:shell_error) call delete('Xscript') if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "call doesnotexist()" +q; echo $?' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()" +q; echo $?' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, a[0]) endif - let cmd = ' -es --headless -u NONE -c "call doesnotexist()" +q' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()" +q' + let a = system(GetVimCommand(). cmd) call assert_notequal(0, v:shell_error) if has('unix') " echo $? only works on Unix - let cmd = ' -es --headless -u NONE -c "call doesnotexist()|let a=1" +q; echo $?' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()|let a=1" +q; echo $?' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, a[0]) endif - let cmd = ' -es --headless -u NONE -c "call doesnotexist()|let a=1" +q' - let a = system(v:progpath. cmd) + let cmd = ' -es -c "call doesnotexist()|let a=1" +q' + let a = system(GetVimCommand() . cmd) call assert_notequal(0, v:shell_error) endfunc diff --git a/src/nvim/testdir/test_tabline.vim b/src/nvim/testdir/test_tabline.vim index 117d962d08..3a18206078 100644 --- a/src/nvim/testdir/test_tabline.vim +++ b/src/nvim/testdir/test_tabline.vim @@ -86,6 +86,17 @@ func Test_tabline_empty_group() set tabline= endfunc +" When there are exactly 20 tabline format items (the exact size of the +" initial tabline items array), test that we don't write beyond the size +" of the array. +func Test_tabline_20_format_items_no_overrun() + set showtabline=2 + + let tabline = repeat('%#StatColorHi2#', 20) + let &tabline = tabline + redrawtabline + set showtabline& tabline& +endfunc " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_tagjump.vim b/src/nvim/testdir/test_tagjump.vim index 2aa04df42a..e0b05edf15 100644 --- a/src/nvim/testdir/test_tagjump.vim +++ b/src/nvim/testdir/test_tagjump.vim @@ -641,7 +641,7 @@ func Test_tag_envvar() endfunc " Test for :ptag -func Test_ptag() +func Test_tag_preview() call writefile(["!_TAG_FILE_ENCODING\tutf-8\t//", \ "second\tXfile1\t2", \ "third\tXfile1\t3",], @@ -655,11 +655,19 @@ func Test_ptag() call assert_equal(2, winnr('$')) call assert_equal(1, getwinvar(1, '&previewwindow')) call assert_equal(0, getwinvar(2, '&previewwindow')) - wincmd w + wincmd P call assert_equal(3, line('.')) " jump to the tag again + wincmd w ptag third + wincmd P + call assert_equal(3, line('.')) + + " jump to the newer tag + wincmd w + ptag + wincmd P call assert_equal(3, line('.')) " close the preview window @@ -829,8 +837,8 @@ func Test_tag_last_search_pat() %bwipe endfunc -" Test for jumping to a tag when the tag stack is full -func Test_tag_stack_full() +" Tag stack tests +func Test_tag_stack() let l = [] for i in range(10, 31) let l += ["var" .. i .. "\tXfoo\t/^int var" .. i .. ";$/"] @@ -844,6 +852,7 @@ func Test_tag_stack_full() endfor call writefile(l, 'Xfoo') + " Jump to a tag when the tag stack is full. Oldest entry should be removed. enew for i in range(10, 30) exe "tag var" .. i @@ -856,9 +865,15 @@ func Test_tag_stack_full() call assert_equal('var12', l.items[0].tagname) call assert_equal('var31', l.items[19].tagname) - " Jump from the top of the stack + " Use tnext with a single match + call assert_fails('tnext', 'E427:') + + " Jump to newest entry from the top of the stack call assert_fails('tag', 'E556:') + " Pop with zero count from the top of the stack + call assert_fails('0pop', 'E556:') + " Pop from an unsaved buffer enew! call append(1, "sample text") @@ -869,6 +884,13 @@ func Test_tag_stack_full() " Pop all the entries in the tag stack call assert_fails('30pop', 'E555:') + " Pop with a count when already at the bottom of the stack + call assert_fails('exe "normal 4\<C-T>"', 'E555:') + call assert_equal(1, gettagstack().curidx) + + " Jump to newest entry from the bottom of the stack with zero count + call assert_fails('0tag', 'E555:') + " Pop the tag stack when it is empty call settagstack(1, {'items' : []}) call assert_fails('pop', 'E73:') @@ -895,6 +917,7 @@ func Test_tag_multimatch() [CODE] call writefile(code, 'Xfoo') + call settagstack(1, {'items' : []}) tag first tlast call assert_equal(3, line('.')) @@ -903,6 +926,151 @@ func Test_tag_multimatch() call assert_equal(1, line('.')) call assert_fails('tprev', 'E425:') + tlast + call feedkeys("5\<CR>", 't') + tselect first + call assert_equal(2, gettagstack().curidx) + + set ignorecase + tag FIRST + tnext + call assert_equal(2, line('.')) + set ignorecase& + + call delete('Xtags') + call delete('Xfoo') + set tags& + %bwipe +endfunc + +" Test for previewing multiple matching tags +func Test_preview_tag_multimatch() + call writefile([ + \ "!_TAG_FILE_ENCODING\tutf-8\t//", + \ "first\tXfoo\t1", + \ "first\tXfoo\t2", + \ "first\tXfoo\t3"], + \ 'Xtags') + set tags=Xtags + let code =<< trim [CODE] + int first() {} + int first() {} + int first() {} + [CODE] + call writefile(code, 'Xfoo') + + enew | only + ptag first + ptlast + wincmd P + call assert_equal(3, line('.')) + wincmd w + call assert_fails('ptnext', 'E428:') + ptprev + wincmd P + call assert_equal(2, line('.')) + wincmd w + ptfirst + wincmd P + call assert_equal(1, line('.')) + wincmd w + call assert_fails('ptprev', 'E425:') + ptnext + wincmd P + call assert_equal(2, line('.')) + wincmd w + ptlast + call feedkeys("5\<CR>", 't') + ptselect first + wincmd P + call assert_equal(3, line('.')) + + pclose + + call delete('Xtags') + call delete('Xfoo') + set tags& + %bwipe +endfunc + +" Test for jumping to multiple matching tags across multiple :tags commands +func Test_tnext_multimatch() + call writefile([ + \ "!_TAG_FILE_ENCODING\tutf-8\t//", + \ "first\tXfoo1\t1", + \ "first\tXfoo2\t1", + \ "first\tXfoo3\t1"], + \ 'Xtags') + set tags=Xtags + let code =<< trim [CODE] + int first() {} + [CODE] + call writefile(code, 'Xfoo1') + call writefile(code, 'Xfoo2') + call writefile(code, 'Xfoo3') + + tag first + tag first + pop + tnext + tnext + call assert_fails('tnext', 'E428:') + + call delete('Xtags') + call delete('Xfoo1') + call delete('Xfoo2') + call delete('Xfoo3') + set tags& + %bwipe +endfunc + +" Test for jumping to multiple matching tags in non-existing files +func Test_multimatch_non_existing_files() + call writefile([ + \ "!_TAG_FILE_ENCODING\tutf-8\t//", + \ "first\tXfoo1\t1", + \ "first\tXfoo2\t1", + \ "first\tXfoo3\t1"], + \ 'Xtags') + set tags=Xtags + + call settagstack(1, {'items' : []}) + call assert_fails('tag first', 'E429:') + call assert_equal(3, gettagstack().items[0].matchnr) + + call delete('Xtags') + set tags& + %bwipe +endfunc + +func Test_tselect_listing() + call writefile([ + \ "!_TAG_FILE_ENCODING\tutf-8\t//", + \ "first\tXfoo\t1" .. ';"' .. "\tv\ttyperef:typename:int\tfile:", + \ "first\tXfoo\t2" .. ';"' .. "\tv\ttyperef:typename:char\tfile:"], + \ 'Xtags') + set tags=Xtags + + let code =<< trim [CODE] + static int first; + static char first; + [CODE] + call writefile(code, 'Xfoo') + + call feedkeys("\<CR>", "t") + let l = split(execute("tselect first"), "\n") + let expected =<< [DATA] + # pri kind tag file + 1 FS v first Xfoo + typeref:typename:int + 1 + 2 FS v first Xfoo + typeref:typename:char + 2 +Type number and <Enter> (q or empty cancels): +[DATA] + call assert_equal(expected, l) + call delete('Xtags') call delete('Xfoo') set tags& diff --git a/src/nvim/testdir/test_textformat.vim b/src/nvim/testdir/test_textformat.vim index bf0706a0c2..e9f846af7b 100644 --- a/src/nvim/testdir/test_textformat.vim +++ b/src/nvim/testdir/test_textformat.vim @@ -196,6 +196,130 @@ func Test_text_format() enew! endfunc +func Test_format_c_comment() + new + setl ai cindent tw=40 et fo=croql + let text =<< trim END + var = 2345; // asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf + END + call setline(1, text) + normal gql + let expected =<< trim END + var = 2345; // asdf asdf asdf asdf asdf + // asdf asdf asdf asdf asdf + END + call assert_equal(expected, getline(1, '$')) + + %del + let text =<< trim END + var = 2345; // asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf + END + call setline(1, text) + normal gql + let expected =<< trim END + var = 2345; // asdf asdf asdf asdf asdf + // asdf asdf asdf asdf asdf + // asdf asdf + END + call assert_equal(expected, getline(1, '$')) + + %del + let text =<< trim END + #if 0 // This is another long end of + // line comment that + // wraps. + END + call setline(1, text) + normal gq2j + let expected =<< trim END + #if 0 // This is another long + // end of line comment + // that wraps. + END + call assert_equal(expected, getline(1, '$')) + + " Using either "o" or "O" repeats a line comment occupying a whole line. + %del + let text =<< trim END + nop; + // This is a comment + val = val; + END + call setline(1, text) + normal 2Go + let expected =<< trim END + nop; + // This is a comment + // + val = val; + END + call assert_equal(expected, getline(1, '$')) + normal 2GO + let expected =<< trim END + nop; + // + // This is a comment + // + val = val; + END + call assert_equal(expected, getline(1, '$')) + + " Using "o" repeats a line comment after a statement, "O" does not. + %del + let text =<< trim END + nop; + val = val; // This is a comment + END + call setline(1, text) + normal 2Go + let expected =<< trim END + nop; + val = val; // This is a comment + // + END + call assert_equal(expected, getline(1, '$')) + normal 2GO + let expected =<< trim END + nop; + + val = val; // This is a comment + // + END + call assert_equal(expected, getline(1, '$')) + + " Using "o" does not repeat a comment in a string + %del + let text =<< trim END + nop; + val = " // This is not a comment"; + END + call setline(1, text) + normal 2Gox + let expected =<< trim END + nop; + val = " // This is not a comment"; + x + END + call assert_equal(expected, getline(1, '$')) + + " Using CTRL-U after "o" fixes the indent + %del + let text =<< trim END + { + val = val; // This is a comment + END + call setline(1, text) + exe "normal! 2Go\<C-U>x\<Esc>" + let expected =<< trim END + { + val = val; // This is a comment + x + END + call assert_equal(expected, getline(1, '$')) + + bwipe! +endfunc + " Tests for :right, :center and :left on text with embedded TAB. func Test_format_align() enew! @@ -433,6 +557,21 @@ func Test_format_align() call assert_equal("\t\t Vim", getline(1)) q! + " align text with 'rightleft' + if has('rightleft') + new + call setline(1, 'Vim') + setlocal rightleft + left 20 + setlocal norightleft + call assert_equal("\t\t Vim", getline(1)) + setlocal rightleft + right + setlocal norightleft + call assert_equal("Vim", getline(1)) + close! + endif + set tw& endfunc diff --git a/src/nvim/testdir/test_timers.vim b/src/nvim/testdir/test_timers.vim index aae315b2c5..09eed4e10d 100644 --- a/src/nvim/testdir/test_timers.vim +++ b/src/nvim/testdir/test_timers.vim @@ -379,4 +379,27 @@ func Test_timer_invalid_callback() call assert_fails('call timer_start(0, "0")', 'E921') endfunc +func Test_timer_using_win_execute_undo_sync() + let bufnr1 = bufnr() + new + let g:bufnr2 = bufnr() + let g:winid = win_getid() + exe "buffer " .. bufnr1 + wincmd w + call setline(1, ['test']) + autocmd InsertEnter * call timer_start(100, { -> win_execute(g:winid, 'buffer ' .. g:bufnr2) }) + call timer_start(200, { -> feedkeys("\<CR>bbbb\<Esc>") }) + call feedkeys("Oaaaa", 'x!t') + " will hang here until the second timer fires + call assert_equal(['aaaa', 'bbbb', 'test'], getline(1, '$')) + undo + call assert_equal(['test'], getline(1, '$')) + + bwipe! + bwipe! + unlet g:winid + unlet g:bufnr2 + au! InsertEnter +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_usercommands.vim b/src/nvim/testdir/test_usercommands.vim index 29e578ac6d..967ad85a64 100644 --- a/src/nvim/testdir/test_usercommands.vim +++ b/src/nvim/testdir/test_usercommands.vim @@ -269,10 +269,10 @@ endfunc func Test_CmdCompletion() call feedkeys(":com -\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_equal('"com -addr bang bar buffer complete count nargs range register', @:) + call assert_equal('"com -addr bang bar buffer complete count keepscript nargs range register', @:) call feedkeys(":com -nargs=0 -\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_equal('"com -nargs=0 -addr bang bar buffer complete count nargs range register', @:) + call assert_equal('"com -nargs=0 -addr bang bar buffer complete count keepscript nargs range register', @:) call feedkeys(":com -nargs=\<C-A>\<C-B>\"\<CR>", 'tx') call assert_equal('"com -nargs=* + 0 1 ?', @:) @@ -350,7 +350,6 @@ func Test_use_execute_in_completion() endfunc func Test_addr_all() - throw 'skipped: requires patch v8.1.0341 to pass' command! -addr=lines DoSomething let g:a1 = <line1> | let g:a2 = <line2> %DoSomething call assert_equal(1, g:a1) @@ -551,3 +550,26 @@ func Test_command_list() call assert_equal("\nNo user-defined commands found", execute(':command Xxx')) call assert_equal("\nNo user-defined commands found", execute('command')) endfunc + +func Test_delcommand_buffer() + command Global echo 'global' + command -buffer OneBuffer echo 'one' + new + command -buffer TwoBuffer echo 'two' + call assert_equal(0, exists(':OneBuffer')) + call assert_equal(2, exists(':Global')) + call assert_equal(2, exists(':TwoBuffer')) + delcommand -buffer TwoBuffer + call assert_equal(0, exists(':TwoBuffer')) + call assert_fails('delcommand -buffer Global', 'E1237:') + call assert_fails('delcommand -buffer OneBuffer', 'E1237:') + bwipe! + call assert_equal(2, exists(':OneBuffer')) + delcommand -buffer OneBuffer + call assert_equal(0, exists(':OneBuffer')) + call assert_fails('delcommand -buffer Global', 'E1237:') + delcommand Global + call assert_equal(0, exists(':Global')) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_utf8.vim b/src/nvim/testdir/test_utf8.vim index 0818c2e4b0..9b010a5dbc 100644 --- a/src/nvim/testdir/test_utf8.vim +++ b/src/nvim/testdir/test_utf8.vim @@ -1,5 +1,6 @@ " Tests for Unicode manipulations +source check.vim source view_util.vim " Visual block Insert adjusts for multi-byte char @@ -7,7 +8,7 @@ func Test_visual_block_insert() new call setline(1, ["aaa", "あああ", "bbb"]) exe ":norm! gg0l\<C-V>jjIx\<Esc>" - call assert_equal(['axaa', 'xあああ', 'bxbb'], getline(1, '$')) + call assert_equal(['axaa', ' xあああ', 'bxbb'], getline(1, '$')) bwipeout! endfunc @@ -148,4 +149,55 @@ func Test_print_overlong() bwipe! endfunc +func Test_recording_with_select_mode_utf8() + call Run_test_recording_with_select_mode_utf8() +endfunc + +func Run_test_recording_with_select_mode_utf8() + new + + " No escaping + call feedkeys("qacc12345\<Esc>gH哦\<Esc>q", "tx") + call assert_equal("哦", getline(1)) + call assert_equal("cc12345\<Esc>gH哦\<Esc>", @a) + call setline(1, 'asdf') + normal! @a + call assert_equal("哦", getline(1)) + + " 固 is 0xE5 0x9B 0xBA where 0x9B is CSI + call feedkeys("qacc12345\<Esc>gH固\<Esc>q", "tx") + call assert_equal("固", getline(1)) + call assert_equal("cc12345\<Esc>gH固\<Esc>", @a) + call setline(1, 'asdf') + normal! @a + call assert_equal("固", getline(1)) + + " 四 is 0xE5 0x9B 0x9B where 0x9B is CSI + call feedkeys("qacc12345\<Esc>gH四\<Esc>q", "tx") + call assert_equal("四", getline(1)) + call assert_equal("cc12345\<Esc>gH四\<Esc>", @a) + call setline(1, 'asdf') + normal! @a + call assert_equal("四", getline(1)) + + " 倒 is 0xE5 0x80 0x92 where 0x80 is K_SPECIAL + call feedkeys("qacc12345\<Esc>gH倒\<Esc>q", "tx") + call assert_equal("倒", getline(1)) + call assert_equal("cc12345\<Esc>gH倒\<Esc>", @a) + call setline(1, 'asdf') + normal! @a + call assert_equal("倒", getline(1)) + + bwipe! +endfunc + +" This must be done as one of the last tests, because it starts the GUI, which +" cannot be undone. +func Test_zz_recording_with_select_mode_utf8_gui() + CheckCanRunGui + + gui -f + call Run_test_recording_with_select_mode_utf8() +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_utf8_comparisons.vim b/src/nvim/testdir/test_utf8_comparisons.vim index fdf9d80802..f3c86b44fb 100644 --- a/src/nvim/testdir/test_utf8_comparisons.vim +++ b/src/nvim/testdir/test_utf8_comparisons.vim @@ -1,12 +1,12 @@ " Tests for case-insensitive UTF-8 comparisons (utf_strnicmp() in mbyte.c) " Also test "g~ap". -function! Ch(a, op, b, expected) +func Ch(a, op, b, expected) call assert_equal(eval(printf('"%s" %s "%s"', a:a, a:op, a:b)), a:expected, \ printf('"%s" %s "%s" should return %d', a:a, a:op, a:b, a:expected)) -endfunction +endfunc -function! Chk(a, b, result) +func Chk(a, b, result) if a:result == 0 call Ch(a:a, '==?', a:b, 1) call Ch(a:a, '!=?', a:b, 0) @@ -86,6 +86,9 @@ endfunc " test that g~ap changes one paragraph only. func Test_gap() new - call feedkeys("iabcd\n\ndefggg0g~ap", "tx") + " setup text + call feedkeys("iabcd\<cr>\<cr>defg", "tx") + " modify only first line + call feedkeys("gg0g~ap", "tx") call assert_equal(["ABCD", "", "defg"], getline(1,3)) endfunc diff --git a/src/nvim/testdir/test_vartabs.vim b/src/nvim/testdir/test_vartabs.vim index 46e0d62313..017bb6675d 100644 --- a/src/nvim/testdir/test_vartabs.vim +++ b/src/nvim/testdir/test_vartabs.vim @@ -135,7 +135,17 @@ func Test_vartabs() bwipeout! endfunc -func! Test_vartabs_breakindent() +func Test_retab_invalid_arg() + new + call setline(1, "\ttext") + retab 0 + call assert_fails("retab -8", 'E487: Argument must be positive') + call assert_fails("retab 10000", 'E475:') + call assert_fails("retab 720575940379279360", 'E475:') + bwipe! +endfunc + +func Test_vartabs_breakindent() if !exists("+breakindent") return endif diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim index 34733f127f..f93eb6e274 100644 --- a/src/nvim/testdir/test_vimscript.vim +++ b/src/nvim/testdir/test_vimscript.vim @@ -1,6 +1,9 @@ " Test various aspects of the Vim script language. " Most of this was formerly in test49. +source check.vim +source shared.vim + "------------------------------------------------------------------------------- " Test environment {{{1 "------------------------------------------------------------------------------- @@ -25,7 +28,7 @@ com! -nargs=1 Xout call Xout(<args>) " in the variable argument list. This function is useful if similar tests are " to be made for a ":return" from a function call or a ":finish" in a script " file. -function! MakeScript(funcname, ...) +func MakeScript(funcname, ...) let script = tempname() execute "redir! >" . script execute "function" a:funcname @@ -1156,6 +1159,82 @@ func Test_type() call assert_equal(v:t_list, type(v:_null_list)) call assert_equal(v:t_dict, type(v:_null_dict)) call assert_equal(v:t_blob, type(v:_null_blob)) + + call assert_equal(0, 0 + v:false) + call assert_equal(1, 0 + v:true) + " call assert_equal(0, 0 + v:none) + call assert_equal(0, 0 + v:null) + + call assert_equal('false', '' . v:false) + call assert_equal('true', '' . v:true) + " call assert_equal('none', '' . v:none) + call assert_equal('null', '' . v:null) + + call assert_true(v:false == 0) + call assert_false(v:false != 0) + call assert_true(v:true == 1) + call assert_false(v:true != 1) + call assert_false(v:true == v:false) + call assert_true(v:true != v:false) + + call assert_true(v:null == 0) + call assert_false(v:null != 0) + " call assert_true(v:none == 0) + " call assert_false(v:none != 0) + + call assert_true(v:false is v:false) + call assert_true(v:true is v:true) + " call assert_true(v:none is v:none) + call assert_true(v:null is v:null) + + call assert_false(v:false isnot v:false) + call assert_false(v:true isnot v:true) + " call assert_false(v:none isnot v:none) + call assert_false(v:null isnot v:null) + + call assert_false(v:false is 0) + call assert_false(v:true is 1) + call assert_false(v:true is v:false) + " call assert_false(v:none is 0) + call assert_false(v:null is 0) + " call assert_false(v:null is v:none) + + call assert_true(v:false isnot 0) + call assert_true(v:true isnot 1) + call assert_true(v:true isnot v:false) + " call assert_true(v:none isnot 0) + call assert_true(v:null isnot 0) + " call assert_true(v:null isnot v:none) + + call assert_equal(v:false, eval(string(v:false))) + call assert_equal(v:true, eval(string(v:true))) + " call assert_equal(v:none, eval(string(v:none))) + call assert_equal(v:null, eval(string(v:null))) + + call assert_equal(v:false, copy(v:false)) + call assert_equal(v:true, copy(v:true)) + " call assert_equal(v:none, copy(v:none)) + call assert_equal(v:null, copy(v:null)) + + call assert_equal([v:false], deepcopy([v:false])) + call assert_equal([v:true], deepcopy([v:true])) + " call assert_equal([v:none], deepcopy([v:none])) + call assert_equal([v:null], deepcopy([v:null])) + + call assert_true(empty(v:false)) + call assert_false(empty(v:true)) + call assert_true(empty(v:null)) + " call assert_true(empty(v:none)) + + func ChangeYourMind() + try + return v:true + finally + return 'something else' + endtry + endfunc + + call ChangeYourMind() endfunc "------------------------------------------------------------------------------- @@ -1668,7 +1747,7 @@ func Test_function_defined_line() [CODE] call writefile(lines, 'Xtest.vim') - let res = system(v:progpath .. ' --clean -es -X -S Xtest.vim') + let res = system(GetVimCommandClean() .. ' -es -X -S Xtest.vim') call assert_equal(0, v:shell_error) let m = matchstr(res, 'function F1()[^[:print:]]*[[:print:]]*') @@ -1692,6 +1771,26 @@ func Test_function_defined_line() call delete('Xtest.vim') endfunc +func Test_for_over_string() + let res = '' + for c in 'aéc̀d' + let res ..= c .. '-' + endfor + call assert_equal('a-é-c̀-d-', res) + + let res = '' + for c in '' + let res ..= c .. '-' + endfor + call assert_equal('', res) + + let res = '' + for c in v:_null_string + let res ..= c .. '-' + endfor + call assert_equal('', res) +endfunc + "------------------------------------------------------------------------------- " Modelines {{{1 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker diff --git a/src/nvim/testdir/test_virtualedit.vim b/src/nvim/testdir/test_virtualedit.vim index 8f992f7501..250b896532 100644 --- a/src/nvim/testdir/test_virtualedit.vim +++ b/src/nvim/testdir/test_virtualedit.vim @@ -81,6 +81,133 @@ func Test_edit_change() normal Cx call assert_equal('x', getline(1)) bwipe! + set virtualedit= +endfunc + +" Tests for pasting at the beginning, end and middle of a tab character +" in virtual edit mode. +func Test_paste_in_tab() + new + call append(0, '') + set virtualedit=all + + " Tests for pasting a register with characterwise mode type + call setreg('"', 'xyz', 'c') + + " paste (p) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal p + call assert_equal('a xyz b', getline(1)) + + " paste (P) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal P + call assert_equal("axyz\tb", getline(1)) + + " paste (p) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal p + call assert_equal("a\txyzb", getline(1)) + + " paste (P) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal P + call assert_equal('a xyz b', getline(1)) + + " Tests for pasting a register with blockwise mode type + call setreg('"', 'xyz', 'b') + + " paste (p) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal p + call assert_equal('a xyz b', getline(1)) + + " paste (P) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal P + call assert_equal("axyz\tb", getline(1)) + + " paste (p) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal p + call assert_equal("a\txyzb", getline(1)) + + " paste (P) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal P + call assert_equal('a xyz b', getline(1)) + + " Tests for pasting with gp and gP in virtual edit mode + + " paste (gp) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal gp + call assert_equal('a xyz b', getline(1)) + call assert_equal([0, 1, 12, 0, 12], getcurpos()) + + " paste (gP) unnamed register at the beginning of a tab + call setline(1, "a\tb") + call cursor(1, 2, 0) + normal gP + call assert_equal("axyz\tb", getline(1)) + call assert_equal([0, 1, 5, 0, 5], getcurpos()) + + " paste (gp) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal gp + call assert_equal("a\txyzb", getline(1)) + call assert_equal([0, 1, 6, 0, 12], getcurpos()) + + " paste (gP) unnamed register at the end of a tab + call setline(1, "a\tb") + call cursor(1, 2, 6) + normal gP + call assert_equal('a xyz b', getline(1)) + call assert_equal([0, 1, 12, 0, 12], getcurpos()) + + " Tests for pasting a named register + let @r = 'xyz' + + " paste (gp) named register in the middle of a tab + call setline(1, "a\tb") + call cursor(1, 2, 2) + normal "rgp + call assert_equal('a xyz b', getline(1)) + call assert_equal([0, 1, 8, 0, 8], getcurpos()) + + " paste (gP) named register in the middle of a tab + call setline(1, "a\tb") + call cursor(1, 2, 2) + normal "rgP + call assert_equal('a xyz b', getline(1)) + call assert_equal([0, 1, 7, 0, 7], getcurpos()) + + bwipe! + set virtualedit= +endfunc + +" Test for yanking a few spaces within a tab to a register +func Test_yank_in_tab() + new + let @r = '' + call setline(1, "a\tb") + set virtualedit=all + call cursor(1, 2, 2) + normal "ry5l + call assert_equal(' ', @r) + + bwipe! + set virtualedit= endfunc " Insert "keyword keyw", ESC, C CTRL-N, shows "keyword ykeyword". @@ -216,4 +343,139 @@ func Test_yank_paste_small_del_reg() set virtualedit= endfunc +" After calling s:TryVirtualeditReplace(), line 1 will contain one of these +" two strings, depending on whether virtual editing is on or off. +let s:result_ve_on = 'a x' +let s:result_ve_off = 'x' + +" Utility function for Test_global_local_virtualedit() +func s:TryVirtualeditReplace() + call setline(1, 'a') + normal gg7l + normal rx +endfunc + +" Test for :set and :setlocal +func Test_global_local_virtualedit() + new + + " Verify that 'virtualedit' is initialized to empty, can be set globally to + " all and to empty, and can be set locally to all and to empty. + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + set ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + set ve= + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve= + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + + " Verify that :set affects multiple windows. + split + set ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + wincmd p + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + set ve= + wincmd p + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + bwipe! + + " Verify that :setlocal affects only the current window. + new + split + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + wincmd p + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + bwipe! + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + + " Verify that the buffer 'virtualedit' state follows the global value only + " when empty and that "none" works as expected. + " + " 'virtualedit' State + " +--------+--------------------------+ + " | Local | Global | + " | | | + " +--------+--------+--------+--------+ + " | | "" | "all" | "none" | + " +--------+--------+--------+--------+ + " | "" | off | on | off | + " | "all" | on | on | on | + " | "none" | off | off | off | + " +--------+--------+--------+--------+ + new + + setglobal ve= + setlocal ve= + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve=none + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + + setglobal ve=all + setlocal ve= + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve=none + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + setlocal ve=NONE + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + + setglobal ve=none + setlocal ve= + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve=none + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + + bwipe! + + " Verify that the 'virtualedit' state is copied to new windows. + new + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + split + setlocal ve=all + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + split + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_on, getline(1)) + setlocal ve= + split + call s:TryVirtualeditReplace() + call assert_equal(s:result_ve_off, getline(1)) + bwipe! + + setlocal virtualedit& + set virtualedit& +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_visual.vim b/src/nvim/testdir/test_visual.vim index 8344598486..ae8f9ba70b 100644 --- a/src/nvim/testdir/test_visual.vim +++ b/src/nvim/testdir/test_visual.vim @@ -22,6 +22,14 @@ func Test_block_shift_overflow() q! endfunc +func Test_dotregister_paste() + new + exe "norm! ihello world\<esc>" + norm! 0ve".p + call assert_equal('hello world world', getline(1)) + q! +endfunc + func Test_Visual_ctrl_o() new call setline(1, ['one', 'two', 'three']) @@ -42,14 +50,6 @@ func Test_Visual_vapo() bwipe! endfunc -func Test_dotregister_paste() - new - exe "norm! ihello world\<esc>" - norm! 0ve".p - call assert_equal('hello world world', getline(1)) - q! -endfunc - func Test_Visual_inner_quote() new normal oxX @@ -57,9 +57,37 @@ func Test_Visual_inner_quote() bwipe! endfunc +" Test for Visual mode not being reset causing E315 error. +func TriggerTheProblem() + " At this point there is no visual selection because :call reset it. + " Let's restore the selection: + normal gv + '<,'>del _ + try + exe "normal \<Esc>" + catch /^Vim\%((\a\+)\)\=:E315/ + echom 'Snap! E315 error!' + let g:msg = 'Snap! E315 error!' + endtry +endfunc + +func Test_visual_mode_reset() + enew + let g:msg = "Everything's fine." + enew + setl buftype=nofile + call append(line('$'), 'Delete this line.') + + " NOTE: this has to be done by a call to a function because executing :del + " the ex-way will require the colon operator which resets the visual mode + " thus preventing the problem: + exe "normal! GV:call TriggerTheProblem()\<CR>" + call assert_equal("Everything's fine.", g:msg) +endfunc + " Test for visual block shift and tab characters. func Test_block_shift_tab() - enew! + new call append(0, repeat(['one two three'], 5)) call cursor(1,1) exe "normal i\<C-G>u" @@ -68,7 +96,7 @@ func Test_block_shift_tab() call assert_equal('on1 two three', getline(2)) call assert_equal('on1 two three', getline(5)) - enew! + %d _ call append(0, repeat(['abcdefghijklmnopqrstuvwxyz'], 5)) call cursor(1,1) exe "normal \<C-V>4jI \<Esc>j<<11|D" @@ -93,12 +121,26 @@ func Test_block_shift_tab() call assert_equal(" abc\<Tab>\<Tab>defghijklmnopqrstuvwxyz", getline(4)) call assert_equal(" abc\<Tab> defghijklmnopqrstuvwxyz", getline(5)) - enew! + " Test for block shift with space characters at the beginning and with + " 'noexpandtab' and 'expandtab' + %d _ + call setline(1, [" 1", " 2", " 3"]) + setlocal shiftwidth=2 noexpandtab + exe "normal gg\<C-V>3j>" + call assert_equal(["\t1", "\t2", "\t3"], getline(1, '$')) + %d _ + call setline(1, [" 1", " 2", " 3"]) + setlocal shiftwidth=2 expandtab + exe "normal gg\<C-V>3j>" + call assert_equal([" 1", " 2", " 3"], getline(1, '$')) + setlocal shiftwidth& + + bw! endfunc " Tests Blockwise Visual when there are TABs before the text. func Test_blockwise_visual() - enew! + new call append(0, ['123456', \ '234567', \ '345678', @@ -120,26 +162,31 @@ func Test_blockwise_visual() \ "\t\tsomext", \ "\t\ttesext"], getline(1, 7)) - enew! + bw! endfunc " Test swapping corners in blockwise visual mode with o and O func Test_blockwise_visual_o_O() - enew! + new exe "norm! 10i.\<Esc>Y4P3lj\<C-V>4l2jr " exe "norm! gvO\<Esc>ra" exe "norm! gvO\<Esc>rb" exe "norm! gvo\<C-c>rc" exe "norm! gvO\<C-c>rd" + set selection=exclusive + exe "norm! gvOo\<C-c>re" + call assert_equal('...a be.', getline(4)) + exe "norm! gvOO\<C-c>rf" + set selection& call assert_equal(['..........', \ '...c d..', \ '... ..', - \ '...a b..', + \ '...a bf.', \ '..........'], getline(1, '$')) - enew! + bw! endfun " Test Virtual replace mode. @@ -242,35 +289,6 @@ func Test_virtual_replace2() set bs&vim endfunc -" Test for Visual mode not being reset causing E315 error. -func TriggerTheProblem() - " At this point there is no visual selection because :call reset it. - " Let's restore the selection: - normal gv - '<,'>del _ - try - exe "normal \<Esc>" - catch /^Vim\%((\a\+)\)\=:E315/ - echom 'Snap! E315 error!' - let g:msg = 'Snap! E315 error!' - endtry -endfunc - -func Test_visual_mode_reset() - enew - let g:msg = "Everything's fine." - enew - setl buftype=nofile - call append(line('$'), 'Delete this line.') - - " NOTE: this has to be done by a call to a function because executing :del - " the ex-way will require the colon operator which resets the visual mode - " thus preventing the problem: - exe "normal! GV:call TriggerTheProblem()\<CR>" - call assert_equal("Everything's fine.", g:msg) - -endfunc - func Test_Visual_word_textobject() new call setline(1, ['First sentence. Second sentence.']) @@ -349,17 +367,6 @@ func Test_Visual_sentence_textobject() bwipe! endfunc -func Test_curswant_not_changed() - new - call setline(1, ['one', 'two']) - au InsertLeave * call getcurpos() - call feedkeys("gg0\<C-V>jI123 \<Esc>j", 'xt') - call assert_equal([0, 2, 1, 0, 1], getcurpos()) - - bwipe! - au! InsertLeave -endfunc - func Test_Visual_paragraph_textobject() new call setline(1, ['First line.', @@ -409,6 +416,17 @@ func Test_Visual_paragraph_textobject() bwipe! endfunc +func Test_curswant_not_changed() + new + call setline(1, ['one', 'two']) + au InsertLeave * call getcurpos() + call feedkeys("gg0\<C-V>jI123 \<Esc>j", 'xt') + call assert_equal([0, 2, 1, 0, 1], getcurpos()) + + bwipe! + au! InsertLeave +endfunc + " Tests for "vaBiB", end could be wrong. func Test_Visual_Block() new @@ -433,13 +451,15 @@ func Test_Visual_Block() close! endfunc -func Test_visual_put_in_block() +" Test for 'p'ut in visual block mode +func Test_visual_block_put() new - call setline(1, ['xxxx', 'y∞yy', 'zzzz']) - normal 1G2yl - exe "normal 1G2l\<C-V>jjlp" - call assert_equal(['xxxx', 'y∞xx', 'zzxx'], getline(1, 3)) - bwipe! + call append(0, ['One', 'Two', 'Three']) + normal gg + yank + call feedkeys("jl\<C-V>ljp", 'xt') + call assert_equal(['One', 'T', 'Tee', 'One', ''], getline(1, '$')) + bw! endfunc " Visual modes (v V CTRL-V) followed by an operator; count; repeating @@ -610,6 +630,17 @@ func Test_characterwise_visual_mode() normal Gkvj$d call assert_equal(['', 'a', ''], getline(1, '$')) + " characterwise visual mode: replace a single character line and the eol + %d _ + call setline(1, "a") + normal v$rx + call assert_equal(['x'], getline(1, '$')) + + " replace a character with composing characters + call setline(1, "xã̳x") + normal gg0lvrb + call assert_equal("xbx", getline(1)) + bwipe! endfunc @@ -645,6 +676,16 @@ func Test_characterwise_select_mode() exe "normal Gkgh\<Down>\<End>\<Del>" call assert_equal(['', 'a', ''], getline(1, '$')) + " CTRL-H in select mode behaves like 'x' + call setline(1, 'abcdef') + exe "normal! gggh\<Right>\<Right>\<Right>\<C-H>" + call assert_equal('ef', getline(1)) + + " CTRL-O in select mode switches to visual mode for one command + call setline(1, 'abcdef') + exe "normal! gggh\<C-O>3lm" + call assert_equal('mef', getline(1)) + sunmap <lt>End> sunmap <lt>Down> sunmap <lt>Del> @@ -744,8 +785,7 @@ endfunc func Test_visual_block_mode() new call append(0, '') - call setline(1, ['abcdefghijklm', 'abcdefghijklm', 'abcdefghijklm', - \ 'abcdefghijklm', 'abcdefghijklm']) + call setline(1, repeat(['abcdefghijklm'], 5)) call cursor(1, 1) " Test shift-right of a block @@ -764,6 +804,76 @@ func Test_visual_block_mode() \ 'axyzqqqqefgmnoklm', \ 'abcdqqqqijklm'], getline(1, 5)) + " Test 'C' to change till the end of the line + call cursor(3, 4) + exe "normal! \<C-V>j3lCooo" + call assert_equal(['axyooo', 'axyooo'], getline(3, 4)) + + " Test 'D' to delete till the end of the line + call cursor(3, 3) + exe "normal! \<C-V>j2lD" + call assert_equal(['ax', 'ax'], getline(3, 4)) + + " Test block insert with a short line that ends before the block + %d _ + call setline(1, [" one", "a", " two"]) + exe "normal gg\<C-V>2jIx" + call assert_equal([" xone", "a", " xtwo"], getline(1, '$')) + + " Test block append at EOL with '$' and without '$' + %d _ + call setline(1, ["one", "a", "two"]) + exe "normal gg$\<C-V>2jAx" + call assert_equal(["onex", "ax", "twox"], getline(1, '$')) + %d _ + call setline(1, ["one", "a", "two"]) + exe "normal gg3l\<C-V>2jAx" + call assert_equal(["onex", "a x", "twox"], getline(1, '$')) + + " Test block replace with an empty line in the middle and use $ to jump to + " the end of the line. + %d _ + call setline(1, ['one', '', 'two']) + exe "normal gg$\<C-V>2jrx" + call assert_equal(["onx", "", "twx"], getline(1, '$')) + + " Test block replace with an empty line in the middle and move cursor to the + " end of the line + %d _ + call setline(1, ['one', '', 'two']) + exe "normal gg2l\<C-V>2jrx" + call assert_equal(["onx", "", "twx"], getline(1, '$')) + + " Replace odd number of characters with a multibyte character + %d _ + call setline(1, ['abcd', 'efgh']) + exe "normal ggl\<C-V>2ljr\u1100" + call assert_equal(["a\u1100 ", "e\u1100 "], getline(1, '$')) + + " During visual block append, if the cursor moved outside of the selected + " range, then the edit should not be applied to the block. + %d _ + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "normal 2G\<C-V>jAx\<Up>" + call assert_equal(['aaa', 'bxbb', 'ccc'], getline(1, '$')) + + " During visual block append, if the cursor is moved before the start of the + " block, then the new text should be appended there. + %d _ + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "normal $\<C-V>2jA\<Left>x" + call assert_equal(['aaxa', 'bbxb', 'ccxc'], getline(1, '$')) + " Repeat the previous test but use 'l' to move the cursor instead of '$' + call setline(1, ['aaa', 'bbb', 'ccc']) + exe "normal! gg2l\<C-V>2jA\<Left>x" + call assert_equal(['aaxa', 'bbxb', 'ccxc'], getline(1, '$')) + + " Change a characterwise motion to a blockwise motion using CTRL-V + %d _ + call setline(1, ['123', '456', '789']) + exe "normal ld\<C-V>j" + call assert_equal(['13', '46', '789'], getline(1, '$')) + " Test from ':help v_b_I_example' %d _ setlocal tabstop=8 shiftwidth=4 @@ -995,6 +1105,15 @@ func Test_block_insert_replace_tabs() bwipe! endfunc +func Test_visual_put_in_block() + new + call setline(1, ['xxxx', 'y∞yy', 'zzzz']) + normal 1G2yl + exe "normal 1G2l\<C-V>jjlp" + call assert_equal(['xxxx', 'y∞xx', 'zzxx'], getline(1, 3)) + bwipe! +endfunc + func Test_visual_put_in_block_using_zp() new " paste using zP @@ -1090,6 +1209,13 @@ func Test_visual_put_blockedit_zy_and_zp() bw! endfunc +func Test_visual_block_yank_zy() + new + " this was reading before the start of the line + exe "norm o\<C-T>\<Esc>\<C-V>zy" + bwipe! +endfunc + func Test_visual_block_with_virtualedit() CheckScreendump @@ -1104,11 +1230,154 @@ func Test_visual_block_with_virtualedit() call term_sendkeys(buf, "\<C-V>gg$") call VerifyScreenDump(buf, 'Test_visual_block_with_virtualedit', {}) + call term_sendkeys(buf, "\<Esc>gg\<C-V>G$") + call VerifyScreenDump(buf, 'Test_visual_block_with_virtualedit2', {}) + " clean up call term_sendkeys(buf, "\<Esc>") call StopVimInTerminal(buf) - call delete('XTest_beval') + call delete('XTest_block') +endfunc + +func Test_visual_block_ctrl_w_f() + " Emtpy block selected in new buffer should not result in an error. + au! BufNew foo sil norm f + edit foo + + au! BufNew +endfunc + +func Test_visual_block_append_invalid_char() + " this was going over the end of the line + new + call setline(1, [' let xxx', 'xxxxx', 'xxxxxxxxxxx']) + exe "normal 0\<C-V>jjA-\<Esc>" + call assert_equal([' - let xxx', 'xxxxx -', 'xxxxxxxx-xxx'], getline(1, 3)) + bwipe! +endfunc + +func Test_visual_reselect_with_count() + " this was causing an illegal memory access + let lines =<< trim END + + + + : + r<sfile> + exe "%norm e3\<c-v>kr\t" + : + + : + END + call writefile(lines, 'XvisualReselect') + source XvisualReselect + + bwipe! + call delete('XvisualReselect') +endfunc + +func Test_visual_block_insert_round_off() + new + " The number of characters are tuned to fill a 4096 byte allocated block, + " so that valgrind reports going over the end. + call setline(1, ['xxxxx', repeat('0', 1350), "\t", repeat('x', 60)]) + exe "normal gg0\<C-V>GI" .. repeat('0', 1320) .. "\<Esc>" + bwipe! endfunc +" this was causing an ml_get error +func Test_visual_exchange_windows() + enew! + new + call setline(1, ['foo', 'bar']) + exe "normal G\<C-V>gg\<C-W>\<C-X>OO\<Esc>" + bwipe! + bwipe! +endfunc + +" this was leaving the end of the Visual area beyond the end of a line +func Test_visual_ex_copy_line() + new + call setline(1, ["aaa", "bbbbbbbbbxbb"]) + /x + exe "normal ggvjfxO" + t0 + normal gNU + bwipe! +endfunc + +" This was leaving the end of the Visual area beyond the end of a line. +" Set 'undolevels' to start a new undo block. +func Test_visual_undo_deletes_last_line() + new + call setline(1, ["aaa", "ccc", "dyd"]) + set undolevels=100 + exe "normal obbbbbbbbbxbb\<Esc>" + set undolevels=100 + /y + exe "normal ggvjfxO" + undo + normal gNU + + bwipe! +endfunc + +func Test_visual_paste() + new + + " v_p overwrites unnamed register. + call setline(1, ['xxxx']) + call setreg('"', 'foo') + call setreg('-', 'bar') + normal gg0vp + call assert_equal('x', @") + call assert_equal('x', @-) + call assert_equal('fooxxx', getline(1)) + normal $vp + call assert_equal('x', @") + call assert_equal('x', @-) + call assert_equal('fooxxx', getline(1)) + " Test with a different register as unnamed register. + call setline(2, ['baz']) + normal 2gg0"rD + call assert_equal('baz', @") + normal gg0vp + call assert_equal('f', @") + call assert_equal('f', @-) + call assert_equal('bazooxxx', getline(1)) + normal $vp + call assert_equal('x', @") + call assert_equal('x', @-) + call assert_equal('bazooxxf', getline(1)) + + if has('clipboard') + " v_P does not overwrite unnamed register. + call setline(1, ['xxxx']) + call setreg('"', 'foo') + call setreg('-', 'bar') + normal gg0vP + call assert_equal('foo', @") + call assert_equal('x', @-) + call assert_equal('fooxxx', getline(1)) + normal $vP + call assert_equal('foo', @") + call assert_equal('x', @-) + call assert_equal('fooxxfoo', getline(1)) + " Test with a different register as unnamed register. + call setline(2, ['baz']) + normal 2gg0"rD + call assert_equal('baz', @") + normal gg0vP + call assert_equal('baz', @") + call assert_equal('f', @-) + call assert_equal('bazooxxfoo', getline(1)) + normal $vP + call assert_equal('baz', @") + call assert_equal('o', @-) + call assert_equal('bazooxxfobaz', getline(1)) + endif + + bwipe! +endfunc " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_winbuf_close.vim b/src/nvim/testdir/test_winbuf_close.vim index 7f5b80e8d3..f4878c2397 100644 --- a/src/nvim/testdir/test_winbuf_close.vim +++ b/src/nvim/testdir/test_winbuf_close.vim @@ -194,3 +194,22 @@ func Test_tabwin_close() call assert_true(v:true) %bwipe! endfunc + +" Test when closing a split window (above/below) restores space to the window +" below when 'noequalalways' and 'splitright' are set. +func Test_window_close_splitright_noequalalways() + set noequalalways + set splitright + new + let w1 = win_getid() + new + let w2 = win_getid() + execute "normal \<c-w>b" + let h = winheight(0) + let w = win_getid() + new + q + call assert_equal(h, winheight(0), "Window height does not match eight before opening and closing another window") + call assert_equal(w, win_getid(), "Did not return to original window after opening and closing a window") +endfunc + diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim index a200bf7d42..ef6dec580f 100644 --- a/src/nvim/testdir/test_window_cmd.vim +++ b/src/nvim/testdir/test_window_cmd.vim @@ -575,7 +575,7 @@ func Test_winrestcmd() only endfunc -function! Fun_RenewFile() +func Fun_RenewFile() " Need to wait a bit for the timestamp to be older. let old_ftime = getftime("tmp.txt") while getftime("tmp.txt") == old_ftime @@ -585,7 +585,7 @@ function! Fun_RenewFile() sp wincmd p edit! tmp.txt -endfunction +endfunc func Test_window_prevwin() " Can we make this work on MS-Windows? @@ -939,4 +939,124 @@ func Test_window_resize() %bwipe! endfunc +func Test_win_move_separator() + edit a + leftabove vsplit b + let w = winwidth(0) + " check win_move_separator from left window on left window + call assert_equal(1, winnr()) + for offset in range(5) + call assert_true(win_move_separator(0, offset)) + call assert_equal(w + offset, winwidth(0)) + call assert_true(0->win_move_separator(-offset)) + call assert_equal(w, winwidth(0)) + endfor + " check win_move_separator from right window on left window number + wincmd l + call assert_notequal(1, winnr()) + for offset in range(5) + call assert_true(1->win_move_separator(offset)) + call assert_equal(w + offset, winwidth(1)) + call assert_true(win_move_separator(1, -offset)) + call assert_equal(w, winwidth(1)) + endfor + " check win_move_separator from right window on left window ID + let id = win_getid(1) + for offset in range(5) + call assert_true(win_move_separator(id, offset)) + call assert_equal(w + offset, winwidth(id)) + call assert_true(id->win_move_separator(-offset)) + call assert_equal(w, winwidth(id)) + endfor + " check win_move_separator from right window on right window is no-op + let w0 = winwidth(0) + call assert_true(win_move_separator(0, 1)) + call assert_equal(w0, winwidth(0)) + call assert_true(win_move_separator(0, -1)) + call assert_equal(w0, winwidth(0)) + " check that win_move_separator doesn't error with offsets beyond moving + " possibility + call assert_true(win_move_separator(id, 5000)) + call assert_true(winwidth(id) > w) + call assert_true(win_move_separator(id, -5000)) + call assert_true(winwidth(id) < w) + " check that win_move_separator returns false for an invalid window + wincmd = + let w = winwidth(0) + call assert_false(win_move_separator(-1, 1)) + call assert_equal(w, winwidth(0)) + " check that win_move_separator returns false for a floating window + let id = nvim_open_win( + \ 0, 0, #{relative: 'editor', row: 2, col: 2, width: 5, height: 3}) + let w = winwidth(id) + call assert_false(win_move_separator(id, 1)) + call assert_equal(w, winwidth(id)) + call nvim_win_close(id, 1) + %bwipe! +endfunc + +func Test_win_move_statusline() + redraw " This test fails in Nvim without a redraw to clear messages. + edit a + leftabove split b + let h = winheight(0) + " check win_move_statusline from top window on top window + call assert_equal(1, winnr()) + for offset in range(5) + call assert_true(win_move_statusline(0, offset)) + call assert_equal(h + offset, winheight(0)) + call assert_true(0->win_move_statusline(-offset)) + call assert_equal(h, winheight(0)) + endfor + " check win_move_statusline from bottom window on top window number + wincmd j + call assert_notequal(1, winnr()) + for offset in range(5) + call assert_true(1->win_move_statusline(offset)) + call assert_equal(h + offset, winheight(1)) + call assert_true(win_move_statusline(1, -offset)) + call assert_equal(h, winheight(1)) + endfor + " check win_move_statusline from bottom window on bottom window + let h0 = winheight(0) + for offset in range(5) + call assert_true(0->win_move_statusline(-offset)) + call assert_equal(h0 - offset, winheight(0)) + call assert_equal(1 + offset, &cmdheight) + call assert_true(win_move_statusline(0, offset)) + call assert_equal(h0, winheight(0)) + call assert_equal(1, &cmdheight) + endfor + call assert_true(win_move_statusline(0, 1)) + call assert_equal(h0, winheight(0)) + call assert_equal(1, &cmdheight) + " check win_move_statusline from bottom window on top window ID + let id = win_getid(1) + for offset in range(5) + call assert_true(win_move_statusline(id, offset)) + call assert_equal(h + offset, winheight(id)) + call assert_true(id->win_move_statusline(-offset)) + call assert_equal(h, winheight(id)) + endfor + " check that win_move_statusline doesn't error with offsets beyond moving + " possibility + call assert_true(win_move_statusline(id, 5000)) + call assert_true(winheight(id) > h) + call assert_true(win_move_statusline(id, -5000)) + call assert_true(winheight(id) < h) + " check that win_move_statusline returns false for an invalid window + wincmd = + let h = winheight(0) + call assert_false(win_move_statusline(-1, 1)) + call assert_equal(h, winheight(0)) + " check that win_move_statusline returns false for a floating window + let id = nvim_open_win( + \ 0, 0, #{relative: 'editor', row: 2, col: 2, width: 5, height: 3}) + let h = winheight(id) + call assert_false(win_move_statusline(id, 1)) + call assert_equal(h, winheight(id)) + call nvim_win_close(id, 1) + %bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_writefile.vim b/src/nvim/testdir/test_writefile.vim index aa7882d129..b42665c9b5 100644 --- a/src/nvim/testdir/test_writefile.vim +++ b/src/nvim/testdir/test_writefile.vim @@ -43,7 +43,7 @@ func Test_writefile_fails_gently() endfunc func Test_writefile_fails_conversion() - if !has('iconv') || system('uname -s') =~ 'SunOS' + if !has('iconv') || has('sun') return endif " Without a backup file the write won't happen if there is a conversion @@ -169,9 +169,7 @@ endfunc " Test for ':w !<cmd>' to pipe lines from the current buffer to an external " command. func Test_write_pipe_to_cmd() - if !has('unix') - return - endif + CheckUnix new call setline(1, ['L1', 'L2', 'L3', 'L4']) 2,3w !cat > Xfile @@ -371,6 +369,154 @@ func Test_write_file_encoding() %bw! endfunc +" Test for writing and reading a file starting with a BOM. +" Byte Order Mark (BOM) character for various encodings is below: +" UTF-8 : EF BB BF +" UTF-16 (BE): FE FF +" UTF-16 (LE): FF FE +" UTF-32 (BE): 00 00 FE FF +" UTF-32 (LE): FF FE 00 00 +func Test_readwrite_file_with_bom() + let utf8_bom = "\xEF\xBB\xBF" + let utf16be_bom = "\xFE\xFF" + let utf16le_bom = "\xFF\xFE" + let utf32be_bom = "\n\n\xFE\xFF" + let utf32le_bom = "\xFF\xFE\n\n" + let save_fileencoding = &fileencoding + set cpoptions+=S + + " Check that editing a latin1 file doesn't see a BOM + call writefile(["\xFE\xFElatin-1"], 'Xtest1') + edit Xtest1 + call assert_equal('latin1', &fileencoding) + call assert_equal(0, &bomb) + set fenc=latin1 + write Xfile2 + call assert_equal(["\xFE\xFElatin-1", ''], readfile('Xfile2', 'b')) + set bomb fenc=latin1 + write Xtest3 + call assert_equal(["\xFE\xFElatin-1", ''], readfile('Xtest3', 'b')) + set bomb& + + " Check utf-8 BOM + %bw! + call writefile([utf8_bom .. "utf-8"], 'Xtest1') + edit! Xtest1 + call assert_equal('utf-8', &fileencoding) + call assert_equal(1, &bomb) + call assert_equal('utf-8', getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal(['utf-8', ''], readfile('Xfile2', 'b')) + set fenc=utf-8 + w! Xtest3 + call assert_equal([utf8_bom .. "utf-8", ''], readfile('Xtest3', 'b')) + + " Check utf-8 with an error (will fall back to latin-1) + %bw! + call writefile([utf8_bom .. "utf-8\x80err"], 'Xtest1') + edit! Xtest1 + call assert_equal('latin1', &fileencoding) + call assert_equal(0, &bomb) + call assert_equal("\xC3\xAF\xC2\xBB\xC2\xBFutf-8\xC2\x80err", getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal([utf8_bom .. "utf-8\x80err", ''], readfile('Xfile2', 'b')) + set fenc=utf-8 + w! Xtest3 + call assert_equal(["\xC3\xAF\xC2\xBB\xC2\xBFutf-8\xC2\x80err", ''], + \ readfile('Xtest3', 'b')) + + " Check ucs-2 BOM + %bw! + call writefile([utf16be_bom .. "\nu\nc\ns\n-\n2\n"], 'Xtest1') + edit! Xtest1 + call assert_equal('utf-16', &fileencoding) + call assert_equal(1, &bomb) + call assert_equal('ucs-2', getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal(["ucs-2", ''], readfile('Xfile2', 'b')) + set fenc=ucs-2 + w! Xtest3 + call assert_equal([utf16be_bom .. "\nu\nc\ns\n-\n2\n", ''], + \ readfile('Xtest3', 'b')) + + " Check ucs-2le BOM + %bw! + call writefile([utf16le_bom .. "u\nc\ns\n-\n2\nl\ne\n"], 'Xtest1') + " Need to add a NUL byte after the NL byte + call writefile(0z00, 'Xtest1', 'a') + edit! Xtest1 + call assert_equal('utf-16le', &fileencoding) + call assert_equal(1, &bomb) + call assert_equal('ucs-2le', getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal(["ucs-2le", ''], readfile('Xfile2', 'b')) + set fenc=ucs-2le + w! Xtest3 + call assert_equal([utf16le_bom .. "u\nc\ns\n-\n2\nl\ne\n", "\n"], + \ readfile('Xtest3', 'b')) + + " Check ucs-4 BOM + %bw! + call writefile([utf32be_bom .. "\n\n\nu\n\n\nc\n\n\ns\n\n\n-\n\n\n4\n\n\n"], 'Xtest1') + edit! Xtest1 + call assert_equal('ucs-4', &fileencoding) + call assert_equal(1, &bomb) + call assert_equal('ucs-4', getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal(["ucs-4", ''], readfile('Xfile2', 'b')) + set fenc=ucs-4 + w! Xtest3 + call assert_equal([utf32be_bom .. "\n\n\nu\n\n\nc\n\n\ns\n\n\n-\n\n\n4\n\n\n", ''], readfile('Xtest3', 'b')) + + " Check ucs-4le BOM + %bw! + call writefile([utf32le_bom .. "u\n\n\nc\n\n\ns\n\n\n-\n\n\n4\n\n\nl\n\n\ne\n\n\n"], 'Xtest1') + " Need to add three NUL bytes after the NL byte + call writefile(0z000000, 'Xtest1', 'a') + edit! Xtest1 + call assert_equal('ucs-4le', &fileencoding) + call assert_equal(1, &bomb) + call assert_equal('ucs-4le', getline(1)) + set fenc=latin1 + write! Xfile2 + call assert_equal(["ucs-4le", ''], readfile('Xfile2', 'b')) + set fenc=ucs-4le + w! Xtest3 + call assert_equal([utf32le_bom .. "u\n\n\nc\n\n\ns\n\n\n-\n\n\n4\n\n\nl\n\n\ne\n\n\n", "\n\n\n"], readfile('Xtest3', 'b')) + + set cpoptions-=S + let &fileencoding = save_fileencoding + call delete('Xtest1') + call delete('Xfile2') + call delete('Xtest3') + %bw! +endfunc + +func Test_read_write_bin() + " write file missing EOL + call writefile(['noeol'], "XNoEolSetEol", 'bS') + call assert_equal(0z6E6F656F6C, readfile('XNoEolSetEol', 'B')) + + " when file is read 'eol' is off + set nofixeol + e! ++ff=unix XNoEolSetEol + call assert_equal(0, &eol) + + " writing with 'eol' set adds the newline + setlocal eol + w + call assert_equal(0z6E6F656F6C0A, readfile('XNoEolSetEol', 'B')) + + call delete('XNoEolSetEol') + set ff& + bwipe! XNoEolSetEol +endfunc + " Check that buffer is written before triggering QuitPre func Test_wq_quitpre_autocommand() edit Xsomefile diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 5fec41f9a5..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" @@ -227,7 +227,7 @@ static void forward_modified_utf8(TermInput *input, TermKeyKey *key) && !(key->modifiers & TERMKEY_KEYMOD_SHIFT) && ASCII_ISUPPER(key->code.codepoint)) { assert(len <= 62); - // Make remove for the S- + // Make room for the S- memmove(buf + 3, buf + 1, len - 1); buf[1] = 'S'; buf[2] = '-'; @@ -315,7 +315,6 @@ static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force) return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key); } -static void tinput_timer_cb(TimeWatcher *watcher, void *data); static void tk_getkeys(TermInput *input, bool force) { @@ -379,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/tui/tui.c b/src/nvim/tui/tui.c index e7a60aca49..58061f020d 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1877,7 +1877,7 @@ static void patch_terminfo_bugs(TUIData *data, const char *term, const char *col "\x1b[?c"); } else if (konsolev > 0 && konsolev < 180770) { // Konsole before version 18.07.70: set up a nonce profile. This has - // side-effects on temporary font resizing. #6798 + // side effects on temporary font resizing. #6798 data->unibi_ext.set_cursor_style = (int)unibi_add_ext_str(ut, "Ss", TMUX_WRAP(tmux, "\x1b]50;CursorShape=%?" diff --git a/src/nvim/types.h b/src/nvim/types.h index 604155c33e..73cd2204d6 100644 --- a/src/nvim/types.h +++ b/src/nvim/types.h @@ -32,4 +32,6 @@ typedef enum { kTrue = 1, } TriState; +typedef struct Decoration Decoration; + #endif // NVIM_TYPES_H 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/src/nvim/undo.c b/src/nvim/undo.c index d18f35a43a..2d8df4cad8 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -2633,6 +2633,10 @@ static void u_undo_end(bool did_undo, bool absolute, bool quiet) } } + if (VIsual_active) { + check_pos(curbuf, &VIsual); + } + smsg_attr_keep(0, _("%" PRId64 " %s; %s #%" PRId64 " %s"), u_oldcount < 0 ? (int64_t)-u_oldcount : (int64_t)u_oldcount, diff --git a/src/nvim/version.c b/src/nvim/version.c index 5e2a81795a..71cca52773 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -2093,7 +2093,7 @@ void list_in_columns(char_u **items, int size, int current) // The rightmost column doesn't need a separator. // Sacrifice it to fit in one more column if possible. - int ncol = (int)(Columns + 1) / width; + int ncol = (Columns + 1) / width; int nrow = item_count / ncol + (item_count % ncol ? 1 : 0); int cur_row = 1; diff --git a/src/nvim/vim.h b/src/nvim/vim.h index 2f8ddd1e88..6e0e9922a6 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -143,6 +143,7 @@ enum { EXPAND_COMPILER, EXPAND_USER_DEFINED, EXPAND_USER_LIST, + EXPAND_USER_LUA, EXPAND_SHELLCMD, EXPAND_CSCOPE, EXPAND_SIGN, diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c index ba6cfab98b..9d1318724e 100644 --- a/src/nvim/viml/parser/expressions.c +++ b/src/nvim/viml/parser/expressions.c @@ -1536,7 +1536,7 @@ static inline void east_set_error(const ParserState *const pstate, ExprASTError /* TODO(ZyX-I): Extend syntax to allow ${expr}. This is needed to */ \ /* handle environment variables like those bash uses for */ \ /* `export -f`: their names consist not only of alphanumeric */ \ - /* characetrs. */ \ + /* characters. */ \ case kExprNodeComplexIdentifier: \ case kExprNodePlainIdentifier: \ case kExprNodeCurlyBracesIdentifier: { \ @@ -1592,7 +1592,7 @@ typedef struct { /// string is a regex. /// @param[in] is_invalid Whether currently processed token is not valid. static void parse_quoted_string(ParserState *const pstate, ExprASTNode *const node, - const LexExprToken token, const ExprASTStack ast_stack, + const LexExprToken token, const ExprASTStack *ast_stack, const bool is_invalid) FUNC_ATTR_NONNULL_ALL { @@ -2907,7 +2907,7 @@ viml_pexpr_parse_no_paren_closing_error: {} ? kExprNodeDoubleQuotedString : kExprNodeSingleQuotedString)); *top_node_p = cur_node; - parse_quoted_string(pstate, cur_node, cur_token, ast_stack, is_invalid); + parse_quoted_string(pstate, cur_node, cur_token, &ast_stack, is_invalid); want_node = kENodeOperator; break; } diff --git a/src/nvim/viml/parser/expressions.h b/src/nvim/viml/parser/expressions.h index fe9327b27d..9d0bc9d468 100644 --- a/src/nvim/viml/parser/expressions.h +++ b/src/nvim/viml/parser/expressions.h @@ -57,7 +57,7 @@ typedef enum { } LexExprTokenType; typedef enum { - kExprCmpEqual, ///< Equality, unequality. + kExprCmpEqual, ///< Equality, inequality. kExprCmpMatches, ///< Matches regex, not matches regex. kExprCmpGreater, ///< `>` or `<=` kExprCmpGreaterOrEqual, ///< `>=` or `<`. diff --git a/src/nvim/window.c b/src/nvim/window.c index c711f462d1..83048d911f 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -304,7 +304,7 @@ newwindow: newtab = curtab; goto_tabpage_tp(oldtab, true, true); if (curwin == wp) { - win_close(curwin, false); + win_close(curwin, false, false); } if (valid_tabpage(newtab)) { goto_tabpage_tp(newtab, true, true); @@ -449,7 +449,7 @@ wingotofile: RESET_BINDING(curwin); if (do_ecmd(0, ptr, NULL, NULL, ECMD_LASTL, ECMD_HIDE, NULL) == FAIL) { // Failed to open the file, close the window opened for it. - win_close(curwin, false); + win_close(curwin, false, false); goto_tabpage_win(oldtab, oldwin); } else if (nchar == 'F' && lnum >= 0) { curwin->w_cursor.lnum = lnum; @@ -577,18 +577,19 @@ static void cmd_with_count(char *cmd, char_u *bufp, size_t bufsize, int64_t Pren void win_set_buf(Window window, Buffer buffer, bool noautocmd, Error *err) { - win_T *win = find_window_by_handle(window, err), *save_curwin = curwin; + win_T *win = find_window_by_handle(window, err); buf_T *buf = find_buffer_by_handle(buffer, err); - tabpage_T *tab = win_find_tabpage(win), *save_curtab = curtab; + tabpage_T *tab = win_find_tabpage(win); if (!win || !buf) { return; } - if (noautocmd) { block_autocmds(); } - if (switch_win_noblock(&save_curwin, &save_curtab, win, tab, false) == FAIL) { + + switchwin_T switchwin; + if (switch_win_noblock(&switchwin, win, tab, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to switch to window %d", @@ -608,7 +609,7 @@ void win_set_buf(Window window, Buffer buffer, bool noautocmd, Error *err) // So do it now. validate_cursor(); - restore_win_noblock(save_curwin, save_curtab, false); + restore_win_noblock(&switchwin, false); if (noautocmd) { unblock_autocmds(); } @@ -957,6 +958,11 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir) int wmh1; bool did_set_fraction = false; + // aucmd_win should always remain floating + if (new_wp != NULL && new_wp == aucmd_win) { + return FAIL; + } + if (flags & WSP_TOP) { oldwin = firstwin; } else if (flags & WSP_BOT || curwin->w_floating) { @@ -1481,8 +1487,6 @@ static void win_init(win_T *newp, win_T *oldp, int flags) copyFoldingState(oldp, newp); win_init_some(newp, oldp); - - didset_window_options(newp); } /* @@ -1591,7 +1595,7 @@ int make_windows(int count, bool vertical) int todo; if (vertical) { - // Each windows needs at least 'winminwidth' lines and a separator + // Each window needs at least 'winminwidth' lines and a separator // column. maxcount = (curwin->w_width + curwin->w_vsep_width - (p_wiw - p_wmw)) / (p_wmw + 1); @@ -1728,6 +1732,12 @@ static void win_exchange(long Prenum) (void)win_comp_pos(); // recompute window positions + if (wp->w_buffer != curbuf) { + reset_VIsual_and_resel(); + } else if (VIsual_active) { + wp->w_cursor = curwin->w_cursor; + } + win_enter(wp, true); redraw_later(curwin, NOT_VALID); redraw_later(wp, NOT_VALID); @@ -1828,6 +1838,9 @@ static void win_totop(int size, int flags) beep_flush(); return; } + if (curwin == aucmd_win) { + return; + } if (curwin->w_floating) { ui_comp_remove_grid(&curwin->w_grid_alloc); @@ -2119,7 +2132,7 @@ static void win_equal_rec(win_T *next_curwin, bool current, frame_T *topfr, int m = frame_minheight(topfr, next_curwin); room = height - m; if (room < 0) { - // The room is less then 'winheight', use all space for the + // The room is less than 'winheight', use all space for the // current window. next_curwin_size = p_wh + room; room = 0; @@ -2289,7 +2302,7 @@ void close_windows(buf_T *buf, int keep_curwin) for (win_T *wp = firstwin; wp != NULL && !ONE_WINDOW;) { if (wp->w_buffer == buf && (!keep_curwin || wp != curwin) && !(wp->w_closing || wp->w_buffer->b_locked > 0)) { - if (win_close(wp, false) == FAIL) { + if (win_close(wp, false, false) == FAIL) { // If closing the window fails give up, to avoid looping forever. break; } @@ -2367,6 +2380,22 @@ bool last_nonfloat(win_T *wp) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT return wp != NULL && firstwin == wp && !(wp->w_next && !wp->w_floating); } +/// Check if floating windows can be closed. +/// +/// @return true if all floating windows can be closed +static bool can_close_floating_windows(tabpage_T *tab) +{ + FOR_ALL_WINDOWS_IN_TAB(wp, tab) { + buf_T *buf = wp->w_buffer; + int need_hide = (bufIsChanged(buf) && buf->b_nwindows <= 1); + + if (need_hide && !buf_hide(buf)) { + return false; + } + } + return true; +} + /// Close the possibly last window in a tab page. /// /// @param win window to close @@ -2431,7 +2460,7 @@ static bool close_last_window_tabpage(win_T *win, bool free_buf, tabpage_T *prev // // Called by :quit, :close, :xit, :wq and findtag(). // Returns FAIL when the window was not closed. -int win_close(win_T *win, bool free_buf) +int win_close(win_T *win, bool free_buf, bool force) { win_T *wp; bool other_buffer = false; @@ -2461,9 +2490,18 @@ int win_close(win_T *win, bool free_buf) } if ((firstwin == win && lastwin_nofloating() == win) && lastwin->w_floating) { - // TODO(bfredl): we might close the float also instead - emsg(e_floatonly); - return FAIL; + if (force || can_close_floating_windows(curtab)) { + win_T *nextwp; + for (win_T *wpp = firstwin; wpp != NULL; wpp = nextwp) { + nextwp = wpp->w_next; + if (wpp->w_floating) { + win_close(wpp, free_buf, force); + } + } + } else { + emsg(e_floatonly); + return FAIL; + } } // When closing the last window in a tab page first go to another tab page @@ -3055,9 +3093,21 @@ static frame_T *win_altframe(win_T *win, tabpage_T *tp) return frp->fr_prev; } + // By default the next window will get the space that was abandoned by this + // window frame_T *target_fr = frp->fr_next; frame_T *other_fr = frp->fr_prev; - if (p_spr || p_sb) { + + // If this is part of a column of windows and 'splitbelow' is true then the + // previous window will get the space. + if (frp->fr_parent != NULL && frp->fr_parent->fr_layout == FR_COL && p_sb) { + target_fr = frp->fr_prev; + other_fr = frp->fr_next; + } + + // If this is part of a row of windows, and 'splitright' is true then the + // previous window will get the space. + if (frp->fr_parent != NULL && frp->fr_parent->fr_layout == FR_ROW && p_spr) { target_fr = frp->fr_prev; other_fr = frp->fr_next; } @@ -3610,7 +3660,9 @@ void close_others(int message, int forceit) continue; } } - win_close(wp, !buf_hide(wp->w_buffer) && !bufIsChanged(wp->w_buffer)); + win_close(wp, + !buf_hide(wp->w_buffer) && !bufIsChanged(wp->w_buffer), + false); } if (message && !ONE_WINDOW) { @@ -4644,20 +4696,29 @@ void fix_current_dir(void) globaldir = (char_u *)xstrdup(cwd); } } + bool dir_differs = pathcmp(new_dir, cwd, -1) != 0; + if (!p_acd && dir_differs) { + do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage, + kCdCauseWindow, true); + } if (os_chdir(new_dir) == 0) { - if (!p_acd && pathcmp(new_dir, cwd, -1) != 0) { - do_autocmd_dirchanged(new_dir, curwin->w_localdir - ? kCdScopeWindow : kCdScopeTabpage, kCdCauseWindow); + if (!p_acd && dir_differs) { + do_autocmd_dirchanged(new_dir, curwin->w_localdir ? kCdScopeWindow : kCdScopeTabpage, + kCdCauseWindow, false); } - last_chdir_reason = NULL; - shorten_fnames(true); } + last_chdir_reason = NULL; + shorten_fnames(true); } else if (globaldir != NULL) { // Window doesn't have a local directory and we are not in the global // directory: Change to the global directory. + bool dir_differs = pathcmp((char *)globaldir, cwd, -1) != 0; + if (!p_acd && dir_differs) { + do_autocmd_dirchanged((char *)globaldir, kCdScopeGlobal, kCdCauseWindow, true); + } if (os_chdir((char *)globaldir) == 0) { - if (!p_acd && pathcmp((char *)globaldir, cwd, -1) != 0) { - do_autocmd_dirchanged((char *)globaldir, kCdScopeGlobal, kCdCauseWindow); + if (!p_acd && dir_differs) { + do_autocmd_dirchanged((char *)globaldir, kCdScopeGlobal, kCdCauseWindow, false); } } XFREE_CLEAR(globaldir); @@ -5795,7 +5856,6 @@ void win_drag_vsep_line(win_T *dragwin, int offset) } fr = curfr; // put fr at window that grows } - assert(fr); // Not enough room if (room < offset) { @@ -5808,7 +5868,9 @@ void win_drag_vsep_line(win_T *dragwin, int offset) } if (fr == NULL) { - return; // Safety check, should not happen. + // This can happen when calling win_move_separator() on the rightmost + // window. Just don't do anything. + return; } // grow frame fr by offset lines @@ -6630,20 +6692,27 @@ static win_T *get_snapshot_focus(int idx) /// triggered, another tabpage access is limited. /// /// @return FAIL if switching to "win" failed. -int switch_win(win_T **save_curwin, tabpage_T **save_curtab, win_T *win, tabpage_T *tp, - bool no_display) +int switch_win(switchwin_T *switchwin, win_T *win, tabpage_T *tp, bool no_display) { block_autocmds(); - return switch_win_noblock(save_curwin, save_curtab, win, tp, no_display); + return switch_win_noblock(switchwin, win, tp, no_display); } // As switch_win() but without blocking autocommands. -int switch_win_noblock(win_T **save_curwin, tabpage_T **save_curtab, win_T *win, tabpage_T *tp, - bool no_display) +int switch_win_noblock(switchwin_T *switchwin, win_T *win, tabpage_T *tp, bool no_display) { - *save_curwin = curwin; + memset(switchwin, 0, sizeof(switchwin_T)); + switchwin->sw_curwin = curwin; + if (win == curwin) { + switchwin->sw_same_win = true; + } else { + // Disable Visual selection, because redrawing may fail. + switchwin->sw_visual_active = VIsual_active; + VIsual_active = false; + } + if (tp != NULL) { - *save_curtab = curtab; + switchwin->sw_curtab = curtab; if (no_display) { curtab->tp_firstwin = firstwin; curtab->tp_lastwin = lastwin; @@ -6665,28 +6734,33 @@ int switch_win_noblock(win_T **save_curwin, tabpage_T **save_curtab, win_T *win, // Restore current tabpage and window saved by switch_win(), if still valid. // When "no_display" is true the display won't be affected, no redraw is // triggered. -void restore_win(win_T *save_curwin, tabpage_T *save_curtab, bool no_display) +void restore_win(switchwin_T *switchwin, bool no_display) { - restore_win_noblock(save_curwin, save_curtab, no_display); + restore_win_noblock(switchwin, no_display); unblock_autocmds(); } // As restore_win() but without unblocking autocommands. -void restore_win_noblock(win_T *save_curwin, tabpage_T *save_curtab, bool no_display) +void restore_win_noblock(switchwin_T *switchwin, bool no_display) { - if (save_curtab != NULL && valid_tabpage(save_curtab)) { + if (switchwin->sw_curtab != NULL && valid_tabpage(switchwin->sw_curtab)) { if (no_display) { curtab->tp_firstwin = firstwin; curtab->tp_lastwin = lastwin; - curtab = save_curtab; + curtab = switchwin->sw_curtab; firstwin = curtab->tp_firstwin; lastwin = curtab->tp_lastwin; } else { - goto_tabpage_tp(save_curtab, false, false); + goto_tabpage_tp(switchwin->sw_curtab, false, false); } } - if (win_valid(save_curwin)) { - curwin = save_curwin; + + if (!switchwin->sw_same_win) { + VIsual_active = switchwin->sw_visual_active; + } + + if (win_valid(switchwin->sw_curwin)) { + curwin = switchwin->sw_curwin; curbuf = curwin->w_buffer; } // If called by win_execute() and executing the command changed the diff --git a/src/nvim/window.h b/src/nvim/window.h index 7e465a9f08..e2fd2c515d 100644 --- a/src/nvim/window.h +++ b/src/nvim/window.h @@ -4,6 +4,7 @@ #include <stdbool.h> #include "nvim/buffer_defs.h" +#include "nvim/mark.h" // Values for file_name_in_line() #define FNAME_MESS 1 // give error message @@ -32,6 +33,38 @@ #define MIN_COLUMNS 12 // minimal columns for screen #define MIN_LINES 2 // minimal lines for screen +/// Structure used by switch_win() to pass values to restore_win() +typedef struct { + win_T *sw_curwin; + tabpage_T *sw_curtab; + bool sw_same_win; ///< VIsual_active was not reset + bool sw_visual_active; +} switchwin_T; + +/// Execute a block of code in the context of window `wp` in tabpage `tp`. +/// Ensures the status line is redrawn and cursor position is valid if it is moved. +#define WIN_EXECUTE(wp, tp, block) \ + do { \ + win_T *const wp_ = (wp); \ + const pos_T curpos_ = wp_->w_cursor; \ + switchwin_T switchwin_; \ + if (switch_win_noblock(&switchwin_, wp_, (tp), true) == OK) { \ + check_cursor(); \ + block; \ + } \ + restore_win_noblock(&switchwin_, true); \ + /* Update the status line if the cursor moved. */ \ + if (win_valid(wp_) && !equalpos(curpos_, wp_->w_cursor)) { \ + wp_->w_redr_status = true; \ + } \ + /* In case the command moved the cursor or changed the Visual area, */ \ + /* check it is valid. */ \ + check_cursor(); \ + if (VIsual_active) { \ + check_pos(curbuf, &VIsual); \ + } \ + } while (false) + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "window.h.generated.h" #endif |