1
1
mirror of https://github.com/github/semantic.git synced 2025-01-03 04:51:57 +03:00
semantic/src/Alignment.hs

161 lines
9.8 KiB
Haskell
Raw Normal View History

2017-07-31 22:23:58 +03:00
{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}
2016-03-11 20:29:17 +03:00
module Alignment
2016-03-14 21:13:06 +03:00
( hasChanges
2016-03-11 20:29:17 +03:00
, numberedRows
2016-03-22 15:20:56 +03:00
, alignDiff
, alignBranch
, applyThese
, modifyJoin
2016-03-11 20:29:17 +03:00
) where
2017-07-28 21:37:02 +03:00
import Data.Bifunctor (bimap, first, second)
import Control.Arrow ((***))
import Control.Monad (join)
2017-09-09 13:23:57 +03:00
import Control.Monad.Free (wrap)
import Data.Align
2016-03-18 21:30:22 +03:00
import Data.Bifunctor.Join
2017-09-27 19:41:41 +03:00
import Data.Diff
2017-07-28 21:37:02 +03:00
import Data.Foldable (toList)
import Data.Function (on)
2016-09-16 17:13:28 +03:00
import Data.Functor.Both
2017-09-13 22:59:34 +03:00
import Data.Functor.Foldable (cata)
2017-07-28 21:37:02 +03:00
import Data.Functor.Identity
import Data.List (partition, sortBy)
2017-09-13 22:59:34 +03:00
import Data.Maybe (catMaybes, fromJust, listToMaybe)
2017-09-27 19:29:07 +03:00
import Data.Patch
2017-06-24 16:46:28 +03:00
import Data.Range
import Data.Record
2017-07-28 21:37:02 +03:00
import Data.Semigroup ((<>))
import Data.Source
import Data.SplitDiff
2017-09-27 19:37:37 +03:00
import Data.Term
import Data.These
2017-09-11 22:16:27 +03:00
import Info (byteRange, setByteRange)
2017-07-28 21:37:02 +03:00
import Prelude hiding (fst, snd)
-- | Assign line numbers to the lines on each side of a list of rows.
numberedRows :: [Join These a] -> [Join These (Int, a)]
numberedRows = countUp (both 1 1)
2016-05-27 23:17:44 +03:00
where countUp _ [] = []
countUp from (row : rows) = numberedLine from row : countUp (nextLineNumbers from row) rows
numberedLine from row = fromJust ((,) <$> modifyJoin (uncurry These) from `applyThese` row)
2017-07-28 21:37:02 +03:00
nextLineNumbers from row = modifyJoin (fromThese id id) (succ <$ row) <*> from
-- | Determine whether a line contains any patches.
hasChanges :: (Foldable f, Functor f) => SplitDiff f annotation -> Bool
hasChanges = or . (True <$)
2016-03-09 09:36:40 +03:00
2016-05-27 22:48:32 +03:00
-- | Align a Diff into a list of Join These SplitDiffs representing the (possibly blank) lines on either side.
alignDiff :: (HasField fields Range, Traversable f) => Both Source -> Diff f (Record fields) (Record fields) -> [Join These (SplitDiff [] (Record fields))]
2017-09-13 22:59:34 +03:00
alignDiff sources = cata $ \ diff -> case diff of
Patch patch -> alignPatch sources patch
Merge (In (ann1, ann2) syntax) -> alignSyntax (runBothWith ((Join .) . These)) wrap getRange sources (In (both ann1 ann2) syntax)
2016-04-15 00:03:53 +03:00
2016-05-27 22:48:32 +03:00
-- | Align the contents of a patch into a list of lines on the corresponding side(s) of the diff.
alignPatch :: forall fields f. (Traversable f, HasField fields Range) => Both Source -> Patch (TermF f (Record fields) [Join These (SplitDiff [] (Record fields))]) (TermF f (Record fields) [Join These (SplitDiff [] (Record fields))]) -> [Join These (SplitDiff [] (Record fields))]
2016-04-15 03:27:24 +03:00
alignPatch sources patch = case patch of
2016-05-28 00:09:22 +03:00
Delete term -> fmap (pure . SplitDelete) <$> alignSyntax' this (fst sources) term
Insert term -> fmap (pure . SplitInsert) <$> alignSyntax' that (snd sources) term
2017-07-28 21:37:02 +03:00
Replace term1 term2 -> fmap (pure . SplitReplace) <$> alignWith (fmap (these id id const . runJoin) . Join)
2016-05-28 00:09:22 +03:00
(alignSyntax' this (fst sources) term1)
(alignSyntax' that (snd sources) term2)
2017-02-16 19:58:26 +03:00
where getRange = byteRange . extract
2017-09-09 23:36:18 +03:00
alignSyntax' :: (forall a. Identity a -> Join These a) -> Source -> TermF f (Record fields) [Join These (SplitDiff [] (Record fields))] -> [Join These (Term [] (Record fields))]
alignSyntax' side source = alignSyntax side Term getRange (Identity source) . bimap Identity (fmap (fmap unSplit))
2016-04-15 03:27:24 +03:00
this = Join . This . runIdentity
that = Join . That . runIdentity
-- | The Applicative instance f is either Identity or Both. Identity is for Terms in Patches, Both is for Diffs in unchanged portions of the diff.
alignSyntax :: (Applicative f, HasField fields Range, Foldable g) => (forall a. f a -> Join These a) -> (TermF [] (Record fields) term -> term) -> (term -> Range) -> f Source -> TermF g (f (Record fields)) [Join These term] -> [Join These term]
2017-09-11 22:36:23 +03:00
alignSyntax toJoinThese toNode getRange sources (In infos syntax) =
catMaybes $ wrapInBranch <$> alignBranch getRange (join (toList syntax)) bothRanges
2016-05-27 23:24:18 +03:00
where bothRanges = modifyJoin (fromThese [] []) lineRanges
2017-06-24 18:13:22 +03:00
lineRanges = toJoinThese $ sourceLineRangesWithin . byteRange <$> infos <*> sources
wrapInBranch = applyThese $ toJoinThese (makeNode <$> infos)
2017-09-11 22:36:23 +03:00
makeNode info (range, children) = toNode (In (setByteRange info range) children)
2016-03-22 15:20:56 +03:00
2016-05-13 23:40:46 +03:00
-- | Given a function to get the range, a list of already-aligned children, and the lists of ranges spanned by a branch, return the aligned lines.
alignBranch :: (term -> Range) -> [Join These term] -> Both [Range] -> [Join These (Range, [term])]
2016-05-13 23:40:46 +03:00
-- There are no more ranges, so were done.
alignBranch _ _ (Join ([], [])) = []
2016-05-13 23:40:46 +03:00
-- There are no more children, so we can just zip the remaining ranges together.
alignBranch _ [] ranges = runBothWith (alignWith Join) (fmap (flip (,) []) <$> ranges)
-- There are both children and ranges, so we need to proceed line by line
alignBranch getRange children ranges = case intersectingChildren of
2016-05-18 21:55:31 +03:00
-- No child intersects the current ranges on either side, so advance.
[] -> (flip (,) [] <$> headRanges) : alignBranch getRange children (drop 1 <$> ranges)
2016-05-18 21:55:31 +03:00
-- At least one child intersects on at least one side.
2016-05-27 23:51:06 +03:00
_ -> case intersectionsWithHeadRanges <$> listToMaybe symmetricalChildren of
-- At least one child intersects on both sides, so align symmetrically.
2016-05-27 23:51:06 +03:00
Just (True, True) -> let (line, remaining) = lineAndRemaining intersectingChildren (Just headRanges) in
2016-06-17 18:29:27 +03:00
line $ alignBranch getRange (remaining <> nonIntersectingChildren) (drop 1 <$> ranges)
-- A symmetrical child intersects on the right, so align asymmetrically on the left.
2016-05-27 23:51:06 +03:00
Just (False, True) -> alignAsymmetrically leftRange first
-- A symmetrical child intersects on the left, so align asymmetrically on the right.
2016-05-27 23:51:06 +03:00
Just (True, False) -> alignAsymmetrically rightRange second
-- No symmetrical child intersects, so align asymmetrically, picking the left side first to match the deletion/insertion order convention in diffs.
_ -> if any (isThis . runJoin) asymmetricalChildren
2016-05-27 23:51:06 +03:00
then alignAsymmetrically leftRange first
else alignAsymmetrically rightRange second
where (intersectingChildren, nonIntersectingChildren) = partition (or . intersects getRange headRanges) children
2016-05-27 23:51:06 +03:00
(symmetricalChildren, asymmetricalChildren) = partition (isThese . runJoin) intersectingChildren
intersectionsWithHeadRanges = fromThese True True . runJoin . intersects getRange headRanges
2016-07-14 17:36:18 +03:00
Just headRanges = Join <$> bisequenceL (runJoin (listToMaybe <$> Join (runBothWith These ranges)))
(leftRange, rightRange) = splitThese headRanges
2016-05-27 23:51:06 +03:00
alignAsymmetrically range advanceBy = let (line, remaining) = lineAndRemaining asymmetricalChildren range in
2016-06-17 18:29:27 +03:00
line $ alignBranch getRange (remaining <> symmetricalChildren <> nonIntersectingChildren) (modifyJoin (advanceBy (drop 1)) ranges)
2017-07-28 21:37:02 +03:00
lineAndRemaining _ Nothing = (id, [])
2016-05-27 23:51:06 +03:00
lineAndRemaining children (Just ranges) = let (intersections, remaining) = alignChildren getRange children ranges in
2016-05-31 02:21:06 +03:00
((:) $ (,) <$> ranges `applyToBoth` (sortBy (compare `on` getRange) <$> intersections), remaining)
2016-05-26 21:49:57 +03:00
2016-05-17 19:22:21 +03:00
-- | Given a list of aligned children, produce lists of their intersecting first lines, and a list of the remaining lines/nonintersecting first lines.
2016-06-02 04:37:38 +03:00
alignChildren :: (term -> Range) -> [Join These term] -> Join These Range -> (Both [term], [Join These term])
alignChildren _ [] _ = (both [] [], [])
alignChildren getRange (first:rest) headRanges
| ~(l, r) <- splitThese first
= case intersectionsWithHeadRanges first of
2016-05-27 17:38:20 +03:00
-- It intersects on both sides, so we can just take the first line whole.
2016-06-24 00:47:01 +03:00
(True, True) -> ((<>) <$> toTerms first <*> firstRemaining, restRemaining)
2016-05-27 17:38:20 +03:00
-- It only intersects on the left, so split it up.
2017-07-28 21:37:02 +03:00
(True, False) -> ((<>) <$> toTerms (fromJust l) <*> firstRemaining, maybe id (:) r restRemaining)
2016-05-27 17:38:20 +03:00
-- It only intersects on the right, so split it up.
2017-07-28 21:37:02 +03:00
(False, True) -> ((<>) <$> toTerms (fromJust r) <*> firstRemaining, maybe id (:) l restRemaining)
2016-05-27 17:38:20 +03:00
-- It doesnt intersect at all, so skip it and move along.
(False, False) -> (firstRemaining, first:restRemaining)
| otherwise = alignChildren getRange rest headRanges
where (firstRemaining, restRemaining) = alignChildren getRange rest headRanges
toTerms line = modifyJoin (fromThese [] []) (pure <$> line)
intersectionsWithHeadRanges = fromThese False False . runJoin . intersects getRange headRanges
2016-05-17 19:22:21 +03:00
2016-04-15 16:35:41 +03:00
-- | Test ranges and terms for intersection on either or both sides.
2016-04-15 03:27:24 +03:00
intersects :: (term -> Range) -> Join These Range -> Join These term -> Join These Bool
intersects getRange ranges line = intersectsRange <$> ranges `applyToBoth` modifyJoin (fromThese (Range (-1) (-1)) (Range (-1) (-1))) (getRange <$> line)
2016-04-15 16:31:28 +03:00
-- | Split a These value up into independent These values representing the left and right sides, if any.
splitThese :: Join These a -> (Maybe (Join These a), Maybe (Join These a))
splitThese these = fromThese Nothing Nothing $ bimap (Just . Join . This) (Just . Join . That) (runJoin these)
infixl 4 `applyThese`
2016-04-15 16:29:23 +03:00
-- | Like `<*>`, but it returns its result in `Maybe` since the result is the intersection of the shapes of the inputs.
applyThese :: Join These (a -> b) -> Join These a -> Maybe (Join These b)
2016-04-15 16:29:23 +03:00
applyThese (Join fg) (Join ab) = fmap Join . uncurry maybeThese $ uncurry (***) (bimap (<*>) (<*>) (unpack fg)) (unpack ab)
where unpack = fromThese Nothing Nothing . bimap Just Just
2016-03-29 16:58:31 +03:00
infixl 4 `applyToBoth`
2016-05-27 17:22:24 +03:00
-- | Like `<*>`, but it takes a `Both` on the right to ensure that it can always return a value.
applyToBoth :: Join These (a -> b) -> Both a -> Join These b
applyToBoth (Join fg) (Join (a, b)) = Join $ these (This . ($ a)) (That . ($ b)) (\ f g -> These (f a) (g b)) fg
2016-04-15 16:30:20 +03:00
-- Map over the bifunctor inside a Join, producing another Join.
modifyJoin :: (p a a -> q b b) -> Join p a -> Join q b
modifyJoin f = Join . f . runJoin
-- | Given a pair of Maybes, produce a These containing Just their values, or Nothing if they havent any.
maybeThese :: Maybe a -> Maybe b -> Maybe (These a b)
maybeThese (Just a) (Just b) = Just (These a b)
maybeThese (Just a) _ = Just (This a)
maybeThese _ (Just b) = Just (That b)
maybeThese _ _ = Nothing