packages feed

ribosome-host-0.9.9.9: lib/Ribosome/Host/Handler/Command.hs

{-# options_haddock prune #-}

-- |Compute the command options and arguments based on handler function parameters.
module Ribosome.Host.Handler.Command where

import Type.Errors.Pretty (type (%), type (<>))

import Ribosome.Host.Data.Args (ArgList, Args, JsonArgs, Options)
import Ribosome.Host.Data.Bang (Bang)
import Ribosome.Host.Data.Bar (Bar)
import Ribosome.Host.Data.CommandMods (CommandMods)
import Ribosome.Host.Data.CommandRegister (CommandRegister)
import Ribosome.Host.Data.Range (Range, RangeStyleOpt (rangeStyleArg, rangeStyleOpt))
import Data.MessagePack (Object)
import Ribosome.Host.Class.Msgpack.Encode (toMsgpack)

-- |Represents the value for the command option @-nargs@.
data ArgCount =
  -- |@-nargs=0@
  Zero
  |
  -- |@-nargs=*@
  MinZero
  |
  -- |@-nargs=+@
  MinOne
  deriving stock (Eq, Show)

type family Max (l :: ArgCount) (r :: ArgCount) :: ArgCount where
  Max 'Zero r = r
  Max 'MinZero 'MinOne = 'MinOne
  Max l _ = l

-- |Determines how different special command handler parameter types may interact.
data OptionState =
  OptionState {
    -- |Are special option parameters allowed at this position?
    allowed :: Bool,
    -- |The minimum number of arguments that are expected
    minArgs :: ArgCount,
    -- |Have all arguments been consumed, by types like 'ArgList'?
    argsConsumed :: Maybe Type
  }

type OptionStateZero =
  'OptionState 'True 'Zero 'Nothing

type family CommandSpecial (a :: Type) :: Bool where
  CommandSpecial (Range _) = 'True
  CommandSpecial Bang = 'True
  CommandSpecial Bar = 'True
  CommandSpecial CommandMods = 'True
  CommandSpecial CommandRegister = 'True
  CommandSpecial Args = 'True
  CommandSpecial ArgList = 'True
  CommandSpecial (JsonArgs _) = 'True
  CommandSpecial (Options _) = 'True
  CommandSpecial _ = 'False

-- |Determine the command options and arguments that need to be specified when registering a command, for a special
-- command option parameter.
--
-- See [Command params]("Ribosome#command-params") for the list of supported special types.
class SpecialParam (state :: OptionState) (a :: Type) where
  type TransSpecial state a :: OptionState
  type TransSpecial s _ =
    s

  specialOpt :: Map Text Object
  specialOpt =
    mempty

  specialArg :: Maybe Text
  specialArg =
    Nothing

-- |Emit a compile error if a special command option type is used as a handler parameter after a regular, value
-- parameter.
--
-- The parameter @allowed@ is set to 'False' when the first non-option parameter is encountered.
type family BeforeRegular (allowed :: Bool) (a :: Type) :: Constraint where
  BeforeRegular 'False a =
    TypeError ("Command option type " <> a <> " may not come after non-option") ~ ()
  BeforeRegular 'True _ =
    ()

instance (
    BeforeRegular al (Range rs),
    RangeStyleOpt rs
  ) => SpecialParam ('OptionState al c ac) (Range rs) where
  specialOpt =
    rangeStyleOpt @rs
  specialArg =
    Just (rangeStyleArg @rs)

instance (
    BeforeRegular al Bang
  ) => SpecialParam ('OptionState al c ac) Bang where
  specialOpt =
    [("bang", toMsgpack True)]
  specialArg =
    Just "'<bang>' == '!'"

instance (
    BeforeRegular al Bar
  ) => SpecialParam ('OptionState al c ac) Bar where
  specialOpt =
    [("bar", toMsgpack True)]
  specialArg =
    Nothing

instance (
    BeforeRegular al CommandMods
  ) => SpecialParam ('OptionState al c ac) CommandMods where
  specialOpt =
    mempty
  specialArg =
    Just "<q-mods>"

instance (
    BeforeRegular al CommandRegister
  ) => SpecialParam ('OptionState al c ac) CommandRegister where
  specialOpt =
    [("register", toMsgpack True)]
  specialArg =
    Just "<q-register>"

instance SpecialParam ('OptionState al count 'Nothing) Args where
  type TransSpecial ('OptionState _ count _) _ =
    'OptionState 'True (Max count 'MinZero) ('Just Args)

instance SpecialParam ('OptionState al count ac) (JsonArgs a) where
  type TransSpecial ('OptionState _ count _) (JsonArgs a) =
    'OptionState 'True (Max count 'MinZero) ('Just (JsonArgs a))

instance SpecialParam ('OptionState al count ac) ArgList where
  type TransSpecial ('OptionState _ count _) _ =
    'OptionState 'True (Max count 'MinZero) ('Just ArgList)

instance SpecialParam ('OptionState al count 'Nothing) (Options a) where
  type TransSpecial ('OptionState _ count _) (Options a) =
    'OptionState 'True (Max count 'MinZero) ('Just (Options a))

-- |Determines whether a regular, value parameter is allowed (it isn't after types like 'ArgList' that consume all
-- remaining arguments), and increases the minimum argument count if the parameter isn't 'Maybe'.
class RegularParam (state :: OptionState) (isMaybe :: Bool) a where
  type TransRegular state isMaybe a :: OptionState

type family ArgsError consumer a where
  ArgsError consumer a =
    TypeError (
      "Custom parameter types (here " <> a <> ") cannot be combined with " <> consumer
      %
      "since " <> consumer <> " consumes all arguments"
    )

instance RegularParam ('OptionState al count ('Just consumer)) m a where
  type TransRegular ('OptionState al count ('Just consumer)) m a =
    ArgsError consumer a

instance RegularParam ('OptionState al count 'Nothing) 'True (Maybe a) where
  type TransRegular ('OptionState al count 'Nothing) 'True (Maybe a) =
    'OptionState 'False (Max count 'MinZero) 'Nothing

instance RegularParam ('OptionState al count 'Nothing) 'False a where
  type TransRegular ('OptionState al count 'Nothing) 'False a =
    'OptionState 'False 'MinOne 'Nothing

-- |Determine the command option and parameter that a handler parameter type requires, if any.
class CommandParam (special :: Bool) (state :: OptionState) (a :: Type) where
  -- |Transition the current 'OptionState'.
  type TransState special state a :: OptionState

  paramOpt :: Map Text Object
  paramOpt =
    mempty

  paramArg :: Maybe Text
  paramArg =
    Nothing

instance (
    SpecialParam state a
  ) => CommandParam 'True state a where
    type TransState 'True state a =
      TransSpecial state a

    paramOpt =
      specialOpt @state @a

    paramArg =
      specialArg @state @a

type family IsMaybe (a :: Type) :: Bool where
  IsMaybe (Maybe _) = 'True
  IsMaybe _ = 'False

instance (
    RegularParam state (IsMaybe a) a
  ) => CommandParam 'False state a where
    type TransState 'False state a =
      TransRegular state (IsMaybe a) a

-- |Derive the command options and arguments that should be used when registering the Neovim command, from the
-- parameters of the handler function.
--
-- See [Command params]("Ribosome#command-params") for the list of supported special types.
--
-- The parameter @state@ is a type level value that determines which parameter types may be used after another and
-- counts the number of command arguments that are required or allowed.
-- It is transitioned by families in the classes 'CommandParam', 'SpecialParam' and 'RegularParam'.
class CommandHandler (state :: OptionState) (h :: Type) where
  -- |Return the list of command options and special arguments determined by the handler function's parameters.
  commandOptions :: (Map Text Object, [Text])

instance CommandHandler ('OptionState _a 'Zero c) (Sem r a) where
  commandOptions =
    ([("nargs", toMsgpack @Int 0)], [])

instance CommandHandler ('OptionState _a 'MinZero c) (Sem r a) where
  commandOptions =
    ([("nargs", toMsgpack @Text "*")], ["<f-args>"])

instance CommandHandler ('OptionState _a 'MinOne c) (Sem r a) where
  commandOptions =
    ([("nargs", toMsgpack @Text "+")], ["<f-args>"])

instance (
    special ~ CommandSpecial a,
    next ~ TransState special state a,
    CommandParam special state a,
    CommandHandler next b
  ) => CommandHandler state (a -> b) where
    commandOptions =
      (opts, args)
      where
        opts =
          paramOpt @special @state @a <> optsAfter
        args =
          maybeToList (paramArg @special @state @a) <> argsAfter
        (optsAfter, argsAfter) =
          commandOptions @next @b