Merge #6247 'api: nvim_get_mode()'

This commit is contained in:
Justin M. Keyes 2017-04-28 20:37:52 +02:00 committed by GitHub
commit 129f107c0c
23 changed files with 263 additions and 120 deletions

View File

@ -154,22 +154,17 @@ Interfaces ~
|if_cscop.txt| using Cscope with Vim
|if_pyth.txt| Python interface
|if_ruby.txt| Ruby interface
|debugger.txt| Interface with a debugger
|sign.txt| debugging signs
Versions ~
|vim_diff.txt| Main differences between Nvim and Vim
|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 plugins ~
|pi_gzip.txt| Reading and writing compressed files
|pi_netrw.txt| Reading and writing files over a network
|pi_paren.txt| Highlight matching parens
|pi_tar.txt| Tar file explorer
|pi_vimball.txt| Create a self-installing Vim script
|pi_zip.txt| Zip archive explorer
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:
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*
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.

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.
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
start of the message. For example, to get help on the message: >

View File

@ -27,6 +27,7 @@
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/option.h"
#include "nvim/state.h"
#include "nvim/syntax.h"
#include "nvim/getchar.h"
#include "nvim/os/input.h"
@ -701,6 +702,24 @@ 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();
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)
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)
{
char_u buf[3];
char *mode = get_mode();
buf[1] = NUL;
buf[2] = NUL;
if (VIsual_active) {
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
// non-empty string.
if (!non_zero_arg(&argvars[0])) {
mode[1] = NUL;
}
/* Clear out the minor mode when the argument is not a non-zero number or
* non-empty string. */
if (!non_zero_arg(&argvars[0]))
buf[1] = NUL;
rettv->vval.v_string = vim_strsave(buf);
rettv->vval.v_string = (char_u *)mode;
rettv->v_type = VAR_STRING;
}

View File

@ -8,16 +8,14 @@
typedef void (*argv_callback)(void **argv);
typedef struct message {
int priority;
argv_callback handler;
void *argv[EVENT_HANDLER_MAX_ARGC];
} Event;
typedef void(*event_scheduler)(Event event, void *data);
#define VA_EVENT_INIT(event, p, h, a) \
#define VA_EVENT_INIT(event, h, a) \
do { \
assert(a <= EVENT_HANDLER_MAX_ARGC); \
(event)->priority = p; \
(event)->handler = h; \
if (a) { \
va_list args; \
@ -29,11 +27,11 @@ typedef void(*event_scheduler)(Event event, void *data);
} \
} while (0)
static inline Event event_create(int priority, argv_callback cb, int argc, ...)
static inline Event event_create(argv_callback cb, int argc, ...)
{
assert(argc <= EVENT_HANDLER_MAX_ARGC);
Event event;
VA_EVENT_INIT(&event, priority, cb, argc);
VA_EVENT_INIT(&event, cb, argc);
return event;
}

View File

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

View File

@ -126,6 +126,7 @@ void multiqueue_free(MultiQueue *this)
xfree(this);
}
/// Removes the next item and returns its Event.
Event multiqueue_get(MultiQueue *this)
{
return multiqueue_empty(this) ? NILEVENT : multiqueue_remove(this);
@ -144,7 +145,7 @@ void multiqueue_process_events(MultiQueue *this)
{
assert(this);
while (!multiqueue_empty(this)) {
Event event = multiqueue_get(this);
Event event = multiqueue_remove(this);
if (event.handler) {
event.handler(event.argv);
}
@ -178,36 +179,48 @@ size_t multiqueue_size(MultiQueue *this)
return this->size;
}
/// Gets an Event from an item.
///
/// @param remove Remove the node from its queue, and free it.
static Event multiqueueitem_get_event(MultiQueueItem *item, bool remove)
{
assert(item != NULL);
Event ev;
if (item->link) {
// 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;
// remove the child node
if (remove) {
QUEUE_REMOVE(&child->node);
xfree(child);
}
} else {
// remove the corresponding link node in the parent queue
if (remove && item->data.item.parent_item) {
QUEUE_REMOVE(&item->data.item.parent_item->node);
xfree(item->data.item.parent_item);
item->data.item.parent_item = NULL;
}
ev = item->data.item.event;
}
return ev;
}
static Event multiqueue_remove(MultiQueue *this)
{
assert(!multiqueue_empty(this));
QUEUE *h = QUEUE_HEAD(&this->headtail);
QUEUE_REMOVE(h);
MultiQueueItem *item = multiqueue_node_data(h);
Event rv;
if (item->link) {
assert(!this->parent);
// remove 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));
QUEUE_REMOVE(&child->node);
rv = child->data.item.event;
xfree(child);
} else {
if (this->parent) {
// remove the corresponding link node in the parent queue
QUEUE_REMOVE(&item->data.item.parent_item->node);
xfree(item->data.item.parent_item);
}
rv = item->data.item.event;
}
assert(!item->link || !this->parent); // Only a parent queue has link-nodes
Event ev = multiqueueitem_get_event(item, true);
this->size--;
xfree(item);
return rv;
return ev;
}
static void multiqueue_push(MultiQueue *this, Event event)
@ -215,6 +228,7 @@ static void multiqueue_push(MultiQueue *this, Event event)
MultiQueueItem *item = xmalloc(sizeof(MultiQueueItem));
item->link = false;
item->data.item.event = event;
item->data.item.parent_item = NULL;
QUEUE_INSERT_TAIL(&this->headtail, &item->node);
if (this->parent) {
// push link node to the parent queue

View File

@ -10,7 +10,7 @@ typedef struct multiqueue MultiQueue;
typedef void (*put_callback)(MultiQueue *multiq, void *data);
#define multiqueue_put(q, h, ...) \
multiqueue_put_event(q, event_create(1, h, __VA_ARGS__));
multiqueue_put_event(q, event_create(h, __VA_ARGS__));
#ifdef INCLUDE_GENERATED_DECLARATIONS

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
/// 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 src
/// @param maxlen

View File

@ -604,7 +604,7 @@ void msg_schedule_emsgf(const char *const fmt, ...)
va_end(ap);
char *s = xstrdup((char *)IObuff);
loop_schedule(&main_loop, event_create(1, msg_emsgf_event, 1, s));
loop_schedule(&main_loop, event_create(msg_emsgf_event, 1, s));
}
/*

View File

@ -29,6 +29,7 @@
#include "nvim/log.h"
#include "nvim/misc1.h"
#include "nvim/lib/kvec.h"
#include "nvim/os/input.h"
#define CHANNEL_BUFFER_SIZE 0xffff
@ -89,6 +90,7 @@ static msgpack_sbuffer out_buffer;
/// Initializes the module
void channel_init(void)
{
ch_before_blocking_events = multiqueue_new_child(main_loop.events);
channels = pmap_new(uint64_t)();
event_strings = pmap_new(cstr_t)();
msgpack_sbuffer_init(&out_buffer);
@ -433,16 +435,25 @@ static void handle_request(Channel *channel, msgpack_object *request)
handler.async = true;
}
RequestEvent *event_data = xmalloc(sizeof(RequestEvent));
event_data->channel = channel;
event_data->handler = handler;
event_data->args = args;
event_data->request_id = request_id;
RequestEvent *evdata = xmalloc(sizeof(RequestEvent));
evdata->channel = channel;
evdata->handler = handler;
evdata->args = args;
evdata->request_id = request_id;
incref(channel);
if (handler.async) {
on_request_event((void **)&event_data);
bool is_get_mode = sizeof("nvim_get_mode") - 1 == method->via.bin.size
&& !strncmp("nvim_get_mode", method->via.bin.ptr, method->via.bin.size);
if (is_get_mode && !input_blocking()) {
// Defer the event to a special queue used by os/input.c. #6247
multiqueue_put(ch_before_blocking_events, on_request_event, 1, evdata);
} else {
// Invoke immediately.
on_request_event((void **)&evdata);
}
} else {
multiqueue_put(channel->events, on_request_event, 1, event_data);
multiqueue_put(channel->events, on_request_event, 1, evdata);
}
}

View File

@ -11,6 +11,11 @@
#define METHOD_MAXLEN 512
/// HACK: os/input.c drains this queue immediately before blocking for input.
/// Events on this queue are async-safe, but they need the resolved state
/// of os_inchar(), so they are processed "just-in-time".
MultiQueue *ch_before_blocking_events;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "msgpack_rpc/channel.h.generated.h"
#endif

View File

@ -76,7 +76,7 @@ typedef struct {
size_t idx;
} 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[out] arg Location where result of conversion will be saved.

View File

@ -541,7 +541,7 @@ static bool normal_handle_special_visual_command(NormalState *s)
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;
bool pending_op = s->oa.op_type != OP_NOP;
@ -1083,7 +1083,7 @@ static int normal_execute(VimState *state, int key)
}
// 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);
}

View File

@ -23,6 +23,7 @@
#include "nvim/main.h"
#include "nvim/misc1.h"
#include "nvim/state.h"
#include "nvim/msgpack_rpc/channel.h"
#define READ_BUFFER_SIZE 0xfff
#define INPUT_BUFFER_SIZE (READ_BUFFER_SIZE * 4)
@ -38,6 +39,7 @@ static RBuffer *input_buffer = NULL;
static bool input_eof = false;
static int global_fd = 0;
static int events_enabled = 0;
static bool blocking = false;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/input.c.generated.h"
@ -327,13 +329,25 @@ static unsigned int handle_mouse_event(char **ptr, uint8_t *buf,
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)
{
if (do_profiling == PROF_YES && ms) {
prof_inchar_enter();
}
if ((ms == - 1 || ms > 0) && !events_enabled && !input_eof) {
// The pending input provoked a blocking wait. Do special events now. #6247
blocking = true;
multiqueue_process_events(ch_before_blocking_events);
}
LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, input_ready() || input_eof);
blocking = false;
if (do_profiling == PROF_YES && ms) {
prof_inchar_exit();

View File

@ -98,3 +98,52 @@ int get_real_state(void)
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

@ -102,7 +102,7 @@ static void flush_input(TermInput *input, bool wait_until_empty)
size_t drain_boundary = wait_until_empty ? 0 : 0xff;
do {
uv_mutex_lock(&input->key_buffer_mutex);
loop_schedule(&main_loop, event_create(1, wait_input_enqueue, 1, input));
loop_schedule(&main_loop, event_create(wait_input_enqueue, 1, input));
input->waiting = true;
while (input->waiting) {
uv_cond_wait(&input->key_buffer_cond, &input->key_buffer_mutex);
@ -352,7 +352,7 @@ static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data,
stream_close(&input->read_stream, NULL, NULL);
multiqueue_put(input->loop->fast_events, restart_reading, 1, input);
} else {
loop_schedule(&main_loop, event_create(1, input_done_event, 0));
loop_schedule(&main_loop, event_create(input_done_event, 0));
}
return;
}

View File

@ -74,9 +74,6 @@ typedef struct {
bool out_isatty;
SignalWatcher winch_handle, cont_handle;
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;
kvec_t(Rect) invalid_regions;
int out_fd;
@ -776,7 +773,7 @@ static void tui_suspend(UI *ui)
// before continuing. This is done in another callback to avoid
// loop_poll_events recursion
multiqueue_put_event(data->loop->fast_events,
event_create(1, suspend_event, 1, ui));
event_create(suspend_event, 1, ui));
}
static void tui_set_title(UI *ui, char *title)

View File

@ -198,7 +198,7 @@ static void ui_refresh_event(void **argv)
void ui_schedule_refresh(void)
{
loop_schedule(&main_loop, event_create(1, ui_refresh_event, 0));
loop_schedule(&main_loop, event_create(ui_refresh_event, 0));
}
void ui_resize(int new_width, int new_height)

View File

@ -40,13 +40,13 @@ static argv_callback uilog_event = NULL;
uilog_event = ui_bridge_##name##_event; \
} \
((UIBridgeData *)ui)->scheduler( \
event_create(1, ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui)); \
event_create(ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui)); \
} while (0)
#else
// Schedule a function call on the UI bridge thread.
#define UI_CALL(ui, name, argc, ...) \
((UIBridgeData *)ui)->scheduler( \
event_create(1, ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui))
event_create(ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui))
#endif
#define INT2PTR(i) ((void *)(uintptr_t)i)

View File

@ -221,6 +221,102 @@ describe('api', function()
end)
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()
it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function()
eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true))
@ -459,7 +555,7 @@ describe('api', function()
eq(very_long_name, err:match('Ax+Z?'))
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'})
eq(false, status)
ok(err:match(': Wrong type for argument 1, expecting String') ~= nil)

View File

@ -385,9 +385,9 @@ local function curbuf(method, ...)
end
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.
session:request('vim_eval', '1')
session:request('nvim_eval', '1')
end
-- sleeps the test runner (_not_ the nvim instance)