packages feed

portray-prettyprinter-0.2.1: src/Data/Portray/Prettyprinter.hs

-- Copyright 2021 Google LLC
-- Copyright 2022 Andrew Pritchard
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--      http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- | Provides rendering of 'Portrayal' to 'Doc'.
--
-- There are two intended uses of this module: firstly, to use @prettyprinter@'s
-- layout and rendering algorithms to render 'Portray' instances, 'Diff's, or
-- other 'Portrayal's; and secondly, to derive 'Pretty' instances based on
-- existing 'Portray' instances.  I find the former more ergonomic, but in
-- established codebases that want to benefit from deriving, the latter may be
-- more achievable.
--
-- The first usage is for codebases with pervasive use of 'Portray', and
-- involves using e.g. 'pp' and 'ppd' in GHCi, or 'showPortrayal' or 'showDiff'
-- in application code.  With this usage, anything you want to pretty-print
-- needs a 'Portray' instance, and the typeclass 'Pretty' is not involved in
-- any way.  With this approach, pretty-printable types and the types they
-- include should derive only 'Portray', and pretty-printing should be done
-- with the aforementioned utility functions:
--
-- @
-- data MyRecord = MyRecord { anInt :: Int, anotherRecord :: MyOtherRecord }
--   deriving Generic
--   deriving Portray via Wrapped Generic MyRecord
--
-- example = 'showPortrayal' (MyRecord 2 ...)
-- @
--
-- This usage provides colorized pretty-printing by default with 'pp'.  Note if
-- you don't like the default choice of colors or don't want colors at all, you
-- can roll your own 'pp' function with 'portray', 'toDocAssocPrec' and your
-- @prettyprinter@ rendering backend of choice.
--
-- The second usage is to use @portray@'s generic deriving to provide derived
-- 'Pretty' instances, in a codebase that uses 'Pretty' as the preferred
-- typeclass for pretty-printable values.  With this usage, things you want to
-- pretty-print need 'Pretty' instances, and 'Portray' is needed for the
-- transitive closure of types included in types you want to derive 'Pretty'
-- instances for.  This may result in many types needing both instances of both
-- 'Pretty' (for direct pretty-printing) and 'Portray' (for deriving 'Portray'
-- on downstream types) instances.  Note that with this approach, types that
-- derive their 'Pretty' instances via 'Portray' will ignore any custom
-- 'Pretty' instances of nested types, since they recurse to nested 'Portray'
-- instances instead.
--
-- To derive an instance for a pretty-printable type, the type itself should
-- look like the following:
--
-- @
-- data MyRecord = MyRecord { anInt :: Int, anotherRecord :: MyOtherRecord }
--   deriving Generic
--   deriving Portray via Wrapped Generic MyRecord
--   deriving Pretty via WrappedPortray MyRecord
--
-- example = 'R.renderStrict' $ 'pretty' (MyRecord 2 ...)
-- @
--
-- And any types transitively included in it should look like the following:
--
-- @
-- data MyOtherRecord = MyOtherRecord
--   deriving Generic
--   deriving Portray via Wrapped Generic MyRecord
-- @
--
-- Since the 'Pretty' class requires a universally-quantified annotation type,
-- its instances cannot provide any annotations.  As such, this usage cannot
-- provide automatic colorization.
--
-- This module also exports the underlying rendering functionality in a variety
-- of forms for more esoteric uses.

{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.Portray.Prettyprinter
         ( -- * Pretty-Printing
           showPortrayal, pp
           -- * Diffing
         , showDiff, ppd
           -- * DerivingVia wrapper
         , WrappedPortray(..)
           -- * Rendering
           -- ** Configuration
         , Config, defaultConfig, prettyConfig
           -- *** Escape Sequences
         , setShouldEscapeChar, escapeNonASCII, escapeSpecialOnly
           -- *** Numeric Literals
         , setTrimTrailingFloatZeros, setScientificNotationThreshold
         , setSeparatorGroupSizes
           -- ** Colorization
         , SyntaxClass(..), LitKind(..)
         , defaultStyling, subtleStyling, noStyling
           -- ** With Associativity
         , DocAssocPrec, toDocAssocPrecF, toDocAssocPrec
           -- ** Convenience Functions
         , portrayalToDoc
         , styleShowPortrayal, styleShowPortrayalLazy
         , prettyShowPortrayal, prettyShowPortrayalLazy
         , basicShowPortrayal
         ) where

import Data.Char (isAscii, isDigit, isPrint)
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as T (putStrLn)
import GHC.Show (showLitChar)

#if MIN_VERSION_prettyprinter(1, 7, 0)
#define Prettyprinter_ Prettyprinter
#else
#define Prettyprinter_ Data.Text.Prettyprint.Doc
#endif

import Prettyprinter_ (Doc, Pretty(..))
import qualified Prettyprinter_.Render.Terminal as A -- for ANSI
import qualified Prettyprinter_ as P

import Data.Portray
         ( Assoc(..), Infixity(..), FactorPortrayal(..)
         , Ident(..), IdentKind(..)
         , Portray, Portrayal(..), PortrayalF(..)
         , cata, portray
         , Base(..), FloatLiteral(..)
         , formatIntLit, formatFloatLit, formatSpecialFloat
         , normalizeFloatLit, trimFloatLit
         )
import Data.Portray.Diff (Diff(..))

-- | Pretty-print a value to stdout using its 'Portray' instance.
--
-- This uses ANSI color codes, so take care not to use it in contexts where it
-- might output to something other than a terminal.
pp :: Portray a => a -> IO ()
pp = T.putStrLn . prettyShowPortrayalLazy . portray

-- | Pretty-print a value using its 'Portray' instance.
--
-- This uses no ANSI terminal escape codes and escapes all non-ASCII characters.
showPortrayal :: Portray a => a -> Text
showPortrayal = basicShowPortrayal . portray

-- | Pretty-print a diff between two values to stdout using a 'Diff' instance.
--
-- This uses ANSI color codes, so take care not to use it in contexts where it
-- might output to something other than a terminal.
ppd :: Diff a => a -> a -> IO ()
ppd x = T.putStrLn . maybe "_" prettyShowPortrayalLazy . diff x

-- | Pretty-print a diff between two values using a 'Diff' instance.
--
-- This uses no ANSI terminal escape codes and escapes all non-ASCII characters.
showDiff :: Diff a => a -> a -> Text
showDiff x = maybe "_" basicShowPortrayal . diff x

-- | A 'Doc' that varies according to associativity and precedence context.
type DocAssocPrec ann = Assoc -> Rational -> Doc ann

fixityCompatible :: Infixity -> Assoc -> Rational -> Bool
fixityCompatible (Infixity assoc p) assoc' p' = case compare p' p of
  GT -> False  -- Context has higher precedence than this binop.
  EQ -> assoc == assoc'
  LT -> True

matchCtx :: Assoc -> Assoc -> Assoc
matchCtx ctx assoc
  | ctx == assoc = ctx
  | otherwise = AssocNope

-- | The particular kind of literal represented by a 'Literal'.
data LitKind = IntLit | RatLit | CharLit | StrLit

-- | The kind of syntactic element represented by an annotated 'Doc'.
data SyntaxClass
  = Identifier IdentKind
    -- ^ Identifiers, whether alphanumeric names or operators.
  | Literal LitKind
    -- ^ Literals, including integers, floats/rationals, chars, and strings.
  | EscapeSequence
    -- ^ Escaped characters in strings and char literals.
  | Keyword
    -- ^ Alphanumeric keywords, e.g. @case@.
  | Bracket
    -- ^ Matched pairs of symbols that denote nesting, e.g. parens.
  | Separator
    -- ^ Syntactic separators/terminators, e.g. @,@ and @;@.
  | Structural
    -- ^ Other fixed syntactic symbols, e.g. @::@, @\@@, @->@, @\\@.

-- | A fairly arbitrary colorization style based on what looked good to me.
--
-- To use a custom color mapping, define it the same way this function is
-- defined, then use it as an argument to 'styleShowPortrayal'.
-- Consider also wrapping that up into a custom 'pp' function for use at the
-- REPL or even as the interactive print function.
defaultStyling :: SyntaxClass -> Maybe A.AnsiStyle
defaultStyling = Just . \case
  Identifier k -> case k of
    OpConIdent -> A.color A.Magenta
    OpIdent -> A.colorDull A.Yellow
    ConIdent -> mempty
    VarIdent -> mempty
  Literal k -> case k of
    StrLit -> A.colorDull A.Blue
    _      -> A.colorDull A.Cyan
  EscapeSequence -> A.colorDull A.Red
  Keyword -> A.colorDull A.Green
  Bracket -> mempty
  Separator -> mempty
  Structural -> A.colorDull A.Green

-- | A subtler style that colorizes only operators (blue) and literals (cyan).
subtleStyling :: SyntaxClass -> Maybe A.AnsiStyle
subtleStyling = Just . \case
  Identifier k -> case k of
    OpConIdent -> A.colorDull A.Blue
    OpIdent -> A.colorDull A.Blue
    _ -> mempty
  Literal _ -> A.colorDull A.Cyan
  EscapeSequence -> A.colorDull A.Cyan
  _ -> mempty

-- | Disable all syntax highlighting.
noStyling :: SyntaxClass -> Maybe A.AnsiStyle
noStyling = const Nothing

-- | An escape-sequence predicate to escape any non-ASCII character.
escapeNonASCII :: Char -> Bool
escapeNonASCII = not . isAscii

-- | An escape-sequence predicate to escape as little as possible.
escapeSpecialOnly :: Char -> Bool
escapeSpecialOnly = const False

-- | Configuration for the conversion to 'Doc'.
--
-- Includes the following:
--
-- * 'setShouldEscapeChar', a function determining whether an escapable
-- character should be escaped in a string or character literal.  Unprintable
-- characters, backslashes, and the relevant quote for the current literal type
-- are always escaped, and anything that wouldn't be escaped by 'Show' is never
-- escaped.
--
-- * 'setTrimTrailingFloatZeros', whether to trim trailing zeros in
-- floating-point literals.
--
-- * 'setScientificNotationThreshold', a limit on the number of padding
-- (non-precision) zeros in floating-point literals before switching to
-- scientific notation.
--
-- * 'setSeparatorGroupSizes', configuration of where to place underscores in
-- the whole-number part of integral and fractional literals.
--
-- For forwards-compatibility reasons, the field selectors and constructor of
-- this type are hidden; use the provided setters to build a config.  For
-- example:
--
-- @
-- config =
--   defaultConfig
--     & setShouldEscapeChar (const True) -- Escape everything we can.
--     & setTrimTrailingFloatZeros True
-- @
data Config = Config
  { _shouldEscapeChar :: Char -> Bool
  , _scientificNotationThreshold :: Int
  , _trimTrailingFloatZeros :: Bool
  , _separatorGroupSizes :: Base -> [Int]
  }

-- | Set the predicate for whether to escape a given character; see 'Config'.
setShouldEscapeChar :: (Char -> Bool) -> Config -> Config
setShouldEscapeChar f c = c { _shouldEscapeChar = f }

-- | Configure trimming of trailing zeros from floating-point literals.
--
-- @since 0.2.1
setTrimTrailingFloatZeros :: Bool -> Config -> Config
setTrimTrailingFloatZeros b c = c { _trimTrailingFloatZeros = b }

-- | Configure the number of zeros to pad with before using scientific notation.
--
-- If the radix point is within or adjaecent to the specified digits in a float
-- literal, it's considered to need no padding zeros.  If the radix point is
-- outside the specified digits, we can either materialize extra zeros to cover
-- the gap between the specified digits and the radix point, or use scientific
-- notation to move the radix point into the specified digits.  A single
-- placeholder zero to the left of the radix point is not considered to be a
-- padding zero.
--
-- @
--     FloatLiteral False "1234" (-4) = _.____1 234
--                                    = 0.00001 234 -- 4 padding 0s
--                                    =       1.234e-5
-- @
--
-- @
--     FloatLiteral False "1234" 8    = 1 234____._
--                                    = 1 2340000   -- 4 padding 0s
--                                    = 1.234e7
-- @
--
-- Trailing that are part of the specified digits are not considered to be
-- padding (if not trimmed by 'setTrimTrailingFloatZeros'):
--
-- @
--     FloatLiteral False "100" 4     = 1 00_._
--                                    = 1 000 -- 1 padding 0
--                                    = 1.000e3
-- @
--
-- This threshold determines how many padding zeros to tolerate before
-- switching over to scientific notation.  Choosing a very high threshold
-- naturally means scientific notation will ~never be used.  Choosing a
-- negative threshold naturally means scientific notation will always be used.
--
-- Deciding based on the number of padding zeros rather than the absolute
-- magnitude of the number means we won't needlessly format @1234567@ as
-- @1.234567e6@ when doing so doesn't actually make the representation more
-- compact.
--
-- @since 0.2.1
setScientificNotationThreshold :: Int -> Config -> Config
setScientificNotationThreshold n c = c { _scientificNotationThreshold = n }

-- | Set the separator spacing for NumericUnderscores, or disable underscores.
--
-- The list of group sizes is used working leftwards from the radix point.  If
-- the list runs out, no further separators will be inserted.
--
-- @
--     [4, 3, 2, 2] : 123456000000 => 1_23_45_600_0000
--     repeat 3     : 123456000000 => 123_456_000_000
--     [1]          : 123456000000 => 12345600000_0
-- @
--
-- This allows both the conventional US separator placement of every three
-- digits by providing @cycle 3@, as well as more complex spacings such as
-- @3 : repeat 2@ reportedly used in India.
--
-- Backends should not cache these lists, and should attempt to use them in a
-- single-use, streaming manner, so that large portions of infinite lists are
-- not held in memory.  Clients should assume returning infinite lists is fine.
--
-- @since 0.2.1
setSeparatorGroupSizes :: (Base -> [Int]) -> Config -> Config
setSeparatorGroupSizes f c = c { _separatorGroupSizes = f }

-- | A sensible, conservative default configuration to build on.
--
-- * Uses 'escapeNonASCII' to escape everything but printable ASCII characters.
-- * Preserves any trailing zeros in float literals.
-- * Uses scientific notation when any padding zeros would be needed.
-- * Does not use numeric underscores.
defaultConfig :: Config
defaultConfig = Config escapeNonASCII 0 False (const [])

-- | A default "pretty" config with more opinionated choices.
--
-- This using numeric underscores, slightly less scientific notation, and less
-- escaping compared to 'defaultConfig'.
--
-- @since 0.2.1
prettyConfig :: Config
prettyConfig =
  defaultConfig
    & setShouldEscapeChar escapeSpecialOnly
    & setScientificNotationThreshold 2
    & setSeparatorGroupSizes
        (\case
          Decimal -> repeat 3 -- Conventional US spacing: powers of 1,000.
          Binary -> repeat 8  -- 8-bit groups.
          Octal ->  []        -- /shrug/ doesn't divide bytes evenly.
          Hex -> repeat 8)    -- 32-bit groups.

-- | Convert a 'Portrayal' to a 'Doc'.
portrayalToDoc :: Config -> Portrayal -> Doc SyntaxClass
portrayalToDoc cfg t = toDocAssocPrec cfg t AssocNope (-1)

parens :: Doc SyntaxClass -> Doc SyntaxClass
parens d =
  P.annotate Bracket (char '(') <> d <> P.annotate Bracket (char ')')

-- Conditionally wrap a document in parentheses.
maybeParens :: Bool -> Doc SyntaxClass -> Doc SyntaxClass
maybeParens = \case True -> parens; False -> id

-- Convert Text to a document; 'pretty' specialized to 'Text'.
text :: Text -> Doc ann
text = pretty

-- Convert a Char to a document; 'pretty' specialized to 'Char'.
char :: Char -> Doc ann
char = pretty

ppInfix :: Ident -> Doc SyntaxClass
ppInfix (Ident k nm) = case k of
  OpConIdent -> nmDoc
  OpIdent -> nmDoc
  VarIdent -> wrappedNm
  ConIdent -> wrappedNm
 where
  backquote = P.annotate Structural (char '`')
  nmDoc = P.annotate (Identifier k) (text nm)
  wrappedNm = backquote <> nmDoc <> backquote

ppPrefix :: Ident -> Doc SyntaxClass
ppPrefix (Ident k nm) = case k of
  OpConIdent -> wrappedNm
  OpIdent -> wrappedNm
  VarIdent -> nmDoc
  ConIdent -> nmDoc
 where
  nmDoc = P.annotate (Identifier k) (text nm)
  wrappedNm = parens nmDoc

ppBinop
  :: Ident
  -> Infixity
  -> DocAssocPrec SyntaxClass
  -> DocAssocPrec SyntaxClass
  -> DocAssocPrec SyntaxClass
ppBinop nm fx@(Infixity assoc opPrec) x y lr p =
  maybeParens (not $ fixityCompatible fx lr p) $ P.nest 2 $ P.sep
    [ x (matchCtx AssocL assoc) opPrec P.<+> ppInfix nm
    , y (matchCtx AssocR assoc) opPrec
    ]

ppBulletList
  :: Doc SyntaxClass -- ^ Open brace,  e.g. {  [  {  (
  -> Doc SyntaxClass -- ^ Separator,   e.g. ;  ,  ,  ,
  -> Doc SyntaxClass -- ^ Close brace, e.g. }  ]  }  )
  -> [Doc SyntaxClass]
  -> Doc SyntaxClass
ppBulletList o s c = \case
  []         -> opener <> closer
  (doc:docs) ->
    P.group $
      opener <> P.flatAlt " " "" <> P.align doc <>
      foldMap
        (\x -> P.group (P.line' <> x))
        (map (\x -> separator P.<+> P.align x) docs) <>
      P.line' <> closer
 where
  opener = P.annotate Bracket o
  separator = P.annotate Separator s
  closer = P.annotate Bracket c

foldl01 :: (b -> a -> b) -> (a -> b) -> b -> [a] -> b
foldl01 f g z = \case
  [] -> z
  (x:xs) -> foldl f (g x) xs

-- 'T.words' coalesces adjacent spaces, so it's not suitable for use in
-- 'ppStrLit'; roll our own that preserves the whitespace between words.
wordsSep :: Text -> [(Text, Text)]
wordsSep "" = []
wordsSep s =
  let isWhitespace = (`elem` [' ', '\t'])
      (word, rest) = T.break isWhitespace s
      (sep, rest') = T.span isWhitespace rest
  in  (word, sep) : wordsSep rest'

-- 'T.lines' also fails to distinguish trailing newlines... ugh.
linesSep :: Text -> [Text]
linesSep "" = []
linesSep s0 = go s0
 where
  go s =
    let (line, rest) = T.break (== '\n') s
    in  line : if T.null rest then [] else go (T.tail rest)

-- Returns True for characters that must always be escaped regardless of
-- configuration; this is unprintable characters and backlashes.
charAlwaysEscaped :: Char -> Bool
charAlwaysEscaped c = not (isPrint c) || c == '\\'

shouldEscapeChar :: Config -> Char -> Bool
shouldEscapeChar cfg c = charAlwaysEscaped c || _shouldEscapeChar cfg c

showLitEscapesChar :: Char -> Bool
showLitEscapesChar c = [c] /= showLitChar c ""

litCharIsEscaped :: Config -> Char -> Bool
litCharIsEscaped cfg = \case
  '\'' -> True
  c    -> showLitEscapesChar c && shouldEscapeChar cfg c

strCharIsEscaped :: Config -> Char -> Bool
strCharIsEscaped cfg = \case
  '"' -> True
  c   -> showLitEscapesChar c && shouldEscapeChar cfg c

-- Between a numeric escape and a digit, or between \SO and H, we need a \& to
-- terminate the escape; detect whether we're in one of those cases.
needsEmptyEscape :: Config -> Char -> Char -> Bool
needsEmptyEscape cfg c0 c1 =
  strCharIsEscaped cfg c0 &&
  case showLitChar c0 "" of
    "\\SO" -> c1 == 'H'
    ('\\' : c : _) -> isDigit c && isDigit c1
    _ -> False

escapeChar :: Config -> Char -> Text
escapeChar cfg c
  | shouldEscapeChar cfg c = T.pack (showLitChar c "")
  | otherwise              = T.singleton c

escapeLitChar :: Config -> Char -> Text
escapeLitChar cfg = \case
  '\'' -> "\\'"
  c -> escapeChar cfg c

escapeStrChar :: Config -> Char -> Text
escapeStrChar cfg = \case
  '"' -> "\\\""
  c -> escapeChar cfg c

ppStrLit :: Config -> Text -> Doc SyntaxClass
ppStrLit cfg unescaped =
  P.annotate (Literal StrLit) $
  P.group $ -- Prefer putting the whole thing on this line whenever possible.
  P.enclose "\"" "\"" $
  -- Then prefer breaking on newlines when the next line doesn't fit.
  foldl01
    (\x l ->
      x <> P.group (P.flatAlt (nl <> backslashBreak <> l) (nl <> l)))
    id
    mempty
    (ppLine <$> escapedLinesOfWords)
 where
  nl = P.annotate EscapeSequence "\\n"

  ppWord :: Text -> Doc SyntaxClass
  ppWord "" = mempty
  ppWord w =
    let (toEscape, rest) = T.span (strCharIsEscaped cfg) w
        (plain, rest') = T.break (strCharIsEscaped cfg) rest
        sep =
          -- Do we need to insert a \& to separate a numeric escape from a
          -- following digit?
          if not (T.null toEscape) &&
               not (T.null plain) &&
               needsEmptyEscape cfg (T.last toEscape) (T.head plain)
             then "\\&"
             else mempty
        escaped = pretty (T.concatMap (escapeStrChar cfg) toEscape) <> sep
    in  P.annotate EscapeSequence escaped <> text plain <> ppWord rest'

  escapedLinesOfWords :: [[(Doc SyntaxClass, Doc SyntaxClass)]]
  escapedLinesOfWords =
    map
        (\ (w, ws) -> (ppWord w, ppWhitespace ws)) .
      wordsSep <$>
    linesSep unescaped

  ppWhitespace :: Text -> Doc SyntaxClass
  ppWhitespace =
    P.annotate EscapeSequence .
    text . T.concatMap (escapeStrChar cfg)

  ppLine :: [(Doc SyntaxClass, Doc SyntaxClass)] -> Doc SyntaxClass
  ppLine ws =
    -- Finally, break at word boundaries if the next word doesn't fit.
    P.group $ uncurry (<>) $ foldl01
      (\(line, space) (w, space') ->
        ( line <> P.group (P.flatAlt (space <> backslashBreak) space <> w)
        , space'
        ))
      id
      mempty
      ws

  backslashBreak = P.annotate EscapeSequence $ "\\" <> P.line' <> "\\"

ppIntLit :: Base -> [Int] -> Integer -> Doc SyntaxClass
ppIntLit b seps = P.annotate (Literal IntLit) . text . formatIntLit b seps

paddingZeros :: FloatLiteral -> Int
paddingZeros (FloatLiteral _ d e)
  | e < 0 = negate e
  | e > T.length d = e - T.length d
  | otherwise = 0

ppFloatLit :: Config -> FloatLiteral -> Doc SyntaxClass
ppFloatLit cfg lit =
  P.annotate (Literal RatLit) $ text $
  formatFloatLit
    (paddingZeros lit' > _scientificNotationThreshold cfg)
    (_separatorGroupSizes cfg Decimal) lit'
 where
  lit' =
    (if _trimTrailingFloatZeros cfg then trimFloatLit else id) $
    normalizeFloatLit lit

-- | Render one layer of 'PortrayalF' to 'DocAssocPrec'.
toDocAssocPrecF
  :: Config
  -> PortrayalF (DocAssocPrec SyntaxClass)
  -> DocAssocPrec SyntaxClass
toDocAssocPrecF cfg = \case
  NameF nm -> \_ _ -> ppPrefix nm
  LitIntBaseF base x -> \_ _ -> ppIntLit base (_separatorGroupSizes cfg base) x
  LitFloatF x -> \_ _ -> ppFloatLit cfg x

  SpecialFloatF x -> \_ _ ->
    P.annotate Structural $ text $ -- Different color from numeric float lits.
    formatSpecialFloat x

  LitStrF x -> \_ _ -> ppStrLit cfg x

  LitCharF x -> \_ _ ->
    -- Likewise Char
    P.annotate (Literal CharLit) $
    P.enclose "'" "'" $
    (if litCharIsEscaped cfg x then P.annotate EscapeSequence else id) $
    text $ escapeLitChar cfg x

  OpaqueF txt -> \_ _ -> text txt
  ApplyF fn [] -> \_ _ -> fn AssocL 10

  ApplyF fn xs -> \lr p ->
    maybeParens (not $ fixityCompatible (Infixity AssocL 10) lr p) $
      P.nest 2 $ P.sep
        [ fn AssocL 10
        , P.sep $ xs <&> \docprec -> docprec AssocR 10
        ]

  BinopF nm fx x y -> ppBinop nm fx x y
  TupleF xs -> \_ _ -> ppBulletList "(" "," ")" $ xs <&> \x -> x AssocNope (-1)
  ListF xs -> \_ _ -> ppBulletList "[" "," "]" $ xs <&> \x -> x AssocNope (-1)

  LambdaCaseF xs -> \_ p ->
    maybeParens (p >= 10) $
      P.nest 2 $ P.sep
        [ P.annotate Structural "\\" <> P.annotate Keyword "case"
        , ppBulletList "{" ";" "}"
            [ P.nest 2 $ P.sep $
                [ pat AssocNope 0 P.<+> P.annotate Structural "->"
                , val AssocNope 0
                ]
            | (pat, val) <- xs
            ]
        ]

  RecordF con sels -> \_ _ -> case sels of
    [] -> con AssocNope (-1)
    _  -> P.nest 2 $ P.sep
      [ con AssocNope 10
      , ppBulletList "{" "," "}"
          [ P.nest 2 $ P.sep
              [ ppPrefix sel P.<+> P.annotate Structural "="
              , val AssocNope 0
              ]
          | FactorPortrayal sel val <- sels
          ]
      ]

  TyAppF val ty -> \_ _ ->
    P.nest 2 $ P.sep
      [ val AssocNope 10
      , P.annotate Structural "@" <> ty AssocNope 10
      ]

  TySigF val ty -> \_ p -> maybeParens (p >= 0) $
    P.nest 2 $ P.sep
      [ val AssocNope 0
      , P.annotate Structural "::" P.<+> ty AssocNope 0
      ]

  QuotF nm content -> \_ _ ->
    P.nest 2 $ P.sep
      [ P.annotate Structural "[" <>
          P.annotate (Identifier VarIdent) (text nm) <>
          P.annotate Structural "|"
      , content AssocNope (-1)
      , P.annotate Structural "|]"
      ]

  UnlinesF ls -> \_ _ -> P.vcat (ls <&> \l -> l AssocNope (-1))
  NestF n x -> \_ _ -> P.nest n (x AssocNope (-1))

-- | Render a 'Portrayal' to a 'Doc' with support for operator associativity.
toDocAssocPrec :: Config -> Portrayal -> DocAssocPrec SyntaxClass
toDocAssocPrec cfg = cata (toDocAssocPrecF cfg) . unPortrayal

-- | Convenience function for rendering a 'Portrayal' to a 'Text'.
basicShowPortrayal :: Portrayal -> Text
basicShowPortrayal = styleShowPortrayal defaultConfig (const mempty)

-- | Convenience function for rendering a 'Portrayal' to colorized 'Text'.
prettyShowPortrayal :: Portrayal -> Text
prettyShowPortrayal =
  styleShowPortrayal prettyConfig defaultStyling

-- | A lazy 'TL.Text' variant of 'prettyShowPortrayal'.
--
-- @since 0.2.1
prettyShowPortrayalLazy :: Portrayal -> TL.Text
prettyShowPortrayalLazy =
  styleShowPortrayalLazy prettyConfig defaultStyling

-- | Convenience function for rendering a 'Portrayal' to stylized 'Text'.
styleShowPortrayal
  :: Config -> (SyntaxClass -> Maybe A.AnsiStyle) -> Portrayal -> Text
styleShowPortrayal cfg style = TL.toStrict . styleShowPortrayalLazy cfg style

-- | A lazy 'TL.Text' variant of 'styleShowPortrayal'.
--
-- @since 0.2.1
styleShowPortrayalLazy
  :: Config -> (SyntaxClass -> Maybe A.AnsiStyle) -> Portrayal -> TL.Text
styleShowPortrayalLazy cfg style p =
  A.renderLazy $ P.alterAnnotationsS style $
  P.layoutPretty P.defaultLayoutOptions $
  toDocAssocPrec cfg p AssocNope (-1)

-- | A newtype providing a 'Pretty' instance via 'Portray', for @DerivingVia@.
--
-- Sadly we can't use @Wrapped@ since it would be an orphan instance.  Oh well.
-- We'll just define a unique 'WrappedPortray' newtype in each
-- pretty-printer-integration package.
newtype WrappedPortray a = WrappedPortray { unWrappedPortray :: a }
  deriving newtype (Eq, Ord, Show)

-- | Provide an instance for 'Pretty' by way of 'Portray'.
instance Portray a => Pretty (WrappedPortray a) where
  pretty x =
    P.unAnnotate $ portrayalToDoc defaultConfig (portray $ unWrappedPortray x)