# HG changeset patch # User Bram Moolenaar # Date 1557862206 -7200 # Node ID 04c2614af21c5c00598dcebce3809009ee963c01 # Parent 155423e5a8fdb4693a7215deddf4d79511b0d3d5 patch 8.1.1332: cannot flush listeners without redrawing, mix of changes commit https://github.com/vim/vim/commit/fe1ade0a78a70a4c7ddaebb6964497f037f4997a Author: Bram Moolenaar Date: Tue May 14 21:20:36 2019 +0200 patch 8.1.1332: cannot flush listeners without redrawing, mix of changes Problem: Cannot flush change listeners without also redrawing. The line numbers in the list of changes may become invalid. Solution: Add listener_flush(). Invoke listeners before adding a change that makes line numbers invalid. diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2459,6 +2459,7 @@ lispindent({lnum}) Number Lisp indent f list2str({list} [, {utf8}]) String turn numbers in {list} into a String listener_add({callback} [, {buf}]) Number add a callback to listen to changes +listener_flush([{buf}]) none invoke listener callbacks listener_remove({id}) none remove a listener callback localtime() Number current time log({expr}) Float natural logarithm (base e) of {expr} @@ -6322,8 +6323,21 @@ listener_add({callback} [, {buf}]) *li buffer is used. Returns a unique ID that can be passed to |listener_remove()|. - The {callback} is invoked with a list of items that indicate a - change. The list cannot be changed. Each list item is a + The {callback} is invoked with four arguments: + a:bufnr the buffer that was changed + a:start first changed line number + a:end first line number below the change + a:added total number of lines added, negative if lines + were deleted + a:changes a List of items with details about the changes + + Example: > + func Listener(bufnr, start, end, added, changes) + echo 'lines ' .. a:start .. ' until ' .. a:end .. ' changed' + endfunc + call listener_add('Listener', bufnr) + +< The List cannot be changed. Each item in a:changes is a dictionary with these entries: lnum the first line number of the change end the first line below the change @@ -6337,35 +6351,32 @@ listener_add({callback} [, {buf}]) *li lnum line below which the new line is added end equal to "lnum" added number of lines inserted - col one + col 1 When lines are deleted the values are: lnum the first deleted line end the line below the first deleted line, before the deletion was done added negative, number of lines deleted - col one + col 1 When lines are changed: lnum the first changed line end the line below the last changed line - added zero - col first column with a change or one - - The entries are in the order the changes was made, thus the - most recent change is at the end. One has to go through the - list from end to start to compute the line numbers in the - current state of the text. - - When using the same function for multiple buffers, you can - pass the buffer to that function using a |Partial|. - Example: > - func Listener(bufnr, changes) - " ... - endfunc - let bufnr = ... - call listener_add(function('Listener', [bufnr]), bufnr) - -< The {callback} is invoked just before the screen is updated. - To trigger this in a script use the `:redraw` command. + 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. + + 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. + + 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()|. The {callback} is not invoked when the buffer is first loaded. Use the |BufReadPost| autocmd event to handle the initial text @@ -6373,6 +6384,14 @@ listener_add({callback} [, {buf}]) *li The {callback} is also not invoked when the buffer is unloaded, use the |BufUnload| autocmd event for that. +listener_flush([{buf}]) *listener_flush()* + Invoke listener callbacks for buffer {buf}. If there are no + pending changes then no callbacks are invoked. + + {buf} refers to a buffer name or number. For the accepted + values, see |bufname()|. When {buf} is omitted the current + buffer is used. + listener_remove({id}) *listener_remove()* Remove a listener previously added with listener_add(). diff --git a/src/change.c b/src/change.c --- a/src/change.c +++ b/src/change.c @@ -169,6 +169,46 @@ may_record_change( if (curbuf->b_listener == NULL) return; + + // If the new change is going to change the line numbers in already listed + // changes, then flush. + if (recorded_changes != NULL && xtra != 0) + { + listitem_T *li; + linenr_T nr; + + for (li = recorded_changes->lv_first; li != NULL; li = li->li_next) + { + nr = (linenr_T)dict_get_number( + li->li_tv.vval.v_dict, (char_u *)"lnum"); + if (nr >= lnum || nr > lnume) + { + if (li->li_next == NULL && lnum == nr + && col + 1 == (colnr_T)dict_get_number( + li->li_tv.vval.v_dict, (char_u *)"col")) + { + dictitem_T *di; + + // Same start point and nothing is following, entries can + // be merged. + di = dict_find(li->li_tv.vval.v_dict, (char_u *)"end", -1); + nr = tv_get_number(&di->di_tv); + if (lnume > nr) + di->di_tv.vval.v_number = lnume; + di = dict_find(li->li_tv.vval.v_dict, + (char_u *)"added", -1); + di->di_tv.vval.v_number += xtra; + return; + } + + // the current change is going to make the line number in the + // older change invalid, flush now + invoke_listeners(curbuf); + break; + } + } + } + if (recorded_changes == NULL) { recorded_changes = list_alloc(); @@ -231,6 +271,23 @@ f_listener_add(typval_T *argvars, typval } /* + * listener_flush() function + */ + void +f_listener_flush(typval_T *argvars, typval_T *rettv UNUSED) +{ + buf_T *buf = curbuf; + + if (argvars[0].v_type != VAR_UNKNOWN) + { + buf = get_buf_arg(&argvars[0]); + if (buf == NULL) + return; + } + invoke_listeners(buf); +} + +/* * listener_remove() function */ void @@ -264,25 +321,56 @@ f_listener_remove(typval_T *argvars, typ * listener_add(). */ void -invoke_listeners(void) +invoke_listeners(buf_T *buf) { listener_T *lnr; typval_T rettv; int dummy; - typval_T argv[2]; + typval_T argv[6]; + listitem_T *li; + linenr_T start = MAXLNUM; + linenr_T end = 0; + linenr_T added = 0; - if (recorded_changes == NULL) // nothing changed + if (recorded_changes == NULL // nothing changed + || buf->b_listener == NULL) // no listeners return; - argv[0].v_type = VAR_LIST; - argv[0].vval.v_list = recorded_changes; + + argv[0].v_type = VAR_NUMBER; + argv[0].vval.v_number = buf->b_fnum; // a:bufnr + + + for (li = recorded_changes->lv_first; li != NULL; li = li->li_next) + { + varnumber_T lnum; - for (lnr = curbuf->b_listener; lnr != NULL; lnr = lnr->lr_next) + lnum = dict_get_number(li->li_tv.vval.v_dict, (char_u *)"lnum"); + if (start > lnum) + start = lnum; + lnum = dict_get_number(li->li_tv.vval.v_dict, (char_u *)"end"); + if (lnum > end) + end = lnum; + added = dict_get_number(li->li_tv.vval.v_dict, (char_u *)"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 = recorded_changes; + ++textlock; + + for (lnr = buf->b_listener; lnr != NULL; lnr = lnr->lr_next) { call_func(lnr->lr_callback, -1, &rettv, - 1, argv, NULL, 0L, 0L, &dummy, TRUE, lnr->lr_partial, NULL); + 5, argv, NULL, 0L, 0L, &dummy, TRUE, lnr->lr_partial, NULL); clear_tv(&rettv); } + --textlock; list_unref(recorded_changes); recorded_changes = NULL; } diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -768,6 +768,7 @@ static struct fst {"lispindent", 1, 1, f_lispindent}, {"list2str", 1, 2, f_list2str}, {"listener_add", 1, 2, f_listener_add}, + {"listener_flush", 0, 1, f_listener_flush}, {"listener_remove", 1, 1, f_listener_remove}, {"localtime", 0, 0, f_localtime}, #ifdef FEAT_FLOAT diff --git a/src/proto/change.pro b/src/proto/change.pro --- a/src/proto/change.pro +++ b/src/proto/change.pro @@ -3,8 +3,9 @@ void change_warning(int col); void changed(void); void changed_internal(void); void f_listener_add(typval_T *argvars, typval_T *rettv); +void f_listener_flush(typval_T *argvars, typval_T *rettv); void f_listener_remove(typval_T *argvars, typval_T *rettv); -void invoke_listeners(void); +void invoke_listeners(buf_T *buf); void changed_bytes(linenr_T lnum, colnr_T col); void inserted_bytes(linenr_T lnum, colnr_T col, int added); void appended_lines(linenr_T lnum, long count); diff --git a/src/screen.c b/src/screen.c --- a/src/screen.c +++ b/src/screen.c @@ -565,8 +565,13 @@ update_screen(int type_arg) } #ifdef FEAT_EVAL - // Before updating the screen, notify any listeners of changed text. - invoke_listeners(); + { + buf_T *buf; + + // Before updating the screen, notify any listeners of changed text. + FOR_ALL_BUFFERS(buf) + invoke_listeners(buf); + } #endif if (must_redraw) diff --git a/src/testdir/test_listener.vim b/src/testdir/test_listener.vim --- a/src/testdir/test_listener.vim +++ b/src/testdir/test_listener.vim @@ -16,9 +16,10 @@ endfunc func Test_listening() new call setline(1, ['one', 'two']) - let id = listener_add({l -> s:StoreList(l)}) + let s:list = [] + let id = listener_add({b, s, e, a, l -> s:StoreList(l)}) call setline(1, 'one one') - redraw + call listener_flush() call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list) " Undo is also a change @@ -26,12 +27,14 @@ func Test_listening() call append(2, 'two two') undo redraw - call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}, - \ {'lnum': 3, 'end': 4, 'col': 1, 'added': -1}, ], s:list) + " the two changes get merged + call assert_equal([{'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list) 1 - " Two listeners, both get called. - let id2 = listener_add({l -> s:AnotherStoreList(l)}) + " Two listeners, both get called. Also check column. + call setline(1, ['one one', 'two']) + call listener_flush() + let id2 = listener_add({b, s, e, a, l -> s:AnotherStoreList(l)}) let s:list = [] let s:list2 = [] exe "normal $asome\" @@ -39,7 +42,10 @@ func Test_listening() call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], s:list) call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], s:list2) + " removing listener works call listener_remove(id2) + call setline(1, ['one one', 'two']) + call listener_flush() let s:list = [] let s:list2 = [] call setline(3, 'three') @@ -47,12 +53,42 @@ func Test_listening() call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}], s:list) call assert_equal([], s:list2) + " a change above a previous change without a line number change is reported + " together + call setline(1, ['one one', 'two']) + call listener_flush() + call append(2, 'two two') + call setline(1, 'something') + call listener_flush() + call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}, + \ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list) + + " an insert just above a previous change that was the last one gets merged + call setline(1, ['one one', 'two']) + call listener_flush() + call setline(2, 'something') + call append(1, 'two two') + call listener_flush() + call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 1}], s:list) + + " an insert above a previous change causes a flush + call setline(1, ['one one', 'two']) + call listener_flush() + call setline(2, 'something') + call append(0, 'two two') + call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list) + call listener_flush() + call assert_equal([{'lnum': 1, 'end': 1, 'col': 1, 'added': 1}], s:list) + " the "o" command first adds an empty line and then changes it + %del + call setline(1, ['one one', 'two']) + call listener_flush() let s:list = [] exe "normal Gofour\" redraw - call assert_equal([{'lnum': 4, 'end': 4, 'col': 1, 'added': 1}, - \ {'lnum': 4, 'end': 5, 'col': 1, 'added': 0}], s:list) + call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}, + \ {'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list) " Remove last listener let s:list = [] @@ -62,7 +98,7 @@ func Test_listening() call assert_equal([], s:list) " Trying to change the list fails - let id = listener_add({l -> s:EvilStoreList(l)}) + let id = listener_add({b, s, e, a, l -> s:EvilStoreList(l)}) let s:list3 = [] call setline(1, 'asdfasdf') redraw @@ -72,9 +108,64 @@ func Test_listening() bwipe! endfunc -func s:StoreBufList(buf, l) +func s:StoreListArgs(buf, start, end, added, list) + let s:buf = a:buf + let s:start = a:start + let s:end = a:end + let s:added = a:added + let s:list = a:list +endfunc + +func Test_listener_args() + new + call setline(1, ['one', 'two']) + let s:list = [] + let id = listener_add('s:StoreListArgs') + + " just one change + call setline(1, 'one one') + call listener_flush() + call assert_equal(bufnr(''), s:buf) + call assert_equal(1, s:start) + call assert_equal(2, s:end) + call assert_equal(0, s:added) + call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list) + + " two disconnected changes + call setline(1, ['one', 'two', 'three', 'four']) + call listener_flush() + call setline(1, 'one one') + call setline(3, 'three three') + call listener_flush() + call assert_equal(bufnr(''), s:buf) + call assert_equal(1, s:start) + call assert_equal(4, s:end) + call assert_equal(0, s:added) + call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}, + \ {'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list) + + " add and remove lines + call setline(1, ['one', 'two', 'three', 'four', 'five', 'six']) + call listener_flush() + call append(2, 'two two') + 4del + call append(5, 'five five') + call listener_flush() + call assert_equal(bufnr(''), s:buf) + call assert_equal(3, s:start) + call assert_equal(6, s:end) + call assert_equal(1, s:added) + call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}, + \ {'lnum': 4, 'end': 5, 'col': 1, 'added': -1}, + \ {'lnum': 6, 'end': 6, 'col': 1, 'added': 1}], s:list) + + call listener_remove(id) + bwipe! +endfunc + +func s:StoreBufList(buf, start, end, added, list) let s:bufnr = a:buf - let s:list = a:l + let s:list = a:list endfunc func Test_listening_other_buf() @@ -82,7 +173,7 @@ func Test_listening_other_buf() call setline(1, ['one', 'two']) let bufnr = bufnr('') normal ww - let id = listener_add(function('s:StoreBufList', [bufnr]), bufnr) + let id = listener_add(function('s:StoreBufList'), bufnr) let s:list = [] call setbufline(bufnr, 1, 'hello') redraw diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -768,6 +768,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1332, +/**/ 1331, /**/ 1330,