summaryrefslogtreecommitdiffstats
path: root/development/eovim/07716.patch
blob: a591038ac939f1abbf2bd69246d1bfe34338d0a6 (plain)
From 0771672b18c6645a7fa4de61ac106bdf3b69a04a Mon Sep 17 00:00:00 2001
From: Jean Guyomarc'h <jean@guyomarch.bzh>
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;
+     }
+}