Merge #10619 'API: context'

This commit is contained in:
Justin M. Keyes 2019-07-27 22:56:05 +02:00 committed by GitHub
commit 8e6b0a73c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1427 additions and 124 deletions

View File

@ -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}.

View File

@ -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.

383
src/nvim/context.c Normal file
View File

@ -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;
}

46
src/nvim/context.h Normal file
View File

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

View File

@ -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, &reg, &is_unnamed);
reg_iter = op_global_reg_iter(reg_iter, &name, &reg, &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(&regmatch, 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;
}
}

View File

@ -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,

View File

@ -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}},

View File

@ -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) {

View File

@ -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.

View File

@ -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);

View File

@ -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, &reg, &is_unnamed);
reg_iter = op_global_reg_iter(reg_iter, &name, &reg, &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);
}

View File

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

View File

@ -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))

View File

@ -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)

View File

@ -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,