patch 9.1.1976: Cannot define callbacks for redraw events
Commit:
https://github.com/vim/vim/commit/4438b8e071ac4702e6cfd68dcc39149e5e95d298
Author: Foxe Chen <[email protected]>
Date: Sat Dec 13 18:14:59 2025 +0100
patch 9.1.1976: Cannot define callbacks for redraw events
Problem: When using listeners, there is no way to run callbacks at
specific points in the redraw cycle.
Solution: Add redraw_listener_add() and redraw_listener_remove() and
allow specifying callbacks for redraw start and end
(Foxe Chen).
closes: #18902
Signed-off-by: Foxe Chen <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 5b143cda8..563cb8791 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 Dec 11
+*builtin.txt* For Vim version 9.1. Last change: 2025 Dec 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -493,6 +493,8 @@ readdirex({dir} [, {expr} [, {dict}]])
List file info in {dir} selected by {expr}
readfile({fname} [, {type} [, {max}]])
List get list of lines from file {fname}
+redraw_listener_add({opts}) Number add callbacks to listen for redraws
+redraw_listener_remove({id}) none remove a redraw listener
reduce({object}, {func} [, {initial}])
any reduce {object} using {func}
reg_executing() String get the executing register name
@@ -8785,6 +8787,48 @@ readfile({fname} [, {type} [, {max}]])
*readfile()*
Return type: list<string> or list<any>
+redraw_listener_add({opts}) *redraw_listener_add()*
+ Add a listener that holds callback functions that will be
+ called at specific times in the redraw cycle. {opts} is a
+ dictionary that contain the callback functions to be defined.
+ At least one callback must be specified. *E1571*
+ Returns a unique ID that can be passed to
+ |redraw_listener_remove()|.
+
+ {opts} may have the following entries:
+
+ on_start Called first on each screen redraw. Takes no
+ arguments and returns nothing.
+ on_end Called at the end of each screen redraw.
+ Takes no arguments and returns nothing.
+
+ A good use case for this function is with the |listener_add()|
+ callback with unbuffered set to TRUE. This allows you to
+ modify the state on buffer changes, and finally render that
+ state just before the next redraw, only if it has changed.
+ Attempting to render or redraw for every single buffer change
+ would be very inefficient.
+
+ You may not call redraw_listener_add() during any of the
+ callbacks defined in {opts}. *E1570*
+
+ Can also be used as a |method|: >
+ GetOpts()->redraw_listener_add()
+<
+ Return type: |Number|
+
+
+redraw_listener_remove({id})
*redraw_listener_remove()*
+ Remove a redraw listener previously added with
+ |redraw_listener_add()|. Returns FALSE when {id} could not be
+ found, TRUE when {id} was removed.
+
+ Can also be used as a |method|: >
+ GetRedrawListenerId()->redraw_listener_remove()
+<
+ Return type: |Number|
+
+
reduce({object}, {func} [, {initial}]) *reduce()* *E998*
{func} is called for every item in {object}, which can be a
|String|, |List|, |Tuple| or a |Blob|. {func} is called with
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 79c3a6fe0..22b470236 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -4755,6 +4755,8 @@ E1567 remote.txt /*E1567*
E1568 options.txt /*E1568*
E1569 builtin.txt /*E1569*
E157 sign.txt /*E157*
+E1570 builtin.txt /*E1570*
+E1571 builtin.txt /*E1571*
E158 sign.txt /*E158*
E159 sign.txt /*E159*
E16 cmdline.txt /*E16*
@@ -10028,6 +10030,8 @@ recovery recover.txt /*recovery*
recursive_mapping map.txt /*recursive_mapping*
redo undo.txt /*redo*
redo-register undo.txt /*redo-register*
+redraw_listener_add() builtin.txt /*redraw_listener_add()*
+redraw_listener_remove() builtin.txt /*redraw_listener_remove()*
reduce() builtin.txt /*reduce()*
ref intro.txt /*ref*
reference intro.txt /*reference*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 5fe5e575c..869e30ee9 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1,4 +1,4 @@
-*usr_41.txt* For Vim version 9.1. Last change: 2025 Nov 27
+*usr_41.txt* For Vim version 9.1. Last change: 2025 Dec 13
VIM USER MANUAL by Bram Moolenaar
@@ -1473,6 +1473,9 @@ Various:
*various-functions*
debugbreak() interrupt a program being debugged
+ redraw_listener_add() add callbacks to listen for redraws
+ redraw_listener_remove() remove a redraw listener
+
==============================================================================
*41.7* Defining a function
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index ff6de1dc5..f897b7d19 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 Dec 11
+*version9.txt* For Vim version 9.1. Last change: 2025 Dec 13
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -41750,6 +41750,8 @@ Functions: ~
- |sha256()| also accepts a |Blob| as argument.
- |listener_add()| allows to register un-buffered listeners, so that changes
are handled as soon as they happen.
+- |redraw_listener_add()| and |redraw_listener_remove()| add/remove callbacks
+ for redrawing events.
Plugins~
- |zip| plugin works with PowerShell Core.
@@ -41826,6 +41828,8 @@ Functions: ~
|popup_setbuf()| switch to a different buffer in a popup
|preinserted()| whether preinserted text has been inserted
during
completion (see 'completeopt')
+|redraw_listener_add()| add callbacks to listen for redraws
+|redraw_listener_remove()| remove a redraw listener
|str2blob()| convert a List of strings into a blob
|test_null_tuple()| return a null tuple
|tuple2list()| turn a Tuple of items into a List
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
index 17161fad4..27d15872c 100644
--- a/runtime/syntax/vim.vim
+++ b/runtime/syntax/vim.vim
@@ -2,7 +2,7 @@
" Language: Vim script
" Maintainer: Hirohito Higashi <h.east.727 ATMARK gmail.com>
" Doug Kearns <[email protected]>
-" Last Change: 2025 Dec 11
+" Last Change: 2025 Dec 13
" Former Maintainer: Charles E. Campbell
" DO NOT CHANGE DIRECTLY.
@@ -157,11 +157,11 @@ syn keyword vimFuncName contained abs acos add and append
appendbufline argc arg
syn keyword vimFuncName contained char2nr charclass charcol charidx chdir
cindent clearmatches cmdcomplete_info col complete complete_add complete_check
complete_info confirm copy cos cosh count cscope_connection cursor debugbreak
deepcopy delete deletebufline did_filetype diff diff_filler diff_hlID
digraph_get digraph_getlist digraph_set digraph_setlist echoraw empty environ
err_teapot escape eval eventhandler executable execute exepath exists
exists_compiled exp expand expandcmd extend extendnew feedkeys filecopy
filereadable filewritable filter finddir findfile flatten flattennew float2nr
floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext
foldtextresult foreach foreground fullcommand funcref function garbagecollect
get getbufinfo
syn keyword vimFuncName contained getbufline getbufoneline getbufvar
getcellpixels getcellwidths getchangelist getchar getcharmod getcharpos
getcharsearch getcharstr getcmdcomplpat getcmdcompltype getcmdline getcmdpos
getcmdprompt getcmdscreenpos getcmdtype getcmdwintype getcompletion
getcompletiontype getcurpos getcursorcharpos getcwd getenv getfontname getfperm
getfsize getftime getftype getimstatus getjumplist getline getloclist
getmarklist getmatches getmousepos getmouseshape getpid getpos getqflist getreg
getreginfo getregion getregionpos getregtype getscriptinfo getstacktrace
gettabinfo gettabvar gettabwinvar gettagstack gettext getwininfo getwinpos
getwinposx getwinposy getwinvar glob glob2regpat globpath has has_key
haslocaldir hasmapto histadd histdel
syn keyword vimFuncName contained histget histnr hlID hlexists hlget hlset
hostname iconv id indent index indexof input inputdialog inputlist inputrestore
inputsave inputsecret insert instanceof interrupt invert isabsolutepath
isdirectory isinf islocked isnan items job_getchannel job_info job_setoptions
job_start job_status job_stop join js_decode js_encode json_decode json_encode
keys keytrans len libcall libcallnr line line2byte lispindent list2blob
list2str list2tuple listener_add listener_flush listener_remove localtime log
log10 luaeval map maparg mapcheck maplist mapnew mapset match matchadd
matchaddpos matcharg matchbufline matchdelete matchend matchfuzzy matchfuzzypos
matchlist matchstr matchstrlist matchstrpos max menu_info min mkdir mode mzeval
nextnonblank
-syn keyword vimFuncName contained ngettext nr2char or pathshorten perleval
popup_atcursor popup_beval popup_clear popup_close popup_create popup_dialog
popup_filter_menu popup_filter_yesno popup_findecho popup_findinfo
popup_findpreview popup_getoptions popup_getpos popup_hide popup_list
popup_locate popup_menu popup_move popup_notification popup_setbuf
popup_setoptions popup_settext popup_show pow preinserted prevnonblank printf
prompt_getprompt prompt_setcallback prompt_setinterrupt prompt_setprompt
prop_add prop_add_list prop_clear prop_find prop_list prop_remove prop_type_add
prop_type_change prop_type_delete prop_type_get prop_type_list pum_getpos
pumvisible py3eval pyeval pyxeval rand range readblob readdir readdirex
readfile reduce reg_executing reg_recording
-syn keyword vimFuncName contained reltime reltimefloat reltimestr remote_expr
remote_foreground remote_peek remote_read remote_send remote_startserver remove
rename repeat resolve reverse round rubyeval screenattr screenchar screenchars
screencol screenpos screenrow screenstring search searchcount searchdecl
searchpair searchpairpos searchpos server2client serverlist setbufline
setbufvar setcellwidths setcharpos setcharsearch setcmdline setcmdpos
setcursorcharpos setenv setfperm setline setloclist setmatches setpos setqflist
setreg settabvar settabwinvar settagstack setwinvar sha256 shellescape
shiftwidth sign_define sign_getdefined sign_getplaced sign_jump sign_place
sign_placelist sign_undefine sign_unplace sign_unplacelist simplify sin sinh
slice sort sound_clear
-syn keyword vimFuncName contained sound_playevent sound_playfile sound_stop
soundfold spellbadword spellsuggest split sqrt srand state str2blob str2float
str2list str2nr strcharlen strcharpart strchars strdisplaywidth strftime
strgetchar stridx string strlen strpart strptime strridx strtrans strutf16len
strwidth submatch substitute swapfilelist swapinfo swapname synID synIDattr
synIDtrans synconcealed synstack system systemlist tabpagebuflist tabpagenr
tabpagewinnr tagfiles taglist tan tanh tempname term_dumpdiff term_dumpload
term_dumpwrite term_getaltscreen term_getansicolors term_getattr term_getcursor
term_getjob term_getline term_getscrolled term_getsize term_getstatus
term_gettitle term_gettty term_list term_scrape term_sendkeys
term_setansicolors term_setapi
-syn keyword vimFuncName contained term_setkill term_setrestore term_setsize
term_start term_wait terminalprops test_alloc_fail test_autochdir
test_feedinput test_garbagecollect_now test_garbagecollect_soon test_getvalue
test_gui_event test_ignore_error test_mswin_event test_null_blob
test_null_channel test_null_dict test_null_function test_null_job
test_null_list test_null_partial test_null_string test_null_tuple
test_option_not_set test_override test_refcount test_setmouse test_settime
test_srand_seed test_unknown test_void timer_info timer_pause timer_start
timer_stop timer_stopall tolower toupper tr trim trunc tuple2list type typename
undofile undotree uniq uri_decode uri_encode utf16idx values virtcol
virtcol2col visualmode wildmenumode wildtrigger win_execute
-syn keyword vimFuncName contained win_findbuf win_getid win_gettype win_gotoid
win_id2tabwin win_id2win win_move_separator win_move_statusline win_screenpos
win_splitmove winbufnr wincol windowsversion winheight winlayout winline winnr
winrestcmd winrestview winsaveview winwidth wordcount writefile xor
+syn keyword vimFuncName contained ngettext nr2char or pathshorten perleval
popup_atcursor popup_beval popup_clear popup_close popup_create popup_dialog
popup_filter_menu popup_filter_yesno popup_findecho popup_findinfo
popup_findpreview popup_getoptions popup_getpos popup_hide popup_list
popup_locate popup_menu popup_move popup_notification popup_setbuf
popup_setoptions popup_settext popup_show pow preinserted prevnonblank printf
prompt_getprompt prompt_setcallback prompt_setinterrupt prompt_setprompt
prop_add prop_add_list prop_clear prop_find prop_list prop_remove prop_type_add
prop_type_change prop_type_delete prop_type_get prop_type_list pum_getpos
pumvisible py3eval pyeval pyxeval rand range readblob readdir readdirex
readfile redraw_listener_add redraw_listener_remove
+syn keyword vimFuncName contained reduce reg_executing reg_recording reltime
reltimefloat reltimestr remote_expr remote_foreground remote_peek remote_read
remote_send remote_startserver remove rename repeat resolve reverse round
rubyeval screenattr screenchar screenchars screencol screenpos screenrow
screenstring search searchcount searchdecl searchpair searchpairpos searchpos
server2client serverlist setbufline setbufvar setcellwidths setcharpos
setcharsearch setcmdline setcmdpos setcursorcharpos setenv setfperm setline
setloclist setmatches setpos setqflist setreg settabvar settabwinvar
settagstack setwinvar sha256 shellescape shiftwidth sign_define sign_getdefined
sign_getplaced sign_jump sign_place sign_placelist sign_undefine sign_unplace
sign_unplacelist
+syn keyword vimFuncName contained simplify sin sinh slice sort sound_clear
sound_playevent sound_playfile sound_stop soundfold spellbadword spellsuggest
split sqrt srand state str2blob str2float str2list str2nr strcharlen
strcharpart strchars strdisplaywidth strftime strgetchar stridx string strlen
strpart strptime strridx strtrans strutf16len strwidth submatch substitute
swapfilelist swapinfo swapname synID synIDattr synIDtrans synconcealed synstack
system systemlist tabpagebuflist tabpagenr tabpagewinnr tagfiles taglist tan
tanh tempname term_dumpdiff term_dumpload term_dumpwrite term_getaltscreen
term_getansicolors term_getattr term_getcursor term_getjob term_getline
term_getscrolled term_getsize term_getstatus term_gettitle term_gettty
term_list term_scrape
+syn keyword vimFuncName contained term_sendkeys term_setansicolors term_setapi
term_setkill term_setrestore term_setsize term_start term_wait terminalprops
test_alloc_fail test_autochdir test_feedinput test_garbagecollect_now
test_garbagecollect_soon test_getvalue test_gui_event test_ignore_error
test_mswin_event test_null_blob test_null_channel test_null_dict
test_null_function test_null_job test_null_list test_null_partial
test_null_string test_null_tuple test_option_not_set test_override
test_refcount test_setmouse test_settime test_srand_seed test_unknown test_void
timer_info timer_pause timer_start timer_stop timer_stopall tolower toupper tr
trim trunc tuple2list type typename undofile undotree uniq uri_decode
uri_encode utf16idx values virtcol virtcol2col
+syn keyword vimFuncName contained visualmode wildmenumode wildtrigger
win_execute win_findbuf win_getid win_gettype win_gotoid win_id2tabwin
win_id2win win_move_separator win_move_statusline win_screenpos win_splitmove
winbufnr wincol windowsversion winheight winlayout winline winnr winrestcmd
winrestview winsaveview winwidth wordcount writefile xor
" Predefined variable names {{{2
" GEN_SYN_VIM: vimVarName, START_STR='syn keyword vimVimVarName contained',
END_STR=''
diff --git a/src/drawscreen.c b/src/drawscreen.c
index 5c7ecfed9..6e37ccbab 100644
--- a/src/drawscreen.c
+++ b/src/drawscreen.c
@@ -73,6 +73,11 @@ static void redraw_custom_statusline(win_T *wp);
static int did_update_one_window;
#endif
+#ifdef FEAT_EVAL
+static void redraw_listener_cleanup(void);
+static void invoke_redraw_listener_start_or_end(bool start);
+#endif
+
/*
* Based on the current value of curwin->w_topline, transfer a screenfull
* of stuff from Filemem to ScreenLines[], and update curwin->w_botline.
@@ -244,6 +249,10 @@ update_screen(int type_arg)
curwin->w_redr_type = UPD_NOT_VALID;
#endif
+#ifdef FEAT_EVAL
+ invoke_redraw_listener_start_or_end(true);
+#endif
+
// Only start redrawing if there is really something to do.
if (type == UPD_INVERTED)
update_curswant();
@@ -405,6 +414,12 @@ update_screen(int type_arg)
gui_update_scrollbars(FALSE);
}
#endif
+
+#ifdef FEAT_EVAL
+ invoke_redraw_listener_start_or_end(false);
+ redraw_listener_cleanup();
+#endif
+
return OK;
}
@@ -3185,6 +3200,13 @@ redraw_win_later(
win_T *wp,
int type)
{
+#ifdef FEAT_EVAL
+ // If inside a redraw_listener_add() callback, then only set the redraw
type
+ // for the window and not request another one right after.
+ if (inside_redraw_on_start_cb && wp->w_redr_type < type)
+ wp->w_redr_type = type;
+ else
+#endif
if (!exiting && !redraw_not_allowed && wp->w_redr_type < type)
{
wp->w_redr_type = type;
@@ -3241,7 +3263,11 @@ redraw_all_windows_later(int type)
void
set_must_redraw(int type)
{
- if (!redraw_not_allowed && must_redraw < type)
+ if (!redraw_not_allowed &&
+#ifdef FEAT_EVAL
+ !inside_redraw_on_start_cb &&
+#endif
+ must_redraw < type)
must_redraw = type;
}
@@ -3410,3 +3436,187 @@ redraw_win_range_later(
redraw_win_later(wp, UPD_VALID);
}
}
+
+#ifdef FEAT_EVAL
+static bool redraw_cb_in_progress = false;
+
+ void
+f_redraw_listener_add(typval_T *argvars, typval_T *rettv)
+{
+ redraw_listener_T *rln;
+ dict_T *dict;
+ typval_T tv;
+ bool got_one = false;
+ static int id;
+
+ if (redraw_cb_in_progress)
+ {
+ emsg(_(e_cannot_add_redraw_listener_in_listener_callback));
+ return;
+ }
+
+ if (check_for_dict_arg(argvars, 0) == FAIL)
+ return;
+
+ rln = ALLOC_CLEAR_ONE(redraw_listener_T);
+
+ if (rln == NULL)
+ return;
+ dict = argvars[0].vval.v_dict;
+
+ /*
+ * on_start: called on each screen redraw
+ *
+ * on_end: called at the end of a redraw cycle
+ */
+ if (dict_get_tv(dict, "on_start", &tv) == OK)
+ {
+ callback_T cb = get_callback(&tv);
+
+ if (cb.cb_name == NULL)
+ {
+ clear_tv(&tv);
+ vim_free(rln);
+ return;
+ }
+ set_callback(&rln->rl_callbacks.on_start, &cb);
+ free_callback(&cb);
+ clear_tv(&tv);
+ got_one = true;
+ }
+
+ if (dict_get_tv(dict, "on_end", &tv) == OK)
+ {
+ callback_T cb = get_callback(&tv);
+
+ if (cb.cb_name == NULL)
+ {
+ clear_tv(&tv);
+ free_callback(&rln->rl_callbacks.on_start);
+ vim_free(rln);
+ return;
+ }
+ set_callback(&rln->rl_callbacks.on_end, &cb);
+ free_callback(&cb);
+ clear_tv(&tv);
+ got_one = true;
+ }
+
+ if (!got_one)
+ {
+ emsg(_(e_no_redraw_listener_callbacks_defined));
+ vim_free(rln);
+ return;
+ }
+
+ rln->rl_next = redraw_listeners;
+ redraw_listeners = rln;
+ rln->rl_id = ++id; // Never zero
+
+ rettv->v_type = VAR_NUMBER;
+ rettv->vval.v_number = id;
+
+ return;
+}
+
+ static void
+redraw_listener_free(redraw_listener_T *rln)
+{
+ free_callback(&rln->rl_callbacks.on_start);
+ free_callback(&rln->rl_callbacks.on_end);
+
+ vim_free(rln);
+}
+
+
+ static void
+redraw_listener_cleanup(void)
+{
+ for (redraw_listener_T *rln = redraw_listeners; rln != NULL;)
+ {
+ redraw_listener_T *next = rln->rl_next;
+ if (rln->rl_id == 0)
+ {
+ if (redraw_listeners == rln)
+ redraw_listeners = rln->rl_next;
+ redraw_listener_free(rln);
+ }
+ rln = next;
+ }
+}
+
+/*
+ * Return the redraw listener struct with the specified id. Returns NULL if not
+ * found.
+ */
+ static redraw_listener_T *
+get_redraw_listener(int id)
+{
+ for (redraw_listener_T *rln = redraw_listeners; rln != NULL; rln =
rln->rl_next)
+ if (rln->rl_id == id)
+ return rln;
+ return NULL;
+}
+
+ void
+f_redraw_listener_remove(typval_T *argvars, typval_T *rettv UNUSED)
+{
+ int id;
+ redraw_listener_T *rln;
+
+ if (check_for_number_arg(argvars, 0) == FAIL)
+ return;
+
+ id = argvars[0].vval.v_number;
+ rln = get_redraw_listener(id);
+
+ rettv->v_type = VAR_NUMBER;
+ if (rln == NULL)
+ {
+ rettv->vval.v_number = 0;
+ return;
+ }
+
+ // We set the id to zero instead of freeing it here, since we still need
+ // rl_next from it.
+ rln->rl_id = 0;
+ rettv->vval.v_number = 1;
+}
+
+/*
+ * Invoke the on_start callbacks.
+ */
+ static void
+invoke_redraw_listener_start_or_end(bool start)
+{
+ typval_T argv[1];
+ typval_T rettv;
+
+ argv[0].v_type = VAR_UNKNOWN;
+
+ if (start)
+ inside_redraw_on_start_cb = true;
+
+ redraw_cb_in_progress = true;
+ for (redraw_listener_T *rln = redraw_listeners; rln != NULL; rln =
rln->rl_next)
+ {
+ if (rln->rl_id == 0)
+ // Listener has been removed, skip
+ continue;
+ if (start && rln->rl_callbacks.on_start.cb_name != NULL)
+ {
+ call_callback(&rln->rl_callbacks.on_start, -1, &rettv, 0, argv);
+ clear_tv(&rettv);
+ }
+ else if (rln->rl_callbacks.on_end.cb_name != NULL)
+ {
+ call_callback(&rln->rl_callbacks.on_end, -1, &rettv, 0, argv);
+ clear_tv(&rettv);
+ }
+ }
+ redraw_cb_in_progress = false;
+
+ if (start)
+ inside_redraw_on_start_cb = false;
+}
+#endif // FEAT_EVAL
diff --git a/src/errors.h b/src/errors.h
index 8779c586b..4ad8e14e8 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3797,3 +3797,9 @@ EXTERN char e_osc_response_timed_out[]
EXTERN char e_cannot_add_listener_in_listener_callback[]
INIT(= N_("E1569: Cannot use listener_add in a listener callback"));
#endif
+#ifdef FEAT_EVAL
+EXTERN char e_cannot_add_redraw_listener_in_listener_callback[]
+ INIT(= N_("E1570: Cannot use redraw_listener_add in a redraw listener
callback"));
+EXTERN char e_no_redraw_listener_callbacks_defined[]
+ INIT(= N_("E1571: Must specify at least one callback for
redraw_listener_add"));
+#endif
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 49b7c72b2..be654a587 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2729,6 +2729,10 @@ static const funcentry_T global_functions[] =
ret_list_dict_any, f_readdirex},
{"readfile", 1, 3, FEARG_1, arg3_string_string_number,
ret_list_string, f_readfile},
+ {"redraw_listener_add", 1, 1, FEARG_1, arg1_dict_any,
+ ret_number, f_redraw_listener_add},
+ {"redraw_listener_remove", 1, 1, FEARG_1, arg1_number,
+ ret_void, f_redraw_listener_remove},
{"reduce", 2, 3, FEARG_1, arg23_reduce,
ret_any, f_reduce},
{"reg_executing", 0, 0, 0, NULL,
diff --git a/src/globals.h b/src/globals.h
index 180e3f44f..ed4affa8c 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -2132,3 +2132,9 @@ EXTERN char_u *client_socket INIT(= NULL);
// If the <xOSC> key should be propogated from vgetc()
EXTERN int allow_osc_key INIT(= 0);
+
+#ifdef FEAT_EVAL
+// Global singly linked list of redraw listeners
+EXTERN redraw_listener_T *redraw_listeners INIT(= NULL);
+EXTERN bool inside_redraw_on_start_cb INIT(= false);
+#endif
diff --git a/src/po/vim.pot b/src/po/vim.pot
index f250fbd17..c76e8170e 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Vim
"
"Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2025-12-06 10:11+0100
"
+"POT-Creation-Date: 2025-12-13 17:59+0100
"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
"Language-Team: LANGUAGE <[email protected]>
"
@@ -8839,6 +8839,12 @@ msgstr ""
msgid "E1569: Cannot use listener_add in a listener callback"
msgstr ""
+msgid "E1570: Cannot use redraw_listener_add in a redraw listener callback"
+msgstr ""
+
+msgid "E1571: Must specify at least one callback for redraw_listener_add"
+msgstr ""
+
#. type of cmdline window or 0
#. result of cmdline window or 0
#. buffer of cmdline window or NULL
diff --git a/src/proto/drawscreen.pro b/src/proto/drawscreen.pro
index 6f1d3e37a..dcf98cb6f 100644
--- a/src/proto/drawscreen.pro
+++ b/src/proto/drawscreen.pro
@@ -25,4 +25,6 @@ void redraw_statuslines(void);
void win_redraw_last_status(frame_T *frp);
void redrawWinline(win_T *wp, linenr_T lnum);
void redraw_win_range_later(win_T *wp, linenr_T first, linenr_T last);
+void f_redraw_listener_add(typval_T *argvars, typval_T *rettv);
+void f_redraw_listener_remove(typval_T *argvars, typval_T *rettv);
/* vim: set ft=c : */
diff --git a/src/structs.h b/src/structs.h
index 37c8ac6c0..b0969f782 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2864,6 +2864,19 @@ struct listener_S
int lr_id;
callback_T lr_callback;
};
+
+// Structure used for listeners added with redraw_listener_add().
+typedef struct redraw_listener_S redraw_listener_T;
+struct redraw_listener_S
+{
+ redraw_listener_T *rl_next;
+ int rl_id;
+ struct
+ {
+ callback_T on_start;
+ callback_T on_end;
+ } rl_callbacks;
+};
#endif
/*
diff --git a/src/testdir/test_listener.vim b/src/testdir/test_listener.vim
index 7fc985ec9..4b073af07 100644
--- a/src/testdir/test_listener.vim
+++ b/src/testdir/test_listener.vim
@@ -1,4 +1,6 @@
-" tests for listener_add() and listener_remove()
+" Tests for Listeners:
+" listener_add() and listener_remove()
+" redraw_listener_add() and redraw_listener_remove()
func s:StoreList(s, e, a, l)
let s:start = a:s
@@ -638,5 +640,138 @@ func Test_no_change_for_empty_undo()
delfunc Listener
endfunc
+" Test if redraws are correctly picked up
+func Test_redraw_listening()
+ CheckRunVimInTerminal
+ CheckFeature eval
+ let lines =<< trim END
+ let g:redrawtick = 0
+ let g:redrawtickend = 0
+
+ func OnRedrawStart()
+ let g:redrawtick += 1
+ call writefile([g:redrawtick, g:redrawtickend], 'XRedrawStartResult')
+ endfunc
+
+ func OnRedrawEnd()
+ let g:redrawtickend += 1
+ call writefile([g:redrawtick, g:redrawtickend], 'XRedrawEndResult')
+ endfunc
+
+ let g:listenerid = redraw_listener_add(#{
+ \ on_start: function("OnRedrawStart"),
+ \ on_end: function("OnRedrawEnd")
+ \ })
+ END
+ call writefile(lines, 'XTest_redrawlistener', 'D')
+ defer delete('XRedrawStartResult')
+ defer delete('XRedrawEndResult')
+
+ let buf = RunVimInTerminal('-S XTest_redrawlistener', {'rows': 10, 'cols':
78})
+
+ " We do it in separate chunks so they dont get bunched up into one redraw
+ call term_sendkeys(buf, "i") " 1 on startup
+ call TermWait(buf)
+ call term_sendkeys(buf, "one\<CR>") " 2
+ call TermWait(buf)
+ call term_sendkeys(buf, "two\<CR>") " 3
+ call TermWait(buf)
+ call term_sendkeys(buf, "three\<Esc>") " 4
+ call TermWait(buf)
+
+ call WaitForAssert({-> assert_equal(["4", "3"],
readfile('XRedrawStartResult'))})
+ call WaitForAssert({-> assert_equal(["4", "4"],
readfile('XRedrawEndResult'))})
+
+ call term_sendkeys(buf, "\<Esc>:vsplit\<CR>:enew\<CR>") " 5 and 6
+ call TermWait(buf)
+
+ call WaitForAssert({-> assert_equal(["6", "5"],
readfile('XRedrawStartResult'))})
+ call WaitForAssert({-> assert_equal(["6", "6"],
readfile('XRedrawEndResult'))})
+
+ call term_sendkeys(buf, "\<Esc>:redraw!\<CR>") " 7
+ call TermWait(buf)
+
+ call WaitForAssert({-> assert_equal(["7", "6"],
readfile('XRedrawStartResult'))})
+ call WaitForAssert({-> assert_equal(["7", "7"],
readfile('XRedrawEndResult'))})
+
+ " Test if removing listener works
+ call term_sendkeys(buf, "\<Esc>:call
redraw_listener_remove(g:listenerid)\<CR>")
+ call TermWait(buf)
+ call term_sendkeys(buf, "\<Esc>:redraw!\<CR>")
+ call TermWait(buf)
+ call term_sendkeys(buf, "\<Esc>:split\<CR>")
+ call TermWait(buf)
+ call WaitForAssert({-> assert_equal(["7", "6"],
readfile('XRedrawStartResult'))})
+ call WaitForAssert({-> assert_equal(["7", "7"],
readfile('XRedrawEndResult'))})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+" Test if another redraw isn't caused right after if on_start callback causes
one.
+func Test_redraw_no_redraw()
+ CheckRunVimInTerminal
+ CheckFeature eval
+ let lines =<< trim END
+ let g:redrawtick = 0
+
+ func OnRedrawStart()
+ call setline(1, "hello")
+
+ let g:redrawtick += 1
+ call writefile([g:redrawtick], 'XRedrawStartResult')
+ endfunc
+
+ let g:listenerid = redraw_listener_add(#{
+ \ on_start: function("OnRedrawStart"),
+ \ })
+ END
+ call writefile(lines, 'XTest_redrawlistener', 'D')
+ defer delete('XRedrawStartResult')
+
+ let buf = RunVimInTerminal('-S XTest_redrawlistener', {'rows': 10, 'cols':
78})
+
+ call term_sendkeys(buf, "ione\<Esc>")
+ call TermWait(buf)
+
+ call WaitForAssert({-> assert_equal(["2"], readfile('XRedrawStartResult'))})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+" Test if listener can be removed in the callback
+func Test_redraw_remove_in_callback()
+ CheckRunVimInTerminal
+ CheckFeature eval
+ let lines =<< trim END
+ let g:redrawtick = 0
+
+ func OnRedrawStart()
+ let g:redrawtick += 1
+ call writefile([g:redrawtick], 'XRedrawStartResult')
+ call redraw_listener_remove(g:listenerid)
+ endfunc
+
+ let g:listenerid = redraw_listener_add(#{
+ \ on_start: function("OnRedrawStart"),
+ \ })
+ END
+ call writefile(lines, 'XTest_redrawlistener', 'D')
+ defer delete('XRedrawStartResult')
+
+ let buf = RunVimInTerminal('-S XTest_redrawlistener', {'rows': 10, 'cols':
78})
+
+ call term_sendkeys(buf, "i")
+ call TermWait(buf)
+ call term_sendkeys(buf, "one\<CR>")
+ call TermWait(buf)
+ call term_sendkeys(buf, "two\<CR>")
+ call TermWait(buf)
+ call term_sendkeys(buf, "three\<Esc>")
+ call TermWait(buf)
+
+ call WaitForAssert({-> assert_equal(["1"], readfile('XRedrawStartResult'))})
+
+ call StopVimInTerminal(buf)
+endfunc
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/test_vim9_builtin.vim
b/src/testdir/test_vim9_builtin.vim
index c06672b23..e66a33b6e 100644
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -3471,6 +3471,14 @@ def Test_readfile()
v9.CheckSourceDefExecAndScriptFailure(['readfile("")'], 'E1175: Non-empty
string required for argument 1')
enddef
+def Test_redraw_listener_add()
+ v9.CheckSourceDefAndScriptFailure(['redraw_listener_add("1")'], ['E1013:
Argument 1: type mismatch, expected dict<any> but got string', 'E1206:
Dictionary required for argument 1'])
+enddef
+
+def Test_redraw_listener_remove()
+ v9.CheckSourceDefAndScriptFailure(['redraw_listener_remove("x")'], ['E1013:
Argument 1: type mismatch, expected number but got string', 'E1210: Number
required for argument 1'])
+enddef
+
def Test_reduce()
v9.CheckSourceDefAndScriptFailure(['reduce({a: 10}, "1")'], ['E1013:
Argument 1: type mismatch, expected list<any> but got dict<number>', 'E1253:
String, List, Tuple or Blob required for argument 1'])
assert_equal(6, [1, 2, 3]->reduce((r, c) => r + c, 0))
diff --git a/src/version.c b/src/version.c
index 467629519..da7c9d60f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1976,
/**/
1975,
/**/
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/vim_dev/E1vUTg1-00DN6F-GS%40256bit.org.