aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjörn Linse <bjorn.linse@gmail.com>2016-04-21 17:06:03 +0200
committerBjörn Linse <bjorn.linse@gmail.com>2016-05-24 22:08:56 +0200
commit61e8adb25e6f37c3c9df0dd8f4fea5e23b62bacd (patch)
tree8b9d3e2d2f4f6c83d6c513b35c56a11e86c921fe
parent176f223ea3d1e8fb5716d5c2c0f09bead4864d59 (diff)
downloadrneovim-61e8adb25e6f37c3c9df0dd8f4fea5e23b62bacd.tar.gz
rneovim-61e8adb25e6f37c3c9df0dd8f4fea5e23b62bacd.tar.bz2
rneovim-61e8adb25e6f37c3c9df0dd8f4fea5e23b62bacd.zip
eval: implement timers. vim-patch: 7.4.1578, 7.4.1831
For the moment, timers are triggered during sleep, but not in wait-for-input modes, like press-RETURN or f_getchar()
-rw-r--r--runtime/doc/eval.txt37
-rw-r--r--src/nvim/eval.c126
-rw-r--r--src/nvim/event/time.c5
-rw-r--r--src/nvim/event/time.h1
-rw-r--r--src/nvim/ex_docmd.c6
-rw-r--r--src/nvim/testdir/Makefile1
-rw-r--r--src/nvim/testdir/test_timers.vim32
-rw-r--r--src/nvim/version.c4
-rw-r--r--test/functional/eval/timer_spec.lua106
-rw-r--r--test/functional/legacy/061_undo_tree_spec.lua13
10 files changed, 318 insertions, 13 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index eebdabd154..8b23d2ff5f 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -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.
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 96c008b0e0..6ae5081550 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -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
*/
diff --git a/src/nvim/event/time.c b/src/nvim/event/time.c
index 7bf333bcea..f68a66345f 100644
--- a/src/nvim/event/time.c
+++ b/src/nvim/event/time.c
@@ -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);
}
diff --git a/src/nvim/event/time.h b/src/nvim/event/time.h
index 7882b2b627..14df176ea3 100644
--- a/src/nvim/event/time.h
+++ b/src/nvim/event/time.h
@@ -13,6 +13,7 @@ struct time_watcher {
void *data;
time_cb cb, close_cb;
Queue *events;
+ bool blockable;
};
#ifdef INCLUDE_GENERATED_DECLARATIONS
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 870284a0f7..59962c153b 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -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();
}
}
diff --git a/src/nvim/testdir/Makefile b/src/nvim/testdir/Makefile
index 2fba6dd563..3e17117db0 100644
--- a/src/nvim/testdir/Makefile
+++ b/src/nvim/testdir/Makefile
@@ -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
diff --git a/src/nvim/testdir/test_timers.vim b/src/nvim/testdir/test_timers.vim
new file mode 100644
index 0000000000..9f58a35909
--- /dev/null
+++ b/src/nvim/testdir/test_timers.vim
@@ -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
diff --git a/src/nvim/version.c b/src/nvim/version.c
index 23bfca6221..820c878998 100644
--- a/src/nvim/version.c
+++ b/src/nvim/version.c
@@ -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
diff --git a/test/functional/eval/timer_spec.lua b/test/functional/eval/timer_spec.lua
new file mode 100644
index 0000000000..df0a55e462
--- /dev/null
+++ b/test/functional/eval/timer_spec.lua
@@ -0,0 +1,106 @@
+local helpers = require('test.functional.helpers')
+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)
+
+end)
diff --git a/test/functional/legacy/061_undo_tree_spec.lua b/test/functional/legacy/061_undo_tree_spec.lua
index 6db37bf1ff..350a77b2c5 100644
--- a/test/functional/legacy/061_undo_tree_spec.lua
+++ b/test/functional/legacy/061_undo_tree_spec.lua
@@ -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>')