0
0
mirror of https://github.com/vim/vim.git synced 2025-10-18 07:54:29 -04:00

patch 9.1.1782: buffer-listener callbacks may not match buffer content

Problem:  buffer-listener callbacks may not match buffer content, since
          they are buffered until the screen is updated.
Solution: Allow to handle buffer-callbacks un-buffered, meaning to
          handle those changes as soon as they happen (Paul Ollis).

fixes: #18183
closes: #18295

Signed-off-by: Paul Ollis <paul@cleversheep.org>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Paul Ollis
2025-09-21 18:53:40 +00:00
committed by Christian Brabandt
parent 3a6cf6d53b
commit e87d17ecfb
10 changed files with 490 additions and 113 deletions

View File

@@ -1,4 +1,4 @@
*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
@@ -369,7 +369,7 @@ lispindent({lnum}) Number Lisp indent for line {lnum}
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
@@ -6715,7 +6715,7 @@ list2tuple({list}) *list2tuple()*
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
@@ -6723,6 +6723,11 @@ listener_add({callback} [, {buf}]) *listener_add()*
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
@@ -6765,20 +6770,37 @@ listener_add({callback} [, {buf}]) *listener_add()*
added 0
col first column with a change or 1
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.
When {unbuffered} is |FALSE| or not provided the {callback} is
invoked:
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.
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.
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.

View File

@@ -4741,6 +4741,7 @@ E1565 remote.txt /*E1565*
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*

View File

@@ -1,4 +1,4 @@
*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
@@ -41736,6 +41736,8 @@ Functions: ~
- |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

View File

@@ -149,8 +149,89 @@ changed_internal(void)
}
#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.
@@ -162,12 +243,13 @@ check_recorded_changes(
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)
{
@@ -188,6 +270,9 @@ check_recorded_changes(
/*
* 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(
@@ -198,6 +283,7 @@ may_record_change(
{
dict_T *dict;
perform_listener_housekeeping();
if (curbuf->b_listener == NULL)
return;
@@ -234,8 +320,17 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
callback_T callback;
listener_T *lnr;
buf_T *buf = curbuf;
int unbuffered = 0;
if (in_vim9script() && check_for_opt_buffer_arg(argvars, 1) == FAIL)
if (recursive)
{
emsg(_(e_cannot_add_listener_in_listener_callback));
return;
}
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]);
@@ -250,6 +345,8 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
free_callback(&callback);
return;
}
if (argvars[2].v_type != VAR_UNKNOWN)
unbuffered = (int)tv_get_bool(&argvars[2]);
}
lnr = ALLOC_CLEAR_ONE(listener_T);
@@ -258,8 +355,23 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
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)
@@ -277,6 +389,9 @@ f_listener_flush(typval_T *argvars, typval_T *rettv UNUSED)
{
buf_T *buf = curbuf;
if (recursive)
return;
if (in_vim9script() && check_for_opt_buffer_arg(argvars, 0) == FAIL)
return;
@@ -286,29 +401,43 @@ f_listener_flush(typval_T *argvars, typval_T *rettv UNUSED)
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;
@@ -319,29 +448,23 @@ f_listener_remove(typval_T *argvars, typval_T *rettv)
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
@@ -350,6 +473,98 @@ may_invoke_listeners(buf_T *buf, linenr_T lnum, linenr_T lnume, int added)
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().
@@ -357,30 +572,15 @@ may_invoke_listeners(buf_T *buf, linenr_T lnum, linenr_T lnume, int added)
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)
{
@@ -394,43 +594,12 @@ invoke_listeners(buf_T *buf)
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;
}
/*
@@ -439,17 +608,10 @@ invoke_listeners(buf_T *buf)
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
/*
@@ -475,6 +637,12 @@ changed_common(
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
@@ -500,7 +668,7 @@ changed_common(
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)
@@ -626,7 +794,7 @@ changed_common(
// 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)
@@ -1073,7 +1241,7 @@ ins_char_bytes(char_u *buf, int charlen)
{
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;
@@ -1342,7 +1510,7 @@ del_bytes(
// 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())
@@ -1470,7 +1638,7 @@ open_line(
// 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

View File

@@ -3795,3 +3795,7 @@ EXTERN char e_socket_server_unavailable[]
#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

View File

@@ -1258,7 +1258,6 @@ static argcheck_T arg1_string_or_list_any[] = {arg_string_or_list_any};
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};
@@ -1301,6 +1300,7 @@ static argcheck_T arg2_string_or_list_number[] = {arg_string_or_list_any, arg_nu
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};
@@ -2507,7 +2507,7 @@ static const funcentry_T global_functions[] =
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},

5
src/po/vim.pot generated
View File

@@ -8,7 +8,7 @@ msgid ""
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"
@@ -8836,6 +8836,9 @@ msgstr ""
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

View File

@@ -3497,7 +3497,8 @@ struct file_buffer
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

View File

@@ -8,6 +8,14 @@ func s:StoreList(s, e, a, l)
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
@@ -131,9 +139,15 @@ func Test_listening()
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
@@ -143,6 +157,166 @@ func Test_listening()
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
call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list3)
eval id->listener_remove()
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

View File

@@ -724,6 +724,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1782,
/**/
1781,
/**/