{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
module Language.Haskell.Stylish.Printer
( Printer(..)
, PrinterConfig(..)
, PrinterState(..)
, P
, runPrinter
, runPrinter_
, comma
, dot
, getAnnot
, getCurrentLine
, getCurrentLineLength
, getDocstrPrev
, newline
, parenthesize
, peekNextCommentPos
, prefix
, putComment
, putEolComment
, putOutputable
, putAllSpanComments
, putCond
, putType
, putRdrName
, putText
, removeCommentTo
, removeCommentToEnd
, removeLineComment
, sep
, groupAttachedComments
, space
, spaces
, suffix
, pad
, withColumns
, modifyCurrentLine
, wrapping
) where
import Prelude hiding (lines)
import ApiAnnotation (AnnKeywordId(..), AnnotationComment(..))
import GHC.Hs.Extension (GhcPs, NoExtField(..))
import GHC.Hs.Types (HsType(..))
import Module (ModuleName, moduleNameString)
import RdrName (RdrName(..))
import SrcLoc (GenLocated(..), RealLocated)
import SrcLoc (Located, SrcSpan(..))
import SrcLoc (srcSpanStartLine, srcSpanEndLine)
import Outputable (Outputable)
import Control.Monad (forM_, replicateM_)
import Control.Monad.Reader (MonadReader, ReaderT(..), asks, local)
import Control.Monad.State (MonadState, State)
import Control.Monad.State (runState)
import Control.Monad.State (get, gets, modify, put)
import Data.Foldable (find)
import Data.Functor ((<&>))
import Data.List (delete, isPrefixOf)
import Data.List.NonEmpty (NonEmpty(..))
import Language.Haskell.Stylish.Module (Module, Lines, lookupAnnotation)
import Language.Haskell.Stylish.GHC (showOutputable, unLocated)
type P = Printer
newtype Printer a = Printer (ReaderT PrinterConfig (State PrinterState) a)
deriving (Applicative, Functor, Monad, MonadReader PrinterConfig, MonadState PrinterState)
data PrinterConfig = PrinterConfig
{ columns :: !(Maybe Int)
}
data PrinterState = PrinterState
{ lines :: !Lines
, linePos :: !Int
, currentLine :: !String
, pendingComments :: ![RealLocated AnnotationComment]
, parsedModule :: !Module
}
runPrinter :: PrinterConfig -> [RealLocated AnnotationComment] -> Module -> Printer a -> (a, Lines)
runPrinter cfg comments m (Printer printer) =
let
(a, PrinterState parsedLines _ startedLine _ _) = runReaderT printer cfg `runState` PrinterState [] 0 "" comments m
in
(a, parsedLines <> if startedLine == [] then [] else [startedLine])
runPrinter_ :: PrinterConfig -> [RealLocated AnnotationComment] -> Module -> Printer a -> Lines
runPrinter_ cfg comments m printer = snd (runPrinter cfg comments m printer)
putText :: String -> P ()
putText txt = do
l <- gets currentLine
modify \s -> s { currentLine = l <> txt }
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
putOutputable :: Outputable a => a -> P ()
putOutputable = putText . showOutputable
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)
putComment :: AnnotationComment -> P ()
putComment = \case
AnnLineComment s -> putText s
AnnDocCommentNext s -> putText s
AnnDocCommentPrev s -> putText s
AnnDocCommentNamed s -> putText s
AnnDocSection _ s -> putText s
AnnDocOptions s -> putText s
AnnBlockComment s -> putText s
putEolComment :: SrcSpan -> P ()
putEolComment = \case
RealSrcSpan rspan -> do
cmt <- removeComment \case
L rloc (AnnLineComment s) ->
and
[ srcSpanStartLine rspan == srcSpanStartLine rloc
, not ("-- ^" `isPrefixOf` s)
, not ("-- |" `isPrefixOf` s)
]
_ -> False
forM_ cmt (\c -> space >> putComment c)
UnhelpfulSpan _ -> pure ()
putRdrName :: Located RdrName -> P ()
putRdrName (L pos n) = case n of
Unqual name -> do
annots <- getAnnot pos
if AnnOpenP `elem` annots then do
putText "("
putText (showOutputable name)
putText ")"
else if AnnBackquote `elem` annots then do
putText "`"
putText (showOutputable name)
putText "`"
else if AnnSimpleQuote `elem` annots then do
putText "'"
putText (showOutputable name)
else
putText (showOutputable name)
Qual modulePrefix name ->
putModuleName modulePrefix >> dot >> putText (showOutputable name)
Orig _ name ->
putText (showOutputable name)
Exact name ->
putText (showOutputable name)
putModuleName :: ModuleName -> P ()
putModuleName = putText . moduleNameString
putType :: Located (HsType GhcPs) -> P ()
putType ltp = case unLocated ltp of
HsFunTy NoExtField argTp funTp -> do
putOutputable argTp
space
putText "->"
space
putType funTp
HsAppTy NoExtField t1 t2 ->
putType t1 >> space >> putType t2
HsExplicitListTy NoExtField _ xs -> do
putText "'["
sep
(comma >> space)
(fmap putType xs)
putText "]"
HsExplicitTupleTy NoExtField xs -> do
putText "'("
sep
(comma >> space)
(fmap putType xs)
putText ")"
HsOpTy NoExtField lhs op rhs -> do
putType lhs
space
putRdrName op
space
putType rhs
HsTyVar NoExtField _ rdrName ->
putRdrName rdrName
HsTyLit _ tp ->
putOutputable tp
HsParTy _ tp -> do
putText "("
putType tp
putText ")"
HsTupleTy NoExtField _ xs -> do
putText "("
sep
(comma >> space)
(fmap putType xs)
putText ")"
HsForAllTy NoExtField _ _ _ ->
putOutputable ltp
HsQualTy NoExtField _ _ ->
putOutputable ltp
HsAppKindTy _ _ _ ->
putOutputable ltp
HsListTy _ _ ->
putOutputable ltp
HsSumTy _ _ ->
putOutputable ltp
HsIParamTy _ _ _ ->
putOutputable ltp
HsKindSig _ _ _ ->
putOutputable ltp
HsStarTy _ _ ->
putOutputable ltp
HsSpliceTy _ _ ->
putOutputable ltp
HsDocTy _ _ _ ->
putOutputable ltp
HsBangTy _ _ _ ->
putOutputable ltp
HsRecTy _ _ ->
putOutputable ltp
HsWildCardTy _ ->
putOutputable ltp
XHsType _ ->
putOutputable ltp
getDocstrPrev :: SrcSpan -> P (Maybe AnnotationComment)
getDocstrPrev = \case
UnhelpfulSpan _ -> pure Nothing
RealSrcSpan rspan -> do
removeComment \case
L rloc (AnnLineComment s) ->
and
[ srcSpanStartLine rspan == srcSpanStartLine rloc
, "-- ^" `isPrefixOf` s
]
_ -> False
newline :: P ()
newline = do
l <- gets currentLine
modify \s -> s { currentLine = "", linePos = 0, lines = lines s <> [l] }
space :: P ()
space = putText " "
spaces :: Int -> P ()
spaces i = replicateM_ i space
dot :: P ()
dot = putText "."
comma :: P ()
comma = putText ","
parenthesize :: P a -> P a
parenthesize action = putText "(" *> action <* putText ")"
sep :: P a -> [P a] -> P ()
sep _ [] = pure ()
sep s (first : rest) = first >> forM_ rest ((>>) s)
prefix :: P a -> P b -> P b
prefix pa pb = pa >> pb
suffix :: P a -> P b -> P a
suffix pa pb = pb >> pa
pad :: Int -> P ()
pad n = do
len <- length <$> getCurrentLine
spaces $ n - len
removeLineComment :: Int -> P (Maybe AnnotationComment)
removeLineComment line =
removeComment (\(L rloc _) -> srcSpanStartLine rloc == line)
removeCommentTo :: SrcSpan -> P [AnnotationComment]
removeCommentTo = \case
UnhelpfulSpan _ -> pure []
RealSrcSpan rspan -> removeCommentTo' (srcSpanStartLine rspan)
removeCommentToEnd :: SrcSpan -> P [AnnotationComment]
removeCommentToEnd = \case
UnhelpfulSpan _ -> pure []
RealSrcSpan rspan -> removeCommentTo' (srcSpanEndLine rspan)
removeCommentTo' :: Int -> P [AnnotationComment]
removeCommentTo' line =
removeComment (\(L rloc _) -> srcSpanStartLine rloc < line) >>= \case
Nothing -> pure []
Just c -> do
rest <- removeCommentTo' line
pure (c : rest)
removeComments :: (RealLocated AnnotationComment -> Bool) -> P [AnnotationComment]
removeComments p =
removeComment p >>= \case
Just c -> do
rest <- removeComments p
pure (c : rest)
Nothing -> pure []
removeComment :: (RealLocated AnnotationComment -> Bool) -> P (Maybe AnnotationComment)
removeComment p = do
comments <- gets pendingComments
let
foundComment =
find p comments
newPendingComments =
maybe comments (`delete` comments) foundComment
modify \s -> s { pendingComments = newPendingComments }
pure $ fmap (\(L _ c) -> c) foundComment
getAnnot :: SrcSpan -> P [AnnKeywordId]
getAnnot spn = gets (lookupAnnotation spn . parsedModule)
getCurrentLine :: P String
getCurrentLine = gets currentLine
getCurrentLineLength :: P Int
getCurrentLineLength = fmap length getCurrentLine
peekNextCommentPos :: P (Maybe SrcSpan)
peekNextCommentPos = do
gets pendingComments <&> \case
(L next _ : _) -> Just (RealSrcSpan next)
[] -> Nothing
groupAttachedComments :: [Located a] -> P [([AnnotationComment], NonEmpty (Located a))]
groupAttachedComments = go
where
go :: [Located a] -> P [([AnnotationComment], NonEmpty (Located a))]
go (L rspan x : xs) = do
comments <- removeCommentTo rspan
nextGroupStartM <- peekNextCommentPos
let
sameGroupOf = maybe xs \nextGroupStart ->
takeWhile (\(L p _)-> p < nextGroupStart) xs
restOf = maybe [] \nextGroupStart ->
dropWhile (\(L p _) -> p <= nextGroupStart) xs
restGroups <- go (restOf nextGroupStartM)
pure $ (comments, L rspan x :| sameGroupOf nextGroupStartM) : restGroups
go _ = pure []
modifyCurrentLine :: (String -> String) -> P ()
modifyCurrentLine f = do
s0 <- get
put s0 {currentLine = f $ currentLine s0}
wrapping
:: P a
-> P a
-> P a
wrapping p1 p2 = do
maxCols <- asks columns
case maxCols of
Nothing -> p1
Just c -> do
s0 <- get
x <- p1
s1 <- get
if length (currentLine s1) <= c
then pure x
else do
put s0
y <- p2
s2 <- get
if length (currentLine s1) == length (currentLine s2)
then put s1 >> pure x
else pure y
withColumns :: Maybe Int -> P a -> P a
withColumns c = local $ \pc -> pc {columns = c}