diff options
-rw-r--r-- | scripts/gendispatch.lua | 2 | ||||
-rw-r--r-- | src/nvim/api/buffer.c | 4 | ||||
-rw-r--r-- | src/nvim/api/private/defs.h | 4 | ||||
-rw-r--r-- | src/nvim/api/private/dispatch.h | 1 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 89 | ||||
-rw-r--r-- | src/nvim/eval.c | 2 | ||||
-rw-r--r-- | src/nvim/msgpack_rpc/channel.c | 2 | ||||
-rw-r--r-- | src/nvim/msgpack_rpc/helpers.c | 2 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 88 |
9 files changed, 184 insertions, 10 deletions
diff --git a/scripts/gendispatch.lua b/scripts/gendispatch.lua index 4f00783825..94789e1ef0 100644 --- a/scripts/gendispatch.lua +++ b/scripts/gendispatch.lua @@ -209,7 +209,7 @@ for i = 1, #functions do if fn.impl_name == nil then local args = {} - output:write('Object handle_'..fn.name..'(uint64_t channel_id, uint64_t request_id, Array args, Error *error)') + output:write('Object handle_'..fn.name..'(uint64_t channel_id, Array args, Error *error)') output:write('\n{') output:write('\n Object ret = NIL;') -- Declare/initialize variables that will hold converted arguments diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index c4415ddf94..09d717bff6 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -191,7 +191,7 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, Object str = STRING_OBJ(cstr_to_string(bufstr)); // Vim represents NULs as NLs, but this may confuse clients. - if (channel_id != INVALID_CHANNEL) { + if (channel_id != INTERNAL_CALL) { strchrsub(str.data.string.data, '\n', '\0'); } @@ -312,7 +312,7 @@ void nvim_buf_set_lines(uint64_t channel_id, // line and convert NULs to newlines to avoid truncation. lines[i] = xmallocz(l.size); for (size_t j = 0; j < l.size; j++) { - if (l.data[j] == '\n' && channel_id != INVALID_CHANNEL) { + if (l.data[j] == '\n' && channel_id != INTERNAL_CALL) { api_set_error(err, Exception, _("string cannot contain newlines")); new_len = i + 1; goto end; diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h index a6710193ff..1d5ecd3071 100644 --- a/src/nvim/api/private/defs.h +++ b/src/nvim/api/private/defs.h @@ -33,8 +33,8 @@ typedef enum { /// Used as the message ID of notifications. #define NO_RESPONSE UINT64_MAX -/// Used as channel_id when the call is local -#define INVALID_CHANNEL UINT64_MAX +/// Used as channel_id when the call is local. +#define INTERNAL_CALL UINT64_MAX typedef struct { ErrorType type; diff --git a/src/nvim/api/private/dispatch.h b/src/nvim/api/private/dispatch.h index c12cf9e698..39aabd708a 100644 --- a/src/nvim/api/private/dispatch.h +++ b/src/nvim/api/private/dispatch.h @@ -4,7 +4,6 @@ #include "nvim/api/private/defs.h" typedef Object (*ApiDispatchWrapper)(uint64_t channel_id, - uint64_t request_id, Array args, Error *error); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index d8cdad961b..9f3a84c88b 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -663,6 +663,95 @@ Array nvim_get_api_info(uint64_t channel_id) return rv; } +/// Call many api methods atomically +/// +/// This has two main usages: Firstly, to perform several requests from an +/// async context atomically, i.e. without processing requests from other rpc +/// clients or redrawing or allowing user interaction in between. Note that api +/// methods that could fire autocommands or do event processing still might do +/// so. For instance invoking the :sleep command might call timer callbacks. +/// Secondly, it can be used to reduce rpc overhead (roundtrips) when doing +/// many requests in sequence. +/// +/// @param calls an array of calls, where each call is described by an array +/// with two elements: the request name, and an array of arguments. +/// @param[out] err Details of a validation error of the nvim_multi_request call +/// itself, i e malformatted `calls` parameter. Errors from called methods will +/// be indicated in the return value, see below. +/// +/// @return an array with two elements. The first is an array of return +/// values. The second is NIL if all calls succeeded. If a call resulted in +/// an error, it is a three-element array with the zero-based index of the call +/// which resulted in an error, the error type and the error message. If an +/// error ocurred, the values from all preceding calls will still be returned. +Array nvim_call_atomic(uint64_t channel_id, Array calls, Error *err) + FUNC_API_NOEVAL +{ + Array rv = ARRAY_DICT_INIT; + Array results = ARRAY_DICT_INIT; + Error nested_error = ERROR_INIT; + + size_t i; // also used for freeing the variables + for (i = 0; i < calls.size; i++) { + if (calls.items[i].type != kObjectTypeArray) { + api_set_error(err, + Validation, + _("All items in calls array must be arrays")); + goto validation_error; + } + Array call = calls.items[i].data.array; + if (call.size != 2) { + api_set_error(err, + Validation, + _("All items in calls array must be arrays of size 2")); + goto validation_error; + } + + if (call.items[0].type != kObjectTypeString) { + api_set_error(err, + Validation, + _("name must be String")); + goto validation_error; + } + String name = call.items[0].data.string; + + if (call.items[1].type != kObjectTypeArray) { + api_set_error(err, + Validation, + _("args must be Array")); + goto validation_error; + } + Array args = call.items[1].data.array; + + MsgpackRpcRequestHandler handler = msgpack_rpc_get_handler_for(name.data, + name.size); + Object result = handler.fn(channel_id, args, &nested_error); + if (nested_error.set) { + // error handled after loop + break; + } + + ADD(results, result); + } + + ADD(rv, ARRAY_OBJ(results)); + if (nested_error.set) { + Array errval = ARRAY_DICT_INIT; + ADD(errval, INTEGER_OBJ((Integer)i)); + ADD(errval, INTEGER_OBJ(nested_error.type)); + ADD(errval, STRING_OBJ(cstr_to_string(nested_error.msg))); + ADD(rv, ARRAY_OBJ(errval)); + } else { + ADD(rv, NIL); + } + return rv; + +validation_error: + api_free_array(results); + return rv; +} + + /// Writes a message to vim output or error buffer. The string is split /// and flushed after each newline. Incomplete lines are kept for writing /// later. diff --git a/src/nvim/eval.c b/src/nvim/eval.c index dce2f32707..76f33e7d8c 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -7133,7 +7133,7 @@ static void api_wrapper(typval_T *argvars, typval_T *rettv, FunPtr fptr) } Error err = ERROR_INIT; - Object result = fn(INVALID_CHANNEL, NO_RESPONSE, args, &err); + Object result = fn(INTERNAL_CALL, args, &err); if (err.set) { nvim_err_writeln(cstr_as_string(err.msg)); diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index fef1d08db7..98636263b9 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -452,7 +452,7 @@ static void on_request_event(void **argv) Array args = e->args; uint64_t request_id = e->request_id; Error error = ERROR_INIT; - Object result = handler.fn(channel->id, request_id, args, &error); + Object result = handler.fn(channel->id, args, &error); if (request_id != NO_RESPONSE) { // send the response msgpack_packer response; diff --git a/src/nvim/msgpack_rpc/helpers.c b/src/nvim/msgpack_rpc/helpers.c index 33e9bb1c07..14e1c2d978 100644 --- a/src/nvim/msgpack_rpc/helpers.c +++ b/src/nvim/msgpack_rpc/helpers.c @@ -455,7 +455,6 @@ void msgpack_rpc_from_dictionary(Dictionary result, msgpack_packer *res) /// Handler executed when an invalid method name is passed Object msgpack_rpc_handle_missing_method(uint64_t channel_id, - uint64_t request_id, Array args, Error *error) { @@ -466,7 +465,6 @@ Object msgpack_rpc_handle_missing_method(uint64_t channel_id, /// Handler executed when malformated arguments are passed Object msgpack_rpc_handle_invalid_arguments(uint64_t channel_id, - uint64_t request_id, Array args, Error *error) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 6b30c0e81c..724d9f1b57 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -298,6 +298,94 @@ describe('vim_* functions', function() end) end) + describe('call_atomic', function() + it('works', function() + meths.buf_set_lines(0, 0, -1, true, {'first'}) + local req = { + {'nvim_get_current_line', {}}, + {'nvim_set_current_line', {'second'}}, + } + eq({{'first', NIL}, NIL}, meths.call_atomic(req)) + eq({'second'}, meths.buf_get_lines(0, 0, -1, true)) + end) + + it('allows multiple return values', function() + local req = { + {'nvim_set_var', {'avar', true}}, + {'nvim_set_var', {'bvar', 'string'}}, + {'nvim_get_var', {'avar'}}, + {'nvim_get_var', {'bvar'}}, + } + eq({{NIL, NIL, true, 'string'}, NIL}, meths.call_atomic(req)) + end) + + it('is aborted by errors in call', function() + local error_types = meths.get_api_info()[2].error_types + local req = { + {'nvim_set_var', {'one', 1}}, + {'nvim_buf_set_lines', {}}, + {'nvim_set_var', {'two', 2}}, + } + eq({{NIL}, {1, error_types.Exception.id, + 'Wrong number of arguments: expecting 5 but got 0'}}, + meths.call_atomic(req)) + eq(1, meths.get_var('one')) + eq(false, pcall(meths.get_var, 'two')) + + -- still returns all previous successful calls + req = { + {'nvim_set_var', {'avar', 5}}, + {'nvim_set_var', {'bvar', 'string'}}, + {'nvim_get_var', {'avar'}}, + {'nvim_buf_get_lines', {0, 10, 20, true}}, + {'nvim_get_var', {'bvar'}}, + } + eq({{NIL, NIL, 5}, {3, error_types.Validation.id, 'Index out of bounds'}}, + meths.call_atomic(req)) + + req = { + {'i_am_not_a_method', {'xx'}}, + {'nvim_set_var', {'avar', 10}}, + } + eq({{}, {0, error_types.Exception.id, 'Invalid method name'}}, + meths.call_atomic(req)) + eq(5, meths.get_var('avar')) + end) + + it('throws error on malformated arguments', function() + local req = { + {'nvim_set_var', {'avar', 1}}, + {'nvim_set_var'}, + {'nvim_set_var', {'avar', 2}}, + } + local status, err = pcall(meths.call_atomic, req) + eq(false, status) + ok(err:match(' All items in calls array must be arrays of size 2') ~= nil) + -- call before was done, but not after + eq(1, meths.get_var('avar')) + + req = { + {'nvim_set_var', {'bvar', {2,3}}}, + 12, + } + status, err = pcall(meths.call_atomic, req) + eq(false, status) + ok(err:match('All items in calls array must be arrays') ~= nil) + eq({2,3}, meths.get_var('bvar')) + + req = { + {'nvim_set_current_line', 'little line'}, + {'nvim_set_var', {'avar', 3}}, + } + status, err = pcall(meths.call_atomic, req) + eq(false, status) + ok(err:match('args must be Array') ~= nil) + -- call before was done, but not after + eq(1, meths.get_var('avar')) + eq({''}, meths.buf_get_lines(0, 0, -1, true)) + end) + end) + it('can throw exceptions', function() local status, err = pcall(nvim, 'get_option', 'invalid-option') eq(false, status) |