ormolu-0.2.0.0: src/Ormolu/Diff/Text.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
-- | This module allows us to diff two 'Text' values.
module Ormolu.Diff.Text
( TextDiff,
diffText,
printTextDiff,
)
where
import Control.Monad
import qualified Data.Algorithm.Diff as D
import Data.Maybe (listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Ormolu.Terminal
----------------------------------------------------------------------------
-- Types
-- | Result of diffing two 'Text's.
data TextDiff = TextDiff FilePath DiffList
deriving (Eq)
instance Show TextDiff where
show (TextDiff path _) = "TextDiff " ++ show path ++ " _"
-- | List of lines tagged by 'D.Both', 'D.First', or 'D.Second'.
type DiffList = [D.Diff [Text]]
-- | Similar to 'DiffList', but with line numbers assigned.
type DiffList' = [D.Diff [(Int, Int, Text)]]
-- | Diff hunk.
data Hunk = Hunk
{ hunkFirstStartLine :: Int,
hunkFirstLength :: Int,
hunkSecondStartLine :: Int,
hunkSecondLength :: Int,
hunkDiff :: DiffList
}
----------------------------------------------------------------------------
-- API
-- | Diff two texts and produce a 'TextDiff'.
diffText ::
-- | Text before
Text ->
-- | Text after
Text ->
-- | Path to use
FilePath ->
-- | The resulting diff or 'Nothing' if the inputs are identical
Maybe TextDiff
diffText a b path =
if all isBoth xs
then Nothing
else Just (TextDiff path xs)
where
xs = D.getGroupedDiff (lines' a) (lines' b)
isBoth = \case
D.Both _ _ -> True
D.First _ -> False
D.Second _ -> False
-- T.lines ignores trailing blank lines
lines' = T.splitOn "\n"
-- | Print the given 'TextDiff' as a 'Term' action. This function tries to
-- mimic the style of @git diff@.
printTextDiff :: TextDiff -> Term ()
printTextDiff (TextDiff path xs) = do
(bold . putS) path
newline
forM_ (toHunks (assignLines xs)) $ \Hunk {..} -> do
cyan $ do
put "@@ -"
putS (show hunkFirstStartLine)
put ","
putS (show hunkFirstLength)
put " +"
putS (show hunkSecondStartLine)
put ","
putS (show hunkSecondLength)
put " @@"
newline
forM_ hunkDiff $ \case
D.Both ys _ ->
forM_ ys $ \y -> do
unless (T.null y) $
put " "
put y
newline
D.First ys ->
forM_ ys $ \y -> red $ do
put "-"
unless (T.null y) $
put " "
put y
newline
D.Second ys ->
forM_ ys $ \y -> green $ do
put "+"
unless (T.null y) $
put " "
put y
newline
----------------------------------------------------------------------------
-- Helpers
-- | Assign lines to a 'DiffList'.
assignLines :: DiffList -> DiffList'
assignLines = go 1 1 id
where
go _ _ acc [] = acc []
go !firstLine !secondLine acc (x : xs) =
case x of
D.Both a b ->
let firstInc = length a
secondInc = length b
a' =
zipWith3
(,,)
(iterate (+ 1) firstLine)
(iterate (+ 1) secondLine)
a
in go
(firstLine + firstInc)
(secondLine + secondInc)
(acc . ((D.Both a' a') :))
xs
D.First a ->
let firstInc = length a
a' =
zipWith3
(,,)
(iterate (+ 1) firstLine)
(repeat secondLine)
a
in go
(firstLine + firstInc)
secondLine
(acc . ((D.First a') :))
xs
D.Second b ->
let secondInc = length b
b' =
zipWith3
(,,)
(repeat firstLine)
(iterate (+ 1) secondLine)
b
in go
firstLine
(secondLine + secondInc)
(acc . ((D.Second b') :))
xs
-- | Form 'Hunk's from a 'DiffList''.
toHunks :: DiffList' -> [Hunk]
toHunks = go 0 False id id []
where
-- How many lines of context (that is, lines present in both texts) to
-- show per hunk.
margin = 3
go ::
Int ->
Bool ->
([Hunk] -> [Hunk]) ->
(DiffList' -> DiffList') ->
[(Int, Int, Text)] ->
DiffList' ->
[Hunk]
go !n gotChanges hunksAcc currentAcc bothHistory = \case
[] ->
if gotChanges
then
let p = reverse (take margin bothHistory)
currentAcc' = addBothAfter p currentAcc
in case formHunk (currentAcc' []) of
Nothing -> hunksAcc []
Just hunk -> hunksAcc [hunk]
else hunksAcc []
(x : xs) ->
case x of
D.Both a _ ->
let currentAcc' = addBothAfter p currentAcc
p = reverse (drop (n' - margin) bothHistory')
hunksAcc' =
case formHunk (currentAcc' []) of
Nothing -> hunksAcc
Just hunk -> hunksAcc . (hunk :)
bothHistory' = reverse a ++ bothHistory
lena = length a
n' = n + lena
in if gotChanges && n' > margin * 2
then go 0 False hunksAcc' id bothHistory' xs
else go n' gotChanges hunksAcc currentAcc bothHistory' xs
piece ->
if gotChanges
then
let p = reverse bothHistory
currentAcc' = currentAcc . addBothBefore p (piece :)
in go 0 True hunksAcc currentAcc' [] xs
else
let p = reverse (take margin bothHistory)
currentAcc' = addBothBefore p (piece :)
in go 0 True hunksAcc currentAcc' [] xs
addBothBefore [] acc = acc
addBothBefore p acc = (D.Both p p :) . acc
addBothAfter [] acc = acc
addBothAfter p acc = acc . (D.Both p p :)
-- | Form a 'Hunk'.
formHunk :: DiffList' -> Maybe Hunk
formHunk xsRaw = do
let xs = trimEmpty xsRaw
hunkFirstStartLine <- listToMaybe xs >>= firstStartLine
let hunkFirstLength = firstLength xs
hunkSecondStartLine <- listToMaybe xs >>= secondStartLine
let hunkSecondLength = secondLength xs
hunkDiff = mapDiff (fmap third) xs
return Hunk {..}
-- | Trim empty “both” lines from beginning and end of a 'DiffList''.
trimEmpty :: DiffList' -> DiffList'
trimEmpty = go True id
where
go isFirst acc = \case
[] -> acc []
[D.Both x _] ->
let x' = reverse $ dropWhile (T.null . third) (reverse x)
in go False (acc . (D.Both x' x' :)) []
(D.Both x _ : xs) ->
let x' = dropWhile (T.null . third) x
in if isFirst
then go False (acc . (D.Both x' x' :)) xs
else go False (acc . (D.Both x x :)) xs
(x : xs) ->
go False (acc . (x :)) xs
firstStartLine :: D.Diff [(Int, Int, a)] -> Maybe Int
firstStartLine = \case
D.Both ((x, _, _) : _) _ -> Just x
D.First ((x, _, _) : _) -> Just x
D.Second ((x, _, _) : _) -> Just x
_ -> Nothing
firstLength :: [D.Diff [(Int, Int, a)]] -> Int
firstLength = go 0
where
go n [] = n
go !n (x : xs) = case x of
D.Both as _ -> go (n + length as) xs
D.First as -> go (n + length as) xs
D.Second _ -> go n xs
secondStartLine :: D.Diff [(Int, Int, a)] -> Maybe Int
secondStartLine = \case
D.Both ((_, x, _) : _) _ -> Just x
D.First ((_, x, _) : _) -> Just x
D.Second ((_, x, _) : _) -> Just x
_ -> Nothing
secondLength :: [D.Diff [(Int, Int, a)]] -> Int
secondLength = go 0
where
go n [] = n
go !n (x : xs) = case x of
D.Both as _ -> go (n + length as) xs
D.First _ -> go n xs
D.Second as -> go (n + length as) xs
mapDiff :: (a -> b) -> [D.Diff a] -> [D.Diff b]
mapDiff f = fmap $ \case
D.Both a b -> D.Both (f a) (f b)
D.First a -> D.First (f a)
D.Second b -> D.Second (f b)
third :: (Int, Int, Text) -> Text
third (_, _, x) = x