{-# LANGUAGE FlexibleInstances, DefaultSignatures, UndecidableInstances , OverloadedStrings #-} {-| Module : Text.ANTLR.Pretty Description : A pretty-printing type class to be used across antlr-haskell modules Copyright : (c) Karl Cronburg, 2018 License : BSD3 Maintainer : karl@cs.tufts.edu Stability : experimental Portability : POSIX I want to have something like Show whereby every time I add a new type to the system, I can implement a function that gets called by existing code which happens to have types that get parametrized by that type. I don't want to modify an existing file / centralizing all of the types in my system into a single file makes little sense because then that one file becomes a hub / single point of failure. * I need a typeclass (no modifying existing files, but they need to call my new code without passing around a new show function) * The prettify function of that typeclass needs to return a state monad so that recursive calls keep the state * A pshow function needs to evalState on the prettify function with an initial indentation of zero (along with any other future state values...) -} module Text.ANTLR.Pretty where import Control.Monad.Trans.State.Lazy import qualified Data.Map.Strict as M import Data.Data (toConstr, Data(..)) import qualified Data.Text as T -- | Pretty-printing state data PState = PState { indent :: Int -- ^ current indentation level , vis_chrs :: Int -- ^ number of visible characters consumed so far , str :: T.Text -- ^ the string, 'T.Text', that we've constructed so far , columns_soft :: Int -- ^ soft limit on number of columns to consume per row , columns_hard :: Int -- ^ hard limit on number of columns to consume per row , curr_col :: Int -- ^ column number we're on in the current row of 'str' , curr_row :: Int -- ^ number of rows (newlines) we've printed to 'str' } -- | The pretty state monad type PrettyM val = State PState val -- | No value being threaded through the monad (because result is in 'str') type Pretty = PrettyM () -- | Define the 'Prettify' type class for your pretty-printable type @t@. class Prettify t where {-# MINIMAL prettify #-} -- | Defines how to pretty-print some type. prettify :: t -> Pretty default prettify :: (Show t) => t -> Pretty prettify = rshow -- | Lists are pretty-printed specially. prettifyList :: [t] -> Pretty prettifyList = prettifyList_ -- | Initial Pretty state with safe soft and hard column defaults. initPState = PState { indent = 0 -- Indentation level , vis_chrs = 0 -- Number of visible characters consumed. , str = T.empty -- The string , columns_soft = 100 -- Soft limit on terminal width. , columns_hard = 120 -- Hard limit on terminal width. , curr_col = 0 -- Column position in the current row. , curr_row = 0 -- Number of newlines seen } -- | Prettify a string by putting it on the end of the current string state pLine :: T.Text -> Pretty pLine s = do pStr s _pNewLine -- | Pretty print a literal string by just printing the string. pStr' :: String -> Pretty pStr' = pStr . T.pack -- | This currently assumes all input strings contain no newlines, and that this is -- only called on relatively small strings because strings running over the end -- of the hard column limit get dumped onto the next line __no matter what__. -- T.Texts can run over the soft limit, but hitting the soft limit after a call -- to 'pStr' forces a newline. pStr :: T.Text -> Pretty pStr s = do pstate <- get _doIf _pNewLine (T.length s + curr_col pstate > columns_hard pstate && curr_col pstate /= 0) pstate <- get _doIf _pIndent (curr_col pstate == 0 && indent pstate > 0) pstate <- get put $ pstate { str = T.append (str pstate) s , curr_col = (curr_col pstate) + T.length s } pstate <- get _doIf _pNewLine (curr_col pstate > columns_soft pstate) -- | Print a single character to the output. pChr :: Char -> Pretty pChr c = pStr $ T.singleton c -- | Gets rid of if-then-else lines in the Pretty monad code: _doIf fncn True = fncn _doIf fncn False = return () -- | Indent by the number of spaces specified in the state. _pIndent :: Pretty _pIndent = do pstate <- get put $ pstate { str = str pstate `T.append` T.replicate (indent pstate) (T.singleton ' ') , curr_col = curr_col pstate + indent pstate , vis_chrs = vis_chrs pstate + indent pstate } -- | Insert a newline _pNewLine :: Pretty _pNewLine = do pstate <- get put $ pstate { str = T.snoc (str pstate) '\n' , curr_col = 0 , curr_row = curr_row pstate + 1 } -- | Run the pretty-printer, returning a 'T.Text'. pshow :: (Prettify t) => t -> T.Text pshow t = str $ execState (prettify t) initPState -- | Run the pretty-printer, returning a 'String'. pshow' :: (Prettify t) => t -> String pshow' = T.unpack . pshow -- | Run the pretty-printer with a specific indentation level. pshowIndent :: (Prettify t) => Int -> t -> T.Text pshowIndent i t = str $ execState (prettify t) $ initPState { indent = i } -- | Plain-vanilla show of something in the 'Pretty' state monad. rshow :: (Show t) => t -> Pretty rshow t = do pstate <- get let s = show t put $ pstate { str = str pstate `T.append` T.pack s , curr_row = curr_row pstate + (T.length . T.filter (== '\n')) (T.pack s) , curr_col = curr_col pstate -- TODO } -- | Parenthesize something in 'Pretty'. pParens fncn = do pChr '(' fncn pChr ')' -- | Increment the indentation level by modifying the pretty-printer state. incrIndent :: Int -> Pretty incrIndent n = do pstate <- get put $ pstate { indent = indent pstate + n } -- | Like 'incrIndent' but set indentation level instead of incrementing. setIndent :: Int -> Pretty setIndent n = do pstate <- get put $ pstate { indent = n } -- | Prettify the given value and compute the number of characters consumed as a -- result. pCount :: (Prettify v) => v -> PrettyM Int pCount v = do i0 <- indent <$> get prettify v i1 <- indent <$> get return (i1 - i0) -- | Pretty-print a list with one entry per line. pListLines :: (Prettify v) => [v] -> Pretty pListLines vs = do pStr $ T.pack "[ " col0 <- curr_col <$> get i0 <- indent <$> get setIndent (col0 - 2) sepBy (pLine T.empty >> (pStr $ T.pack ", ")) (map prettify vs) pLine T.empty >> pChr ']' setIndent i0 -- Reset indentation back to what it was instance (Prettify k, Prettify v) => Prettify (M.Map k v) where prettify m = do -- (5 == length of "Map: ") ==> TODO: indentation "discipline" pStr "Map: "; incrIndent 5 prettify $ M.toList m -- TODO: prettier map incrIndent (-5) instance (Prettify v) => Prettify (Maybe v) where prettify Nothing = pStr "Nope" prettify (Just v) = pStr "Yep" >> pParens (prettify v) -- | Prettify a list with possibly more than one entry per line. prettifyList_ [] = pStr "[]" prettifyList_ vs = do pChr '[' sepBy (pStr ", ") (map prettify vs) pChr ']' instance (Prettify v) => Prettify [v] where prettify = prettifyList -- TODO: template haskell-ify for larger tuples instance (Prettify a, Prettify b) => Prettify (a,b) where prettify (a,b) = do pChr '(' prettify a pChr ',' prettify b pChr ')' instance (Prettify a, Prettify b, Prettify c) => Prettify (a,b,c) where prettify (a,b,c) = do pChr '(' prettify a pChr ',' prettify b pChr ',' prettify c pChr ')' instance (Prettify a, Prettify b, Prettify c, Prettify d) => Prettify (a,b,c,d) where prettify (a,b,c,d) = do pChr '(' prettify a pChr ',' prettify b pChr ',' prettify c pChr ',' prettify d pChr ')' -- | Pretty-print a list of values, separated by some other pretty-printer. sepBy s [] = return () sepBy s (v:vs) = foldl (_sepBy s) v vs -- | Reorder pretty-printer bind. _sepBy s ma mb = ma >> s >> mb instance Prettify Char where prettify = pChr prettifyList = pStr . T.pack instance Prettify () where prettify = rshow instance Prettify Bool where prettify = rshow instance Prettify Int where prettify = rshow instance Prettify Double where prettify = rshow