packages feed

reflex-dom-ace-0.3.0.0: src/Reflex/Dom/Ace.hs

{-|

Basic support for using the Ace editor with Reflex.

-}

module Reflex.Dom.Ace where

import Control.Lens ((^.))
import Control.Monad (unless, void, join)
import Data.Default (Default)
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Control.Monad.Trans as Trans
import qualified Data.Bifunctor as Bi

import qualified Language.Javascript.JSaddle as JS
import Language.Javascript.JSaddle.Object ((<#))
import qualified Reflex.Dom.Core as R


data AceTheme
  = AceTheme_Chrome
  | AceTheme_Clouds
  | AceTheme_CrimsonEditor
  | AceTheme_Dawn
  | AceTheme_Dreamweaver
  | AceTheme_Eclipse
  | AceTheme_Github
  | AceTheme_Iplastic
  | AceTheme_SolarizedLight
  | AceTheme_Textmate
  | AceTheme_Tomorrow
  | AceTheme_Xcode
  | AceTheme_Kuroir
  | AceTheme_Katzenmilch
  | AceTheme_Sqlserver
  | AceTheme_Ambiance
  | AceTheme_Chaos
  | AceTheme_CloudsMidnight
  | AceTheme_Cobalt
  | AceTheme_Gruvbox
  | AceTheme_IdleFingers
  | AceTheme_KrTheme
  | AceTheme_Merbivore
  | AceTheme_MerbivoreSoft
  | AceTheme_MonoIndustrial
  | AceTheme_Monokai
  | AceTheme_PastelOnDark
  | AceTheme_SolarizedDark
  | AceTheme_Terminal
  | AceTheme_TomorrowNight
  | AceTheme_TomorrowNightBlue
  | AceTheme_TomorrowNightBright
  | AceTheme_TomorrowNightEighties
  | AceTheme_Twilight
  | AceTheme_VibrantInk
  deriving (Eq,Ord,Enum,Bounded)

instance Show AceTheme where
    show AceTheme_Ambiance              = "ambiance"
    show AceTheme_Chaos                 = "chaos"
    show AceTheme_Chrome                = "chrome"
    show AceTheme_Clouds                = "clouds"
    show AceTheme_CloudsMidnight        = "clouds_midnight"
    show AceTheme_Cobalt                = "cobalt"
    show AceTheme_CrimsonEditor         = "crimson_editor"
    show AceTheme_Dawn                  = "dawn"
    show AceTheme_Dreamweaver           = "dreamweaver"
    show AceTheme_Eclipse               = "eclipse"
    show AceTheme_Github                = "github"
    show AceTheme_Gruvbox               = "gruvbox"
    show AceTheme_IdleFingers           = "idle_fingers"
    show AceTheme_Iplastic              = "iplastic"
    show AceTheme_Katzenmilch           = "katzenmilch"
    show AceTheme_KrTheme               = "kr_theme"
    show AceTheme_Kuroir                = "kuroir"
    show AceTheme_Merbivore             = "merbivore"
    show AceTheme_MerbivoreSoft         = "merbivore_soft"
    show AceTheme_MonoIndustrial        = "mono_industrial"
    show AceTheme_Monokai               = "monokai"
    show AceTheme_PastelOnDark          = "pastel_on_dark"
    show AceTheme_SolarizedDark         = "solarized_dark"
    show AceTheme_SolarizedLight        = "solarized_light"
    show AceTheme_Sqlserver             = "sqlserver"
    show AceTheme_Terminal              = "terminal"
    show AceTheme_Textmate              = "textmate"
    show AceTheme_Tomorrow              = "tomorrow"
    show AceTheme_TomorrowNight         = "tomorrow_night"
    show AceTheme_TomorrowNightBlue     = "tomorrow_night_blue"
    show AceTheme_TomorrowNightBright   = "tomorrow_night_bright"
    show AceTheme_TomorrowNightEighties = "tomorrow_night_eighties"
    show AceTheme_Twilight              = "twilight"
    show AceTheme_VibrantInk            = "vibrant_ink"
    show AceTheme_Xcode                 = "xcode"


data AceConfig = AceConfig
    { _aceConfigElemAttrs       :: Map Text Text
    , _aceConfigBasePath        :: Maybe Text
    , _aceConfigMode            :: Maybe Text
    , _aceConfigWordWrap        :: Bool
    , _aceConfigShowPrintMargin :: Bool
    }


data AceDynConfig = AceDynConfig
    { _aceDynConfigTheme :: Maybe AceTheme
    }


instance Default AceConfig where
    def = AceConfig R.def R.def R.def False False


newtype AceInstance = AceInstance { unAceInstance :: JS.JSVal }


data Ace t = Ace
    { aceRef   :: R.Dynamic t (Maybe AceInstance)
    , aceValue :: R.Dynamic t Text
    }


------------------------------------------------------------------------------
-- The type of editor session line annotation.
data AnnotationType = AnnotationError
                    | AnnotationWarning
                    deriving (Show, Read)

------------------------------------------------------------------------------
instance JS.ToJSVal AnnotationType where
  toJSVal AnnotationError   = JS.toJSVal ("error":: Text)
  toJSVal AnnotationWarning = JS.toJSVal ("warning" :: Text)


------------------------------------------------------------------------------
-- A line annotation for marking a specific line within the editor session as
-- an error or a warning.
data Annotation = Annotation { annotationRow    :: Int
                             , annotationColumn :: Int
                             , annotationText   :: Text
                             , annotationType   :: AnnotationType
                             } deriving (Show, Read)


------------------------------------------------------------------------------
instance JS.MakeObject Annotation where
  makeObject (Annotation row col txt typ) = do
    o <- JS.create
    (o <# ("row" :: Text)   ) row
    (o <# ("column" :: Text)) col
    (o <# ("text" :: Text)  ) txt
    (o <# ("type" :: Text)  ) typ
    return o


instance JS.ToJSVal Annotation where
  toJSVal = (JS.toJSVal =<<) . JS.makeObject


------------------------------------------------------------------------------
startAce :: JS.MonadJSM m => Text -> AceConfig -> m AceInstance
startAce containerId ac = JS.liftJSM $ do
  aceVal <- JS.jsg ("ace" :: Text)
  let
    (basePath, mode) = join Bi.bimap (fromMaybe (T.pack "") . ($ ac))
      (_aceConfigBasePath, _aceConfigMode)
  -- Set the base path if given
  unless (T.null basePath) $ do
    config <- aceVal ^. JS.js ("config" :: Text)
    void $ config ^. JS.js2 ("set" :: Text) ("basePath" :: Text) basePath
  -- Start and return an editing session
  editor <- aceVal ^. JS.js1 ("edit" :: Text) containerId
  let aceInst = AceInstance editor
  -- Set the mode if given
  unless (T.null mode) $ do
    setModeAce mode aceInst
  setUseWrapMode (_aceConfigWordWrap ac) aceInst
  setShowPrintMargin (_aceConfigShowPrintMargin ac) aceInst
  return aceInst


------------------------------------------------------------------------------
moveCursorToPosition :: JS.MonadJSM m => (Int, Int) -> AceInstance -> m ()
moveCursorToPosition (r, c) (AceInstance ace) =
  JS.liftJSM $ void $ ace ^. JS.js2 ("gotoLine" :: Text) r c


------------------------------------------------------------------------------
setThemeAce :: JS.MonadJSM m => Maybe AceTheme -> AceInstance -> m ()
setThemeAce Nothing      _                 = return ()
setThemeAce (Just theme) (AceInstance ace) =
  JS.liftJSM $ void $ ace ^. JS.js1 ("setTheme" :: Text) themeStr
  where themeStr = "ace/theme/" <> show theme


------------------------------------------------------------------------------
setModeAce :: JS.MonadJSM m => Text -> AceInstance -> m ()
setModeAce mode (AceInstance ace) = JS.liftJSM $ do
  session <- ace ^. JS.js ("session" :: Text)
  void $ session ^. JS.js1 ("setMode" :: Text) modeStr
  where modeStr = "ace/mode/" <> mode


------------------------------------------------------------------------------
setUseWrapMode :: JS.MonadJSM m => Bool -> AceInstance -> m ()
setUseWrapMode shouldWrap (AceInstance ace) = JS.liftJSM $ do
  session <- ace ^. JS.js0 ("getSession" :: Text)
  void $ session ^. JS.js1 ("setUseWrapMode" :: Text) shouldWrap


------------------------------------------------------------------------------
setShowPrintMargin :: JS.MonadJSM m => Bool -> AceInstance -> m ()
setShowPrintMargin shouldShow (AceInstance ace) =
  JS.liftJSM $ void $ ace ^. JS.js2 ("setOption" :: Text) ("showPrintMargin" :: Text) shouldShow


------------------------------------------------------------------------------
setUseWorker :: JS.MonadJSM m => Bool -> AceInstance -> m ()
setUseWorker shouldUse (AceInstance ace) =
  JS.liftJSM $ void $ ace ^. JS.js2 ("setOption" :: Text) ("useWorker" :: Text) shouldUse


------------------------------------------------------------------------------
setAnnotations :: JS.MonadJSM m => [Annotation] -> AceInstance -> m ()
setAnnotations as (AceInstance ace) = JS.liftJSM $ do
  session <- ace ^. JS.js0 ("getSession" :: Text)
  annotations <- JS.toJSValListOf as
  void $ session ^. JS.js1 ("setAnnotations" :: Text) annotations


------------------------------------------------------------------------------
setConfigAce :: JS.MonadJSM m => Text -> Text -> AceInstance -> m ()
setConfigAce t1 t2 (AceInstance ace) = JS.liftJSM $ do
  cfg <- ace ^. JS.js ("config" :: Text)
  void $ cfg ^. JS.js2 ("set" :: Text) t1 t2


------------------------------------------------------------------------------
getValueAce :: JS.MonadJSM m => AceInstance -> m Text
getValueAce (AceInstance ace) =
  JS.liftJSM $ ace ^. JS.js0 ("getValue" :: Text) >>= JS.fromJSValUnchecked


------------------------------------------------------------------------------
setValueAce :: JS.MonadJSM m => Text -> AceInstance -> m ()
setValueAce t (AceInstance ace) =
  JS.liftJSM $ void $ ace ^. JS.js2 ("setValue" :: Text) t (-1 :: Int)


------------------------------------------------------------------------------
setupValueListener
  :: ( JS.MonadJSM (R.Performable m)
     , R.DomBuilder t m
     , R.PostBuild t m
     , R.TriggerEvent t m
     , R.PerformEvent t m
     )
  => AceInstance
  -> m (R.Event t Text)
setupValueListener (AceInstance ace) = do
  pb  <- R.getPostBuild
  let act cb = JS.liftJSM $ do
        jscb <- JS.asyncFunction $ \_ _ _ ->
          getValueAce (AceInstance ace) >>= Trans.liftIO . cb
        void $ ace ^. JS.js2 ("on" :: Text) ("change" :: Text) jscb
  R.performEventAsync (act <$ pb)


------------------------------------------------------------------------------
-- | Main entry point
--
-- IMPORTANT NOTE:
--
-- This currently does not work if your app is using reflex-dom's
-- mainWidgetWithHead or mainWidgetWithCss.
aceWidget
    :: ( R.DomBuilder t m
       , R.PostBuild t m
       , R.MonadHold t m
       , JS.MonadJSM m
       , R.TriggerEvent t m
       , R.PerformEvent t m
       , JS.MonadJSM (R.Performable m)
       )
    => AceConfig -- ^ Ace editor configurations
    -> AceDynConfig -- ^ Ace editor theme
    -> R.Event t AceDynConfig -- ^ Updatable Ace editor theme
    -> Text -- ^ ID of desired container element
    -> Text -- ^ Initial Ace editor contents
    -> R.Event t Text -- ^ Updatable Ace editor contents
    -> m (Ace t)
aceWidget ac adc adcUps containerId initContents contentsUps = do
    aceInstance <- startAce containerId ac
    onChange <- setupValueListener aceInstance
    updatesDyn <- R.holdDyn initContents onChange

    let ace = Ace (R.constDyn $ pure aceInstance) updatesDyn
    setThemeAce (_aceDynConfigTheme adc) aceInstance
    void $ withAceInstance ace (setThemeAce . _aceDynConfigTheme <$> adcUps)
    R.performEvent_ $ R.ffor contentsUps $ \c -> setValueAce c aceInstance
    return ace


------------------------------------------------------------------------------
-- | Convenient helper function for running functions that need an AceInstance.
withAceInstance
    :: R.PerformEvent t m
    => Ace t
    -> R.Event t (AceInstance -> R.Performable m ())
    -> m (R.Event t ())
withAceInstance ace evt = withAceInstance' ace (f <$> evt)
  where
    f _ Nothing  = return ()
    f g (Just a) = g a


------------------------------------------------------------------------------
-- | More powerful function for running functions that need an AceInstance.
withAceInstance'
    :: R.PerformEvent t m
    => Ace t
    -> R.Event t (Maybe AceInstance -> R.Performable m a)
    -> m (R.Event t a)
withAceInstance' ace =
  R.performEvent . R.attachPromptlyDynWith (flip ($)) (aceRef ace)