diff options
author | Justin M. Keyes <justinkz@gmail.com> | 2019-07-27 22:56:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-07-27 22:56:05 +0200 |
commit | 8e6b0a73c91bb8650e21f56183c7fa83f6fb312f (patch) | |
tree | ac70d41d2179452b01fd618afeb6d0e39be26576 | |
parent | 0e23ee3cc77960f5348dfa6eeb56e97432019126 (diff) | |
parent | b6278bbf12dd4946095b76f47b7c2ace3f929245 (diff) | |
download | rneovim-8e6b0a73c91bb8650e21f56183c7fa83f6fb312f.tar.gz rneovim-8e6b0a73c91bb8650e21f56183c7fa83f6fb312f.tar.bz2 rneovim-8e6b0a73c91bb8650e21f56183c7fa83f6fb312f.zip |
Merge #10619 'API: context'
-rw-r--r-- | runtime/doc/eval.txt | 6 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 62 | ||||
-rw-r--r-- | src/nvim/context.c | 383 | ||||
-rw-r--r-- | src/nvim/context.h | 46 | ||||
-rw-r--r-- | src/nvim/eval.c | 178 | ||||
-rw-r--r-- | src/nvim/eval.h | 7 | ||||
-rw-r--r-- | src/nvim/eval.lua | 5 | ||||
-rw-r--r-- | src/nvim/mark.c | 4 | ||||
-rw-r--r-- | src/nvim/memory.c | 2 | ||||
-rw-r--r-- | src/nvim/ops.c | 58 | ||||
-rw-r--r-- | src/nvim/shada.c | 302 | ||||
-rw-r--r-- | src/nvim/shada.h | 11 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 60 | ||||
-rw-r--r-- | test/functional/eval/ctx_functions_spec.lua | 408 | ||||
-rw-r--r-- | test/functional/helpers.lua | 19 |
15 files changed, 1427 insertions, 124 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 01bd2a0c65..cfcfa8e929 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -9110,9 +9110,9 @@ functions. *:fu* *:function* *E128* *E129* *E123* :fu[nction] List all functions and their arguments. -:fu[nction] {name} List function {name}. - {name} can also be a |Dictionary| entry that is a - |Funcref|: > +:fu[nction][!] {name} List function {name}, annotated with line numbers + unless "!" is given. + {name} may be a |Dictionary| |Funcref| entry: > :function dict.init :fu[nction] /{pattern} List functions with a name matching {pattern}. diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index dbe3b66fd5..ed6a28bcda 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -21,6 +21,7 @@ #include "nvim/lua/executor.h" #include "nvim/vim.h" #include "nvim/buffer.h" +#include "nvim/context.h" #include "nvim/file_search.h" #include "nvim/highlight.h" #include "nvim/window.h" @@ -1268,6 +1269,67 @@ Dictionary nvim_get_color_map(void) return colors; } +/// Gets a map of the current editor state. +/// +/// @param types Context types ("regs", "jumps", "buflist", "gvars", ...) +/// to gather, or NIL for all. +/// +/// @return map of global context +Dictionary nvim_get_context(Array types) + FUNC_API_SINCE(6) +{ + int int_types = 0; + if (types.size == 1 && types.items[0].type == kObjectTypeNil) { + int_types = kCtxAll; + } else { + for (size_t i = 0; i < types.size; i++) { + if (types.items[i].type == kObjectTypeString) { + const char *const current = types.items[i].data.string.data; + if (strequal(current, "regs")) { + int_types |= kCtxRegs; + } else if (strequal(current, "jumps")) { + int_types |= kCtxJumps; + } else if (strequal(current, "buflist")) { + int_types |= kCtxBuflist; + } else if (strequal(current, "gvars")) { + int_types |= kCtxGVars; + } else if (strequal(current, "sfuncs")) { + int_types |= kCtxSFuncs; + } else if (strequal(current, "funcs")) { + int_types |= kCtxFuncs; + } + } + } + } + + Context ctx = CONTEXT_INIT; + ctx_save(&ctx, int_types); + Dictionary dict = ctx_to_dict(&ctx); + ctx_free(&ctx); + return dict; +} + +/// Sets the current editor state to that in given context dictionary. +/// +/// @param ctx_dict Context dictionary. +Object nvim_load_context(Dictionary dict) + FUNC_API_SINCE(6) +{ + Context ctx = CONTEXT_INIT; + + int save_did_emsg = did_emsg; + did_emsg = false; + + ctx_from_dict(dict, &ctx); + if (!did_emsg) { + ctx_restore(&ctx, kCtxAll); + } + + ctx_free(&ctx); + + did_emsg = save_did_emsg; + return (Object)OBJECT_INIT; +} /// Gets the current mode. |mode()| /// "blocking" is true if Nvim is waiting for input. diff --git a/src/nvim/context.c b/src/nvim/context.c new file mode 100644 index 0000000000..b2a2fd3fd9 --- /dev/null +++ b/src/nvim/context.c @@ -0,0 +1,383 @@ +// 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 + +// Context: snapshot of the entire editor state as one big object/map + +#include "nvim/context.h" +#include "nvim/eval/encode.h" +#include "nvim/ex_docmd.h" +#include "nvim/option.h" +#include "nvim/shada.h" +#include "nvim/api/vim.h" +#include "nvim/api/private/helpers.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "context.c.generated.h" +#endif + +int kCtxAll = (kCtxRegs | kCtxJumps | kCtxBuflist | kCtxGVars | kCtxSFuncs + | kCtxFuncs); + +static ContextVec ctx_stack = KV_INITIAL_VALUE; + +/// Clears and frees the context stack +void ctx_free_all(void) +{ + for (size_t i = 0; i < kv_size(ctx_stack); i++) { + ctx_free(&kv_A(ctx_stack, i)); + } + kv_destroy(ctx_stack); +} + +/// Returns the size of the context stack. +size_t ctx_size(void) +{ + return kv_size(ctx_stack); +} + +/// Returns pointer to Context object with given zero-based index from the top +/// of context stack or NULL if index is out of bounds. +Context *ctx_get(size_t index) +{ + if (index < kv_size(ctx_stack)) { + return &kv_Z(ctx_stack, index); + } + return NULL; +} + +/// Free resources used by Context object. +/// +/// param[in] ctx pointer to Context object to free. +void ctx_free(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + if (ctx->regs.data) { + msgpack_sbuffer_destroy(&ctx->regs); + } + if (ctx->jumps.data) { + msgpack_sbuffer_destroy(&ctx->jumps); + } + if (ctx->buflist.data) { + msgpack_sbuffer_destroy(&ctx->buflist); + } + if (ctx->gvars.data) { + msgpack_sbuffer_destroy(&ctx->gvars); + } + if (ctx->funcs.items) { + api_free_array(ctx->funcs); + } +} + +/// Saves the editor state to a context. +/// +/// If "context" is NULL, pushes context on context stack. +/// Use "flags" to select particular types of context. +/// +/// @param ctx Save to this context, or push on context stack if NULL. +/// @param flags Flags, see ContextTypeFlags enum. +void ctx_save(Context *ctx, const int flags) +{ + if (ctx == NULL) { + kv_push(ctx_stack, CONTEXT_INIT); + ctx = &kv_last(ctx_stack); + } + + if (flags & kCtxRegs) { + ctx_save_regs(ctx); + } + + if (flags & kCtxJumps) { + ctx_save_jumps(ctx); + } + + if (flags & kCtxBuflist) { + ctx_save_buflist(ctx); + } + + if (flags & kCtxGVars) { + ctx_save_gvars(ctx); + } + + if (flags & kCtxFuncs) { + ctx_save_funcs(ctx, false); + } else if (flags & kCtxSFuncs) { + ctx_save_funcs(ctx, true); + } +} + +/// Restores the editor state from a context. +/// +/// If "context" is NULL, pops context from context stack. +/// Use "flags" to select particular types of context. +/// +/// @param ctx Restore from this context. Pop from context stack if NULL. +/// @param flags Flags, see ContextTypeFlags enum. +/// +/// @return true on success, false otherwise (i.e.: empty context stack). +bool ctx_restore(Context *ctx, const int flags) +{ + bool free_ctx = false; + if (ctx == NULL) { + if (ctx_stack.size == 0) { + return false; + } + ctx = &kv_pop(ctx_stack); + free_ctx = true; + } + + char_u *op_shada; + get_option_value((char_u *)"shada", NULL, &op_shada, OPT_GLOBAL); + set_option_value("shada", 0L, "!,'100,%", OPT_GLOBAL); + + if (flags & kCtxRegs) { + ctx_restore_regs(ctx); + } + + if (flags & kCtxJumps) { + ctx_restore_jumps(ctx); + } + + if (flags & kCtxBuflist) { + ctx_restore_buflist(ctx); + } + + if (flags & kCtxGVars) { + ctx_restore_gvars(ctx); + } + + if (flags & kCtxFuncs) { + ctx_restore_funcs(ctx); + } + + if (free_ctx) { + ctx_free(ctx); + } + + set_option_value("shada", 0L, (char *)op_shada, OPT_GLOBAL); + xfree(op_shada); + + return true; +} + +/// Saves the global registers to a context. +/// +/// @param ctx Save to this context. +static inline void ctx_save_regs(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer_init(&ctx->regs); + shada_encode_regs(&ctx->regs); +} + +/// Restores the global registers from a context. +/// +/// @param ctx Restore from this context. +static inline void ctx_restore_regs(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + shada_read_sbuf(&ctx->regs, kShaDaWantInfo | kShaDaForceit); +} + +/// Saves the jumplist to a context. +/// +/// @param ctx Save to this context. +static inline void ctx_save_jumps(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer_init(&ctx->jumps); + shada_encode_jumps(&ctx->jumps); +} + +/// Restores the jumplist from a context. +/// +/// @param ctx Restore from this context. +static inline void ctx_restore_jumps(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + shada_read_sbuf(&ctx->jumps, kShaDaWantInfo | kShaDaForceit); +} + +/// Saves the buffer list to a context. +/// +/// @param ctx Save to this context. +static inline void ctx_save_buflist(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer_init(&ctx->buflist); + shada_encode_buflist(&ctx->buflist); +} + +/// Restores the buffer list from a context. +/// +/// @param ctx Restore from this context. +static inline void ctx_restore_buflist(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + shada_read_sbuf(&ctx->buflist, kShaDaWantInfo | kShaDaForceit); +} + +/// Saves global variables to a context. +/// +/// @param ctx Save to this context. +static inline void ctx_save_gvars(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer_init(&ctx->gvars); + shada_encode_gvars(&ctx->gvars); +} + +/// Restores global variables from a context. +/// +/// @param ctx Restore from this context. +static inline void ctx_restore_gvars(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + shada_read_sbuf(&ctx->gvars, kShaDaWantInfo | kShaDaForceit); +} + +/// Saves functions to a context. +/// +/// @param ctx Save to this context. +/// @param scriptonly Save script-local (s:) functions only. +static inline void ctx_save_funcs(Context *ctx, bool scriptonly) + FUNC_ATTR_NONNULL_ALL +{ + ctx->funcs = (Array)ARRAY_DICT_INIT; + Error err = ERROR_INIT; + + HASHTAB_ITER(&func_hashtab, hi, { + const char_u *const name = hi->hi_key; + bool islambda = (STRNCMP(name, "<lambda>", 8) == 0); + bool isscript = (name[0] == K_SPECIAL); + + if (!islambda && (!scriptonly || isscript)) { + size_t cmd_len = sizeof("func! ") + STRLEN(name); + char *cmd = xmalloc(cmd_len); + snprintf(cmd, cmd_len, "func! %s", name); + String func_body = nvim_command_output(cstr_as_string(cmd), &err); + xfree(cmd); + if (!ERROR_SET(&err)) { + ADD(ctx->funcs, STRING_OBJ(func_body)); + } + api_clear_error(&err); + } + }); +} + +/// Restores functions from a context. +/// +/// @param ctx Restore from this context. +static inline void ctx_restore_funcs(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + for (size_t i = 0; i < ctx->funcs.size; i++) { + do_cmdline_cmd(ctx->funcs.items[i].data.string.data); + } +} + +/// Convert msgpack_sbuffer to readfile()-style array. +/// +/// @param[in] sbuf msgpack_sbuffer to convert. +/// +/// @return readfile()-style array representation of "sbuf". +static inline Array sbuf_to_array(msgpack_sbuffer sbuf) +{ + list_T *const list = tv_list_alloc(kListLenMayKnow); + tv_list_append_string(list, "", 0); + if (sbuf.size > 0) { + encode_list_write(list, sbuf.data, sbuf.size); + } + + typval_T list_tv = (typval_T) { + .v_lock = VAR_UNLOCKED, + .v_type = VAR_LIST, + .vval.v_list = list + }; + + Array array = vim_to_object(&list_tv).data.array; + tv_clear(&list_tv); + return array; +} + +/// Convert readfile()-style array to msgpack_sbuffer. +/// +/// @param[in] array readfile()-style array to convert. +/// +/// @return msgpack_sbuffer with conversion result. +static inline msgpack_sbuffer array_to_sbuf(Array array) +{ + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + + typval_T list_tv; + Error err = ERROR_INIT; + object_to_vim(ARRAY_OBJ(array), &list_tv, &err); + + if (!encode_vim_list_to_buf(list_tv.vval.v_list, &sbuf.size, &sbuf.data)) { + EMSG(_("E474: Failed to convert list to msgpack string buffer")); + } + sbuf.alloc = sbuf.size; + + tv_clear(&list_tv); + api_clear_error(&err); + return sbuf; +} + +/// Converts Context to Dictionary representation. +/// +/// @param[in] ctx Context to convert. +/// +/// @return Dictionary representing "ctx". +Dictionary ctx_to_dict(Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + assert(ctx != NULL); + + Dictionary rv = ARRAY_DICT_INIT; + + PUT(rv, "regs", ARRAY_OBJ(sbuf_to_array(ctx->regs))); + PUT(rv, "jumps", ARRAY_OBJ(sbuf_to_array(ctx->jumps))); + PUT(rv, "buflist", ARRAY_OBJ(sbuf_to_array(ctx->buflist))); + PUT(rv, "gvars", ARRAY_OBJ(sbuf_to_array(ctx->gvars))); + PUT(rv, "funcs", ARRAY_OBJ(copy_array(ctx->funcs))); + + return rv; +} + +/// Converts Dictionary representation of Context back to Context object. +/// +/// @param[in] dict Context Dictionary representation. +/// @param[out] ctx Context object to store conversion result into. +/// +/// @return types of included context items. +int ctx_from_dict(Dictionary dict, Context *ctx) + FUNC_ATTR_NONNULL_ALL +{ + assert(ctx != NULL); + + int types = 0; + for (size_t i = 0; i < dict.size; i++) { + KeyValuePair item = dict.items[i]; + if (item.value.type != kObjectTypeArray) { + continue; + } + if (strequal(item.key.data, "regs")) { + types |= kCtxRegs; + ctx->regs = array_to_sbuf(item.value.data.array); + } else if (strequal(item.key.data, "jumps")) { + types |= kCtxJumps; + ctx->jumps = array_to_sbuf(item.value.data.array); + } else if (strequal(item.key.data, "buflist")) { + types |= kCtxBuflist; + ctx->buflist = array_to_sbuf(item.value.data.array); + } else if (strequal(item.key.data, "gvars")) { + types |= kCtxGVars; + ctx->gvars = array_to_sbuf(item.value.data.array); + } else if (strequal(item.key.data, "funcs")) { + types |= kCtxFuncs; + ctx->funcs = copy_object(item.value).data.array; + } + } + + return types; +} diff --git a/src/nvim/context.h b/src/nvim/context.h new file mode 100644 index 0000000000..328e12c6a6 --- /dev/null +++ b/src/nvim/context.h @@ -0,0 +1,46 @@ +#ifndef NVIM_CONTEXT_H +#define NVIM_CONTEXT_H + +#include <msgpack.h> +#include "nvim/api/private/defs.h" +#include "nvim/lib/kvec.h" + +typedef struct { + msgpack_sbuffer regs; ///< Registers. + msgpack_sbuffer jumps; ///< Jumplist. + msgpack_sbuffer buflist; ///< Buffer list. + msgpack_sbuffer gvars; ///< Global variables. + Array funcs; ///< Functions. +} Context; +typedef kvec_t(Context) ContextVec; + +#define MSGPACK_SBUFFER_INIT (msgpack_sbuffer) { \ + .size = 0, \ + .data = NULL, \ + .alloc = 0, \ +} + +#define CONTEXT_INIT (Context) { \ + .regs = MSGPACK_SBUFFER_INIT, \ + .jumps = MSGPACK_SBUFFER_INIT, \ + .buflist = MSGPACK_SBUFFER_INIT, \ + .gvars = MSGPACK_SBUFFER_INIT, \ + .funcs = ARRAY_DICT_INIT, \ +} + +typedef enum { + kCtxRegs = 1, ///< Registers + kCtxJumps = 2, ///< Jumplist + kCtxBuflist = 4, ///< Buffer list + kCtxGVars = 8, ///< Global variables + kCtxSFuncs = 16, ///< Script functions + kCtxFuncs = 32, ///< Functions +} ContextTypeFlags; + +extern int kCtxAll; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "context.h.generated.h" +#endif + +#endif // NVIM_CONTEXT_H diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 0b943cedc7..cd447d3e0f 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -26,6 +26,7 @@ #include "nvim/buffer.h" #include "nvim/channel.h" #include "nvim/charset.h" +#include "nvim/context.h" #include "nvim/cursor.h" #include "nvim/diff.h" #include "nvim/edit.h" @@ -303,15 +304,6 @@ typedef struct { list_T *fi_list; /* list being used */ } forinfo_T; -/* - * enum used by var_flavour() - */ -typedef enum { - VAR_FLAVOUR_DEFAULT, /* doesn't start with uppercase */ - VAR_FLAVOUR_SESSION, /* starts with uppercase, some lower */ - VAR_FLAVOUR_SHADA /* all uppercase */ -} var_flavour_T; - /* values for vv_flags: */ #define VV_COMPAT 1 /* compatible, also used without "v:" */ #define VV_RO 2 /* read-only */ @@ -5261,7 +5253,7 @@ bool garbage_collect(bool testing) yankreg_T reg; char name = NUL; bool is_unnamed = false; - reg_iter = op_register_iter(reg_iter, &name, ®, &is_unnamed); + reg_iter = op_global_reg_iter(reg_iter, &name, ®, &is_unnamed); if (name != NUL) { ABORTING(set_ref_dict)(reg.additional_data, copyID); } @@ -8023,6 +8015,116 @@ static void f_cscope_connection(typval_T *argvars, typval_T *rettv, FunPtr fptr) (char_u *)prepend); } +/// "ctxget([{index}])" function +static void f_ctxget(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + size_t index = 0; + if (argvars[0].v_type == VAR_NUMBER) { + index = argvars[0].vval.v_number; + } else if (argvars[0].v_type != VAR_UNKNOWN) { + EMSG2(_(e_invarg2), "expected nothing or a Number as an argument"); + return; + } + + Context *ctx = ctx_get(index); + if (ctx == NULL) { + EMSG3(_(e_invargNval), "index", "out of bounds"); + return; + } + + Dictionary ctx_dict = ctx_to_dict(ctx); + Error err = ERROR_INIT; + object_to_vim(DICTIONARY_OBJ(ctx_dict), rettv, &err); + api_free_dictionary(ctx_dict); + api_clear_error(&err); +} + +/// "ctxpop()" function +static void f_ctxpop(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + if (!ctx_restore(NULL, kCtxAll)) { + EMSG(_("Context stack is empty")); + } +} + +/// "ctxpush([{types}])" function +static void f_ctxpush(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + int types = kCtxAll; + if (argvars[0].v_type == VAR_LIST) { + types = 0; + TV_LIST_ITER(argvars[0].vval.v_list, li, { + typval_T *tv_li = TV_LIST_ITEM_TV(li); + if (tv_li->v_type == VAR_STRING) { + if (strequal((char *)tv_li->vval.v_string, "regs")) { + types |= kCtxRegs; + } else if (strequal((char *)tv_li->vval.v_string, "jumps")) { + types |= kCtxJumps; + } else if (strequal((char *)tv_li->vval.v_string, "buflist")) { + types |= kCtxBuflist; + } else if (strequal((char *)tv_li->vval.v_string, "gvars")) { + types |= kCtxGVars; + } else if (strequal((char *)tv_li->vval.v_string, "sfuncs")) { + types |= kCtxSFuncs; + } else if (strequal((char *)tv_li->vval.v_string, "funcs")) { + types |= kCtxFuncs; + } + } + }); + } else if (argvars[0].v_type != VAR_UNKNOWN) { + EMSG2(_(e_invarg2), "expected nothing or a List as an argument"); + return; + } + ctx_save(NULL, types); +} + +/// "ctxset({context}[, {index}])" function +static void f_ctxset(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + if (argvars[0].v_type != VAR_DICT) { + EMSG2(_(e_invarg2), "expected dictionary as first argument"); + return; + } + + size_t index = 0; + if (argvars[1].v_type == VAR_NUMBER) { + index = argvars[1].vval.v_number; + } else if (argvars[1].v_type != VAR_UNKNOWN) { + EMSG2(_(e_invarg2), "expected nothing or a Number as second argument"); + return; + } + + Context *ctx = ctx_get(index); + if (ctx == NULL) { + EMSG3(_(e_invargNval), "index", "out of bounds"); + return; + } + + int save_did_emsg = did_emsg; + did_emsg = false; + + Dictionary dict = vim_to_object(&argvars[0]).data.dictionary; + Context tmp = CONTEXT_INIT; + ctx_from_dict(dict, &tmp); + + if (did_emsg) { + ctx_free(&tmp); + } else { + ctx_free(ctx); + *ctx = tmp; + } + + api_free_dictionary(dict); + did_emsg = save_did_emsg; +} + +/// "ctxsize()" function +static void f_ctxsize(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = ctx_size(); +} + /// "cursor(lnum, col)" function, or /// "cursor(list)" /// @@ -15618,7 +15720,7 @@ free_lstval: if (set_unnamed) { // Discard the result. We already handle the error case. - if (op_register_set_previous(regname)) { } + if (op_reg_set_previous(regname)) { } } } @@ -20883,7 +20985,7 @@ void ex_function(exarg_T *eap) continue; } if (!func_name_refcount(fp->uf_name)) { - list_func_head(fp, false); + list_func_head(fp, false, false); } } } @@ -20914,7 +21016,7 @@ void ex_function(exarg_T *eap) fp = HI2UF(hi); if (!isdigit(*fp->uf_name) && vim_regexec(®match, fp->uf_name, 0)) - list_func_head(fp, FALSE); + list_func_head(fp, false, false); } } vim_regfree(regmatch.regprog); @@ -20964,9 +21066,12 @@ void ex_function(exarg_T *eap) saved_did_emsg = did_emsg; did_emsg = FALSE; - /* - * ":function func" with only function name: list function. - */ + // + // ":function func" with only function name: list function. + // If bang is given: + // - include "!" in function head + // - exclude line numbers from function body + // if (!paren) { if (!ends_excmd(*skipwhite(p))) { EMSG(_(e_trailing)); @@ -20978,17 +21083,20 @@ void ex_function(exarg_T *eap) if (!eap->skip && !got_int) { fp = find_func(name); if (fp != NULL) { - list_func_head(fp, TRUE); - for (int j = 0; j < fp->uf_lines.ga_len && !got_int; ++j) { - if (FUNCLINE(fp, j) == NULL) + list_func_head(fp, !eap->forceit, eap->forceit); + for (int j = 0; j < fp->uf_lines.ga_len && !got_int; j++) { + if (FUNCLINE(fp, j) == NULL) { continue; - msg_putchar('\n'); - msg_outnum((long)j + 1); - if (j < 9) { - msg_putchar(' '); } - if (j < 99) { - msg_putchar(' '); + msg_putchar('\n'); + if (!eap->forceit) { + msg_outnum((long)j + 1); + if (j < 9) { + msg_putchar(' '); + } + if (j < 99) { + msg_putchar(' '); + } } msg_prt_line(FUNCLINE(fp, j), false); ui_flush(); // show a line at a time @@ -20996,7 +21104,7 @@ void ex_function(exarg_T *eap) } if (!got_int) { msg_putchar('\n'); - msg_puts(" endfunction"); + msg_puts(eap->forceit ? "endfunction" : " endfunction"); } } else emsg_funcname(N_("E123: Undefined function: %s"), name); @@ -21686,15 +21794,17 @@ static inline bool eval_fname_sid(const char *const name) return *name == 's' || TOUPPER_ASC(name[2]) == 'I'; } -/* - * List the head of the function: "name(arg1, arg2)". - */ -static void list_func_head(ufunc_T *fp, int indent) +/// List the head of the function: "name(arg1, arg2)". +/// +/// @param[in] fp Function pointer. +/// @param[in] indent Indent line. +/// @param[in] force Include bang "!" (i.e.: "function!"). +static void list_func_head(ufunc_T *fp, int indent, bool force) { msg_start(); if (indent) MSG_PUTS(" "); - MSG_PUTS("function "); + MSG_PUTS(force ? "function! " : "function "); if (fp->uf_name[0] == K_SPECIAL) { MSG_PUTS_ATTR("<SNR>", HL_ATTR(HLF_8)); msg_puts((const char *)fp->uf_name + 3); @@ -23278,7 +23388,7 @@ dictitem_T *find_var_in_scoped_ht(const char *name, const size_t namelen, /// @return Pointer that needs to be passed to next `var_shada_iter` invocation /// or NULL to indicate that iteration is over. const void *var_shada_iter(const void *const iter, const char **const name, - typval_T *rettv) + typval_T *rettv, var_flavour_T flavour) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(2, 3) { const hashitem_T *hi; @@ -23289,7 +23399,7 @@ const void *var_shada_iter(const void *const iter, const char **const name, hi = globvarht.ht_array; while ((size_t) (hi - hifirst) < hinum && (HASHITEM_EMPTY(hi) - || var_flavour(hi->hi_key) != VAR_FLAVOUR_SHADA)) { + || !(var_flavour(hi->hi_key) & flavour))) { hi++; } if ((size_t) (hi - hifirst) == hinum) { @@ -23301,7 +23411,7 @@ const void *var_shada_iter(const void *const iter, const char **const name, *name = (char *)TV_DICT_HI2DI(hi)->di_key; tv_copy(&TV_DICT_HI2DI(hi)->di_tv, rettv); while ((size_t)(++hi - hifirst) < hinum) { - if (!HASHITEM_EMPTY(hi) && var_flavour(hi->hi_key) == VAR_FLAVOUR_SHADA) { + if (!HASHITEM_EMPTY(hi) && (var_flavour(hi->hi_key) & flavour)) { return hi; } } diff --git a/src/nvim/eval.h b/src/nvim/eval.h index 149dae688e..abe032a96e 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -24,6 +24,13 @@ EXTERN ufunc_T dumuf; #define HIKEY2UF(p) ((ufunc_T *)(p - offsetof(ufunc_T, uf_name))) #define HI2UF(hi) HIKEY2UF((hi)->hi_key) +/// enum used by var_flavour() +typedef enum { + VAR_FLAVOUR_DEFAULT = 1, // doesn't start with uppercase + VAR_FLAVOUR_SESSION = 2, // starts with uppercase, some lower + VAR_FLAVOUR_SHADA = 4 // all uppercase +} var_flavour_T; + /// Defines for Vim variables typedef enum { VV_COUNT, diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 089b08d5d1..0b77a24f7a 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -74,6 +74,11 @@ return { cosh={args=1, func="float_op_wrapper", data="&cosh"}, count={args={2, 4}}, cscope_connection={args={0, 3}}, + ctxget={args={0, 1}}, + ctxpop={}, + ctxpush={args={0, 1}}, + ctxset={args={1, 2}}, + ctxsize={}, cursor={args={1, 3}}, deepcopy={args={1, 2}}, delete={args={1,2}}, diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 3736004527..9f357575d0 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -1213,8 +1213,8 @@ void cleanup_jumplist(win_T *wp, bool loadfiles) // When pointer is below last jump, remove the jump if it matches the current // line. This avoids useless/phantom jumps. #9805 - if (wp->w_jumplistlen - && wp->w_jumplistidx == wp->w_jumplistlen) { + if (loadfiles // otherwise (i.e.: Shada), last entry should be kept + && wp->w_jumplistlen && wp->w_jumplistidx == wp->w_jumplistlen) { const xfmark_T *fm_last = &wp->w_jumplist[wp->w_jumplistlen - 1]; if (fm_last->fmark.fnum == curbuf->b_fnum && fm_last->fmark.mark.lnum == wp->w_cursor.lnum) { diff --git a/src/nvim/memory.c b/src/nvim/memory.c index 1384aa177b..64aae71433 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -9,6 +9,7 @@ #include <stdbool.h> #include "nvim/vim.h" +#include "nvim/context.h" #include "nvim/eval.h" #include "nvim/highlight.h" #include "nvim/memfile.h" @@ -671,6 +672,7 @@ void free_all_mem(void) eval_clear(); api_vim_free_all_mem(); + ctx_free_all(); // Free all buffers. Reset 'autochdir' to avoid accessing things that // were freed already. diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 40c96eb333..a1f2a2f1b6 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -3516,8 +3516,8 @@ void ex_display(exarg_T *eap) * display a string for do_dis() * truncate at end of screen line */ -static void -dis_msg ( +static void +dis_msg( char_u *p, int skip_esc /* if TRUE, ignore trailing ESC */ ) @@ -3857,8 +3857,8 @@ static int same_leader(linenr_T lnum, int leader1_len, char_u *leader1_flags, in /* * Implementation of the format operator 'gq'. */ -void -op_format ( +void +op_format( oparg_T *oap, int keep_cursor /* keep cursor on same text char */ ) @@ -3937,8 +3937,8 @@ void op_formatexpr(oparg_T *oap) op_format(oap, FALSE); } -int -fex_format ( +int +fex_format( linenr_T lnum, long count, int c /* character to be inserted */ @@ -3980,8 +3980,8 @@ fex_format ( * Lines after the cursor line are saved for undo, caller must have saved the * first line. */ -void -format_lines ( +void +format_lines( linenr_T line_count, int avoid_fex /* don't use 'formatexpr' */ ) @@ -5892,33 +5892,45 @@ static inline bool reg_empty(const yankreg_T *const reg) && *(reg->y_array[0]) == NUL)); } -/// Iterate over registerrs +/// Iterate over global registers. +/// +/// @see op_register_iter +const void *op_global_reg_iter(const void *const iter, char *const name, + yankreg_T *const reg, bool *is_unnamed) + FUNC_ATTR_NONNULL_ARG(2, 3, 4) FUNC_ATTR_WARN_UNUSED_RESULT +{ + return op_reg_iter(iter, y_regs, name, reg, is_unnamed); +} + +/// Iterate over registers `regs`. /// /// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[in] regs Registers list to be iterated. /// @param[out] name Register name. /// @param[out] reg Register contents. /// -/// @return Pointer that needs to be passed to next `op_register_iter` call or +/// @return Pointer that must be passed to next `op_register_iter` call or /// NULL if iteration is over. -const void *op_register_iter(const void *const iter, char *const name, - yankreg_T *const reg, bool *is_unnamed) - FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT +const void *op_reg_iter(const void *const iter, const yankreg_T *const regs, + char *const name, yankreg_T *const reg, + bool *is_unnamed) + FUNC_ATTR_NONNULL_ARG(3, 4, 5) FUNC_ATTR_WARN_UNUSED_RESULT { *name = NUL; const yankreg_T *iter_reg = (iter == NULL - ? &(y_regs[0]) - : (const yankreg_T *const) iter); - while (iter_reg - &(y_regs[0]) < NUM_SAVED_REGISTERS && reg_empty(iter_reg)) { + ? &(regs[0]) + : (const yankreg_T *const)iter); + while (iter_reg - &(regs[0]) < NUM_SAVED_REGISTERS && reg_empty(iter_reg)) { iter_reg++; } - if (iter_reg - &(y_regs[0]) == NUM_SAVED_REGISTERS || reg_empty(iter_reg)) { + if (iter_reg - &(regs[0]) == NUM_SAVED_REGISTERS || reg_empty(iter_reg)) { return NULL; } - int iter_off = (int)(iter_reg - &(y_regs[0])); + int iter_off = (int)(iter_reg - &(regs[0])); *name = (char)get_register_name(iter_off); *reg = *iter_reg; *is_unnamed = (iter_reg == y_previous); - while (++iter_reg - &(y_regs[0]) < NUM_SAVED_REGISTERS) { + while (++iter_reg - &(regs[0]) < NUM_SAVED_REGISTERS) { if (!reg_empty(iter_reg)) { return (void *) iter_reg; } @@ -5927,7 +5939,7 @@ const void *op_register_iter(const void *const iter, char *const name, } /// Get a number of non-empty registers -size_t op_register_amount(void) +size_t op_reg_amount(void) FUNC_ATTR_WARN_UNUSED_RESULT { size_t ret = 0; @@ -5946,7 +5958,7 @@ size_t op_register_amount(void) /// @param[in] is_unnamed Whether to set the unnamed regiseter to reg /// /// @return true on success, false on failure. -bool op_register_set(const char name, const yankreg_T reg, bool is_unnamed) +bool op_reg_set(const char name, const yankreg_T reg, bool is_unnamed) { int i = op_reg_index(name); if (i == -1) { @@ -5966,7 +5978,7 @@ bool op_register_set(const char name, const yankreg_T reg, bool is_unnamed) /// @param[in] name Register name. /// /// @return Pointer to the register contents or NULL. -const yankreg_T *op_register_get(const char name) +const yankreg_T *op_reg_get(const char name) { int i = op_reg_index(name); if (i == -1) { @@ -5980,7 +5992,7 @@ const yankreg_T *op_register_get(const char name) /// @param[in] name Register name. /// /// @return true on success, false on failure. -bool op_register_set_previous(const char name) +bool op_reg_set_previous(const char name) FUNC_ATTR_WARN_UNUSED_RESULT { int i = op_reg_index(name); diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 4aafc669dc..d23985ca86 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -151,15 +151,6 @@ KHASH_SET_INIT_STR(strset) /// Callback function for add_search_pattern typedef void (*SearchPatternGetter)(SearchPattern *); -/// Flags for shada_read_file and children -typedef enum { - kShaDaWantInfo = 1, ///< Load non-mark information - kShaDaWantMarks = 2, ///< Load local file marks and change list - kShaDaForceit = 4, ///< Overwrite info already read - kShaDaGetOldfiles = 8, ///< Load v:oldfiles. - kShaDaMissingError = 16, ///< Error out when os_open returns -ENOENT. -} ShaDaReadFileFlags; - /// Possible ShaDa entry types /// /// @warning Enum values are part of the API and must not be altered. @@ -1328,13 +1319,13 @@ static void shada_read(ShaDaReadDef *const sd_reader, const int flags) break; } if (!force) { - const yankreg_T *const reg = op_register_get(cur_entry.data.reg.name); + const yankreg_T *const reg = op_reg_get(cur_entry.data.reg.name); if (reg == NULL || reg->timestamp >= cur_entry.timestamp) { shada_free_shada_entry(&cur_entry); break; } } - if (!op_register_set(cur_entry.data.reg.name, (yankreg_T) { + if (!op_reg_set(cur_entry.data.reg.name, (yankreg_T) { .y_array = (char_u **)cur_entry.data.reg.contents, .y_size = cur_entry.data.reg.contents_size, .y_type = cur_entry.data.reg.type, @@ -2496,7 +2487,7 @@ static inline void shada_initialize_registers(WriteMergerState *const wms, yankreg_T reg; char name = NUL; bool is_unnamed = false; - reg_iter = op_register_iter(reg_iter, &name, ®, &is_unnamed); + reg_iter = op_global_reg_iter(reg_iter, &name, ®, &is_unnamed); if (name == NUL) { break; } @@ -2552,6 +2543,19 @@ static inline void replace_numbered_mark(WriteMergerState *const wms, wms->numbered_marks[idx].data.data.filemark.name = (char)('0' + (int)idx); } +/// Find buffers ignored due to their location. +/// +/// @param[out] removable_bufs Cache of buffers ignored due to their location. +static inline void find_removable_bufs(khash_t(bufset) *removable_bufs) +{ + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL && shada_removable((char *)buf->b_ffname)) { + int kh_ret; + (void)kh_put(bufset, removable_bufs, (uintptr_t)buf, &kh_ret); + } + } +} + /// Write ShaDa file /// /// @param[in] sd_writer Structure containing file writer definition. @@ -2621,12 +2625,7 @@ static ShaDaWriteResult shada_write(ShaDaWriteDef *const sd_writer, set_last_cursor(wp); } - FOR_ALL_BUFFERS(buf) { - if (buf->b_ffname != NULL && shada_removable((char *) buf->b_ffname)) { - int kh_ret; - (void) kh_put(bufset, &removable_bufs, (uintptr_t) buf, &kh_ret); - } - } + find_removable_bufs(&removable_bufs); // Write header if (shada_pack_entry(packer, (ShadaEntry) { @@ -2673,7 +2672,7 @@ static ShaDaWriteResult shada_write(ShaDaWriteDef *const sd_writer, do { typval_T vartv; const char *name = NULL; - var_iter = var_shada_iter(var_iter, &name, &vartv); + var_iter = var_shada_iter(var_iter, &name, &vartv, VAR_FLAVOUR_SHADA); if (name == NULL) { break; } @@ -2737,49 +2736,7 @@ static ShaDaWriteResult shada_write(ShaDaWriteDef *const sd_writer, } // Initialize jump list - const void *jump_iter = NULL; - cleanup_jumplist(curwin, false); - setpcmark(); - do { - xfmark_T fm; - jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); - - if (fm.fmark.mark.lnum == 0) { - iemsgf("ShaDa: mark lnum zero (ji:%p, js:%p, len:%i)", - (void *)jump_iter, (void *)&curwin->w_jumplist[0], - curwin->w_jumplistlen); - continue; - } - const buf_T *const buf = (fm.fmark.fnum == 0 - ? NULL - : buflist_findnr(fm.fmark.fnum)); - if (buf != NULL - ? in_bufset(&removable_bufs, buf) - : fm.fmark.fnum != 0) { - continue; - } - const char *const fname = (char *) (fm.fmark.fnum == 0 - ? (fm.fname == NULL ? NULL : fm.fname) - : buf->b_ffname); - if (fname == NULL) { - continue; - } - wms->jumps[wms->jumps_size++] = (PossiblyFreedShadaEntry) { - .can_free_entry = false, - .data = { - .type = kSDItemJump, - .timestamp = fm.fmark.timestamp, - .data = { - .filemark = { - .name = NUL, - .mark = fm.fmark.mark, - .fname = (char *) fname, - .additional_data = fm.fmark.additional_data, - } - } - } - }; - } while (jump_iter != NULL); + wms->jumps_size = shada_init_jumps(wms->jumps, &removable_bufs); // Initialize global marks if (dump_global_marks) { @@ -4117,3 +4074,224 @@ static bool shada_removable(const char *name) xfree(new_name); return retval; } + +/// Initialize ShaDa jumplist entries. +/// +/// @param[in,out] jumps Array of ShaDa entries to set. +/// @param[in] removable_bufs Cache of buffers ignored due to their +/// location. +/// +/// @return number of jumplist entries +static inline size_t shada_init_jumps( + PossiblyFreedShadaEntry *jumps, khash_t(bufset) *const removable_bufs) +{ + // Initialize jump list + size_t jumps_size = 0; + const void *jump_iter = NULL; + setpcmark(); + cleanup_jumplist(curwin, false); + do { + xfmark_T fm; + jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); + + if (fm.fmark.mark.lnum == 0) { + iemsgf("ShaDa: mark lnum zero (ji:%p, js:%p, len:%i)", + (void *)jump_iter, (void *)&curwin->w_jumplist[0], + curwin->w_jumplistlen); + continue; + } + const buf_T *const buf = (fm.fmark.fnum == 0 + ? NULL + : buflist_findnr(fm.fmark.fnum)); + if (buf != NULL + ? in_bufset(removable_bufs, buf) + : fm.fmark.fnum != 0) { + continue; + } + const char *const fname = (char *) (fm.fmark.fnum == 0 + ? (fm.fname == NULL ? NULL : fm.fname) + : buf->b_ffname); + if (fname == NULL) { + continue; + } + jumps[jumps_size++] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemJump, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .name = NUL, + .mark = fm.fmark.mark, + .fname = (char *) fname, + .additional_data = fm.fmark.additional_data, + } + } + } + }; + } while (jump_iter != NULL); + return jumps_size; +} + +/// Write registers ShaDa entries in given msgpack_sbuffer. +/// +/// @param[in] sbuf target msgpack_sbuffer to write to. +void shada_encode_regs(msgpack_sbuffer *const sbuf) + FUNC_ATTR_NONNULL_ALL +{ + WriteMergerState wms; + shada_initialize_registers(&wms, -1); + msgpack_packer packer; + msgpack_packer_init(&packer, sbuf, msgpack_sbuffer_write); + for (size_t i = 0; i < ARRAY_SIZE(wms.registers); i++) { + if (wms.registers[i].data.type == kSDItemRegister) { + shada_pack_pfreed_entry(&packer, wms.registers[i], 0); + } + } +} + +/// Write jumplist ShaDa entries in given msgpack_sbuffer. +/// +/// @param[in] sbuf target msgpack_sbuffer to write to. +void shada_encode_jumps(msgpack_sbuffer *const sbuf) + FUNC_ATTR_NONNULL_ALL +{ + khash_t(bufset) removable_bufs = KHASH_EMPTY_TABLE(bufset); + find_removable_bufs(&removable_bufs); + PossiblyFreedShadaEntry jumps[JUMPLISTSIZE]; + size_t jumps_size = shada_init_jumps(jumps, &removable_bufs); + msgpack_packer packer; + msgpack_packer_init(&packer, sbuf, msgpack_sbuffer_write); + for (size_t i = 0; i < jumps_size; i++) { + shada_pack_pfreed_entry(&packer, jumps[i], 0); + } +} + +/// Write buffer list ShaDa entry in given msgpack_sbuffer. +/// +/// @param[in] sbuf target msgpack_sbuffer to write to. +void shada_encode_buflist(msgpack_sbuffer *const sbuf) + FUNC_ATTR_NONNULL_ALL +{ + khash_t(bufset) removable_bufs = KHASH_EMPTY_TABLE(bufset); + find_removable_bufs(&removable_bufs); + ShadaEntry buflist_entry = shada_get_buflist(&removable_bufs); + msgpack_packer packer; + msgpack_packer_init(&packer, sbuf, msgpack_sbuffer_write); + shada_pack_entry(&packer, buflist_entry, 0); + xfree(buflist_entry.data.buffer_list.buffers); +} + +/// Write global variables ShaDa entries in given msgpack_sbuffer. +/// +/// @param[in] sbuf target msgpack_sbuffer to write to. +void shada_encode_gvars(msgpack_sbuffer *const sbuf) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_packer packer; + msgpack_packer_init(&packer, sbuf, msgpack_sbuffer_write); + const void *var_iter = NULL; + const Timestamp cur_timestamp = os_time(); + do { + typval_T vartv; + const char *name = NULL; + var_iter = var_shada_iter( + var_iter, &name, &vartv, + VAR_FLAVOUR_DEFAULT | VAR_FLAVOUR_SESSION | VAR_FLAVOUR_SHADA); + if (name == NULL) { + break; + } + if (vartv.v_type != VAR_FUNC && vartv.v_type != VAR_PARTIAL) { + typval_T tgttv; + tv_copy(&vartv, &tgttv); + shada_pack_entry(&packer, (ShadaEntry) { + .type = kSDItemVariable, + .timestamp = cur_timestamp, + .data = { + .global_var = { + .name = (char *)name, + .value = tgttv, + .additional_elements = NULL, + } + } + }, 0); + tv_clear(&tgttv); + } + tv_clear(&vartv); + } while (var_iter != NULL); +} + +/// Wrapper for reading from msgpack_sbuffer. +/// +/// @return number of bytes read. +static ptrdiff_t read_sbuf(ShaDaReadDef *const sd_reader, void *const dest, + const size_t size) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + msgpack_sbuffer *sbuf = (msgpack_sbuffer *)sd_reader->cookie; + const uintmax_t bytes_read = MIN(size, sbuf->size - sd_reader->fpos); + if (bytes_read < size) { + sd_reader->eof = true; + } + memcpy(dest, sbuf->data + sd_reader->fpos, (size_t)bytes_read); + sd_reader->fpos += bytes_read; + return (ptrdiff_t)bytes_read; +} + +/// Wrapper for read that ignores bytes read from msgpack_sbuffer. +/// +/// Used for skipping. +/// +/// @param[in,out] sd_reader ShaDaReadDef with msgpack_sbuffer. +/// @param[in] offset Amount of bytes to skip. +/// +/// @return FAIL in case of failure, OK in case of success. May set +/// sd_reader->eof. +static int sd_sbuf_reader_skip_read(ShaDaReadDef *const sd_reader, + const size_t offset) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + msgpack_sbuffer *sbuf = (msgpack_sbuffer *)sd_reader->cookie; + const uintmax_t bytes_skipped = MIN(offset, sbuf->size - sd_reader->fpos); + if (bytes_skipped < offset) { + sd_reader->eof = true; + return FAIL; + } + sd_reader->fpos += offset; + return OK; +} + +/// Prepare ShaDaReadDef with msgpack_sbuffer for reading. +/// +/// @param[in] sbuf msgpack_sbuffer to read from. +/// @param[out] sd_reader Location where reader structure will be saved. +static void open_shada_sbuf_for_reading(const msgpack_sbuffer *const sbuf, + ShaDaReadDef *sd_reader) + FUNC_ATTR_NONNULL_ALL +{ + *sd_reader = (ShaDaReadDef) { + .read = &read_sbuf, + .close = NULL, + .skip = &sd_sbuf_reader_skip_read, + .error = NULL, + .eof = false, + .fpos = 0, + .cookie = (void *)sbuf, + }; +} + +/// Read ShaDa from msgpack_sbuffer. +/// +/// @param[in] file msgpack_sbuffer to read from. +/// @param[in] flags Flags, see ShaDaReadFileFlags enum. +void shada_read_sbuf(msgpack_sbuffer *const sbuf, const int flags) + FUNC_ATTR_NONNULL_ALL +{ + assert(sbuf != NULL); + if (sbuf->data == NULL) { + return; + } + ShaDaReadDef sd_reader; + open_shada_sbuf_for_reading(sbuf, &sd_reader); + shada_read(&sd_reader, flags); +} diff --git a/src/nvim/shada.h b/src/nvim/shada.h index 49986ac1c1..2a945a06bc 100644 --- a/src/nvim/shada.h +++ b/src/nvim/shada.h @@ -1,6 +1,17 @@ #ifndef NVIM_SHADA_H #define NVIM_SHADA_H +#include <msgpack.h> + +/// Flags for shada_read_file and children +typedef enum { + kShaDaWantInfo = 1, ///< Load non-mark information + kShaDaWantMarks = 2, ///< Load local file marks and change list + kShaDaForceit = 4, ///< Overwrite info already read + kShaDaGetOldfiles = 8, ///< Load v:oldfiles. + kShaDaMissingError = 16, ///< Error out when os_open returns -ENOENT. +} ShaDaReadFileFlags; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "shada.h.generated.h" #endif diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index a3d57662b3..110b3a4b16 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -11,6 +11,7 @@ local meth_pcall = helpers.meth_pcall local meths = helpers.meths local ok, nvim_async, feed = helpers.ok, helpers.nvim_async, helpers.feed local os_name = helpers.os_name +local parse_context = helpers.parse_context local request = helpers.request local source = helpers.source local next_msg = helpers.next_msg @@ -680,6 +681,65 @@ describe('API', function() end) end) + describe('nvim_get_context', function() + it('returns context dictionary of current editor state', function() + local ctx_items = {'regs', 'jumps', 'buflist', 'gvars'} + eq({}, parse_context(nvim('get_context', ctx_items))) + + feed('i1<cr>2<cr>3<c-[>ddddddqahjklquuu') + feed('gg') + feed('G') + command('edit! BUF1') + command('edit BUF2') + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + + local expected_ctx = { + ['regs'] = { + {['rt'] = 1, ['rc'] = {'1'}, ['n'] = 49, ['ru'] = true}, + {['rt'] = 1, ['rc'] = {'2'}, ['n'] = 50}, + {['rt'] = 1, ['rc'] = {'3'}, ['n'] = 51}, + {['rc'] = {'hjkl'}, ['n'] = 97}, + }, + + ['jumps'] = eval(([[ + filter(map(add( + getjumplist()[0], { 'bufnr': bufnr('%'), 'lnum': getcurpos()[1] }), + 'filter( + { "f": expand("#".v:val.bufnr.":p"), "l": v:val.lnum }, + { k, v -> k != "l" || v != 1 })'), '!empty(v:val.f)') + ]]):gsub('\n', '')), + + ['buflist'] = eval([[ + filter(map(getbufinfo(), '{ "f": v:val.name }'), '!empty(v:val.f)') + ]]), + + ['gvars'] = {{'one', 1}, {'Two', 2}, {'THREE', 3}}, + } + + eq(expected_ctx, parse_context(nvim('get_context', ctx_items))) + end) + end) + + describe('nvim_load_context', function() + it('sets current editor state to given context dictionary', function() + local ctx_items = {'regs', 'jumps', 'buflist', 'gvars'} + eq({}, parse_context(nvim('get_context', ctx_items))) + + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + local ctx = nvim('get_context', ctx_items) + nvim('set_var', 'one', 'a') + nvim('set_var', 'Two', 'b') + nvim('set_var', 'THREE', 'c') + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + nvim('load_context', ctx) + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + end) + end) + describe('nvim_replace_termcodes', function() it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function() eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true)) diff --git a/test/functional/eval/ctx_functions_spec.lua b/test/functional/eval/ctx_functions_spec.lua new file mode 100644 index 0000000000..35133e2341 --- /dev/null +++ b/test/functional/eval/ctx_functions_spec.lua @@ -0,0 +1,408 @@ +local helpers = require('test.functional.helpers')(after_each) + +local call = helpers.call +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local eval = helpers.eval +local expect_err = helpers.expect_err +local feed = helpers.feed +local map = helpers.map +local nvim = helpers.nvim +local parse_context = helpers.parse_context +local redir_exec = helpers.redir_exec +local source = helpers.source +local trim = helpers.trim +local write_file = helpers.write_file + +describe('context functions', function() + local fname1 = 'Xtest-functional-eval-ctx1' + local fname2 = 'Xtest-functional-eval-ctx2' + local outofbounds = + 'Vim:E475: Invalid value for argument index: out of bounds' + + before_each(function() + clear() + write_file(fname1, "1\n2\n3") + write_file(fname2, "a\nb\nc") + end) + + after_each(function() + os.remove(fname1) + os.remove(fname2) + end) + + describe('ctxpush/ctxpop', function() + it('saves and restores registers properly', function() + local regs = {'1', '2', '3', 'a'} + local vals = {'1', '2', '3', 'hjkl'} + feed('i1<cr>2<cr>3<c-[>ddddddqahjklq') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + call('ctxpush') + call('ctxpush', {'regs'}) + + map(function(r) call('setreg', r, {}) end, regs) + eq({'', '', '', ''}, + map(function(r) return trim(call('getreg', r)) end, regs)) + + call('ctxpop') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + + map(function(r) call('setreg', r, {}) end, regs) + eq({'', '', '', ''}, + map(function(r) return trim(call('getreg', r)) end, regs)) + + call('ctxpop') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + end) + + it('saves and restores jumplist properly', function() + command('edit '..fname1) + feed('G') + feed('gg') + command('edit '..fname2) + local jumplist = call('getjumplist') + call('ctxpush') + call('ctxpush', {'jumps'}) + + command('clearjumps') + eq({{}, 0}, call('getjumplist')) + + call('ctxpop') + eq(jumplist, call('getjumplist')) + + command('clearjumps') + eq({{}, 0}, call('getjumplist')) + + call('ctxpop') + eq(jumplist, call('getjumplist')) + end) + + it('saves and restores buffer list properly', function() + command('edit '..fname1) + command('edit '..fname2) + command('edit TEST') + local buflist = call('map', call('getbufinfo'), 'v:val.name') + call('ctxpush') + call('ctxpush', {'buflist'}) + + command('%bwipeout') + eq({''}, call('map', call('getbufinfo'), 'v:val.name')) + + call('ctxpop') + eq({'', unpack(buflist)}, call('map', call('getbufinfo'), 'v:val.name')) + + command('%bwipeout') + eq({''}, call('map', call('getbufinfo'), 'v:val.name')) + + call('ctxpop') + eq({'', unpack(buflist)}, call('map', call('getbufinfo'), 'v:val.name')) + end) + + it('saves and restores global variables properly', function() + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpush') + call('ctxpush', {'gvars'}) + + nvim('del_var', 'one') + nvim('del_var', 'Two') + nvim('del_var', 'THREE') + expect_err('E121: Undefined variable: g:one', eval, 'g:one') + expect_err('E121: Undefined variable: g:Two', eval, 'g:Two') + expect_err('E121: Undefined variable: g:THREE', eval, 'g:THREE') + + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + + nvim('del_var', 'one') + nvim('del_var', 'Two') + nvim('del_var', 'THREE') + expect_err('E121: Undefined variable: g:one', eval, 'g:one') + expect_err('E121: Undefined variable: g:Two', eval, 'g:Two') + expect_err('E121: Undefined variable: g:THREE', eval, 'g:THREE') + + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + end) + + it('saves and restores script functions properly', function() + source([[ + function s:greet(name) + echom 'Hello, '.a:name.'!' + endfunction + + function s:greet_all(name, ...) + echom 'Hello, '.a:name.'!' + for more in a:000 + echom 'Hello, '.more.'!' + endfor + endfunction + + function Greet(name) + call call('s:greet', [a:name]) + endfunction + + function GreetAll(name, ...) + call call('s:greet_all', extend([a:name], a:000)) + endfunction + + function SaveSFuncs() + call ctxpush(['sfuncs']) + endfunction + + function DeleteSFuncs() + delfunction s:greet + delfunction s:greet_all + endfunction + + function RestoreFuncs() + call ctxpop() + endfunction + ]]) + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('SaveSFuncs') + call('DeleteSFuncs') + + eq('\nError detected while processing function Greet:'.. + '\nline 1:'.. + '\nE117: Unknown function: s:greet', + redir_exec([[call Greet('World')]])) + eq('\nError detected while processing function GreetAll:'.. + '\nline 1:'.. + '\nE117: Unknown function: s:greet_all', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('RestoreFuncs') + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + end) + + it('saves and restores functions properly', function() + source([[ + function Greet(name) + echom 'Hello, '.a:name.'!' + endfunction + + function GreetAll(name, ...) + echom 'Hello, '.a:name.'!' + for more in a:000 + echom 'Hello, '.more.'!' + endfor + endfunction + ]]) + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('ctxpush', {'funcs'}) + command('delfunction Greet') + command('delfunction GreetAll') + + expect_err('Vim:E117: Unknown function: Greet', call, 'Greet', 'World') + expect_err('Vim:E117: Unknown function: Greet', call, 'GreetAll', + 'World', 'One', 'Two', 'Three') + + call('ctxpop') + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + end) + + it('errors out when context stack is empty', function() + local err = 'Vim:Context stack is empty' + expect_err(err, call, 'ctxpop') + expect_err(err, call, 'ctxpop') + call('ctxpush') + call('ctxpush') + call('ctxpop') + call('ctxpop') + expect_err(err, call, 'ctxpop') + end) + end) + + describe('ctxsize()', function() + it('returns context stack size', function() + eq(0, call('ctxsize')) + call('ctxpush') + eq(1, call('ctxsize')) + call('ctxpush') + eq(2, call('ctxsize')) + call('ctxpush') + eq(3, call('ctxsize')) + call('ctxpop') + eq(2, call('ctxsize')) + call('ctxpop') + eq(1, call('ctxsize')) + call('ctxpop') + eq(0, call('ctxsize')) + end) + end) + + describe('ctxget()', function() + it('errors out when index is out of bounds', function() + expect_err(outofbounds, call, 'ctxget') + call('ctxpush') + expect_err(outofbounds, call, 'ctxget', 1) + call('ctxpop') + expect_err(outofbounds, call, 'ctxget', 0) + end) + + it('returns context dictionary at index in context stack', function() + feed('i1<cr>2<cr>3<c-[>ddddddqahjklq') + command('edit! '..fname1) + feed('G') + feed('gg') + command('edit '..fname2) + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + + local with_regs = { + ['regs'] = { + {['rt'] = 1, ['rc'] = {'1'}, ['n'] = 49, ['ru'] = true}, + {['rt'] = 1, ['rc'] = {'2'}, ['n'] = 50}, + {['rt'] = 1, ['rc'] = {'3'}, ['n'] = 51}, + {['rc'] = {'hjkl'}, ['n'] = 97}, + } + } + + local with_jumps = { + ['jumps'] = eval(([[ + filter(map(add( + getjumplist()[0], { 'bufnr': bufnr('%'), 'lnum': getcurpos()[1] }), + 'filter( + { "f": expand("#".v:val.bufnr.":p"), "l": v:val.lnum }, + { k, v -> k != "l" || v != 1 })'), '!empty(v:val.f)') + ]]):gsub('\n', '')) + } + + local with_buflist = { + ['buflist'] = eval([[ + filter(map(getbufinfo(), '{ "f": v:val.name }'), '!empty(v:val.f)') + ]]) + } + + local with_gvars = { + ['gvars'] = {{'one', 1}, {'Two', 2}, {'THREE', 3}} + } + + local with_all = { + ['regs'] = with_regs['regs'], + ['jumps'] = with_jumps['jumps'], + ['buflist'] = with_buflist['buflist'], + ['gvars'] = with_gvars['gvars'], + } + + call('ctxpush') + eq(with_all, parse_context(call('ctxget'))) + eq(with_all, parse_context(call('ctxget', 0))) + + call('ctxpush', {'gvars'}) + eq(with_gvars, parse_context(call('ctxget'))) + eq(with_gvars, parse_context(call('ctxget', 0))) + eq(with_all, parse_context(call('ctxget', 1))) + + call('ctxpush', {'buflist'}) + eq(with_buflist, parse_context(call('ctxget'))) + eq(with_buflist, parse_context(call('ctxget', 0))) + eq(with_gvars, parse_context(call('ctxget', 1))) + eq(with_all, parse_context(call('ctxget', 2))) + + call('ctxpush', {'jumps'}) + eq(with_jumps, parse_context(call('ctxget'))) + eq(with_jumps, parse_context(call('ctxget', 0))) + eq(with_buflist, parse_context(call('ctxget', 1))) + eq(with_gvars, parse_context(call('ctxget', 2))) + eq(with_all, parse_context(call('ctxget', 3))) + + call('ctxpush', {'regs'}) + eq(with_regs, parse_context(call('ctxget'))) + eq(with_regs, parse_context(call('ctxget', 0))) + eq(with_jumps, parse_context(call('ctxget', 1))) + eq(with_buflist, parse_context(call('ctxget', 2))) + eq(with_gvars, parse_context(call('ctxget', 3))) + eq(with_all, parse_context(call('ctxget', 4))) + + call('ctxpop') + eq(with_jumps, parse_context(call('ctxget'))) + eq(with_jumps, parse_context(call('ctxget', 0))) + eq(with_buflist, parse_context(call('ctxget', 1))) + eq(with_gvars, parse_context(call('ctxget', 2))) + eq(with_all, parse_context(call('ctxget', 3))) + + call('ctxpop') + eq(with_buflist, parse_context(call('ctxget'))) + eq(with_buflist, parse_context(call('ctxget', 0))) + eq(with_gvars, parse_context(call('ctxget', 1))) + eq(with_all, parse_context(call('ctxget', 2))) + + call('ctxpop') + eq(with_gvars, parse_context(call('ctxget'))) + eq(with_gvars, parse_context(call('ctxget', 0))) + eq(with_all, parse_context(call('ctxget', 1))) + + call('ctxpop') + eq(with_all, parse_context(call('ctxget'))) + eq(with_all, parse_context(call('ctxget', 0))) + end) + end) + + describe('ctxset()', function() + it('errors out when index is out of bounds', function() + expect_err(outofbounds, call, 'ctxset', {dummy = 1}) + call('ctxpush') + expect_err(outofbounds, call, 'ctxset', {dummy = 1}, 1) + call('ctxpop') + expect_err(outofbounds, call, 'ctxset', {dummy = 1}, 0) + end) + + it('sets context dictionary at index in context stack', function() + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + call('ctxpush') + local ctx1 = call('ctxget') + nvim('set_var', 'one', 'a') + nvim('set_var', 'Two', 'b') + nvim('set_var', 'THREE', 'c') + call('ctxpush') + call('ctxpush') + local ctx2 = call('ctxget') + + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + call('ctxset', ctx1) + call('ctxset', ctx2, 2) + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpop') + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + nvim('set_var', 'one', 1.5) + eq({1.5, 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpop') + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + end) + end) +end) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index ce7d348747..8223290760 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -13,6 +13,8 @@ local check_cores = global_helpers.check_cores local check_logs = global_helpers.check_logs local dedent = global_helpers.dedent local eq = global_helpers.eq +local filter = global_helpers.filter +local map = global_helpers.map local ok = global_helpers.ok local sleep = global_helpers.sleep local tbl_contains = global_helpers.tbl_contains @@ -763,6 +765,22 @@ local function load_adjust(num) return math.ceil(num * load_factor) end +local function parse_context(ctx) + local parsed = {} + for _, item in ipairs({'regs', 'jumps', 'buflist', 'gvars'}) do + parsed[item] = filter(function(v) + return type(v) == 'table' + end, nvim_call('msgpackparse', ctx[item])) + end + parsed['buflist'] = parsed['buflist'][1] + return map(function(v) + if #v == 0 then + return nil + end + return v + end, parsed) +end + local module = { NIL = mpack.NIL, alter_slashes = alter_slashes, @@ -810,6 +828,7 @@ local module = { nvim_prog_abs = nvim_prog_abs, nvim_set = nvim_set, os_name = os_name, + parse_context = parse_context, pathroot = pathroot, pending_win32 = pending_win32, prepend_argv = prepend_argv, |