-*builtin.txt* For Vim version 9.1. Last change: 2025 Sep 18
+*builtin.txt* For Vim version 9.1. Last change: 2025 Sep 21
VIM REFERENCE MANUAL by Bram Moolenaar
list2blob({list}) Blob turn {list} of numbers into a Blob
list2str({list} [, {utf8}]) String turn {list} of numbers into a String
list2tuple({list}) Tuple turn {list} of items into a tuple
-listener_add({callback} [, {buf}])
+listener_add({callback} [, {buf} [, {unbuffered}]])
Number add a callback to listen to changes
listener_flush([{buf}]) none invoke listener callbacks
listener_remove({id}) none remove a listener callback
Return type: tuple<{type}> (depending on the given |List|)
-listener_add({callback} [, {buf}]) *listener_add()*
+listener_add({callback} [, {buf} [, {unbuffered}]]) *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
buffer is used.
Returns a unique ID that can be passed to |listener_remove()|.
+ If the {buf} already has registered callbacks then the
+ equivalent of >
+ listener_flush({buf})
+< is performed before the new callback is added.
+
The {callback} is invoked with five arguments:
bufnr the buffer that was changed
start first changed line number
added 0
col first column with a change or 1
+ When {unbuffered} is |FALSE| or not provided the {callback} is
+ invoked:
+
+ 1. Just before the screen is updated.
+ 2. When |listener_flush()| is called.
+ 3. When a change is being made that changes the line count in
+ a way that causes a line number in the list of changes to
+ become invalid.
+
The entries are in the order the changes were made, thus the
- most recent change is at the end. The line numbers are valid
- when the callback is invoked, but later changes may make them
- invalid, thus keeping a copy for later might not work.
+ most recent change is at the end.
- The {callback} is invoked just before the screen is updated,
- when |listener_flush()| is called or when a change is being
- made that changes the line count in a way it causes a line
- number in the list of changes to become invalid.
+ Because of the third trigger reason for triggering a callback
+ listed above, the line numbers passed to the callback are not
+ guaranteed to be valid. If this is a problem then make
+ {unbuffered} |TRUE|.
+
+ When {unbuffered} is |TRUE| the {callback} is invoked for every
+ single change. The changes list only holds a single dictionary
+ and the "start", "end" and "added" values in the dictionary are
+ the same as the corresponding callback arguments. The line
+ numbers are valid when the callback is invoked, but later
+ changes may make them invalid, thus keeping a copy for later
+ might not work.
The {callback} is invoked with the text locked, see
|textlock|. If you do need to make changes to the buffer, use
a timer to do this later |timer_start()|.
+ You may not call listener_add() during the {callback}. *E1569*
+
The {callback} is not invoked when the buffer is first loaded.
Use the |BufReadPost| autocmd event to handle the initial text
of a buffer.
E1566 remote.txt /*E1566*
E1567 remote.txt /*E1567*
E1568 options.txt /*E1568*
+E1569 builtin.txt /*E1569*
E157 sign.txt /*E157*
E158 sign.txt /*E158*
E159 sign.txt /*E159*
-*version9.txt* For Vim version 9.1. Last change: 2025 Sep 18
+*version9.txt* For Vim version 9.1. Last change: 2025 Sep 21
VIM REFERENCE MANUAL by Bram Moolenaar
- |matchfuzzy()| and |matchfuzzypos()| use an improved fuzzy matching
algorithm (same as fzy).
- |sha256()| also accepts a |Blob| as argument.
+- |listener_add()| allows to register un-buffered listeners, so that chagnes
+ are handled as soon as they happen.
Others: ~
- the regex engines match correctly case-insensitive multi-byte characters
}
#ifdef FEAT_EVAL
+// Set when listener callbacks are being invoked.
+static int recursive = FALSE;
+
static long next_listener_id = 0;
+// A flag that is set when any buffer listener housekeeping is required.
+// Currently the only condition is when a listener is marked for removal.
+static bool houskeeping_required;
+
+/*
+ * Remove a given listener_T entry from its containing list.
+ */
+ static void
+remove_listener_from_list(
+ listener_T **list,
+ listener_T *lnr,
+ listener_T *prev)
+{
+ if (prev != NULL)
+ prev->lr_next = lnr->lr_next;
+ else
+ *list = lnr->lr_next;
+ free_callback(&lnr->lr_callback);
+ vim_free(lnr);
+}
+
+/*
+ * Clean up a buffer change listener list.
+ *
+ * If "all" is TRUE then all entries are removed. Otherwise only those with an ID
+ * of zero are removed. If "buf" is non-NULL then the buffer's recorded changes
+ * will be discarded in the event that all listeners were removed.
+ *
+ */
+ static void
+clean_listener_list(buf_T *buf, listener_T **list, bool all)
+{
+ listener_T *prev;
+ listener_T *lnr;
+ listener_T *next;
+
+ prev = NULL;
+ for (lnr = *list; lnr != NULL; lnr = next)
+ {
+ next = lnr->lr_next;
+ if (all || lnr->lr_id == 0)
+ remove_listener_from_list(list, lnr, prev);
+ else
+ prev = lnr;
+ }
+
+ // Drop any recorded changes for a buffer with no listeners.
+ if (buf != NULL)
+ {
+ if (*list == NULL && buf->b_recorded_changes != NULL)
+ {
+ list_unref(buf->b_recorded_changes);
+ buf->b_recorded_changes = NULL;
+ }
+ }
+}
+
+/*
+ * Perform houskeeping tasks for buffer change listeners.
+ *
+ * This does nothing unless the "houskeeping_required" flag has been set.
+ */
+ static void
+perform_listener_housekeeping(void)
+{
+ buf_T *buf;
+
+ if (houskeeping_required)
+ {
+ FOR_ALL_BUFFERS(buf)
+ {
+ clean_listener_list(buf, &buf->b_listener, FALSE);
+ clean_listener_list(NULL, &buf->b_sync_listener, FALSE);
+ }
+ houskeeping_required = FALSE;
+ }
+}
+
/*
* Check if the change at "lnum" is above or overlaps with an existing
* change. If above then flush changes and invoke listeners.
linenr_T lnume,
long xtra)
{
+ perform_listener_housekeeping();
if (buf->b_recorded_changes == NULL || xtra == 0)
return;
listitem_T *li;
- linenr_T prev_lnum;
- linenr_T prev_lnume;
+ linenr_T prev_lnum;
+ linenr_T prev_lnume;
FOR_ALL_LIST_ITEMS(buf->b_recorded_changes, li)
{
/*
* Record a change for listeners added with listener_add().
* Always for the current buffer.
+ *
+ * This only deals with listeners that are prepared to accept multiple buffered
+ * changes.
*/
static void
may_record_change(
{
dict_T *dict;
+ perform_listener_housekeeping();
if (curbuf->b_listener == NULL)
return;
callback_T callback;
listener_T *lnr;
buf_T *buf = curbuf;
+ int unbuffered = 0;
+
+ if (recursive)
+ {
+ emsg(_(e_cannot_add_listener_in_listener_callback));
+ return;
+ }
- if (in_vim9script() && check_for_opt_buffer_arg(argvars, 1) == FAIL)
+ if (in_vim9script() && (
+ check_for_opt_buffer_arg(argvars, 1) == FAIL
+ || check_for_opt_bool_arg(argvars, 2) == FAIL))
return;
callback = get_callback(&argvars[0]);
free_callback(&callback);
return;
}
+ if (argvars[2].v_type != VAR_UNKNOWN)
+ unbuffered = (int)tv_get_bool(&argvars[2]);
}
lnr = ALLOC_CLEAR_ONE(listener_T);
free_callback(&callback);
return;
}
- lnr->lr_next = buf->b_listener;
- buf->b_listener = lnr;
+
+ // Perform any pending housekeeping and then make sure any buffered change
+ // reports are flushed so that the new listener does not see out of date
+ // changes.
+ perform_listener_housekeeping();
+ invoke_listeners(buf);
+
+ if (unbuffered)
+ {
+ lnr->lr_next = buf->b_sync_listener;
+ buf->b_sync_listener = lnr;
+ }
+ else
+ {
+ lnr->lr_next = buf->b_listener;
+ buf->b_listener = lnr;
+ }
set_callback(&lnr->lr_callback, &callback);
if (callback.cb_free_name)
{
buf_T *buf = curbuf;
+ if (recursive)
+ return;
+
if (in_vim9script() && check_for_opt_buffer_arg(argvars, 0) == FAIL)
return;
if (buf == NULL)
return;
}
+ perform_listener_housekeeping();
invoke_listeners(buf);
}
-
- static void
-remove_listener(buf_T *buf, listener_T *lnr, listener_T *prev)
+/*
+ * Find the buffer change listener entry for a given unique ID.
+ */
+ static listener_T *
+find_listener(
+ int id,
+ listener_T *list_start,
+ listener_T **prev)
{
- if (prev != NULL)
- prev->lr_next = lnr->lr_next;
- else
- buf->b_listener = lnr->lr_next;
- free_callback(&lnr->lr_callback);
- vim_free(lnr);
+ listener_T *next;
+ listener_T *lnr;
+
+ *prev = NULL;
+ for (lnr = list_start; lnr != NULL; lnr = next)
+ {
+ next = lnr->lr_next;
+ if (lnr->lr_id == id)
+ return lnr;
+ *prev = lnr;
+ }
+ return NULL;
}
/*
* listener_remove() function
+ *
+ * This simply marks the listener_T entry as unused, by setting its ID to zero.
+ * The listener_T entry gets removed later by housekeeping.
*/
void
f_listener_remove(typval_T *argvars, typval_T *rettv)
{
listener_T *lnr;
- listener_T *next;
listener_T *prev;
int id;
buf_T *buf;
id = tv_get_number(argvars);
FOR_ALL_BUFFERS(buf)
{
- prev = NULL;
- for (lnr = buf->b_listener; lnr != NULL; lnr = next)
+ lnr = find_listener(id, buf->b_listener, &prev);
+ if (lnr == NULL)
+ lnr = find_listener(id, buf->b_sync_listener, &prev);
+ if (lnr != NULL)
{
- next = lnr->lr_next;
- if (lnr->lr_id == id)
- {
- if (textlock > 0)
- {
- // in invoke_listeners(), clear ID and delete later
- lnr->lr_id = 0;
- return;
- }
- remove_listener(buf, lnr, prev);
- rettv->vval.v_number = 1;
- return;
- }
- prev = lnr;
+ // Clear the ID to indicate that the listener is unused flag
+ // houskeeping.
+ lnr->lr_id = 0;
+ houskeeping_required = TRUE;
+ rettv->vval.v_number = 1;
+ return;
}
}
}
/*
- * Called before inserting a line above "lnum"/"lnum3" or deleting line "lnum"
+ * Called before inserting a line above "lnum"/"lnume" or deleting line "lnum"
* to "lnume".
*/
void
check_recorded_changes(buf, lnum, lnume, added);
}
+/*
+ * Common processing for invoke_listeners and invoke_sync_listeners.
+ */
+ static void
+invoke_listener_set(
+ buf_T *buf,
+ linenr_T start,
+ linenr_T end,
+ long added,
+ list_T *recorded_changes,
+ listener_T *listeners)
+{
+ int save_updating_screen = updating_screen;
+ listener_T *lnr;
+ typval_T rettv;
+ typval_T argv[6];
+
+ argv[0].v_type = VAR_NUMBER;
+ argv[0].vval.v_number = buf->b_fnum; // a:bufnr
+ argv[1].v_type = VAR_NUMBER;
+ argv[1].vval.v_number = start;
+ argv[2].v_type = VAR_NUMBER;
+ argv[2].vval.v_number = end;
+ argv[3].v_type = VAR_NUMBER;
+ argv[3].vval.v_number = added;
+ argv[4].v_type = VAR_LIST;
+ argv[4].vval.v_list = recorded_changes;
+
+ // Protect against recursive callbacks, lock the buffer against changes and
+ // set the updating_screen flag to prevent channel input processing, which
+ // might also try to update the buffer.
+ recursive = TRUE;
+ ++textlock;
+ updating_screen = TRUE;
+
+ for (lnr = listeners; lnr != NULL; lnr = lnr->lr_next)
+ {
+ call_callback(&lnr->lr_callback, -1, &rettv, 5, argv);
+ clear_tv(&rettv);
+ }
+
+ --textlock;
+ if (save_updating_screen)
+ updating_screen = TRUE;
+ else
+ after_updating_screen(TRUE);
+ recursive = FALSE;
+}
+
+/*
+ * Called when any change occurs: invoke listeners added with the "unbuffered"
+ * parameter set.
+ */
+ static void
+invoke_sync_listeners(
+ buf_T *buf,
+ linenr_T start,
+ colnr_T col,
+ linenr_T end,
+ long added)
+{
+ list_T *recorded_changes;
+ dict_T *dict;
+
+ if (recursive || curbuf->b_sync_listener == NULL)
+ return;
+
+ // Create a single entry list to store the details of the change (including
+ // the column).
+ recorded_changes = list_alloc();
+ if (recorded_changes == NULL) // out of memory
+ return;
+
+ ++recorded_changes->lv_refcount;
+ recorded_changes->lv_lock = VAR_FIXED;
+
+ dict = dict_alloc();
+ if (dict == NULL)
+ return;
+
+ dict_add_number(dict, "lnum", (varnumber_T)start);
+ dict_add_number(dict, "end", (varnumber_T)end);
+ dict_add_number(dict, "added", (varnumber_T)added);
+ dict_add_number(dict, "col", (varnumber_T)col + 1);
+ list_append_dict(recorded_changes, dict);
+
+ invoke_listener_set(
+ buf, start, end, added, recorded_changes, buf->b_sync_listener);
+
+ list_unref(recorded_changes);
+}
+
/*
* Called when a sequence of changes is done: invoke listeners added with
* listener_add().
void
invoke_listeners(buf_T *buf)
{
- listener_T *lnr;
- typval_T rettv;
- typval_T argv[6];
listitem_T *li;
linenr_T start = MAXLNUM;
linenr_T end = 0;
linenr_T added = 0;
- int save_updating_screen = updating_screen;
- static int recursive = FALSE;
- listener_T *next;
- listener_T *prev;
if (buf->b_recorded_changes == NULL // nothing changed
- || buf->b_listener == NULL // no listeners
+ || buf->b_listener == NULL // no listeners
|| recursive) // already busy
return;
- recursive = TRUE;
-
- // Block messages on channels from being handled, so that they don't make
- // text changes here.
- ++updating_screen;
-
- argv[0].v_type = VAR_NUMBER;
- argv[0].vval.v_number = buf->b_fnum; // a:bufnr
FOR_ALL_LIST_ITEMS(buf->b_recorded_changes, li)
{
end = lnum;
added += dict_get_number(li->li_tv.vval.v_dict, "added");
}
- argv[1].v_type = VAR_NUMBER;
- argv[1].vval.v_number = start;
- argv[2].v_type = VAR_NUMBER;
- argv[2].vval.v_number = end;
- argv[3].v_type = VAR_NUMBER;
- argv[3].vval.v_number = added;
- argv[4].v_type = VAR_LIST;
- argv[4].vval.v_list = buf->b_recorded_changes;
- ++textlock;
+ invoke_listener_set(
+ buf, start, end, added, buf->b_recorded_changes, buf->b_listener);
- for (lnr = buf->b_listener; lnr != NULL; lnr = lnr->lr_next)
- {
- call_callback(&lnr->lr_callback, -1, &rettv, 5, argv);
- clear_tv(&rettv);
- }
-
- // If f_listener_remove() was called may have to remove a listener now.
- prev = NULL;
- for (lnr = buf->b_listener; lnr != NULL; lnr = next)
- {
- next = lnr->lr_next;
- if (lnr->lr_id == 0)
- remove_listener(buf, lnr, prev);
- else
- prev = lnr;
- }
-
- --textlock;
list_unref(buf->b_recorded_changes);
buf->b_recorded_changes = NULL;
-
- if (save_updating_screen)
- updating_screen = TRUE;
- else
- after_updating_screen(TRUE);
- recursive = FALSE;
}
/*
void
remove_listeners(buf_T *buf)
{
- listener_T *lnr;
- listener_T *next;
-
- for (lnr = buf->b_listener; lnr != NULL; lnr = next)
- {
- next = lnr->lr_next;
- free_callback(&lnr->lr_callback);
- vim_free(lnr);
- }
- buf->b_listener = NULL;
+ clean_listener_list(buf, &buf->b_listener, TRUE);
+ clean_listener_list(NULL, &buf->b_sync_listener, TRUE);
}
+
#endif
/*
changed();
#ifdef FEAT_EVAL
+ // Immediately send this change to any listeners that require changes no to
+ // be buffered.
+ invoke_sync_listeners(curbuf, lnum, col, lnume, xtra);
+
+ // If there are any listeners accepting buffered changes then add changes
+ // to the current buffer's list, flushing previous changes first if necessary.
may_record_change(lnum, col, lnume, xtra);
#endif
#ifdef FEAT_DIFF
else
{
// Don't create a new entry when the line number is the same
- // as the last one and the column is not too far away. Avoids
+ // as the last one and the column is not too far away. Avoids
// creating many entries for typing "xxxxx".
p = &curbuf->b_changelist[curbuf->b_changelistlen - 1];
if (p->lnum != lnum)
// Check if any w_lines[] entries have become invalid.
// For entries below the change: Correct the lnums for
- // inserted/deleted lines. Makes it possible to stop displaying
+ // inserted/deleted lines. Makes it possible to stop displaying
// after the change.
for (i = 0; i < wp->w_lines_valid; ++i)
if (wp->w_lines[i].wl_valid)
{
if (State & VREPLACE_FLAG)
{
- colnr_T new_vcol = 0; // init for GCC
+ colnr_T new_vcol = 0; // init for GCC
colnr_T vcol;
int old_list;
// If the old line has been allocated the deletion can be done in the
// existing line. Otherwise a new line has to be allocated
// Can't do this when using Netbeans, because we would need to invoke
- // netbeans_removed(), which deallocates the line. Let ml_replace() take
+ // netbeans_removed(), which deallocates the line. Let ml_replace() take
// care of notifying Netbeans.
#ifdef FEAT_NETBEANS_INTG
if (netbeans_active())
// In MODE_VREPLACE state, a NL replaces the rest of the line, and
// starts replacing the next line, so push all of the characters left
- // on the line onto the replace stack. We'll push any other characters
+ // on the line onto the replace stack. We'll push any other characters
// that might be replaced at the start of the next line (due to
// autoindent etc) a bit later.
replace_push(NUL); // Call twice because BS over NL expects it
#endif
EXTERN char e_osc_response_timed_out[]
INIT(= N_("E1568: OSC command response timed out: %.*s"));
+#ifdef FEAT_EVAL
+EXTERN char e_cannot_add_listener_in_listener_callback[]
+ INIT(= N_("E1569: Cannot use listener_add in a listener callback"));
+#endif
static argcheck_T arg1_string_or_list_string[] = {arg_string_or_list_string};
static argcheck_T arg1_string_or_nr[] = {arg_string_or_nr};
static argcheck_T arg1_string_or_blob[] = {arg_string_or_blob};
-static argcheck_T arg2_any_buffer[] = {arg_any, arg_buffer};
static argcheck_T arg2_buffer_any[] = {arg_buffer, arg_any};
static argcheck_T arg2_buffer_bool[] = {arg_buffer, arg_bool};
static argcheck_T arg2_buffer_list_any[] = {arg_buffer, arg_list_any};
static argcheck_T arg2_string_string_or_number[] = {arg_string, arg_string_or_nr};
static argcheck_T arg2_blob_dict[] = {arg_blob, arg_dict_any};
static argcheck_T arg2_list_or_tuple_string[] = {arg_list_or_tuple, arg_string};
+static argcheck_T arg3_any_buffer_bool[] = {arg_any, arg_buffer, arg_bool};
static argcheck_T arg3_any_list_dict[] = {arg_any, arg_list_any, arg_dict_any};
static argcheck_T arg3_buffer_lnum_lnum[] = {arg_buffer, arg_lnum, arg_lnum};
static argcheck_T arg3_buffer_number_number[] = {arg_buffer, arg_number, arg_number};
ret_string, f_list2str},
{"list2tuple", 1, 1, FEARG_1, arg1_list_any,
ret_tuple_any, f_list2tuple},
- {"listener_add", 1, 2, FEARG_2, arg2_any_buffer,
+ {"listener_add", 1, 3, FEARG_2, arg3_any_buffer_bool,
ret_number, f_listener_add},
{"listener_flush", 0, 1, FEARG_1, arg1_buffer,
ret_void, f_listener_flush},
msgstr ""
"Project-Id-Version: Vim\n"
"Report-Msgid-Bugs-To: vim-dev@vim.org\n"
-"POT-Creation-Date: 2025-08-27 19:10+0200\n"
+"POT-Creation-Date: 2025-09-21 18:48+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
msgid "E1568: OSC command response timed out: %.*s"
msgstr ""
+msgid "E1569: Cannot use listener_add in a listener callback"
+msgstr ""
+
#. type of cmdline window or 0
#. result of cmdline window or 0
#. buffer of cmdline window or NULL
dictitem_T b_bufvar; // variable for "b:" Dictionary
dict_T *b_vars; // internal variables, local to buffer
- listener_T *b_listener;
+ listener_T *b_listener; // Listeners accepting buffered reports.
+ listener_T *b_sync_listener; // Listeners requiring unbuffered reports.
list_T *b_recorded_changes;
#endif
#ifdef FEAT_PROP_POPUP
let s:list = a:l
endfunc
+func s:StoreListUnbuffered(s, e, a, l)
+ let s:start = a:s
+ let s:end = a:e
+ let s:added = a:a
+ let s:text = getline(a:s)
+ let s:list2 = a:l
+endfunc
+
func s:AnotherStoreList(l)
let s:list2 = a:l
endfunc
call setline(1, 'asdfasdf')
redraw
call assert_equal([], s:list)
+ bwipe!
+endfunc
- " Trying to change the list fails
+func Test_change_list_is_locked()
+ " Trying to change the list passed to the callback fails
+ new
+ call setline(1, ['one', 'two'])
let id = listener_add({b, s, e, a, l -> s:EvilStoreList(l)})
+
+ let s:list3 = []
+ call setline(1, 'asdfasdf')
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list3)
+
+ eval id->listener_remove()
+ bwipe!
+endfunc
+
+func Test_change_list_is_locked_unbuffered()
+ " Trying to change the list passed to the callback fails (unbuffered mode).
+ new
+ call setline(1, ['one', 'two'])
+ let id = listener_add({b, s, e, a, l -> s:EvilStoreList(l)}, bufnr(), v:true)
+
let s:list3 = []
call setline(1, 'asdfasdf')
redraw
bwipe!
endfunc
+func Test_new_listener_does_not_receive_ood_changes()
+ new
+ call setline(1, ['one', 'two', 'three'])
+ let s:list = []
+ let s:list2 = []
+
+ " Add a listener and make a change.
+ let id = listener_add({b, s, e, a, l -> s:StoreList(s, e, a, l)})
+ call setline(1, 'one one')
+
+ " Add a second listener, it should not see the above change to the buffer,
+ " only the change after it was added.
+ let id = listener_add({b, s, e, a, l -> s:AnotherStoreList(l)})
+ call setline(2, 'two two')
+
+ redraw
+ call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
+
+ call listener_remove(id)
+ bwipe!
+endfunc
+
+func Test_callbacks_do_not_recurse()
+ func DodgyExtendList(b, s, e, a, l)
+ call extend(s:list, a:l)
+ if len(s:list) < 3 " Limit recursion in the face of test failure.
+ call listener_flush()
+ redraw
+ endif
+ endfunc
+
+ new
+ call setline(1, ['one', 'two', 'three'])
+ let s:list = []
+
+ " Add a listener and make a change.
+ let id = listener_add("DodgyExtendList")
+ call setline(1, 'one one')
+
+ " The callback should only be invoked once, i.e. recursion is blocked.
+ redraw
+ call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list)
+
+ call listener_remove(id)
+ bwipe!
+endfunc
+
+func Test_clean_up_after_last_listener_removed()
+ new
+ call setline(1, ['one', 'two', 'three'])
+ let s:list = []
+
+ " Add a listener, make a change, but then remove the listener before the
+ " listener gets invoked.
+ let id = listener_add({b, s, e, a, l -> s:StoreList(s, e, a, l)})
+ call setline(3, 'three three')
+ let ok = listener_remove(id)
+ call assert_equal(1, ok)
+
+ " Further buffer changes should (obviously) have no effect.
+ let s:list = []
+ call setline(2, 'two two')
+ redraw
+ call assert_equal([], s:list)
+
+ " Add a new listener, it should not see the above change to line 3 of the
+ " buffer.
+ let id = listener_add({b, s, e, a, l -> s:StoreList(s, e, a, l)})
+ redraw
+ call assert_equal([], s:list)
+
+ call listener_remove(id)
+ bwipe!
+endfunc
+
+func Test_a_callback_may_not_add_a_listener()
+ func ListenerWotAdds_listener(bufnr, start, end, added, changes)
+ call s:StoreList(a:start, a:end, a:added, a:changes)
+ call assert_fails(
+ \ "call listener_add({b, s, e, a, l -> s:AnotherStoreList(l)})", "E1569:")
+ endfunc
+
+ new
+ call setline(1, ['one', 'two', 'three'])
+ let s:list = []
+
+ " Add a listener, make a change, but then remove the listener before the
+ " listener gets invoked.
+ let id = listener_add("ListenerWotAdds_listener")
+ call setline(3, 'three three')
+ redraw
+ call assert_equal([{'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list)
+
+ let s:list2 = []
+ call setline(2, 'two two')
+ redraw
+ call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
+ call assert_equal([], s:list2)
+
+ call listener_remove(id)
+ bwipe!
+endfunc
+
+func Test_changes_can_be_unbuffered()
+ new
+ call setline(1, ['one', 'two', 'three'])
+ let s:list = []
+ let s:list2 = []
+
+ " Add both a buffered and an unbuffered listener.
+ let id_a = listener_add({b, s, e, a, l -> s:StoreList(s, e, a, l)})
+ let id_b = listener_add(
+ \ {b, s, e, a, l -> s:StoreListUnbuffered(s, e, a, l)},
+ \ bufnr(), v:true)
+
+ " Make a change, which only the second listener should see immediately.
+ call setline(2, 'two two')
+ call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list2)
+ call assert_equal(2, s:start)
+ call assert_equal(3, s:end)
+ call assert_equal(0, s:added)
+ call assert_equal([], s:list)
+
+ " Make another change, which only the second listener should see immediately.
+ call setline(3, 'three three')
+ call assert_equal([{'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list2)
+ call assert_equal(3, s:start)
+ call assert_equal(4, s:end)
+ call assert_equal(0, s:added)
+ call assert_equal([], s:list)
+
+ " Force changes to be flushed. Only the first listener should be invoked,
+ " with both the above changes.
+ let s:list2 = []
+ redraw
+ call assert_equal([
+ \ {'lnum': 2, 'end': 3, 'col': 1, 'added': 0},
+ \ {'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list)
+ call assert_equal([], s:list2)
+
+ call listener_remove(id_a)
+ call listener_remove(id_b)
+ bwipe!
+endfunc
+
func s:StoreListArgs(buf, start, end, added, list)
let s:buf = a:buf
let s:start = a:start
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1782,
/**/
1781,
/**/