{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-| Module : Errata.Internal.Render Copyright : (c) 2020 comp License : MIT Maintainer : onecomputer00@gmail.com Stability : stable Portability : portable Functions for rendering the errors. You should not need to import this, as these functions are lower-level. This module is internal, and may break across non-breaking versions. -} module Errata.Internal.Render ( renderErrors , renderErrata , renderBlock , renderSourceLines ) where import Data.List import qualified Data.List.NonEmpty as N import qualified Data.Sequence as S import qualified Data.Map.Strict as M import Data.Maybe import Data.String (IsString) import qualified Data.Text as T import qualified Data.Text.Lazy.Builder as TB import Errata.Source import Errata.Types -- | Renders errors. renderErrors :: Source source => source -> [Errata] -> TB.Builder renderErrors source errs = unsplit "\n\n" prettified where sortedErrata = sortOn (\(Errata {..}) -> blockLocation errataBlock) $ errs -- We may arbitrarily index the source lines a lot, so a Seq is appropriate. -- If push comes to shove, this could be replaced with an 'Array' or a 'Vec'. slines = S.fromList (sourceToLines source) prettified = map (renderErrata slines) sortedErrata -- | A single pretty error from metadata and source lines. renderErrata :: Source source => S.Seq source -> Errata -> TB.Builder renderErrata slines (Errata {..}) = errorMessage where errorMessage = mconcat [ TB.fromText $ maybe "" (<> "\n") errataHeader , unsplit "\n\n" (map (renderBlock slines) (errataBlock : errataBlocks)) , TB.fromText $ maybe "" ("\n\n" <>) errataBody ] -- | A single pretty block from block data and source lines. renderBlock :: Source source => S.Seq source -> Block -> TB.Builder renderBlock slines block@(Block {..}) = blockMessage where blockMessage = mconcat [ TB.fromText $ styleLocation blockStyle blockLocation , maybe "" ("\n" <>) (renderSourceLines slines block <$> N.nonEmpty blockPointers) , TB.fromText $ maybe "" ("\n" <>) blockBody ] -- | The source lines for a block. renderSourceLines :: Source source => S.Seq source -> Block -> N.NonEmpty Pointer -> TB.Builder renderSourceLines slines (Block {..}) lspans = unsplit "\n" sourceLines where Style {..} = blockStyle -- Min and max line numbers, as well padding size before the line prefix. minLine = fst (M.findMin pointersGrouped) maxLine = fst (M.findMax pointersGrouped) padding = length (show maxLine) -- Shows a line in accordance to the style. -- We might get a line that's out-of-bounds, usually the EOF line, so we can default to empty. showLine :: [(Int, Int)] -> Int -> TB.Builder showLine hs n = TB.fromText . maybe "" id . fmap (styleLine hs . sourceToText) $ S.lookup (n - 1) slines -- Generic prefix without line number. prefix = mconcat [ replicateB padding " ", " ", TB.fromText styleLinePrefix, " " ] -- Prefix for omitting lines when spanning many lines. omitPrefix = mconcat [ TB.fromText styleEllipsis, replicateB (padding - 1) " ", " ", TB.fromText styleLinePrefix, " " ] -- Prefix with a line number. linePrefix :: Int -> TB.Builder linePrefix n = mconcat [ TB.fromText (styleNumber n), replicateB (padding - length (show n)) " ", " " , TB.fromText styleLinePrefix, " " ] -- The pointers grouped by line. pointersGrouped = M.fromListWith (<>) $ map (\x -> (pointerLine x, pure x)) (N.toList lspans) -- The resulting source lines. -- Extra prefix for padding. sourceLines = mconcat [replicateB padding " ", " ", TB.fromText styleLinePrefix] : makeSourceLines 0 [minLine .. maxLine] -- Whether there will be a multiline span. hasConnMulti = M.size (M.filter (any pointerConnect) pointersGrouped) > 1 -- Whether line /n/ has a connection to somewhere else (including the same line). hasConn :: Int -> Bool hasConn n = maybe False (any pointerConnect) $ M.lookup n pointersGrouped -- Whether line /n/ has a connection to a line before or after it (but not including). connAround :: Int -> (Bool, Bool) connAround n = let (a, b) = M.split n pointersGrouped in ((any . any) pointerConnect a, (any . any) pointerConnect b) -- Makes the source lines. -- We have an @extra@ parameter to keep track of extra lines when spanning multiple lines. makeSourceLines :: Int -> [Int] -> [TB.Builder] -- No lines left. makeSourceLines _ [] = [] -- The next line is a line we have to decorate with pointers. makeSourceLines _ (n:ns) | Just p <- M.lookup n pointersGrouped = makeDecoratedLines p <> makeSourceLines 0 ns -- The next line is an extra line, within a limit (currently 2, may be configurable later). makeSourceLines extra (n:ns) | extra < 2 = let mid = if | snd (connAround n) -> TB.fromText styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in (linePrefix n <> mid <> showLine [] n) : makeSourceLines (extra + 1) ns -- We reached the extra line limit, so now there's some logic to figure out what's next. makeSourceLines _ ns = let (es, ns') = break (`M.member` pointersGrouped) ns in case (es, ns') of -- There were no lines left to decorate anyways. (_, []) -> [] -- There are lines left to decorate, and it came right after. ([], _) -> makeSourceLines 0 ns' -- There is a single extra line, so we can use that as the before-line. -- No need for omission, because it came right before. ([n], _) -> let mid = if | snd (connAround n) -> TB.fromText styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in (linePrefix n <> mid <> showLine [] n) : makeSourceLines 0 ns' -- There are more than one line in between, so we omit all but the last. -- We use the last one as the before-line. (_, _) -> let n = last es mid = if | snd (connAround n) -> TB.fromText styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in (omitPrefix <> mid) : (linePrefix n <> mid <> showLine [] n) : makeSourceLines 0 ns' -- Decorate a line that has pointers. -- The pointers we get are assumed to be all on the same line. makeDecoratedLines :: N.NonEmpty Pointer -> [TB.Builder] makeDecoratedLines pointers = (linePrefix line <> TB.fromText lineConnector <> sline) : decorationLines where lineConnector = if | hasConnBefore && hasConnUnder -> styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" -- Shortcuts to where this line connects to. hasConnHere = hasConn line (hasConnBefore, hasConnAfter) = connAround line hasConnAround = hasConnBefore || hasConnAfter hasConnOver = hasConnHere || hasConnBefore hasConnUnder = hasConnHere || hasConnAfter -- The sorted pointers by column. -- There's a reverse for when we create decorations. pointersSorted = N.fromList . sortOn pointerColumns $ N.toList pointers pointersSorted' = N.reverse pointersSorted -- The line we're on. line = pointerLine $ N.head pointers sline = showLine (map pointerColumns (N.toList pointersSorted)) line -- The resulting decoration lines. decorationLines = if -- There's only one pointer, so no need for more than just an underline and label. | N.length pointersSorted' == 1 -> [underline pointersSorted'] -- There's no labels at all, so we just need the underline. | all (isNothing . pointerLabel) (N.tail pointersSorted') -> [underline pointersSorted'] -- Otherwise, we have three steps to do: -- The underline directly underneath. -- An extra connector for the labels other than the rightmost one. -- The remaining connectors and the labels. | otherwise -> let hasLabels = filter (isJust . pointerLabel) $ N.tail pointersSorted' in underline pointersSorted' : connectors hasLabels : parar (\a (rest, xs) -> connectorAndLabel rest a : xs) [] hasLabels -- Create an underline directly under the source. underline :: N.NonEmpty Pointer -> TB.Builder underline ps = let (decor, _) = foldDecorations (\n isFirst rest -> if | isFirst && any pointerConnect rest && hasConnAround -> replicateB n styleHorizontal | isFirst -> replicateB n " " | any pointerConnect rest -> replicateB n styleHorizontal | otherwise -> replicateB n " " ) "" (\n -> replicateB n styleUnderline) (N.toList ps) lbl = maybe "" (" " <>) . pointerLabel $ N.head ps mid = if | hasConnHere && hasConnBefore && hasConnAfter -> styleUpDownRight <> styleHorizontal | hasConnHere && hasConnBefore -> styleUpRight <> styleHorizontal | hasConnHere && hasConnAfter -> styleDownRight <> styleHorizontal | hasConnBefore && hasConnAfter -> styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in prefix <> TB.fromText mid <> decor <> TB.fromText lbl -- Create connectors underneath. -- It's assumed all these pointers have labels. connectors :: [Pointer] -> TB.Builder connectors ps = let (decor, _) = foldDecorations (\n _ _ -> replicateB n " ") (TB.fromText styleVertical) (\n -> replicateB (n - 1) " ") ps mid = if | hasConnOver && hasConnAfter -> styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in prefix <> TB.fromText mid <> decor -- Create connectors and labels underneath. -- It's assumed all these pointers have labels. -- The single pointer passed in is the label to make at the end of the decorations. connectorAndLabel :: [Pointer] -> Pointer -> TB.Builder connectorAndLabel ps p = let (decor, finalCol) = foldDecorations (\n _ _ -> replicateB n " ") (TB.fromText styleVertical) (\n -> replicateB (n - 1) " ") ps lbl = maybe "" (\x -> mconcat [ replicateB (pointerColStart p - finalCol) " " , TB.fromText styleUpRight , " " , TB.fromText x ] ) (pointerLabel p) mid = if | hasConnOver && hasConnAfter -> styleVertical <> " " | hasConnMulti -> " " | otherwise -> "" in prefix <> TB.fromText mid <> decor <> lbl -- | Makes a line of decorations below the source. foldDecorations :: (Int -> Bool -> [Pointer] -> TB.Builder) -- ^ Catch up from the previous pointer to this pointer. -> TB.Builder -- ^ Something in the middle. -> (Int -> TB.Builder) -- ^ Reach the next pointer. -> [Pointer] -> (TB.Builder, Int) foldDecorations catchUp something reachAfter ps = let (decor, finalCol, _, _) = foldr (\(Pointer {..}) (xs, c, rest, isFirst) -> ( mconcat [ xs , catchUp (pointerColStart - c) isFirst rest , something , reachAfter (pointerColEnd - pointerColStart) ] , pointerColEnd , tail rest , False ) ) ("", 1, reverse ps, True) ps in (decor, finalCol) -- | Paramorphism on lists (lazily, from the right). parar :: (a -> ([a], b) -> b) -> b -> [a] -> b parar _ b [] = b parar f b (a:as) = f a (as, parar f b as) -- | Puts text between each item. unsplit :: (Semigroup a, IsString a) => a -> [a] -> a unsplit _ [] = "" unsplit a (x:xs) = foldl' (\acc y -> acc <> a <> y) x xs -- | Replicates text into a builder. replicateB :: Int -> T.Text -> TB.Builder replicateB n = TB.fromText . T.replicate n