packages feed

ribosome-0.3.0.0: lib/Ribosome/Menu/Prompt/Nvim.hs

module Ribosome.Menu.Prompt.Nvim where

import Conduit (ConduitT, yield)
import Control.Exception.Lifted (bracket_)
import Control.Monad.DeepError (ignoreError)
import qualified Data.Text as Text (singleton, splitAt, uncons)

import Ribosome.Api.Atomic (atomic)
import Ribosome.Api.Function (defineFunction)
import Ribosome.Api.Variable (setVar)
import Ribosome.Api.Window (redraw)
import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
import Ribosome.Data.Text (escapeQuotes)
import Ribosome.Menu.Prompt.Data.Codes (decodeInputChar, decodeInputNum)
import Ribosome.Menu.Prompt.Data.InputEvent (InputEvent)
import qualified Ribosome.Menu.Prompt.Data.InputEvent as InputEvent (InputEvent(..))
import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
import Ribosome.Menu.Prompt.Data.PromptRenderer (PromptRenderer(PromptRenderer))
import Ribosome.Msgpack.Encode (toMsgpack)
import Ribosome.Msgpack.Error (DecodeError)
import qualified Ribosome.Nvim.Api.Data as ApiData (vimCommand)
import Ribosome.Nvim.Api.IO (vimCallFunction, vimCommand, vimCommandOutput, vimGetOption, vimSetOption)
import Ribosome.Nvim.Api.RpcCall (RpcError, syncRpcCall)
import Ribosome.System.Time (sleep)

quitChar :: Char
quitChar =
  '†'

quitCharOrd :: Int
quitCharOrd =
  ord quitChar

getChar ::
  NvimE e m =>
  MonadRibo m =>
  MonadBaseControl IO m =>
  m InputEvent
getChar =
  catchAs @RpcError InputEvent.Interrupt request
  where
    request =
      event =<< vimCallFunction "getchar" [toMsgpack False]
    event (Right c) =
      return $ InputEvent.Character (fromMaybe c (decodeInputChar c))
    event (Left 0) =
      return InputEvent.NoInput
    event (Left num) | num == quitCharOrd =
      return InputEvent.Interrupt
    event (Left num) =
      maybe (InputEvent.Unexpected num) InputEvent.Character <$> decodeInputNum num

getCharC ::
  MonadIO m =>
  MonadBaseControl IO m =>
  NvimE e m =>
  MonadRibo m =>
  Double ->
  ConduitT () PromptEvent m ()
getCharC interval =
  recurse
  where
    recurse =
      translate =<< lift getChar
    translate (InputEvent.Character a) =
      yield (PromptEvent.Character a) *> recurse
    translate InputEvent.Interrupt =
      yield PromptEvent.Interrupt
    translate (InputEvent.Error e) =
      yield (PromptEvent.Error e)
    translate InputEvent.NoInput =
      sleep interval *> recurse
    translate (InputEvent.Unexpected _) =
      recurse

promptFragment :: Text -> Text -> [Text]
promptFragment hl text =
  ["echohl " <> hl, "echon '" <> escapeQuotes text <> "'"]

nvimRenderPrompt ::
  Monad m =>
  NvimE e m =>
  MonadDeepError e DecodeError m =>
  Prompt ->
  m ()
nvimRenderPrompt (Prompt cursor _ text) =
  void $ atomic calls
  where
    calls = syncRpcCall . ApiData.vimCommand <$> ("silent! redraw!" : (fragments >>= uncurry promptFragment))
    fragments =
      [
        ("RibosomePromptSign", sign),
        ("None", pre),
        ("RibosomePromptCaret", Text.singleton cursorChar),
        ("None", post)
        ]
    (pre, rest) =
      Text.splitAt cursor text
    (cursorChar, post) =
      fromMaybe (' ', "") (Text.uncons rest)
    sign =
      "% "

loopFunctionName :: Text
loopFunctionName =
  "RibosomeMenuLoop"

loopVarName :: Text
loopVarName =
  "ribosome_menu_looping"

defineLoopFunction ::
  NvimE e m =>
  m ()
defineLoopFunction =
  defineFunction loopFunctionName [] lns
  where
    lns =
      [
        "echo ''",
        "while g:" <> loopVarName,
        "try",
        "sleep 5m",
        "catch /^Vim:Interrupt$/",
        "silent! call feedkeys('" <> Text.singleton quitChar <> "')",
        "endtry",
        "endwhile"
        ]

startLoop ::
  NvimE e m =>
  m ()
startLoop = do
  defineLoopFunction
  setVar loopVarName True
  vimCommand $ "call feedkeys(\":call " <> loopFunctionName <> "()\\<cr>\")"

-- FIXME need to wait for the loop to stop before deleting the function
killLoop ::
  NvimE e m =>
  m ()
killLoop = do
  setVar loopVarName False
  ignoreError @RpcError $ vimCommand $ "delfunction! " <> loopFunctionName

promptBlocker ::
  NvimE e m =>
  MonadBaseControl IO m =>
  m a ->
  m a
promptBlocker =
  bracket_ startLoop killLoop

newtype NvimPromptResources =
  NvimPromptResources {
    _guicursor :: Text
  }
  deriving (Eq, Show)

nvimAcquire ::
  NvimE e m =>
  m NvimPromptResources
nvimAcquire = do
  highlightSet <- catchAs @RpcError False $ True <$ vimCommandOutput "highlight RibosomePromptCaret"
  unless highlightSet $ vimCommand "highlight link RibosomePromptCaret TermCursor"
  res <- NvimPromptResources <$> vimGetOption "guicursor"
  vimSetOption "guicursor" (toMsgpack ("a:None" :: Text))
  () <- vimCallFunction "inputsave" []
  startLoop
  return res

nvimRelease ::
  NvimE e m =>
  MonadRibo m =>
  NvimPromptResources ->
  m ()
nvimRelease (NvimPromptResources gc) = do
  vimSetOption "guicursor" (toMsgpack gc)
  redraw
  vimCommand "echon ''"
  () <- vimCallFunction "inputrestore" []
  killLoop

nvimPromptRenderer ::
  NvimE e m =>
  MonadRibo m =>
  MonadDeepError e DecodeError m =>
  PromptRenderer m
nvimPromptRenderer =
  PromptRenderer nvimAcquire nvimRelease nvimRenderPrompt