stylish-haskell-0.15.0.0: lib/Language/Haskell/Stylish/Printer.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
module Language.Haskell.Stylish.Printer
( Printer(..)
, PrinterConfig(..)
, PrinterState(..)
-- * Alias
, P
-- * Functions to use the printer
, runPrinter
, runPrinter_
-- ** Combinators
, comma
, dot
, getCurrentLine
, getCurrentLineLength
, newline
, parenthesize
, prefix
, putComment
, putMaybeLineComment
, putOutputable
, putCond
, putType
, putRdrName
, putText
, sep
, space
, spaces
, suffix
, pad
-- ** Advanced combinators
, withColumns
, modifyCurrentLine
, wrapping
) where
--------------------------------------------------------------------------------
import Prelude hiding (lines)
--------------------------------------------------------------------------------
import qualified GHC.Hs as GHC
import GHC.Hs.Extension (GhcPs)
import GHC.Types.Name.Reader (RdrName (..))
import GHC.Types.SrcLoc (GenLocated (..))
import qualified GHC.Types.SrcLoc as GHC
import GHC.Utils.Outputable (Outputable)
--------------------------------------------------------------------------------
import Control.Monad (forM_, replicateM_)
import Control.Monad.Reader (MonadReader, ReaderT (..),
asks, local)
import Control.Monad.State (MonadState, State, get, gets,
modify, put, runState)
--------------------------------------------------------------------------------
import Language.Haskell.Stylish.GHC (showOutputable)
import Language.Haskell.Stylish.Module (Lines)
-- | Shorthand for 'Printer' monad
type P = Printer
-- | Printer that keeps state of file
newtype Printer a = Printer (ReaderT PrinterConfig (State PrinterState) a)
deriving (Applicative, Functor, Monad, MonadReader PrinterConfig, MonadState PrinterState)
-- | Configuration for printer, currently empty
data PrinterConfig = PrinterConfig
{ columns :: !(Maybe Int)
}
-- | State of printer
data PrinterState = PrinterState
{ lines :: !Lines
, linePos :: !Int
, currentLine :: !String
}
-- | Run printer to get printed lines out of module as well as return value of monad
runPrinter :: PrinterConfig -> Printer a -> (a, Lines)
runPrinter cfg (Printer printer) =
let
(a, PrinterState parsedLines _ startedLine) = runReaderT printer cfg `runState` PrinterState [] 0 ""
in
(a, parsedLines <> if startedLine == [] then [] else [startedLine])
-- | Run printer to get printed lines only
runPrinter_ :: PrinterConfig -> Printer a -> Lines
runPrinter_ cfg printer = snd (runPrinter cfg printer)
-- | Print text
putText :: String -> P ()
putText txt = do
l <- gets currentLine
modify \s -> s { currentLine = l <> txt }
-- | Check condition post action, and use fallback if false
putCond :: (PrinterState -> Bool) -> P b -> P b -> P b
putCond p action fallback = do
prevState <- get
res <- action
currState <- get
if p currState then pure res
else put prevState >> fallback
-- | Print an 'Outputable'
putOutputable :: Outputable a => a -> P ()
putOutputable = putText . showOutputable
-- | Put all comments that has positions within 'SrcSpan' and separate by
-- passed @P ()@
{-
putAllSpanComments :: P () -> SrcSpan -> P ()
putAllSpanComments suff = \case
UnhelpfulSpan _ -> pure ()
RealSrcSpan rspan -> do
cmts <- removeComments \(L rloc _) ->
srcSpanStartLine rloc >= srcSpanStartLine rspan &&
srcSpanEndLine rloc <= srcSpanEndLine rspan
forM_ cmts (\c -> putComment c >> suff)
-}
-- | Print any comment
putComment :: GHC.EpaComment -> P ()
putComment epaComment = case GHC.ac_tok epaComment of
GHC.EpaDocComment hs -> putText $ show hs
GHC.EpaLineComment s -> putText s
GHC.EpaDocOptions s -> putText s
GHC.EpaBlockComment s -> putText s
putMaybeLineComment :: Maybe GHC.EpaComment -> P ()
putMaybeLineComment = \case
Nothing -> pure ()
Just cmt -> space >> putComment cmt
-- | Print a 'RdrName'
putRdrName :: GenLocated GHC.SrcSpanAnnN RdrName -> P ()
putRdrName rdrName = case GHC.unLoc rdrName of
Unqual name -> do
let (pre, post) = nameAnnAdornment $ GHC.anns $ GHC.getLoc rdrName
putText pre
putText (showOutputable name)
putText post
Qual modulePrefix name ->
putModuleName modulePrefix >> dot >> putText (showOutputable name)
Orig _ name ->
putText (showOutputable name)
Exact name ->
putText (showOutputable name)
nameAnnAdornment :: GHC.NameAnn -> (String, String)
nameAnnAdornment = \case
GHC.NameAnn {..} -> fromAdornment nann_adornment
GHC.NameAnnCommas {..} -> fromAdornment nann_adornment
GHC.NameAnnBars {..} -> fromAdornment nann_adornment
GHC.NameAnnOnly {..} -> fromAdornment nann_adornment
GHC.NameAnnRArrow {} -> (mempty, mempty)
GHC.NameAnnQuote {} -> ("'", mempty)
GHC.NameAnnTrailing {} -> (mempty, mempty)
where
fromAdornment GHC.NameParens = ("(", ")")
fromAdornment GHC.NameBackquotes = ("`", "`")
fromAdornment GHC.NameParensHash = ("#(", "#)")
fromAdornment GHC.NameSquare = ("[", "]")
-- | Print module name
putModuleName :: GHC.ModuleName -> P ()
putModuleName = putText . GHC.moduleNameString
-- | Print type
putType :: GHC.LHsType GhcPs -> P ()
putType ltp = case GHC.unLoc ltp of
GHC.HsFunTy _ arrowTp argTp funTp -> do
putOutputable argTp
space
case arrowTp of
GHC.HsUnrestrictedArrow {} -> putText "->"
GHC.HsLinearArrow {} -> putText "%1 ->"
GHC.HsExplicitMult {} -> putOutputable arrowTp
space
putType funTp
GHC.HsAppTy _ t1 t2 ->
putType t1 >> space >> putType t2
GHC.HsExplicitListTy _ _ xs -> do
putText "'["
sep
(comma >> space)
(fmap putType xs)
putText "]"
GHC.HsExplicitTupleTy _ xs -> do
putText "'("
sep
(comma >> space)
(fmap putType xs)
putText ")"
GHC.HsOpTy _ _ lhs op rhs -> do
putType lhs
space
putRdrName op
space
putType rhs
GHC.HsTyVar _ flag rdrName -> do
case flag of
GHC.IsPromoted -> putText "'"
GHC.NotPromoted -> pure ()
putRdrName rdrName
GHC.HsTyLit _ tp ->
putOutputable tp
GHC.HsParTy _ tp -> do
putText "("
putType tp
putText ")"
GHC.HsTupleTy _ _ xs -> do
putText "("
sep
(comma >> space)
(fmap putType xs)
putText ")"
GHC.HsForAllTy {} ->
putOutputable ltp
GHC.HsQualTy {} ->
putOutputable ltp
GHC.HsAppKindTy _ _ _ ->
putOutputable ltp
GHC.HsListTy _ _ ->
putOutputable ltp
GHC.HsSumTy _ _ ->
putOutputable ltp
GHC.HsIParamTy _ _ _ ->
putOutputable ltp
GHC.HsKindSig _ _ _ ->
putOutputable ltp
GHC.HsStarTy _ _ ->
putOutputable ltp
GHC.HsSpliceTy _ _ ->
putOutputable ltp
GHC.HsDocTy _ _ _ ->
putOutputable ltp
GHC.HsBangTy _ _ _ ->
putOutputable ltp
GHC.HsRecTy _ _ ->
putOutputable ltp
GHC.HsWildCardTy _ ->
putOutputable ltp
GHC.XHsType _ ->
putOutputable ltp
-- | Print a newline
newline :: P ()
newline = do
l <- gets currentLine
modify \s -> s { currentLine = "", linePos = 0, lines = lines s <> [l] }
-- | Print a space
space :: P ()
space = putText " "
-- | Print a number of spaces
spaces :: Int -> P ()
spaces i = replicateM_ i space
-- | Print a dot
dot :: P ()
dot = putText "."
-- | Print a comma
comma :: P ()
comma = putText ","
-- | Add parens around a printed action
parenthesize :: P a -> P a
parenthesize action = putText "(" *> action <* putText ")"
-- | Add separator between each element of the given printers
sep :: P a -> [P a] -> P ()
sep _ [] = pure ()
sep s (first : rest) = first >> forM_ rest ((>>) s)
-- | Prefix a printer with another one
prefix :: P a -> P b -> P b
prefix pa pb = pa >> pb
-- | Suffix a printer with another one
suffix :: P a -> P b -> P a
suffix pa pb = pb >> pa
-- | Indent to a given number of spaces. If the current line already exceeds
-- that number in length, nothing happens.
pad :: Int -> P ()
pad n = do
len <- length <$> getCurrentLine
spaces $ n - len
-- | Get current line
getCurrentLine :: P String
getCurrentLine = gets currentLine
-- | Get current line length
getCurrentLineLength :: P Int
getCurrentLineLength = fmap length getCurrentLine
modifyCurrentLine :: (String -> String) -> P ()
modifyCurrentLine f = do
s0 <- get
put s0 {currentLine = f $ currentLine s0}
wrapping
:: P a -- ^ First printer to run
-> P a -- ^ Printer to run if first printer violates max columns
-> P a -- ^ Result of either the first or the second printer
wrapping p1 p2 = do
maxCols <- asks columns
case maxCols of
-- No wrapping
Nothing -> p1
Just c -> do
s0 <- get
x <- p1
s1 <- get
if length (currentLine s1) <= c
-- No need to wrap
then pure x
else do
put s0
y <- p2
s2 <- get
if length (currentLine s1) == length (currentLine s2)
-- Wrapping didn't help!
then put s1 >> pure x
-- Wrapped
else pure y
withColumns :: Maybe Int -> P a -> P a
withColumns c = local $ \pc -> pc {columns = c}