Revert "buffered listener cannot obtain the text of a change"

This reverts commit 9ce6801812.
This commit is contained in:
h-east committed 2026-08-17 17:48:28 +09:00
1 parent 9ce6801812
commit 3cd5d1d1aa
8 files changed
+12 -298

No files matched your search

+6 -41
View File
@@ -390,7 +390,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} [, {options}]])
listener_add({callback} [, {buf} [, {unbuffered}]])
Number add a callback to listen to changes
listener_flush([{buf}]) none invoke listener callbacks
listener_remove({id}) Number remove a listener callback
@@ -6811,7 +6811,7 @@ list2tuple({list}) *list2tuple()*
Return type: tuple<{type}> (depending on the given |List|)
listener_add({callback} [, {buf} [, {options}]]) *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
@@ -6848,8 +6848,6 @@ listener_add({callback} [, {buf} [, {options}]]) *listener_add()*
the change; one if unknown or the whole line
was affected; this is a byte index, first
character has a value of one.
text only present when the "text" option is set,
see below
When lines are inserted (not when a line is split, e.g. by
typing CR in Insert mode) the values are:
lnum line above which the new line is added
@@ -6868,15 +6866,7 @@ listener_add({callback} [, {buf} [, {options}]]) *listener_add()*
added 0
col first column with a change or 1
{options} is a Dictionary with these optional entries:
unbuffered invoke the {callback} for every single change
instead of for a batch of changes
text include the resulting text in each item of
"changes"
For backwards compatibility {options} may also be a |Boolean|,
which is then used for the "unbuffered" entry.
When "unbuffered" is |FALSE| or not provided the {callback} is
When {unbuffered} is |FALSE| or not provided the {callback} is
invoked:
1. Just before the screen is updated.
@@ -6884,7 +6874,6 @@ listener_add({callback} [, {buf} [, {options}]]) *listener_add()*
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.
4. When "text" is set and a lot of text has been recorded.
The entries are in the order the changes were made, thus the
most recent change is at the end.
@@ -6892,10 +6881,10 @@ listener_add({callback} [, {buf} [, {options}]]) *listener_add()*
Because of the third reason for triggering a callback listed
above, the line numbers passed to the callback are not
guaranteed to be valid. In particular, the end value can be
greater than line('$') + 1. If this is a problem then set
"unbuffered".
greater than line('$') + 1. If this is a problem then make
{unbuffered} |TRUE|.
When "unbuffered" is |TRUE| the {callback} is invoked for every
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
@@ -6903,30 +6892,6 @@ listener_add({callback} [, {buf} [, {options}]]) *listener_add()*
invoked, but later changes may make them invalid, thus keeping
a copy for later might not work.
*listener-text*
When "text" is set each item in "changes" gets a "text" entry,
a List with the lines that occupy the changed region right
after that change was made: the lines "lnum" up to but not
including "end" plus "added". This List is empty when the
change only deleted lines. Together with "lnum", "end" and
"added" the item then fully describes the change: replace the
lines "lnum" up to but not including "end" with "text".
The text is copied at the moment the change is recorded, so it
is not affected by later changes. Without it a buffered
{callback} cannot reliably obtain the text of a change: two
changes to the same line do not change the line count and are
reported together, and by then |getbufline()| can only return
the result of the second one. Setting "unbuffered" avoids
that as well, but then the {callback} is invoked for every
single change, which is expensive for a command that changes
many lines.
Setting "text" costs a copy of the changed lines for every
change. To keep the memory used by a long sequence of changes
bounded, the {callback} is also invoked when a lot of text has
been recorded.
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()|.
+4 -77
View File
@@ -207,7 +207,6 @@ clean_listener_list(buf_T *buf, listener_T **list, bool all)
{
list_unref(buf->b_recorded_changes);
buf->b_recorded_changes = NULL;
buf->b_recorded_text_size = 0;
}
}
}
@@ -268,60 +267,6 @@ check_recorded_changes(
}
}
// Amount of recorded text after which the listeners are invoked, to bound the
// memory used by a long sequence of changes.
#define LISTENER_TEXT_MAX (4 * 1024 * 1024)
/*
* Return true when any listener in "list" asked for the resulting text.
*/
static bool
listeners_want_text(listener_T *list)
{
listener_T *lnr;
for (lnr = list; lnr != NULL; lnr = lnr->lr_next)
if (lnr->lr_text)
return true;
return false;
}
/*
* Store in "dict" the text that occupies the changed region right after the
* change: the lines from "lnum" up to but not including "lnume" + "xtra".
* Returns the number of bytes stored.
*/
static size_t
add_change_text(
buf_T *buf,
dict_T *dict,
linenr_T lnum,
linenr_T lnume,
long xtra)
{
list_T *l = list_alloc();
linenr_T below = lnume + xtra; // line below the changed region
size_t size = 0;
if (l == NULL)
return 0;
if (dict_add_list(dict, "text", l) == FAIL)
{
list_free(l);
return 0;
}
if (below > buf->b_ml.ml_line_count + 1)
below = buf->b_ml.ml_line_count + 1;
for (linenr_T lp = lnum < 1 ? 1 : lnum; lp < below; ++lp)
{
if (list_append_string(l, ml_get_buf(buf, lp, FALSE), -1) == FAIL)
break;
size += (size_t)ml_get_buf_len(buf, lp) + 1;
}
return size;
}
/*
* Record a change for listeners added with listener_add().
* Always for the current buffer.
@@ -361,14 +306,8 @@ may_record_change(
dict_add_number(dict, "end", (varnumber_T)lnume);
dict_add_number(dict, "added", (varnumber_T)xtra);
dict_add_number(dict, "col", (varnumber_T)col + 1);
if (listeners_want_text(curbuf->b_listener))
curbuf->b_recorded_text_size +=
add_change_text(curbuf, dict, lnum, lnume, xtra);
list_append_dict(curbuf->b_recorded_changes, dict);
if (curbuf->b_recorded_text_size > LISTENER_TEXT_MAX)
invoke_listeners(curbuf);
}
/*
@@ -380,8 +319,7 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
callback_T callback;
listener_T *lnr;
buf_T *buf = curbuf;
bool unbuffered = false;
bool want_text = false;
int unbuffered = 0;
if (check_secure())
return;
@@ -394,7 +332,7 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
if (in_vim9script() && (
check_for_opt_buffer_arg(argvars, 1) == FAIL
|| check_for_opt_bool_or_dict_arg(argvars, 2) == FAIL))
|| check_for_opt_bool_arg(argvars, 2) == FAIL))
return;
callback = get_callback(&argvars[0]);
@@ -409,15 +347,8 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
free_callback(&callback);
return;
}
if (argvars[2].v_type == VAR_DICT)
{
dict_T *d = argvars[2].vval.v_dict;
unbuffered = dict_get_bool(d, "unbuffered", false);
want_text = dict_get_bool(d, "text", false);
}
else if (argvars[2].v_type != VAR_UNKNOWN)
unbuffered = tv_get_bool(&argvars[2]);
if (argvars[2].v_type != VAR_UNKNOWN)
unbuffered = (int)tv_get_bool(&argvars[2]);
}
lnr = ALLOC_CLEAR_ONE(listener_T);
@@ -446,7 +377,6 @@ f_listener_add(typval_T *argvars, typval_T *rettv)
set_callback(&lnr->lr_callback, &callback);
lnr->lr_text = want_text;
lnr->lr_id = ++next_listener_id;
rettv->vval.v_number = lnr->lr_id;
}
@@ -643,8 +573,6 @@ invoke_sync_listeners(
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);
if (listeners_want_text(buf->b_sync_listener))
(void)add_change_text(buf, dict, start, end, added);
list_append_dict(recorded_changes, dict);
invoke_listener_set(
@@ -688,7 +616,6 @@ invoke_listeners(buf_T *buf)
list_unref(buf->b_recorded_changes);
buf->b_recorded_changes = NULL;
buf->b_recorded_text_size = 0;
}
/*
+2 -17
View File
@@ -412,20 +412,6 @@ arg_bool_or_nr(type_T *type, type_T *decl_type UNUSED, argcontext_T *context)
return FAIL;
}
/*
* Check "type" is a bool or a dict of 'any'.
*/
static int
arg_bool_or_dict_any(
type_T *type,
type_T *decl_type UNUSED,
argcontext_T *context)
{
if (type->tt_type == VAR_DICT || type_any_or_unknown(type))
return OK;
return check_arg_type(&t_bool, type, context);
}
/*
* Check "type" is a list of 'any' or a blob.
*/
@@ -1315,8 +1301,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_or_dict[] = {
arg_any, arg_buffer, arg_bool_or_dict_any};
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};
@@ -2530,7 +2515,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, 3, FEARG_2, arg3_any_buffer_bool_or_dict,
{"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},
-1
View File
@@ -20,7 +20,6 @@ int check_for_float_or_nr_arg(typval_T *args, int idx);
int check_for_bool_arg(typval_T *args, int idx);
int check_for_opt_bool_arg(typval_T *args, int idx);
int check_for_opt_bool_or_number_arg(typval_T *args, int idx);
int check_for_opt_bool_or_dict_arg(typval_T *args, int idx);
int check_for_blob_arg(typval_T *args, int idx);
int check_for_list_arg(typval_T *args, int idx);
int check_for_nonnull_list_arg(typval_T *args, int idx);
-2
View File
@@ -2973,7 +2973,6 @@ struct listener_S
{
listener_T *lr_next;
int lr_id;
bool lr_text; // include the resulting text in each change
callback_T lr_callback;
};
@@ -3634,7 +3633,6 @@ struct file_buffer
listener_T *b_listener; // Listeners accepting buffered reports.
listener_T *b_sync_listener; // Listeners requiring unbuffered reports.
list_T *b_recorded_changes;
size_t b_recorded_text_size; // bytes of text held by the above
#endif
#ifdef FEAT_PROP_POPUP
bool b_has_textprop; // true when text props were added
-147
View File
@@ -801,153 +801,6 @@ func Test_listener_blockwise_paste()
bwipe!
endfunc
func s:StoreChanges(l)
call add(s:changes, deepcopy(a:l))
endfunc
" The "text" option makes each change self-contained.
func Test_listener_text()
new
call setline(1, ['one', 'two', 'three'])
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)},
\ bufnr(), #{text: v:true})
" Two changes to the same line do not change the line count, so they are
" reported together. Each one still carries the text it resulted in.
call setline(1, 'first')
call setline(1, 'second')
call listener_flush()
call assert_equal([[
\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0, 'text': ['first']},
\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0, 'text': ['second']}]],
\ s:changes)
" Inserted lines are reported as the text that was inserted.
let s:changes = []
call append(1, ['a', 'b'])
call listener_flush()
call assert_equal([[
\ {'lnum': 2, 'end': 2, 'col': 1, 'added': 2, 'text': ['a', 'b']}]],
\ s:changes)
" Deleting lines leaves an empty text.
let s:changes = []
2,3del
call listener_flush()
call assert_equal([[
\ {'lnum': 2, 'end': 4, 'col': 1, 'added': -2, 'text': []}]],
\ s:changes)
" Deleting and inserting at the same spot keeps both entries apart.
let s:changes = []
call setline(1, ['one', 'two', 'three'])
call listener_flush()
let s:changes = []
1del
call append(0, 'zero')
call listener_flush()
call assert_equal([
\ [{'lnum': 1, 'end': 2, 'col': 1, 'added': -1, 'text': []}],
\ [{'lnum': 1, 'end': 1, 'col': 1, 'added': 1, 'text': ['zero']}]],
\ s:changes)
call listener_remove(id)
bwipe!
endfunc
" Deleting every line and undoing it are each reported as one change.
func Test_listener_text_whole_buffer()
new
call setline(1, range(1, 3000)->map({_, v -> 'line ' .. v}))
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)},
\ bufnr(), #{text: v:true})
let &undolevels = &undolevels
" Nothing is left, so no text is copied.
%delete _
call listener_flush()
call assert_equal([[
\ {'lnum': 1, 'end': 3001, 'col': 1, 'added': -3000, 'text': []}]],
\ s:changes)
" The undo restores all the lines, so they are all copied.
let s:changes = []
undo
call listener_flush()
call assert_equal(1, len(s:changes))
call assert_equal(1, len(s:changes[0]))
let change = s:changes[0][0]
call assert_equal(1, change.lnum)
call assert_equal(2, change.end)
call assert_equal(2999, change.added)
call assert_equal(3000, len(change.text))
call assert_equal('line 1', change.text[0])
call assert_equal('line 3000', change.text[-1])
call listener_remove(id)
bwipe!
endfunc
" The "text" option also applies to an unbuffered listener.
func Test_listener_text_unbuffered()
new
call setline(1, ['one', 'two', 'three'])
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)},
\ bufnr(), #{unbuffered: v:true, text: v:true})
call setline(2, 'two two')
call assert_equal([[
\ {'lnum': 2, 'end': 3, 'col': 1, 'added': 0, 'text': ['two two']}]],
\ s:changes)
call listener_remove(id)
bwipe!
endfunc
" Without the "text" option there is no "text" entry.
func Test_listener_no_text_by_default()
new
call setline(1, ['one', 'two'])
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)},
\ bufnr(), #{unbuffered: v:true})
call setline(1, 'one one')
call assert_equal([[
\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0}]], s:changes)
call listener_remove(id)
bwipe!
endfunc
" The third argument keeps accepting a Boolean.
func Test_listener_options_argument()
new
call setline(1, ['one', 'two'])
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)}, bufnr(), v:true)
call setline(1, 'one one')
call assert_equal([[
\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0}]], s:changes)
call listener_remove(id)
" An empty Dictionary is the same as not passing the argument.
let s:changes = []
let id = listener_add({b, s, e, a, l -> s:StoreChanges(l)}, bufnr(), {})
call setline(1, 'one two')
call assert_equal([], s:changes)
call listener_flush()
call assert_equal([[
\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0}]], s:changes)
call listener_remove(id)
call assert_fails('call listener_add("Foo", bufnr(), [])', 'E745:')
bwipe!
endfunc
func Test_listener_add_in_sandbox()
call assert_fails(
\ 'sandbox call redraw_listener_add({"on_start": function("tr")})',
-1
View File
@@ -2657,7 +2657,6 @@ enddef
def Test_listener_add()
v9.CheckSourceDefAndScriptFailure(['listener_add("1", true)'], ['E1013: Argument 2: type mismatch, expected string but got bool', 'E1220: String or Number required for argument 2'])
v9.CheckSourceDefAndScriptFailure(['listener_add("1", 1, [1])'], ['E1013: Argument 3: type mismatch, expected bool but got list<number>', 'E1212: Bool required for argument 3'])
enddef
def Test_listener_flush()
-12
View File
@@ -577,18 +577,6 @@ check_for_opt_bool_or_number_arg(typval_T *args, int idx)
return check_for_bool_or_number_arg(args, idx);
}
/*
* Check for an optional bool or dict argument at 'idx'.
* Return FAIL if the type is wrong.
*/
int
check_for_opt_bool_or_dict_arg(typval_T *args, int idx)
{
if (args[idx].v_type == VAR_UNKNOWN || args[idx].v_type == VAR_DICT)
return OK;
return check_for_bool_arg(args, idx);
}
/*
* Give an error and return FAIL unless "args[idx]" is a blob.
*/