packages feed

trifecta-0.7.2: Text/Trifecta/Render.hs

{-# LANGUAGE TypeSynonymInstances #-}
-- | Diagnostics rendering
module Text.Trifecta.Render 
  ( Rendering(..)
  , Renderable(..)
  , Source(..)
  , surface
  , draw
  , addSym
  , addFix
  , addCaret
  , addSpan
  , addFixit
  -- * Internals
  , caretEffects, fixitEffects, spanEffects, outOfRangeEffects
  , blankLine
  ) where

import Control.Applicative hiding (empty)
import Data.ByteString hiding (groupBy, empty, any)
import qualified Data.ByteString.UTF8 as UTF8 
import Data.List (groupBy)
import Data.Function (on)
import Text.Trifecta.Delta
import Data.Foldable (toList)
import Data.Array
import Text.Trifecta.Bytes
import Text.PrettyPrint.Leijen.Extras hiding (column)
import System.Console.Terminfo.Color
import System.Console.Terminfo.PrettyPrint
import Control.Monad.State
import Prelude as P

caretEffects, fixitEffects, spanEffects :: [ScopedEffect]
caretEffects = [soft (Foreground Green), soft Bold]
fixitEffects = [soft (Foreground Blue)]
spanEffects  = [soft (Foreground Green)]

outOfRangeEffects :: [ScopedEffect] -> [ScopedEffect]
outOfRangeEffects xs = soft Bold : xs

type Line = Array Int ([ScopedEffect], Char)

(///) :: Ix i => Array i e -> [(i, e)] -> Array i e
a /// xs = a // P.filter (inRange (bounds a) . fst) xs

draw :: [ScopedEffect] -> Int -> String -> Line -> Line
draw e n xs a = gt $ lt $ a /// out
    where (lo,hi) = bounds a
          out = P.zipWith (\i c -> (i,(e,c))) [n..] xs
          lt | any (\el -> fst el < lo) out = (// [(lo,(outOfRangeEffects e,'<'))])
             | otherwise = id
          gt | any (\el -> fst el > hi) out = (// [(hi,(outOfRangeEffects e,'>'))])
             | otherwise = id

data Rendering = Rendering 
  { rDelta   :: !Delta                -- focus, the rendering will keep this visible
  , rLineLen :: {-# UNPACK #-} !Int   -- actual line length
  , rLine    :: Line -> Line          -- source contents
  , rSymbols :: Line -> Line          -- annotations about the line
  , rFixits  :: Maybe (Line -> Line)  -- fixits providing alternate text
  }

addSym, addFix :: Rendering -> (Line -> Line) -> Rendering
addSym r f = r { rSymbols = f . rSymbols r }
addFix r f = r { rFixits = fmap (f .) (rFixits r) <|> Just f } 

instance HasDelta Rendering where
  delta = rDelta

class Renderable t where
  rendering :: t -> Rendering 

instance Renderable Rendering where
  rendering = id

class Source t where
  source :: t -> (Int, Line -> Line)

instance Source String where
  source s = (P.length s', draw [] 0 s') where s' = expand s

instance Source ByteString where
  source = source . UTF8.toString

-- | create a drawing surface
surface :: Source s => Delta -> s -> Rendering
surface d s = case source s of 
  (ls, doc) -> Rendering d ls doc id Nothing

expand :: String -> String
expand = go 0 where
  go n ('\t':xs) = let t = 8 - mod n 8 in P.replicate t ' ' ++ go (n + t) xs
  go _ ('\n':_)  = []
  go n (x:xs)    = x : go (n + 1) xs
  go _ []        = []

addCaret :: Delta -> Rendering -> Rendering 
addCaret p r 
  | near p r  = addSym r $ draw caretEffects (column p) "^"
  | otherwise = r

addSpan :: Delta -> Delta -> Rendering -> Rendering 
addSpan s e r
  | nl && nh  = addSym r $ draw spanEffects (column l) $ P.replicate (max (column h   - column l + 1) 0) '~' 
  | nl        = addSym r $ draw spanEffects (column l) $ P.replicate (max (rLineLen r - column l + 2) 0) '~'
  |       nh  = addSym r $ draw spanEffects (-1)       $ P.replicate (max (column h + 1) 0) '~'
  | otherwise = r
  where 
    l = argmin bytes s e 
    h = argmax bytes s e
    nl = near l r
    nh = near h r

addFixit :: Delta -> Delta -> String -> Rendering -> Rendering
addFixit s e rpl r
  | near l r = addFix r' $ draw fixitEffects (column l) rpl
  | otherwise = r'
  where 
    l = argmin bytes s e
    r' = addSpan s e r

instance Pretty Rendering where
  pretty r = prettyTerm r >>= const empty

instance PrettyTerm Rendering where
  prettyTerm r = nesting $ \k -> columns $ \n -> go (n - k) where
    go cols = align (vsep img) <> linebreak where 
      (lo, hi) = window (column r) (rLineLen r) (cols - 2)
      line1 = cluster $ rLine r
      line2 = cluster $ rSymbols r 
      img = case cluster <$> rFixits r of 
        Just line3 -> [line1, line2, line3] 
        Nothing    -> [line1, line2]
      cluster :: (Line -> Line) -> TermDoc
      cluster m = hcat 
                . P.map (\g -> P.foldr with (string (P.map snd g)) (fst (P.head g)))
                . groupBy ((==) `on` fst)
                . toList 
                $ m (blankLine lo hi)

blankLine :: Int -> Int -> Line
blankLine lo hi = listArray (lo,hi) (repeat ([],' '))

window :: Int -> Int -> Int -> (Int, Int)
window c l w 
  | c <= w2     = (0, min w l)
  | c + w2 >= l = if l > w then (l-w, l) else (0, w)
  | otherwise   = (c-w2,c + w2)
  where w2 = div w 2

argmin :: Ord b => (a -> b) -> a -> a -> a
argmin f a b 
  | f a <= f b = a
  | otherwise = b

argmax :: Ord b => (a -> b) -> a -> a -> a
argmax f a b 
  | f a > f b = a
  | otherwise = b