packages feed

Diff-2.0.0: src/Data/Algorithm/DiffContext.hs

{-@ LIQUID "--ple" @-}
{-@ LIQUID "--ple-with-undecided-guards" @-}
-- Import of the 'Data.Algorithm.Diff.Refinement' module is required for the
-- LiquidHaskell specifications in this module, but is unused in the actual code.
-- The following GHC option suppresses the unused import warning.
{-# OPTIONS_GHC -Wno-unused-imports #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Data.Algorithm.DiffContext
-- Copyright   :  (c) David Fox (2015)
-- License     :  BSD 3 Clause
-- Maintainer  :  s.clover@gmail.com
-- Stability   :  experimental
-- Portability :  portable
-- Author      :  David Fox (ddssff at the email service from google)
--
-- Generates a grouped diff with merged runs, and outputs them in the manner of @diff -u@.
-----------------------------------------------------------------------------
module Data.Algorithm.DiffContext
    ( ContextDiff, Hunk
    , getContextDiff
    , prettyContextDiff
    , prettyContextDiffOld
    , getContextDiffNumbered
    , Numbered(Numbered), numbered, unnumber
    , unNumberContextDiff
    ) where

import Data.Algorithm.Diff (PolyDiff(..), Diff, getGroupedDiff)
import Data.Algorithm.Diff.Refinement (noStuttering, noFFSS, headIsFirst, headIsSecond)
import Data.Bifunctor
import Text.PrettyPrint (Doc, text, empty, hcat)

{-@ type ContextDiff c = [Hunk c] @-}
-- | A diff consisting of disjoint 'Hunk's.
type ContextDiff c = [Hunk c]

{-@ type Hunk c = { h : [ValidListDiff c c] | noStuttering h} @-}
-- | A 'Hunk' is a list of adjacent 'Diff's.
--
-- No two consecutive elements in a 'Hunk' are both applications
-- of 'First', 'Second', or 'Both', i.e. the list does not stutter
-- on 'Diff' constructors.
type Hunk c = [Diff [c]]

-- | Split a 'Diff' list at consecutive 'Both'-'Both' boundaries.
{-@ splitBothBoth :: {ds : [ValidListDiff c c] | noFFSS ds} -> [Hunk c] @-}
splitBothBoth :: [Diff [c]] -> [Hunk c]
splitBothBoth = go []
  where
    {-@ go
          :: g:Hunk c
          -> {xs : [ValidListDiff c c] | noFFSS xs && not (headAlike g xs) }
          -> [Hunk c] / [len xs]
      @-}
    go :: Hunk c -> [Diff [c]] -> [Hunk c]
    go g (x@Both{} : y@Both{} : xs) = reverse (x:g) : go [] (y:xs)
      where
        lemma = lemmaReverseStuttering (x:g)
    go g (x : xs) = go (x:g) xs
    go g [] = [reverse g]
      where
        lemma = lemmaReverseStuttering g

{-@ type ContextSize = Nat @-}
type ContextSize = Int

{-@ opaque-reflect reverse @-}

{-@ assume lemmaReverseStuttering
      :: xs:_ -> { noStuttering (reverse xs) = noStuttering xs } @-}
lemmaReverseStuttering :: Hunk c -> ()
lemmaReverseStuttering _ = ()

{-@ reflect headAlike  @-}
headAlike :: Hunk c -> Hunk c -> Bool
headAlike (Both{} : _) (Both{} : _) = True
headAlike (First{} : _) (First{} : _) = True
headAlike (Second{} : _) (Second{} : _) = True
headAlike _ _ = False

data Numbered a = Numbered Int a deriving Show
instance Eq a => Eq (Numbered a) where
  Numbered _ a == Numbered _ b = a == b
instance Ord a => Ord (Numbered a) where
  compare (Numbered _ a) (Numbered _ b) = compare a b

numbered :: [a] -> [Numbered a]
numbered xs = fmap (uncurry Numbered) (zip [1..] xs)

unnumber :: Numbered a -> a
unnumber (Numbered _ a) = a

-- |
-- > > let textA = ["a","b","c","d","e","f","g","h","i","j","k"]
-- > > let textB = ["a","b","d","e","f","g","h","i","j"]
-- > > let diff = getContextDiff (Just 2) textA textB
-- > > prettyContextDiff (text "file1") (text "file2") (text . unnumber) diff
-- > --- file1
-- > +++ file2
-- > @@ -1,5 +1,4 @@
-- >  a
-- >  b
-- > -c
-- >  d
-- >  e
-- > @@ -9,3 +8,2 @@
-- >  i
-- >  j
-- > -k
{-@
getContextDiff ::
  Eq a
  => Maybe ContextSize
  -> [a]
  -> [a]
  -> ContextDiff (Numbered a)
@-}
getContextDiff ::
  Eq a
  => Maybe ContextSize -- ^ Context size. 'Nothing' means returning a whole-diff 'Hunk'.
  -> [a]
  -> [a]
  -> ContextDiff (Numbered a)
getContextDiff contextSize a b =
  getContextDiffNumbered contextSize (numbered a) (numbered b)

-- | If for some reason you need the line numbers stripped from the
-- result of 'getContextDiff' for backwards compatibility.
{-@ unNumberContextDiff :: ContextDiff (Numbered a) -> [[Diff [a]]] @-}
unNumberContextDiff :: ContextDiff (Numbered a) -> ContextDiff a
unNumberContextDiff = fmap (fmap (bimap (fmap unnumber) (fmap unnumber)))

-- | Create a diff of separate 'Hunk's, each containing a sequence
-- of differing elements surrounded by common elements for context.
--
-- The context size determines when to merge adjacent hunks:
-- two hunks are merged when the number of common elements between them does not
-- exceed twice the context size. Furthermore, if @contextSize@ is 'Nothing'
-- a single hunk with the whole diff is produced.
{-@
getContextDiffNumbered ::
  Eq a
  => Maybe ContextSize
  -> [Numbered a]
  -> [Numbered a]
  -> ContextDiff (Numbered a)
@-}
getContextDiffNumbered ::
  Eq a
  => Maybe ContextSize -- ^ Context size. 'Nothing' means returning a whole-diff 'Hunk'.
  -> [Numbered a]
  -> [Numbered a]
  -> ContextDiff (Numbered a)
getContextDiffNumbered Nothing a0 b0 = [getGroupedDiff a0 b0]
getContextDiffNumbered (Just contextSize) a0 b0 =
    -- The 'Diff' list is grouped into 'Hunks' that begin and end
    -- with matching ('Both') text, having non-matching ('First' and 'Second')
    -- text in the middle. Note that a non-trivial partition can only happen after
    -- the matching text has been reduced to become consecutive 'Both' values
    -- corresponding to a hunk's suffix and the following hunk prefix.
    splitBothBoth $ doPrefix $ getGroupedDiff a0 b0
    where
      -- | Handle the common text leading up to a diff.
      --
      -- Postcondition: The @a@ elements in @doPrefix h@ are a subset of those in @h@,
      -- in the same order. Additionaly, 'First' and 'Second' diffs
      -- are identical in both lists.
      --
      -- The difference between input and output is that some 'Both' diffs might
      -- be split into two other 'Both' diffs. This happens when their contents
      -- are too large compared with the contex size, resulting in some @a@
      -- elements being dropped.
      {-@ doPrefix ::  h : Hunk c
                   -> {v : [ValidListDiff c c] | noFFSS v
                       && (headIsFirst h <=> headIsFirst v)
                       && (headIsSecond h <=> headIsSecond v)} / [len h, 0] @-}
      doPrefix :: Hunk c -> [Diff [c]]
      doPrefix [] = []
      -- Trailing common elements are no prefix.
      -- This case corresponds to when both input lists are identical, so the
      -- resulting 'ContextDiff' is empty.
      doPrefix [Both _ _] = []
      -- Do the prefix and then make the suffix.
      doPrefix (Both xs ys : more) =
        Both (drop (length xs - contextSize) xs)
             (drop (length ys - contextSize) ys) : doSuffix more
      -- Prefix finished, do the diff then the following suffix.
      doPrefix (d : ds) = d : doSuffix ds

      -- | Handle the common text following a diff.
      --
      -- Precondition: The input does not start with a 'Both' diff. Otherwise,
      -- it behaves like @doPrefix@.
      {-@ doSuffix ::  h : Hunk c
                   -> {v : [ValidListDiff c c] | noFFSS v
                       && (headIsFirst h <=> headIsFirst v)
                       && (headIsSecond h <=> headIsSecond v)} / [len h, 1] @-}
      doSuffix :: Hunk c -> [Diff [c]]
      doSuffix [] = []
      -- A trailing suffix.
      doSuffix [Both xs ys] = [Both (take contextSize xs) (take contextSize ys)]
      -- If the common text is too short compared with the context,
      -- we preserve it and continue. As the following element cannot be a 'Both'
      -- as well, this effectively places the common text in the inner part of the diff.
      -- Otherwise, we split it into a suffix and prefix
      -- (resulting in some elements excluded from the diff in the middle).
      doSuffix (Both xs ys : more)
        | length xs <= contextSize * 2 =
            Both xs ys : doPrefix more
        | otherwise =
            Both (take contextSize xs) (take contextSize ys) :
            doPrefix (Both (drop contextSize xs) (drop contextSize ys) : more)
      -- 'First' and 'Second' elements are no suffix, preserve them and continue looking.
      doSuffix (d : ds) = d : doSuffix ds

-- | Pretty print a ContextDiff in the manner of diff -u.
prettyContextDiff ::
       Doc            -- ^ Document 1 name
    -> Doc            -- ^ Document 2 name
    -> (Numbered c -> Doc)     -- ^ Element pretty printer
    -> ContextDiff (Numbered c)
    -> Doc
prettyContextDiff _ _ _ [] = empty
prettyContextDiff old new prettyElem hunks =
    hcat . map (<> text "\n") $ (text "--- " <> old :
                                 text "+++ " <> new :
                                 concatMap prettyRun hunks)
    where
      -- Pretty print a run of adjacent changes
      prettyRun hunk =
        text ("@@ " <> formatHunk hunk <> " @@") : concatMap prettyChange hunk

      -- Pretty print a single change (e.g. one line of a text file)
      prettyChange (Both ts _) = map (\ l -> text " " <> prettyElem l) ts
      prettyChange (First ts)  = map (\ l -> text "-" <> prettyElem l) ts
      prettyChange (Second ts) = map (\ l -> text "+" <> prettyElem l) ts

      formatHunk hunk = "-" <> formatRun (firsts hunk) <> " +" <> formatRun (seconds hunk)

      formatRun :: [Int] -> String
      formatRun [] = "-0,0"
      formatRun [n] = show n
      formatRun ns@(n : _) = show n <> "," <> show (length ns)

      firsts (Both ns _ : more) = fmap (\(Numbered n _) -> n) ns <> firsts more
      firsts (First ns : more) = fmap (\(Numbered n _) -> n) ns <> firsts more
      firsts (Second _ : more) = firsts more
      firsts [] = []

      seconds (Both _ ns : more) = fmap (\(Numbered n _) -> n) ns <> seconds more
      seconds (First _ : more) = seconds more
      seconds (Second ns : more) = fmap (\(Numbered n _) -> n) ns <> seconds more
      seconds [] = []

-- | Pretty print without line numbers.
prettyContextDiffOld ::
       Doc            -- ^ Document 1 name
    -> Doc            -- ^ Document 2 name
    -> (c -> Doc)     -- ^ Element pretty printer
    -> ContextDiff c
    -> Doc
prettyContextDiffOld _ _ _ [] = empty
prettyContextDiffOld old new prettyElem hunks =
    hcat . map (<> text "\n") $ (text "--- " <> old :
                                 text "+++ " <> new :
                                 concatMap prettyRun hunks)
    where
      -- Pretty print a run of adjacent changes
      prettyRun hunk =
          text "@@" : concatMap prettyChange hunk

      -- Pretty print a single change (e.g. one line of a text file)
      prettyChange (Both ts _) = map (\ l -> text " " <> prettyElem l) ts
      prettyChange (First ts)  = map (\ l -> text "-" <> prettyElem l) ts
      prettyChange (Second ts) = map (\ l -> text "+" <> prettyElem l) ts