diff --git a/semantic-diff.cabal b/semantic-diff.cabal
index e7b346e5b..a0bb8982c 100644
--- a/semantic-diff.cabal
+++ b/semantic-diff.cabal
@@ -25,7 +25,6 @@ library
, Diff
, Diff.Arbitrary
, Diffing
- , DiffOutput
, Info
, Interpreter
, Language
diff --git a/src/Alignment.hs b/src/Alignment.hs
index c59cc7e5a..fdb61405c 100644
--- a/src/Alignment.hs
+++ b/src/Alignment.hs
@@ -23,7 +23,6 @@ import Diff
import Info
import Patch
import Prologue hiding (fst, snd)
-import qualified Prologue
import Range
import Source hiding (break, fromList, uncons, (++))
import SplitDiff
@@ -43,11 +42,11 @@ hasChanges :: SplitDiff leaf annotation -> Bool
hasChanges = or . (True <$)
-- | Align a Diff into a list of Join These SplitDiffs representing the (possibly blank) lines on either side.
-alignDiff :: (Show leaf, Show (Record fields), HasField fields Range) => Both (Source Char) -> Diff leaf (Record fields) -> [Join These (SplitDiff leaf (Record fields))]
-alignDiff sources diff = iter (alignSyntax (runBothWith ((Join .) . These)) (free . Free) getRange sources) (alignPatch sources <$> diff)
+alignDiff :: HasField fields Range => Both (Source Char) -> Diff leaf (Record fields) -> [Join These (SplitDiff leaf (Record fields))]
+alignDiff sources diff = iter (alignSyntax (runBothWith ((Join .) . These)) wrap getRange sources) (alignPatch sources <$> diff)
-- | Align the contents of a patch into a list of lines on the corresponding side(s) of the diff.
-alignPatch :: forall fields leaf. (Show leaf, Show (Record fields), HasField fields Range) => Both (Source Char) -> Patch (Term leaf (Record fields)) -> [Join These (SplitDiff leaf (Record fields))]
+alignPatch :: forall fields leaf. HasField fields Range => Both (Source Char) -> Patch (Term leaf (Record fields)) -> [Join These (SplitDiff leaf (Record fields))]
alignPatch sources patch = case patch of
Delete term -> fmap (pure . SplitDelete) <$> alignSyntax' this (fst sources) term
Insert term -> fmap (pure . SplitInsert) <$> alignSyntax' that (snd sources) term
@@ -61,7 +60,7 @@ alignPatch sources patch = case patch of
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, Show term, HasField fields Range) => (forall a. f a -> Join These a) -> (CofreeF (Syntax leaf) (Record fields) term -> term) -> (term -> Range) -> f (Source Char) -> CofreeF (Syntax leaf) (f (Record fields)) [Join These term] -> [Join These term]
+alignSyntax :: (Applicative f, HasField fields Range) => (forall a. f a -> Join These a) -> (CofreeF (Syntax leaf) (Record fields) term -> term) -> (term -> Range) -> f (Source Char) -> CofreeF (Syntax leaf) (f (Record fields)) [Join These term] -> [Join These term]
alignSyntax toJoinThese toNode getRange sources (infos :< syntax) = case syntax of
Leaf s -> catMaybes $ wrapInBranch (const (Leaf s)) . fmap (flip (,) []) <$> sequenceL lineRanges
Comment a -> catMaybes $ wrapInBranch (const (Comment a)) . fmap (flip (,) []) <$> sequenceL lineRanges
@@ -99,10 +98,9 @@ alignSyntax toJoinThese toNode getRange sources (infos :< syntax) = case syntax
where bothRanges = modifyJoin (fromThese [] []) lineRanges
lineRanges = toJoinThese $ actualLineRanges <$> (characterRange <$> infos) <*> sources
wrapInBranch constructor = applyThese $ toJoinThese ((\ info (range, children) -> toNode (setCharacterRange info range :< constructor children)) <$> infos)
- pairWithKey (key, values) = fmap ((,) key) <$> values
-- | 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 :: Show term => (term -> Range) -> [Join These term] -> Both [Range] -> [Join These (Range, [term])]
+alignBranch :: (term -> Range) -> [Join These term] -> Both [Range] -> [Join These (Range, [term])]
-- There are no more ranges, so we’re done.
alignBranch _ _ (Join ([], [])) = []
-- There are no more children, so we can just zip the remaining ranges together.
diff --git a/src/Data/RandomWalkSimilarity.hs b/src/Data/RandomWalkSimilarity.hs
index 08bb4c2ee..6f95a90bf 100644
--- a/src/Data/RandomWalkSimilarity.hs
+++ b/src/Data/RandomWalkSimilarity.hs
@@ -1,24 +1,33 @@
+{-# LANGUAGE RankNTypes #-}
module Data.RandomWalkSimilarity where
import Control.Arrow ((&&&))
import Control.Monad.Random
import Control.Monad.State
import qualified Data.DList as DList
+import Data.Functor.Both hiding (fst, snd)
import Data.Functor.Foldable as Foldable
import Data.Hashable
import qualified Data.KdTree.Static as KdTree
import qualified Data.List as List
import qualified Data.Vector as Vector
-import Diff
import Patch
import Prologue
-import Syntax
-import Term
+import Term ()
import Test.QuickCheck hiding (Fixed)
import Test.QuickCheck.Random
--- | Given a function comparing two terms recursively, and a function to compute a Hashable label from an annotation, compute the diff of a pair of lists of terms using a random walk similarity metric, which completes in log-linear time. This implementation is based on the paper [_RWS-Diff—Flexible and Efficient Change Detection in Hierarchical Data_](https://github.com/github/semantic-diff/files/325837/RWS-Diff.Flexible.and.Efficient.Change.Detection.in.Hierarchical.Data.pdf).
-rws :: (Hashable label, Hashable leaf, Eq leaf, Eq annotation) => (Term leaf annotation -> Term leaf annotation -> Maybe (Diff leaf annotation)) -> (annotation -> label) -> [Term leaf annotation] -> [Term leaf annotation] -> [Diff leaf annotation]
+-- | Given a function comparing two terms recursively, and a function to compute a Hashable label from an unpacked term, compute the diff of a pair of lists of terms using a random walk similarity metric, which completes in log-linear time. This implementation is based on the paper [_RWS-Diff—Flexible and Efficient Change Detection in Hierarchical Data_](https://github.com/github/semantic-diff/files/325837/RWS-Diff.Flexible.and.Efficient.Change.Detection.in.Hierarchical.Data.pdf).
+rws :: (Hashable label, Eq annotation, Prologue.Foldable f, Functor f, Eq (f (Cofree f annotation))) =>
+ -- | A function which comapres a pair of terms recursively, returning 'Just' their diffed value if appropriate, or 'Nothing' if they should not be compared.
+ (Cofree f annotation -> Cofree f annotation -> Maybe (Free (CofreeF f (Both annotation)) (Patch (Cofree f annotation)))) ->
+ -- | A function to compute a label for an unpacked term.
+ (forall b. CofreeF f annotation b -> label) ->
+ -- | The old list of terms.
+ [Cofree f annotation] ->
+ -- | The new list of terms.
+ [Cofree f annotation] ->
+ [Free (CofreeF f (Both annotation)) (Patch (Cofree f annotation))]
rws compare getLabel as bs
| null as, null bs = []
| null as = insert <$> bs
@@ -44,29 +53,24 @@ rws compare getLabel as bs
deleteRemaining diffs (_, unmapped) = foldl' (flip (List.insertBy (comparing fst))) diffs ((termIndex &&& delete . term) <$> unmapped)
-- | A term which has not yet been mapped by `rws`, along with its feature vector summary & index.
-data UnmappedTerm leaf annotation = UnmappedTerm { termIndex :: {-# UNPACK #-} !Int, feature :: !(Vector.Vector Double), term :: !(Term leaf annotation) }
+data UnmappedTerm a = UnmappedTerm { termIndex :: {-# UNPACK #-} !Int, feature :: !(Vector.Vector Double), term :: !a }
deriving Eq
+
-- | A `Gram` is a fixed-size view of some portion of a tree, consisting of a `stem` of _p_ labels for parent nodes, and a `base` of _q_ labels of sibling nodes. Collectively, the bag of `Gram`s for each node of a tree (e.g. as computed by `pqGrams`) form a summary of the tree.
data Gram label = Gram { stem :: [Maybe label], base :: [Maybe label] }
deriving (Eq, Show)
-- | Compute the bag of grams with stems of length _p_ and bases of length _q_, with labels computed from annotations, which summarize the entire subtree of a term.
-pqGrams :: Int -> Int -> (annotation -> label) -> Cofree (Syntax leaf) annotation -> DList.DList (Gram (label, Maybe leaf))
-pqGrams p q getLabel = cata merge . setRootBase . setRootStem . hylo go project
- where go (annotation :< functor) = cofree (Gram [] [ Just (getLabel annotation, leafValue functor) ] :< (assignParent (Just (getLabel annotation, leafValue functor)) p <$> functor))
- leafValue (Leaf s) = Just s
- leafValue _ = Nothing
- merge (head :< tail) = DList.cons head (Prologue.fold tail)
+pqGrams :: (Prologue.Foldable f, Functor f) => Int -> Int -> (forall b. CofreeF f annotation b -> label) -> Cofree f annotation -> DList.DList (Gram label)
+pqGrams p q getLabel = uncurry DList.cons . cata merge . setRootBase . setRootStem . cata go
+ where go c = cofree (Gram [] [ Just (getLabel c) ] :< (assignParent (Just (getLabel c)) p <$> tailF c))
+ merge (head :< tail) = let tail' = toList tail in (head, DList.fromList (windowed q setBases [] (fst <$> tail')) <> foldMap snd tail')
assignParent parentLabel n tree
- | n > 0 = let gram :< functor = runCofree tree in cofree $ prependParent parentLabel gram :< assignSiblings (assignParent parentLabel (pred n) <$> functor)
+ | n > 0 = let gram :< functor = runCofree tree in cofree $ prependParent parentLabel gram :< (assignParent parentLabel (pred n) <$> functor)
| otherwise = tree
prependParent parentLabel gram = gram { stem = parentLabel : stem gram }
- assignSiblings functor = case functor of
- Leaf a -> Leaf a
- Indexed a -> Indexed $ windowed q setBases [] a
- Fixed a -> Fixed $ windowed q setBases [] a
- setBases child siblings rest = let (gram :< further) = (runCofree child) in cofree (setBase gram (siblings >>= base . extract) :< further) : rest
+ setBases gram siblings rest = setBase gram (siblings >>= base) : rest
setBase gram newBase = gram { base = take q (newBase <> repeat Nothing) }
setRootBase term = let (a :< f) = runCofree term in cofree (setBase a (base a) :< f)
setRootStem = foldr (\ p rest -> assignParent Nothing p . rest) identity [0..p]
diff --git a/src/DiffOutput.hs b/src/DiffOutput.hs
deleted file mode 100644
index 026786e2d..000000000
--- a/src/DiffOutput.hs
+++ /dev/null
@@ -1,48 +0,0 @@
-module DiffOutput where
-
-import Prologue
-import qualified Data.Text.IO as TextIO
-import Data.Functor.Both
-import Diffing
-import Parser
-import qualified Renderer.JSON as J
-import qualified Renderer.Patch as P
-import qualified Renderer.Summary as S
-import Renderer
-import Renderer.Split
-import Source
-import System.Directory
-import System.FilePath
-import qualified System.IO as IO
-
--- | Returns a rendered diff given a parser, diff arguments and two source blobs.
-textDiff :: Parser -> DiffArguments -> Both SourceBlob -> IO Text
-textDiff parser arguments sources = case format arguments of
- Split -> diffFiles parser split sources
- Patch -> diffFiles parser P.patch sources
- JSON -> diffFiles parser J.json sources
- Summary -> diffFiles parser S.summary sources
-
--- | Returns a truncated diff given diff arguments and two source blobs.
-truncatedDiff :: DiffArguments -> Both SourceBlob -> IO Text
-truncatedDiff arguments sources = case format arguments of
- Split -> pure ""
- Patch -> pure $ P.truncatePatch arguments sources
- JSON -> pure "{}"
- Summary -> pure ""
-
--- | Prints a rendered diff to stdio or a filepath given a parser, diff arguments and two source blobs.
-printDiff :: Parser -> DiffArguments -> Both SourceBlob -> IO ()
-printDiff parser arguments sources = case format arguments of
- Split -> put (output arguments) =<< diffFiles parser split sources
- where
- put Nothing rendered = TextIO.putStr rendered
- put (Just path) rendered = do
- isDir <- doesDirectoryExist path
- let outputPath = if isDir
- then path > (takeFileName outputPath -<.> ".html")
- else path
- IO.withFile outputPath IO.WriteMode (`TextIO.hPutStr` rendered)
- Patch -> TextIO.putStr =<< diffFiles parser P.patch sources
- JSON -> TextIO.putStr =<< diffFiles parser J.json sources
- Summary -> TextIO.putStr =<< diffFiles parser S.summary sources
diff --git a/src/Diffing.hs b/src/Diffing.hs
index cc813c00f..5e0b0725e 100644
--- a/src/Diffing.hs
+++ b/src/Diffing.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DataKinds #-}
module Diffing where
import Prologue hiding (fst, snd)
@@ -5,7 +6,7 @@ import qualified Data.ByteString.Char8 as B1
import Data.Functor.Both
import Data.Functor.Foldable
import Data.Record
-import qualified Data.Text as T
+import qualified Data.Text.IO as TextIO
import qualified Data.Text.ICU.Detect as Detect
import qualified Data.Text.ICU.Convert as Convert
import Data.These
@@ -18,15 +19,22 @@ import Parser
import Patch
import Range
import Renderer
+import Renderer.JSON
+import Renderer.Patch
+import Renderer.Split
+import Renderer.Summary
import Source hiding ((++))
import Syntax
+import System.Directory
import System.FilePath
+import qualified System.IO as IO
import Term
import TreeSitter
import Text.Parser.TreeSitter.Language
+import qualified Data.Text as T
-- | Return a parser based on the file extension (including the ".").
-parserForType :: T.Text -> Parser
+parserForType :: Text -> Parser '[Range, Category, Cost]
parserForType mediaType = case languageForType mediaType of
Just C -> treeSitterParser C ts_language_c
Just JavaScript -> treeSitterParser JavaScript ts_language_javascript
@@ -34,35 +42,33 @@ parserForType mediaType = case languageForType mediaType of
_ -> lineByLineParser
-- | A fallback parser that treats a file simply as rows of strings.
-lineByLineParser :: Parser
+lineByLineParser :: Parser '[Range, Category, Cost]
lineByLineParser input = pure . cofree . root $ case foldl' annotateLeaves ([], 0) lines of
(leaves, _) -> cofree <$> leaves
where
lines = actualLines input
- root children = let size = 1 + fromIntegral (length children) in
- ((Range 0 $ length input) .: Other "program" .: size .: Cost (unSize size) .: RNil) :< Indexed children
- leaf charIndex line = ((Range charIndex $ charIndex + T.length line) .: Other "program" .: 1 .: 1 .: RNil) :< Leaf line
+ root children = let cost = 1 + fromIntegral (length children) in
+ ((Range 0 $ length input) .: Other "program" .: cost .: RNil) :< Indexed children
+ leaf charIndex line = ((Range charIndex $ charIndex + T.length line) .: Other "program" .: 1 .: RNil) :< Leaf line
annotateLeaves (accum, charIndex) line =
- (accum ++ [ leaf charIndex (toText line) ]
- , charIndex + length line)
+ (accum <> [ leaf charIndex (toText line) ] , charIndex + length line)
toText = T.pack . Source.toString
-- | Return the parser that should be used for a given path.
-parserForFilepath :: FilePath -> Parser
-parserForFilepath = parserForType . T.pack . takeExtension
+parserForFilepath :: FilePath -> Parser '[Range, Category, Cost]
+parserForFilepath = parserForType . toS . takeExtension
-- | Replace every string leaf with leaves of the words in the string.
-breakDownLeavesByWord :: Source Char -> Term T.Text Info -> Term T.Text Info
+breakDownLeavesByWord :: (HasField fields Category, HasField fields Cost, HasField fields Range) => Source Char -> Term Text (Record fields) -> Term Text (Record fields)
breakDownLeavesByWord source = cata replaceIn
where
- replaceIn :: TermF T.Text Info (Term T.Text Info) -> Term T.Text Info
- replaceIn (info :< syntax) = let size' = 1 + sum (size . extract <$> syntax') in cofree $ setCost (setSize info size') (Cost (unSize size')) :< syntax'
+ replaceIn (info :< syntax) = let cost' = 1 + sum (cost . extract <$> syntax') in cofree $ setCost info cost' :< syntax'
where syntax' = case (ranges, syntax) of
(_:_:_, Leaf _) | category info /= Regex -> Indexed (makeLeaf info <$> ranges)
_ -> syntax
ranges = rangesAndWordsInSource (characterRange info)
rangesAndWordsInSource range = rangesAndWordsFrom (start range) (toString $ slice range source)
- makeLeaf info (range, substring) = cofree $ setCharacterRange info range :< Leaf (T.pack substring)
+ makeLeaf info (range, substring) = cofree $ setCharacterRange info range :< Leaf (toS substring)
-- | Transcode a file to a unicode source.
transcode :: B1.ByteString -> IO (Source Char)
@@ -81,7 +87,7 @@ readAndTranscodeFile path = do
-- | result.
-- | Returns the rendered result strictly, so it's always fully evaluated
-- | with respect to other IO actions.
-diffFiles :: Parser -> Renderer -> Both SourceBlob -> IO T.Text
+diffFiles :: (HasField fields Category, HasField fields Cost, HasField fields Range, Eq (Record fields)) => Parser fields -> Renderer (Record fields) -> Both SourceBlob -> IO Text
diffFiles parser renderer sourceBlobs = do
let sources = source <$> sourceBlobs
terms <- sequence $ parser <$> sources
@@ -92,11 +98,10 @@ diffFiles parser renderer sourceBlobs = do
(True, False) -> pure $ Insert (snd terms)
(False, True) -> pure $ Delete (fst terms)
(_, _) ->
- runBothWith (diffTerms construct shouldCompareTerms diffCostWithCachedTermSizes) $ replaceLeaves <*> terms
+ runBothWith (diffTerms construct shouldCompareTerms diffCostWithCachedTermCosts) (replaceLeaves <*> terms)
pure $! renderer textDiff sourceBlobs
- where construct :: CofreeF (Syntax Text) (Both Info) (Diff Text Info) -> Diff Text Info
- construct (info :< syntax) = free (Free ((setCost <$> info <*> sumCost syntax) :< syntax))
+ where construct (info :< syntax) = free (Free ((setCost <$> info <*> sumCost syntax) :< syntax))
sumCost = fmap getSum . foldMap (fmap Sum . getCost)
getCost diff = case runFree diff of
Free (info :< _) -> cost <$> info
@@ -104,7 +109,37 @@ diffFiles parser renderer sourceBlobs = do
shouldCompareTerms = (==) `on` category . extract
-- | The sum of the node count of the diff’s patches.
-diffCostWithCachedTermSizes :: Diff a Info -> Integer
-diffCostWithCachedTermSizes diff = unCost $ case runFree diff of
+diffCostWithCachedTermCosts :: HasField fields Cost => Diff leaf (Record fields) -> Integer
+diffCostWithCachedTermCosts diff = unCost $ case runFree diff of
Free (info :< _) -> sum (cost <$> info)
Pure patch -> sum (cost . extract <$> patch)
+
+
+-- | Returns a rendered diff given a parser, diff arguments and two source blobs.
+textDiff :: (Eq (Record fields), HasField fields Category, HasField fields Cost, HasField fields Range) => Parser fields -> DiffArguments -> Both SourceBlob -> IO Text
+textDiff parser arguments sources = case format arguments of
+ Split -> diffFiles parser split sources
+ Patch -> diffFiles parser patch sources
+ JSON -> diffFiles parser json sources
+ Summary -> diffFiles parser summary sources
+
+-- | Returns a truncated diff given diff arguments and two source blobs.
+truncatedDiff :: DiffArguments -> Both SourceBlob -> IO Text
+truncatedDiff arguments sources = case format arguments of
+ Split -> pure ""
+ Patch -> pure $ truncatePatch arguments sources
+ JSON -> pure "{}"
+ Summary -> pure ""
+
+-- | Prints a rendered diff to stdio or a filepath given a parser, diff arguments and two source blobs.
+printDiff :: (Eq (Record fields), HasField fields Category, HasField fields Cost, HasField fields Range) => Parser fields -> DiffArguments -> Both SourceBlob -> IO ()
+printDiff parser arguments sources = do
+ rendered <- textDiff parser arguments sources
+ case (output arguments) of
+ Nothing -> TextIO.putStr rendered
+ Just path -> do
+ isDir <- doesDirectoryExist path
+ let outputPath = if isDir
+ then path > (takeFileName outputPath -<.> ".html")
+ else path
+ IO.withFile outputPath IO.WriteMode (`TextIO.hPutStr` rendered)
diff --git a/src/Info.hs b/src/Info.hs
index 22f59cfd2..5c7b8223d 100644
--- a/src/Info.hs
+++ b/src/Info.hs
@@ -7,15 +7,9 @@ import Category
import Range
import Test.QuickCheck
-newtype Size = Size { unSize :: Integer }
- deriving (Eq, Num, Ord, Show)
newtype Cost = Cost { unCost :: Integer }
deriving (Eq, Num, Ord, Show)
-type InfoFields = '[ Range, Category, Size, Cost ]
-
-type Info = Record InfoFields
-
characterRange :: HasField fields Range => Record fields -> Range
characterRange = getField
@@ -28,12 +22,6 @@ category = getField
setCategory :: HasField fields Category => Record fields -> Category -> Record fields
setCategory = setField
-size :: HasField fields Size => Record fields -> Size
-size = getField
-
-setSize :: HasField fields Size => Record fields -> Size -> Record fields
-setSize = setField
-
cost :: HasField fields Cost => Record fields -> Cost
cost = getField
@@ -43,11 +31,6 @@ setCost = setField
-- Instances
-instance Arbitrary Size where
- arbitrary = Size <$> arbitrary
-
- shrink = fmap Size . shrink . unSize
-
instance Arbitrary Cost where
arbitrary = Cost <$> arbitrary
diff --git a/src/Interpreter.hs b/src/Interpreter.hs
index 15d8b0148..3ffec2810 100644
--- a/src/Interpreter.hs
+++ b/src/Interpreter.hs
@@ -1,8 +1,7 @@
module Interpreter (Comparable, DiffConstructor, diffTerms) where
import Algorithm
-import qualified Category as C
-import Data.Align
+import Category (Category)
import Data.Align.Generic
import Data.Functor.Foldable
import Data.Functor.Both hiding (fst, snd)
@@ -26,11 +25,11 @@ type Comparable leaf annotation = Term leaf annotation -> Term leaf annotation -
type DiffConstructor leaf annotation = CofreeF (Syntax leaf) (Both annotation) (Diff leaf annotation) -> Diff leaf annotation
-- | Diff two terms, given a function that determines whether two terms can be compared and a cost function.
-diffTerms :: (Show leaf, Show (Record fields), Eq leaf, Hashable leaf, Ord (Record fields), HasField fields C.Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Term leaf (Record fields) -> Term leaf (Record fields) -> Diff leaf (Record fields)
+diffTerms :: (Eq leaf, Hashable leaf, Eq (Record fields), HasField fields Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Term leaf (Record fields) -> Term leaf (Record fields) -> Diff leaf (Record fields)
diffTerms construct comparable cost a b = fromMaybe (pure $ Replace a b) $ constructAndRun construct comparable cost a b
-- | Constructs an algorithm and runs it
-constructAndRun :: (Show leaf, Show (Record fields), Eq leaf, Hashable leaf, Ord (Record fields), HasField fields C.Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Term leaf (Record fields) -> Term leaf (Record fields) -> Maybe (Diff leaf (Record fields))
+constructAndRun :: (Eq leaf, Hashable leaf, Eq (Record fields), HasField fields Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Term leaf (Record fields) -> Term leaf (Record fields) -> Maybe (Diff leaf (Record fields))
constructAndRun construct comparable cost t1 t2
| not $ comparable t1 t2 = Nothing
| (category <$> t1) == (category <$> t2) = hylo construct runCofree <$> zipTerms t1 t2
@@ -43,17 +42,18 @@ constructAndRun construct comparable cost t1 t2
annotate = pure . construct . (both annotation1 annotation2 :<)
-- | Runs the diff algorithm
-run :: (Show leaf, Show (Record fields), Eq leaf, Hashable leaf, Ord (Record fields), HasField fields C.Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Algorithm leaf (Record fields) (Diff leaf (Record fields)) -> Maybe (Diff leaf (Record fields))
+run :: (Eq leaf, Hashable leaf, Eq (Record fields), HasField fields Category) => DiffConstructor leaf (Record fields) -> Comparable leaf (Record fields) -> SES.Cost (Diff leaf (Record fields)) -> Algorithm leaf (Record fields) (Diff leaf (Record fields)) -> Maybe (Diff leaf (Record fields))
run construct comparable cost algorithm = case runFree algorithm of
Pure diff -> Just diff
Free (Recursive t1 t2 f) -> run construct comparable cost . f $ recur a b where
(annotation1 :< a, annotation2 :< b) = (runCofree t1, runCofree t2)
annotate = construct . (both annotation1 annotation2 :<)
- diffTerms' = diffTerms construct comparable cost
recur a b = maybe (pure (Replace t1 t2)) (annotate . fmap diffThese) (galign a b)
diffThese = these (pure . Delete) (pure . Insert) (diffTerms construct comparable cost)
Free (ByIndex a b f) -> run construct comparable cost . f $ ses (constructAndRun construct comparable cost) cost a b
Free (ByRandomWalkSimilarity a b f) -> run construct comparable cost . f $ rws (constructAndRun construct comparable cost) getLabel a b
- where getLabel = category
+ where getLabel (h :< t) = (category h, case t of
+ Leaf s -> Just s
+ _ -> Nothing)
diff --git a/src/Parser.hs b/src/Parser.hs
index 38f68a63a..9a1f8b64c 100644
--- a/src/Parser.hs
+++ b/src/Parser.hs
@@ -1,9 +1,12 @@
+{-# LANGUAGE RankNTypes #-}
module Parser where
import Prologue hiding (Constructor)
+import Data.Record
import Data.Text (pack)
import Category
import Info
+import Range
import qualified Syntax as S
import Term
import qualified Data.Set as Set
@@ -12,10 +15,7 @@ import Source
-- | A function that takes a source file and returns an annotated AST.
-- | The return is in the IO monad because some of the parsers are written in C
-- | and aren't pure.
-type Parser = Source Char -> IO (Term Text Info)
-
--- | A function which constructs a term from a source string, annotation, and children.
-type Constructor = Source Char -> Info -> [Term Text Info] -> Term Text Info
+type Parser fields = Source Char -> IO (Term Text (Record fields))
-- | Categories that are treated as fixed nodes.
fixedCategories :: Set.Set Category
@@ -27,11 +27,10 @@ isFixed = flip Set.member fixedCategories
-- | Given a function that maps production names to sets of categories, produce
-- | a Constructor.
-termConstructor :: Constructor
+termConstructor :: (Show (Record fields), HasField fields Category, HasField fields Range) => Source Char -> (Record fields) -> [Term Text (Record fields)] -> Term Text (Record fields)
termConstructor source info = cofree . construct
where
withDefaultInfo syntax = (info :< syntax)
- construct :: [Term Text Info] -> CofreeF (S.Syntax Text) Info (Term Text Info)
construct [] = withDefaultInfo . S.Leaf . pack . toString $ slice (characterRange info) source
construct children | Assignment == category info = case children of
(identifier:value:[]) -> withDefaultInfo $ S.Assignment identifier value
@@ -66,7 +65,7 @@ termConstructor source info = cofree . construct
, [x, y] <- children = withDefaultInfo $ S.VarAssignment x y
construct children | VarDecl == category info = withDefaultInfo . S.Indexed $ toVarDecl <$> children
where
- toVarDecl :: Term Text Info -> Term Text Info
+ toVarDecl :: (HasField fields Category) => Term Text (Record fields) -> Term Text (Record fields)
toVarDecl child = cofree $ (setCategory (extract child) VarDecl :< S.VarDecl child)
construct children | Switch == category info, (expr:_) <- children =
@@ -77,7 +76,7 @@ termConstructor source info = cofree . construct
construct children | Object == category info = withDefaultInfo . S.Object $ foldMap toTuple children
where
- toTuple :: Term Text Info -> [Term Text Info]
+ toTuple :: Term Text (Record fields) -> [Term Text (Record fields)]
toTuple child | S.Indexed [key,value] <- unwrap child = [cofree (extract child :< S.Pair key value)]
toTuple child | S.Fixed [key,value] <- unwrap child = [cofree (extract child :< S.Pair key value)]
toTuple child | S.Leaf c <- unwrap child = [cofree (extract child :< S.Comment c)]
diff --git a/src/Renderer.hs b/src/Renderer.hs
index 2d8e8efed..7696016b2 100644
--- a/src/Renderer.hs
+++ b/src/Renderer.hs
@@ -3,11 +3,10 @@ module Renderer where
import Prologue
import Data.Functor.Both
import Diff
-import Info
import Source
-- | A function that will render a diff, given the two source files.
-type Renderer = Diff Text Info -> Both SourceBlob -> Text
+type Renderer annotation = Diff Text annotation -> Both SourceBlob -> Text
data DiffArguments = DiffArguments { format :: Format, output :: Maybe FilePath, outputPath :: FilePath }
deriving (Show)
diff --git a/src/Renderer/JSON.hs b/src/Renderer/JSON.hs
index 7fbccc3d4..a2ad3cb81 100644
--- a/src/Renderer/JSON.hs
+++ b/src/Renderer/JSON.hs
@@ -10,6 +10,7 @@ import Category
import Data.Aeson hiding (json)
import Data.Bifunctor.Join
import Data.ByteString.Builder
+import Data.Record
import qualified Data.Text as T
import Data.These
import Data.Vector hiding (toList)
@@ -22,13 +23,13 @@ import Syntax as S
import Term
-- | Render a diff to a string representing its JSON.
-json :: Renderer
+json :: (HasField fields Category, HasField fields Range) => Renderer (Record fields)
json diff sources = toS . toLazyByteString . fromEncoding . pairs $ "rows" .= annotateRows (alignDiff (source <$> sources) diff) <> "oids" .= (oid <$> sources) <> "paths" .= (path <$> sources)
where annotateRows = fmap (fmap NumberedLine) . numberedRows
newtype NumberedLine a = NumberedLine (Int, a)
-instance ToJSON (NumberedLine (SplitDiff leaf Info)) where
+instance (HasField fields Category, HasField fields Range) => ToJSON (NumberedLine (SplitDiff leaf (Record fields))) where
toJSON (NumberedLine (n, a)) = object (lineFields n a (getRange a))
toEncoding (NumberedLine (n, a)) = pairs $ mconcat (lineFields n a (getRange a))
instance ToJSON Category where
@@ -43,25 +44,25 @@ instance ToJSON a => ToJSON (Join These a) where
instance ToJSON a => ToJSON (Join (,) a) where
toJSON (Join (a, b)) = Array . fromList $ toJSON <$> [ a, b ]
toEncoding = foldable
-instance ToJSON (SplitDiff leaf Info) where
+instance (HasField fields Category, HasField fields Range) => ToJSON (SplitDiff leaf (Record fields)) where
toJSON splitDiff = case runFree splitDiff of
(Free (info :< syntax)) -> object (termFields info syntax)
(Pure patch) -> object (patchFields patch)
toEncoding splitDiff = case runFree splitDiff of
(Free (info :< syntax)) -> pairs $ mconcat (termFields info syntax)
(Pure patch) -> pairs $ mconcat (patchFields patch)
-instance ToJSON (Term leaf Info) where
+instance (HasField fields Category, HasField fields Range) => ToJSON (Term leaf (Record fields)) where
toJSON term | (info :< syntax) <- runCofree term = object (termFields info syntax)
toEncoding term | (info :< syntax) <- runCofree term = pairs $ mconcat (termFields info syntax)
-lineFields :: KeyValue kv => Int -> SplitDiff leaf Info -> Range -> [kv]
+lineFields :: (HasField fields Category, HasField fields Range) => KeyValue kv => Int -> SplitDiff leaf (Record fields) -> Range -> [kv]
lineFields n term range = [ "number" .= n
, "terms" .= [ term ]
, "range" .= range
, "hasChanges" .= hasChanges term
]
-termFields :: (ToJSON recur, KeyValue kv) => Info -> Syntax leaf recur -> [kv]
+termFields :: (ToJSON recur, KeyValue kv, HasField fields Category, HasField fields Range) => Record fields -> Syntax leaf recur -> [kv]
termFields info syntax = "range" .= characterRange info : "category" .= category info : case syntax of
Leaf _ -> []
Indexed c -> childrenFields c
@@ -86,7 +87,7 @@ termFields info syntax = "range" .= characterRange info : "category" .= category
S.Commented comments child -> childrenFields (comments <> maybeToList child)
where childrenFields c = [ "children" .= c ]
-patchFields :: KeyValue kv => SplitPatch (Term leaf Info) -> [kv]
+patchFields :: (KeyValue kv, HasField fields Category, HasField fields Range) => SplitPatch (Term leaf (Record fields)) -> [kv]
patchFields patch = case patch of
SplitInsert term -> fields "insert" term
SplitDelete term -> fields "delete" term
diff --git a/src/Renderer/Patch.hs b/src/Renderer/Patch.hs
index d97a0800d..0246f4013 100644
--- a/src/Renderer/Patch.hs
+++ b/src/Renderer/Patch.hs
@@ -9,27 +9,28 @@ import Alignment
import Data.Bifunctor.Join
import Data.Functor.Both as Both
import Data.List (span, unzip)
+import Data.Record
import Data.String
import Data.Text (pack)
import Data.These
import Diff
-import Info
import Patch
import Prologue hiding (fst, snd)
+import Range
import Renderer
-import Source hiding ((++), break)
+import Source hiding (break)
import SplitDiff
-- | Render a timed out file as a truncated diff.
truncatePatch :: DiffArguments -> Both SourceBlob -> Text
-truncatePatch _ blobs = pack $ header blobs ++ "#timed_out\nTruncating diff: timeout reached.\n"
+truncatePatch _ blobs = pack $ header blobs <> "#timed_out\nTruncating diff: timeout reached.\n"
-- | Render a diff in the traditional patch format.
-patch :: Renderer
+patch :: HasField fields Range => Renderer (Record fields)
patch diff blobs = pack $ case getLast (foldMap (Last . Just) string) of
- Just c | c /= '\n' -> string ++ "\n\\ No newline at end of file\n"
+ Just c | c /= '\n' -> string <> "\n\\ No newline at end of file\n"
_ -> string
- where string = header blobs ++ mconcat (showHunk blobs <$> hunks diff blobs)
+ where string = header blobs <> mconcat (showHunk blobs <$> hunks diff blobs)
-- | A hunk in a patch, including the offset, changes, and context.
data Hunk a = Hunk { offset :: Both (Sum Int), changes :: [Change a], trailingContext :: [Join These a] }
@@ -52,65 +53,65 @@ rowIncrement :: Join These a -> Both (Sum Int)
rowIncrement = Join . fromThese (Sum 0) (Sum 0) . runJoin . (Sum 1 <$)
-- | Given the before and after sources, render a hunk to a string.
-showHunk :: Both SourceBlob -> Hunk (SplitDiff a Info) -> String
-showHunk blobs hunk = maybeOffsetHeader ++
- concat (showChange sources <$> changes hunk) ++
+showHunk :: HasField fields Range => Both SourceBlob -> Hunk (SplitDiff a (Record fields)) -> String
+showHunk blobs hunk = maybeOffsetHeader <>
+ concat (showChange sources <$> changes hunk) <>
showLines (snd sources) ' ' (maybeSnd . runJoin <$> trailingContext hunk)
where sources = source <$> blobs
maybeOffsetHeader = if lengthA > 0 && lengthB > 0
then offsetHeader
else mempty
- offsetHeader = "@@ -" ++ offsetA ++ "," ++ show lengthA ++ " +" ++ offsetB ++ "," ++ show lengthB ++ " @@" ++ "\n"
+ offsetHeader = "@@ -" <> offsetA <> "," <> show lengthA <> " +" <> offsetB <> "," <> show lengthB <> " @@" <> "\n"
(lengthA, lengthB) = runJoin . fmap getSum $ hunkLength hunk
(offsetA, offsetB) = runJoin . fmap (show . getSum) $ offset hunk
-- | Given the before and after sources, render a change to a string.
-showChange :: Both (Source Char) -> Change (SplitDiff a Info) -> String
-showChange sources change = showLines (snd sources) ' ' (maybeSnd . runJoin <$> context change) ++ deleted ++ inserted
+showChange :: HasField fields Range => Both (Source Char) -> Change (SplitDiff a (Record fields)) -> String
+showChange sources change = showLines (snd sources) ' ' (maybeSnd . runJoin <$> context change) <> deleted <> inserted
where (deleted, inserted) = runJoin $ pure showLines <*> sources <*> both '-' '+' <*> Join (unzip (fromThese Nothing Nothing . runJoin . fmap Just <$> contents change))
-- | Given a source, render a set of lines to a string with a prefix.
-showLines :: Source Char -> Char -> [Maybe (SplitDiff leaf Info)] -> String
+showLines :: HasField fields Range => Source Char -> Char -> [Maybe (SplitDiff leaf (Record fields))] -> String
showLines source prefix lines = fromMaybe "" . mconcat $ fmap prepend . showLine source <$> lines
where prepend "" = ""
prepend source = prefix : source
-- | Given a source, render a line to a string.
-showLine :: Source Char -> Maybe (SplitDiff leaf Info) -> Maybe String
+showLine :: HasField fields Range => Source Char -> Maybe (SplitDiff leaf (Record fields)) -> Maybe String
showLine source line | Just line <- line = Just . toString . (`slice` source) $ getRange line
| otherwise = Nothing
-- | Returns the header given two source blobs and a hunk.
header :: Both SourceBlob -> String
-header blobs = intercalate "\n" [filepathHeader, fileModeHeader, beforeFilepath, afterFilepath] ++ "\n"
- where filepathHeader = "diff --git a/" ++ pathA ++ " b/" ++ pathB
+header blobs = intercalate "\n" [filepathHeader, fileModeHeader, beforeFilepath, afterFilepath] <> "\n"
+ where filepathHeader = "diff --git a/" <> pathA <> " b/" <> pathB
fileModeHeader = case (modeA, modeB) of
- (Nothing, Just mode) -> intercalate "\n" [ "new file mode " ++ modeToDigits mode, blobOidHeader ]
- (Just mode, Nothing) -> intercalate "\n" [ "deleted file mode " ++ modeToDigits mode, blobOidHeader ]
- (Just mode, Just other) | mode == other -> "index " ++ oidA ++ ".." ++ oidB ++ " " ++ modeToDigits mode
+ (Nothing, Just mode) -> intercalate "\n" [ "new file mode " <> modeToDigits mode, blobOidHeader ]
+ (Just mode, Nothing) -> intercalate "\n" [ "deleted file mode " <> modeToDigits mode, blobOidHeader ]
+ (Just mode, Just other) | mode == other -> "index " <> oidA <> ".." <> oidB <> " " <> modeToDigits mode
(Just mode1, Just mode2) -> intercalate "\n" [
- "old mode " ++ modeToDigits mode1,
- "new mode " ++ modeToDigits mode2,
+ "old mode " <> modeToDigits mode1,
+ "new mode " <> modeToDigits mode2,
blobOidHeader
]
(Nothing, Nothing) -> ""
- blobOidHeader = "index " ++ oidA ++ ".." ++ oidB
+ blobOidHeader = "index " <> oidA <> ".." <> oidB
modeHeader :: String -> Maybe SourceKind -> String -> String
modeHeader ty maybeMode path = case maybeMode of
- Just _ -> ty ++ "/" ++ path
+ Just _ -> ty <> "/" <> path
Nothing -> "/dev/null"
- beforeFilepath = "--- " ++ modeHeader "a" modeA pathA
- afterFilepath = "+++ " ++ modeHeader "b" modeB pathB
+ beforeFilepath = "--- " <> modeHeader "a" modeA pathA
+ afterFilepath = "+++ " <> modeHeader "b" modeB pathB
(pathA, pathB) = runJoin $ path <$> blobs
(oidA, oidB) = runJoin $ oid <$> blobs
(modeA, modeB) = runJoin $ blobKind <$> blobs
-- | A hunk representing no changes.
-emptyHunk :: Hunk (SplitDiff a Info)
+emptyHunk :: Hunk (SplitDiff a annotation)
emptyHunk = Hunk { offset = mempty, changes = [], trailingContext = [] }
-- | Render a diff as a series of hunks.
-hunks :: Show a => Diff a Info -> Both SourceBlob -> [Hunk (SplitDiff a Info)]
+hunks :: HasField fields Range => Diff a (Record fields) -> Both SourceBlob -> [Hunk (SplitDiff a (Record fields))]
hunks _ blobs | sources <- source <$> blobs
, sourcesEqual <- runBothWith (==) sources
, sourcesNull <- runBothWith (&&) (null <$> sources)
@@ -120,14 +121,14 @@ hunks diff blobs = hunksInRows (pure 1) $ alignDiff (source <$> blobs) diff
-- | Given beginning line numbers, turn rows in a split diff into hunks in a
-- | patch.
-hunksInRows :: Both (Sum Int) -> [Join These (SplitDiff a Info)] -> [Hunk (SplitDiff a Info)]
+hunksInRows :: Both (Sum Int) -> [Join These (SplitDiff a annotation)] -> [Hunk (SplitDiff a annotation)]
hunksInRows start rows = case nextHunk start rows of
Nothing -> []
Just (hunk, rest) -> hunk : hunksInRows (offset hunk <> hunkLength hunk) rest
-- | Given beginning line numbers, return the next hunk and the remaining rows
-- | of the split diff.
-nextHunk :: Both (Sum Int) -> [Join These (SplitDiff a Info)] -> Maybe (Hunk (SplitDiff a Info), [Join These (SplitDiff a Info)])
+nextHunk :: Both (Sum Int) -> [Join These (SplitDiff a annotation)] -> Maybe (Hunk (SplitDiff a annotation), [Join These (SplitDiff a annotation)])
nextHunk start rows = case nextChange start rows of
Nothing -> Nothing
Just (offset, change, rest) -> let (changes, rest') = contiguousChanges rest in Just (Hunk offset (change : changes) $ take 3 rest', drop 3 rest')
@@ -139,7 +140,7 @@ nextHunk start rows = case nextChange start rows of
-- | Given beginning line numbers, return the number of lines to the next
-- | the next change, and the remaining rows of the split diff.
-nextChange :: Both (Sum Int) -> [Join These (SplitDiff a Info)] -> Maybe (Both (Sum Int), Change (SplitDiff a Info), [Join These (SplitDiff a Info)])
+nextChange :: Both (Sum Int) -> [Join These (SplitDiff a annotation)] -> Maybe (Both (Sum Int), Change (SplitDiff a annotation), [Join These (SplitDiff a annotation)])
nextChange start rows = case changeIncludingContext leadingContext afterLeadingContext of
Nothing -> Nothing
Just (change, afterChanges) -> Just (start <> mconcat (rowIncrement <$> skippedContext), change, afterChanges)
@@ -149,12 +150,12 @@ nextChange start rows = case changeIncludingContext leadingContext afterLeadingC
-- | Return a Change with the given context and the rows from the begginning of
-- | the given rows that have changes, or Nothing if the first row has no
-- | changes.
-changeIncludingContext :: [Join These (SplitDiff a Info)] -> [Join These (SplitDiff a Info)] -> Maybe (Change (SplitDiff a Info), [Join These (SplitDiff a Info)])
+changeIncludingContext :: [Join These (SplitDiff a annotation)] -> [Join These (SplitDiff a annotation)] -> Maybe (Change (SplitDiff a annotation), [Join These (SplitDiff a annotation)])
changeIncludingContext leadingContext rows = case changes of
[] -> Nothing
_ -> Just (Change leadingContext changes, afterChanges)
where (changes, afterChanges) = span rowHasChanges rows
-- | Whether a row has changes on either side.
-rowHasChanges :: Join These (SplitDiff a Info) -> Bool
+rowHasChanges :: Join These (SplitDiff a annotation) -> Bool
rowHasChanges row = or (hasChanges <$> row)
diff --git a/src/Renderer/Split.hs b/src/Renderer/Split.hs
index 8436357be..fe67726fa 100644
--- a/src/Renderer/Split.hs
+++ b/src/Renderer/Split.hs
@@ -1,5 +1,4 @@
-{-# LANGUAGE OverloadedStrings #-}
-module Renderer.Split where
+module Renderer.Split (split) where
import Alignment
import Category as C
@@ -7,6 +6,7 @@ import Data.Bifunctor.Join
import Data.Foldable
import Data.Functor.Both
import Data.Functor.Foldable (cata)
+import Data.Record
import qualified Data.Text.Lazy as TL
import Data.These
import Info
@@ -14,7 +14,7 @@ import Prologue hiding (div, head, fst, snd, link)
import qualified Prologue
import Range
import Renderer
-import Source hiding ((++))
+import Source
import SplitDiff
import Syntax
import Term
@@ -24,10 +24,6 @@ import Text.Blaze.Html5 hiding (map)
import qualified Text.Blaze.Html5.Attributes as A
import qualified Text.Blaze.Internal as Blaze
--- | Return the first item in the Foldable, or Nothing if it's empty.
-maybeFirst :: Foldable f => f a -> Maybe a
-maybeFirst = foldr (const . Just) Nothing
-
-- | Add the first category from a Foldable of categories as a class name as a
-- | class name on the markup, prefixed by `category-`.
classifyMarkup :: Category -> Markup -> Markup
@@ -70,13 +66,13 @@ styleName category = "category-" <> case category of
-- | Pick the class name for a split patch.
splitPatchToClassName :: SplitPatch a -> AttributeValue
-splitPatchToClassName patch = stringValue $ "patch " ++ case patch of
+splitPatchToClassName patch = stringValue $ "patch " <> case patch of
SplitInsert _ -> "insert"
SplitDelete _ -> "delete"
SplitReplace _ -> "replace"
-- | Render a diff as an HTML split diff.
-split :: Renderer
+split :: (HasField fields Category, HasField fields Cost, HasField fields Range) => Renderer (Record fields)
split diff blobs = TL.toStrict . renderHtml
. docTypeHtml
. ((head $ link ! A.rel "stylesheet" ! A.href "style.css") <>)
@@ -99,50 +95,19 @@ split diff blobs = TL.toStrict . renderHtml
columnWidth = max (20 + digits maxNumber * 8) 40
-- | Render a line with numbers as an HTML row.
- numberedLinesToMarkup :: Join These (Int, SplitDiff a Info) -> Markup
numberedLinesToMarkup numberedLines = tr $ runBothWith (<>) (renderLine <$> Join (fromThese Nothing Nothing (runJoin (Just <$> numberedLines))) <*> sources) <> string "\n"
- renderLine :: Maybe (Int, SplitDiff leaf Info) -> Source Char -> Markup
- renderLine (Just (number, line)) source = toMarkup $ Renderable (hasChanges line, number, Renderable (source, line))
+ renderLine (Just (number, line)) source = toMarkup $ Cell (hasChanges line) number (Renderable source line)
renderLine _ _ =
td mempty ! A.class_ (stringValue "blob-num blob-num-empty empty-cell")
<> td mempty ! A.class_ (stringValue "blob-code blob-code-empty empty-cell")
<> string "\n"
--- | Something that can be rendered as markup.
-newtype Renderable a = Renderable a
+-- | A cell in a table, characterized by whether it contains changes & its line number.
+data Cell a = Cell !Bool !Int !a
-instance ToMarkup f => ToMarkup (Renderable (Source Char, Info, Syntax a (f, Range))) where
- toMarkup (Renderable (source, info, syntax)) = (! A.data_ (textValue (show . unSize $ size info))) . classifyMarkup (category info) $ case syntax of
- Leaf _ -> span . text . toText $ slice (characterRange info) source
- Indexed children -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) children
- Fixed children -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) children
- Comment _ -> span . text . toText $ slice (characterRange info) source
- Commented cs child -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) (cs <> maybeToList child)
- Syntax.FunctionCall identifier children -> dl . mconcat $ (wrapIn dt <$> (contentElements source (characterRange info) [identifier])) <> (wrapIn dd <$> contentElements source (characterRange info) children)
- Syntax.Function identifier params expressions -> ul . mconcat $ wrapIn li <$>
- contentElements source (characterRange info) (catMaybes [identifier, params, Just expressions])
- Syntax.MethodCall targetId methodId methodParams ->
- dl . mconcat $ (wrapIn dt <$> (contentElements source (characterRange info) [targetId])) <> (wrapIn dd <$> contentElements source (characterRange info) [methodId, methodParams])
- Syntax.Args children -> ul . mconcat $ wrapIn li <$>
- contentElements source (characterRange info) children
- Syntax.MemberAccess memberId property -> ul . mconcat $ wrapIn li <$>
- contentElements source (characterRange info) [memberId, property]
- Syntax.Assignment memberId value -> ul . mconcat $ wrapIn li <$>
- contentElements source (characterRange info) [memberId, value]
- Syntax.VarDecl decl -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) [decl]
- Syntax.VarAssignment varId value ->
- dl . mconcat $ (wrapIn dt <$> (contentElements source (characterRange info) [varId])) <> (wrapIn dd <$> contentElements source (characterRange info) [value])
- Syntax.Switch expr cases -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) (expr : cases)
- Syntax.Case expr body -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) [expr, body]
- Syntax.Ternary expr cases -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) (expr : cases)
- Syntax.MathAssignment id value ->
- dl . mconcat $ (wrapIn dt <$> (contentElements source (characterRange info) [id])) <> (wrapIn dd <$> contentElements source (characterRange info) [value])
- Syntax.SubscriptAccess target property ->
- dl . mconcat $ (wrapIn dt <$> (contentElements source (characterRange info) [target])) <> (wrapIn dd <$> contentElements source (characterRange info) [property])
- Syntax.Operator syntaxes -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) syntaxes
- Syntax.Object children -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) children
- Syntax.Pair a b -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) [a, b]
+-- | Something that can be rendered as markup with reference to some source.
+data Renderable a = Renderable !(Source Char) !a
contentElements :: (Foldable t, ToMarkup f) => Source Char -> Range -> t (f, Range) -> [Markup]
contentElements source range children = let (elements, next) = foldr' (markupForContextAndChild source) ([], end range) children in
@@ -158,17 +123,27 @@ wrapIn _ l@Blaze.Content{} = l
wrapIn _ l@Blaze.Comment{} = l
wrapIn f p = f p
-instance ToMarkup (Renderable (Source Char, Term a Info)) where
- toMarkup (Renderable (source, term)) = Prologue.fst $ cata (\ (info :< syntax) -> (toMarkup $ Renderable (source, info, syntax), characterRange info)) term
-instance ToMarkup (Renderable (Source Char, SplitDiff a Info)) where
- toMarkup (Renderable (source, diff)) = Prologue.fst . iter (\ (info :< syntax) -> (toMarkup $ Renderable (source, info, syntax), characterRange info)) $ toMarkupAndRange <$> diff
- where toMarkupAndRange :: SplitPatch (Term a Info) -> (Markup, Range)
- toMarkupAndRange patch = let term@(info :< _) = runCofree $ getSplitTerm patch in
- ((div ! A.class_ (splitPatchToClassName patch) ! A.data_ (stringValue (show (unSize (size info))))) . toMarkup $ Renderable (source, cofree term), characterRange info)
+-- Instances
-instance ToMarkup a => ToMarkup (Renderable (Bool, Int, a)) where
- toMarkup (Renderable (hasChanges, num, line)) =
+instance (ToMarkup f, HasField fields Category, HasField fields Cost, HasField fields Range) => ToMarkup (Renderable (CofreeF (Syntax leaf) (Record fields) (f, Range))) where
+ toMarkup (Renderable source (info :< syntax)) = classifyMarkup (category info) $ case syntax of
+ Leaf _ -> span . string . toString $ slice (characterRange info) source
+ _ -> ul . mconcat $ wrapIn li <$> contentElements source (characterRange info) (toList syntax)
+
+instance (HasField fields Category, HasField fields Cost, HasField fields Range) => ToMarkup (Renderable (Term leaf (Record fields))) where
+ toMarkup (Renderable source term) = Prologue.fst $ cata (\ t -> (toMarkup $ Renderable source t, characterRange (headF t))) term
+
+instance (HasField fields Category, HasField fields Cost, HasField fields Range) => ToMarkup (Renderable (SplitDiff leaf (Record fields))) where
+ toMarkup (Renderable source diff) = Prologue.fst . iter (\ t -> (toMarkup $ Renderable source t, characterRange (headF t))) $ toMarkupAndRange <$> diff
+ where toMarkupAndRange patch = let term@(info :< _) = runCofree $ getSplitTerm patch in
+ ((div ! patchAttribute patch `withCostAttribute` cost info) . toMarkup $ Renderable source (cofree term), characterRange info)
+ patchAttribute patch = A.class_ (splitPatchToClassName patch)
+ withCostAttribute a (Cost c) | c > 0 = a ! A.data_ (stringValue (show c))
+ | otherwise = identity
+
+instance ToMarkup a => ToMarkup (Cell a) where
+ toMarkup (Cell hasChanges num line) =
td (string $ show num) ! A.class_ (stringValue $ if hasChanges then "blob-num blob-num-replacement" else "blob-num")
<> td (toMarkup line) ! A.class_ (stringValue $ if hasChanges then "blob-code blob-code-replacement" else "blob-code")
<> string "\n"
diff --git a/src/Renderer/Summary.hs b/src/Renderer/Summary.hs
index 4536e9d2c..708e331e0 100644
--- a/src/Renderer/Summary.hs
+++ b/src/Renderer/Summary.hs
@@ -1,11 +1,14 @@
module Renderer.Summary where
+import Category
import Prologue
import Renderer
-import DiffSummary
import Data.Aeson
+import Data.Record
+import Range
+import DiffSummary
import Data.Text (pack)
import Text.PrettyPrint.Leijen.Text (pretty)
-summary :: Renderer
+summary :: (HasField fields Category, HasField fields Range) => Renderer (Record fields)
summary diff _ = toS . encode $ pack . show . pretty <$> diffSummary diff
diff --git a/src/TreeSitter.hs b/src/TreeSitter.hs
index cd2279230..a567817fc 100644
--- a/src/TreeSitter.hs
+++ b/src/TreeSitter.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DataKinds #-}
module TreeSitter where
import Prologue hiding (Constructor)
@@ -14,7 +15,7 @@ import Text.Parser.TreeSitter hiding (Language(..))
import qualified Text.Parser.TreeSitter as TS
-- | Returns a TreeSitter parser for the given language and TreeSitter grammar.
-treeSitterParser :: Language -> Ptr TS.Language -> Parser
+treeSitterParser :: Language -> Ptr TS.Language -> Parser '[Range, Category, Cost]
treeSitterParser language grammar contents = do
document <- ts_document_make
ts_document_set_language document grammar
@@ -75,7 +76,7 @@ defaultCategoryForNodeName name = case name of
_ -> Other name
-- | Return a parser for a tree sitter language & document.
-documentToTerm :: Language -> Ptr Document -> Parser
+documentToTerm :: Language -> Ptr Document -> Parser '[Range, Category, Cost]
documentToTerm language document contents = alloca $ \ root -> do
ts_document_root_node_p document root
toTerm root
@@ -87,8 +88,8 @@ documentToTerm language document contents = alloca $ \ root -> do
-- Note: The strict application here is semantically important. Without it, we may not evaluate the range until after we’ve exited the scope that `node` was allocated within, meaning `alloca` will free it & other stack data may overwrite it.
range <- pure $! Range { start = fromIntegral $ ts_node_p_start_char node, end = fromIntegral $ ts_node_p_end_char node }
- let size' = 1 + sum (size . extract <$> children)
- let info = range .: (categoriesForLanguage language (toS name)) .: size' .: Cost (unSize size') .: RNil
+ let cost' = 1 + sum (cost . extract <$> children)
+ let info = range .: (categoriesForLanguage language (toS name)) .: cost' .: RNil
pure $! termConstructor contents info children
getChild node n out = do
_ <- ts_node_p_named_child node n out
diff --git a/test/AlignmentSpec.hs b/test/AlignmentSpec.hs
index 13730d8a2..57b267355 100644
--- a/test/AlignmentSpec.hs
+++ b/test/AlignmentSpec.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DataKinds #-}
module AlignmentSpec where
import Alignment
@@ -14,7 +15,6 @@ import Data.Record
import Data.String
import Data.Text.Arbitrary ()
import Data.These
-import Info
import Patch
import Prologue hiding (fst, snd)
import qualified Prologue
@@ -22,7 +22,6 @@ import Range
import qualified Source
import SplitDiff
import Syntax
-import Category
import Term
import Test.Hspec
import Test.Hspec.QuickCheck
@@ -32,7 +31,7 @@ spec :: Spec
spec = parallel $ do
describe "alignBranch" $ do
it "produces symmetrical context" $
- alignBranch getRange ([] :: [Join These (SplitDiff String Info)]) (both [Range 0 2, Range 2 4] [Range 0 2, Range 2 4]) `shouldBe`
+ alignBranch getRange ([] :: [Join These (SplitDiff String (Record '[Range]))]) (both [Range 0 2, Range 2 4] [Range 0 2, Range 2 4]) `shouldBe`
[ Join (These (Range 0 2, [])
(Range 0 2, []))
, Join (These (Range 2 4, [])
@@ -40,7 +39,7 @@ spec = parallel $ do
]
it "produces asymmetrical context" $
- alignBranch getRange ([] :: [Join These (SplitDiff String Info)]) (both [Range 0 2, Range 2 4] [Range 0 1]) `shouldBe`
+ alignBranch getRange ([] :: [Join These (SplitDiff String (Record '[Range]))]) (both [Range 0 2, Range 2 4] [Range 0 1]) `shouldBe`
[ Join (These (Range 0 2, [])
(Range 0 1, []))
, Join (This (Range 2 4, []))
@@ -254,13 +253,13 @@ instance Arbitrary BranchElement where
counts :: [Join These (Int, a)] -> Both Int
counts numbered = fromMaybe 0 . getLast . mconcat . fmap Last <$> Join (unalign (runJoin . fmap Prologue.fst <$> numbered))
-align :: Both (Source.Source Char) -> ConstructibleFree (Patch (Term String Info)) (Both Info) -> PrettyDiff (SplitDiff String Info)
+align :: Both (Source.Source Char) -> ConstructibleFree (Patch (Term String (Record '[Range]))) (Both (Record '[Range])) -> PrettyDiff (SplitDiff String (Record '[Range]))
align sources = PrettyDiff sources . fmap (fmap (getRange &&& identity)) . alignDiff sources . deconstruct
-info :: Int -> Int -> Info
-info start end = Range start end .: StringLiteral .: 0 .: 0 .: RNil
+info :: Int -> Int -> Record '[Range]
+info start end = Range start end .: RNil
-prettyDiff :: Both (Source.Source Char) -> [Join These (ConstructibleFree (SplitPatch (Term String Info)) Info)] -> PrettyDiff (SplitDiff String Info)
+prettyDiff :: Both (Source.Source Char) -> [Join These (ConstructibleFree (SplitPatch (Term String (Record '[Range]))) (Record '[Range]))] -> PrettyDiff (SplitDiff String (Record '[Range]))
prettyDiff sources = PrettyDiff sources . fmap (fmap ((getRange &&& identity) . deconstruct))
data PrettyDiff a = PrettyDiff { unPrettySources :: Both (Source.Source Char), unPrettyLines :: [Join These (Range, a)] }
@@ -279,14 +278,14 @@ newtype ConstructibleFree patch annotation = ConstructibleFree { deconstruct ::
class PatchConstructible p where
- insert :: Term String Info -> p
- delete :: Term String Info -> p
+ insert :: Term String (Record '[Range]) -> p
+ delete :: Term String (Record '[Range]) -> p
-instance PatchConstructible (Patch (Term String Info)) where
+instance PatchConstructible (Patch (Term String (Record '[Range]))) where
insert = Insert
delete = Delete
-instance PatchConstructible (SplitPatch (Term String Info)) where
+instance PatchConstructible (SplitPatch (Term String (Record '[Range]))) where
insert = SplitInsert
delete = SplitDelete
diff --git a/test/CorpusSpec.hs b/test/CorpusSpec.hs
index 57459ac38..1b10f015d 100644
--- a/test/CorpusSpec.hs
+++ b/test/CorpusSpec.hs
@@ -1,21 +1,24 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DataKinds, FlexibleContexts, GeneralizedNewtypeDeriving #-}
module CorpusSpec where
import System.IO
-import Data.String
import Diffing
import Renderer
import qualified Renderer.JSON as J
import qualified Renderer.Patch as P
import qualified Renderer.Split as Split
+import Category
import Control.DeepSeq
import Data.Functor.Both
import Data.List as List
import Data.Map as Map
+import Data.Record
import Data.Set as Set
import qualified Data.Text as T
+import Info
import Prologue hiding (fst, snd)
+import Range
import qualified Source as S
import System.FilePath
import System.FilePath.Glob
@@ -39,10 +42,8 @@ spec = parallel $ do
let tests = correctTests =<< paths
traverse_ (\ (formatName, renderer, paths, output) -> it (normalizeName (fst paths) ++ " (" ++ formatName ++ ")") $ testDiff renderer paths output matcher) tests
- correctTests :: (Both FilePath, Maybe FilePath, Maybe FilePath, Maybe FilePath) -> [(String, Renderer, Both FilePath, Maybe FilePath)]
correctTests paths@(_, Nothing, Nothing, Nothing) = testsForPaths paths
correctTests paths = List.filter (\(_, _, _, output) -> isJust output) $ testsForPaths paths
- testsForPaths :: (Both FilePath, Maybe FilePath, Maybe FilePath, Maybe FilePath) -> [(String, Renderer, Both FilePath, Maybe FilePath)]
testsForPaths (paths, json, patch, split) = [ ("json", J.json, paths, json), ("patch", P.patch, paths, patch), ("split", Split.split, paths, split) ]
-- | Return all the examples from the given directory. Examples are expected to
@@ -69,7 +70,7 @@ normalizeName path = addExtension (dropExtension $ dropExtension path) (takeExte
-- | Given file paths for A, B, and, optionally, a diff, return whether diffing
-- | the files will produce the diff. If no diff is provided, then the result
-- | is true, but the diff will still be calculated.
-testDiff :: Renderer -> Both FilePath -> Maybe FilePath -> (Verbatim -> Verbatim -> Expectation) -> Expectation
+testDiff :: Renderer (Record '[Range, Category, Cost]) -> Both FilePath -> Maybe FilePath -> (Verbatim -> Verbatim -> Expectation) -> Expectation
testDiff renderer paths diff matcher = do
sources <- sequence $ readAndTranscodeFile <$> paths
actual <- Verbatim <$> diffFiles parser renderer (sourceBlobs sources)
diff --git a/test/Data/RandomWalkSimilarity/Spec.hs b/test/Data/RandomWalkSimilarity/Spec.hs
index 288e16d3b..97074107b 100644
--- a/test/Data/RandomWalkSimilarity/Spec.hs
+++ b/test/Data/RandomWalkSimilarity/Spec.hs
@@ -17,10 +17,10 @@ spec :: Spec
spec = parallel $ do
describe "pqGrams" $ do
prop "produces grams with stems of the specified length" . forAll (arbitrary `suchThat` (\ (_, p, q) -> p > 0 && q > 0)) $
- \ (term, p, q) -> pqGrams p q identity (toTerm term :: Term Text Text) `shouldSatisfy` all ((== p) . length . stem)
+ \ (term, p, q) -> pqGrams p q headF (toTerm term :: Term Text Text) `shouldSatisfy` all ((== p) . length . stem)
prop "produces grams with bases of the specified length" . forAll (arbitrary `suchThat` (\ (_, p, q) -> p > 0 && q > 0)) $
- \ (term, p, q) -> pqGrams p q identity (toTerm term :: Term Text Text) `shouldSatisfy` all ((== q) . length . base)
+ \ (term, p, q) -> pqGrams p q headF (toTerm term :: Term Text Text) `shouldSatisfy` all ((== q) . length . base)
describe "featureVector" $ do
prop "produces a vector of the specified dimension" . forAll (arbitrary `suchThat` ((> 0) . Prologue.snd)) $
@@ -31,5 +31,5 @@ spec = parallel $ do
prop "produces correct diffs" . forAll (scale (`div` 4) arbitrary) $
\ (as, bs) -> let tas = toTerm <$> as
tbs = toTerm <$> bs
- diff = free (Free (pure Program :< Indexed (rws compare identity tas tbs :: [Diff Text Category]))) in
+ diff = free (Free (pure Program :< Indexed (rws compare headF tas tbs :: [Diff Text Category]))) in
(beforeTerm diff, afterTerm diff) `shouldBe` (Just (cofree (Program :< Indexed tas)), Just (cofree (Program :< Indexed tbs)))
diff --git a/test/DiffSummarySpec.hs b/test/DiffSummarySpec.hs
index f381d66ba..cdf8cc1e5 100644
--- a/test/DiffSummarySpec.hs
+++ b/test/DiffSummarySpec.hs
@@ -6,23 +6,21 @@ import Data.Record
import Test.Hspec
import Test.Hspec.QuickCheck
import Diff
-import Info
-import Interpreter
-import Term.Arbitrary
import Syntax
import Patch
-import Range
import Category
import DiffSummary
import Text.PrettyPrint.Leijen.Text
+import Term.Arbitrary
+import Interpreter
-arrayInfo :: Info
-arrayInfo = rangeAt 0 .: ArrayLiteral .: 2 .: 0 .: RNil
+arrayInfo :: Record '[Category]
+arrayInfo = ArrayLiteral .: RNil
-literalInfo :: Info
-literalInfo = rangeAt 1 .: StringLiteral .: 1 .: 0 .: RNil
+literalInfo :: Record '[Category]
+literalInfo = StringLiteral .: RNil
-testDiff :: Diff Text Info
+testDiff :: Diff Text (Record '[Category])
testDiff = free $ Free (pure arrayInfo :< Indexed [ free $ Pure (Insert (cofree $ literalInfo :< Leaf "a")) ])
testSummary :: DiffSummary DiffInfo
diff --git a/test/diffs/asymmetrical-context.split.js b/test/diffs/asymmetrical-context.split.js
index 716e71293..57447bda9 100644
--- a/test/diffs/asymmetrical-context.split.js
+++ b/test/diffs/asymmetrical-context.split.js
@@ -1,31 +1,31 @@
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
2
+
2
-
3
+
3
-
4
+
4
-
5
+
5
-
2
console
.
log
(
'
world
'
)
;
+
2
console
.
log
(
'
world
'
)
;
-
6
console
.
log
(
'
world
'
)
;
+
6
console
.
log
(
'
world
'
)
;
-
3
-
7
+
3
+
7
\ No newline at end of file
diff --git a/test/diffs/dictionary.split.js b/test/diffs/dictionary.split.js
index e441d3780..f4d729479 100644
--- a/test/diffs/dictionary.split.js
+++ b/test/diffs/dictionary.split.js
@@ -1,25 +1,25 @@
-
1
{
+
1
{
-
1
{
+
1
{
-
2
"
b
"
:
4
,
+
2
"
b
"
:
4
,
-
2
"
b
"
:
5
,
+
2
"
b
"
:
5
,
-
3
"
a
"
:
5
+
3
"
a
"
:
5
-
3
"
a
"
:
5
+
3
"
a
"
:
5
-
4
}
+
4
}
-
4
}
+
4
}
-
5
-
5
+
5
+
5
\ No newline at end of file
diff --git a/test/diffs/insert.split.js b/test/diffs/insert.split.js
index be6673da3..32ebc47e6 100644
--- a/test/diffs/insert.split.js
+++ b/test/diffs/insert.split.js
@@ -1,14 +1,14 @@
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
2
console
.
log
(
'
world
'
)
;
+
2
console
.
log
(
'
world
'
)
;
-
2
-
3
+
2
+
3
\ No newline at end of file
diff --git a/test/diffs/multiline-insert.split.js b/test/diffs/multiline-insert.split.js
index cd9466b1c..46622452f 100644
--- a/test/diffs/multiline-insert.split.js
+++ b/test/diffs/multiline-insert.split.js
@@ -1,27 +1,27 @@
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
1
console
.
log
(
'
hello
'
)
;
+
1
console
.
log
(
'
hello
'
)
;
-
2
if (
true
)
{
+
2
if (
true
)
{
-
3
console
.
log
(
'
cruel
'
)
;
+
3
console
.
log
(
'
cruel
'
)
;
-
4
}
+
4
}
-
2
console
.
log
(
'
world
'
)
;
+
2
console
.
log
(
'
world
'
)
;
-
5
console
.
log
(
'
world
'
)
;
+
5
console
.
log
(
'
world
'
)
;
-
3
-
6
+
3
+
6
\ No newline at end of file
diff --git a/test/diffs/nested-insert.split.js b/test/diffs/nested-insert.split.js
index 77749ff37..818b295f1 100644
--- a/test/diffs/nested-insert.split.js
+++ b/test/diffs/nested-insert.split.js
@@ -1,24 +1,24 @@
-
1
if (
true
)
{
+
1
if (
true
)
{
-
1
if (
true
)
{
+
1
if (
true
)
{
-
2
console
.
log
(
'
hello
'
)
;
+
2
console
.
log
(
'
hello
'
)
;
-
2
console
.
log
(
'
hello
'
)
;
+
2
console
.
log
(
'
hello
'
)
;
-
3
console
.
log
(
'
world
'
)
;
+
3
console
.
log
(
'
world
'
)
;
-
3
}
+
3
}
-
4
}
+
4
}
-
4
-
5
+
4
+
5
\ No newline at end of file
diff --git a/test/diffs/newline-at-eof.split.js b/test/diffs/newline-at-eof.split.js
index 4ff547bbe..a648bae01 100644
--- a/test/diffs/newline-at-eof.split.js
+++ b/test/diffs/newline-at-eof.split.js
@@ -1,18 +1,18 @@
-
1
console
.
log
(
"
hello
,
world
"
)
;
+
1
console
.
log
(
"
hello
,
world
"
)
;
-
1
console
.
log
(
"
hello
,
world
"
)
;
+
1
console
.
log
(
"
hello
,
world
"
)
;
-
2
-
2
+
2
+
2
-
3
console
.
log
(
"
insertion
"
)
;
+
3
console
.
log
(
"
insertion
"
)
;
-
4
+
4
\ No newline at end of file
diff --git a/test/diffs/no-newline-at-eof.split.js b/test/diffs/no-newline-at-eof.split.js
index bc15089d8..135895541 100644
--- a/test/diffs/no-newline-at-eof.split.js
+++ b/test/diffs/no-newline-at-eof.split.js
@@ -1,13 +1,13 @@
-
1
console
.
log
(
"
hello
,
world
"
)
;
-
1
console
.
log
(
"
hello
,
world
"
)
;
+
1
console
.
log
(
"
hello
,
world
"
)
;
+
1
console
.
log
(
"
hello
,
world
"
)
;
-
2
+
2
-
3
console
.
log
(
"
insertion
"
)
;
+
3
console
.
log
(
"
insertion
"
)
;
\ No newline at end of file
diff --git a/test/diffs/reformat.split.js b/test/diffs/reformat.split.js
index 6e146f8b2..95f6033f6 100644
--- a/test/diffs/reformat.split.js
+++ b/test/diffs/reformat.split.js
@@ -1,13 +1,13 @@