{-# LANGUAGE DefaultSignatures, FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving, KindSignatures, LambdaCase, MultiWayIf #-}
{-# LANGUAGE TemplateHaskell, UnicodeSyntax #-}
module Printcess.Core where
import Control.Monad.State.Strict
import Control.Lens
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty (NonEmpty(..))
import Printcess.Config
data Assoc = AssocN | AssocL | AssocR
deriving (Eq, Ord, Read, Show)
data PrettySt = PrettySt
{ _indentation :: Int
, _precedence :: Int
, _assoc :: Assoc
, _maxLineWidth :: Maybe Int
, _text :: NE.NonEmpty String
, _indentChar :: Char
, _indentDepth :: Int
, _indentAfterBreaks :: Int
}
stFromConfig :: Config → PrettySt
stFromConfig c = PrettySt
{ _indentation = _configInitIndent c
, _precedence = _configInitPrecedence c
, _assoc = AssocN
, _maxLineWidth = _configMaxLineWidth c
, _text = "" :| []
, _indentChar = _configIndentChar c
, _indentDepth = _configIndentDepth c
, _indentAfterBreaks = _configIndentAfterBreaks c
}
makeLenses ''PrettySt
-- Pretty Printing -------------------------------------------------------------
-- | Render a 'Pretty' printable @a@ to 'String' using a 'Config', that
-- specifies how the @a@ should be rendered. For example
--
-- > pretty defConfig (1 :: Int) -- evaluates to "1"
pretty
:: Pretty a
=> State Config () -- ^ Updates for the default pretty printing 'Config'.
-> a -- ^ A 'Pretty' printable @a@.
-> String -- ^ The pretty printed @a@.
pretty c
= concat
. (`sepByList` "\n")
. reverse
. NE.toList
. view text
. (`execState` stFromConfig (execState c def))
. runPrettyM
. pp
-- | Render a 'Pretty' printable @a@ to @stdout@ using a 'Config', that
-- specifies how the @a@ should be rendered.
--
-- Convenience function, defined as:
--
-- > prettyPrint c = liftIO . putStrLn . pretty c
prettyPrint
:: (MonadIO m, Pretty a)
=> State Config () -- ^ Updates for the default pretty printing 'Config'.
-> a -- ^ A 'Pretty' printable @a@.
-> m () -- ^ An 'IO' action pretty printing the @a@ to @stdout@.
prettyPrint c =
liftIO . putStrLn . pretty c
-- Type Classes ----------------------------------------------------------------
-- | Instanciating this class for a type, declares how values of that type
-- should be pretty printed.
--
-- As pretty printing may depend on some context, e.g. the current indentation
-- level, a 'State' monad for pretty printing ('PrettyM') is used.
--
-- A default implementation is provided copying behavior from a 'Show' instance.
-- This can be convenient for deriving 'Pretty', e.g. for base types or
-- debugging. The default implementation is defined by @pp = pp . show@.
class Pretty a where
-- | Pretty print an @a@ as a 'PrettyM' action.
pp :: a → PrettyM ()
default
pp :: Show a => a → PrettyM ();
pp = pp . show
head1L :: Lens' (NE.NonEmpty a) a
head1L = lens NE.head (\(_ :| xs) x → x :| xs)
tail1L :: Lens' (NE.NonEmpty a) [a]
tail1L = lens NE.tail (\(x :| _) xs → x :| xs)
charsBeforeWord :: Int -> (Char -> Bool) -> String -> Int
charsBeforeWord nWords0 cIndent s0 =
go s0 nWords0
where
go s n =
length sIndent + if n == 0 then 0 else length sWord + go sAfterWord (n-1)
where
(sIndent, sBeforeWord) = break (not . cIndent) s
(sWord, sAfterWord) = break cIndent sBeforeWord
charsBeforeWordM :: Int -> PrettyM Int
charsBeforeWordM n0 = do
cIndent ← use indentChar
curText ← use $ text . head1L
pure $ charsBeforeWord n0 (`elem` [' ', '\t', cIndent]) curText
-- Isomorphic lines and unlines implementations
lines' :: String → [String]
lines' = go "" where
go s = \case
"" → [s]
c:cs | c == '\n' → s : go "" cs
| otherwise → go (s++[c]) cs
unlines' :: [String] → String
unlines' = \case
[] → ""
[x] → x
x:xs → x ++ '\n' : unlines' xs
isWS, isNoWS :: Char → Bool
isWS = (`elem` [' ', '\t'])
isNoWS = not . isWS
dropWhileEnd :: (a → Bool) → [a] → [a]
dropWhileEnd f = reverse . dropWhile f . reverse
-- | In contrast to 'Show', @"foo"@ is printed as @"foo"@ and not @"\\"foo\\""@.
-- Most of the other instances are defined in terms of this instance.
-- If the 'String' contains newline characters (@'\n'@), indentation is inserted
-- automatically afterwards.
-- If the current line gets too long, it is automatically broken.
instance Pretty String where
pp = go . lines' where
go :: [String] -> PrettyM ()
go [] = pure ()
go [s] = ppLine True s
go (s:ss) = do ppLine True s; text %= ("" NE.<|); go ss
ppLine :: Bool -> String -> PrettyM ()
ppLine first s = do
oldLine ← use $ text . head1L
when (null oldLine) addIndent
text . head1L %= (++s)
curLine ← use $ text . head1L
-- We have to allow at least indentation + 1 word, otherwise indenting after
-- line break due to line continuation can cause infinite loop
mMaxLineLength ← use maxLineWidth
forM_ mMaxLineLength $ \maxLineLength → do
maxLineLength' ← max <$> charsBeforeWordM 1 <*> pure maxLineLength
when (length curLine > maxLineLength') $ do
let (curLine', lineRest) = splitAt maxLineLength curLine -- length s1 == maxLineLength
let (wordRest, curLine'')
| isNoWS (head lineRest)
= both %~ reverse $ break (==' ') $ reverse curLine'
| otherwise
= ("", curLine')
text . head1L .= dropWhileEnd isWS curLine''
-- Increase indentation once after the first forced line break, this results into:
-- this line was too long
-- still the first line
-- it won't stop
i ← use indentAfterBreaks
let f | first = indentedByChars i
| otherwise = id
f $ do text %= ("" NE.<|); ppLine False $ dropWhile isWS $ wordRest ++ lineRest
-- | In contrast to 'Show', @\'c\'@ is printed as @"c"@ and not @"\'c\'"@.
instance Pretty Char where pp = pp . (:"")
-- | Behaves like 'Show': @1@ is printed to @"1"@.
instance Pretty Int
-- | Behaves like 'Show': @1.2@ is printed to @"1.2"@.
instance Pretty Float
-- | Behaves like 'Show': @1.2@ is printed to @"1.2"@.
instance Pretty Double
-- -- | Print a map @M.fromList [("k1","v1"), ("k2","v2")]@
-- -- as @"[ k1 → v1, k2 → v2 ]"@.
-- instance (Pretty k, Pretty v) => Pretty (M.Map k v) where
-- pp = foldl pp' (pp "") . M.toList where
-- pp' s (k, v) = s +> k ~> "=>" ~> indented v +> nl
-- | The 'Pretty1' type class lifts 'Pretty' printing to unary type constructors.
-- It can be used in special cases to abstract over type constructors which
-- are 'Pretty' printable for any 'Pretty' printable type argument.
class Pretty1 f where
pp1 :: Pretty a => f a → PrettyM ()
default pp1 :: Pretty (f a) => f a -> PrettyM ()
pp1 = pp
-- | The 'Pretty2' type class lifts 'Pretty' printing to binary type constructors.
-- It can be used in special cases to abstract over type constructors which
-- are 'Pretty' printable for any 'Pretty' printable type arguments.
class Pretty2 (f :: * → * → *) where
pp2 :: (Pretty a, Pretty b) => f a b → PrettyM ()
default pp2 :: Pretty (f a b) => f a b -> PrettyM ()
pp2 = pp
-- Pretty Monad ----------------------------------------------------------------
-- | The 'PrettyM' monad is run in the pretty printing process, e.g. in
-- 'pretty' or 'prettyPrint'.
--
-- 'PrettyM' is internally a 'State' monad manipulating a 'Config' and a list of
-- pretty printed lines.
--
-- Most of the combinators from this library take values of 'Pretty' printable types,
-- convert them to @'PrettyM' ()@ actions using 'pp', and combine the actions in
-- some way resulting in a new @'PrettyM' ()@ action.
newtype PrettyM a = PrettyM { runPrettyM :: State PrettySt a }
deriving (Functor, Applicative, Monad, MonadState PrettySt)
-- | This instance makes it possible to nest operators like @('+>')@.
-- Implemented as: @pp = id@
instance Pretty (PrettyM ()) where pp = id
sepByList :: [[a]] → [a] → [[a]]
sepByList [] _ = []
sepByList [s] _ = [s]
sepByList (s:ss) s' = s : s' : sepByList ss s'
addIndent :: PrettyM ()
addIndent = do
i <- use indentation
c <- use indentChar
text . head1L %= (++ replicate i c)
-- Indentation -----------------------------------------------------------------
indentByChars
:: Int
-> PrettyM ()
indentByChars =
(indentation +=)
indentByLevels
:: Int
-> PrettyM ()
indentByLevels i =
(indentation +=) . (i *) =<< use indentDepth
-- | Print an @a@ with indentation increased by a certain amount of
-- 'cIndentChar' characters.
--
-- Example:
--
-- > pretty defConfig $
-- > "while (true) {" \>
-- > indentedByChars 2 ("f();" \> "g();") \>
-- > "}"
-- > -- ↪ "while (true) {
-- > -- f();
-- > -- g();
-- > -- }"
indentedByChars
:: Pretty a
=> Int -- ^ Number of characters to increase indentation.
-> a -- ^ A 'Pretty' printable @a@
-> PrettyM () -- ^ An action printing the @a@ with increased indentation.
indentedByChars i a = do
indentByChars i
pp a
indentByChars (-i)
-- | Same as 'indentedByChars' but increases indentation in 'cIndentDepth' steps.
indentedBy
:: Pretty a
=> Int -- ^ Number of indentation levels to increase.
-- One indentation level consists of 'cIndentDepth' characters.
-> a -- ^ A 'Pretty' printable @a@
-> PrettyM () -- ^ An action printing the @a@ with increased indentation.
indentedBy i a = do
indentByLevels i
pp a
indentByLevels (-i)
-- | Convenience function defined as:
--
-- > indented = indentedBy 1
indented
:: Pretty a
=> a -- ^ A 'Pretty' printable @a@
-> PrettyM () -- ^ An action printing the @a@ indented 1 level deeper.
indented =
indentedBy 1
indentToCurPos :: PrettyM ()
indentToCurPos = do
curLine ← use $ text . head1L
indentation .= length curLine
indentedToCurPos :: PrettyM a → PrettyM a
indentedToCurPos ma = do
i ← use indentation
indentToCurPos
a ← ma
indentation .= i
pure a
-- Associativity & Fixity ------------------------------------------------------
withPrecedence :: (Assoc, Int) → PrettyM () → PrettyM ()
withPrecedence (a, p) ma = do
p' ← use precedence
a' ← use assoc
precedence .= p
assoc .= a
if | p' == p && a' == a && a /= AssocN → ma
| p' < p → ma
| otherwise → do pp "("; ma; pp ")"
precedence .= p'
assoc .= a'
-- | Print an @a@ as a left-associative operator of a certain fixity.
assocL :: Pretty a => Int → a → PrettyM ()
assocL i = withPrecedence (AssocL, i) . pp
-- | Print an @a@ as a right-associative operator of a certain fixity.
assocR :: Pretty a => Int → a → PrettyM ()
assocR i = withPrecedence (AssocR, i) . pp
-- | Print an @a@ as a non-associative operator of a certain fixity.
assocN :: Pretty a => Int → a → PrettyM ()
assocN i = withPrecedence (AssocN, i) . pp
-- | The constructors of this type can be used as short forms of 'left',
-- 'right', and 'inner'.
data AssocAnn a
= L a -- ^ Print an @a@ as the left argument of a mixfix operator (behaves like 'left').
| R a -- ^ Print an @a@ as the right argument of a mixfix operator (behaves like 'right').
| I a -- ^ Print an @a@ as the inner argument of a mixfix operator (behaves like 'inner').
deriving (Eq, Ord, Read, Show)
instance Pretty1 AssocAnn
-- | Let the associativity annotations for arguments ('L', 'R', 'I')
-- behave as the 'left', 'right', and 'inner' functions.
instance Pretty a => Pretty (AssocAnn a) where
pp = \case
L a → left a
R a → right a
I a → inner a
-- | Print an @a@ as the left argument of a mixfix operator.
left :: Pretty a => a → PrettyM ()
left = assocDir AssocL
-- | Print an @a@ as the right argument of a mixfix operator.
right :: Pretty a => a → PrettyM ()
right = assocDir AssocR
-- | Print an @a@ as an inner argument of a mixfix operator.
inner :: Pretty a => a → PrettyM ()
inner ma = do
p' ← use precedence
a' ← use assoc
precedence .= (-1)
assoc .= AssocN
pp ma
precedence .= p'
assoc .= a'
assocDir :: Pretty a => Assoc → a → PrettyM ()
assocDir a ma = do
a' ← use assoc
if | a' == a → pp ma
| otherwise → do
assoc .= AssocN
pp ma
assoc .= a'