Merge pull request #4624 from bfredl/timers

implement timers and process events during sleep
This commit is contained in:
Björn Linse 2016-05-25 11:00:54 +02:00
commit c74ce334f2
14 changed files with 374 additions and 20 deletions

View File

@ -2114,9 +2114,12 @@ tabpagewinnr({tabarg}[, {arg}])
Number number of current window in tab page
taglist({expr}) List list of tags matching {expr}
tagfiles() List tags files used
tempname() String name for a temporary file
tan({expr}) Float tangent of {expr}
tanh({expr}) Float hyperbolic tangent of {expr}
tempname() String name for a temporary file
timer_start({time}, {callback} [, {options}])
Number create a timer
timer_stop({timer}) none stop a timer
tolower({expr}) String the String {expr} switched to lowercase
toupper({expr}) String the String {expr} switched to uppercase
tr({src}, {fromstr}, {tostr}) String translate chars of {src} in {fromstr}
@ -6840,6 +6843,37 @@ tanh({expr}) *tanh()*
< -0.761594
*timer_start()*
timer_start({time}, {callback} [, {options}])
Create a timer and return the timer ID.
{time} is the waiting time in milliseconds. This is the
minimum time before invoking the callback. When the system is
busy or Vim is not waiting for input the time will be longer.
{callback} is the function to call. It can be the name of a
function or a Funcref. It is called with one argument, which
is the timer ID. The callback is only invoked when Vim is
waiting for input.
{options} is a dictionary. Supported entries:
"repeat" Number of times to repeat calling the
callback. -1 means forever.
Example: >
func MyHandler(timer)
echo 'Handler called'
endfunc
let timer = timer_start(500, 'MyHandler',
\ {'repeat': 3})
< This will invoke MyHandler() three times at 500 msec
intervals.
{only available when compiled with the |+timers| feature}
timer_stop({timer}) *timer_stop()*
Stop a timer. {timer} is an ID returned by timer_start().
The timer callback will no longer be invoked.
tolower({expr}) *tolower()*
The result is a copy of the String given, with all uppercase
characters turned into lowercase (just like applying |gu| to
@ -7323,6 +7357,7 @@ termresponse Compiled with support for |t_RV| and |v:termresponse|.
textobjects Compiled with support for |text-objects|.
tgetent Compiled with tgetent support, able to use a termcap
or terminfo file.
timers Compiled with |timer_start()| support.
title Compiled with window title support |'title'|.
toolbar Compiled with support for |gui-toolbar|.
unix Unix version of Vim.

View File

@ -79,6 +79,7 @@
#include "nvim/event/pty_process.h"
#include "nvim/event/rstream.h"
#include "nvim/event/wstream.h"
#include "nvim/event/time.h"
#include "nvim/os/time.h"
#include "nvim/msgpack_rpc/channel.h"
#include "nvim/msgpack_rpc/server.h"
@ -428,6 +429,14 @@ typedef struct {
int status;
} JobEvent;
typedef struct {
TimeWatcher tw;
int timer_id;
int repeat_count;
bool stopped;
ufunc_T *callback;
} timer_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval.c.generated.h"
#endif
@ -438,6 +447,9 @@ typedef struct {
static uint64_t current_job_id = 1;
static PMap(uint64_t) *jobs = NULL;
static uint64_t last_timer_id = 0;
static PMap(uint64_t) *timers = NULL;
static const char *const msgpack_type_names[] = {
[kMPNil] = "nil",
[kMPBoolean] = "boolean",
@ -469,6 +481,7 @@ void eval_init(void)
vimvars[VV_VERSION].vv_nr = VIM_VERSION_100;
jobs = pmap_new(uint64_t)();
timers = pmap_new(uint64_t)();
struct vimvar *p;
init_var_dict(&globvardict, &globvars_var, VAR_DEF_SCOPE);
@ -6930,6 +6943,8 @@ static struct fst {
{ "tempname", 0, 0, f_tempname },
{ "termopen", 1, 2, f_termopen },
{ "test", 1, 1, f_test },
{ "timer_start", 2, 3, f_timer_start },
{ "timer_stop", 1, 1, f_timer_stop },
{ "tolower", 1, 1, f_tolower },
{ "toupper", 1, 1, f_toupper },
{ "tr", 3, 3, f_tr },
@ -10688,6 +10703,7 @@ static void f_has(typval_T *argvars, typval_T *rettv)
"termguicolors",
"termresponse",
"textobjects",
"timers",
"title",
"user-commands", /* was accidentally included in 5.4 */
"user_commands",
@ -16441,6 +16457,116 @@ static void f_tanh(typval_T *argvars, typval_T *rettv)
float_op_wrapper(argvars, rettv, &tanh);
}
/// "timer_start(timeout, callback, opts)" function
static void f_timer_start(typval_T *argvars, typval_T *rettv)
{
long timeout = get_tv_number(&argvars[0]);
timer_T *timer;
int repeat = 1;
dict_T *dict;
rettv->vval.v_number = -1;
if (argvars[2].v_type != VAR_UNKNOWN) {
if (argvars[2].v_type != VAR_DICT
|| (dict = argvars[2].vval.v_dict) == NULL) {
EMSG2(_(e_invarg2), get_tv_string(&argvars[2]));
return;
}
if (dict_find(dict, (char_u *)"repeat", -1) != NULL) {
repeat = get_dict_number(dict, (char_u *)"repeat");
}
}
if (argvars[1].v_type != VAR_FUNC && argvars[1].v_type != VAR_STRING) {
EMSG2(e_invarg2, "funcref");
return;
}
ufunc_T *func = find_ufunc(argvars[1].vval.v_string);
if (!func) {
// Invalid function name. Error already reported by `find_ufunc`.
return;
}
func->uf_refcount++;
timer = xmalloc(sizeof *timer);
timer->stopped = false;
timer->repeat_count = repeat;
timer->timer_id = last_timer_id++;
timer->callback = func;
time_watcher_init(&loop, &timer->tw, timer);
timer->tw.events = queue_new_child(loop.events);
// if main loop is blocked, don't queue up multiple events
timer->tw.blockable = true;
time_watcher_start(&timer->tw, timer_due_cb, timeout,
timeout * (repeat != 1));
pmap_put(uint64_t)(timers, timer->timer_id, timer);
rettv->vval.v_number = timer->timer_id;
}
// "timer_stop(timerid)" function
static void f_timer_stop(typval_T *argvars, typval_T *rettv)
{
if (argvars[0].v_type != VAR_NUMBER) {
EMSG(_(e_number_exp));
return;
}
timer_T *timer = pmap_get(uint64_t)(timers, get_tv_number(&argvars[0]));
if (timer == NULL) {
return;
}
timer_stop(timer);
}
// invoked on the main loop
static void timer_due_cb(TimeWatcher *tw, void *data)
{
timer_T *timer = (timer_T *)data;
// if repeat was negative repeat forever
if (timer->repeat_count >= 0 && --timer->repeat_count == 0) {
timer_stop(timer);
}
typval_T argv[1];
init_tv(argv);
argv[0].v_type = VAR_NUMBER;
argv[0].vval.v_number = timer->timer_id;
typval_T rettv;
init_tv(&rettv);
call_user_func(timer->callback, ARRAY_SIZE(argv), argv, &rettv,
curwin->w_cursor.lnum, curwin->w_cursor.lnum, NULL);
clear_tv(&rettv);
}
static void timer_stop(timer_T *timer)
{
if (timer->stopped) {
// avoid double free
return;
}
timer->stopped = true;
time_watcher_stop(&timer->tw);
time_watcher_close(&timer->tw, timer_free_cb);
}
// invoked on next event loop tick, so queue is empty
static void timer_free_cb(TimeWatcher *tw, void *data)
{
timer_T *timer = (timer_T *)data;
queue_free(timer->tw.events);
user_func_unref(timer->callback);
pmap_del(uint64_t)(timers, timer->timer_id);
xfree(timer);
}
/*
* "tolower(string)" function
*/

View File

@ -17,6 +17,7 @@ void time_watcher_init(Loop *loop, TimeWatcher *watcher, void *data)
watcher->uv.data = watcher;
watcher->data = data;
watcher->events = loop->fast_events;
watcher->blockable = false;
}
void time_watcher_start(TimeWatcher *watcher, time_cb cb, uint64_t timeout,
@ -50,6 +51,10 @@ static void time_watcher_cb(uv_timer_t *handle)
FUNC_ATTR_NONNULL_ALL
{
TimeWatcher *watcher = handle->data;
if (watcher->blockable && !queue_empty(watcher->events)) {
// the timer blocked and there already is an unprocessed event waiting
return;
}
CREATE_EVENT(watcher->events, time_event, 1, watcher);
}

View File

@ -13,6 +13,7 @@ struct time_watcher {
void *data;
time_cb cb, close_cb;
Queue *events;
bool blockable;
};
#ifdef INCLUDE_GENERATED_DECLARATIONS

View File

@ -6989,10 +6989,10 @@ static void ex_sleep(exarg_T *eap)
*/
void do_sleep(long msec)
{
long done;
ui_flush(); // flush before waiting
for (done = 0; !got_int && done < msec; done += 1000L) {
os_delay(msec - done > 1000L ? 1000L : msec - done, true);
for (long left = msec; !got_int && left > 0; left -= 1000L) {
int next = left > 1000l ? 1000 : (int)left;
LOOP_PROCESS_EVENTS_UNTIL(&loop, loop.events, (int)next, got_int);
os_breakcheck();
}
}

View File

@ -359,6 +359,7 @@ static int command_line_execute(VimState *state, int key)
if (s->c == K_EVENT) {
queue_process_events(loop.events);
redrawcmdline();
return 1;
}

View File

@ -785,11 +785,13 @@ void wait_return(int redraw)
State = HITRETURN;
setmouse();
/* Avoid the sequence that the user types ":" at the hit-return prompt
* to start an Ex command, but the file-changed dialog gets in the
* way. */
if (need_check_timestamps)
check_timestamps(FALSE);
cmdline_row = msg_row;
// Avoid the sequence that the user types ":" at the hit-return prompt
// to start an Ex command, but the file-changed dialog gets in the
// way.
if (need_check_timestamps) {
check_timestamps(false);
}
hit_return_msg();
@ -1970,6 +1972,7 @@ static void msg_puts_printf(char *str, int maxlen)
*/
static int do_more_prompt(int typed_char)
{
static bool entered = false;
int used_typed_char = typed_char;
int oldState = State;
int c;
@ -1979,6 +1982,13 @@ static int do_more_prompt(int typed_char)
msgchunk_T *mp;
int i;
// We get called recursively when a timer callback outputs a message. In
// that case don't show another prompt. Also when at the hit-Enter prompt.
if (entered || State == HITRETURN) {
return false;
}
entered = true;
if (typed_char == 'G') {
/* "g<": Find first line on the last page. */
mp_last = msg_sb_start(last_msgchunk);
@ -2153,9 +2163,11 @@ static int do_more_prompt(int typed_char)
if (quit_more) {
msg_row = Rows - 1;
msg_col = 0;
} else if (cmdmsg_rl)
} else if (cmdmsg_rl) {
msg_col = Columns - 1;
}
entered = false;
return retval;
}

View File

@ -40,6 +40,7 @@ NEW_TESTS = \
test_cursor_func.res \
test_help_tagjump.res \
test_menu.res \
test_timers.res \
test_viml.res \
test_alot.res

View File

@ -0,0 +1,32 @@
" Test for timers
if !has('timers')
finish
endif
func MyHandler(timer)
let s:val += 1
endfunc
func Test_oneshot()
let s:val = 0
let timer = timer_start(50, 'MyHandler')
sleep 200m
call assert_equal(1, s:val)
endfunc
func Test_repeat_three()
let s:val = 0
let timer = timer_start(50, 'MyHandler', {'repeat': 3})
sleep 500m
call assert_equal(3, s:val)
endfunc
func Test_repeat_many()
let s:val = 0
let timer = timer_start(50, 'MyHandler', {'repeat': -1})
sleep 200m
call timer_stop(timer)
call assert_true(s:val > 1)
call assert_true(s:val < 5)
endfunc

View File

@ -70,6 +70,7 @@ static char *features[] = {
// clang-format off
static int included_patches[] = {
1832,
1831,
1809,
1808,
1806,
@ -82,6 +83,7 @@ static int included_patches[] = {
1652,
1643,
1641,
// 1624 NA
// 1600 NA
// 1599 NA
@ -106,7 +108,7 @@ static int included_patches[] = {
// 1581,
// 1580,
// 1579,
// 1578,
1578,
// 1577,
1576,
// 1575 NA

View File

@ -0,0 +1,129 @@
local helpers = require('test.functional.helpers')
local Screen = require('test.functional.ui.screen')
local ok, feed, eq, eval = helpers.ok, helpers.feed, helpers.eq, helpers.eval
local source, nvim_async, run = helpers.source, helpers.nvim_async, helpers.run
local clear, execute, funcs = helpers.clear, helpers.execute, helpers.funcs
describe('timers', function()
before_each(function()
clear()
source([[
let g:val = 0
func MyHandler(timer)
let g:val += 1
endfunc
]])
end)
it('works one-shot', function()
execute("call timer_start(50, 'MyHandler')")
eq(0,eval("g:val"))
run(nil, nil, nil, 200)
eq(1,eval("g:val"))
end)
it('works with repeat two', function()
execute("call timer_start(50, 'MyHandler', {'repeat': 2})")
eq(0,eval("g:val"))
run(nil, nil, nil, 300)
eq(2,eval("g:val"))
end)
it('are triggered during sleep', function()
execute("call timer_start(50, 'MyHandler', {'repeat': 2})")
nvim_async("command", "sleep 10")
eq(0,eval("g:val"))
run(nil, nil, nil, 300)
eq(2,eval("g:val"))
end)
it('can be started during sleep', function()
nvim_async("command", "sleep 10")
-- this also tests that remote requests works during sleep
eval("timer_start(50, 'MyHandler', {'repeat': 2})")
eq(0,eval("g:val"))
run(nil, nil, nil, 300)
eq(2,eval("g:val"))
end)
it('are paused when event processing is disabled', function()
-- this is not the intended behavior, but at least there will
-- not be a burst of queued up callbacks
execute("call timer_start(50, 'MyHandler', {'repeat': 2})")
run(nil, nil, nil, 100)
local count = eval("g:val")
nvim_async("command", "let g:c = getchar()")
run(nil, nil, nil, 300)
feed("c")
local diff = eval("g:val") - count
ok(0 <= diff and diff <= 2)
eq(99, eval("g:c"))
end)
it('can be stopped', function()
local t = eval("timer_start(50, 'MyHandler', {'repeat': -1})")
eq(0,eval("g:val"))
run(nil, nil, nil, 300)
funcs.timer_stop(t)
local count = eval("g:val")
run(nil, nil, nil, 300)
local count2 = eval("g:val")
ok(4 <= count and count <= 7)
-- when count is eval:ed after timer_stop this should be non-racy
eq(count, count2)
end)
it('can be stopped from the handler', function()
source([[
func! MyHandler(timer)
let g:val += 1
if g:val == 3
call timer_stop(a:timer)
" check double stop is ignored
call timer_stop(a:timer)
endif
endfunc
]])
execute("call timer_start(50, 'MyHandler', {'repeat': -1})")
eq(0,eval("g:val"))
run(nil, nil, nil, 300)
eq(3,eval("g:val"))
end)
it('can have two timers', function()
source([[
let g:val2 = 0
func! MyHandler2(timer)
let g:val2 += 1
endfunc
]])
execute("call timer_start(50, 'MyHandler', {'repeat': 3})")
execute("call timer_start(100, 'MyHandler2', {'repeat': 2})")
run(nil, nil, nil, 300)
eq(3,eval("g:val"))
eq(2,eval("g:val2"))
end)
it("doesn't mess up the cmdline", function()
local screen = Screen.new(40, 6)
screen:attach()
screen:set_default_attr_ignore({{bold=true, foreground=Screen.colors.Blue}})
source([[
func! MyHandler(timer)
echo "evil"
endfunc
]])
execute("call timer_start(100, 'MyHandler', {'repeat': 1})")
feed(":good")
screen:sleep(200)
screen:expect([[
|
~ |
~ |
~ |
~ |
:good^ |
]])
end)
end)

View File

@ -1,10 +1,9 @@
-- Tests for undo tree and :earlier and :later.
local helpers = require('test.functional.helpers')
local feed, source, eq, eval, clear, execute, expect, wait, write_file =
helpers.feed, helpers.source, helpers.eq, helpers.eval,
helpers.clear, helpers.execute, helpers.expect, helpers.wait,
helpers.write_file
local expect, feed, source = helpers.expect, helpers.feed, helpers.source
local eval, clear, execute = helpers.eval, helpers.clear, helpers.execute
local write_file, command, eq = helpers.write_file, helpers.command, helpers.eq
local function expect_empty_buffer()
-- The space will be removed by helpers.dedent but is needed because dedent
@ -57,8 +56,7 @@ describe('undo tree:', function()
-- Delete three other characters and go back in time step by step.
feed('$xxx')
expect_line('123456')
execute('sleep 1')
wait()
command('sleep 1')
feed('g-')
expect_line('1234567')
feed('g-')
@ -79,8 +77,7 @@ describe('undo tree:', function()
expect_line('123456')
-- Delay for two seconds and go some seconds forward and backward.
execute('sleep 2')
wait()
command('sleep 2')
feed('Aa<esc>')
feed('Ab<esc>')
feed('Ac<esc>')

View File

@ -290,6 +290,10 @@ If everything else fails, use Screen:redraw_debug to help investigate what is
end
end
function Screen:sleep(ms)
pcall(function() self:wait(function() return "error" end, ms) end)
end
function Screen:_redraw(updates)
for _, update in ipairs(updates) do
-- print('--')
@ -501,7 +505,7 @@ end
function Screen:snapshot_util(attrs, ignore)
-- util to generate screen test
pcall(function() self:wait(function() return "error" end, 250) end)
self:sleep(250)
self:print_snapshot(attrs, ignore)
end

View File

@ -114,6 +114,15 @@ add_custom_target(lpeg
list(APPEND THIRD_PARTY_DEPS lpeg)
add_custom_command(OUTPUT ${HOSTDEPS_LIB_DIR}/luarocks/rocks/inspect
COMMAND ${LUAROCKS_BINARY}
ARGS build inspect ${LUAROCKS_BUILDARGS}
DEPENDS mpack)
add_custom_target(inspect
DEPENDS ${HOSTDEPS_LIB_DIR}/luarocks/rocks/inspect)
list(APPEND THIRD_PARTY_DEPS inspect)
if(USE_BUNDLED_BUSTED)
add_custom_command(OUTPUT ${HOSTDEPS_BIN_DIR}/busted
COMMAND ${LUAROCKS_BINARY}