packages feed

spade-0.1.0.0: src/IDE/IDE.hs

module IDE.IDE where

import Common
import Compiler.Lexer
import Compiler.Parser
import Control.Concurrent
import Control.Concurrent.STM (TChan, TVar, atomically, dupTChan, modifyTVar,
                               newTChan, newTChanIO, newTVarIO, readTChan,
                               readTVar, readTVarIO, writeTChan, writeTVar)
import Control.Exception
import Control.Monad
import Data.IORef
import qualified Data.List as DL
import Data.Map as Map
import Data.Maybe
import qualified Data.Set as S
import Data.Text as T
import Data.Text.IO as T

import Compiler.AST.Program
import IDE.Common
import IDE.Help
import Interpreter
import Interpreter.Common
import Interpreter.Initialize
import Parser.Parser
import UI.Widgets
import UI.Widgets.AutoComplete
import UI.Widgets.Layout
import UI.Widgets.LogWidget
import UI.Widgets.WatchWidget

data IDEState = IDEState
  { idsDebugThreadId       :: Maybe ThreadId
  , idsDebugIn             :: Maybe (MVar DebugIn)
  , idsDebugOut            :: Maybe (MVar DebugOut)
  , idsDebugThreadInput    :: Maybe (TChan TerminalEvent)
  , idsCodeAutocompletions :: [(Text, Text)]
  , idsCodeParseError      :: Maybe (Location, Text)
  , idsKeyInputReciever    :: Maybe SomeKeyInputWidget
  }

ideStartState :: IDEState
ideStartState = IDEState Nothing Nothing Nothing Nothing [] Nothing Nothing

type Clipboard = (Text -> IO (), IO (Maybe Text))

type IDEM a = WidgetM IO a

editorId :: Text
editorId = "editor"

watchWidgetId :: Text
watchWidgetId = "watch"

extractBuiltIns :: IO ([(Text, Text)], [(Text, Text)])
extractBuiltIns = do
  debugIn <- newEmptyMVar
  debugOut <- newEmptyMVar
  sdlWindowsRef <- newIORef []
  scope <- isGlobalScope . snd <$> runStateT loadBuiltIns (emptyIs sdlWindowsRef debugIn debugOut (error "Input stream not available"))
  let keywords = (\x -> let s = toSource $ toEnum @Keyword x in (s, s)) <$> [0 .. fromEnum $ maxBound @Keyword]
  let builtIns = DL.foldl' extractOneDoc [] (Map.assocs scope)
  pure $ (keywords, builtIns)
  where
    extractOneDoc :: [(Text, Text)] -> (ScopeKey, Value) -> [(Text, Text)]
    extractOneDoc in_ (SkIdentifier (unIdentifer ->ide), BuiltIn (BuiltinVal (ObjectValue m))) =
      in_ <> ((\k -> let a = ide <> "." <> k in (a, a)) <$> Map.keys m)
    extractOneDoc in_ (SkIdentifier ide, BuiltIn (BuiltinCallWithDoc s)) =
      (unIdentifer ide, T.concat [
        unIdentifer ide,
        "(",
        T.intercalate ", " ((\(l, r) -> T.concat [l, ": ", r]) <$> extractDoc s),
        ")"
        ]
      ) : in_
    extractOneDoc in_ (SkIdentifier ide, BuiltIn (BuiltinCall _)) = (unIdentifer ide, unIdentifer ide) : in_
    extractOneDoc in_ (SkOperator op, _) = (toSource op, toSource op) : in_
    extractOneDoc in_ _ = in_

getAutocompleteSuggestions :: MonadIO m => TVar IDEState -> [(Text, Text)] -> Text -> m [(Text, Text)]
getAutocompleteSuggestions ideStateRef dict key = do
  acs <- liftIO $ idsCodeAutocompletions <$> readTVarIO ideStateRef
  pure $ Prelude.filter fn (acs <> dict)
  where
    fn :: (Text, Text) -> Bool
    fn (i, _) = T.isPrefixOf key i

getTerminalSize' :: IO Dimensions
getTerminalSize' = do
    s <- getTerminalSizeIO
    case s of
      Just (xs, ys) -> pure $ Dimensions xs ys
      _             -> error "Cannot read terminal size"

-- This thread reads the editor contents and try to parse into
-- tokens and ast. And then fills some autocomplete suggestions and
-- makes any parse error location.
codeProcessingThread :: IORef [Token] -> TChan IDEEvent -> MVar Text -> IO ()
codeProcessingThread tokensRef ideEventRef editorContentRef =  forever $ do
  liftIO $ atomically $ writeTChan ideEventRef IDEReqEditorContent
  code <- takeMVar editorContentRef
  tokens <- tokenize code
  liftIO $ writeIORef tokensRef tokens
  mError <- parseEither @Program tokens >>= \case
    Left (ParseErrorWithParsed _ loc (FatalError a)) -> pure $ Just (loc, T.pack $ show a)
    Left (ParseErrorWithParsed _ _ (FatalErrorWithLocation loc fpe)) -> pure $ Just (loc, T.pack $ show fpe)
    _ -> pure Nothing
  liftIO $ atomically $ writeTChan ideEventRef $ IDEParseErrorUpdate mError
  liftIO $ atomically $ writeTChan ideEventRef $ IDEACUpdateIdentifiers $ S.toList $ S.fromList $ catMaybes $ filterIdentifiers <$> tokens
  wait 1
  where
    filterIdentifiers :: Token -> Maybe Text
    filterIdentifiers Token { tkRaw = (TkIdentifier (unIdentifer -> idf))} = Just idf
    filterIdentifiers _ = Nothing

-- | A type to differentiat debuging mode in IDE when input events
-- need to be fed to the interpreted program, and not to the IDE.
data InputRouting
  = RouteIDE
  | RouteProgram (TChan TerminalEvent)

instance Show InputRouting where
  show RouteIDE = "Route To IDE"
  show (RouteProgram _) = "Route To Program"

runIDE :: FilePath -> TChan TerminalEvent -> IO ()
runIDE filePath inputBroadcastChan = do
  dim <- getTerminalSize'
  content <- try (T.readFile filePath) >>= \case
    Right c                   -> pure c
    Left (_ :: SomeException) -> pure ""
  (keywords, builtIns) <- extractBuiltIns
  ideStateRef <- newTVarIO ideStartState
  ideEventsRef <- liftIO newTChanIO

  clipboardRef <- newIORef @(Maybe Text) Nothing
  let clipboard = (\t -> modifyIORef clipboardRef (\_ -> Just t), readIORef clipboardRef)
  editorContentMVar <- newEmptyMVar @Text

  tokensRef <- newIORef @[Token] []

  void $ forkIO $ codeProcessingThread tokensRef ideEventsRef editorContentMVar

  inputRouteToRef <- newTVarIO RouteIDE

  inputChan <- liftIO $ atomically $ dupTChan inputBroadcastChan

  void $ liftIO $ forkIO $ forever $ do
    (atomically $ readTVar inputRouteToRef) >>= appendLog
    atomically $ do
      kv <- readTChan inputChan
      readTVar inputRouteToRef >>= \case
        RouteIDE          -> writeTChan ideEventsRef (IDETerminalEvent kv)
        RouteProgram chan -> writeTChan chan kv

  void $ runWidgetM $ do

    -- The editor
    editorRef <- editor (getAutocompleteSuggestions ideStateRef (keywords <> ((\(a, b) -> (a <>"()", b)) <$> builtIns))) (Just $ SomeTokenStream (readIORef tokensRef))
    setContent editorRef content
    liftIO $ atomically $ modifyTVar ideStateRef (\ids -> ids { idsKeyInputReciever = Just $ SomeKeyInputWidget editorRef })

    -- Watch widget
    watchWidgetRef <- watchWidget

    let dimensionDistributionFn = \case
          1 -> [1]
          2 -> [0.85, 0.15]
          3 -> [0.70, 0.15, 0.15]
          a -> error $ "Unsupported widget count:" <> show a

    watchAndLogLayout <- layoutWidget Horizontal dimensionDistributionFn Nothing
    logRef <- logWidget
    insertLog logRef "Welcome to S.P.A.D.E: The Simple Programming And Debugging Environment."
    insertLog logRef "By Sandeep.C.R"
    addWidget watchAndLogLayout "log" logRef
    addWidget watchAndLogLayout watchWidgetId watchWidgetRef

    helpWidgetRef <- helpWidget ((Prelude.head . T.split (== '.'). fst) <$> builtIns) ideEventsRef
    -- The autocomplete widget
    ac <- autoComplete
    modifyWRef editorRef (\ed -> ed { ewAutocompleteWidget = Just $ SomeWidgetRef ac })

    -- The Outermost layout
    layoutRef <- layoutWidget Vertical dimensionDistributionFn (Just $ SomeKeyInputWidget editorRef)
    modifyWRef layoutRef (\lw -> lw { lowFloatingContent = Just $ SomeWidgetRef ac })

    addWidget layoutRef editorId editorRef
    addWidget layoutRef "helpwidget" helpWidgetRef
    addWidget layoutRef "watchandlog" watchAndLogLayout
    setVisibility helpWidgetRef False
    setTextFocus layoutRef editorId

    -- The menu
    let menu = Menu
          [ ("File", SubMenu ["Save (F6)", "Beautify", "Exit"])
          , ("Edit", SubMenu ["Cut", "Copy", "Paste", "Select All"])
          , ("Run", SubMenu ["Run (F5)", "Step (Ctrl+F8)"])
          , ("Windows", SubMenu ["Clear log"])
          , ("Help", SubMenu ["Contents"])
          ] Nothing
    menuContainerRef <- menuContainer (SomeWidgetRef layoutRef) menu (selectionHandler ideEventsRef)

    modifyWRef menuContainerRef (\mc -> mc { mcwDim = dim })

    modify (\ws -> ws { wsCursorWidget = Just $ SomeKeyInputWidget editorRef })

    -- Initial screen paint
    csInitialize dim
    draw menuContainerRef
    csDraw

    -- A snippet that refresh the screen.
    let ideRedraw = do
          csClear
          draw menuContainerRef
          csDraw

    let getActiveEditor = do
          (getVisibility editorRef) >>= \case
            True -> pure editorRef
            _ -> getVisibility helpWidgetRef >>= \case
              True -> hwContentWidget <$> (readWRef helpWidgetRef)
              _    -> error "No visible editor"
    let
      launchProgram = do
        source <- getContent editorRef
        liftIO (compileEither source >>= \case
            Right p  -> do
              debugIn <- newEmptyMVar
              debugOut <- newEmptyMVar
              programInputChan <- liftIO $ atomically newTChan
              threadId <- forkOS (void $ do
                liftIO $ atomically $ do
                  writeTVar inputRouteToRef (RouteProgram programInputChan)
                interpret debugIn debugOut programInputChan p)
              atomically $
                modifyTVar ideStateRef
                  (\idestate ->
                      idestate
                        { idsDebugThreadId = Just threadId
                        , idsDebugIn = Just debugIn
                        , idsDebugOut = Just debugOut
                        , idsDebugThreadInput = Just programInputChan
                        })
              pure $ Right (threadId, debugIn, debugOut, programInputChan)
            Left err -> do
              pure $ Left (hReadable err)
            )

    uiLoop ideEventsRef (\case
      IDEToggleHelp ->  do
        getVisibility helpWidgetRef >>= \case
          True -> do
            setVisibility helpWidgetRef False
            setVisibility editorRef True
            setVisibility watchAndLogLayout True
            modify (\ws -> ws { wsCursorWidget = Just $ SomeKeyInputWidget editorRef })
            liftIO $ atomically (modifyTVar ideStateRef (\is -> is { idsKeyInputReciever = Just $ SomeKeyInputWidget editorRef }))
          False -> do
            setVisibility helpWidgetRef True
            setVisibility editorRef False
            setVisibility watchAndLogLayout False
            modify (\ws -> ws { wsCursorWidget = Just $ SomeKeyInputWidget helpWidgetRef })
            liftIO $ atomically (modifyTVar ideStateRef (\is -> is { idsKeyInputReciever = Just $ SomeKeyInputWidget helpWidgetRef }))
        d <- getScreenBounds
        csInitialize d
        draw menuContainerRef
        csDraw
        pure True
      IDESave -> do
        c <- getContent editorRef
        liftIO $ T.writeFile filePath c
        insertLog logRef ("Saved to " <> (T.pack filePath) <>".")
        pure True
      IDEEdit Copy -> do
        ew <- getActiveEditor >>= readWRef
        case ewSelection ew of
          Just (r1, r2) -> let
            contents = ewContent ew
            removeLength = (r2 - r1 + 1)
            copyContent = T.take (removeLength + 1) (T.drop r1 contents)
            in liftIO $ (fst clipboard) copyContent
          Nothing -> pass
        pure True
      IDEEdit Cut -> do
        modifyWRefM editorRef (\ew -> case ewSelection ew of
          Just (r1, r2) -> let
            contents = ewContent ew
            (p1, p2) = T.splitAt r1 contents
            removeLength = (r2 - r1 + 1)
            cutContent = T.take removeLength p2
            in do
              liftIO $ (fst clipboard) cutContent
              let newCursor = max 0 (r1 - 1)
              pure $ putCursor Nothing newCursor $ ew { ewSelection = Nothing, ewContent = p1 <> (T.drop removeLength p2) }
          Nothing -> pure ew
          )
        ideRedraw
        pure True
      IDEEdit Paste -> do
        modifyWRefM editorRef (\ew -> do
          let
            contents = ewContent ew
            (p1, p2) = T.splitAt (ewCursor ew) contents
          (liftIO (snd clipboard)) >>= \case
            Nothing -> pure ew
            Just insertContent ->
              pure $ putCursor Nothing (ewCursor ew + T.length insertContent) $ ew { ewContent = p1 <> insertContent <> p2 }
          )
        ideRedraw
        pure True
      IDEEdit SelectAll -> do
        modifyWRef editorRef (\ew -> ew { ewSelection = Just (0, (T.length $ ewContent ew) - 1) })
        ideRedraw
        pure True
      IDEClearLog -> do
        modifyWRef logRef (\lw -> lw { lwContent = [], lwScrollOffset = 0 })
        pure True
      IDEBeautify -> do
        insertLog logRef "Formatting..."
        code <- getContent editorRef
        (liftIO $ compileEither @Program code) >>= \case
          Right a -> do
            setContent editorRef (T.strip $ toSource a)
            liftIO $ atomically $ writeTChan ideEventsRef IDEDraw
          Left (ParseErrorWithParsed _ _ e) -> insertLog logRef ("Parse error:" <> T.pack (show e))
        pure True
      IDEDraw -> do
        d <- getScreenBounds
        csInitialize d
        draw menuContainerRef
        csDraw
        pure True
      IDEACUpdateIdentifiers acs -> do
        liftIO $ atomically (modifyTVar ideStateRef (\is -> is { idsCodeAutocompletions = (\x -> (x, x)) <$> acs }))
        pure True
      IDEParseErrorUpdate mError -> do
        liftIO $ atomically (modifyTVar ideStateRef (\is -> is { idsCodeParseError = mError }))
        idestate <- liftIO $ readTVarIO ideStateRef
        case idsCodeParseError idestate of
          Just (loc, _) -> modifyWRef editorRef (\ew -> ew { ewParseErrorLocation = Just loc })
          Nothing -> modifyWRef editorRef (\ew -> ew { ewParseErrorLocation = Nothing })
        ideRedraw
        pure True
      IDEReqEditorContent ->  do
        c <- getContent editorRef
        liftIO $ void $ putMVar editorContentMVar c
        pure True
      IDEExit ->  do
        pure False
      IDERun -> do
        (\x' -> (idsDebugThreadId x', idsDebugIn x', idsDebugOut x', idsDebugThreadInput x')) <$> liftIO (readTVarIO ideStateRef) >>= \case
          -- If we are in a debug session, just run to completion.
          (Just threadId, Just debugIn, Just debugOut, Just programInputChan) -> do
            insertLog logRef "Continuing debug session to end..."
            executeAndUpdateIDEDebugState inputRouteToRef logRef editorRef ideEventsRef ideStateRef (Just Run) threadId debugIn debugOut programInputChan
          _ -> do
            -- Or else launch a new run of the program.
            clearscreen
            setCursorPosition 0 0
            launchProgram >>= \case
              Right (threadId, debugIn, debugOut, programInputChan) -> do
                insertLog logRef "Starting..."
                executeAndUpdateIDEDebugState inputRouteToRef logRef editorRef ideEventsRef ideStateRef (Just Run) threadId debugIn debugOut programInputChan
              Left err -> do
                insertLog logRef $ "Compile Error:" <> err
                liftIO $ atomically $ writeTChan ideEventsRef IDEDraw
        pure True
      IDEStep -> do
        csSetCursorPosition 0 0
        (\x' -> (idsDebugThreadId x', idsDebugIn x', idsDebugOut x', idsDebugThreadInput x')) <$> liftIO (readTVarIO ideStateRef) >>= \case
          -- A debugging session exists already.
          -- Send the step command, wait for updated state to arrive.
          (Just threadId, Just debugIn, Just debugOut, Just programInputChan) ->
            executeAndUpdateIDEDebugState inputRouteToRef logRef editorRef ideEventsRef ideStateRef (Just StepIn) threadId debugIn debugOut programInputChan
          _ -> do
            -- No debugging session exist. Launch one.
            launchProgram >>= \case
              Right (threadId, debugIn, debugOut, programInputChan) -> do
                insertLog logRef "Starting..."
                executeAndUpdateIDEDebugState inputRouteToRef logRef editorRef ideEventsRef ideStateRef Nothing threadId debugIn debugOut programInputChan
              Left err -> insertLog logRef $ "Compile Error:" <> err
        pure True
      IDETerminalEvent (TerminalResize w h)  -> do
        let dim' = Dimensions w h
        csInitialize dim'
        modifyWRef menuContainerRef (\mc -> mc { mcwDim = dim' })
        draw menuContainerRef
        adjustScrollOffset editorRef
        draw menuContainerRef
        csDraw
        pure True
      IDEDebugUpdate mds -> do
        case mds of
          Just ds -> do
            modifyWRef editorRef (\ew -> ew { ewReadOnly = True, ewDebugLocation = Just $ dsLocation ds, ewCursor = lcOffset (dsLocation ds) - 1 })
            modifyWRef watchWidgetRef (\ww -> ww { wwVisible = True, wwContent = scopeToWatchItems $ dsScope ds })
            case dsCurrenEvaluation ds of
              Just cv -> insertLog logRef ("Evaluating: " <> cv)
              Nothing -> pass

          Nothing -> do
            modifyWRef editorRef (\ew -> ew { ewReadOnly = False, ewDebugLocation = Nothing })
            modifyWRef watchWidgetRef (\ww -> ww { wwVisible = False })
        liftIO $ atomically $ writeTChan ideEventsRef IDEDraw
        pure True
      IDETerminalEvent (TerminalKey ev)  -> case ev of
        KeyChar True _ _ 'c' -> pure False
        KeyChar True _ _ 'C' -> pure False
        k -> do
          updateMenuOnKey menuContainerRef k >>= \case
            True -> pure ()
            False -> shortcutsHandler ideEventsRef k >>= \case
              True -> pure ()
              False -> idsKeyInputReciever <$> (liftIO $ readTVarIO ideStateRef) >>= \case
                Just (SomeKeyInputWidget w) -> handleInput w k
                Nothing                     -> pure ()
          idestate <- liftIO $ readTVarIO ideStateRef
          case idsCodeParseError idestate of
            Just (loc, _) -> modifyWRef editorRef (\ew -> ew { ewParseErrorLocation = Just loc })
            Nothing -> modifyWRef editorRef (\ew -> ew { ewParseErrorLocation = Nothing })
          ideRedraw
          pure True
      )
    where
      executeAndUpdateIDEDebugState inputRouteToRef logRef editorRef ideEventRef ideStateRef mcmd threadId debugIn debugOut programInputChan  = do
          case mcmd of
            Just cmd -> do
                liftIO $ do
                  -- Empty out any pending debug message, and discard it.
                  void $ tryTakeMVar debugOut
                  -- before sending in a debug input.
                  atomically $ writeTVar inputRouteToRef (RouteProgram programInputChan)
                  putMVar debugIn cmd
            _        -> pure ()
          (liftIO (catch (do
              dd <- takeMVar debugOut
              atomically $ writeTVar inputRouteToRef RouteIDE
              pure dd
              )
              -- Possibly handle a control-c triggered by user to break
              -- a long runing operation.
              (\(e :: SomeException) -> do
                killThread threadId
                pure $ Errored $ T.pack $ show e
              )
            )) >>= \case
            -- Update the IDE debug state with the received debug state.
            DebugData ds -> do
              liftIO $ atomically $ writeTChan ideEventRef (IDEDebugUpdate $ Just ds)
            Errored err -> do
              liftIO $ atomically $ modifyTVar ideStateRef (\i -> ideStartState { idsKeyInputReciever = idsKeyInputReciever i })
              modifyWRef editorRef (\ew -> ew { ewReadOnly = False })
              insertLog logRef err
              liftIO $ atomically $ do
                writeTChan ideEventRef (IDEDebugUpdate Nothing)
            Finished -> do
              modifyWRef editorRef (\ew -> ew { ewReadOnly = False })
              liftIO $ atomically $ modifyTVar ideStateRef (\i -> ideStartState { idsKeyInputReciever = idsKeyInputReciever i })
              insertLog logRef "Finished program"
              liftIO $ atomically $ do
                writeTChan ideEventRef (IDEDebugUpdate Nothing)

updateMenuOnKey :: WidgetC m => WRef MenuContainerWidget -> KeyEvent -> m Bool
updateMenuOnKey ref kv = do
  w <- readWRef ref
  case (kv, menuActive $ mcwMenu w) of
    (KeyCtrl _ _ _ Esc, Just _) -> do
        modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Nothing }})
        setCursorVisibility True
        pure True
    (KeyChar _ _ True c, _) -> do
      foldM_ (\x (n, _) -> case T.uncons (T.toLower n) of
        Nothing -> pure $ x + 1
        Just (c1, _) ->
          if c == c1
            then do
              modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x, 0) }})
              setCursorVisibility False
              pure $ x + 1
            else
              pure $ x + 1) 0 (menuItems $ mcwMenu w)
      pure True
    (KeyCtrl _ _ _ ArrowLeft, Just (x, _))
      | x > 0 -> do
          modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x - 1, 0) }})
          pure True
      | otherwise -> do
          modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (Prelude.length (menuItems $ mcwMenu w) - 1, 0) }})
          pure True

    (KeyCtrl _ _ _ ArrowRight, Just (x, _)) -> do
      let menuItemsLength = Prelude.length $ menuItems $ mcwMenu w
      let newX = x + 1
      if newX < menuItemsLength
        then do
          modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (newX, 0) }})
          pure True
        else do
          modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (0, 0) }})
          pure True

    (KeyCtrl _ _ _ ArrowUp, Just (x, y))
      | y > 0 -> do
              modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x, y - 1) }})
              pure True
      | otherwise -> do
          case getSubmenuAt (mcwMenu w) x of
            Nothing -> pure True
            Just (SubMenu smi) -> do
              modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x, Prelude.length smi - 1) }})
              pure True

    (KeyCtrl _ _ _ ArrowDown, Just (x, y)) -> do
      case getSubmenuAt (mcwMenu w) x of
        Just (SubMenu smi) -> do
          let newY = y + 1
          if newY < (Prelude.length smi)
            then do
              modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x, newY) }})
              pure True
            else do
              modifyWRef ref (\w' -> w' { mcwMenu = (mcwMenu w) { menuActive = Just (x, 0) }})
              pure True
        Nothing -> pure True
    (KeyCtrl _ _ _ Return, Just (x, y)) -> do
      (mcwSelectionHandler w) ref (x, y)
      setCursorVisibility True
      pure True
    _ -> pure False

scopeToWatchItems :: Scope -> [(Text, Text)]
scopeToWatchItems s = Map.foldlWithKey' (\o k v ->
  let
    vKey = case k of
      SkIdentifier i -> unIdentifer i
      SkOperator op  -> T.pack $ show op
    vText = case v of
      StringValue t                    -> Just t
      NumberValue (NumberInt t)        -> Just $ T.pack $ show t
      NumberValue (NumberFractional t) -> Just $ T.pack $ show t
      BoolValue t                      -> Just $ T.pack $ show t
      ArrayValue t                     -> Just $ T.pack $ show t
      ObjectValue t                    -> Just $ T.pack $ show t
      _                                -> Nothing

  in case vText of
    Just t  -> (vKey, t) : o
    Nothing -> o
  ) [] s

selectionHandler
  :: forall m. WidgetC m
  => TChan IDEEvent
  -> WRef MenuContainerWidget
  -> (Int, Int)
  -> m ()
selectionHandler ideEventsRef ref selection = do
  case selection of
    (0, 0) -> liftIO $ atomically $ writeTChan ideEventsRef IDESave
    (0, 1) -> liftIO $ atomically $ writeTChan ideEventsRef IDEBeautify
    (0, 2) -> liftIO $ atomically $ writeTChan ideEventsRef IDEExit
    (1, 0) -> liftIO $ atomically $ writeTChan ideEventsRef (IDEEdit Cut)
    (1, 1) -> liftIO $ atomically $ writeTChan ideEventsRef (IDEEdit Copy)
    (1, 2) -> liftIO $ atomically $ writeTChan ideEventsRef (IDEEdit Paste)
    (1, 3) -> liftIO $ atomically $ writeTChan ideEventsRef (IDEEdit SelectAll)
    (2, 0) -> liftIO $ atomically $ writeTChan ideEventsRef IDERun
    (2, 1) -> liftIO $ atomically $ writeTChan ideEventsRef IDEStep
    (3, 0) -> liftIO $ atomically $ writeTChan ideEventsRef IDEClearLog
    (4, 0) -> liftIO $ atomically $ writeTChan ideEventsRef IDEToggleHelp
    _      -> pass
  modifyWRef ref (\mcw -> mcw { mcwMenu = (mcwMenu mcw) { menuActive = Nothing } })
  liftIO $ atomically $ writeTChan ideEventsRef IDEDraw

shortcutsHandler
  :: forall m. WidgetC m
  => TChan IDEEvent
  -> KeyEvent
  -> m Bool
shortcutsHandler ideEventRef kv = do
    case kv of
      KeyCtrl _ _ _ (Fun 6) -> do
        liftIO $ atomically $ writeTChan ideEventRef IDESave
        pure True

      KeyCtrl _ _ _ (Fun 1) -> do
        liftIO $ atomically $ writeTChan ideEventRef IDEToggleHelp
        pure True

      KeyCtrl True _ _ (Fun 8) -> do
        liftIO $ atomically $ writeTChan ideEventRef IDEStep
        pure True

      KeyCtrl _ _ _ (Fun 5) -> do
        liftIO $ atomically $ writeTChan ideEventRef IDERun
        pure True
      _               -> pure False