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)