descript-lang-0.2.0.0: src/Descript/Misc/Build/Write/Print/Patch.hs
module Descript.Misc.Build.Write.Print.Patch
( Change
, CChange
, Patch (..)
, CPatch (..)
, replacePatch
, mkCPatch
, liftCPatch
, apPatch
, alignRange
, isInsertPatch
) where
import Descript.Misc.Loc
import Data.Monoid
import Core.Data.Group
import Data.List hiding ((\\))
import Data.Text (Text)
import qualified Data.Text as Text
-- | Completely replaces or patches text.
type Change = Either Text Patch
-- | Completely replaces or patches text.
type CChange = Either Text CPatch
-- | Replaces parts of text inside a bigger block.
data Patch
= Patch
{ cpatches :: [CPatch] -- ^ Continuous blocks which are replaced. Assumed to be in order.
, patchOffset :: LocDiff -- ^ The change in size after the patch is applied.
} deriving (Eq, Ord, Read, Show)
-- | Replaces a continuous block of text inside a bigger block.
data CPatch
= CPatch
{ cpatchRange :: Range
, cpatchText :: Text
} deriving (Eq, Ord, Read, Show)
-- | Assumes when 2 patches are appended, the later patch's range is
-- after the earlier patch's range.
instance Monoid Patch where
mempty
= Patch
{ cpatches = []
, patchOffset = mempty
}
-- | Assumes the later patch's range is after the earlier patch's range.
Patch xCPatches xPatchOffset `mappend` Patch yCPatches yPatchOffset
= Patch
{ cpatches = xCPatches ++ yCPatches'
, patchOffset = xPatchOffset <> yPatchOffset
}
where yCPatches' = map (offsetCPatch xPatchOffset) yCPatches
-- | Creates a patch which replaces the first text with the second.
replacePatch :: Text -> Text -> Patch
replacePatch = mkCPatch . textRange
-- | Creates a continuous 'Patch', spanning the given range with the
-- given text.
mkCPatch :: Range -> Text -> Patch
mkCPatch range = liftCPatch . CPatch range
liftCPatch :: CPatch -> Patch
liftCPatch cpatch
| isCPatchEmpty cpatch = mempty
| otherwise
= Patch
{ cpatches = [cpatch]
, patchOffset = cpatchOffset cpatch
}
isCPatchEmpty :: CPatch -> Bool
isCPatchEmpty (CPatch range text) = isSingletonRange range && Text.null text
cpatchOffset :: CPatch -> LocDiff
cpatchOffset (CPatch range text)
= textDiff (line $ start range) text \\ rangeDiff range
offsetCPatch :: LocDiff -> CPatch -> CPatch
offsetCPatch diff (CPatch range text)
= CPatch (offsetRange diff range) text
-- | Applies the patch to the text - replaces all patched ranges.
apPatch :: Patch -> Text -> Text
apPatch patch text = foldl' (flip apCPatch) text $ cpatches patch
apCPatch :: CPatch -> Text -> Text
apCPatch (CPatch range patch) orig
= start range `beforeInText` orig
<> patch
<> end range `afterInText` orig
-- | Adjusts the range in text before the patch so it contains the same
-- content (except if deleted) in text after the patch.
alignRange :: Patch -> Range -> Range
alignRange = alignRangeCs . cpatches
alignRangeCs :: [CPatch] -> Range -> Range
alignRangeCs = flip $ foldl' $ flip alignRangeC
-- | Adjusts the range in text before the patch so it contains the same
-- content (except if deleted) in text after the patch.
alignRangeC :: CPatch -> Range -> Range
alignRangeC (CPatch (Range pStart pEnd) content) (Range xStart xEnd)
| pEnd <= xStart
= Range
{ start = xStart `addDiff` coff
, end = xEnd `addDiff` coff
}
| pStart >= xEnd
= Range
{ start = xStart
, end = xEnd
}
| pStart <= xStart && pEnd < xEnd
= Range
{ start = pEnd `addDiff` coff
, end = xEnd `addDiff` coff
}
| pStart <= xStart && pEnd >= xEnd -- Patch completely replaces range
= Range -- Both could be `pStart or `offsetRange coff pEnd`, must be same
{ start = pStart
, end = pStart
}
| pStart > xStart && pEnd < xEnd
= Range
{ start = xStart
, end = xEnd `addDiff` coff
}
| pStart > xStart && pEnd >= xEnd
= Range
{ start = xStart
, end = pEnd `addDiff` coff
}
| otherwise
= error "unexpected \"impossible\" location comparison"
where coff = textDiff (line pStart) content
-- | Whether the patch only inserts text, doesn't remove any.
isInsertPatch :: Patch -> Bool
isInsertPatch = all isInsertCPatch . cpatches
-- | Whether the patch only inserts text, doesn't remove any.
isInsertCPatch :: CPatch -> Bool
isInsertCPatch (CPatch range _) = isSingletonRange range