fourmolu-0.4.0.0: src/Ormolu/Printer/Comments.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Helpers for formatting of comments. This is low-level code, use
-- "Ormolu.Printer.Combinators" unless you know what you are doing.
module Ormolu.Printer.Comments
( spitPrecedingComments,
spitFollowingComments,
spitRemainingComments,
spitCommentNow,
spitCommentPending,
)
where
import Control.Monad
import qualified Data.List.NonEmpty as NE
import Data.Maybe (listToMaybe)
import qualified Data.Text as T
import GHC.Types.SrcLoc
import Ormolu.Parser.CommentStream
import Ormolu.Printer.Internal
----------------------------------------------------------------------------
-- Top-level
-- | Output all preceding comments for an element at given location.
spitPrecedingComments ::
-- | Span of the element to attach comments to
RealSrcSpan ->
R ()
spitPrecedingComments ref = do
gotSome <- handleCommentSeries (spitPrecedingComment ref)
when gotSome $ do
lastMark <- getSpanMark
-- Insert a blank line between the preceding comments and the thing
-- after them if there was a blank line in the input.
when (needsNewlineBefore ref lastMark) newline
-- | Output all comments following an element at given location.
spitFollowingComments ::
-- | Span of the element to attach comments to
RealSrcSpan ->
R ()
spitFollowingComments ref = do
trimSpanStream ref
void $ handleCommentSeries (spitFollowingComment ref)
-- | Output all remaining comments in the comment stream.
spitRemainingComments :: R ()
spitRemainingComments = do
-- Make sure we have a blank a line between the last definition and the
-- trailing comments.
newline
void $ handleCommentSeries spitRemainingComment
----------------------------------------------------------------------------
-- Single-comment functions
-- | Output a single preceding comment for an element at given location.
spitPrecedingComment ::
-- | Span of the element to attach comments to
RealSrcSpan ->
-- | Are we done?
R Bool
spitPrecedingComment ref = do
mlastMark <- getSpanMark
let p (L l _) = realSrcSpanEnd l <= realSrcSpanStart ref
withPoppedComment p $ \l comment -> do
lineSpans <- thisLineSpans
let thisCommentLine = srcLocLine (realSrcSpanStart l)
needsNewline =
case listToMaybe lineSpans of
Nothing -> False
Just spn -> srcLocLine (realSrcSpanEnd spn) /= thisCommentLine
when (needsNewline || needsNewlineBefore l mlastMark) newline
spitCommentNow l comment
if theSameLinePre l ref
then space
else newline
-- | Output a comment that follows element at given location immediately on
-- the same line, if there is any.
spitFollowingComment ::
-- | AST element to attach comments to
RealSrcSpan ->
-- | Are we done?
R Bool
spitFollowingComment ref = do
mlastMark <- getSpanMark
mnSpn <- nextEltSpan
-- Get first enclosing span that is not equal to reference span, i.e. it's
-- truly something enclosing the AST element.
meSpn <- getEnclosingSpan (/= ref)
withPoppedComment (commentFollowsElt ref mnSpn meSpn mlastMark) $ \l comment ->
if theSameLinePost l ref
then
if isMultilineComment comment
then space >> spitCommentNow l comment
else spitCommentPending OnTheSameLine l comment
else do
when (needsNewlineBefore l mlastMark) $
registerPendingCommentLine OnNextLine ""
spitCommentPending OnNextLine l comment
-- | Output a single remaining comment from the comment stream.
spitRemainingComment ::
-- | Are we done?
R Bool
spitRemainingComment = do
mlastMark <- getSpanMark
withPoppedComment (const True) $ \l comment -> do
when (needsNewlineBefore l mlastMark) newline
spitCommentNow l comment
newline
----------------------------------------------------------------------------
-- Helpers
-- | Output series of comments.
handleCommentSeries ::
-- | Given location of previous comment, output the next comment
-- returning 'True' if we're done
R Bool ->
-- | Whether we printed any comments
R Bool
handleCommentSeries f = go False
where
go gotSome = do
done <- f
if done
then return gotSome
else go True
-- | Try to pop a comment using given predicate and if there is a comment
-- matching the predicate, print it out.
withPoppedComment ::
-- | Comment predicate
(RealLocated Comment -> Bool) ->
-- | Printing function
(RealSrcSpan -> Comment -> R ()) ->
-- | Are we done?
R Bool
withPoppedComment p f = do
r <- popComment p
case r of
Nothing -> return True
Just (L l comment) -> False <$ f l comment
-- | Determine if we need to insert a newline between current comment and
-- last printed comment.
needsNewlineBefore ::
-- | Current comment span
RealSrcSpan ->
-- | Last printed comment span
Maybe SpanMark ->
Bool
needsNewlineBefore _ (Just (HaddockSpan _ _)) = True
needsNewlineBefore l mlastMark =
case spanMarkSpan <$> mlastMark of
Nothing -> False
Just lastMark ->
srcSpanStartLine l > srcSpanEndLine lastMark + 1
-- | Is the preceding comment and AST element are on the same line?
theSameLinePre ::
-- | Current comment span
RealSrcSpan ->
-- | AST element location
RealSrcSpan ->
Bool
theSameLinePre l ref =
srcSpanEndLine l == srcSpanStartLine ref
-- | Is the following comment and AST element are on the same line?
theSameLinePost ::
-- | Current comment span
RealSrcSpan ->
-- | AST element location
RealSrcSpan ->
Bool
theSameLinePost l ref =
srcSpanStartLine l == srcSpanEndLine ref
-- | Determine if given comment follows AST element.
commentFollowsElt ::
-- | Location of AST element
RealSrcSpan ->
-- | Location of next AST element
Maybe RealSrcSpan ->
-- | Location of enclosing AST element
Maybe RealSrcSpan ->
-- | Location of last comment in the series
Maybe SpanMark ->
-- | Comment to test
RealLocated Comment ->
Bool
commentFollowsElt ref mnSpn meSpn mlastMark (L l comment) =
-- A comment follows a AST element if all 4 conditions are satisfied:
goesAfter
&& logicallyFollows
&& noEltBetween
&& (continuation || lastInEnclosing || supersedesParentElt)
where
-- 1) The comment starts after end of the AST element:
goesAfter =
realSrcSpanStart l >= realSrcSpanEnd ref
-- 2) The comment logically belongs to the element, four cases:
logicallyFollows =
theSameLinePost l ref -- a) it's on the same line
|| continuation -- b) it's a continuation of a comment block
|| lastInEnclosing -- c) it's the last element in the enclosing construct
-- 3) There is no other AST element between this element and the comment:
noEltBetween =
case mnSpn of
Nothing -> True
Just nspn ->
realSrcSpanStart nspn >= realSrcSpanEnd l
-- 4) Less obvious: if column of comment is closer to the start of
-- enclosing element, it probably related to that parent element, not to
-- the current child element. This rule is important because otherwise
-- all comments would end up assigned to closest inner elements, and
-- parent elements won't have a chance to get any comments assigned to
-- them. This is not OK because comments will get indented according to
-- the AST elements they are attached to.
--
-- Skip this rule if the comment is a continuation of a comment block.
supersedesParentElt =
case meSpn of
Nothing -> True
Just espn ->
let startColumn = srcLocCol . realSrcSpanStart
in startColumn espn > startColumn ref
|| ( abs (startColumn espn - startColumn l)
>= abs (startColumn ref - startColumn l)
)
continuation =
-- A comment is a continuation when it doesn't have non-whitespace
-- lexemes in front of it and goes right after the previous comment.
not (hasAtomsBefore comment)
&& ( case mlastMark of
Just (HaddockSpan _ _) -> False
Just (CommentSpan spn) ->
srcSpanEndLine spn + 1 == srcSpanStartLine l
_ -> False
)
lastInEnclosing =
case meSpn of
-- When there is no enclosing element, return false
Nothing -> False
-- When there is an enclosing element,
Just espn ->
let -- Make sure that the comment is inside the enclosing element
insideParent = realSrcSpanEnd l <= realSrcSpanEnd espn
-- And check if the next element is outside of the parent
nextOutsideParent = case mnSpn of
Nothing -> True
Just nspn -> realSrcSpanEnd espn < realSrcSpanStart nspn
in insideParent && nextOutsideParent
-- | Output a 'Comment' immediately. This is a low-level printing function.
spitCommentNow :: RealSrcSpan -> Comment -> R ()
spitCommentNow spn comment = do
sitcc
. sequence_
. NE.intersperse newline
. fmap (txt . T.pack)
. unComment
$ comment
setSpanMark (CommentSpan spn)
-- | Output a 'Comment' at the end of correct line or after it depending on
-- 'CommentPosition'. Used for comments that may potentially follow on the
-- same line as something we just rendered, but not immediately after it.
spitCommentPending :: CommentPosition -> RealSrcSpan -> Comment -> R ()
spitCommentPending position spn comment = do
let wrapper = case position of
OnTheSameLine -> sitcc
OnNextLine -> id
wrapper
. sequence_
. NE.toList
. fmap (registerPendingCommentLine position . T.pack)
. unComment
$ comment
setSpanMark (CommentSpan spn)