patat-0.11.0.0: lib/Patat/PrettyPrint/Internal.hs
--------------------------------------------------------------------------------
-- | This is a small pretty-printing library.
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
module Patat.PrettyPrint.Internal
( Control (..)
, Chunk (..)
, Chunks
, chunkToString
, chunkLines
, DocE (..)
, chunkToDocE
, Indentation (..)
, Doc (..)
, docToChunks
, toString
, dimensions
, null
, hPutDoc
, putDoc
, mkDoc
, string
) where
--------------------------------------------------------------------------------
import Control.Monad.Reader (asks, local)
import Control.Monad.RWS (RWS, runRWS)
import Control.Monad.State (get, modify)
import Control.Monad.Writer (tell)
import Data.Char.WCWidth.Extended (wcstrwidth)
import qualified Data.List as L
import Data.String (IsString (..))
import Prelude hiding (null)
import qualified System.Console.ANSI as Ansi
import qualified System.IO as IO
--------------------------------------------------------------------------------
-- | Control actions for the terminal.
data Control
= ClearScreenControl
| GoToLineControl Int
deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | A simple chunk of text. All ANSI codes are "reset" after printing.
data Chunk
= StringChunk [Ansi.SGR] String
| NewlineChunk
| ControlChunk Control
deriving (Eq, Show)
--------------------------------------------------------------------------------
type Chunks = [Chunk]
--------------------------------------------------------------------------------
hPutChunk :: IO.Handle -> Chunk -> IO ()
hPutChunk h NewlineChunk = IO.hPutStrLn h ""
hPutChunk h (StringChunk codes str) = do
Ansi.hSetSGR h (reverse codes)
IO.hPutStr h str
Ansi.hSetSGR h [Ansi.Reset]
hPutChunk h (ControlChunk ctrl) = case ctrl of
ClearScreenControl -> Ansi.hClearScreen h
GoToLineControl l -> Ansi.hSetCursorPosition h l 0
--------------------------------------------------------------------------------
chunkToString :: Chunk -> String
chunkToString NewlineChunk = "\n"
chunkToString (StringChunk _ str) = str
chunkToString (ControlChunk _) = ""
--------------------------------------------------------------------------------
-- | If two neighboring chunks have the same set of ANSI codes, we can group
-- them together.
optimizeChunks :: Chunks -> Chunks
optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks)
| c1 == c2 = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks)
| otherwise =
StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks)
optimizeChunks (x : chunks) = x : optimizeChunks chunks
optimizeChunks [] = []
--------------------------------------------------------------------------------
chunkLines :: Chunks -> [Chunks]
chunkLines chunks = case break (== NewlineChunk) chunks of
(xs, _newline : ys) -> xs : chunkLines ys
(xs, []) -> [xs]
--------------------------------------------------------------------------------
data DocE d
= String String
| Softspace
| Hardspace
| Softline
| Hardline
| WrapAt
{ wrapAtCol :: Maybe Int
, wrapDoc :: d
}
| Ansi
{ ansiCode :: [Ansi.SGR] -> [Ansi.SGR] -- ^ Modifies current codes.
, ansiDoc :: d
}
| Indent
{ indentFirstLine :: Indentation [Chunk]
, indentOtherLines :: Indentation [Chunk]
, indentDoc :: d
}
| Control Control
deriving (Functor)
--------------------------------------------------------------------------------
chunkToDocE :: Chunk -> DocE Doc
chunkToDocE NewlineChunk = Hardline
chunkToDocE (StringChunk c1 str) = Ansi (\c0 -> c1 ++ c0) (Doc [String str])
chunkToDocE (ControlChunk ctrl) = Control ctrl
--------------------------------------------------------------------------------
newtype Doc = Doc {unDoc :: [DocE Doc]}
deriving (Monoid, Semigroup)
--------------------------------------------------------------------------------
instance Show Doc where
show = toString
--------------------------------------------------------------------------------
instance IsString Doc where
fromString = string
--------------------------------------------------------------------------------
data DocEnv = DocEnv
{ deCodes :: [Ansi.SGR] -- ^ Most recent ones first in the list
, deIndent :: [Indentation [Chunk]] -- ^ No need to store first-line indent
, deWrap :: Maybe Int -- ^ Wrap at columns
}
--------------------------------------------------------------------------------
type DocM = RWS DocEnv Chunks LineBuffer
--------------------------------------------------------------------------------
-- | Note that the lists here are reversed so we have fast append.
-- We also store the current length to avoid having to recompute it.
data LineBuffer = LineBuffer Int [Indentation [Chunk]] [Chunk]
--------------------------------------------------------------------------------
emptyLineBuffer :: LineBuffer
emptyLineBuffer = LineBuffer 0 [] []
--------------------------------------------------------------------------------
data Indentation a = Indentation Int a
deriving (Foldable, Functor, Traversable)
--------------------------------------------------------------------------------
indentationToChunks :: Indentation [Chunk] -> [Chunk]
indentationToChunks (Indentation 0 c) = c
indentationToChunks (Indentation n c) = StringChunk [] (replicate n ' ') : c
--------------------------------------------------------------------------------
indentationWidth :: Indentation [Chunk] -> Int
indentationWidth (Indentation s c) =
s + sum (map (wcstrwidth . chunkToString) c)
--------------------------------------------------------------------------------
bufferToChunks :: LineBuffer -> Chunks
bufferToChunks (LineBuffer _ ind chunks) = case chunks of
[] -> concatMap indentationToChunks $ reverse $
dropWhile emptyIndentation ind
_ -> concatMap indentationToChunks (reverse ind) ++ reverse chunks
where
emptyIndentation (Indentation _ []) = True
emptyIndentation _ = False
--------------------------------------------------------------------------------
docToChunks :: Doc -> Chunks
docToChunks doc0 =
let env0 = DocEnv [] [] Nothing
((), b, cs) = runRWS (go $ unDoc doc0) env0 emptyLineBuffer in
optimizeChunks (cs <> bufferToChunks b)
where
go :: [DocE Doc] -> DocM ()
go [] = return ()
go (String str : docs) = do
chunk <- makeChunk str
appendChunk chunk
go docs
go (Softspace : docs) = do
hard <- softConversion Softspace docs
go (hard : docs)
go (Hardspace : docs) = do
chunk <- makeChunk " "
appendChunk chunk
go docs
go (Softline : docs) = do
hard <- softConversion Softline docs
go (hard : docs)
go (Hardline : docs) = do
buffer <- get
tell $ bufferToChunks buffer <> [NewlineChunk]
ind <- asks deIndent
modify $ \_ -> case docs of
[] -> emptyLineBuffer
_ : _ -> LineBuffer (sum $ map indentationWidth ind) ind []
go docs
go (WrapAt {..} : docs) = do
il <- asks $ sum . map indentationWidth . deIndent
local (\env -> env {deWrap = fmap (+ il) wrapAtCol}) $ go (unDoc wrapDoc)
go docs
go (Ansi {..} : docs) = do
local (\env -> env {deCodes = ansiCode (deCodes env)}) $
go (unDoc ansiDoc)
go docs
go (Indent {..} : docs) = do
local (\e -> e {deIndent = indentOtherLines : deIndent e}) $ do
modify $ \(LineBuffer w i c) -> LineBuffer
(w + indentationWidth indentFirstLine) (indentFirstLine : i) c
go (unDoc indentDoc)
go docs
go (Control ctrl : docs) = do
tell [ControlChunk ctrl]
go docs
makeChunk :: String -> DocM Chunk
makeChunk str = do
codes <- asks deCodes
return $ StringChunk codes str
appendChunk :: Chunk -> DocM ()
appendChunk c = modify $ \(LineBuffer w i cs) ->
LineBuffer (w + wcstrwidth (chunkToString c)) i (c : cs)
-- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline'
softConversion :: DocE Doc -> [DocE Doc] -> DocM (DocE Doc)
softConversion soft docs = do
mbWrapCol <- asks deWrap
case mbWrapCol of
Nothing -> return hard
Just maxCol -> do
LineBuffer currentCol _ _ <- get
case nextWordLength docs of
Nothing -> return hard
Just l
| currentCol + 1 + l <= maxCol -> return Hardspace
| otherwise -> return Hardline
where
hard = case soft of
Softspace -> Hardspace
Softline -> Hardline
_ -> soft
nextWordLength :: [DocE Doc] -> Maybe Int
nextWordLength [] = Nothing
nextWordLength (String x : xs)
| L.null x = nextWordLength xs
| otherwise = Just (wcstrwidth x)
nextWordLength (Softspace : xs) = nextWordLength xs
nextWordLength (Hardspace : xs) = nextWordLength xs
nextWordLength (Softline : xs) = nextWordLength xs
nextWordLength (Hardline : _) = Nothing
nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc ++ xs)
nextWordLength (Ansi {..} : xs) = nextWordLength (unDoc ansiDoc ++ xs)
nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs)
nextWordLength (Control _ : _) = Nothing
--------------------------------------------------------------------------------
toString :: Doc -> String
toString = concat . map chunkToString . docToChunks
--------------------------------------------------------------------------------
-- | Returns the rows and columns necessary to render this document
dimensions :: Doc -> (Int, Int)
dimensions doc =
let ls = lines (toString doc) in
(length ls, foldr max 0 (map wcstrwidth ls))
--------------------------------------------------------------------------------
null :: Doc -> Bool
null doc = case unDoc doc of [] -> True; _ -> False
--------------------------------------------------------------------------------
hPutDoc :: IO.Handle -> Doc -> IO ()
hPutDoc h = mapM_ (hPutChunk h) . docToChunks
--------------------------------------------------------------------------------
putDoc :: Doc -> IO ()
putDoc = hPutDoc IO.stdout
--------------------------------------------------------------------------------
mkDoc :: DocE Doc -> Doc
mkDoc e = Doc [e]
--------------------------------------------------------------------------------
string :: String -> Doc
string "" = Doc []
string str = mkDoc $ String str -- TODO (jaspervdj): Newline conversion?