{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} ----------------------------------------------------------------------------- -- | -- Module : Text.Trifecta.Diagnostic.Rendering.Prim -- Copyright : (C) 2011 Edward Kmett -- License : BSD-style (see the file LICENSE) -- -- Maintainer : Edward Kmett -- Stability : experimental -- Portability : non-portable -- -- The type for Lines will very likely change over time, to enable drawing -- lit up multi-character versions of control characters for @^Z@, @^[@, -- @<0xff>@, etc. This will make for much nicer diagnostics when -- working with protocols. -- ----------------------------------------------------------------------------- module Text.Trifecta.Diagnostic.Rendering.Prim ( Rendering(..) , nullRendering , emptyRendering , Source(..) , rendering , Renderable(..) , Rendered(..) -- * Lower level drawing primitives , Lines , draw , ifNear , (.#) ) where import Control.Applicative import Control.Comonad import Control.Monad.State import Data.Array import Data.ByteString as B hiding (groupBy, empty, any) import Data.Foldable import Data.Function (on) import Data.Int (Int64) import Data.Functor.Bind import Data.List (groupBy) import Data.Semigroup import Data.Semigroup.Foldable import Data.Semigroup.Traversable import Data.Traversable import Text.Trifecta.IntervalMap import Prelude as P import Prelude hiding (span) import System.Console.Terminfo.Color import System.Console.Terminfo.PrettyPrint import Text.PrettyPrint.Free hiding (column) import Text.Trifecta.Rope.Bytes import Text.Trifecta.Rope.Delta import Text.Trifecta.Highlight.Class import Text.Trifecta.Highlight.Effects import qualified Data.ByteString.UTF8 as UTF8 outOfRangeEffects :: [ScopedEffect] -> [ScopedEffect] outOfRangeEffects xs = soft Bold : xs type Lines = Array (Int,Int64) ([ScopedEffect], Char) (///) :: Ix i => Array i e -> [(i, e)] -> Array i e a /// xs = a // P.filter (inRange (bounds a) . fst) xs grow :: Int -> Lines -> Lines grow y a | inRange (t,b) y = a | otherwise = array new [ (i, if inRange old i then a ! i else ([],' ')) | i <- range new ] where old@((t,lo),(b,hi)) = bounds a new = ((min t y,lo),(max b y,hi)) draw :: [ScopedEffect] -> Int -> Int64 -> String -> Lines -> Lines draw e y n xs a0 | Prelude.null xs = a0 | otherwise = gt $ lt (a /// out) where a = grow y a0 ((_,lo),(_,hi)) = bounds a out = P.zipWith (\i c -> ((y,i),(e,c))) [n..] xs lt | Prelude.any (\el -> snd (fst el) < lo) out = (// [((y,lo),(outOfRangeEffects e,'<'))]) | otherwise = id gt | Prelude.any (\el -> snd (fst el) > hi) out = (// [((y,hi),(outOfRangeEffects e,'>'))]) | otherwise = id -- | fill the interval from [n .. m) with a given effect recolor :: ([ScopedEffect] -> [ScopedEffect]) -> Maybe Int64 -> Maybe Int64 -> Lines -> Lines recolor f n0 m0 a0 | m <= n = a0 | otherwise = a /// P.map rc [n .. m - 1] where ((_,lo),(_,hi)) = bounds a n = maybe lo id n0 m = maybe (hi + 1) id m0 a = grow 0 a0 rc i = (yi, (f e, c)) -- only if not isSpace? where yi = (0, i) (e,c) = a ! yi data Rendering = Rendering { renderingDelta :: !Delta -- focus, the render will keep this visible , renderingLineLen :: {-# UNPACK #-} !Int64 -- actual line length , renderingLineBytes :: {-# UNPACK #-} !Int64 -- line length in bytes , renderingLine :: Lines -> Lines , renderingOverlays :: Delta -> Lines -> Lines } instance Highlightable Rendering where addHighlights intervals (Rendering d ll lb l o) = Rendering d ll lb l' o where d' = rewind d l' = Prelude.foldr (.) l [ recolor (eff tok) (column lo <$ guard (near d lo)) (column hi <$ guard (near d hi)) | (Interval lo hi, tok) <- intersections d' (d' <> Columns ll lb) intervals ] eff t _ = highlightEffects t instance Show Rendering where showsPrec d (Rendering p ll lb _ _) = showParen (d > 10) $ showString "Rendering " . showsPrec 11 p . showChar ' ' . showsPrec 11 ll . showChar ' ' . showsPrec 11 lb . showString " ... ..." nullRendering :: Rendering -> Bool nullRendering (Rendering (Columns 0 0) 0 0 _ _) = True nullRendering _ = False emptyRendering :: Rendering emptyRendering = rendering (Columns 0 0) "" instance Semigroup Rendering where -- an unprincipled hack Rendering (Columns 0 0) 0 0 _ f <> Rendering del len lb doc g = Rendering del len lb doc $ \d l -> f d (g d l) Rendering del len lb doc f <> Rendering _ _ _ _ g = Rendering del len lb doc $ \d l -> f d (g d l) instance Monoid Rendering where mappend = (<>) mempty = emptyRendering ifNear :: Delta -> (Lines -> Lines) -> Delta -> Lines -> Lines ifNear d f d' l | near d d' = f l | otherwise = l instance HasDelta Rendering where delta = renderingDelta class Renderable t where render :: t -> Rendering instance Renderable Rendering where render = id class Source t where source :: t -> (Int64, Int64, Lines -> Lines) {- the number of (padded) columns, number of bytes, and the the line -} instance Source String where source s | Prelude.elem '\n' s = ( ls, bs, draw [] 0 0 s') | otherwise = ( ls + fromIntegral (Prelude.length end), bs, draw [soft (Foreground Blue), soft Bold] 0 ls end . draw [] 0 0 s') where end = "" s' = go 0 s bs = fromIntegral $ B.length $ UTF8.fromString $ Prelude.takeWhile (/='\n') s ls = fromIntegral $ Prelude.length s' 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 _ [] = [] instance Source ByteString where source = source . UTF8.toString -- | create a drawing surface rendering :: Source s => Delta -> s -> Rendering rendering del s = case source s of (len, lb, doc) -> Rendering del len lb doc (\_ l -> l) (.#) :: (Delta -> Lines -> Lines) -> Rendering -> Rendering f .# Rendering d ll lb s g = Rendering d ll lb s $ \e l -> f e $ g e l instance Pretty Rendering where pretty r = prettyTerm r >>= const empty instance PrettyTerm Rendering where prettyTerm (Rendering d ll _ l f) = nesting $ \k -> columns $ \n -> go (fromIntegral (n - k)) where go cols = align (vsep (P.map ln [t..b])) where (lo, hi) = window (column d) ll (min (max (cols - 2) 30) 200) a = f d $ l $ array ((0,lo),(-1,hi)) [] ((t,_),(b,_)) = bounds a ln y = hcat $ P.map (\g -> P.foldr with (pretty (P.map snd g)) (fst (P.head g))) $ groupBy ((==) `on` fst) [ a ! (y,i) | i <- [lo..hi] ] window :: Int64 -> Int64 -> Int64 -> (Int64, Int64) 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 data Rendered a = a :@ Rendering deriving Show instance Functor Rendered where fmap f (a :@ s) = f a :@ s instance HasDelta (Rendered a) where delta = delta . render instance HasBytes (Rendered a) where bytes = bytes . delta instance Extend Rendered where extend f as@(_ :@ s) = f as :@ s instance Comonad Rendered where extract (a :@ _) = a instance Apply Rendered where (f :@ s) <.> (a :@ t) = f a :@ (s <> t) instance Bind Rendered where (a :@ s) >>- f = case f a of b :@ t -> b :@ (s <> t) instance Foldable Rendered where foldMap f (a :@ _) = f a instance Traversable Rendered where traverse f (a :@ s) = (:@ s) <$> f a instance Foldable1 Rendered where foldMap1 f (a :@ _) = f a instance Traversable1 Rendered where traverse1 f (a :@ s) = (:@ s) <$> f a instance Renderable (Rendered a) where render (_ :@ s) = s