tilia-0.1.0.0: src/Tilia/Cpp/Fragment.hs
{-# LANGUAGE LambdaCase #-}
-- | Formatting the declarations a conditional reaches rather than the whole
-- module around them.
module Tilia.Cpp.Fragment
( Body,
bodyOf,
Fragment,
fragmentsOf,
fragmentText,
reassembled,
linesHeld,
)
where
import Control.Monad (guard)
import Data.List (sortOn)
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Monoid (Any (..))
import Data.Text (Text)
import Data.Text qualified as T
import Tilia.Cpp.Directives
( GroupSpec (..),
allGroups,
blanking,
blankingFor,
droppedFor,
)
import Tilia.Doc.Combinators (declarationsStart)
import Tilia.Doc.Internal
( Doc (..),
Layout (..),
foldChildren,
printedFrom,
spineAt,
)
-- | A module's document, taken apart where its declarations begin.
data Body = Body
{ -- | The last line before the first run of declarations.
bodyHeadEnd :: !Int,
-- | Whether anything above the declarations is a choice.
bodyHeadVaries :: !Bool,
-- | The declarations and the breaks between them.
bodyItems :: [Doc],
-- | The runs of declarations the printer keeps together, in order.
bodyRuns :: [Run],
-- | The document with other declarations in place of these.
bodyWith :: Doc -> Doc
}
-- | Declarations the printer keeps together, with no empty line between
-- them.
data Run = Run
{ -- | The first element of the body that was printed from something.
runFirst :: !Int,
-- | The last one.
runLast :: !Int,
-- | The first line it was printed from.
runFrom :: !Int,
-- | The last one.
runTo :: !Int
}
-- | Find the declarations in a module's document.
bodyOf :: Doc -> Maybe Body
bodyOf doc = do
(head', _ : items) <- Just (break (== declarationsStart) (spineAt Broken doc))
guard (case items of [DGroup Flat _] -> False; _ -> True)
let with = (mconcat (head' <> [declarationsStart]) <>)
runs <- runsOf items
pure
Body
{ bodyHeadEnd = case runs of
r : _ -> runFrom r - 1
[] -> maximum (0 : fmap snd (printedFrom doc)),
bodyHeadVaries = choosing (with mempty),
bodyItems = items,
bodyRuns = runs,
bodyWith = with
}
-- | Cut the elements of a body into runs at the empty lines between them.
runsOf :: [Doc] -> Maybe [Run]
runsOf items =
ordered (mapMaybe asRun (pieces (zip [0 ..] items)))
where
pieces xs = case break ((== DBreak) . snd) xs of
(piece, []) -> [piece]
(piece, _ : rest) -> piece : pieces rest
asRun piece = case [(i, e) | (i, d) <- piece, Just e <- [extent d]] of
[] -> Nothing
located@((i, _) : _) ->
Just
Run
{ runFirst = i,
runLast = fst (last located),
runFrom = minimum (fmap (fst . snd) located),
runTo = maximum (fmap (snd . snd) located)
}
ordered runs
| and (zipWith (\a b -> runTo a < runFrom b) runs (drop 1 runs)) = Just runs
| otherwise = Nothing
-- | The conditionals that reach one fragment of a module.
data Fragment = Fragment
{ -- | The conditionals, outermost ones only.
fragmentGroups :: [GroupSpec],
-- | Whether it takes in what is above the declarations.
fragmentAbove :: !Bool,
-- | The run before it, if there is one.
fragmentBefore :: !(Maybe Run),
-- | The run after it, if there is one.
fragmentAfter :: !(Maybe Run)
}
-- | Split a module's outermost conditionals into fragments that can be
-- formatted apart.
fragmentsOf :: Body -> [GroupSpec] -> [Fragment]
fragmentsOf body forest =
[ Fragment gs (any above gs) (runAt (lo - 1)) (runAt hi)
| (gs, (lo, hi)) <-
clustered . sortOn (fst . snd) $
[(g, reach (bodyRuns body) (gsWhole g)) | g <- forest]
]
where
above = (<= bodyHeadEnd body) . fst . gsWhole
runAt i
| i < 0 = Nothing
| otherwise = listToMaybe (drop i (bodyRuns body))
-- | The runs a conditional's lines touch, or where it falls between two of
-- them.
reach :: [Run] -> (Int, Int) -> (Int, Int)
reach runs (a, b) =
case [i | (i, r) <- zip [0 ..] runs, runFrom r <= b, a <= runTo r] of
[] -> let p = length (takeWhile ((< a) . runTo) runs) in (p, p)
is -> (minimum is, maximum is + 1)
-- | Join conditionals whose fragments would overlap once the runs either
-- side of them are taken along.
clustered :: [(GroupSpec, (Int, Int))] -> [([GroupSpec], (Int, Int))]
clustered = \case
[] -> []
(g, r) : rest -> go [g] r rest
where
go gs (lo, hi) = \case
(g, (lo', hi')) : rest | lo' <= hi -> go (gs <> [g]) (lo, max hi hi') rest
rest -> (gs, (lo, hi)) : clustered rest
-- | The text a fragment is formatted from, with the lines it leaves out.
--
-- It keeps what is above the declarations, the fragment, and a run either
-- side of it, and takes the first branch of every other conditional. What
-- it leaves out above the fragment is blanked, so that every line stays
-- where it was written, and what it leaves out below is cut off. The text
-- is 'Nothing' where that leaves out nothing.
fragmentText ::
Body ->
-- | The module's outermost conditionals.
[GroupSpec] ->
-- | The module.
Text ->
Fragment ->
Maybe (Text, [(Int, Int)])
fragmentText body forest source f
| linesHeld text < linesHeld source = Just (text, outside <> concatMap (`droppedFor` 0) others)
| otherwise = Nothing
where
text =
T.unlines . take to . T.lines $
blanking (outside <> concatMap (`blankingFor` 0) others) source
lineCount = length (T.lines source)
headEnd = bodyHeadEnd body
from = maybe (headEnd + 1) runFrom (fragmentBefore f)
to = maybe lineCount runTo (fragmentAfter f)
outside =
[(headEnd + 1, from - 1) | not (fragmentAbove f), from > headEnd + 1]
<> [(to + 1, lineCount) | to < lineCount]
others =
allGroups
[ g
| g <- forest,
gsWhole g `notElem` fmap gsWhole (fragmentGroups f)
]
-- | Put what each fragment was formatted to in place of what the module's
-- body holds there, or 'Nothing' where one of them came out other than a
-- fragment of declarations.
reassembled ::
Body ->
-- | Each fragment, with what it was formatted to.
[(Fragment, Doc)] ->
Maybe Doc
reassembled body formatted = do
placed <- traverse place formatted
with <- case [b | (f, b, _) <- placed, fragmentAbove f] of
[] -> Just (bodyWith body)
[b] -> Just (bodyWith b)
_ -> Nothing
let items = foldr put (bodyItems body) (sortOn (\(i, _, _) -> i) [p | (_, _, p) <- placed])
pure (with (mconcat items))
where
put (from, to, new) items = take from items <> new <> drop to items
-- The fragment, the body of what it was formatted to, and the
-- elements to put in place of the module's, with where they go.
place (f, d) = do
b <- bodyOf d
guard (fragmentAbove f || not (bodyHeadVaries b))
let indexed = zip [0 :: Int ..] (bodyItems b)
within r x = maybe False (\(s, e) -> runFrom r <= s && e <= runTo r) (extent x)
start <- case fragmentBefore f of
Nothing -> Just (-1)
Just r -> listToMaybe (reverse [i | (i, x) <- indexed, within r x])
end <- case fragmentAfter f of
Nothing -> Just (length indexed)
Just r -> listToMaybe [i | (i, x) <- indexed, within r x]
let new = [x | (i, x) <- indexed, start < i, i < end]
low
| fragmentAbove f = 0
| otherwise = maybe (bodyHeadEnd body) runTo (fragmentBefore f)
high = maybe maxBound runFrom (fragmentAfter f)
guard (start < end)
guard (all (maybe True (\(s, e) -> low < s && e < high) . extent) new)
pure
( f,
b,
( maybe 0 ((+ 1) . runLast) (fragmentBefore f),
maybe (length (bodyItems body)) runFirst (fragmentAfter f),
new
)
)
-- | How many lines of a text hold anything, which is what formatting it
-- costs.
linesHeld :: Text -> Int
linesHeld = length . filter (not . T.null . T.strip) . T.lines
-- | The first and the last line a document was printed from.
extent :: Doc -> Maybe (Int, Int)
extent d = case printedFrom d of
[] -> Nothing
ls -> Just (minimum (fmap fst ls), maximum (fmap snd ls))
-- | Is there a choice anywhere in this document?
choosing :: Doc -> Bool
choosing = getAny . go
where
go = \case
DCppChoice{} -> Any True
d -> foldChildren go d