patch 8.1.1321: no docs or tests for listener functions
Problem: No docs or tests for listener functions.
Solution: Add help and tests for listener_add() and listener_remove().
Invoke the callbacks before redrawing.
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index a8109d9..0b71c44 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2457,6 +2457,9 @@
line2byte({lnum}) Number byte count of line {lnum}
lispindent({lnum}) Number Lisp indent for line {lnum}
list2str({list} [, {utf8}]) String turn numbers in {list} into a String
+listener_add({callback} [, {buf}])
+ Number add a callback to listen to changes
+listener_remove({id}) none remove a listener callback
localtime() Number current time
log({expr}) Float natural logarithm (base e) of {expr}
log10({expr}) Float logarithm of Float {expr} to base 10
@@ -6311,6 +6314,53 @@
With utf-8 composing characters work as expected: >
list2str([97, 769]) returns "á"
<
+listener_add({callback} [, {buf}]) *listener_add()*
+ Add a callback function that will be invoked when changes have
+ been made to buffer {buf}.
+ {buf} refers to a buffer name or number. For the accepted
+ values, see |bufname()|. When {buf} is omitted the current
+ buffer is used.
+ Returns a unique ID that can be passed to |listener_remove()|.
+
+ The {callback} is invoked with a list of items that indicate a
+ change. Each list item is a dictionary with these entries:
+ lnum the first line number of the change
+ end the first line below the change
+ added number of lines added; negative if lines were
+ deleted
+ col first column in "lnum" that was affected by
+ the change; one if unknown or the whole line
+ was affected; this is a byte index, first
+ character has a value of one.
+ When lines are inserted the values are:
+ lnum line below which the new line is added
+ end equal to "lnum"
+ added number of lines inserted
+ col one
+ When lines are deleted the values are:
+ lnum the first deleted line
+ end the line below the first deleted line, before
+ the deletion was done
+ added negative, number of lines deleted
+ col one
+ When lines are changed:
+ lnum the first changed line
+ end the line below the last changed line
+ added zero
+ col first column with a change or one
+
+ The {callback} is invoked just before the screen is updated.
+ To trigger this in a script use the `:redraw` command.
+
+ The {callback} is not invoked when the buffer is first loaded.
+ Use the |BufReadPost| autocmd event to handle the initial text
+ of a buffer.
+ The {callback} is also not invoked when the buffer is
+ unloaded, use the |BufUnload| autocmd event for that.
+
+listener_remove({id}) *listener_remove()*
+ Remove a listener previously added with listener_add().
+
localtime() *localtime()*
Return the current time, measured as seconds since 1st Jan
1970. See also |strftime()| and |getftime()|.
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index c5317e2..866664c 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -812,6 +812,8 @@
setbufline() replace a line in the specified buffer
appendbufline() append a list of lines in the specified buffer
deletebufline() delete lines from a specified buffer
+ listener_add() add a callback to listen to changes
+ listener_remove() remove a listener callback
win_findbuf() find windows containing a buffer
win_getid() get window ID of a window
win_gotoid() go to window with ID
diff --git a/src/change.c b/src/change.c
index 06d20f4..27ea9ac 100644
--- a/src/change.c
+++ b/src/change.c
@@ -184,7 +184,7 @@
dict_add_number(dict, "lnum", (varnumber_T)lnum);
dict_add_number(dict, "end", (varnumber_T)lnume);
dict_add_number(dict, "added", (varnumber_T)xtra);
- dict_add_number(dict, "col", (varnumber_T)col);
+ dict_add_number(dict, "col", (varnumber_T)col + 1);
list_append_dict(recorded_changes, dict);
}
@@ -198,19 +198,27 @@
char_u *callback;
partial_T *partial;
listener_T *lnr;
+ buf_T *buf = curbuf;
callback = get_callback(&argvars[0], &partial);
if (callback == NULL)
return;
+ if (argvars[1].v_type != VAR_UNKNOWN)
+ {
+ buf = get_buf_arg(&argvars[1]);
+ if (buf == NULL)
+ return;
+ }
+
lnr = (listener_T *)alloc_clear((sizeof(listener_T)));
if (lnr == NULL)
{
free_callback(callback, partial);
return;
}
- lnr->lr_next = curbuf->b_listener;
- curbuf->b_listener = lnr;
+ lnr->lr_next = buf->b_listener;
+ buf->b_listener = lnr;
if (partial == NULL)
lnr->lr_callback = vim_strsave(callback);
@@ -232,22 +240,23 @@
listener_T *next;
listener_T *prev = NULL;
int id = tv_get_number(argvars);
- buf_T *buf = curbuf;
+ buf_T *buf;
- for (lnr = buf->b_listener; lnr != NULL; lnr = next)
- {
- next = lnr->lr_next;
- if (lnr->lr_id == id)
+ for (buf = firstbuf; buf != NULL; buf = buf->b_next)
+ for (lnr = buf->b_listener; lnr != NULL; lnr = next)
{
- if (prev != NULL)
- prev->lr_next = lnr->lr_next;
- else
- buf->b_listener = lnr->lr_next;
- free_callback(lnr->lr_callback, lnr->lr_partial);
- vim_free(lnr);
+ next = lnr->lr_next;
+ if (lnr->lr_id == id)
+ {
+ if (prev != NULL)
+ prev->lr_next = lnr->lr_next;
+ else
+ buf->b_listener = lnr->lr_next;
+ free_callback(lnr->lr_callback, lnr->lr_partial);
+ vim_free(lnr);
+ }
+ prev = lnr;
}
- prev = lnr;
- }
}
/*
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 02ca1ae..b67aeb8 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2009,12 +2009,11 @@
return buf;
}
-#ifdef FEAT_SIGNS
/*
* Get the buffer from "arg" and give an error and return NULL if it is not
* valid.
*/
- static buf_T *
+ buf_T *
get_buf_arg(typval_T *arg)
{
buf_T *buf;
@@ -2026,7 +2025,6 @@
semsg(_("E158: Invalid buffer name: %s"), tv_get_string(arg));
return buf;
}
-#endif
/*
* "bufname(expr)" function
diff --git a/src/proto/evalfunc.pro b/src/proto/evalfunc.pro
index c0ada9d..5a53136 100644
--- a/src/proto/evalfunc.pro
+++ b/src/proto/evalfunc.pro
@@ -5,6 +5,7 @@
int call_internal_func(char_u *name, int argcount, typval_T *argvars, typval_T *rettv);
buf_T *buflist_find_by_name(char_u *name, int curtab_only);
buf_T *tv_get_buf(typval_T *tv, int curtab_only);
+buf_T *get_buf_arg(typval_T *arg);
void execute_redir_str(char_u *value, int value_len);
void mzscheme_call_vim(char_u *name, typval_T *args, typval_T *rettv);
float_T vim_round(float_T f);
diff --git a/src/screen.c b/src/screen.c
index 3ebc244..5cdbd2c 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -564,6 +564,11 @@
type = 0;
}
+#ifdef FEAT_EVAL
+ // Before updating the screen, notify any listeners of changed text.
+ invoke_listeners();
+#endif
+
if (must_redraw)
{
if (type < must_redraw) /* use maximal type */
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 1f50bd8..7dc7e36 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -168,6 +168,7 @@
test_lispwords \
test_listchars \
test_listdict \
+ test_listener \
test_listlbr \
test_listlbr_utf8 \
test_lua \
@@ -359,6 +360,7 @@
test_lineending.res \
test_listchars.res \
test_listdict.res \
+ test_listener.res \
test_listlbr.res \
test_lua.res \
test_makeencoding.res \
diff --git a/src/testdir/test_listener.vim b/src/testdir/test_listener.vim
new file mode 100644
index 0000000..87183e5
--- /dev/null
+++ b/src/testdir/test_listener.vim
@@ -0,0 +1,77 @@
+" tests for listener_add() and listener_remove()
+
+func StoreList(l)
+ let g:list = a:l
+endfunc
+
+func AnotherStoreList(l)
+ let g:list2 = a:l
+endfunc
+
+func EvilStoreList(l)
+ let g:list3 = a:l
+ call assert_fails("call add(a:l, 'myitem')", "E742:")
+endfunc
+
+func Test_listening()
+ new
+ call setline(1, ['one', 'two'])
+ let id = listener_add({l -> StoreList(l)})
+ call setline(1, 'one one')
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], g:list)
+
+ " Two listeners, both get called.
+ let id2 = listener_add({l -> AnotherStoreList(l)})
+ let g:list = []
+ let g:list2 = []
+ exe "normal $asome\<Esc>"
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], g:list)
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], g:list2)
+
+ call listener_remove(id2)
+ let g:list = []
+ let g:list2 = []
+ call setline(3, 'three')
+ redraw
+ call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}], g:list)
+ call assert_equal([], g:list2)
+
+ " the "o" command first adds an empty line and then changes it
+ let g:list = []
+ exe "normal Gofour\<Esc>"
+ redraw
+ call assert_equal([{'lnum': 4, 'end': 4, 'col': 1, 'added': 1},
+ \ {'lnum': 4, 'end': 5, 'col': 1, 'added': 0}], g:list)
+
+ let g:list = []
+ call listener_remove(id)
+ call setline(1, 'asdfasdf')
+ redraw
+ call assert_equal([], g:list)
+
+ " Trying to change the list fails
+ let id = listener_add({l -> EvilStoreList(l)})
+ let g:list3 = []
+ call setline(1, 'asdfasdf')
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], g:list3)
+
+ bwipe!
+endfunc
+
+func Test_listening_other_buf()
+ new
+ call setline(1, ['one', 'two'])
+ let bufnr = bufnr('')
+ normal ww
+ let id = listener_add({l -> StoreList(l)}, bufnr)
+ let g:list = []
+ call setbufline(bufnr, 1, 'hello')
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], g:list)
+
+ exe "buf " .. bufnr
+ bwipe!
+endfunc
diff --git a/src/version.c b/src/version.c
index 1829fa3..85704a6 100644
--- a/src/version.c
+++ b/src/version.c
@@ -768,6 +768,8 @@
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
+ 1321,
+/**/
1320,
/**/
1319,