packages feed

spade-0.1.0.9: src/IDE/Help.hs

module IDE.Help where

import           Compiler.Lexer.Keywords      (Keyword)
import           Compiler.Lexer.Operators     (Operator)
import           Control.Concurrent
import           Control.Concurrent.STM       (atomically)
import           Control.Concurrent.STM.TChan
import           Control.Monad                as CM
import qualified Data.ByteString              as BS
import           Data.IORef
import qualified Data.List                    as DL
import qualified Data.Map                     as Map
import qualified Data.Set                     as Set
import qualified Data.Text                    as T
import           Data.Text.Encoding           (decodeUtf8)
import           Data.Vector                  as V

import           Common
import           Compiler.AST.Program
import           Compiler.Parser
import           IDE.Common
import           IDE.Help.Contents
import           IDE.Help.Parser
import           UI.Widgets
import           UI.Widgets.Layout
import           UI.Widgets.NullWidget

newtype SearchPage = SearchPage Text
  deriving (Show, Eq)

type HelpPage = Either SearchPage FilePath

data HelpWidget = HelpWidget
  { hwSearchWidget          :: WRef EditorWidget
  , hwContentWidget         :: WRef EditorWidget
  , hwMainLayout            :: WRef LayoutWidget
  , hwVisible               :: Bool
  , hwCursorRef             :: MVar Int
  , hwTokenProcessingThread :: Maybe ThreadId
  , hwIdeEventsChan         :: TChan IDEEvent
  , hwTokensRef             :: IORef (V.Vector DisplayToken)
  , hwContent               :: Map.Map FilePath (Text, V.Vector DisplayToken)
  , hwContentIndex          :: Map.Map Text (FilePath, Int)
  , hwCurrentPage           :: Maybe HelpPage
  , hwHistory               :: [(HelpPage, Int)]
  , hwSearchResult          :: MVar (SearchPage, V.Vector HToken)
  , hwSearchChan            :: TChan SearchPage
  , hwBuiltIns              :: Map.Map Text Text
  }

instance Drawable HelpWidget where
  draw r = do
    w <- readWRef r
    case hwVisible w of
      True -> do
        (liftIO $ tryTakeMVar (hwSearchResult w)) >>= \case
          Just (sp, htokens) -> do
            let (contentText, dTokens) = convertToDisplay (hwBuiltIns w) htokens
            loadTokens r (contentText, dTokens)
            modifyWRef r (\wd -> wd { hwCurrentPage = Just (Left sp) })
          Nothing -> pass
        draw $ hwMainLayout w
      _    -> pass
  setVisibility r v = do
    modifyWRef r (\hw -> hw { hwVisible = v })
  getVisibility r =
    hwVisible <$> readWRef r

instance KeyInput HelpWidget where
  getCursorInfo r = do
    w <- readWRef r
    getCursorInfo $ hwSearchWidget w
  handleInput :: forall m. WidgetC m => WRef HelpWidget -> KeyEvent -> m ()
  handleInput r ke = do
    w <- readWRef r
    let
      pushCurrentPageToHistory :: m ()
      pushCurrentPageToHistory = do
        case hwCurrentPage w of
          Just p -> do
            cw <- readWRef (hwContentWidget w)
            let currentOffset = ewCursor cw
            modifyWRef r (\hw ->
              hw { hwHistory = (p, currentOffset) : hwHistory hw }
              )
          Nothing -> pure ()

      loadPage' :: HelpPage -> Int -> m ()
      loadPage' page idx  = do
        loadPage r page
        modifyWRef (hwContentWidget w) (\x -> let
          newEw = putCursor Nothing idx x
          newSo = computeScrollOffset newEw (fst $ ewCursorInfo newEw)
          in newEw { ewScrollOffset = if newSo /= ewScrollOffset x then newSo + (div (diH $ ewDim newEw) 2) else newSo }
          )
    case ke of
      KeyCtrl False False False Esc    -> do
        case hwHistory w of
          [] -> liftIO $ atomically $ writeTChan (hwIdeEventsChan w) IDEToggleHelp
          ((p, o) :rst) -> do
            modifyWRef r (\hw -> hw { hwHistory = rst })
            loadPage' p o
      -- In the matches below don't insist that shift is not pressed
      -- so that shift+arrow key selections are possible.
      KeyCtrl False _ False ArrowUp    -> handleInput (hwContentWidget w) ke
      KeyCtrl False _ False ArrowDown  -> handleInput (hwContentWidget w) ke
      KeyCtrl False _ False ArrowRight -> handleInput (hwContentWidget w) ke
      KeyCtrl False _ False ArrowLeft  -> handleInput (hwContentWidget w) ke
      KeyCtrl False _ False PageUp     -> handleInput (hwContentWidget w) ke
      KeyCtrl False _ False PageDown   -> handleInput (hwContentWidget w) ke
      KeyCtrl False False False Return     -> do
        tokens <- liftIO $ readIORef (hwTokensRef w)
        case V.find (\case
          -- True in the first field signifies cursor is over this link
          DisplayToken (DTLink True _) _ _ -> True
          _                                -> False) tokens of
            Just (DisplayToken (DTLink _ n) _ _) -> do
              case Map.lookup n (hwContentIndex w) of
                Just (page, idx) -> do
                  pushCurrentPageToHistory
                  loadPage' (Right page) idx
                Nothing -> pure ()
            _ -> pure ()
      _                        -> do
        if hasKeyCombination ke
          -- This check prevents unintendend, redundant firing of search.
          -- which was causing a loaded page to immediately replaced by
          -- the search results that precedded the loading of this page.
          then pass
          else do
            handleInput (hwSearchWidget w) ke
            swr <- readWRef (hwSearchWidget w)
            let newPage = Left $ SearchPage $ ewContent swr
            loadPage' newPage 0

    ew <- readWRef (hwContentWidget w)
    void $ liftIO $ tryPutMVar (hwCursorRef w) (ewCursor ew)
    where
      hasKeyCombination (KeyCtrl False False False _) = False
      hasKeyCombination (KeyChar False False False _) = False
      hasKeyCombination _                             = True

instance Moveable HelpWidget where
  move r pos = do
    w <- hwMainLayout <$> readWRef r
    move w pos
  getPos r = do
    w <- hwMainLayout <$> readWRef r
    getPos w
  getDim r = do
    w <- hwMainLayout <$> readWRef r
    getDim w
  resize r fn = do
    w <- hwMainLayout <$> readWRef r
    resize w fn

instance Widget HelpWidget where
  hasCapability (DrawableCap _) = Just Dict
  hasCapability (KeyInputCap _) = Just Dict
  hasCapability (MoveableCap _) = Just Dict
  hasCapability _               = Nothing

searchThread
  :: Map.Map Text (FilePath, Int)
  -> TChan SearchPage
  -> MVar (SearchPage, V.Vector HToken)
  -> IO ()
searchThread contentIndex spChan resultRef = do
  let indexKeys = Map.keys contentIndex
  forever $ do
    SearchPage (T.toLower -> skey) <- atomically $ readTChan spChan
    let results = DL.foldl' (foldFn skey) [] indexKeys
    putMVar resultRef (SearchPage skey, V.fromList (mkHToken <$> results))
  where
    mkHToken x = HToken x emptyLocation 0
    foldFn :: Text -> [HTokenRaw] -> Text -> [HTokenRaw]
    foldFn k rs hd = if k `T.isInfixOf` (T.toLower hd) then (Link hd : NewLine : rs) else rs

loadPage
  :: WidgetC m
  => WRef HelpWidget
  -> HelpPage
  -> m ()
loadPage ref page' = do
  HelpWidget { hwContent = map' } <- readWRef ref
  case page' of
    Right page -> case Map.lookup page map' of
      Nothing -> pure ()
      Just (contentText, dtTokens) -> do
        loadTokens ref (contentText, dtTokens)
        modifyWRef ref (\hw -> hw { hwCurrentPage = Just page' })
    Left (SearchPage "") -> loadPage ref (Right "toc.md")
    Left sk -> do
      w <- readWRef ref
      liftIO $ atomically $ writeTChan (hwSearchChan w) sk

loadTokens
  :: WidgetC m
  => WRef HelpWidget
  -> (Text, V.Vector DisplayToken)
  -> m ()
loadTokens helpWidgetRef (contentText, dtTokens) = do
  HelpWidget { hwIdeEventsChan = ideEventRef, hwContentWidget = ewRef, hwTokensRef = tokensRef} <- readWRef helpWidgetRef
  liftIO $ writeIORef tokensRef dtTokens
  modifyWRef ewRef (\ew -> (resetCursor ew) { ewContent = contentText, ewShowVirtualCursor = True })
  liftIO $ atomically $ writeTChan ideEventRef IDEClearDraw

collectLinks
  :: Map.Map FilePath (Text, V.Vector DisplayToken)
  -> [Text]
collectLinks docs = Map.foldrWithKey mapFoldFn mempty docs
  where
    mapFoldFn
      :: FilePath
      -> (Text, V.Vector DisplayToken)
      -> [Text]
      -> [Text]
    mapFoldFn fp (_, tkns) idx = V.foldl' (foldFn fp) idx tkns

    foldFn
      :: FilePath
      -> [Text]
      -> DisplayToken
      -> [Text]
    foldFn _ idx (DisplayToken (DTLink _ t) _ _) = t : idx
    foldFn _ idx _                               = idx

buildIndex
  :: Map.Map FilePath (Text, V.Vector DisplayToken)
  -> Map.Map Text (FilePath, Int)
buildIndex docs = Map.foldrWithKey mapFoldFn mempty docs
  where
    mapFoldFn
      :: FilePath
      -> (Text, V.Vector DisplayToken)
      -> Map.Map Text (FilePath, Int)
      -> Map.Map Text (FilePath, Int)
    mapFoldFn fp (_, tkns) idx = V.foldl' (foldFn fp) idx tkns

    foldFn
      :: FilePath
      -> Map.Map Text (FilePath, Int)
      -> DisplayToken
      -> Map.Map Text (FilePath, Int)
    foldFn fp idx (DisplayToken (DTHeading _ t) l _) = Map.insert t (fp, lcOffset l) idx
    foldFn _ idx _ = idx

tokenProcessing :: TChan IDEEvent -> MVar Int -> IORef (V.Vector DisplayToken) -> IO ()
tokenProcessing ideEventRef cursorRef tokensRef = forever $ do
  cursor <- takeMVar cursorRef
  modifyIORef tokensRef (V.map (mapFn cursor))
  liftIO $ atomically $ writeTChan ideEventRef IDEClearDraw
  where
    mapFn :: Int -> DisplayToken -> DisplayToken
    mapFn cursor dt = if cursor >= (lcOffset $ dtLocation dt) && cursor <= (dtOffsetEnd dt)
      then case dt of
        DisplayToken (DTLink _ n) l e -> DisplayToken (DTLink True n) l e
        _                             -> dt
      else case dt of
        DisplayToken (DTLink _ n) l e -> DisplayToken (DTLink False n) l e
        _                             -> dt

parsedDocContent :: Map.Map Text Text -> IO (Map.Map FilePath (Text, V.Vector DisplayToken))
parsedDocContent builtIns = UI.Widgets.foldM foldFn mempty docContent
  where
    foldFn
      :: Map.Map FilePath (Text, V.Vector DisplayToken)
      -> (FilePath, BS.ByteString)
      -> IO (Map.Map FilePath (Text, V.Vector DisplayToken))
    foldFn idx (fp,sbs) = do
      parseHelp (decodeUtf8 sbs) >>= \case
        Right tokens -> do
          checkSamples fp tokens
          pure $ Map.insert fp (convertToDisplay builtIns (V.fromList tokens)) idx
        Left err -> do
          putStrLn err
          error $ "Error while parsing help file: " <> fp

checkSamples :: FilePath -> [HToken] -> IO ()
checkSamples fp = CM.mapM_ checkOneSample . fmap tkRaw
  where
    checkOneSample :: HTokenRaw -> IO ()
    checkOneSample (Code _ tks) = parseEither @Program tks >>= \case
      Right _  -> pure ()
      Left err -> do
        error ("Error parsing code in help file:"<> fp <> ":" <> show err)
    checkOneSample _ = pure ()

helpWidget :: WidgetC m => Map.Map Text Text -> TChan IDEEvent -> m (WRef HelpWidget)
helpWidget builtinsFull ideEventRef = do
    let builtins = (Prelude.head . T.split (== '.')) <$> (Map.keys builtinsFull)
    -- The help window
    let helpWidgetDimDistrbution = [
          undefined
          , [1]
          , [0, 1]
          ]
    let helpWidgetTopDimDistrbution =
          [ undefined
          , undefined
          , [0.1, 0.9]
          ]
    cursorRef <- liftIO $ newMVar @Int 0
    searchResultRef <- liftIO newEmptyMVar
    searchChan <- liftIO (newTChanIO @SearchPage)
    helpLayoutRef <- layoutWidget Vertical helpWidgetDimDistrbution
    helptopLayoutRef <- layoutWidget Horizontal helpWidgetTopDimDistrbution
    helpSearchInput <- editor (\_ -> pure []) Nothing

    tokensRef <- liftIO $ newIORef @(V.Vector DisplayToken) V.empty

    tokenProcessingThreadId <- liftIO $ forkIO (tokenProcessing ideEventRef cursorRef tokensRef)

    helpContent <- editor (\_ -> pure []) (Just $ SomeTokenStream (V.toList <$> readIORef tokensRef))
    modifyWRef helpSearchInput (\hw -> hw { ewParams = (ewParams hw) { epBorder = True, epGutterSize = 0, epLineNos = False} })
    modifyWRef helpContent (\hw -> hw { ewShowVirtualCursor = True, ewParams = (ewParams hw) { epLineNos = False} })
    addWidget helptopLayoutRef helpSearchInput
    (newWRef nullWidget) >>= addWidget helptopLayoutRef
    addWidget helpLayoutRef helptopLayoutRef
    addWidget helpLayoutRef helpContent
    docs <- liftIO $ parsedDocContent builtinsFull
    let docIndex = buildIndex docs
    void $ liftIO $ forkIO (searchThread docIndex searchChan searchResultRef)
    let builtinOperators = "=" : ((\x -> toSource $ toEnum @Operator x) <$> [0 .. fromEnum $ maxBound @Operator])
    let builtinKeywords = (\x -> toSource $ toEnum @Keyword x) <$> [0 .. fromEnum $ maxBound @Keyword]
    let linksInDocs = Set.fromList $ collectLinks docs
    let linksInIndex = Set.fromList $ Map.keys docIndex
    let builtinSymbols = Set.fromList (T.dropWhileEnd (\c -> c == '(' || c == ')') <$> builtins)
    let missingLinks = Set.difference linksInDocs linksInIndex
    let missingDocs = Set.difference (Set.difference builtinSymbols (Set.fromList (builtinKeywords <> builtinOperators))) linksInIndex
    if (Set.size(missingLinks) > 0)
      then error $ "Missing links in document: " <> show missingLinks
      else do
        if (Set.size(missingDocs) > 0)
          then error $ "Missing documents for: " <> show missingDocs
          else do
            r <- newWRef $ HelpWidget helpSearchInput helpContent helpLayoutRef True cursorRef (Just tokenProcessingThreadId) ideEventRef tokensRef docs docIndex Nothing [] searchResultRef searchChan builtinsFull
            loadPage r (Right "toc.md")
            pure r