Merge pull request #1446 from splinterofchaos/obj

Allow the execution of msgpack notifications and extend vimL lightly.
This commit is contained in:
Scott Prager 2015-04-13 10:25:14 -04:00
commit d60ae3159e
7 changed files with 166 additions and 38 deletions

View File

@ -116,6 +116,12 @@ functions can be called interactively:
>>> nvim = attach('socket', path='[address]') >>> nvim = attach('socket', path='[address]')
>>> nvim.command('echo "hello world!"') >>> nvim.command('echo "hello world!"')
< <
One can also spawn and connect to an embedded nvim instance via |rpcstart()|
>
let vim = rpcstart('nvim', ['--embed'])
echo rpcrequest(vim, 'vim_eval', '"Hello " . "world!"')
call rpcstop(vim)
<
============================================================================== ==============================================================================
4. Implementing new clients *msgpack-rpc-clients* 4. Implementing new clients *msgpack-rpc-clients*
@ -177,6 +183,10 @@ Buffer -> enum value kObjectTypeBuffer
Window -> enum value kObjectTypeWindow Window -> enum value kObjectTypeWindow
Tabpage -> enum value kObjectTypeTabpage Tabpage -> enum value kObjectTypeTabpage
An API method expecting one of these types may be passed an integer instead,
although they are not interchangeable. For example, a Buffer may be passed as
an integer, but not a Window or Tabpage.
The most reliable way of determining the type codes for the special nvim types The most reliable way of determining the type codes for the special nvim types
is at runtime by inspecting the `types` key of metadata dictionary returned by is at runtime by inspecting the `types` key of metadata dictionary returned by
`vim_get_api_info` method. Here's an example json representation of the `vim_get_api_info` method. Here's an example json representation of the
@ -216,7 +226,7 @@ that makes this task easier:
- Methods that operate instances of Nvim's types are prefixed with the type - Methods that operate instances of Nvim's types are prefixed with the type
name in lower case, e.g. `buffer_get_line` represents the `get_line` method name in lower case, e.g. `buffer_get_line` represents the `get_line` method
of a Buffer instance. of a Buffer instance.
- Global methods are prefixed with `vim`, e.g. `vim_list_buffers`. - Global methods are prefixed with `vim`, e.g. `vim_get_buffers`.
So, for an object-oriented language, a client library would have the classes So, for an object-oriented language, a client library would have the classes
that represent Nvim's types, and the methods of each class could be defined that represent Nvim's types, and the methods of each class could be defined

View File

@ -183,13 +183,20 @@ for i = 1, #functions do
local converted, convert_arg, param, arg local converted, convert_arg, param, arg
param = fn.parameters[j] param = fn.parameters[j]
converted = 'arg_'..j converted = 'arg_'..j
if real_type(param[1]) ~= 'Object' then local rt = real_type(param[1])
output:write('\n if (args.items['..(j - 1)..'].type != kObjectType'..real_type(param[1])..') {') if rt ~= 'Object' then
output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..') {')
output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..rt:lower()..';')
if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') or rt:match('^Boolean$') then
-- accept positive integers for Buffers, Windows and Tabpages
output:write('\n } else if (args.items['..(j - 1)..'].type == kObjectTypeInteger && args.items['..(j - 1)..'].data.integer > 0) {')
output:write('\n '..converted..' = (unsigned)args.items['..(j - 1)..'].data.integer;')
end
output:write('\n } else {')
output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong type for argument '..j..', expecting '..param[1]..'");') output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong type for argument '..j..', expecting '..param[1]..'");')
output:write('\n error->set = true;') output:write('\n error->set = true;')
output:write('\n goto cleanup;') output:write('\n goto cleanup;')
output:write('\n }') output:write('\n }\n')
output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..real_type(param[1]):lower()..';\n')
else else
output:write('\n '..converted..' = args.items['..(j - 1)..'];\n') output:write('\n '..converted..' = args.items['..(j - 1)..'];\n')
end end

View File

@ -22,6 +22,15 @@ typedef enum {
kErrorTypeValidation kErrorTypeValidation
} ErrorType; } ErrorType;
typedef enum {
kMessageTypeRequest,
kMessageTypeResponse,
kMessageTypeNotification
} MessageType;
/// Used as the message ID of notifications.
#define NO_RESPONSE UINT64_MAX
typedef struct { typedef struct {
ErrorType type; ErrorType type;
char msg[1024]; char msg[1024];

View File

@ -405,6 +405,9 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err)
tv->vval.v_number = obj.data.boolean; tv->vval.v_number = obj.data.boolean;
break; break;
case kObjectTypeBuffer:
case kObjectTypeWindow:
case kObjectTypeTabpage:
case kObjectTypeInteger: case kObjectTypeInteger:
if (obj.data.integer > INT_MAX || obj.data.integer < INT_MIN) { if (obj.data.integer > INT_MAX || obj.data.integer < INT_MIN) {
api_set_error(err, Validation, _("Integer value outside range")); api_set_error(err, Validation, _("Integer value outside range"));

View File

@ -225,7 +225,25 @@ Object channel_send_call(uint64_t id,
channel->pending_requests--; channel->pending_requests--;
if (frame.errored) { if (frame.errored) {
api_set_error(err, Exception, "%s", frame.result.data.string.data); if (frame.result.type == kObjectTypeString) {
api_set_error(err, Exception, "%s", frame.result.data.string.data);
} else if (frame.result.type == kObjectTypeArray) {
// Should be an error in the form [type, message]
Array array = frame.result.data.array;
if (array.size == 2 && array.items[0].type == kObjectTypeInteger
&& (array.items[0].data.integer == kErrorTypeException
|| array.items[0].data.integer == kErrorTypeValidation)
&& array.items[1].type == kObjectTypeString) {
err->type = (ErrorType) array.items[0].data.integer;
xstrlcpy(err->msg, array.items[1].data.string.data, sizeof(err->msg));
err->set = true;
} else {
api_set_error(err, Exception, "%s", "unknown error");
}
} else {
api_set_error(err, Exception, "%s", "unknown error");
}
api_free_object(frame.result); api_free_object(frame.result);
} }
@ -435,18 +453,18 @@ static void handle_request(Channel *channel, msgpack_object *request)
// Retrieve the request handler // Retrieve the request handler
MsgpackRpcRequestHandler handler; MsgpackRpcRequestHandler handler;
msgpack_object method = request->via.array.ptr[2]; msgpack_object *method = msgpack_rpc_method(request);
if (method.type == MSGPACK_OBJECT_BIN || method.type == MSGPACK_OBJECT_STR) { if (method) {
handler = msgpack_rpc_get_handler_for(method.via.bin.ptr, handler = msgpack_rpc_get_handler_for(method->via.bin.ptr,
method.via.bin.size); method->via.bin.size);
} else { } else {
handler.fn = msgpack_rpc_handle_missing_method; handler.fn = msgpack_rpc_handle_missing_method;
handler.defer = false; handler.defer = false;
} }
Array args = ARRAY_DICT_INIT; Array args = ARRAY_DICT_INIT;
msgpack_rpc_to_array(request->via.array.ptr + 3, &args); msgpack_rpc_to_array(msgpack_rpc_args(request), &args);
bool defer = (!kv_size(channel->call_stack) && handler.defer); bool defer = (!kv_size(channel->call_stack) && handler.defer);
RequestEvent *event_data = xmalloc(sizeof(RequestEvent)); RequestEvent *event_data = xmalloc(sizeof(RequestEvent));
event_data->channel = channel; event_data->channel = channel;
@ -469,14 +487,18 @@ static void on_request_event(Event event)
uint64_t request_id = e->request_id; uint64_t request_id = e->request_id;
Error error = ERROR_INIT; Error error = ERROR_INIT;
Object result = handler.fn(channel->id, request_id, args, &error); Object result = handler.fn(channel->id, request_id, args, &error);
// send the response if (request_id != NO_RESPONSE) {
msgpack_packer response; // send the response
msgpack_packer_init(&response, &out_buffer, msgpack_sbuffer_write); msgpack_packer response;
channel_write(channel, serialize_response(channel->id, msgpack_packer_init(&response, &out_buffer, msgpack_sbuffer_write);
request_id, channel_write(channel, serialize_response(channel->id,
&error, request_id,
result, &error,
&out_buffer)); result,
&out_buffer));
} else {
api_free_object(result);
}
// All arguments were freed already, but we still need to free the array // All arguments were freed already, but we still need to free the array
xfree(args.items); xfree(args.items);
decref(channel); decref(channel);

View File

@ -351,49 +351,86 @@ void msgpack_rpc_serialize_response(uint64_t response_id,
} }
} }
static bool msgpack_rpc_is_notification(msgpack_object *req)
{
return req->via.array.ptr[0].via.u64 == 2;
}
msgpack_object *msgpack_rpc_method(msgpack_object *req)
{
msgpack_object *obj = req->via.array.ptr
+ (msgpack_rpc_is_notification(req) ? 1 : 2);
return obj->type == MSGPACK_OBJECT_STR || obj->type == MSGPACK_OBJECT_BIN ?
obj : NULL;
}
msgpack_object *msgpack_rpc_args(msgpack_object *req)
{
msgpack_object *obj = req->via.array.ptr
+ (msgpack_rpc_is_notification(req) ? 2 : 3);
return obj->type == MSGPACK_OBJECT_ARRAY ? obj : NULL;
}
static msgpack_object *msgpack_rpc_msg_id(msgpack_object *req)
{
if (msgpack_rpc_is_notification(req)) {
return NULL;
}
msgpack_object *obj = &req->via.array.ptr[1];
return obj->type == MSGPACK_OBJECT_POSITIVE_INTEGER ? obj : NULL;
}
void msgpack_rpc_validate(uint64_t *response_id, void msgpack_rpc_validate(uint64_t *response_id,
msgpack_object *req, msgpack_object *req,
Error *err) Error *err)
{ {
// response id not known yet // response id not known yet
*response_id = 0; *response_id = NO_RESPONSE;
// Validate the basic structure of the msgpack-rpc payload // Validate the basic structure of the msgpack-rpc payload
if (req->type != MSGPACK_OBJECT_ARRAY) { if (req->type != MSGPACK_OBJECT_ARRAY) {
api_set_error(err, Validation, _("Request is not an array")); api_set_error(err, Validation, _("Message is not an array"));
return; return;
} }
if (req->via.array.size != 4) { if (req->via.array.size == 0) {
api_set_error(err, Validation, _("Request array size should be 4")); api_set_error(err, Validation, _("Message is empty"));
return; return;
} }
if (req->via.array.ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER) {
api_set_error(err, Validation, _("Id must be a positive integer"));
return;
}
// Set the response id, which is the same as the request
*response_id = req->via.array.ptr[1].via.u64;
if (req->via.array.ptr[0].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { if (req->via.array.ptr[0].type != MSGPACK_OBJECT_POSITIVE_INTEGER) {
api_set_error(err, Validation, _("Message type must be an integer")); api_set_error(err, Validation, _("Message type must be an integer"));
return; return;
} }
if (req->via.array.ptr[0].via.u64 != 0) { uint64_t type = req->via.array.ptr[0].via.u64;
api_set_error(err, Validation, _("Message type must be 0")); if (type != kMessageTypeRequest && type != kMessageTypeNotification) {
api_set_error(err, Validation, _("Unknown message type"));
return; return;
} }
if (req->via.array.ptr[2].type != MSGPACK_OBJECT_BIN if ((type == kMessageTypeRequest && req->via.array.size != 4) ||
&& req->via.array.ptr[2].type != MSGPACK_OBJECT_STR) { (type == kMessageTypeNotification && req->via.array.size != 3)) {
api_set_error(err, Validation, _("Request array size should be 4 (request) "
"or 3 (notification)"));
return;
}
if (type == kMessageTypeRequest) {
msgpack_object *id_obj = msgpack_rpc_msg_id(req);
if (!id_obj) {
api_set_error(err, Validation, _("ID must be a positive integer"));
return;
}
*response_id = id_obj->via.u64;
}
if (!msgpack_rpc_method(req)) {
api_set_error(err, Validation, _("Method must be a string")); api_set_error(err, Validation, _("Method must be a string"));
return; return;
} }
if (req->via.array.ptr[3].type != MSGPACK_OBJECT_ARRAY) { if (!msgpack_rpc_args(req)) {
api_set_error(err, Validation, _("Parameters must be an array")); api_set_error(err, Validation, _("Parameters must be an array"));
return; return;
} }

View File

@ -3,8 +3,8 @@
-- be running. -- be running.
local helpers = require('test.functional.helpers') local helpers = require('test.functional.helpers')
local clear, nvim, eval = helpers.clear, helpers.nvim, helpers.eval local clear, nvim, eval = helpers.clear, helpers.nvim, helpers.eval
local eq, run, stop = helpers.eq, helpers.run, helpers.stop local eq, neq, run, stop = helpers.eq, helpers.neq, helpers.run, helpers.stop
local nvim_prog = helpers.nvim_prog
describe('server -> client', function() describe('server -> client', function()
@ -115,4 +115,44 @@ describe('server -> client', function()
eq(expected, notified) eq(expected, notified)
end) end)
end) end)
describe('when the client is a recursive vim instance', function()
before_each(function()
nvim('command', "let vim = rpcstart('"..nvim_prog.."', ['--embed'])")
neq(0, eval('vim'))
end)
after_each(function() nvim('command', 'call rpcstop(vim)') end)
it('can send/recieve notifications and make requests', function()
nvim('command', "call rpcnotify(vim, 'vim_set_current_line', 'SOME TEXT')")
-- Wait for the notification to complete.
nvim('command', "call rpcrequest(vim, 'vim_eval', '0')")
eq('SOME TEXT', eval("rpcrequest(vim, 'vim_get_current_line')"))
end)
it('can communicate buffers, tabpages, and windows', function()
eq({3}, eval("rpcrequest(vim, 'vim_get_tabpages')"))
eq({1}, eval("rpcrequest(vim, 'vim_get_windows')"))
local buf = eval("rpcrequest(vim, 'vim_get_buffers')")[1]
eq(2, buf)
eval("rpcnotify(vim, 'buffer_set_line', "..buf..", 0, 'SOME TEXT')")
nvim('command', "call rpcrequest(vim, 'vim_eval', '0')") -- wait
eq('SOME TEXT', eval("rpcrequest(vim, 'buffer_get_line', "..buf..", 0)"))
-- Call get_line_slice(buf, range [0,0], includes start, includes end)
eq({'SOME TEXT'}, eval("rpcrequest(vim, 'buffer_get_line_slice', "..buf..", 0, 0, 1, 1)"))
end)
it('returns an error if the request failed', function()
local status, err = pcall(eval, "rpcrequest(vim, 'does-not-exist')")
eq(false, status)
eq(true, string.match(err, ': (.*)') == 'Failed to evaluate expression')
end)
end)
end) end)