packages feed

hs-bindgen-1.0.0.0: src-internal/Text/SimplePrettyPrint.hs

{-# LANGUAGE CPP #-}

-- | Simple pretty-printing library
--
-- This library wraps the @pretty@ library.  Instead of using 'PP.Doc' directly,
-- type t'CtxDoc' threads a t'Context'.  Keeping track of the current indentation
-- and maximum line length enables the implementation of 'renderedLines', which
-- can be used to format documentation comments for the current indentation.
-- Keeping track of a unique name index enables rendering of unique names with
-- 'withFreshName'.
--
-- Some of the API functions provided by this library differ in behavior from
-- @pretty@ API functions of similar/same names.  In particular, @($+$)@
-- vertically joins documents with blank lines, while '($$)' vertically joins
-- documents without blank lines, behaving like the @pretty@ '(PP.$+$)'
-- function.  The list equivalents of these functions are similarly different.
--
-- The underlying @pretty@ library gives very little control over indentation.
-- If we would like to have better indentation, we should either switch to a
-- different underlying library or write our own.
--
-- Intended for qualified import
--
-- > import Text.SimplePrettyPrint (Pretty(..), CtxDoc)
-- > import Text.SimplePrettyPrint qualified as PP
--
-- There are also operators that can be imported, though you may prefer to use
-- 'hsep' or 'hcat' instead of '(><)', 'vsep' or 'vcat' instead of '($$)', etc.

-- > import Text.SimplePrettyPrint ((><), (<+>), ($$), ($+$))
module Text.SimplePrettyPrint (
    CtxDoc -- opaque
    -- * t'CtxDoc' features
  , withFreshName
  , ifFits
    -- * Rendering
  , runCtxDoc
  , renderCtxDoc
    -- ** Context
  , Context -- opaque
  , mkContext
  , defaultContext
  , debugContext
    -- * Pretty class
  , Pretty(..)
  , renderPretty
    -- * Construction
    -- ** Primitives
  , empty
  , char
  , string
  , show
  , text
  , renderedLines
    -- ** Horizontal and vertical composition
  , (><)
  , hcat
  , (<+>)
  , hsep
  , ($$)
  , vcat
  , ($+$)
  , vsep
  , cat
  , fcat
  , sep
  , fsep
    -- ** Bracketing
  , parens
  , parensWhen
  , vparensWhen
  , brackets
  , singleQuotes
    -- ** Lists
  , hlist
  , vlist
    -- ** Indentation
  , nest
  , hang
  , hangs
  , hangs'
  ) where

import Prelude hiding (show)
import Prelude qualified

import Data.List qualified as List
import Data.String (IsString (fromString))
import Data.Text qualified as Text
import Text.PrettyPrint.HughesPJ qualified as PP

#if MIN_VERSION_base(4,19,0)
import GHC.TypeError (ErrorMessage (..), Unsatisfiable)
#endif

{-------------------------------------------------------------------------------
  Context
-------------------------------------------------------------------------------}

-- | Pretty-printing context
data Context = Context {
      -- | Current indentation (number of spaces)
      indentation :: !Int
      -- | Maximum number of columns per line, when possible
    , maxLineCols :: !Int
      -- | Unique name index
    , uniqueNameIdx :: !Int
    }

-- | Construct an initial t'Context' with the specified line length
mkContext :: Int -> Context
mkContext maxLineCols = Context {
      indentation   = 0
    , maxLineCols   = maxLineCols
    , uniqueNameIdx = 0
    }

-- | Default pretty-printing context
defaultContext :: Context
defaultContext = mkContext 80

-- | Add to the indentation in a t'Context'
indentContext :: Int -> Context -> Context
indentContext n ctx = ctx{indentation = ctx.indentation + n}

-- | Get the next unique name index (and the updated context)
getUniqueNameIdx :: Context -> (Int, Context)
getUniqueNameIdx ctx =
    let i = ctx.uniqueNameIdx
    in  (i, ctx{uniqueNameIdx = i + 1})

{-------------------------------------------------------------------------------
  CtxDoc
-------------------------------------------------------------------------------}

-- | Contextualized document
newtype CtxDoc = CtxDoc (Context -> PP.Doc)

-- | Create a document with the specified string
instance IsString CtxDoc where
  fromString = string

-- | Not valid Haskell syntax, may be used for debugging purposes
instance Show CtxDoc where
  show = renderCtxDoc defaultContext

-- | Run a t'CtxDoc' with the specified context
runCtxDoc :: Context -> CtxDoc -> PP.Doc
runCtxDoc ctx (CtxDoc f) = f ctx

-- | Create a unique name with the specified hint
withFreshName :: String -> (CtxDoc -> CtxDoc) -> CtxDoc
withFreshName nameHint k = CtxDoc $ \ctx ->
    let (i, ctx') = getUniqueNameIdx ctx
    in  runCtxDoc ctx' . k $ CtxDoc (\_ -> PP.text (nameHint ++ Prelude.show i))

-- | Render a t'CtxDoc'
renderCtxDoc :: Context -> CtxDoc -> String
renderCtxDoc ctx = PP.renderStyle style . runCtxDoc ctx
  where
    style :: PP.Style
    style = PP.style {PP.lineLength = ctx.maxLineCols}

{-------------------------------------------------------------------------------
  Pretty
-------------------------------------------------------------------------------}

-- | Types that can be pretty-printed
class Pretty a where
  -- | Create a document for the specified value with @0@ precedence
  pretty :: a -> CtxDoc
  pretty = prettyPrec 0

  -- | Create a document for the specified value with the specified precedence
  prettyPrec :: Int -> a -> CtxDoc
  prettyPrec _prec = pretty

  {-# MINIMAL pretty | prettyPrec #-}

#if MIN_VERSION_base(4,19,0)
instance Unsatisfiable (Text "Don't pretty Strings, use fromString") => Pretty [a]
#endif

-- | Render a 'Pretty' value
renderPretty :: Pretty a => Context -> a -> String
renderPretty ctx = renderCtxDoc ctx . pretty

{-------------------------------------------------------------------------------
  API
-------------------------------------------------------------------------------}

-- | The empty document
empty :: CtxDoc
empty = CtxDoc $ const PP.empty

-- | Create a document with the specified character
char :: Char -> CtxDoc
char = CtxDoc . const . PP.char

-- | Create a document with the specified string
string :: String -> CtxDoc
string = CtxDoc . const . PP.text

-- | Create a document with the result of 'show'
show :: Show a => a -> CtxDoc
show = string . Prelude.show

-- | Create a document with the specified 'Text'
text :: Text.Text -> CtxDoc
text = string . Text.unpack

-- | Horizontally join two documents
--
-- Note that 'Semigroup' '(<>)' is /not/ used because it is right-associative.
infixl 6 ><
(><) :: CtxDoc -> CtxDoc -> CtxDoc
dL >< dR = CtxDoc $ \ctx ->
    runCtxDoc ctx dL PP.<> runCtxDoc ctx dR

-- | Horizontally join documents
hcat :: [CtxDoc] -> CtxDoc
hcat ds = CtxDoc $ \ctx ->
    PP.hcat $ map (runCtxDoc ctx) ds

-- | Horizontally join two documents, separating by space
infixl 6 <+>
(<+>) :: CtxDoc -> CtxDoc -> CtxDoc
dL <+> dR = CtxDoc $ \ctx ->
    runCtxDoc ctx dL PP.<+> runCtxDoc ctx dR

-- | Horizontally join documents, separating by space
hsep :: [CtxDoc] -> CtxDoc
hsep ds = CtxDoc $ \ctx ->
    PP.hsep $ map (runCtxDoc ctx) ds

-- | Vertically join two documents
infixl 5 $$
($$) :: CtxDoc -> CtxDoc -> CtxDoc
dL $$ dR = CtxDoc $ \ctx ->
    runCtxDoc ctx dL PP.$+$ runCtxDoc ctx dR

-- | Vertically join documents
vcat :: [CtxDoc] -> CtxDoc
vcat = \case
    [] -> empty
    ds -> CtxDoc $ \ctx -> foldl1 aux $ map (runCtxDoc ctx) ds
  where
    -- If there are empty spaces, aka newlines we won't create extra
    -- unneeded whitespaces
    aux :: PP.Doc -> PP.Doc -> PP.Doc
    aux dL dR
        | dL == "" && dR == "" = PP.nest (-100) dL PP.$+$ PP.nest (-100) dR
        | dL == ""             = PP.nest (-100) dL PP.$+$ dR
        | dR == ""             = dL PP.$+$ PP.nest (-100) dR
        | otherwise            = dL PP.$+$ dR

-- | Vertically join two documents, separating by a blank line
infixl 5 $+$
($+$) :: CtxDoc -> CtxDoc -> CtxDoc
dL $+$ dR = CtxDoc $ \ctx ->
    runCtxDoc ctx dL PP.$+$ PP.nest (-100) "" PP.$+$ runCtxDoc ctx dR

-- | Vertically join documents, separating by blank lines
--
-- If there the context is nested the new line won't create unneded empty
-- spaces.
vsep :: [CtxDoc] -> CtxDoc
vsep = \case
    [] -> empty
    ds -> CtxDoc $ \ctx -> foldl1 aux $ map (runCtxDoc ctx) ds
  where
    aux :: PP.Doc -> PP.Doc -> PP.Doc
    aux dL dR
        | PP.isEmpty dL = dR
        | PP.isEmpty dR = dL
        | otherwise     = dL PP.$+$ PP.nest (-100) "" PP.$+$ dR

-- | Horizontally /or/ vertically join documents, depending on if there is
-- room on the line
cat :: [CtxDoc] -> CtxDoc
cat ds = CtxDoc $ \ctx -> PP.cat $ map (runCtxDoc ctx) ds

-- | Horizontally /or/ vertically join documents, depending on if there is
-- room on the line, \"paragraph fill\" version
fcat :: [CtxDoc] -> CtxDoc
fcat ds = CtxDoc $ \ctx -> PP.fcat $ map (runCtxDoc ctx) ds

-- | Horizontally /or/ vertically join documents, depending on if there is
-- room on the line, horizontally separating by space
sep :: [CtxDoc] -> CtxDoc
sep ds = CtxDoc $ \ctx -> PP.sep $ map (runCtxDoc ctx) ds

-- | Horizontally /or/ vertically join documents, depending on if there is
-- room on the line, horizontally separating by space, \"paragraph fill\"
-- version
fsep :: [CtxDoc] -> CtxDoc
fsep ds = CtxDoc $ \ctx -> PP.fsep $ map (runCtxDoc ctx) ds

-- | Parenthesize a document horizontally
parens :: CtxDoc -> CtxDoc
parens d = hcat [char '(', d, char ')']

-- | Surround a document with left and right bracket horizontally
brackets :: CtxDoc -> CtxDoc
brackets d = hcat [char '[', d, char ']']

-- | Surround a document with single quotes horizontally
singleQuotes :: CtxDoc -> CtxDoc
singleQuotes d = hcat [char '\'', d, char '\'']

-- | Parenthesize a document horizontally when true
parensWhen :: Bool -> CtxDoc -> CtxDoc
parensWhen = \case
    False -> id
    True  -> parens

-- | Format a list horizontally
hlist :: String -> String -> [CtxDoc] -> CtxDoc
hlist cL cR ds =
    hcat $ string cL : (List.intersperse (string ", ") ds) ++ [string cR]

-- | Parenthesize a document vertically
vparens :: CtxDoc -> CtxDoc
vparens d = nest 2 (char '(' <+> d) $$ char ')'

-- | Parenthesize a document vertically when true
vparensWhen :: Bool -> CtxDoc -> CtxDoc
vparensWhen = \case
    False -> id
    True  -> vparens

-- | Format a list vertically (one item per line)
vlist :: String -> String -> [CtxDoc] -> CtxDoc
vlist cL cR = \case
    [] -> string cL >< string cR
    (d:ds') -> vcat
      $ (string cL <+> d)
      : [char ',' <+> d' | d' <- ds']
      ++ [string cR]

-- | Nest/indent a document by the specified number of spaces
nest :: Int -> CtxDoc -> CtxDoc
nest n d = CtxDoc $ \ctx ->
    PP.nest n $ runCtxDoc (indentContext n ctx) d

-- | Hang a document below another with the specified indentation
hang :: CtxDoc -> Int -> CtxDoc -> CtxDoc
hang dA n dB = dA $$ nest n dB

-- | Hang multiple documents below another with the specified indentation,
-- separated by blank lines
hangs :: CtxDoc -> Int -> [CtxDoc] -> CtxDoc
hangs dA n dBs = dA $$ vsep (nest n <$> dBs)

-- | Hang multiple documents below another with the specified indentation,
-- do __not__ seprate by blank lines
hangs' :: CtxDoc -> Int -> [CtxDoc] -> CtxDoc
hangs' dA n dBs = dA $$ vcat (nest n <$> dBs)

-- | Select a t'CtxDoc' depending on if a rendered t'CtxDoc' fits within the
-- maximum line length
ifFits :: CtxDoc -> CtxDoc -> CtxDoc -> CtxDoc
ifFits condD thenD elseD = CtxDoc $ \ctx ->
    if length (renderCtxDoc ctx condD) <= ctx.maxLineCols
      then runCtxDoc ctx thenD
      else runCtxDoc ctx elseD

-- | Create a document with rendered lines
--
-- The function is passed the maximum width of each line, computed as the
-- maximum line length minus the current indentation.  It must return a list
-- of lines, using an empty string to represent a blank line.  None of the
-- strings may contain newline characters.
--
-- The lines are /automatically/ indented according to the current indentation.
renderedLines :: (Int -> [String]) -> CtxDoc
renderedLines f = CtxDoc $ \ctx ->
    case f (max 0 (ctx.maxLineCols - ctx.indentation)) of
      [] -> PP.empty
      ss -> foldl1 (PP.$+$) $ map PP.text ss

-- | Create a document with context information, for debugging
debugContext :: CtxDoc
debugContext = CtxDoc $ \ctx -> PP.parens $
    PP.hsep ["Context", PP.int ctx.indentation, PP.int ctx.maxLineCols]