packages feed

nvim-hs-2.3.2.1: src/Neovim/RPC/Common.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE LambdaCase #-} 

{- |
Module      :  Neovim.RPC.Common
Description :  Common functons for the RPC module
Copyright   :  (c) Sebastian Witte
License     :  Apache-2.0

Maintainer  :  woozletoff@gmail.com
Stability   :  experimental
-}
module Neovim.RPC.Common where

import Neovim.OS (getSocketUnix)

import Control.Applicative (Alternative ((<|>)))
import Control.Monad (unless)
import Data.Int (Int64)
import Data.Map (Map)
import Data.MessagePack (Object)
import Data.Streaming.Network (getSocketTCP)
import Data.String (IsString (fromString))
import Data.Time (UTCTime)
import Neovim.Compat.Megaparsec as P (
    MonadParsec (eof, try),
    Parser,
    anySingle,
    anySingleBut,
    many,
    parse,
    single,
    some,
 )
import Network.Socket as N (socketToHandle)
import System.Log.Logger (errorM, warningM)

import Data.List (intercalate)
import qualified Data.List as List
import Data.Maybe (catMaybes)
import qualified Text.Megaparsec.Char.Lexer as L
import UnliftIO.Environment (lookupEnv)
import UnliftIO

import Prelude

-- | Things shared between the socket reader and the event handler.
data RPCConfig = RPCConfig
    { recipients :: TVar (Map Int64 (UTCTime, TMVar (Either Object Object)))
    -- ^ A map from message identifiers (as per RPC spec) to a tuple with a
    -- timestamp and a 'TMVar' that is used to communicate the result back to
    -- the calling thread.
    , nextMessageId :: TVar Int64
    -- ^ Message identifier for the next message as per RPC spec.
    }

{- | Create a new basic configuration containing a communication channel for
 remote procedure call events and an empty lookup table for functions to
 mediate.
-}
newRPCConfig :: (Applicative io, MonadUnliftIO io) => io RPCConfig
newRPCConfig =
    RPCConfig
        <$> liftIO (newTVarIO mempty)
        <*> liftIO (newTVarIO 1)

-- | Simple data type defining the kind of socket the socket reader should use.
data SocketType
    = -- | Use the handle for receiving msgpack-rpc messages. This is
      -- suitable for an embedded neovim which is used in test cases.
      Stdout Handle
    | -- | Read the connection information from the environment
      -- variable @NVIM@.
      Environment
    | -- | Use a unix socket.
      UnixSocket FilePath
    | -- | Use an IP socket. First argument is the port and the
      -- second is the host name.
      TCP Int String

{- | Create a 'Handle' from the given socket description.

 The handle is not automatically closed.
-}
createHandle ::
    (Functor io, MonadUnliftIO io) =>
    SocketType ->
    io Handle
createHandle = \case
    Stdout h -> do
        liftIO $ hSetBuffering h (BlockBuffering Nothing)
        return h
    UnixSocket f ->
        liftIO $ createHandle . Stdout =<< flip socketToHandle ReadWriteMode =<< getSocketUnix f
    TCP p h ->
        createHandle . Stdout =<< createTCPSocketHandle p h
    Environment ->
        createHandle . Stdout =<< createSocketHandleFromEnvironment
  where
    createTCPSocketHandle :: (MonadUnliftIO io) => Int -> String -> io Handle
    createTCPSocketHandle p h =
        liftIO $
            getSocketTCP (fromString h) p
                >>= flip socketToHandle ReadWriteMode . fst

    createSocketHandleFromEnvironment = liftIO $ do
        -- NVIM_LISTEN_ADDRESS is for backwards compatibility
        envValues <- catMaybes <$> mapM lookupEnv ["NVIM", "NVIM_LISTEN_ADDRESS"]
        listenAdresses <- mapM parseNvimEnvironmentVariable envValues
        case listenAdresses of
            (s : _) -> createHandle s
            _ -> do
                let errMsg =
                        unlines
                            [ "Unhandled socket type from environment variable: "
                            , "\t" <> intercalate ", " envValues
                            ]
                liftIO $ errorM "createHandle" errMsg
                error errMsg

parseNvimEnvironmentVariable :: MonadFail m => String -> m SocketType
parseNvimEnvironmentVariable envValue =
    either (fail . show) pure $ parse (P.try pTcpAddress <|> pUnixSocket) envValue envValue

pUnixSocket :: P.Parser SocketType
pUnixSocket = UnixSocket <$> P.some anySingle <* P.eof

pTcpAddress :: P.Parser SocketType
pTcpAddress = do
    prefixes <- P.some (P.try (P.many (P.anySingleBut ':') <* P.single ':'))
    port <- L.lexeme P.eof L.decimal
    P.eof
    pure $ TCP port (List.intercalate ":" prefixes)

{- | Close the handle and print a warning if the conduit chain has been
 interrupted prematurely.
-}
cleanUpHandle :: (MonadUnliftIO io) => Handle -> Bool -> io ()
cleanUpHandle h completed = liftIO $ do
    hClose h
    unless completed $
        warningM "cleanUpHandle" "Cleanup called on uncompleted handle."