{-| Module : Language.Rust.Pretty.Util Description : pretty printing utilities Copyright : (c) Alec Theriault, 2017-2018 License : BSD-style Maintainer : alec.theriault@gmail.com Stability : experimental Portability : portable This module contains a variety of utility functions for pretty printing. Most of these require inspecting the internal structure of 'Doc'. Wadler's take on pretty printing is super useful because it allows us to print thinks like blocks much more nicely (see [this](http://stackoverflow.com/a/41424946/3072788)) that Hughes'. Unfortunately, unlike Hughes', Wadler does not have 'mempty' as the identity of @<+>@ - the space between the arguments of @<+>@ does not go away even if either argument is empty. The same problem shows up for @hsep@, @<#>@, @vsep@, @</>@, etc. My solution has been to redefine my versions of these functions which _do_ treat 'mempty' as a neutral element for @<+>@, @hsep@, @<#>@, @vsep@, and @</>@. -} {-# LANGUAGE OverloadedStrings #-} module Language.Rust.Pretty.Util where import Data.Monoid ( (<>) ) import qualified Data.Text.Prettyprint.Doc as PP import Data.Text.Prettyprint.Doc.Internal.Type ( Doc(..) ) import Language.Rust.Syntax.Token ( Delim(..) ) -- | Indentation level n :: Int n = 2 -- | Similar to 'maybe', but where the 'Nothing' case is an empty 'Doc' emptyElim :: Doc a -- ^ result if scrutinee is empty -> (Doc a -> Doc a) -- ^ how to process scrutinee if it is not empty -> Doc a -- ^ scrutinee 'Doc' -> Doc a emptyElim a _ Empty = a emptyElim _ f doc = f doc -- | Vertically concatenate two 'Doc's with a collapsible line between them (<##>) :: Doc a -> Doc a -> Doc a d1 <##> d2 = d1 <> PP.line' <> d2 -- | Flatten a 'Doc' flatten :: Doc a -> Doc a flatten d@Fail{} = d flatten d@Empty{} = d flatten d@Char{} = d flatten d@Text{} = d flatten d@Line{} = d flatten (FlatAlt _ d) = d flatten (Cat d1 d2) = Cat (flatten d1) (flatten d2) flatten (Nest i d) = Nest i (flatten d) flatten (Union d _) = flatten d flatten (Column f) = Column (flatten . f) flatten (WithPageWidth f) = WithPageWidth (flatten . f) flatten (Nesting f) = Nesting (flatten . f) flatten (Annotated a d) = Annotated a (flatten d) -- | Map the list of items into 'Doc's using the provided function and add comma punctuation commas :: [a] -> (a -> Doc b) -> Doc b commas xs f = PP.hsep (PP.punctuate "," (map f xs)) -- | Take a binary operation on docs and lift it to one that has (left and right) identity 'mempty' liftOp :: (Doc a -> Doc a -> Doc a) -> Doc a -> Doc a -> Doc a liftOp _ Empty d = d liftOp _ d Empty = d liftOp (#) d d' = d # d' -- | Lifted version of Wadler's @<+>@ (<+>) :: Doc a -> Doc a -> Doc a (<+>) = liftOp (PP.<+>) -- | Lifted version of Wadler's @hsep@ hsep :: Foldable f => f (Doc a) -> Doc a hsep = foldr (<+>) mempty -- | Lifted version of Wadler's @<#>@ (<#>) :: Doc a -> Doc a -> Doc a (<#>) = liftOp (\x y -> x <> PP.line <> y) -- | Lifted version of Wadler's @vsep@ vsep :: Foldable f => f (Doc a) -> Doc a vsep = foldr (<#>) mempty -- | Lifted version of Wadler's @</>@ (</>) :: Doc a -> Doc a -> Doc a (</>) = liftOp (\x y -> x <> PP.softline <> y) -- | Unless the condition holds, print the document unless :: Bool -> Doc a -> Doc a unless b = when (not b) -- | When the condition holds, print the document when :: Bool -> Doc a -> Doc a when cond d = if cond then d else mempty -- | Apply a printing function to an optional value. If the value is 'Nothing', 'perhaps' returns -- the empty 'Doc'. perhaps :: (a -> Doc b) -> Maybe a -> Doc b perhaps = maybe mempty -- | Indent the given 'Doc', but only if multi-line indent :: Int -> Doc a -> Doc a indent m doc = PP.flatAlt (PP.indent m doc) (flatten doc) -- | Undo what group does. This function is pretty dangerous... ungroup :: Doc a -> Doc a ungroup (Union _ x) = x ungroup y = y -- | Remove all indent noIndent :: Doc a -> Doc a noIndent d = PP.nesting (\i -> PP.nest (negate i) d) -- | Translate '\n' in a string using the provided 'Doc' instead of 'line' string :: Doc a -> String -> Doc a string new = foldMap (\c -> case c of { '\n' -> new; _ -> Char c }) -- | This is the most general function for printing blocks. It operates with any delimiter, any -- seperator, an optional leading attribute doc (which isn't followed by a seperator), and wraps a -- list of entries. It has been tweaked to look Just Right (TM) for the usual cases. -- -- Note that this will try to fit things on one line when possible, so if you want a block that is -- sure /not/ to be condensed on one line (e.g. for a function), you have to construct it manually. block :: Delim -- ^ outer delimiters -> Bool -- ^ prefer to be on one line (as opposed to multiline)? -> Doc a -- ^ seperator -> Doc a -- ^ attributes doc, after which no seperator will (use 'mempty' to ignore) -> [Doc a] -- ^ entries -> Doc a block delim p s as xs = group' (lDel # PP.vsep (as' ++ ys) # rDel) where group' = if p || null (as' ++ ys) then PP.group else id -- left and right delimiter lists (lDel,rDel) = case delim of Paren -> ("(", ")") Bracket -> ("[", "]") Brace -> ("{", "}") NoDelim -> (mempty, mempty) -- method of contenating delimiters with the rest of the body (#) = case delim of { Paren -> (<##>); _ -> (<#>) } -- attributes as' = case as of Empty -> [] _ -> [ PP.flatAlt (PP.indent n as) (flatten as) ] -- list of entries ys = go xs where go [] = [] go [z] = [ PP.flatAlt (PP.indent n z <> s) (flatten z) ] go (z:zs) = PP.flatAlt (PP.indent n z <> s) (flatten z <> s) : go zs