diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 74cb9666ff..2e5edbbdc9 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -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. diff --git a/runtime/doc/tags b/runtime/doc/tags index 827ce4f7a0..b0662994ec 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -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* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index 1433cfb45b..78c2702eab 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -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 diff --git a/src/change.c b/src/change.c index 438bbda18b..220eb8e7b5 100644 --- a/src/change.c +++ b/src/change.c @@ -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 diff --git a/src/errors.h b/src/errors.h index ebe8ed4312..91b1c944f3 100644 --- a/src/errors.h +++ b/src/errors.h @@ -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 diff --git a/src/evalfunc.c b/src/evalfunc.c index 34e3291abb..da53777999 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -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}, diff --git a/src/po/vim.pot b/src/po/vim.pot index cb7214ad4a..d5cb0795da 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -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 \n" "Language-Team: LANGUAGE \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 diff --git a/src/structs.h b/src/structs.h index 3efcabda75..e28c38340a 100644 --- a/src/structs.h +++ b/src/structs.h @@ -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 diff --git a/src/testdir/test_listener.vim b/src/testdir/test_listener.vim index d30add0cb2..88e4a3f785 100644 --- a/src/testdir/test_listener.vim +++ b/src/testdir/test_listener.vim @@ -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 diff --git a/src/version.c b/src/version.c index a27fe7caef..74dcf0e4fc 100644 --- a/src/version.c +++ b/src/version.c @@ -724,6 +724,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1782, /**/ 1781, /**/