packages feed

portray-prettyprinter 0.1.0 → 0.2.0

raw patch · 4 files changed

+559/−113 lines, 4 filesdep +QuickCheckdep +prettyprinter-ansi-terminaldep +test-framework-quickcheck2dep ~portraydep ~prettyprinterPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, prettyprinter-ansi-terminal, test-framework-quickcheck2

Dependency ranges changed: portray, prettyprinter

API changes (from Hackage documentation)

- Data.Portray.Prettyprinter: portrayalToDocPrec :: Portrayal -> Rational -> Doc ann
- Data.Portray.Prettyprinter: portrayalToDocPrecF :: PortrayalF (DocAssocPrec ann) -> Rational -> Doc ann
+ Data.Portray.Prettyprinter: Bracket :: SyntaxClass
+ Data.Portray.Prettyprinter: CharLit :: LitKind
+ Data.Portray.Prettyprinter: EscapeSequence :: SyntaxClass
+ Data.Portray.Prettyprinter: Identifier :: IdentKind -> SyntaxClass
+ Data.Portray.Prettyprinter: IntLit :: LitKind
+ Data.Portray.Prettyprinter: Keyword :: SyntaxClass
+ Data.Portray.Prettyprinter: Literal :: LitKind -> SyntaxClass
+ Data.Portray.Prettyprinter: RatLit :: LitKind
+ Data.Portray.Prettyprinter: Separator :: SyntaxClass
+ Data.Portray.Prettyprinter: StrLit :: LitKind
+ Data.Portray.Prettyprinter: Structural :: SyntaxClass
+ Data.Portray.Prettyprinter: basicShowPortrayal :: Portrayal -> Text
+ Data.Portray.Prettyprinter: data Config
+ Data.Portray.Prettyprinter: data LitKind
+ Data.Portray.Prettyprinter: data SyntaxClass
+ Data.Portray.Prettyprinter: defaultConfig :: Config
+ Data.Portray.Prettyprinter: defaultStyling :: SyntaxClass -> Maybe AnsiStyle
+ Data.Portray.Prettyprinter: escapeNonASCII :: Char -> Bool
+ Data.Portray.Prettyprinter: escapeSpecialOnly :: Char -> Bool
+ Data.Portray.Prettyprinter: noStyling :: SyntaxClass -> Maybe AnsiStyle
+ Data.Portray.Prettyprinter: setShouldEscapeChar :: (Char -> Bool) -> Config -> Config
+ Data.Portray.Prettyprinter: styleShowPortrayal :: Config -> (SyntaxClass -> Maybe AnsiStyle) -> Portrayal -> Text
+ Data.Portray.Prettyprinter: subtleStyling :: SyntaxClass -> Maybe AnsiStyle
- Data.Portray.Prettyprinter: portrayalToDoc :: Portrayal -> Doc ann
+ Data.Portray.Prettyprinter: portrayalToDoc :: Config -> Portrayal -> Doc SyntaxClass
- Data.Portray.Prettyprinter: toDocAssocPrec :: Portrayal -> DocAssocPrec ann
+ Data.Portray.Prettyprinter: toDocAssocPrec :: Config -> Portrayal -> DocAssocPrec SyntaxClass
- Data.Portray.Prettyprinter: toDocAssocPrecF :: PortrayalF (DocAssocPrec ann) -> DocAssocPrec ann
+ Data.Portray.Prettyprinter: toDocAssocPrecF :: Config -> PortrayalF (DocAssocPrec SyntaxClass) -> DocAssocPrec SyntaxClass

Files

CHANGELOG.md view
@@ -1,3 +1,11 @@ # 0.1.0  Initial version.++# 0.2.0++* Update to portray-0.2.0.+* Add colorized pretty-printing for `pp` and `ppd`.+* Fix records/lists/tuples putting initial elements on their own lines.+* Flow string literals onto multiple lines, preferring to break after `\n`s.+* Support configurable string escaping, to allow printing Unicode in strings.
portray-prettyprinter.cabal view
@@ -4,24 +4,29 @@ -- -- see: https://github.com/sol/hpack ----- hash: c09be156b143855f336e4a1cb5847e88d29f06163780223de49dd8ec74711e3d+-- hash: 9a1c9decccd042e10d029755324f91da86d55e74574de17e95f313f00dd9a59b  name:           portray-prettyprinter-version:        0.1.0+version:        0.2.0 synopsis:       A portray backend using the prettyprinter package. description:    This provides a                 <https://hackage.haskell.org/package/prettyprinter prettyprinter> backend for                 <https://hackage.haskell.org/package/portray portray> along with derivable                 'Pretty' instances for types with 'Portray' instances.                 .-                This started out as a copy of+                This started out as a fork of                 <https://hackage.haskell.org/package/portray-pretty portray-pretty>, and is                 still very similar.  However, @prettyprinter@ gives more flexibility to                 documents at the cost of requiring more behavior to be specified explicitly.-                As such, some formats can be nicer with the @prettyprinter@ backend, but as-                of the initial release, this backend is less mature and hasn't been used in-                practice, so there are likely more formatting bugs.  Please do file bugs on-                the GitHub issue tracker if you find anything that seems obviously wrong!+                As such, some formats are nicer with the @prettyprinter@ backend,+                configurable syntax highlighting is supported, and unicode escaping is+                configurable.  However, this backend is less mature and hasn't been used as+                much in practice, so there are likely more formatting bugs than in+                @portray-pretty@.  Please do file bugs on the GitHub issue tracker if you+                find anything that seems obviously wrong!+                .+                Try using 'pp' as the print function in GHCi:+                @:set -interactive-print=Data.Portray.Prettyprinter.pp@! category:       Text homepage:       https://github.com/google/hs-portray#readme bug-reports:    https://github.com/google/hs-portray/issues@@ -48,9 +53,10 @@       src   build-depends:       base >=4.12 && <4.16-    , portray >=0.1 && <0.2+    , portray >=0.2 && <0.3     , portray-diff >=0.1 && <0.2-    , prettyprinter+    , prettyprinter >=1.7 && <1.8+    , prettyprinter-ansi-terminal >=1.1.2 && <1.2     , text >=0.1 && <1.3   default-language: Haskell2010 @@ -63,12 +69,15 @@       test   build-depends:       HUnit+    , QuickCheck     , base >=4.12 && <4.16     , portray     , portray-diff >=0.1 && <0.2     , portray-prettyprinter-    , prettyprinter+    , prettyprinter >=1.7 && <1.8+    , prettyprinter-ansi-terminal >=1.1.2 && <1.2     , test-framework     , test-framework-hunit+    , test-framework-quickcheck2     , text >=0.1 && <1.3   default-language: Haskell2010
src/Data/Portray/Prettyprinter.hs view
@@ -37,6 +37,11 @@ -- 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@@ -69,6 +74,10 @@ --   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. @@ -85,46 +94,66 @@          , showDiff, ppd            -- * DerivingVia wrapper          , WrappedPortray(..)-           -- * Rendering Functions+           -- * Rendering+           -- ** Configuration+         , Config, defaultConfig+           -- *** Escape Sequences+         , setShouldEscapeChar, escapeNonASCII, escapeSpecialOnly+           -- ** Colorization+         , SyntaxClass(..), LitKind(..)+         , defaultStyling, subtleStyling, noStyling            -- ** With Associativity          , DocAssocPrec, toDocAssocPrecF, toDocAssocPrec-           -- ** With Precedence-         , portrayalToDocPrecF, portrayalToDocPrec            -- ** Convenience Functions          , portrayalToDoc-         , prettyShowPortrayal+         , styleShowPortrayal, prettyShowPortrayal, 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.IO as T (putStrLn)+import GHC.Show (showLitChar)  import Prettyprinter (Doc, Pretty(..))+import qualified Prettyprinter.Render.Terminal as A -- for ANSI import qualified Prettyprinter as P-import qualified Prettyprinter.Render.Text as R  import Data.Portray          ( Assoc(..), Infixity(..), FactorPortrayal(..)+         , Ident(..), IdentKind(..)          , Portray, Portrayal(..), PortrayalF(..)          , cata, portray          ) 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 . showPortrayal+pp = T.putStrLn . prettyShowPortrayal . 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 = prettyShowPortrayal . portray+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 . showDiff x+ppd x = T.putStrLn . maybe "_" prettyShowPortrayal . 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 "_" prettyShowPortrayal . diff x+showDiff x = maybe "_" basicShowPortrayal . diff x  -- | A 'Doc' that varies according to associativity and precedence context. type DocAssocPrec ann = Assoc -> Rational -> Doc ann@@ -140,13 +169,115 @@   | 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.+--+-- 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.+-- @+data Config = Config+  { _shouldEscapeChar :: Char -> Bool+  }++-- | Set the predicate for whether to escape a given character; see 'Config'.+setShouldEscapeChar :: (Char -> Bool) -> Config -> Config+setShouldEscapeChar f _ = Config f++-- | A sensible default configuration to build on.+--+-- Uses 'escapeNonASCII'.+defaultConfig :: Config+defaultConfig = Config escapeNonASCII+ -- | Convert a 'Portrayal' to a 'Doc'.-portrayalToDoc :: Portrayal -> Doc ann-portrayalToDoc t = portrayalToDocPrec t (-1)+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 ann -> Doc ann-maybeParens = \case True -> P.parens; False -> id+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@@ -156,34 +287,206 @@ 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-  :: Text+  :: Ident   -> Infixity-  -> DocAssocPrec ann -> DocAssocPrec ann -> DocAssocPrec ann+  -> 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.<+> text nm+    [ x (matchCtx AssocL assoc) opPrec P.<+> ppInfix nm     , y (matchCtx AssocR assoc) opPrec     ]  ppBulletList-  :: Doc ann -- ^ Open brace,  e.g. {  [  {  (-  -> Doc ann -- ^ Separator,   e.g. ;  ,  ,  ,-  -> Doc ann -- ^ Close brace, e.g. }  ]  }  )-  -> [Doc ann]-  -> Doc ann-ppBulletList opener _         closer []         = opener <> closer-ppBulletList opener separator closer (doc:docs) =-  P.group $-    P.concatWith (\x y -> x <> P.group (P.line' <> y))-      (opener <> P.flatAlt " " "" <> doc :-        zipWith (P.<+>) (repeat separator) docs) <>-    P.line' <> closer+  :: 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 $+      foldl01+        (\x y -> x <> P.group (P.line' <> y))+        id+        mempty+        (opener <> P.flatAlt " " "" <> doc :+          zipWith (P.<+>) (repeat separator) 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' <> "\\"+ -- | Render one layer of 'PortrayalF' to 'DocAssocPrec'.-toDocAssocPrecF :: PortrayalF (DocAssocPrec ann) -> DocAssocPrec ann-toDocAssocPrecF = \case-  AtomF txt -> \_ _ -> text txt+toDocAssocPrecF+  :: Config+  -> PortrayalF (DocAssocPrec SyntaxClass)+  -> DocAssocPrec SyntaxClass+toDocAssocPrecF cfg = \case+  NameF nm -> \_ _ -> ppPrefix nm+  LitIntF x -> \_ _ -> P.annotate (Literal IntLit) $ pretty x+  LitRatF x -> \_ _ ->+    P.annotate (Literal RatLit) $ pretty (fromRational x :: Double)+  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) $@@ -197,10 +500,10 @@   LambdaCaseF xs -> \_ p ->     maybeParens (p >= 10) $       P.nest 2 $ P.sep-        [ "\\case"+        [ P.annotate Structural "\\" <> P.annotate Keyword "case"         , ppBulletList "{" ";" "}"             [ P.nest 2 $ P.sep $-                [ pat AssocNope 0 P.<+> "->"+                [ pat AssocNope 0 P.<+> P.annotate Structural "->"                 , val AssocNope 0                 ]             | (pat, val) <- xs@@ -211,48 +514,57 @@     _  -> P.nest 2 $ P.sep       [ con AssocNope 10       , ppBulletList "{" "," "}"-          [ P.nest 2 $ P.sep-              [ text sel P.<+> "="+          [ P.nest 4 $ 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, "@" <> ty AssocNope 10]+    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.<+> ty AssocNope 0]+    P.nest 2 $ P.sep+      [ val AssocNope 0+      , P.annotate Structural "::" P.<+> ty AssocNope 0+      ]   QuotF nm content -> \_ _ ->     P.nest 2 $ P.sep-      [ char '[' <> text nm <> char '|'+      [ 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)) -toDocPrec :: DocAssocPrec ann -> Rational -> Doc ann-toDocPrec dap = dap AssocNope . subtract 1---- | Render a 'PortrayalF' to a 'Doc'.-portrayalToDocPrecF-  :: PortrayalF (DocAssocPrec ann) -> Rational -> Doc ann-portrayalToDocPrecF = toDocPrec . toDocAssocPrecF- -- | Render a 'Portrayal' to a 'Doc' with support for operator associativity.-toDocAssocPrec :: Portrayal -> DocAssocPrec ann-toDocAssocPrec = cata toDocAssocPrecF . unPortrayal+toDocAssocPrec :: Config -> Portrayal -> DocAssocPrec SyntaxClass+toDocAssocPrec cfg = cata (toDocAssocPrecF cfg) . unPortrayal --- | Render a 'Portrayal' to a 'Doc' with only operator precedence.-portrayalToDocPrec :: Portrayal -> Rational -> Doc ann-portrayalToDocPrec = toDocPrec . toDocAssocPrec+-- | Convenience function for rendering a 'Portrayal' to a 'Text'.+basicShowPortrayal :: Portrayal -> Text+basicShowPortrayal = styleShowPortrayal defaultConfig (const mempty) --- | Convenience function for rendering a 'Portrayal' to a 'String'.+-- | Convenience function for rendering a 'Portrayal' to colorized 'Text'. prettyShowPortrayal :: Portrayal -> Text-prettyShowPortrayal p =-  R.renderStrict $ P.layoutPretty P.defaultLayoutOptions $-  toDocAssocPrec p AssocNope (-1)+prettyShowPortrayal =+  styleShowPortrayal+    (defaultConfig & setShouldEscapeChar escapeSpecialOnly)+    defaultStyling +-- | Convenience function for rendering a 'Portrayal' to stylized 'Text'.+styleShowPortrayal+  :: Config -> (SyntaxClass -> Maybe A.AnsiStyle) -> Portrayal -> Text+styleShowPortrayal cfg style p =+  A.renderStrict $ 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.@@ -263,4 +575,5 @@  -- | Provide an instance for 'Pretty' by way of 'Portray'. instance Portray a => Pretty (WrappedPortray a) where-  pretty x = portrayalToDocPrec (portray $ unWrappedPortray x) 0+  pretty x =+    P.unAnnotate $ portrayalToDoc defaultConfig (portray $ unWrappedPortray x)
test/Main.hs view
@@ -15,80 +15,150 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingVia #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-}  module Main where +import Data.Function ((&))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Read (readLitChar)+import GHC.Show (showLitChar)++import qualified Prettyprinter as P+import qualified Prettyprinter.Render.Text as R import Test.Framework (defaultMain, testGroup) import Test.Framework.Providers.HUnit (testCase)+import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.HUnit ((@?=))+import Test.QuickCheck ((===), Property)  import Data.Portray import Data.Portray.Prettyprinter +-- 'read' doesn't implement this feature of Haskell string literals, so strip+-- it out ourselves.+stripEscapedWhitespace :: String -> String+stripEscapedWhitespace = \case+  [] -> []+  '\\' : c : rest+    | c `elem` [' ', '\t', '\n'] ->+        stripEscapedWhitespace $ tail $ dropWhile (/= '\\') rest+  -- showLitChar doesn't have to escape '"', but we do.+  ('\\' : '"' : rest) -> '\\' : '"' : stripEscapedWhitespace rest+  ('\\' : s) | ((c, rest):_) <- readLitChar ('\\' : s) ->+        showLitChar c (stripEscapedWhitespace rest)+  c:rest -> c : stripEscapedWhitespace rest++showPortrayalCfg :: Portray a => Config -> a -> Text+showPortrayalCfg cfg x =+  R.renderStrict $ P.layoutPretty P.defaultLayoutOptions $+  toDocAssocPrec cfg (portray x) AssocNope (-1)++propTextRoundTrips :: Config -> String -> Property+propTextRoundTrips cfg t =+  t ===+    read (stripEscapedWhitespace $ T.unpack $ showPortrayalCfg cfg $ T.pack t)++propStringRoundTrips :: Config -> String -> Property+propStringRoundTrips cfg t =+  t === read (stripEscapedWhitespace $ T.unpack $ showPortrayalCfg cfg t)++propCharRoundTrips :: Config -> Char -> Property+propCharRoundTrips cfg c = c === read (T.unpack $ showPortrayalCfg cfg c)+ main :: IO () main = defaultMain   [ testGroup "Atom"-      [ testCase "()" $ prettyShowPortrayal "()" @?= "()"-      , testCase "2" $ prettyShowPortrayal "2" @?= "2"+      [ testCase "()" $ basicShowPortrayal (Tuple []) @?= "()"+      , testCase "2" $ basicShowPortrayal (LitInt 2) @?= "2"       ]    , testGroup "Apply"       [ testCase "nullary" $-          prettyShowPortrayal (Apply "Nothing" []) @?= "Nothing"+          basicShowPortrayal (Apply (Name "Nothing") []) @?= "Nothing"       , testCase "nullary 2" $-          prettyShowPortrayal (Apply "id" [Apply "Nothing" []]) @?= "id Nothing"+          basicShowPortrayal+              (Apply (Name "id") [Apply (Name "Nothing") []]) @?=+            "id Nothing"       , testCase "unary" $-          prettyShowPortrayal (Apply "Just" ["2"]) @?= "Just 2"+          basicShowPortrayal (Apply (Name "Just") [LitInt 2]) @?= "Just 2"       , testCase "parens" $-          prettyShowPortrayal (Apply "Just" [Apply "Just" ["2"]]) @?=+          basicShowPortrayal+              (Apply (Name "Just") [Apply (Name "Just") [LitInt 2]]) @?=             "Just (Just 2)"       , testCase "binary" $-          prettyShowPortrayal (Apply "These" ["2", "4"]) @?=+          basicShowPortrayal (Apply (Name "These") [LitInt 2, LitInt 4]) @?=             "These 2 4"       , testCase "nested" $-          prettyShowPortrayal (Apply (Apply "These" ["2"]) ["4"]) @?=+          basicShowPortrayal+              (Apply (Apply (Name "These") [LitInt 2]) [LitInt 4]) @?=             "These 2 4"       ]    , testGroup "Binop"       [ testCase "operator" $-          prettyShowPortrayal (Binop ":|" (infixr_ 5) "5" (List [])) @?=+          basicShowPortrayal+              (Binop ":|" (infixr_ 5) (LitInt 5) (List [])) @?=             "5 :| []"        , testCase "con" $-          prettyShowPortrayal (Binop "`InfixCon`" (infixl_ 9) "2" "True") @?=+          basicShowPortrayal+              (Binop+                (Ident ConIdent "InfixCon")+                (infixl_ 9)+                (LitInt 2)+                (Name "True")) @?=             "2 `InfixCon` True"        , testCase "nest prec" $-          prettyShowPortrayal-              (Binop "+" (infixl_ 6) (Binop "*" (infixl_ 7) "2" "4") "6") @?=+          basicShowPortrayal+              (Binop+                "+"+                (infixl_ 6)+                (Binop "*" (infixl_ 7) (LitInt 2) (LitInt 4))+                (LitInt 6)) @?=             "2 * 4 + 6"        , testCase "nest anti-prec" $-          prettyShowPortrayal-              (Binop "*" (infixl_ 7) (Binop "+" (infixl_ 6) "2" "4") "6") @?=+          basicShowPortrayal+              (Binop+                "*"+                (infixl_ 7)+                (Binop "+" (infixl_ 6) (LitInt 2) (LitInt 4))+                (LitInt 6)) @?=             "(2 + 4) * 6"        , testCase "nest assoc" $-          prettyShowPortrayal-              (Binop "+" (infixl_ 6) (Binop "+" (infixl_ 6) "2" "4") "6") @?=+          basicShowPortrayal+              (Binop+                "+"+                (infixl_ 6)+                (Binop "+" (infixl_ 6) (LitInt 2) (LitInt 4))+                (LitInt 6)) @?=             "2 + 4 + 6"        , testCase "nest anti-assoc" $-          prettyShowPortrayal-              (Binop "+" (infixl_ 6) "2" (Binop "+" (infixl_ 6) "4" "6")) @?=+          basicShowPortrayal+              (Binop+                "+"+                (infixl_ 6)+                (LitInt 2)+                (Binop "+" (infixl_ 6) (LitInt 4) (LitInt 6))) @?=             "2 + (4 + 6)"       ]    , testGroup "Tuple"       [ testCase "pair" $-          prettyShowPortrayal (Tuple ["2", "4"]) @?= "(2, 4)"+          basicShowPortrayal (Tuple [LitInt 2, LitInt 4]) @?= "(2, 4)"       , testCase "triple" $-          prettyShowPortrayal (Tuple ["2", "4", "6"]) @?= "(2, 4, 6)"+          basicShowPortrayal (Tuple [LitInt 2, LitInt 4, LitInt 6]) @?=+            "(2, 4, 6)"       , testCase "line-break" $-          prettyShowPortrayal (Tuple ["222", strAtom (replicate 80 '2')]) @?=+          basicShowPortrayal+              (Tuple [strAtom "222", strAtom (replicate 80 '2')]) @?=             "( 222\n\             \, 2222222222222222222222222222222222222222222222222222222222222\             \2222222222222222222\n\@@ -96,48 +166,59 @@       ]    , testGroup "List"-      [ testCase "empty" $ prettyShowPortrayal (List []) @?= "[]"-      , testCase "singleton" $ prettyShowPortrayal (List ["2"]) @?= "[2]"+      [ testCase "empty" $ basicShowPortrayal (List []) @?= "[]"+      , testCase "singleton" $ basicShowPortrayal (List [LitInt 2]) @?=+          "[2]"       ]    , testGroup "LambdaCase"-      [ testCase "empty" $ prettyShowPortrayal (LambdaCase []) @?= "\\case {}"+      [ testCase "empty" $ basicShowPortrayal (LambdaCase []) @?= "\\case {}"       , testCase "singleton" $-          prettyShowPortrayal (LambdaCase [("()", "2")]) @?=+          basicShowPortrayal (LambdaCase [(Tuple [], LitInt 2)]) @?=             "\\case {() -> 2}"       , testCase "two" $-          prettyShowPortrayal (LambdaCase [("True", "2"), ("False", "4")]) @?=+          basicShowPortrayal+              (LambdaCase+                [(Name "True", LitInt 2), (Name "False", LitInt 4)]) @?=             "\\case {True -> 2; False -> 4}"       , testCase "line-break" $-          prettyShowPortrayal+          basicShowPortrayal               (LambdaCase-                [ ("True", strAtom (replicate 40 '2'))-                , ("False", strAtom (replicate 40 '4'))+                [ (Name "True", strAtom (replicate 40 '2'))+                , (Name "False", strAtom (replicate 40 '4'))                 ]) @?=             "\\case\n\             \  { True -> 2222222222222222222222222222222222222222\n\             \  ; False -> 4444444444444444444444444444444444444444\n\             \  }"       , testCase "no-parens" $-          prettyShowPortrayal-              (LambdaCase [(Apply "Just" ["2"], Apply "Just" ["4"])]) @?=+          basicShowPortrayal+              (LambdaCase+                [( Apply (Name "Just") [LitInt 2]+                 , Apply (Name "Just") [LitInt 4]+                 )]) @?=             "\\case {Just 2 -> Just 4}"       ]    , testGroup "Record"-      [ testCase "empty" $ prettyShowPortrayal (Record "Nothing" []) @?=+      [ testCase "empty" $ basicShowPortrayal (Record (Name "Nothing") []) @?=           "Nothing"       , testCase "singleton" $-            prettyShowPortrayal (Record "Just" [FactorPortrayal "it" "2"]) @?=+            basicShowPortrayal+              (Record+                (Name "Just")+                [FactorPortrayal "it" (LitInt 2)]) @?=           "Just {it = 2}"       , testCase "two" $-            prettyShowPortrayal-              (Record "These"-                [FactorPortrayal "l" "2", FactorPortrayal "r" "4"]) @?=+            basicShowPortrayal+              (Record (Name "These")+                [ FactorPortrayal "l" (LitInt 2)+                , FactorPortrayal "r" (LitInt 4)+                ]) @?=           "These {l = 2, r = 4}"       , testCase "line-break" $-            prettyShowPortrayal-              (Record "These"+            basicShowPortrayal+              (Record (Name "These")                 [ FactorPortrayal "l" (portray @[Int] [0..10])                 , FactorPortrayal "r" (portray @[Int] [0..10])                 ]) @?=@@ -145,16 +226,29 @@           \  { l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n\           \  , r = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n\           \  }"+      , testCase "break-equals" $+          basicShowPortrayal+              (Record (Name "These")+                [ FactorPortrayal+                    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+                    (Name "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")+                ]) @?=+            "These\n\+            \  { aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa =\n\+            \      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\+            \  }"       ]    , testGroup "TyApp"       [ testCase "con" $-          prettyShowPortrayal (TyApp "typeRep" "Int") @?= "typeRep @Int"+          basicShowPortrayal (TyApp (Name "typeRep") (Name "Int")) @?=+            "typeRep @Int"       , testCase "parens" $-          prettyShowPortrayal (TyApp "typeRep" (Apply "Maybe" ["Int"])) @?=+          basicShowPortrayal+              (TyApp (Name "typeRep") (Apply (Name "Maybe") [Name "Int"])) @?=             "typeRep @(Maybe Int)"       , testCase "line-break" $-          prettyShowPortrayal+          basicShowPortrayal               (TyApp                 (strAtom $ replicate 50 'a')                 (strAtom $ replicate 50 'a')) @?=@@ -163,20 +257,42 @@       ]    , testGroup "TySig"-      [ testCase "con" $ prettyShowPortrayal (TySig "2" "Int") @?= "2 :: Int"+      [ testCase "con" $ basicShowPortrayal (TySig (LitInt 2) (Name "Int")) @?=+          "2 :: Int"       , testCase "no-parens" $-          prettyShowPortrayal-              (TySig (Apply "Just" ["2"]) (Apply "Maybe" ["Int"])) @?=+          basicShowPortrayal+              (TySig+                (Apply (Name "Just") [LitInt 2])+                (Apply (Name "Maybe") [Name "Int"])) @?=             "Just 2 :: Maybe Int"       , testCase "line-break" $-          prettyShowPortrayal+          basicShowPortrayal               (TySig                 (strAtom $ replicate 50 'a')                 (strAtom $ replicate 50 'a')) @?=             "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\             \  :: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"       , testCase "parens" $-          prettyShowPortrayal (Apply "Just" [TySig "2" "Int"]) @?=+          basicShowPortrayal+              (Apply (Name "Just") [TySig (LitInt 2) (Name "Int")]) @?=             "Just (2 :: Int)"       ]++  , testGroup "StrLit"+      [ testProperty "Text round-trips ascii"+          (propTextRoundTrips defaultConfig)+      , testProperty "String round-trips ascii"+          (propStringRoundTrips defaultConfig)+      , testProperty "Char round-trips ascii"+          (propCharRoundTrips defaultConfig)++      , testProperty "Text round-trips unicode"+          (propTextRoundTrips unicodeConfig)+      , testProperty "String round-trips unicode"+          (propStringRoundTrips unicodeConfig)+      , testProperty "Char round-trips unicode"+          (propCharRoundTrips unicodeConfig)+      ]   ]+ where+  unicodeConfig = defaultConfig & setShouldEscapeChar escapeSpecialOnly