api: nvim_get_mode()

Asynchronous API functions are served immediately, which means pending
input could change the state of Nvim shortly after an async API function
result is returned.

nvim_get_mode() is different:
  - If RPCs are known to be blocked, it responds immediately (without
    flushing the input/event queue)
  - else it is handled just-in-time before waiting for input, after
    pending input was processed. This makes the result more reliable
    (but not perfect).

Internally this is handled as a special case, but _semantically_ nothing
has changed: API users never know when input flushes, so this internal
special-case doesn't violate that. As far as API users are concerned,
nvim_get_mode() is just another asynchronous API function.

In all cases nvim_get_mode() never blocks for more than the time it
takes to flush the input/event queue (~µs).

Note: This doesn't address #6166; nvim_get_mode() will provoke #6166 if
e.g. `d` is operator-pending.

Closes #6159
This commit is contained in:
Justin M. Keyes
2017-03-13 15:02:37 +01:00
parent 7044aa6e82
commit 3ea1007753
16 changed files with 250 additions and 77 deletions

View File

@@ -154,22 +154,17 @@ Interfaces ~
|if_cscop.txt| using Cscope with Vim |if_cscop.txt| using Cscope with Vim
|if_pyth.txt| Python interface |if_pyth.txt| Python interface
|if_ruby.txt| Ruby interface |if_ruby.txt| Ruby interface
|debugger.txt| Interface with a debugger
|sign.txt| debugging signs |sign.txt| debugging signs
Versions ~ Versions ~
|vim_diff.txt| Main differences between Nvim and Vim |vim_diff.txt| Main differences between Nvim and Vim
|vi_diff.txt| Main differences between Vim and Vi |vi_diff.txt| Main differences between Vim and Vi
*sys-file-list*
Remarks about specific systems ~
|os_win32.txt| MS-Windows
*standard-plugin-list* *standard-plugin-list*
Standard plugins ~ Standard plugins ~
|pi_gzip.txt| Reading and writing compressed files |pi_gzip.txt| Reading and writing compressed files
|pi_netrw.txt| Reading and writing files over a network |pi_netrw.txt| Reading and writing files over a network
|pi_paren.txt| Highlight matching parens |pi_paren.txt| Highlight matching parens
|pi_tar.txt| Tar file explorer |pi_tar.txt| Tar file explorer
|pi_vimball.txt| Create a self-installing Vim script
|pi_zip.txt| Zip archive explorer |pi_zip.txt| Zip archive explorer
LOCAL ADDITIONS: *local-additions* LOCAL ADDITIONS: *local-additions*

View File

@@ -34,11 +34,6 @@ It can be accessed from within Vim with the <Help> or <F1> key and with the
is not located in the default place. You can jump to subjects like with tags: is not located in the default place. You can jump to subjects like with tags:
Use CTRL-] to jump to a subject under the cursor, use CTRL-T to jump back. Use CTRL-] to jump to a subject under the cursor, use CTRL-T to jump back.
This manual refers to Vim on various machines. There may be small differences
between different computers and terminals. Besides the remarks given in this
document, there is a separate document for each supported system, see
|sys-file-list|.
*pronounce* *pronounce*
Vim is pronounced as one word, like Jim, not vi-ai-em. It's written with a Vim is pronounced as one word, like Jim, not vi-ai-em. It's written with a
capital, since it's a name, again like Jim. capital, since it's a name, again like Jim.

View File

@@ -40,10 +40,6 @@ Note: If the output has been stopped with "q" at the more prompt, it will only
be displayed up to this point. be displayed up to this point.
The previous command output is cleared when another command produces output. The previous command output is cleared when another command produces output.
If you are using translated messages, the first printed line tells who
maintains the messages or the translations. You can use this to contact the
maintainer when you spot a mistake.
If you want to find help on a specific (error) message, use the ID at the If you want to find help on a specific (error) message, use the ID at the
start of the message. For example, to get help on the message: > start of the message. For example, to get help on the message: >

View File

@@ -11,6 +11,7 @@
#include "nvim/api/vim.h" #include "nvim/api/vim.h"
#include "nvim/ascii.h" #include "nvim/ascii.h"
#include "nvim/log.h"
#include "nvim/api/private/helpers.h" #include "nvim/api/private/helpers.h"
#include "nvim/api/private/defs.h" #include "nvim/api/private/defs.h"
#include "nvim/api/buffer.h" #include "nvim/api/buffer.h"
@@ -27,6 +28,7 @@
#include "nvim/eval.h" #include "nvim/eval.h"
#include "nvim/eval/typval.h" #include "nvim/eval/typval.h"
#include "nvim/option.h" #include "nvim/option.h"
#include "nvim/state.h"
#include "nvim/syntax.h" #include "nvim/syntax.h"
#include "nvim/getchar.h" #include "nvim/getchar.h"
#include "nvim/os/input.h" #include "nvim/os/input.h"
@@ -701,6 +703,25 @@ Dictionary nvim_get_color_map(void)
} }
/// Gets the current mode.
/// mode: Mode string. |mode()|
/// blocking: true if Nvim is waiting for input.
///
/// @returns Dictionary { "mode": String, "blocking": Boolean }
Dictionary nvim_get_mode(void)
FUNC_API_SINCE(2) FUNC_API_ASYNC
{
Dictionary rv = ARRAY_DICT_INIT;
char *modestr = get_mode();
bool blocked = input_blocking();
ILOG("blocked=%d", blocked);
PUT(rv, "mode", STRING_OBJ(cstr_as_string(modestr)));
PUT(rv, "blocking", BOOLEAN_OBJ(blocked));
return rv;
}
Array nvim_get_api_info(uint64_t channel_id) Array nvim_get_api_info(uint64_t channel_id)
FUNC_API_SINCE(1) FUNC_API_ASYNC FUNC_API_NOEVAL FUNC_API_SINCE(1) FUNC_API_ASYNC FUNC_API_NOEVAL
{ {

View File

@@ -12575,59 +12575,18 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, FunPtr fptr)
} }
} }
/* /// "mode()" function
* "mode()" function
*/
static void f_mode(typval_T *argvars, typval_T *rettv, FunPtr fptr) static void f_mode(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{ {
char_u buf[3]; char *mode = get_mode();
buf[1] = NUL; // Clear out the minor mode when the argument is not a non-zero number or
buf[2] = NUL; // non-empty string.
if (!non_zero_arg(&argvars[0])) {
if (VIsual_active) { mode[1] = NUL;
if (VIsual_select)
buf[0] = VIsual_mode + 's' - 'v';
else
buf[0] = VIsual_mode;
} else if (State == HITRETURN || State == ASKMORE || State == SETWSIZE
|| State == CONFIRM) {
buf[0] = 'r';
if (State == ASKMORE)
buf[1] = 'm';
else if (State == CONFIRM)
buf[1] = '?';
} else if (State == EXTERNCMD)
buf[0] = '!';
else if (State & INSERT) {
if (State & VREPLACE_FLAG) {
buf[0] = 'R';
buf[1] = 'v';
} else if (State & REPLACE_FLAG)
buf[0] = 'R';
else
buf[0] = 'i';
} else if (State & CMDLINE) {
buf[0] = 'c';
if (exmode_active)
buf[1] = 'v';
} else if (exmode_active) {
buf[0] = 'c';
buf[1] = 'e';
} else if (State & TERM_FOCUS) {
buf[0] = 't';
} else {
buf[0] = 'n';
if (finish_op)
buf[1] = 'o';
} }
/* Clear out the minor mode when the argument is not a non-zero number or rettv->vval.v_string = (char_u *)mode;
* non-empty string. */
if (!non_zero_arg(&argvars[0]))
buf[1] = NUL;
rettv->vval.v_string = vim_strsave(buf);
rettv->v_type = VAR_STRING; rettv->v_type = VAR_STRING;
} }

View File

@@ -44,8 +44,7 @@ void loop_poll_events(Loop *loop, int ms)
// we do not block indefinitely for I/O. // we do not block indefinitely for I/O.
uv_timer_start(&loop->poll_timer, timer_cb, (uint64_t)ms, (uint64_t)ms); uv_timer_start(&loop->poll_timer, timer_cb, (uint64_t)ms, (uint64_t)ms);
} else if (ms == 0) { } else if (ms == 0) {
// For ms == 0, we need to do a non-blocking event poll by // For ms == 0, do a non-blocking event poll.
// setting the run mode to UV_RUN_NOWAIT.
mode = UV_RUN_NOWAIT; mode = UV_RUN_NOWAIT;
} }

View File

@@ -55,6 +55,7 @@
#include "nvim/event/multiqueue.h" #include "nvim/event/multiqueue.h"
#include "nvim/memory.h" #include "nvim/memory.h"
#include "nvim/log.h"
#include "nvim/os/time.h" #include "nvim/os/time.h"
typedef struct multiqueue_item MultiQueueItem; typedef struct multiqueue_item MultiQueueItem;
@@ -151,6 +152,40 @@ void multiqueue_process_events(MultiQueue *this)
} }
} }
void multiqueue_process_debug(MultiQueue *this)
{
assert(this);
QUEUE *start = QUEUE_HEAD(&this->headtail);
QUEUE *cur = start;
// MultiQueue *start = this;
// MultiQueue *cur = start;
do {
MultiQueueItem *item = multiqueue_node_data(cur);
Event ev;
if (item->link) {
assert(!this->parent);
// get the next node in the linked queue
MultiQueue *linked = item->data.queue;
assert(!multiqueue_empty(linked));
MultiQueueItem *child =
multiqueue_node_data(QUEUE_HEAD(&linked->headtail));
ev = child->data.item.event;
} else {
ev = item->data.item.event;
}
// Event event = multiqueue_get(this);
// if (event.handler) {
// event.handler(event.argv);
// }
ILOG("ev: priority=%d, handler=%p arg1=%s", ev.priority, ev.handler,
ev.argv ? ev.argv[0] : "(null)");
cur = cur->next;
} while (cur && cur != start);
}
/// Removes all events without processing them. /// Removes all events without processing them.
void multiqueue_purge_events(MultiQueue *this) void multiqueue_purge_events(MultiQueue *this)
{ {

View File

@@ -345,10 +345,6 @@ char *xstpcpy(char *restrict dst, const char *restrict src)
/// WARNING: xstpncpy will ALWAYS write maxlen bytes. If src is shorter than /// WARNING: xstpncpy will ALWAYS write maxlen bytes. If src is shorter than
/// maxlen, zeroes will be written to the remaining bytes. /// maxlen, zeroes will be written to the remaining bytes.
/// ///
/// TODO(aktau): I don't see a good reason to have this last behaviour, and
/// it is potentially wasteful. Could we perhaps deviate from the standard
/// and not zero the rest of the buffer?
///
/// @param dst /// @param dst
/// @param src /// @param src
/// @param maxlen /// @param maxlen

View File

@@ -28,7 +28,9 @@
#include "nvim/map.h" #include "nvim/map.h"
#include "nvim/log.h" #include "nvim/log.h"
#include "nvim/misc1.h" #include "nvim/misc1.h"
#include "nvim/state.h"
#include "nvim/lib/kvec.h" #include "nvim/lib/kvec.h"
#include "nvim/os/input.h"
#define CHANNEL_BUFFER_SIZE 0xffff #define CHANNEL_BUFFER_SIZE 0xffff
@@ -433,6 +435,14 @@ static void handle_request(Channel *channel, msgpack_object *request)
handler.async = true; handler.async = true;
} }
if (handler.async) {
char buf[256] = { 0 };
memcpy(buf, method->via.bin.ptr, MIN(255, method->via.bin.size));
if (strcmp("nvim_get_mode", buf) == 0) {
handler.async = input_blocking();
}
}
RequestEvent *event_data = xmalloc(sizeof(RequestEvent)); RequestEvent *event_data = xmalloc(sizeof(RequestEvent));
event_data->channel = channel; event_data->channel = channel;
event_data->handler = handler; event_data->handler = handler;

View File

@@ -76,7 +76,7 @@ typedef struct {
size_t idx; size_t idx;
} MPToAPIObjectStackItem; } MPToAPIObjectStackItem;
/// Convert type used by msgpack parser to Neovim own API type /// Convert type used by msgpack parser to Nvim API type.
/// ///
/// @param[in] obj Msgpack value to convert. /// @param[in] obj Msgpack value to convert.
/// @param[out] arg Location where result of conversion will be saved. /// @param[out] arg Location where result of conversion will be saved.

View File

@@ -14,6 +14,7 @@
#include <stdlib.h> #include <stdlib.h>
#include "nvim/vim.h" #include "nvim/vim.h"
#include "nvim/log.h"
#include "nvim/ascii.h" #include "nvim/ascii.h"
#include "nvim/normal.h" #include "nvim/normal.h"
#include "nvim/buffer.h" #include "nvim/buffer.h"
@@ -541,7 +542,7 @@ static bool normal_handle_special_visual_command(NormalState *s)
return false; return false;
} }
static bool normal_need_aditional_char(NormalState *s) static bool normal_need_additional_char(NormalState *s)
{ {
int flags = nv_cmds[s->idx].cmd_flags; int flags = nv_cmds[s->idx].cmd_flags;
bool pending_op = s->oa.op_type != OP_NOP; bool pending_op = s->oa.op_type != OP_NOP;
@@ -1083,7 +1084,7 @@ static int normal_execute(VimState *state, int key)
} }
// Get an additional character if we need one. // Get an additional character if we need one.
if (normal_need_aditional_char(s)) { if (normal_need_additional_char(s)) {
normal_get_additional_char(s); normal_get_additional_char(s);
} }

View File

@@ -23,6 +23,7 @@
#include "nvim/main.h" #include "nvim/main.h"
#include "nvim/misc1.h" #include "nvim/misc1.h"
#include "nvim/state.h" #include "nvim/state.h"
#include "nvim/log.h"
#define READ_BUFFER_SIZE 0xfff #define READ_BUFFER_SIZE 0xfff
#define INPUT_BUFFER_SIZE (READ_BUFFER_SIZE * 4) #define INPUT_BUFFER_SIZE (READ_BUFFER_SIZE * 4)
@@ -38,6 +39,7 @@ static RBuffer *input_buffer = NULL;
static bool input_eof = false; static bool input_eof = false;
static int global_fd = 0; static int global_fd = 0;
static int events_enabled = 0; static int events_enabled = 0;
static bool blocking = false;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/input.c.generated.h" # include "os/input.c.generated.h"
@@ -327,13 +329,27 @@ static unsigned int handle_mouse_event(char **ptr, uint8_t *buf,
return bufsize; return bufsize;
} }
/// @return true if the main loop is blocked and waiting for input.
bool input_blocking(void)
{
return blocking;
}
static bool input_poll(int ms) static bool input_poll(int ms)
{ {
if (do_profiling == PROF_YES && ms) { if (do_profiling == PROF_YES && ms) {
prof_inchar_enter(); prof_inchar_enter();
} }
if ((ms == - 1 || ms > 0)
&& !(events_enabled || input_ready() || input_eof)
) {
blocking = true;
multiqueue_process_debug(main_loop.events);
multiqueue_process_events(main_loop.events);
}
LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, input_ready() || input_eof); LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, input_ready() || input_eof);
blocking = false;
if (do_profiling == PROF_YES && ms) { if (do_profiling == PROF_YES && ms) {
prof_inchar_exit(); prof_inchar_exit();

View File

@@ -98,3 +98,52 @@ int get_real_state(void)
return State; return State;
} }
/// @returns[allocated] mode string
char *get_mode(void)
{
char *buf = xcalloc(3, sizeof(char));
if (VIsual_active) {
if (VIsual_select) {
buf[0] = (char)(VIsual_mode + 's' - 'v');
} else {
buf[0] = (char)VIsual_mode;
}
} else if (State == HITRETURN || State == ASKMORE || State == SETWSIZE
|| State == CONFIRM) {
buf[0] = 'r';
if (State == ASKMORE) {
buf[1] = 'm';
} else if (State == CONFIRM) {
buf[1] = '?';
}
} else if (State == EXTERNCMD) {
buf[0] = '!';
} else if (State & INSERT) {
if (State & VREPLACE_FLAG) {
buf[0] = 'R';
buf[1] = 'v';
} else if (State & REPLACE_FLAG) {
buf[0] = 'R';
} else {
buf[0] = 'i';
}
} else if (State & CMDLINE) {
buf[0] = 'c';
if (exmode_active) {
buf[1] = 'v';
}
} else if (exmode_active) {
buf[0] = 'c';
buf[1] = 'e';
} else if (State & TERM_FOCUS) {
buf[0] = 't';
} else {
buf[0] = 'n';
if (finish_op) {
buf[1] = 'o';
}
}
return buf;
}

View File

@@ -74,9 +74,6 @@ typedef struct {
bool out_isatty; bool out_isatty;
SignalWatcher winch_handle, cont_handle; SignalWatcher winch_handle, cont_handle;
bool cont_received; bool cont_received;
// Event scheduled by the ui bridge. Since the main thread suspends until
// the event is handled, it is fine to use a single field instead of a queue
Event scheduled_event;
UGrid grid; UGrid grid;
kvec_t(Rect) invalid_regions; kvec_t(Rect) invalid_regions;
int out_fd; int out_fd;

View File

@@ -9,6 +9,7 @@ local funcs = helpers.funcs
local request = helpers.request local request = helpers.request
local meth_pcall = helpers.meth_pcall local meth_pcall = helpers.meth_pcall
local command = helpers.command local command = helpers.command
local wait = helpers.wait
describe('api', function() describe('api', function()
before_each(clear) before_each(clear)
@@ -221,6 +222,109 @@ describe('api', function()
end) end)
end) end)
local function appendfile(fname, text)
local file = io.open(fname, 'a')
file:write(text)
file:flush()
file:close()
end
describe('nvim_get_mode', function()
it("during normal-mode `g` returns blocking=true", function()
nvim("input", "o") -- add a line
eq({mode='i', blocking=false}, nvim("get_mode"))
nvim("input", [[<C-\><C-N>]])
eq(2, nvim("eval", "line('.')"))
eq({mode='n', blocking=false}, nvim("get_mode"))
nvim("input", "g")
eq({mode='n', blocking=true}, nvim("get_mode"))
nvim("input", "k") -- complete the operator
eq(1, nvim("eval", "line('.')")) -- verify the completed operator
eq({mode='n', blocking=false}, nvim("get_mode"))
end)
it("returns the correct result multiple consecutive times", function()
for _ = 1,5 do
eq({mode='n', blocking=false}, nvim("get_mode"))
end
nvim("input", "g")
for _ = 1,4 do
eq({mode='n', blocking=true}, nvim("get_mode"))
end
nvim("input", "g")
for _ = 1,7 do
eq({mode='n', blocking=false}, nvim("get_mode"))
end
end)
it("during normal-mode CTRL-W, returns blocking=true", function()
nvim("input", "<C-W>")
eq({mode='n', blocking=true}, nvim("get_mode"))
nvim("input", "s") -- complete the operator
eq(2, nvim("eval", "winnr('$')")) -- verify the completed operator
eq({mode='n', blocking=false}, nvim("get_mode"))
end)
it("during press-enter prompt returns blocking=true", function()
eq({mode='n', blocking=false}, nvim("get_mode"))
command("echom 'msg1'")
command("echom 'msg2'")
command("echom 'msg3'")
command("echom 'msg4'")
command("echom 'msg5'")
eq({mode='n', blocking=false}, nvim("get_mode"))
nvim("input", ":messages<CR>")
eq({mode='r', blocking=true}, nvim("get_mode"))
end)
it("during getchar() returns blocking=false", function()
nvim("input", ":let g:test_input = nr2char(getchar())<CR>")
-- Events are enabled during getchar(), RPC calls are *not* blocked. #5384
eq({mode='n', blocking=false}, nvim("get_mode"))
eq(0, nvim("eval", "exists('g:test_input')"))
nvim("input", "J")
eq("J", nvim("eval", "g:test_input"))
eq({mode='n', blocking=false}, nvim("get_mode"))
end)
-- TODO: bug #6247#issuecomment-286403810
it("batched with input", function()
eq({mode='n', blocking=false}, nvim("get_mode"))
command("echom 'msg1'")
command("echom 'msg2'")
command("echom 'msg3'")
command("echom 'msg4'")
command("echom 'msg5'")
local req = {
{'nvim_get_mode', {}},
{'nvim_input', {':messages<CR>'}},
{'nvim_get_mode', {}},
{'nvim_eval', {'1'}},
}
eq({{{mode='n', blocking=false},
13,
{mode='n', blocking=false}, -- TODO: should be blocked=true
1},
NIL}, meths.call_atomic(req))
eq({mode='r', blocking=true}, nvim("get_mode"))
end)
-- TODO: bug #6166
it("during insert-mode map-pending, returns blocking=true #6166", function()
command("inoremap xx foo")
nvim("input", "ix")
eq({mode='i', blocking=true}, nvim("get_mode"))
end)
-- TODO: bug #6166
it("during normal-mode gU, returns blocking=false #6166", function()
nvim("input", "gu")
eq({mode='no', blocking=false}, nvim("get_mode"))
end)
end)
describe('nvim_replace_termcodes', function() describe('nvim_replace_termcodes', function()
it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function() it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function()
eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true)) eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true))
@@ -459,7 +563,7 @@ describe('api', function()
eq(very_long_name, err:match('Ax+Z?')) eq(very_long_name, err:match('Ax+Z?'))
end) end)
it("doesn't leak memory on incorrect argument types", function() it("does not leak memory on incorrect argument types", function()
local status, err = pcall(nvim, 'set_current_dir',{'not', 'a', 'dir'}) local status, err = pcall(nvim, 'set_current_dir',{'not', 'a', 'dir'})
eq(false, status) eq(false, status)
ok(err:match(': Wrong type for argument 1, expecting String') ~= nil) ok(err:match(': Wrong type for argument 1, expecting String') ~= nil)

View File

@@ -385,9 +385,9 @@ local function curbuf(method, ...)
end end
local function wait() local function wait()
-- Execute 'vim_eval' (a deferred function) to block -- Execute 'nvim_eval' (a deferred function) to block
-- until all pending input is processed. -- until all pending input is processed.
session:request('vim_eval', '1') session:request('nvim_eval', '1')
end end
-- sleeps the test runner (_not_ the nvim instance) -- sleeps the test runner (_not_ the nvim instance)