spectacle-1.0.0: src/Language/Spectacle/Interaction/Diagram.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Language.Spectacle.Interaction.Diagram where
import Control.Applicative (liftA2)
import Control.Arrow (Kleisli (Kleisli))
import Control.Comonad (Comonad (duplicate))
import Control.Comonad.Store (extract)
import Data.Foldable (Foldable (fold))
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Maybe ()
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import Lens.Micro ((^.))
import Lens.Micro.Extras (view)
import Prettyprinter (Doc, pretty)
import qualified Prettyprinter as Doc
import qualified Prettyprinter.Internal as Doc.Internal
import Prettyprinter.Render.Terminal (AnsiStyle, Color (Blue, Cyan, Green, Magenta, Red, Yellow))
import qualified Prettyprinter.Render.Terminal as Doc
import Control.Comonad.Tape (Tape (Tape), after, before, focus, tabulatel, tabulater, viewl)
import Data.Fingerprint (Fingerprint)
import Data.Traversable (for)
import Language.Spectacle.Interaction.Doc
( Cardinal (CLeft, CRight),
cline,
hline,
tab,
tack,
turnLeftUp,
turnRightUp,
turnUpLeft,
turnUpRight,
)
import qualified Language.Spectacle.Interaction.Doc as Doc
import Language.Spectacle.Interaction.Paths (takeMinRow)
import Language.Spectacle.Interaction.Point (Point, column, extent, fields, label, parent)
-- ---------------------------------------------------------------------------------------------------------------------
data LogNode = LogNode
{ nodeColor :: Color
, nodeLabel :: Fingerprint
}
-- | @'traverseRowsOf' f ps@ is a traversal of @f@ on rows of @ps@, collecting the results in a list.
--
-- @since 1.0.0
traverseRowsOf :: (Set Point -> DiagramM a) -> Set Point -> DiagramM [a]
traverseRowsOf f ps =
case takeMinRow ps of
(next, paths')
| Set.null paths' -> pure @[] <$> f next
| otherwise -> liftA2 (:) (f next) (traverseRowsOf f paths')
diagramFull :: Set Point -> DiagramM (Doc AnsiStyle)
diagramFull points = do
rowDocs <- traverseRowsOf diagramRow points
pure (Doc.concatWith (flip mappend) rowDocs)
diagramRow :: Set Point -> DiagramM (Doc AnsiStyle)
diagramRow rows =
case viewl (foldMap Seq.singleton rows) of
Nothing -> pure mempty
Just points -> do
branchDoc <- branchSection points
subtreeDoc <- subtreeDiagram points
curveDocs <- curveSection points
return (branchDoc <> Doc.line <> curveDocs <> subtreeDoc)
subtreeDiagram :: Tape Point -> DiagramM (Doc AnsiStyle)
subtreeDiagram = fmap (foldMap (`mappend` Doc.line)) . traverse pointSection . duplicate
pointSection :: Tape Point -> DiagramM (Doc AnsiStyle)
pointSection ps = do
lx <- labelSection ps
fs <- fieldSection ps
pure (lx <> Doc.vsep fs)
-- | @'labelSection' points@ documents the label of the focused points as well as neighboring paths, for example:
--
-- @
-- | * | 0x8f46a202
-- @
--
-- @since 1.0.0
labelSection :: Tape Point -> DiagramM (Doc AnsiStyle)
labelSection ps = do
paths <- labelPaths ps
let lx = Doc.annotate (Doc.colorDull Yellow) (pretty (focus ps ^. label))
pure (paths <> lx <> Doc.line)
-- | @'fieldSection' points@ documents each of the fields in the focused point as well a neighboring paths, for example:
--
-- @
-- | | | #field1 = "foo"
-- | | | #field2 = 12345
-- @
--
-- @since 1.0.0
fieldSection :: Tape Point -> DiagramM [Doc AnsiStyle]
fieldSection points = do
paths <- fieldPaths points
let fs = extract points ^. fields
pure (map (mappend paths . Doc.indent 2) fs)
branchSection :: Tape Point -> DiagramM (Doc AnsiStyle)
branchSection = foldr cons (pure mempty) . duplicate
where
cons points
| focus points ^. extent == 0 = id
| otherwise = liftA2 (<>) (go points)
go points@Tape {..} = do
doc <- branchSegment points lcol ucol
withColor doc (focus ^. label)
where
lcol = sum (view extent <$> before)
ucol = lcol + focus ^. extent
curveSection :: Tape Point -> DiagramM (Doc AnsiStyle)
curveSection = foldr cons (pure mempty) . duplicate
where
cons points
| focus points ^. extent == 0 = id
| otherwise = liftA2 (mappend) (go points)
go points@Tape {..}
| lcol <= col && col < ucol = pure mempty
| otherwise = do
curve <- curveSegment points lcol ucol
withColor (curve <> Doc.line) (focus ^. label)
where
lcol = sum (view extent <$> before)
ucol = lcol + focus ^. extent
col = focus ^. column
curveSegment :: Tape Point -> Int -> Int -> DiagramM (Doc AnsiStyle)
curveSegment points lcol ucol
| col < lcol = do
let curve = rightTurnSegment (lcol - col) <> turnRightUp
ls <- for (tabulatel points) \ps -> do
if focus ps ^. extent == 0
then pure tab
else withColor (verticalSegment 1) (focus ps ^. label)
withColor (fold ls <> curve) lx
| ucol < 1 + col = do
let curve = turnLeftUp <> Doc.copies (2 * (col - ucol) + 1) hline <> turnUpLeft
rs <- for (tabulater points) \ps -> do
if focus ps ^. extent == 0
then pure tab
else withColor (verticalSegment 1) (focus ps ^. label)
ls <- for (tabulatel points) \ps -> do
if focus ps ^. extent == 0
then pure mempty
else do
let spc = Doc.tabs (focus ps ^. extent - 1)
doc <- withColor (verticalSegment 1) (focus ps ^. label)
pure (spc <> doc)
let spc = Doc.tabs (focus points ^. extent - 1)
withColor (fold ls <> spc <> curve <> Doc.space <> fold rs) lx
| otherwise = pure mempty
where
col = focus points ^. column
lx = focus points ^. label
-- | @'labelPaths' points@ inserts a line segment for each point in @points@, and a asterrisk for the point under
-- focus.
--
-- @since 1.0.0
labelPaths :: Tape Point -> DiagramM (Doc AnsiStyle)
labelPaths = iteratePaths (const (pure "* "))
-- | @'labelPaths' points@ inserts a line segment for each point in @points@.
--
-- @since 1.0.0
fieldPaths :: Tape Point -> DiagramM (Doc AnsiStyle)
fieldPaths = iteratePaths foci
where
foci point = case point ^. parent of
Nothing -> pure tab
Just lx -> withColor (verticalSegment 1) lx
branchSegment :: Tape Point -> Int -> Int -> DiagramM (Doc AnsiStyle)
branchSegment points lcol ucol
| col <= lcol = pure $ rightFanoutSegment len
| ucol <= 1 + col = pure $ leftFanoutSegment len
| otherwise = do
let ls = leavesSegment (col - lcol - 1)
let rs = leavesSegment (ucol - col - 2)
let fans = Doc.cat [turnLeftUp, hline, ls, cline, Doc.hline, rs, turnRightUp]
pure (fans <> Doc.space)
where
col = focus points ^. column
len = focus points ^. extent
iteratePaths :: (Point -> DiagramM (Doc AnsiStyle)) -> Tape Point -> DiagramM (Doc AnsiStyle)
iteratePaths f = iterateLine groupBefore f groupAfter
groupAfter :: Point -> DiagramM (Doc AnsiStyle)
groupAfter point = do
let len = min (point ^. extent) 1
let lx = point ^. label
withColor (verticalSegment len) lx
groupBefore :: Point -> DiagramM (Doc AnsiStyle)
groupBefore point =
case point ^. parent of
Nothing -> pure Doc.tab
Just lx -> withColor (verticalSegment 1) lx
iterateLine ::
(Point -> DiagramM (Doc AnsiStyle)) ->
(Point -> DiagramM (Doc AnsiStyle)) ->
(Point -> DiagramM (Doc AnsiStyle)) ->
Tape Point ->
DiagramM (Doc AnsiStyle)
iterateLine handleLT handleEQ handleGT points = do
lt <- foldr (liftA2 (<>) . handleLT) (pure mempty) (before points)
gt <- foldr (liftA2 (<>) . handleGT) (pure mempty) (after points)
eq <- handleEQ (focus points)
pure (lt <> eq <> gt)
-- | @'verticalSegment' len@ fills a line of length @len@ comprised of segments "│ ".
--
-- @since 1.0.0
verticalSegment :: Int -> Doc AnsiStyle
verticalSegment len
| len <= 0 = Doc.tab
| len == 1 = Doc.Internal.Text 2 "│ "
| otherwise = Doc.copies len (Doc.Internal.Text 2 "│ ")
leftFanoutSegment :: Int -> Doc AnsiStyle
leftFanoutSegment len
| len <= 0 = mempty
| len == 1 = Doc.Internal.Text 2 "│ "
| otherwise =
let begin = Doc.Internal.Text 2 "╰─"
end = tack CLeft <> Doc.space
in begin <> leavesSegment (len - 2) <> end
rightFanoutSegment :: Int -> Doc AnsiStyle
rightFanoutSegment len
| len <= 0 = mempty
| len == 1 = Doc.Internal.Text 2 "│ "
| otherwise =
let begin = tack CRight <> Doc.hline
end = Doc.Internal.Text 2 "╯ "
in begin <> leavesSegment (len - 2) <> end
rightTurnSegment :: Int -> Doc AnsiStyle
rightTurnSegment len
| len <= 0 = mempty
| len == 1 = Doc.Internal.Text 2 "╭─"
| otherwise = turnUpRight <> Doc.copies (2 * len - 1) (Doc.Internal.Char '─')
-- | @'leavesPaths' len@ fills a line of length @len@ comprised of segments "┴─".
--
-- @since 1.0.0
leavesSegment :: Int -> Doc AnsiStyle
leavesSegment len
| len <= 0 = mempty
| len == 1 = Doc.Internal.Text 2 "┴─"
| otherwise = Doc.copies len (Doc.Internal.Text 2 "┴─")
withColor :: Doc AnsiStyle -> Fingerprint -> DiagramM (Doc AnsiStyle)
withColor doc hash = fmap (\c -> Doc.annotate (Doc.color c) doc) (colorOf hash)
-- ---------------------------------------------------------------------------------------------------------------------
newtype DiagramM a = DiagramM
{runDiagramM :: IORef DiagramCtx -> IO a}
deriving
(Functor, Applicative, Monad)
via Kleisli IO (IORef DiagramCtx)
runDiagram :: DiagramM a -> IO a
runDiagram (DiagramM io) = newIORef (DiagramCtx (ColorSrc mempty mempty)) >>= io
newtype DiagramCtx = DiagramCtx {ctxColorSource :: ColorSrc}
colorOf :: Fingerprint -> DiagramM Color
colorOf hash =
DiagramM \ref -> do
ctx <- readIORef ref
case lookupColor hash (ctxColorSource ctx) of
Nothing -> do
let (c, cs') = takeColor hash (ctxColorSource ctx)
writeIORef ref ctx {ctxColorSource = cs'}
pure c
Just c -> pure c
-- ---------------------------------------------------------------------------------------------------------------------
data ColorSrc = ColorSrc
{ srcColorCtx :: {-# UNPACK #-} !ColorCtx
, srcColorMap :: !(IntMap Color)
}
lookupColor :: Fingerprint -> ColorSrc -> Maybe Color
lookupColor hash (ColorSrc _ cs) = IntMap.lookup (fromIntegral hash) cs
takeColor :: Fingerprint -> ColorSrc -> (Color, ColorSrc)
takeColor hash (ColorSrc !cctx cmap) =
let c = getColorCtx cctx
cctx' = succ cctx
cmap' = IntMap.insert (fromIntegral hash) c cmap
in (c, ColorSrc cctx' cmap')
-- ---------------------------------------------------------------------------------------------------------------------
newtype ColorCtx = ColorCtx Int
deriving (Eq, Ord)
getColorCtx :: ColorCtx -> Color
getColorCtx (ColorCtx i) =
case mod i 6 of
0 -> Cyan
1 -> Red
2 -> Green
3 -> Yellow
4 -> Blue
_ -> Magenta
{-# INLINE getColorCtx #-}
-- | @since 1.0.0
instance Semigroup ColorCtx where
-- Note: We use addition in Z mod 6 as the semigroup for 'ColorCtx' since the 'Enum' instance is cycles through 6
-- colors.
ColorCtx x <> ColorCtx y = ColorCtx (mod (x + y) 6)
{-# INLINE (<>) #-}
-- | @since 1.0.0
instance Monoid ColorCtx where
mempty = ColorCtx 0
{-# INLINE mempty #-}
-- | @since 1.0.0
instance Enum ColorCtx where
-- Note: AnsiStyle have six unique colors that are not black or white (cyan, red, green, yellow, blue and magenta).
-- 'DiagramM' uses this 'Enum' instance and 'succ' to cycle through these six colors since the 'Color' datatype
-- exported does not provide one out of the box.
toEnum i = ColorCtx (mod i 6)
{-# INLINE toEnum #-}
fromEnum (ColorCtx i) = mod i 6
{-# INLINE fromEnum #-}
succ (ColorCtx i) = ColorCtx (mod (succ i) 6)
{-# INLINE succ #-}