spade-0.1.0.0: 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
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
}
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 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 _ _ _ Esc -> do
case hwHistory w of
[] -> pure ()
((p, o) :rst) -> do
modifyWRef r (\hw -> hw { hwHistory = rst })
loadPage' p o
KeyCtrl _ _ _ ArrowUp -> handleInput (hwContentWidget w) ke
KeyCtrl _ _ _ ArrowDown -> handleInput (hwContentWidget w) ke
KeyCtrl _ _ _ ArrowRight -> handleInput (hwContentWidget w) ke
KeyCtrl _ _ _ ArrowLeft -> handleInput (hwContentWidget w) ke
KeyCtrl _ _ _ Return -> do
tokens <- liftIO $ readIORef (hwTokensRef w)
case V.find (\case
DisplayToken (DTLink True _) _ _ -> True
_ -> False) tokens of
Just (DisplayToken (DTLink _ n) _ _) -> case Map.lookup n (hwContentIndex w) of
Just (page, idx) -> do
pushCurrentPageToHistory
loadPage' (Right page) idx
Nothing -> pure ()
_ -> pure ()
_ -> do
handleInput (hwSearchWidget w) ke
swr <- readWRef (hwSearchWidget w)
loadPage' (Left (SearchPage $ ewContent swr)) 0
ew <- readWRef (hwContentWidget w)
void $ liftIO $ tryPutMVar (hwCursorRef w) (ewCursor ew)
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 -> ew { ewScrollOffset = 0, ewContent = contentText, ewShowVirtualCursor = True })
liftIO $ atomically $ writeTChan ideEventRef IDEDraw
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 IDEDraw
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 :: IO (Map.Map FilePath (Text, V.Vector DisplayToken))
parsedDocContent = 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 (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 => [Text] -> TChan IDEEvent -> m (WRef HelpWidget)
helpWidget builtins ideEventRef = do
-- The help window
let helpWidgetDimDistrbution = \case
1 -> [1]
2 -> [0, 1]
_ -> error "Unsupported widget count"
let helpWidgetTopDimDistrbution = \case
2 -> [0.1, 0.9]
_ -> error "Unsupported widget count"
cursorRef <- liftIO $ newMVar @Int 0
searchResultRef <- liftIO newEmptyMVar
searchChan <- liftIO (newTChanIO @SearchPage)
helpLayoutRef <- layoutWidget Vertical helpWidgetDimDistrbution Nothing
helptopLayoutRef <- layoutWidget Horizontal helpWidgetTopDimDistrbution Nothing
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) { epLineNos = False} })
modifyWRef helpContent (\hw -> hw { ewShowVirtualCursor = True, ewParams = (ewParams hw) { epLineNos = False} })
addWidget helptopLayoutRef "helpsearchinput" helpSearchInput
(newWRef nullWidget) >>= addWidget helptopLayoutRef "helpsearchspacer"
addWidget helpLayoutRef "helpsearch" helptopLayoutRef
addWidget helpLayoutRef "helpcontent" helpContent
docs <- liftIO parsedDocContent
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 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
loadPage r (Right "toc.md")
pure r