nvim-hs-2.3.2.1: src/Neovim/RPC/SocketReader.hs
{-# LANGUAGE LambdaCase #-}
{- |
Module : Neovim.RPC.SocketReader
Description : The component which reads RPC messages from the neovim instance
Copyright : (c) Sebastian Witte
License : Apache-2.0
Maintainer : woozletoff@gmail.com
Stability : experimental
-}
module Neovim.RPC.SocketReader (
runSocketReader,
parseParams,
) where
import Neovim.Classes ( Int64, NvimObject(toObject, fromObject) )
import Neovim.Context ( MonadIO(liftIO), asks, Neovim, runNeovim )
import qualified Neovim.Context.Internal as Internal
import Neovim.Plugin.Classes (
CommandArguments (..),
CommandOption (..),
FunctionName (..),
FunctionalityDescription (..),
NeovimEventId (..),
NvimMethod (..),
Subscription (..),
getCommandOptions,
)
import Neovim.Plugin.IPC.Classes
( getCurrentTime,
Notification(Notification),
Request(Request),
writeMessage )
import qualified Neovim.RPC.Classes as MsgpackRPC
import Neovim.RPC.Common ( RPCConfig(recipients) )
import Neovim.RPC.FunctionCall ( atomically' )
import Conduit as C
( Void,
MonadTrans(lift),
sourceHandle,
(.|),
awaitForever,
runConduit,
ConduitT )
import Control.Monad (void)
import Data.Conduit.Cereal (conduitGet2)
import Data.Default (def)
import Data.Foldable (foldl', forM_)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.MessagePack ( Object(ObjectArray) )
import qualified Data.Serialize (get)
import System.IO (Handle)
import System.Log.Logger ( debugM, errorM, warningM )
import UnliftIO (atomically, timeout, readTVarIO, modifyTVar', putTMVar, readTMVar, async, newEmptyTMVarIO, modifyTVar)
import Prelude
logger :: String
logger = "Socket Reader"
type SocketHandler = Neovim RPCConfig
{- | This function will establish a connection to the given socket and read
msgpack-rpc events from it.
-}
runSocketReader ::
Handle ->
Internal.Config RPCConfig ->
IO ()
runSocketReader readableHandle cfg =
void . runNeovim (Internal.retypeConfig (Internal.customConfig cfg) cfg) . runConduit $ do
sourceHandle readableHandle
.| conduitGet2 Data.Serialize.get
.| messageHandlerSink
{- | Sink that delegates the messages depending on their type.
<https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md>
-}
messageHandlerSink :: ConduitT Object Void SocketHandler ()
messageHandlerSink = awaitForever $ \rpc -> do
liftIO . debugM logger $ "Received: " <> show rpc
case fromObject rpc of
Right (MsgpackRPC.Request (Request fn i ps)) ->
handleRequest i fn ps
Right (MsgpackRPC.Response i r) ->
handleResponse i r
Right (MsgpackRPC.Notification (Notification eventId args)) ->
handleNotification eventId args
Left e ->
liftIO . errorM logger $ "Unhandled rpc message: " <> show e
handleResponse :: Int64 -> Either Object Object -> ConduitT a Void SocketHandler ()
handleResponse i result = do
answerMap <- asks recipients
mReply <- Map.lookup i <$> liftIO (readTVarIO answerMap)
case mReply of
Nothing ->
liftIO $ warningM logger "Received response but could not find a matching recipient."
Just (_, reply) -> do
atomically' . modifyTVar' answerMap $ Map.delete i
atomically' $ putTMVar reply result
lookupFunction ::
Internal.Config RPCConfig ->
FunctionName ->
IO (Maybe (FunctionalityDescription, Internal.FunctionType))
lookupFunction rpc (F functionName) = do
functionMap <- atomically $ readTMVar (Internal.globalFunctionMap rpc)
pure $ Map.lookup (NvimMethod functionName) functionMap
handleRequest :: Int64 -> FunctionName -> [Object] -> ConduitT a Void SocketHandler ()
handleRequest requestId functionToCall params = do
cfg <- lift Internal.ask'
void . liftIO . async $ timeout (10 * 1000 * 1000) (handle cfg)
return ()
where
handle :: Internal.Config RPCConfig -> IO ()
handle rpc =
lookupFunction rpc functionToCall >>= \case
Nothing -> do
let errM = "No provider for: " <> show functionToCall
debugM logger errM
writeMessage (Internal.eventQueue rpc) $
MsgpackRPC.Response requestId (Left (toObject errM))
Just (copts, Internal.Stateful c) -> do
now <- liftIO getCurrentTime
reply <- liftIO newEmptyTMVarIO
let q = (recipients . Internal.customConfig) rpc
liftIO . debugM logger $ "Executing stateful function with ID: " <> show requestId
atomically' . modifyTVar q $ Map.insert requestId (now, reply)
writeMessage c $ Request functionToCall requestId (parseParams copts params)
handleNotification :: NeovimEventId -> [Object] -> ConduitT a Void SocketHandler ()
handleNotification eventId@(NeovimEventId str) args = do
cfg <- lift Internal.ask'
liftIO (lookupFunction cfg (F str)) >>= \case
Just (copts, Internal.Stateful c) -> liftIO $ do
debugM logger $ "Executing function asynchronously: " <> show str
writeMessage c $ Notification eventId (parseParams copts args)
Nothing -> do
liftIO $ debugM logger $ "Handling event: " <> show str
subscriptions' <- lift $ Internal.asks' Internal.subscriptions
subscribers <- liftIO $
atomically $ do
s <- readTMVar subscriptions'
pure $ fromMaybe [] $ Map.lookup eventId (Internal.byEventId s)
forM_ subscribers $ \subscription -> liftIO $ subAction subscription args
parseParams :: FunctionalityDescription -> [Object] -> [Object]
parseParams (Function _ _) args = case args of
-- Defining a function on the remote host creates a function that, that
-- passes all arguments in a list. At the time of this writing, no other
-- arguments are passed for such a function.
--
-- The function generating the function on neovim side is called:
-- @remote#define#FunctionOnHost@
[ObjectArray fArgs] -> fArgs
_ -> args
parseParams cmd@(Command _ opts) args = case args of
(ObjectArray _ : _) ->
let cmdArgs = filter isPassedViaRPC (getCommandOptions opts)
(c, args') = foldl' createCommandArguments (def, []) $ zip cmdArgs args
in toObject c : args'
_ -> parseParams cmd [ObjectArray args]
where
isPassedViaRPC :: CommandOption -> Bool
isPassedViaRPC = \case
CmdSync{} -> False
_ -> True
-- Neovim passes arguments in a special form, depending on the
-- CommandOption values used to export the (command) function (e.g. via
-- 'command' or 'command'').
createCommandArguments ::
(CommandArguments, [Object]) ->
(CommandOption, Object) ->
(CommandArguments, [Object])
createCommandArguments old@(c, args') = \case
(CmdRange _, o) ->
either (const old) (\r -> (c{range = Just r}, args')) $ fromObject o
(CmdCount _, o) ->
either (const old) (\n -> (c{count = Just n}, args')) $ fromObject o
(CmdBang, o) ->
either (const old) (\b -> (c{bang = Just b}, args')) $ fromObject o
(CmdNargs "*", ObjectArray os) ->
-- CommandArguments -> [String] -> Neovim r st a
(c, os)
(CmdNargs "+", ObjectArray (o : os)) ->
-- CommandArguments -> String -> [String] -> Neovim r st a
(c, o : [ObjectArray os])
(CmdNargs "?", ObjectArray [o]) ->
-- CommandArguments -> Maybe String -> Neovim r st a
(c, [toObject (Just o)])
(CmdNargs "?", ObjectArray []) ->
-- CommandArguments -> Maybe String -> Neovim r st a
(c, [toObject (Nothing :: Maybe Object)])
(CmdNargs "0", ObjectArray []) ->
-- CommandArguments -> Neovim r st a
(c, [])
(CmdNargs "1", ObjectArray [o]) ->
-- CommandArguments -> String -> Neovim r st a
(c, [o])
(CmdRegister, o) ->
either (const old) (\r -> (c{register = Just r}, args')) $ fromObject o
_ -> old
parseParams Autocmd{} args = case args of
[ObjectArray fArgs] -> fArgs
_ -> args