mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 18:55:14 -07:00
eval: Implement dictionary change notifications
This commit is contained in:
parent
ba1f327200
commit
df37aa6115
@ -1,2 +1,3 @@
|
||||
# libuv queue.h pointer arithmetic is not accepted by asan
|
||||
fun:queue_node_data
|
||||
fun:dictwatcher_node_data
|
||||
|
@ -1763,6 +1763,8 @@ cursor( {lnum}, {col} [, {off}])
|
||||
cursor( {list}) Number move cursor to position in {list}
|
||||
deepcopy( {expr} [, {noref}]) any make a full copy of {expr}
|
||||
delete( {fname}) Number delete file {fname}
|
||||
dictwatcheradd({dict}, {pattern}, {callback}) Start watching a dictionary
|
||||
dictwatcherdel({dict}, {pattern}, {callback}) Stop watching a dictionary
|
||||
did_filetype() Number TRUE if FileType autocommand event used
|
||||
diff_filler( {lnum}) Number diff filler lines about {lnum}
|
||||
diff_hlID( {lnum}, {col}) Number diff highlighting at {lnum}/{col}
|
||||
@ -2664,6 +2666,44 @@ delete({fname}) *delete()*
|
||||
To delete a line from the buffer use |:delete|. Use |:exe|
|
||||
when the line number is in a variable.
|
||||
|
||||
dictwatcheradd({dict}, {pattern}, {callback}) *dictwatcheradd()*
|
||||
Adds a watcher to a dictionary. A dictionary watcher is
|
||||
identified by three components:
|
||||
|
||||
- A dictionary({dict});
|
||||
- A key pattern({pattern}).
|
||||
- A function({callback}).
|
||||
|
||||
After this is called, every change on {dict} and on keys
|
||||
matching {pattern} will result in {callback} being invoked.
|
||||
|
||||
For now {pattern} only accepts very simple patterns that can
|
||||
contain a '*' at the end of the string, in which case it will
|
||||
match every key that begins with the substring before the '*'.
|
||||
That means if '*' is not the last character of {pattern}, only
|
||||
keys that are exactly equal as {pattern} will be matched.
|
||||
|
||||
The {callback} receives three arguments:
|
||||
|
||||
- The dictionary being watched.
|
||||
- The key which changed.
|
||||
- A dictionary containg the new and old values for the key.
|
||||
|
||||
The type of change can be determined by examining the keys
|
||||
present on the third argument:
|
||||
|
||||
- If contains both `old` and `new`, the key was updated.
|
||||
- If it contains only `new`, the key was added.
|
||||
- If it contains only `old`, the key was deleted.
|
||||
|
||||
This function can be used by plugins to implement options with
|
||||
validation and parsing logic.
|
||||
|
||||
dictwatcherdel({dict}, {pattern}, {callback}) *dictwatcherdel()*
|
||||
Removes a watcher added with |dictwatcheradd()|. All three
|
||||
arguments must match the ones passed to |dictwatcheradd()| in
|
||||
order for the watcher to be successfully deleted.
|
||||
|
||||
*did_filetype()*
|
||||
did_filetype() Returns non-zero when autocommands are being executed and the
|
||||
FileType event has been triggered at least once. Can be used
|
||||
|
362
src/nvim/eval.c
362
src/nvim/eval.c
@ -98,6 +98,7 @@
|
||||
#include "nvim/os/input.h"
|
||||
#include "nvim/event/loop.h"
|
||||
#include "nvim/lib/kvec.h"
|
||||
#include "nvim/lib/queue.h"
|
||||
|
||||
#define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */
|
||||
|
||||
@ -459,6 +460,13 @@ typedef struct {
|
||||
Queue *events;
|
||||
} TerminalJobData;
|
||||
|
||||
typedef struct dict_watcher {
|
||||
ufunc_T *callback;
|
||||
char *key_pattern;
|
||||
QUEUE node;
|
||||
bool busy; // prevent recursion if the dict is changed in the callback
|
||||
} DictWatcher;
|
||||
|
||||
/// Structure representing current VimL to messagepack conversion state
|
||||
typedef struct {
|
||||
enum {
|
||||
@ -2376,6 +2384,14 @@ static void set_var_lval(lval_T *lp, char_u *endp, typval_T *rettv, int copy, ch
|
||||
: lp->ll_n1 != lp->ll_n2)
|
||||
EMSG(_("E711: List value has not enough items"));
|
||||
} else {
|
||||
typval_T oldtv;
|
||||
dict_T *dict = lp->ll_dict;
|
||||
bool watched = is_watched(dict);
|
||||
|
||||
if (watched) {
|
||||
init_tv(&oldtv);
|
||||
}
|
||||
|
||||
/*
|
||||
* Assign to a List or Dictionary item.
|
||||
*/
|
||||
@ -2392,22 +2408,38 @@ static void set_var_lval(lval_T *lp, char_u *endp, typval_T *rettv, int copy, ch
|
||||
return;
|
||||
}
|
||||
lp->ll_tv = &di->di_tv;
|
||||
} else if (op != NULL && *op != '=') {
|
||||
tv_op(lp->ll_tv, rettv, op);
|
||||
return;
|
||||
} else
|
||||
clear_tv(lp->ll_tv);
|
||||
} else {
|
||||
if (watched) {
|
||||
copy_tv(lp->ll_tv, &oldtv);
|
||||
}
|
||||
|
||||
/*
|
||||
* Assign the value to the variable or list item.
|
||||
*/
|
||||
if (copy)
|
||||
if (op != NULL && *op != '=') {
|
||||
tv_op(lp->ll_tv, rettv, op);
|
||||
goto notify;
|
||||
} else {
|
||||
clear_tv(lp->ll_tv);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the value to the variable or list item.
|
||||
if (copy) {
|
||||
copy_tv(rettv, lp->ll_tv);
|
||||
else {
|
||||
} else {
|
||||
*lp->ll_tv = *rettv;
|
||||
lp->ll_tv->v_lock = 0;
|
||||
init_tv(rettv);
|
||||
}
|
||||
|
||||
notify:
|
||||
if (watched) {
|
||||
if (oldtv.v_type == VAR_UNKNOWN) {
|
||||
dictwatcher_notify(dict, (char *)lp->ll_newkey, lp->ll_tv, NULL);
|
||||
} else {
|
||||
dictitem_T *di = lp->ll_di;
|
||||
dictwatcher_notify(dict, (char *)di->di_key, lp->ll_tv, &oldtv);
|
||||
clear_tv(&oldtv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2932,12 +2964,31 @@ static int do_unlet_var(lval_T *lp, char_u *name_end, int forceit)
|
||||
++lp->ll_n1;
|
||||
}
|
||||
} else {
|
||||
if (lp->ll_list != NULL)
|
||||
/* unlet a List item. */
|
||||
if (lp->ll_list != NULL) {
|
||||
// unlet a List item.
|
||||
listitem_remove(lp->ll_list, lp->ll_li);
|
||||
else
|
||||
/* unlet a Dictionary item. */
|
||||
dictitem_remove(lp->ll_dict, lp->ll_di);
|
||||
} else {
|
||||
// unlet a Dictionary item.
|
||||
dict_T *d = lp->ll_dict;
|
||||
dictitem_T *di = lp->ll_di;
|
||||
bool watched = is_watched(d);
|
||||
char *key = NULL;
|
||||
typval_T oldtv;
|
||||
|
||||
if (watched) {
|
||||
copy_tv(&di->di_tv, &oldtv);
|
||||
// need to save key because dictitem_remove will free it
|
||||
key = xstrdup((char *)di->di_key);
|
||||
}
|
||||
|
||||
dictitem_remove(d, di);
|
||||
|
||||
if (watched) {
|
||||
dictwatcher_notify(d, key, NULL, &oldtv);
|
||||
clear_tv(&oldtv);
|
||||
xfree(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -2953,8 +3004,9 @@ int do_unlet(char_u *name, int forceit)
|
||||
hashitem_T *hi;
|
||||
char_u *varname;
|
||||
dictitem_T *di;
|
||||
dict_T *dict;
|
||||
ht = find_var_ht_dict(name, &varname, &dict);
|
||||
|
||||
ht = find_var_ht(name, &varname);
|
||||
if (ht != NULL && *varname != NUL) {
|
||||
hi = hash_find(ht, varname);
|
||||
if (!HASHITEM_EMPTY(hi)) {
|
||||
@ -2962,7 +3014,19 @@ int do_unlet(char_u *name, int forceit)
|
||||
if (var_check_fixed(di->di_flags, name)
|
||||
|| var_check_ro(di->di_flags, name))
|
||||
return FAIL;
|
||||
typval_T oldtv;
|
||||
bool watched = is_watched(dict);
|
||||
|
||||
if (watched) {
|
||||
copy_tv(&di->di_tv, &oldtv);
|
||||
}
|
||||
|
||||
delete_var(ht, hi);
|
||||
|
||||
if (watched) {
|
||||
dictwatcher_notify(dict, (char *)varname, NULL, &oldtv);
|
||||
clear_tv(&oldtv);
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
@ -5959,6 +6023,7 @@ dict_T *dict_alloc(void) FUNC_ATTR_NONNULL_RET
|
||||
d->dv_refcount = 0;
|
||||
d->dv_copyID = 0;
|
||||
d->internal_refcount = 0;
|
||||
QUEUE_INIT(&d->watchers);
|
||||
|
||||
return d;
|
||||
}
|
||||
@ -6025,6 +6090,14 @@ dict_free (
|
||||
--todo;
|
||||
}
|
||||
}
|
||||
|
||||
while (!QUEUE_EMPTY(&d->watchers)) {
|
||||
QUEUE *w = QUEUE_HEAD(&d->watchers);
|
||||
DictWatcher *watcher = dictwatcher_node_data(w);
|
||||
dictwatcher_free(watcher);
|
||||
QUEUE_REMOVE(w);
|
||||
}
|
||||
|
||||
hash_clear(&d->dv_hashtab);
|
||||
xfree(d);
|
||||
}
|
||||
@ -6066,10 +6139,11 @@ static void dictitem_remove(dict_T *dict, dictitem_T *item)
|
||||
hashitem_T *hi;
|
||||
|
||||
hi = hash_find(&dict->dv_hashtab, item->di_key);
|
||||
if (HASHITEM_EMPTY(hi))
|
||||
if (HASHITEM_EMPTY(hi)) {
|
||||
EMSG2(_(e_intern2), "dictitem_remove()");
|
||||
else
|
||||
} else {
|
||||
hash_remove(&dict->dv_hashtab, hi);
|
||||
}
|
||||
dictitem_free(item);
|
||||
}
|
||||
|
||||
@ -6285,6 +6359,12 @@ static ufunc_T *find_ufunc(uint8_t *name)
|
||||
// dict function, name is already translated
|
||||
rv = find_func(n);
|
||||
}
|
||||
|
||||
if (!rv) {
|
||||
EMSG2(_("Function %s doesn't exist"), name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
@ -7098,6 +7178,8 @@ static struct fst {
|
||||
{"cursor", 1, 3, f_cursor},
|
||||
{"deepcopy", 1, 2, f_deepcopy},
|
||||
{"delete", 1, 1, f_delete},
|
||||
{"dictwatcheradd", 3, 3, f_dictwatcheradd},
|
||||
{"dictwatcherdel", 3, 3, f_dictwatcherdel},
|
||||
{"did_filetype", 0, 0, f_did_filetype},
|
||||
{"diff_filler", 1, 1, f_diff_filler},
|
||||
{"diff_hlID", 2, 2, f_diff_hlID},
|
||||
@ -8666,6 +8748,110 @@ static void f_delete(typval_T *argvars, typval_T *rettv)
|
||||
rettv->vval.v_number = os_remove((char *)get_tv_string(&argvars[0]));
|
||||
}
|
||||
|
||||
// dictwatcheradd(dict, key, funcref) function
|
||||
static void f_dictwatcheradd(typval_T *argvars, typval_T *rettv)
|
||||
{
|
||||
if (check_restricted() || check_secure()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[0].v_type != VAR_DICT) {
|
||||
EMSG2(e_invarg2, "dict");
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[1].v_type != VAR_STRING && argvars[1].v_type != VAR_NUMBER) {
|
||||
EMSG2(e_invarg2, "key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[2].v_type != VAR_FUNC && argvars[2].v_type != VAR_STRING) {
|
||||
EMSG2(e_invarg2, "funcref");
|
||||
return;
|
||||
}
|
||||
|
||||
char *key_pattern = (char *)get_tv_string_chk(argvars + 1);
|
||||
assert(key_pattern);
|
||||
const size_t key_len = STRLEN(argvars[1].vval.v_string);
|
||||
|
||||
if (key_len == 0) {
|
||||
EMSG(_(e_emptykey));
|
||||
return;
|
||||
}
|
||||
|
||||
ufunc_T *func = find_ufunc(argvars[2].vval.v_string);
|
||||
if (!func) {
|
||||
// Invalid function name. Error already reported by `find_ufunc`.
|
||||
return;
|
||||
}
|
||||
|
||||
func->uf_refcount++;
|
||||
DictWatcher *watcher = xmalloc(sizeof(DictWatcher));
|
||||
watcher->key_pattern = xmemdupz(key_pattern, key_len);
|
||||
watcher->callback = func;
|
||||
watcher->busy = false;
|
||||
QUEUE_INSERT_TAIL(&argvars[0].vval.v_dict->watchers, &watcher->node);
|
||||
}
|
||||
|
||||
// dictwatcherdel(dict, key, funcref) function
|
||||
static void f_dictwatcherdel(typval_T *argvars, typval_T *rettv)
|
||||
{
|
||||
if (check_restricted() || check_secure()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[0].v_type != VAR_DICT) {
|
||||
EMSG2(e_invarg2, "dict");
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[1].v_type != VAR_STRING && argvars[1].v_type != VAR_NUMBER) {
|
||||
EMSG2(e_invarg2, "key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[2].v_type != VAR_FUNC && argvars[2].v_type != VAR_STRING) {
|
||||
EMSG2(e_invarg2, "funcref");
|
||||
return;
|
||||
}
|
||||
|
||||
char *key_pattern = (char *)get_tv_string_chk(argvars + 1);
|
||||
assert(key_pattern);
|
||||
const size_t key_len = STRLEN(argvars[1].vval.v_string);
|
||||
|
||||
if (key_len == 0) {
|
||||
EMSG(_(e_emptykey));
|
||||
return;
|
||||
}
|
||||
|
||||
ufunc_T *func = find_ufunc(argvars[2].vval.v_string);
|
||||
if (!func) {
|
||||
// Invalid function name. Error already reported by `find_ufunc`.
|
||||
return;
|
||||
}
|
||||
|
||||
dict_T *dict = argvars[0].vval.v_dict;
|
||||
QUEUE *w = NULL;
|
||||
DictWatcher *watcher = NULL;
|
||||
bool matched = false;
|
||||
QUEUE_FOREACH(w, &dict->watchers) {
|
||||
watcher = dictwatcher_node_data(w);
|
||||
if (func == watcher->callback
|
||||
&& !strcmp(watcher->key_pattern, key_pattern)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
EMSG("Couldn't find a watcher matching key and callback");
|
||||
return;
|
||||
}
|
||||
|
||||
QUEUE_REMOVE(w);
|
||||
dictwatcher_free(watcher);
|
||||
}
|
||||
|
||||
/*
|
||||
* "did_filetype()" function
|
||||
*/
|
||||
@ -8972,6 +9158,7 @@ void dict_extend(dict_T *d1, dict_T *d2, char_u *action)
|
||||
dictitem_T *di1;
|
||||
hashitem_T *hi2;
|
||||
int todo;
|
||||
bool watched = is_watched(d1);
|
||||
|
||||
todo = (int)d2->dv_hashtab.ht_used;
|
||||
for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2) {
|
||||
@ -8992,14 +9179,30 @@ void dict_extend(dict_T *d1, dict_T *d2, char_u *action)
|
||||
}
|
||||
if (di1 == NULL) {
|
||||
di1 = dictitem_copy(HI2DI(hi2));
|
||||
if (dict_add(d1, di1) == FAIL)
|
||||
if (dict_add(d1, di1) == FAIL) {
|
||||
dictitem_free(di1);
|
||||
}
|
||||
|
||||
if (watched) {
|
||||
dictwatcher_notify(d1, (char *)di1->di_key, &di1->di_tv, NULL);
|
||||
}
|
||||
} else if (*action == 'e') {
|
||||
EMSG2(_("E737: Key already exists: %s"), hi2->hi_key);
|
||||
break;
|
||||
} else if (*action == 'f' && HI2DI(hi2) != di1) {
|
||||
typval_T oldtv;
|
||||
|
||||
if (watched) {
|
||||
copy_tv(&di1->di_tv, &oldtv);
|
||||
}
|
||||
|
||||
clear_tv(&di1->di_tv);
|
||||
copy_tv(&HI2DI(hi2)->di_tv, &di1->di_tv);
|
||||
|
||||
if (watched) {
|
||||
dictwatcher_notify(d1, (char *)di1->di_key, &di1->di_tv, &oldtv);
|
||||
clear_tv(&oldtv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13517,6 +13720,9 @@ static void f_remove(typval_T *argvars, typval_T *rettv)
|
||||
*rettv = di->di_tv;
|
||||
init_tv(&di->di_tv);
|
||||
dictitem_remove(d, di);
|
||||
if (is_watched(d)) {
|
||||
dictwatcher_notify(d, (char *)key, NULL, rettv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18233,6 +18439,7 @@ void init_var_dict(dict_T *dict, dictitem_T *dict_var, int scope)
|
||||
dict_var->di_tv.v_lock = VAR_FIXED;
|
||||
dict_var->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
|
||||
dict_var->di_key[0] = NUL;
|
||||
QUEUE_INIT(&dict->watchers);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -18365,8 +18572,16 @@ set_var (
|
||||
dictitem_T *v;
|
||||
char_u *varname;
|
||||
hashtab_T *ht;
|
||||
typval_T oldtv;
|
||||
dict_T *dict;
|
||||
|
||||
ht = find_var_ht_dict(name, &varname, &dict);
|
||||
bool watched = is_watched(dict);
|
||||
|
||||
if (watched) {
|
||||
init_tv(&oldtv);
|
||||
}
|
||||
|
||||
ht = find_var_ht(name, &varname);
|
||||
if (ht == NULL || *varname == NUL) {
|
||||
EMSG2(_(e_illvar), name);
|
||||
return;
|
||||
@ -18423,6 +18638,9 @@ set_var (
|
||||
return;
|
||||
}
|
||||
|
||||
if (watched) {
|
||||
copy_tv(&v->di_tv, &oldtv);
|
||||
}
|
||||
clear_tv(&v->di_tv);
|
||||
} else { /* add a new variable */
|
||||
/* Can't add "v:" variable. */
|
||||
@ -18444,13 +18662,22 @@ set_var (
|
||||
v->di_flags = 0;
|
||||
}
|
||||
|
||||
if (copy || tv->v_type == VAR_NUMBER || tv->v_type == VAR_FLOAT)
|
||||
if (copy || tv->v_type == VAR_NUMBER || tv->v_type == VAR_FLOAT) {
|
||||
copy_tv(tv, &v->di_tv);
|
||||
else {
|
||||
} else {
|
||||
v->di_tv = *tv;
|
||||
v->di_tv.v_lock = 0;
|
||||
init_tv(tv);
|
||||
}
|
||||
|
||||
if (watched) {
|
||||
if (oldtv.v_type == VAR_UNKNOWN) {
|
||||
dictwatcher_notify(dict, (char *)v->di_key, &v->di_tv, NULL);
|
||||
} else {
|
||||
dictwatcher_notify(dict, (char *)v->di_key, &v->di_tv, &oldtv);
|
||||
clear_tv(&oldtv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -21771,3 +21998,94 @@ bool eval_has_provider(char *name)
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute the `DictWatcher` address from a QUEUE node. This only exists because
|
||||
// ASAN doesn't handle `QUEUE_DATA` pointer arithmetic, and we blacklist this
|
||||
// function on .asan-blacklist.
|
||||
static DictWatcher *dictwatcher_node_data(QUEUE *q)
|
||||
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET
|
||||
{
|
||||
return QUEUE_DATA(q, DictWatcher, node);
|
||||
}
|
||||
|
||||
// Send a change notification to all `dict` watchers that match `key`.
|
||||
static void dictwatcher_notify(dict_T *dict, const char *key, typval_T *newtv,
|
||||
typval_T *oldtv)
|
||||
FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2)
|
||||
{
|
||||
typval_T argv[3];
|
||||
for (size_t i = 0; i < ARRAY_SIZE(argv); i++) {
|
||||
init_tv(argv + i);
|
||||
}
|
||||
|
||||
argv[0].v_type = VAR_DICT;
|
||||
argv[0].vval.v_dict = dict;
|
||||
argv[1].v_type = VAR_STRING;
|
||||
argv[1].vval.v_string = (char_u *)xstrdup(key);
|
||||
argv[2].v_type = VAR_DICT;
|
||||
argv[2].vval.v_dict = dict_alloc();
|
||||
argv[2].vval.v_dict->dv_refcount++;
|
||||
|
||||
if (newtv) {
|
||||
dictitem_T *v = dictitem_alloc((char_u *)"new");
|
||||
copy_tv(newtv, &v->di_tv);
|
||||
dict_add(argv[2].vval.v_dict, v);
|
||||
}
|
||||
|
||||
if (oldtv) {
|
||||
dictitem_T *v = dictitem_alloc((char_u *)"old");
|
||||
copy_tv(oldtv, &v->di_tv);
|
||||
dict_add(argv[2].vval.v_dict, v);
|
||||
}
|
||||
|
||||
typval_T rettv;
|
||||
|
||||
QUEUE *w;
|
||||
QUEUE_FOREACH(w, &dict->watchers) {
|
||||
DictWatcher *watcher = dictwatcher_node_data(w);
|
||||
if (!watcher->busy && dictwatcher_matches(watcher, key)) {
|
||||
init_tv(&rettv);
|
||||
watcher->busy = true;
|
||||
call_user_func(watcher->callback, ARRAY_SIZE(argv), argv, &rettv,
|
||||
curwin->w_cursor.lnum, curwin->w_cursor.lnum, NULL);
|
||||
watcher->busy = false;
|
||||
clear_tv(&rettv);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < ARRAY_SIZE(argv); i++) {
|
||||
clear_tv(argv + i);
|
||||
}
|
||||
}
|
||||
|
||||
// Test if `key` matches with with `watcher->key_pattern`
|
||||
static bool dictwatcher_matches(DictWatcher *watcher, const char *key)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
// For now only allow very simple globbing in key patterns: a '*' at the end
|
||||
// of the string means it should match everything up to the '*' instead of the
|
||||
// whole string.
|
||||
char *nul = strchr(watcher->key_pattern, NUL);
|
||||
size_t len = nul - watcher->key_pattern;
|
||||
if (*(nul - 1) == '*') {
|
||||
return !strncmp(key, watcher->key_pattern, len - 1);
|
||||
} else {
|
||||
return !strcmp(key, watcher->key_pattern);
|
||||
}
|
||||
}
|
||||
|
||||
// Perform all necessary cleanup for a `DictWatcher` instance.
|
||||
static void dictwatcher_free(DictWatcher *watcher)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
user_func_unref(watcher->callback);
|
||||
xfree(watcher->key_pattern);
|
||||
xfree(watcher);
|
||||
}
|
||||
|
||||
// Check if `d` has at least one watcher.
|
||||
static bool is_watched(dict_T *d)
|
||||
{
|
||||
return d && !QUEUE_EMPTY(&d->watchers);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include "nvim/hashtab.h"
|
||||
#include "nvim/lib/queue.h"
|
||||
|
||||
typedef int varnumber_T;
|
||||
typedef double float_T;
|
||||
@ -119,6 +120,7 @@ struct dictvar_S {
|
||||
dict_T *dv_used_prev; /* previous dict in used dicts list */
|
||||
int internal_refcount; // number of internal references to
|
||||
// prevent garbage collection
|
||||
QUEUE watchers; // dictionary key watchers set by user code
|
||||
};
|
||||
|
||||
// structure used for explicit stack while garbage collecting hash tables
|
||||
|
257
test/functional/dict_notifications_spec.lua
Normal file
257
test/functional/dict_notifications_spec.lua
Normal file
@ -0,0 +1,257 @@
|
||||
local helpers = require('test.functional.helpers')
|
||||
local clear, nvim, source = helpers.clear, helpers.nvim, helpers.source
|
||||
local eq, next_msg = helpers.eq, helpers.next_message
|
||||
local exc_exec = helpers.exc_exec
|
||||
|
||||
|
||||
describe('dictionary change notifications', function()
|
||||
local channel
|
||||
|
||||
setup(function()
|
||||
clear()
|
||||
channel = nvim('get_api_info')[1]
|
||||
nvim('set_var', 'channel', channel)
|
||||
end)
|
||||
|
||||
-- the same set of tests are applied to top-level dictionaries(g:, b:, w: and
|
||||
-- t:) and a dictionary variable, so we generate them in the following
|
||||
-- function.
|
||||
local function gentests(dict_expr, dict_expr_suffix, dict_init)
|
||||
if not dict_expr_suffix then
|
||||
dict_expr_suffix = ''
|
||||
end
|
||||
|
||||
local function update(opval, key)
|
||||
if not key then
|
||||
key = 'watched'
|
||||
end
|
||||
if opval == '' then
|
||||
nvim('command', "unlet "..dict_expr..dict_expr_suffix..key)
|
||||
else
|
||||
nvim('command', "let "..dict_expr..dict_expr_suffix..key.." "..opval)
|
||||
end
|
||||
end
|
||||
|
||||
local function verify_echo()
|
||||
-- helper to verify that no notifications are sent after certain change
|
||||
-- to a dict
|
||||
nvim('command', "call rpcnotify(g:channel, 'echo')")
|
||||
eq({'notification', 'echo', {}}, next_msg())
|
||||
end
|
||||
|
||||
local function verify_value(vals, key)
|
||||
if not key then
|
||||
key = 'watched'
|
||||
end
|
||||
eq({'notification', 'values', {key, vals}}, next_msg())
|
||||
end
|
||||
|
||||
describe('watcher', function()
|
||||
if dict_init then
|
||||
setup(function()
|
||||
source(dict_init)
|
||||
end)
|
||||
end
|
||||
|
||||
before_each(function()
|
||||
source([[
|
||||
function! g:Changed(dict, key, value)
|
||||
if a:dict != ]]..dict_expr..[[ |
|
||||
throw 'invalid dict'
|
||||
endif
|
||||
call rpcnotify(g:channel, 'values', a:key, a:value)
|
||||
endfunction
|
||||
call dictwatcheradd(]]..dict_expr..[[, "watched", "g:Changed")
|
||||
call dictwatcheradd(]]..dict_expr..[[, "watched2", "g:Changed")
|
||||
]])
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
source([[
|
||||
call dictwatcherdel(]]..dict_expr..[[, "watched", "g:Changed")
|
||||
call dictwatcherdel(]]..dict_expr..[[, "watched2", "g:Changed")
|
||||
]])
|
||||
update('= "test"')
|
||||
update('= "test2"', 'watched2')
|
||||
update('', 'watched2')
|
||||
update('')
|
||||
verify_echo()
|
||||
end)
|
||||
|
||||
it('is not triggered when unwatched keys are updated', function()
|
||||
update('= "noop"', 'unwatched')
|
||||
update('.= "noop2"', 'unwatched')
|
||||
update('', 'unwatched')
|
||||
verify_echo()
|
||||
end)
|
||||
|
||||
it('is triggered by remove()', function()
|
||||
update('= "test"')
|
||||
verify_value({new = 'test'})
|
||||
nvim('command', 'call remove('..dict_expr..', "watched")')
|
||||
verify_value({old = 'test'})
|
||||
end)
|
||||
|
||||
it('is triggered by extend()', function()
|
||||
update('= "xtend"')
|
||||
verify_value({new = 'xtend'})
|
||||
nvim('command', [[
|
||||
call extend(]]..dict_expr..[[, {'watched': 'xtend2', 'watched2': 5, 'watched3': 'a'})
|
||||
]])
|
||||
verify_value({old = 'xtend', new = 'xtend2'})
|
||||
verify_value({new = 5}, 'watched2')
|
||||
update('')
|
||||
verify_value({old = 'xtend2'})
|
||||
update('', 'watched2')
|
||||
verify_value({old = 5}, 'watched2')
|
||||
update('', 'watched3')
|
||||
verify_echo()
|
||||
end)
|
||||
|
||||
it('is triggered with key patterns', function()
|
||||
source([[
|
||||
call dictwatcheradd(]]..dict_expr..[[, "wat*", "g:Changed")
|
||||
]])
|
||||
update('= 1')
|
||||
verify_value({new = 1})
|
||||
verify_value({new = 1})
|
||||
update('= 3', 'watched2')
|
||||
verify_value({new = 3}, 'watched2')
|
||||
verify_value({new = 3}, 'watched2')
|
||||
verify_echo()
|
||||
source([[
|
||||
call dictwatcherdel(]]..dict_expr..[[, "wat*", "g:Changed")
|
||||
]])
|
||||
-- watch every key pattern
|
||||
source([[
|
||||
call dictwatcheradd(]]..dict_expr..[[, "*", "g:Changed")
|
||||
]])
|
||||
update('= 3', 'another_key')
|
||||
update('= 4', 'another_key')
|
||||
update('', 'another_key')
|
||||
update('= 2')
|
||||
verify_value({new = 3}, 'another_key')
|
||||
verify_value({old = 3, new = 4}, 'another_key')
|
||||
verify_value({old = 4}, 'another_key')
|
||||
verify_value({old = 1, new = 2})
|
||||
verify_value({old = 1, new = 2})
|
||||
verify_echo()
|
||||
source([[
|
||||
call dictwatcherdel(]]..dict_expr..[[, "*", "g:Changed")
|
||||
]])
|
||||
end)
|
||||
|
||||
-- test a sequence of updates of different types to ensure proper memory
|
||||
-- management(with ASAN)
|
||||
local function test_updates(tests)
|
||||
it('test change sequence', function()
|
||||
local input, output
|
||||
for i = 1, #tests do
|
||||
input, output = unpack(tests[i])
|
||||
update(input)
|
||||
verify_value(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test_updates({
|
||||
{'= 3', {new = 3}},
|
||||
{'= 6', {old = 3, new = 6}},
|
||||
{'+= 3', {old = 6, new = 9}},
|
||||
{'', {old = 9}}
|
||||
})
|
||||
|
||||
test_updates({
|
||||
{'= "str"', {new = 'str'}},
|
||||
{'= "str2"', {old = 'str', new = 'str2'}},
|
||||
{'.= "2str"', {old = 'str2', new = 'str22str'}},
|
||||
{'', {old = 'str22str'}}
|
||||
})
|
||||
|
||||
test_updates({
|
||||
{'= [1, 2]', {new = {1, 2}}},
|
||||
{'= [1, 2, 3]', {old = {1, 2}, new = {1, 2, 3}}},
|
||||
-- the += will update the list in place, so old and new are the same
|
||||
{'+= [4, 5]', {old = {1, 2, 3, 4, 5}, new = {1, 2, 3, 4, 5}}},
|
||||
{'', {old = {1, 2, 3, 4 ,5}}}
|
||||
})
|
||||
|
||||
test_updates({
|
||||
{'= {"k": "v"}', {new = {k = 'v'}}},
|
||||
{'= {"k1": 2}', {old = {k = 'v'}, new = {k1 = 2}}},
|
||||
{'', {old = {k1 = 2}}},
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
gentests('g:')
|
||||
gentests('b:')
|
||||
gentests('w:')
|
||||
gentests('t:')
|
||||
gentests('g:dict_var', '.', 'let g:dict_var = {}')
|
||||
|
||||
describe('multiple watchers on the same dict/key', function()
|
||||
setup(function()
|
||||
source([[
|
||||
function! g:Watcher1(dict, key, value)
|
||||
call rpcnotify(g:channel, '1', a:key, a:value)
|
||||
endfunction
|
||||
function! g:Watcher2(dict, key, value)
|
||||
call rpcnotify(g:channel, '2', a:key, a:value)
|
||||
endfunction
|
||||
call dictwatcheradd(g:, "key", "g:Watcher1")
|
||||
call dictwatcheradd(g:, "key", "g:Watcher2")
|
||||
]])
|
||||
end)
|
||||
|
||||
it('invokes all callbacks when the key is changed', function()
|
||||
nvim('command', 'let g:key = "value"')
|
||||
eq({'notification', '1', {'key', {new = 'value'}}}, next_msg())
|
||||
eq({'notification', '2', {'key', {new = 'value'}}}, next_msg())
|
||||
end)
|
||||
|
||||
it('only removes watchers that fully match dict, key and callback', function()
|
||||
nvim('command', 'call dictwatcherdel(g:, "key", "g:Watcher1")')
|
||||
nvim('command', 'let g:key = "v2"')
|
||||
eq({'notification', '2', {'key', {old = 'value', new = 'v2'}}}, next_msg())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('errors', function()
|
||||
-- WARNING: This suite depends on the above tests
|
||||
it('fails to remove if no watcher with matching callback is found', function()
|
||||
eq("Vim(call):Couldn't find a watcher matching key and callback",
|
||||
exc_exec('call dictwatcherdel(g:, "key", "g:Watcher1")'))
|
||||
end)
|
||||
|
||||
it('fails to remove if no watcher with matching key is found', function()
|
||||
eq("Vim(call):Couldn't find a watcher matching key and callback",
|
||||
exc_exec('call dictwatcherdel(g:, "invalid_key", "g:Watcher2")'))
|
||||
end)
|
||||
|
||||
it("fails to add/remove if the callback doesn't exist", function()
|
||||
eq("Vim(call):Function g:InvalidCb doesn't exist",
|
||||
exc_exec('call dictwatcheradd(g:, "key", "g:InvalidCb")'))
|
||||
eq("Vim(call):Function g:InvalidCb doesn't exist",
|
||||
exc_exec('call dictwatcherdel(g:, "key", "g:InvalidCb")'))
|
||||
end)
|
||||
|
||||
it('fails with empty keys', function()
|
||||
eq("Vim(call):E713: Cannot use empty key for Dictionary",
|
||||
exc_exec('call dictwatcheradd(g:, "", "g:Watcher1")'))
|
||||
eq("Vim(call):E713: Cannot use empty key for Dictionary",
|
||||
exc_exec('call dictwatcherdel(g:, "", "g:Watcher1")'))
|
||||
end)
|
||||
|
||||
it('fails to replace a watcher function', function()
|
||||
source([[
|
||||
function! g:ReplaceWatcher2()
|
||||
function! g:Watcher2()
|
||||
endfunction
|
||||
endfunction
|
||||
]])
|
||||
eq("Vim(function):E127: Cannot redefine function Watcher2: It is in use",
|
||||
exc_exec('call g:ReplaceWatcher2()'))
|
||||
end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user