module Language.Haskell.Formatter.Process.AttachComments (attachComments) where
import qualified Data.Foldable as Foldable
import qualified Data.Function as Function
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified Data.Monoid as Monoid
import qualified Data.Semigroup as Semigroup
import qualified Data.Traversable as Traversable
import qualified Language.Haskell.Formatter.CommentCore as CommentCore
import qualified Language.Haskell.Formatter.ExactCode as ExactCode
import qualified Language.Haskell.Formatter.Location as Location
import qualified Language.Haskell.Formatter.Process.Code as Code
import qualified Language.Haskell.Formatter.Process.LineTool as LineTool
import qualified Language.Haskell.Formatter.Process.Note as Note
import qualified Language.Haskell.Formatter.Result as Result
import qualified Language.Haskell.Formatter.Source as Source
import qualified Language.Haskell.Formatter.Style as Style
newtype Assignment = Assignment (Map.Map Location.SrcSpan Note.CommentNote)
deriving (Eq, Ord, Show)
data CodeGap = InfiniteLower Location.SrcSpan
| FiniteGap Location.SrcSpan Location.SrcSpan
| InfiniteUpper Location.SrcSpan
deriving (Eq, Ord, Show)
instance Semigroup.Semigroup Assignment where
(Assignment left) <> (Assignment right) = Assignment merged
where merged = Map.unionWith Monoid.mappend left right
instance Monoid.Monoid Assignment where
mempty = Assignment Map.empty
attachComments ::
Style.Style ->
ExactCode.ExactCode -> Result.Result Code.CommentableCode
attachComments _ exact
= do assignment <- assignForCode exact
let (Assignment unassigned, commentable) = spread assignment locatable
if Map.null unassigned then return commentable else
Result.fatalAssertionError message
where locatable = ExactCode.actualCode exact
message = "Attaching the comments failed with an unassigned rest."
assignForCode :: ExactCode.ExactCode -> Result.Result Assignment
assignForCode exact
= case unassigned of
[] -> return $ Monoid.mconcat assignments
(_ : _) -> Result.fatalAssertionError message
where ((_, unassigned), assignments)
= Traversable.mapAccumL move base maybeOrderedPortions
move (maybeLower, rest) maybeUpper = ((maybeUpper, rest'), assignment)
where (rest', assignment)
= case maybeUpper of
Nothing -> case maybeLower of
Nothing -> (rest, Monoid.mempty)
Just lower -> ([],
assign
(InfiniteUpper lower)
rest)
Just upper -> (greaterUpper, assign gap lessEqualUpper)
where (lessEqualUpper, greaterUpper)
= span ((<= upper) . Location.getPortion) rest
gap
= case maybeLower of
Nothing -> InfiniteLower upper
Just lower -> FiniteGap lower upper
assign gap = assignComments gap . createComments startLine endLine
where (startLine, endLine)
= case gap of
InfiniteLower upper -> (pred Location.base,
Location.getStartLine upper)
FiniteGap lower upper -> (Location.getEndLine lower,
Location.getStartLine upper)
InfiniteUpper lower -> (Location.getEndLine lower,
codeEndLine)
codeEndLine = Location.getEndLine $ Location.getPortion exact
base = (Nothing, orderedComments)
(orderedPortions, orderedComments) = orderByStartEnd exact
maybeOrderedPortions
= Monoid.mappend (fmap Just orderedPortions) [Nothing]
message = "Assigning the comments failed with an unexpected rest."
assignComments :: CodeGap -> [Note.CommentBox] -> Assignment
assignComments (InfiniteLower upper) comments = assignBefore upper comments
assignComments (FiniteGap lower upper) comments
= Monoid.mappend assignedAfter assignedBefore
where assignedAfter = assignAfter lower after
(after, spaces, before) = divideComments comments
assignedBefore = assignBefore upper $ Monoid.mappend spaces before
assignComments (InfiniteUpper lower) comments = assignAfter lower comments
assignBefore :: Location.SrcSpan -> [Note.CommentBox] -> Assignment
assignBefore portion = flip (assignSingleton portion) []
assignSingleton ::
Location.SrcSpan ->
[Note.CommentBox] -> [Note.CommentBox] -> Assignment
assignSingleton portion before after = Assignment $ Map.singleton portion note
where note = Note.createCommentNote before after
assignAfter :: Location.SrcSpan -> [Note.CommentBox] -> Assignment
assignAfter portion = assignSingleton portion []
divideComments ::
[Note.CommentBox] ->
([Note.CommentBox], [Note.CommentBox], [Note.CommentBox])
divideComments = divide [] []
where divide after spaces [] = (after, spaces, [])
divide after spaces rest@(box@(Note.ActualComment comment) : unwrapped)
= case (after, spaces) of
(_ : _, []) -> ifAfter
_ -> case displacement of
CommentCore.BeforeActualCode -> ifBefore
CommentCore.AfterActualCode -> ifAfter
CommentCore.None -> ifBefore
where ifAfter = divide (concat [after, spaces, [box]]) [] unwrapped
displacement = CommentCore.documentationDisplacement core
core = Note.commentCore comment
ifBefore = (after, spaces, rest)
divide after spaces (Note.EmptyLine : unwrapped)
= divide after (Monoid.mappend spaces [Note.EmptyLine]) unwrapped
createComments ::
Location.Line ->
Location.Line -> [Source.Comment] -> [Note.CommentBox]
createComments gapStartLine gapEndLine comments
= Monoid.mappend (concat untilLast) lastBoxes
where (lastEndLine, untilLast)
= Traversable.mapAccumL create gapStartLine comments
create endLine comment = (endLine', boxes)
where endLine' = Location.getEndLine portion
portion = Location.getPortion comment
boxes = Monoid.mappend emptyLines actualComments
emptyLines = createEmptyLines endLine startLine
startLine = Location.getStartLine portion
actualComments = [Note.ActualComment indentedComment]
indentedComment = Note.createIndentedComment core Location.base
core = Source.commentCore comment
lastBoxes = createEmptyLines lastEndLine gapEndLine
createEmptyLines :: Location.Line -> Location.Line -> [Note.CommentBox]
createEmptyLines endLine startLine = replicate emptyLineCount Note.EmptyLine
where emptyLineCount = LineTool.countEmptyLines endLine startLine
orderByStartEnd :: ExactCode.ExactCode -> ([Location.SrcSpan], [Source.Comment])
orderByStartEnd exact = (orderedPortions, orderedComments)
where orderedPortions = fmap Location.getPortion nestedPortions
nestedPortions = List.sort $ Foldable.toList actualCode
actualCode = ExactCode.actualCode exact
orderedComments
= List.sortBy (Function.on compare Location.getPortion) comments
comments = ExactCode.comments exact
spread :: Assignment -> Code.LocatableCode -> (Assignment, Code.CommentableCode)
spread (Assignment assignment) locatable = (Assignment unassigned, commentable)
where (unassigned, commentable)
= Traversable.mapAccumL move assignment locatable
move rest nestedPortion = (rest', note)
where (maybeNote, rest') = Map.updateLookupWithKey remove portion rest
remove = const . const Nothing
portion = Location.getPortion nestedPortion
note = Foldable.fold maybeNote