From 0771672b18c6645a7fa4de61ac106bdf3b69a04a Mon Sep 17 00:00:00 2001 From: Jean Guyomarc'h Date: Sat, 12 Jan 2019 08:43:01 +0100 Subject: [PATCH] nvim: handle requests initiates by neovim Neovim is able to initiate requests to the UI client (via the 'rpcrequest()') API. Eovim is now able to run a user-defined callback function when a request is emitted. A request response is sent back to neovim. This is one step to solve #38. --- CMakeLists.txt | 1 + include/eovim/nvim.h | 11 +++ include/eovim/nvim_request.h | 52 ++++++++++++++ src/main.c | 2 + src/nvim.c | 72 +++++++++++++++++++- src/nvim_api.c | 18 ++--- src/nvim_request.c | 127 +++++++++++++++++++++++++++++++++++ 7 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 include/eovim/nvim_request.h create mode 100644 src/nvim_request.c diff --git a/CMakeLists.txt b/CMakeLists.txt index adf75dc..cfc6dbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,6 +115,7 @@ add_executable(eovim "${SRC_DIR}/event/cmdline.c" "${SRC_DIR}/nvim_api.c" "${SRC_DIR}/nvim_helper.c" + "${SRC_DIR}/nvim_request.c" "${SRC_DIR}/plugin.c" "${SRC_DIR}/options.c" "${SRC_DIR}/contrib.c" diff --git a/include/eovim/nvim.h b/include/eovim/nvim.h index 13c77ab..f27e3f4 100644 --- a/include/eovim/nvim.h +++ b/include/eovim/nvim.h @@ -52,6 +52,8 @@ struct nvim Eina_List *requests; msgpack_unpacker unpacker; + + /* The following msgpack structures must be handled on the main loop only */ msgpack_sbuffer sbuffer; msgpack_packer packer; uint32_t request_id; @@ -76,4 +78,13 @@ void nvim_mouse_enabled_set(s_nvim *nvim, Eina_Bool enable); Eina_Bool nvim_mouse_enabled_get(const s_nvim *nvim); Eina_Stringshare *nvim_eovimrc_path_get(const s_nvim *nvim); +/** + * Flush the msgpack buffer to the neovim instance, by writing to its standard + * input + * + * @param[in] nvim The neovim handle + * @return EINA_TRUE on success, EINA_FALSE on failure. + */ +Eina_Bool nvim_flush(s_nvim *nvim); + #endif /* ! __EOVIM_NVIM_H__ */ diff --git a/include/eovim/nvim_request.h b/include/eovim/nvim_request.h new file mode 100644 index 0000000..68de980 --- /dev/null +++ b/include/eovim/nvim_request.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 Jean Guyomarc'h + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef EOVIM_NVIM_REQUEST_H__ +#define EOVIM_NVIM_REQUEST_H__ + +#include "eovim/types.h" + +/** + * Callback signature used when replying to a request. + * + * @param[in] nvim The neovim handle + * @param[in] args Array of arguments from the request + * @param[in,out] pk Msgpack packer to be used to write the error and the + * result of the request. See msgpack-rpc. + * @return EINA_TRUE on success, EINA_FALSE on failure + * + * @note This function should not call nvim_flush(). It is automatically handled. + */ +typedef Eina_Bool (*f_nvim_request_cb)(s_nvim *nvim, const msgpack_object_array *args, + msgpack_packer *pk); + +Eina_Bool nvim_request_init(void); +void nvim_request_shutdown(void); + +Eina_Bool nvim_request_add(const char *request_name, f_nvim_request_cb func); +void nvim_request_del(const char *request_name); + +Eina_Bool +nvim_request_process(s_nvim *nvim, Eina_Stringshare *request, + const msgpack_object_array *args, uint32_t req_id); + +#endif /* ! EOVIM_NVIM_REQUEST_H__ */ diff --git a/src/main.c b/src/main.c index 2708186..4beb699 100644 --- a/src/main.c +++ b/src/main.c @@ -24,6 +24,7 @@ #include "eovim/config.h" #include "eovim/nvim.h" #include "eovim/nvim_api.h" +#include "eovim/nvim_request.h" #include "eovim/nvim_event.h" #include "eovim/termview.h" #include "eovim/main.h" @@ -53,6 +54,7 @@ static const s_module _modules[] = MODULE(config), MODULE(keymap), MODULE(nvim_api), + MODULE(nvim_request), MODULE(nvim_event), MODULE(plugin), MODULE(prefs), diff --git a/src/nvim.c b/src/nvim.c index 6c9d18f..83fdee4 100644 --- a/src/nvim.c +++ b/src/nvim.c @@ -26,6 +26,7 @@ #include "eovim/config.h" #include "eovim/nvim_api.h" #include "eovim/nvim_event.h" +#include "eovim/nvim_request.h" #include "eovim/nvim_helper.h" #include "eovim/log.h" #include "eovim/main.h" @@ -53,6 +54,51 @@ _nvim_get(void) return _nvim_instance; } +static Eina_Bool +_handle_request(s_nvim *nvim, const msgpack_object_array *args) +{ + /* Retrieve the request identifier ****************************************/ + if (EINA_UNLIKELY(args->ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER)) + { + ERR("Second argument in request is expected to be an integer"); + return EINA_FALSE; + } + const uint64_t long_req_id = args->ptr[1].via.u64; + if (EINA_UNLIKELY(long_req_id > UINT32_MAX)) + { + ERR("Request ID '%" PRIu64 " is too big", long_req_id); + return EINA_FALSE; + } + const uint32_t req_id = (uint32_t)long_req_id; + + /* Retrieve the request arguments *****************************************/ + if (EINA_UNLIKELY(args->ptr[3].type != MSGPACK_OBJECT_ARRAY)) + { + ERR("Fourth argument in request is expected to be an array"); + return EINA_FALSE; + } + const msgpack_object_array *const req_args = &(args->ptr[3].via.array); + + /* Retrieve the request name **********************************************/ + if (EINA_UNLIKELY(args->ptr[2].type != MSGPACK_OBJECT_STR)) + { + ERR("Third argument in request is expected to be a string"); + return EINA_FALSE; + } + const msgpack_object_str *const str = &(args->ptr[2].via.str); + Eina_Stringshare *const request = + eina_stringshare_add_length(str->ptr, str->size); + if (EINA_UNLIKELY(! request)) + { + ERR("Failed to create stringshare"); + return EINA_FALSE; + } + + const Eina_Bool ok = nvim_request_process(nvim, request, req_args, req_id); + eina_stringshare_del(request); + return ok; +} + static Eina_Bool _handle_request_response(s_nvim *nvim, const msgpack_object_array *args) @@ -289,6 +335,7 @@ _nvim_received_data_cb(void *data EINA_UNUSED, int type EINA_UNUSED, void *event) { + /* See https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md */ const Ecore_Exe_Event_Data *const info = event; s_nvim *const nvim = _nvim_get(); msgpack_unpacker *const unpacker = &nvim->unpacker; @@ -356,11 +403,15 @@ _nvim_received_data_cb(void *data EINA_UNUSED, } switch (args->ptr[0].via.u64) { - case 1: + case 0: /* msgpack-rpc request */ + _handle_request(nvim, args); + break; + + case 1: /* msgpack-rpc response */ _handle_request_response(nvim, args); break; - case 2: + case 2: /* msgpack-rpc notification */ _handle_notification(nvim, args); break; @@ -824,6 +875,23 @@ nvim_free(s_nvim *nvim) } } +Eina_Bool nvim_flush(s_nvim *nvim) +{ + /* Send the data present in the msgpack buffer */ + const Eina_Bool ok = + ecore_exe_send(nvim->exe, nvim->sbuffer.data, (int)nvim->sbuffer.size); + + /* Now that the data is gone (hopefully), clear the buffer */ + msgpack_sbuffer_clear(&nvim->sbuffer); + if (EINA_UNLIKELY(! ok)) + { + CRI("Failed to send %zu bytes to neovim", nvim->sbuffer.size); + return EINA_FALSE; + } + DBG("Sent %zu bytes to neovim", nvim->sbuffer.size); + return EINA_TRUE; +} + void nvim_mouse_enabled_set(s_nvim *nvim, Eina_Bool enable) diff --git a/src/nvim_api.c b/src/nvim_api.c index 0b7e6ee..8082352 100644 --- a/src/nvim_api.c +++ b/src/nvim_api.c @@ -55,8 +55,13 @@ _request_new(s_nvim *nvim, req->uid = nvim_next_uid_get(nvim); DBG("Preparing request '%s' with id %"PRIu32, rpc_name, req->uid); - /* Clear the serialization buffer before pushing a new request */ - msgpack_sbuffer_clear(&nvim->sbuffer); + /* The buffer MUST be empty before preparing another request. If this is not + * the case, something went very wrong! Discard the buffer and keep going */ + if (EINA_UNLIKELY(nvim->sbuffer.size != 0u)) + { + ERR("The buffer is not empty. I've messed up somewhere"); + msgpack_sbuffer_clear(&nvim->sbuffer); + } /* Keep the request around */ nvim->requests = eina_list_append(nvim->requests, req); @@ -91,19 +96,14 @@ _request_cleanup(s_nvim *nvim, } static Eina_Bool -_request_send(s_nvim *nvim, - s_request *req) +_request_send(s_nvim *nvim, s_request *req) { /* Finally, send that to the slave neovim process */ - const Eina_Bool ok = - ecore_exe_send(nvim->exe, nvim->sbuffer.data, (int)nvim->sbuffer.size); - if (EINA_UNLIKELY(! ok)) + if (EINA_UNLIKELY(! nvim_flush(nvim))) { - CRI("Failed to send %zu bytes to neovim", nvim->sbuffer.size); _request_cleanup(nvim, req); return EINA_FALSE; } - DBG("Sent %zu bytes to neovim", nvim->sbuffer.size); return EINA_TRUE; } diff --git a/src/nvim_request.c b/src/nvim_request.c new file mode 100644 index 0000000..7d34602 --- /dev/null +++ b/src/nvim_request.c @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 Jean Guyomarc'h + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "eovim/nvim_request.h" +#include "eovim/nvim.h" +#include "eovim/log.h" + +static Eina_Hash *_nvim_requests; + + +/*============================================================================* + * API * + *============================================================================*/ + +Eina_Bool +nvim_request_add(const char *request_name, f_nvim_request_cb func) +{ + Eina_Stringshare *const name = eina_stringshare_add(request_name); + const Eina_Bool ok = eina_hash_direct_add(_nvim_requests, name, func); + if (EINA_UNLIKELY(! ok)) + { + ERR("Failed to register request \"%s\"", request_name); + return EINA_FALSE; + } + return EINA_TRUE; +} + +void +nvim_request_del(const char *request_name) +{ + Eina_Stringshare *const name = eina_stringshare_add(request_name); + eina_hash_del(_nvim_requests, name, NULL); + eina_stringshare_del(name); +} + +Eina_Bool +nvim_request_init(void) +{ + _nvim_requests = eina_hash_stringshared_new(NULL); + if (EINA_UNLIKELY(! _nvim_requests)) + { + CRI("Failed to create hash table"); + return EINA_FALSE; + } + return EINA_TRUE; +} + +void +nvim_request_shutdown(void) +{ + assert(_nvim_requests != NULL); + eina_hash_free(_nvim_requests); + _nvim_requests = NULL; +} + +Eina_Bool +nvim_request_process(s_nvim *nvim, Eina_Stringshare *request, + const msgpack_object_array *args, uint32_t req_id) +{ + /* This function shall only be used on the main loop. Otherwise, we cannot + * use this packer */ + msgpack_packer *const pk = &nvim->packer; + + /* The buffer MUST be empty before preparing the response. If this is not + * the case, something went very wrong! Discard the buffer and keep going */ + if (EINA_UNLIKELY(nvim->sbuffer.size != 0u)) + { + ERR("The buffer is not empty. I've messed up somewhere"); + msgpack_sbuffer_clear(&nvim->sbuffer); + } + + /* + * Pack the message! It is an array of four (4) items: + * - the rpc type: + * - 1 is a request response + * - the unique identifier of the request + * - the error return + * - the result return + * + * We start to reply with the two first elements. If we are not prepared to + * handle this request, we will finish the message with an error and no + * result. But if someone handles the request, it is up to the handler to + * finish the message by setting both the error and result. + */ + msgpack_pack_array(pk, 4); + msgpack_pack_int(pk, 1); + msgpack_pack_uint32(pk, req_id); + + const f_nvim_request_cb func = eina_hash_find(_nvim_requests, request); + if (EINA_UNLIKELY(! func)) + { + WRN("No handler for request '%s'", request); + const char error[] = "unknown request"; + + /* See msgpack-rpc request response. Reply there is an error */ + msgpack_pack_str(pk, sizeof(error) - 1u); + msgpack_pack_str_body(pk, error, sizeof(error) - 1u); + msgpack_pack_nil(pk); + nvim_flush(nvim); + return EINA_FALSE; + } + else + { + const Eina_Bool ok = func(nvim, args, pk); + nvim_flush(nvim); + return ok; + } +}