eval: Implement dictionary change notifications

This commit is contained in:
Thiago de Arruda 2015-11-05 14:02:48 -03:00
parent ba1f327200
commit df37aa6115
5 changed files with 640 additions and 22 deletions

View File

@ -1,2 +1,3 @@
# libuv queue.h pointer arithmetic is not accepted by asan # libuv queue.h pointer arithmetic is not accepted by asan
fun:queue_node_data fun:queue_node_data
fun:dictwatcher_node_data

View File

@ -1763,6 +1763,8 @@ cursor( {lnum}, {col} [, {off}])
cursor( {list}) Number move cursor to position in {list} cursor( {list}) Number move cursor to position in {list}
deepcopy( {expr} [, {noref}]) any make a full copy of {expr} deepcopy( {expr} [, {noref}]) any make a full copy of {expr}
delete( {fname}) Number delete file {fname} 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 did_filetype() Number TRUE if FileType autocommand event used
diff_filler( {lnum}) Number diff filler lines about {lnum} diff_filler( {lnum}) Number diff filler lines about {lnum}
diff_hlID( {lnum}, {col}) Number diff highlighting at {lnum}/{col} 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| To delete a line from the buffer use |:delete|. Use |:exe|
when the line number is in a variable. 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()*
did_filetype() Returns non-zero when autocommands are being executed and the did_filetype() Returns non-zero when autocommands are being executed and the
FileType event has been triggered at least once. Can be used FileType event has been triggered at least once. Can be used

View File

@ -98,6 +98,7 @@
#include "nvim/os/input.h" #include "nvim/os/input.h"
#include "nvim/event/loop.h" #include "nvim/event/loop.h"
#include "nvim/lib/kvec.h" #include "nvim/lib/kvec.h"
#include "nvim/lib/queue.h"
#define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */ #define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */
@ -459,6 +460,13 @@ typedef struct {
Queue *events; Queue *events;
} TerminalJobData; } 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 /// Structure representing current VimL to messagepack conversion state
typedef struct { typedef struct {
enum { 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) : lp->ll_n1 != lp->ll_n2)
EMSG(_("E711: List value has not enough items")); EMSG(_("E711: List value has not enough items"));
} else { } 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. * 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; return;
} }
lp->ll_tv = &di->di_tv; lp->ll_tv = &di->di_tv;
} else if (op != NULL && *op != '=') { } else {
tv_op(lp->ll_tv, rettv, op); if (watched) {
return; copy_tv(lp->ll_tv, &oldtv);
} else }
clear_tv(lp->ll_tv);
/* if (op != NULL && *op != '=') {
* Assign the value to the variable or list item. tv_op(lp->ll_tv, rettv, op);
*/ goto notify;
if (copy) } else {
clear_tv(lp->ll_tv);
}
}
// Assign the value to the variable or list item.
if (copy) {
copy_tv(rettv, lp->ll_tv); copy_tv(rettv, lp->ll_tv);
else { } else {
*lp->ll_tv = *rettv; *lp->ll_tv = *rettv;
lp->ll_tv->v_lock = 0; lp->ll_tv->v_lock = 0;
init_tv(rettv); 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; ++lp->ll_n1;
} }
} else { } else {
if (lp->ll_list != NULL) if (lp->ll_list != NULL) {
/* unlet a List item. */ // unlet a List item.
listitem_remove(lp->ll_list, lp->ll_li); listitem_remove(lp->ll_list, lp->ll_li);
else } else {
/* unlet a Dictionary item. */ // unlet a Dictionary item.
dictitem_remove(lp->ll_dict, lp->ll_di); 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; return ret;
@ -2953,8 +3004,9 @@ int do_unlet(char_u *name, int forceit)
hashitem_T *hi; hashitem_T *hi;
char_u *varname; char_u *varname;
dictitem_T *di; 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) { if (ht != NULL && *varname != NUL) {
hi = hash_find(ht, varname); hi = hash_find(ht, varname);
if (!HASHITEM_EMPTY(hi)) { if (!HASHITEM_EMPTY(hi)) {
@ -2962,7 +3014,19 @@ int do_unlet(char_u *name, int forceit)
if (var_check_fixed(di->di_flags, name) if (var_check_fixed(di->di_flags, name)
|| var_check_ro(di->di_flags, name)) || var_check_ro(di->di_flags, name))
return FAIL; return FAIL;
typval_T oldtv;
bool watched = is_watched(dict);
if (watched) {
copy_tv(&di->di_tv, &oldtv);
}
delete_var(ht, hi); delete_var(ht, hi);
if (watched) {
dictwatcher_notify(dict, (char *)varname, NULL, &oldtv);
clear_tv(&oldtv);
}
return OK; return OK;
} }
} }
@ -5959,6 +6023,7 @@ dict_T *dict_alloc(void) FUNC_ATTR_NONNULL_RET
d->dv_refcount = 0; d->dv_refcount = 0;
d->dv_copyID = 0; d->dv_copyID = 0;
d->internal_refcount = 0; d->internal_refcount = 0;
QUEUE_INIT(&d->watchers);
return d; return d;
} }
@ -6025,6 +6090,14 @@ dict_free (
--todo; --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); hash_clear(&d->dv_hashtab);
xfree(d); xfree(d);
} }
@ -6066,10 +6139,11 @@ static void dictitem_remove(dict_T *dict, dictitem_T *item)
hashitem_T *hi; hashitem_T *hi;
hi = hash_find(&dict->dv_hashtab, item->di_key); hi = hash_find(&dict->dv_hashtab, item->di_key);
if (HASHITEM_EMPTY(hi)) if (HASHITEM_EMPTY(hi)) {
EMSG2(_(e_intern2), "dictitem_remove()"); EMSG2(_(e_intern2), "dictitem_remove()");
else } else {
hash_remove(&dict->dv_hashtab, hi); hash_remove(&dict->dv_hashtab, hi);
}
dictitem_free(item); dictitem_free(item);
} }
@ -6285,6 +6359,12 @@ static ufunc_T *find_ufunc(uint8_t *name)
// dict function, name is already translated // dict function, name is already translated
rv = find_func(n); rv = find_func(n);
} }
if (!rv) {
EMSG2(_("Function %s doesn't exist"), name);
return NULL;
}
return rv; return rv;
} }
@ -7098,6 +7178,8 @@ static struct fst {
{"cursor", 1, 3, f_cursor}, {"cursor", 1, 3, f_cursor},
{"deepcopy", 1, 2, f_deepcopy}, {"deepcopy", 1, 2, f_deepcopy},
{"delete", 1, 1, f_delete}, {"delete", 1, 1, f_delete},
{"dictwatcheradd", 3, 3, f_dictwatcheradd},
{"dictwatcherdel", 3, 3, f_dictwatcherdel},
{"did_filetype", 0, 0, f_did_filetype}, {"did_filetype", 0, 0, f_did_filetype},
{"diff_filler", 1, 1, f_diff_filler}, {"diff_filler", 1, 1, f_diff_filler},
{"diff_hlID", 2, 2, f_diff_hlID}, {"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])); 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 * "did_filetype()" function
*/ */
@ -8972,6 +9158,7 @@ void dict_extend(dict_T *d1, dict_T *d2, char_u *action)
dictitem_T *di1; dictitem_T *di1;
hashitem_T *hi2; hashitem_T *hi2;
int todo; int todo;
bool watched = is_watched(d1);
todo = (int)d2->dv_hashtab.ht_used; todo = (int)d2->dv_hashtab.ht_used;
for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2) { 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) { if (di1 == NULL) {
di1 = dictitem_copy(HI2DI(hi2)); di1 = dictitem_copy(HI2DI(hi2));
if (dict_add(d1, di1) == FAIL) if (dict_add(d1, di1) == FAIL) {
dictitem_free(di1); dictitem_free(di1);
}
if (watched) {
dictwatcher_notify(d1, (char *)di1->di_key, &di1->di_tv, NULL);
}
} else if (*action == 'e') { } else if (*action == 'e') {
EMSG2(_("E737: Key already exists: %s"), hi2->hi_key); EMSG2(_("E737: Key already exists: %s"), hi2->hi_key);
break; break;
} else if (*action == 'f' && HI2DI(hi2) != di1) { } else if (*action == 'f' && HI2DI(hi2) != di1) {
typval_T oldtv;
if (watched) {
copy_tv(&di1->di_tv, &oldtv);
}
clear_tv(&di1->di_tv); clear_tv(&di1->di_tv);
copy_tv(&HI2DI(hi2)->di_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; *rettv = di->di_tv;
init_tv(&di->di_tv); init_tv(&di->di_tv);
dictitem_remove(d, di); 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_tv.v_lock = VAR_FIXED;
dict_var->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; dict_var->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX;
dict_var->di_key[0] = NUL; dict_var->di_key[0] = NUL;
QUEUE_INIT(&dict->watchers);
} }
/* /*
@ -18365,8 +18572,16 @@ set_var (
dictitem_T *v; dictitem_T *v;
char_u *varname; char_u *varname;
hashtab_T *ht; 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) { if (ht == NULL || *varname == NUL) {
EMSG2(_(e_illvar), name); EMSG2(_(e_illvar), name);
return; return;
@ -18423,6 +18638,9 @@ set_var (
return; return;
} }
if (watched) {
copy_tv(&v->di_tv, &oldtv);
}
clear_tv(&v->di_tv); clear_tv(&v->di_tv);
} else { /* add a new variable */ } else { /* add a new variable */
/* Can't add "v:" variable. */ /* Can't add "v:" variable. */
@ -18444,13 +18662,22 @@ set_var (
v->di_flags = 0; 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); copy_tv(tv, &v->di_tv);
else { } else {
v->di_tv = *tv; v->di_tv = *tv;
v->di_tv.v_lock = 0; v->di_tv.v_lock = 0;
init_tv(tv); 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; 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);
}

View File

@ -5,6 +5,7 @@
#include <stddef.h> #include <stddef.h>
#include "nvim/hashtab.h" #include "nvim/hashtab.h"
#include "nvim/lib/queue.h"
typedef int varnumber_T; typedef int varnumber_T;
typedef double float_T; typedef double float_T;
@ -119,6 +120,7 @@ struct dictvar_S {
dict_T *dv_used_prev; /* previous dict in used dicts list */ dict_T *dv_used_prev; /* previous dict in used dicts list */
int internal_refcount; // number of internal references to int internal_refcount; // number of internal references to
// prevent garbage collection // prevent garbage collection
QUEUE watchers; // dictionary key watchers set by user code
}; };
// structure used for explicit stack while garbage collecting hash tables // structure used for explicit stack while garbage collecting hash tables

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