1
1
mirror of https://github.com/github/semantic.git synced 2024-12-21 05:41:54 +03:00

Merge pull request #357 from github/split-refactoring

Split refactoring
This commit is contained in:
Josh Vera 2015-12-27 09:23:20 -08:00
commit e795d2434a
10 changed files with 318 additions and 427 deletions

View File

@ -81,12 +81,14 @@ body {
padding: 0;
font-family: monospace;
}
.blob-code-replacement:last-child .insert,
.blob-code-replacement:last-child .replace {
.blob-code:last-child .patch,
.blob-code:last-child .insert,
.blob-code:last-child .replace {
background-color: #a6f3a6;
}
.blob-code-replacement .delete,
.blob-code-replacement .replace {
.blob-code .patch,
.blob-code .delete,
.blob-code .replace {
background-color: #f8cbcb;
}

View File

@ -18,6 +18,8 @@ library
, Operation
, Algorithm
, Interpreter
, Line
, Row
, OrderedMap
, Patch
, SES
@ -78,6 +80,7 @@ test-suite semantic-diff-test
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Spec.hs
other-modules: ArbitraryTerm
build-depends: base
, containers
, free

45
src/Line.hs Normal file
View File

@ -0,0 +1,45 @@
module Line where
import Data.Monoid
import Data.List (intercalate)
import Text.Blaze.Html5 hiding (map)
import qualified Text.Blaze.Html5.Attributes as A
data Line a =
Line [a]
| EmptyLine
deriving (Eq, Functor, Foldable)
unLine :: Line a -> [a]
unLine EmptyLine = []
unLine (Line elements) = elements
maybeLast :: Foldable f => f a -> Maybe a
maybeLast = foldl (flip $ const . Just) Nothing
openLineBy :: (a -> Maybe a) -> [Line a] -> Maybe (Line a)
openLineBy _ [] = Nothing
openLineBy f (EmptyLine : rest) = openLineBy f rest
openLineBy f (line : _) = const line <$> (f =<< maybeLast (unLine line))
adjoinLinesBy :: (a -> Maybe a) -> [Line a] -> Line a -> [Line a]
adjoinLinesBy _ [] line = [line]
adjoinLinesBy f (EmptyLine : xs) line | Just _ <- openLineBy f xs = EmptyLine : adjoinLinesBy f xs line
adjoinLinesBy f (prev:rest) line | Just _ <- openLineBy f [ prev ] = (prev <> line) : rest
adjoinLinesBy _ lines line = line : lines
instance Show a => Show (Line a) where
show (Line elements) = "[" ++ intercalate ", " (show <$> elements) ++ "]"
show EmptyLine = "EmptyLine"
instance Monoid (Line a) where
mempty = EmptyLine
mappend EmptyLine line = line
mappend line EmptyLine = line
mappend (Line xs) (Line ys) = Line (xs <> ys)
instance ToMarkup a => ToMarkup (Bool, Int, Line a) where
toMarkup (_, _, EmptyLine) = 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"
toMarkup (hasChanges, num, Line contents)
= td (string $ show num) ! A.class_ (stringValue $ if hasChanges then "blob-num blob-num-replacement" else "blob-num")
<> td (mconcat $ toMarkup <$> contents) ! A.class_ (stringValue $ if hasChanges then "blob-code blob-code-replacement" else "blob-code") <> string "\n"

View File

@ -13,11 +13,17 @@ module OrderedMap (
, difference
) where
import qualified Data.Maybe as Maybe
data OrderedMap key value = OrderedMap { toList :: [(key, value)] }
deriving (Show, Eq, Functor, Foldable, Traversable)
instance Eq key => Monoid (OrderedMap key value) where
mempty = fromList []
mappend = union
fromList :: [(key, value)] -> OrderedMap key value
fromList list = OrderedMap list
fromList = OrderedMap
keys :: OrderedMap key value -> [key]
keys (OrderedMap pairs) = fst <$> pairs
@ -25,9 +31,7 @@ keys (OrderedMap pairs) = fst <$> pairs
infixl 9 !
(!) :: Eq key => OrderedMap key value -> key -> value
map ! key = case OrderedMap.lookup key map of
Just value -> value
Nothing -> error "no value found for key"
map ! key = Maybe.fromMaybe (error "no value found for key") $ OrderedMap.lookup key map
lookup :: Eq key => key -> OrderedMap key value -> Maybe value
lookup key = Prelude.lookup key . toList
@ -46,7 +50,7 @@ unions :: Eq key => [OrderedMap key value] -> OrderedMap key value
unions = foldl union empty
intersectionWith :: Eq key => (a -> b -> c) -> OrderedMap key a -> OrderedMap key b -> OrderedMap key c
intersectionWith combine (OrderedMap a) (OrderedMap b) = OrderedMap $ a >>= (\ (key, value) -> maybe [] (pure . ((,) key) . combine value) $ Prelude.lookup key b)
intersectionWith combine (OrderedMap a) (OrderedMap b) = OrderedMap $ a >>= (\ (key, value) -> maybe [] (pure . (,) key . combine value) $ Prelude.lookup key b)
difference :: Eq key => OrderedMap key a -> OrderedMap key b -> OrderedMap key a
difference (OrderedMap a) (OrderedMap b) = OrderedMap $ filter (not . (`elem` extant) . fst) a

View File

@ -4,6 +4,7 @@ import Control.Applicative ((<|>))
import qualified Data.Char as Char
import Data.Maybe (fromMaybe)
-- | A half-open interval of integers, defined by start & end indices.
data Range = Range { start :: !Int, end :: !Int }
deriving (Eq, Show)
@ -35,6 +36,10 @@ rangesAndWordsFrom startIndex string = fromMaybe [] $ takeAndContinue <$> (word
-- | > A member of one of the following Unicode general category _Letter_, _Mark_, _Number_, _Connector_Punctuation_
isWord c = Char.isLetter c || Char.isNumber c || Char.isMark c || Char.generalCategory c == Char.ConnectorPunctuation
-- | Return Just the last index from a non-empty range, or if the range is empty, Nothing.
maybeLastIndex :: Range -> Maybe Int
maybeLastIndex (Range start end) | start == end = Nothing
maybeLastIndex (Range _ end) = Just $ end - 1
instance Ord Range where
a <= b = start a <= start b

33
src/Row.hs Normal file
View File

@ -0,0 +1,33 @@
module Row where
import Line
data Row a = Row { unLeft :: Line a, unRight :: Line a }
deriving (Eq, Functor)
adjoinRowsBy :: (a -> Maybe a) -> (a -> Maybe a) -> [Row a] -> Row a -> [Row a]
adjoinRowsBy _ _ [] row = [row]
adjoinRowsBy f g rows (Row left' right') | Just _ <- openLineBy f $ unLeft <$> rows, Just _ <- openLineBy g $ unRight <$> rows = zipWith Row lefts rights
where lefts = adjoinLinesBy f (unLeft <$> rows) left'
rights = adjoinLinesBy g (unRight <$> rows) right'
adjoinRowsBy f _ rows (Row left' right') | Just _ <- openLineBy f $ unLeft <$> rows = case right' of
EmptyLine -> rest
_ -> Row EmptyLine right' : rest
where rest = zipWith Row lefts rights
lefts = adjoinLinesBy f (unLeft <$> rows) left'
rights = unRight <$> rows
adjoinRowsBy _ g rows (Row left' right') | Just _ <- openLineBy g $ unRight <$> rows = case left' of
EmptyLine -> rest
_ -> Row left' EmptyLine : rest
where rest = zipWith Row lefts rights
lefts = unLeft <$> rows
rights = adjoinLinesBy g (unRight <$> rows) right'
adjoinRowsBy _ _ rows row = row : rows
instance Show a => Show (Row a) where
show (Row left right) = "\n" ++ show left ++ " | " ++ show right

View File

@ -2,10 +2,11 @@ module Split where
import Prelude hiding (div, head, span)
import Diff
import Line
import Row
import Patch
import Term
import Syntax
import Control.Comonad.Cofree
import Range
import Control.Monad.Free
@ -14,48 +15,13 @@ import Text.Blaze.Html
import Text.Blaze.Html5 hiding (map)
import qualified Text.Blaze.Html5.Attributes as A
import Text.Blaze.Html.Renderer.Utf8
import qualified OrderedMap as Map
import Data.Maybe
import Data.Monoid
import qualified Data.Set as Set
import Debug.Trace
import Data.List (intersperse)
type ClassName = String
data HTML =
Break
| Text String
| Span (Maybe ClassName) String
| Ul (Maybe ClassName) [HTML]
| Dl (Maybe ClassName) [HTML]
| Div (Maybe ClassName) [HTML]
| Dt String
deriving (Show, Eq)
classifyMarkup :: Maybe ClassName -> Markup -> Markup
classifyMarkup (Just className) element = element ! A.class_ (stringValue className)
classifyMarkup _ element = element
toLi :: HTML -> Markup
toLi (Text s) = string s
toLi e = li $ toMarkup e
toDd :: HTML -> Markup
toDd (Text s) = string s
toDd e = dd $ toMarkup e
instance ToMarkup HTML where
toMarkup Break = br
toMarkup (Text s) = string s
toMarkup (Span className s) = classifyMarkup className . span $ string s
toMarkup (Ul className children) = classifyMarkup className . ul $ mconcat (toLi <$> children)
toMarkup (Dl className children) = classifyMarkup className . dl $ mconcat (toDd <$> children)
toMarkup (Div className children) = classifyMarkup className . div $ mconcat (toMarkup <$> children)
toMarkup (Dt key) = dt $ string key
trace' :: Show a => a -> a
trace' a = traceShow a a
classifyMarkup :: Foldable f => f String -> Markup -> Markup
classifyMarkup categories element = maybe element ((element !) . A.class_ . stringValue . ("category-" ++)) $ maybeLast categories
split :: Diff a Info -> String -> String -> IO ByteString
split diff before after = return . renderHtml
@ -63,10 +29,10 @@ split diff before after = return . renderHtml
. ((head $ link ! A.rel (stringValue "stylesheet") ! A.href (stringValue "style.css")) <>)
. body
. (table ! A.class_ (stringValue "diff")) $
((<>) (colgroup $ (col ! A.width (stringValue . show $ columnWidth)) <> col <> (col ! A.width (stringValue . show $ columnWidth)) <> col))
. mconcat $ toMarkup <$> reverse numbered
((colgroup $ (col ! A.width (stringValue . show $ columnWidth)) <> col <> (col ! A.width (stringValue . show $ columnWidth)) <> col) <>)
. mconcat $ numberedLinesToMarkup <$> reverse numbered
where
rows = fst $ diffToRows diff (0, 0) before after
rows = fst (splitDiffByLines diff (0, 0) (before, after))
numbered = foldl numberRows [] rows
maxNumber = case numbered of
[] -> 0
@ -74,237 +40,124 @@ split diff before after = return . renderHtml
digits :: Int -> Int
digits n = let base = 10 :: Int in
ceiling (log(fromIntegral n) / log(fromIntegral base) :: Double)
ceiling (logBase (fromIntegral base) (fromIntegral n) :: Double)
columnWidth = max (20 + digits maxNumber * 8) 40
numberRows :: [(Int, Line, Int, Line)] -> Row -> [(Int, Line, Int, Line)]
numberedLinesToMarkup :: (Int, Line (SplitDiff a Info), Int, Line (SplitDiff a Info)) -> Markup
numberedLinesToMarkup (m, left, n, right) = tr $ toMarkup (or $ hasChanges <$> left, m, renderable before left) <> toMarkup (or $ hasChanges <$> right, n, renderable after right) <> string "\n"
renderable source = fmap (Renderable . (,) source)
hasChanges diff = or $ const True <$> diff
numberRows :: [(Int, Line a, Int, Line a)] -> Row a -> [(Int, Line a, Int, Line a)]
numberRows [] (Row EmptyLine EmptyLine) = []
numberRows [] (Row left@(Line _ _) EmptyLine) = [(1, left, 0, EmptyLine)]
numberRows [] (Row EmptyLine right@(Line _ _)) = [(0, EmptyLine, 1, right)]
numberRows [] (Row left@(Line _) EmptyLine) = [(1, left, 0, EmptyLine)]
numberRows [] (Row EmptyLine right@(Line _)) = [(0, EmptyLine, 1, right)]
numberRows [] (Row left right) = [(1, left, 1, right)]
numberRows rows@((leftCount, _, rightCount, _):_) (Row EmptyLine EmptyLine) = (leftCount, EmptyLine, rightCount, EmptyLine):rows
numberRows rows@((leftCount, _, rightCount, _):_) (Row left@(Line _ _) EmptyLine) = (leftCount + 1, left, rightCount, EmptyLine):rows
numberRows rows@((leftCount, _, rightCount, _):_) (Row EmptyLine right@(Line _ _)) = (leftCount, EmptyLine, rightCount + 1, right):rows
numberRows rows@((leftCount, _, rightCount, _):_) (Row left@(Line _) EmptyLine) = (leftCount + 1, left, rightCount, EmptyLine):rows
numberRows rows@((leftCount, _, rightCount, _):_) (Row EmptyLine right@(Line _)) = (leftCount, EmptyLine, rightCount + 1, right):rows
numberRows rows@((leftCount, _, rightCount, _):_) (Row left right) = (leftCount + 1, left, rightCount + 1, right):rows
-- | A diff with only one sides annotations.
type SplitDiff leaf annotation = Free (Annotated leaf annotation) (Term leaf annotation)
data Row = Row Line Line
deriving Eq
newtype Renderable a = Renderable (String, a)
instance Show Row where
show (Row left right) = "\n" ++ show left ++ " | " ++ show right
instance ToMarkup f => ToMarkup (Renderable (Info, Syntax a (f, Range))) where
toMarkup (Renderable (source, (Info range categories, syntax))) = classifyMarkup categories $ case syntax of
Leaf _ -> span . string $ substring range source
Indexed children -> ul . mconcat $ contentElements children
Fixed children -> ul . mconcat $ contentElements children
Keyed children -> dl . mconcat $ contentElements children
where markupForSeparatorAndChild :: ToMarkup f => ([Markup], Int) -> (f, Range) -> ([Markup], Int)
markupForSeparatorAndChild (rows, previous) child = (rows ++ [ string (substring (Range previous $ start $ snd child) source), toMarkup $ fst child ], end $ snd child)
instance ToMarkup (Int, Line, Int, Line) where
toMarkup (m, left, n, right) = tr $ toMarkup (m, left) <> toMarkup (n, right) <> string "\n"
contentElements children = let (elements, previous) = foldl markupForSeparatorAndChild ([], start range) children in
elements ++ [ string $ substring (Range previous $ end range) source ]
instance ToMarkup (Int, Line) where
toMarkup (_, EmptyLine) = numberTd "" <> toMarkup EmptyLine <> string "\n"
toMarkup (num, line@(Line True _)) = td (string $ show num) ! A.class_ (stringValue "blob-num blob-num-replacement") <> toMarkup line <> string "\n"
toMarkup (num, line@(Line _ _)) = numberTd (show num) <> toMarkup line <> string "\n"
instance ToMarkup (Renderable (Term a Info)) where
toMarkup (Renderable (source, term)) = fst $ cata (\ info@(Info range _) syntax -> (toMarkup $ Renderable (source, (info, syntax)), range)) term
numberTd :: String -> Html
numberTd "" = td mempty ! A.class_ (stringValue "blob-num blob-num-empty empty-cell")
numberTd s = td (string s) ! A.class_ (stringValue "blob-num")
instance ToMarkup (Renderable (SplitDiff a Info)) where
toMarkup (Renderable (source, diff)) = fst $ iter (\ (Annotated info@(Info range _) syntax) -> (toMarkup $ Renderable (source, (info, syntax)), range)) $ toMarkupAndRange <$> diff
where toMarkupAndRange :: Term a Info -> (Markup, Range)
toMarkupAndRange term@(Info range _ :< _) = ((div ! A.class_ (stringValue "patch")) . toMarkup $ Renderable (source, term), range)
codeTd :: Bool -> Maybe Html -> Html
codeTd _ Nothing = td mempty ! A.class_ (stringValue "blob-code blob-code-empty empty-cell")
codeTd True (Just el) = td el ! A.class_ (stringValue "blob-code blob-code-replacement")
codeTd _ (Just el) = td el ! A.class_ (stringValue "blob-code")
splitDiffByLines :: Diff a Info -> (Int, Int) -> (String, String) -> ([Row (SplitDiff a Info)], (Range, Range))
splitDiffByLines diff (prevLeft, prevRight) sources = case diff of
Free (Annotated annotation syntax) -> (splitAnnotatedByLines sources (ranges annotation) (categories annotation) syntax, ranges annotation)
Pure (Insert term) -> let (lines, range) = splitTermByLines term (snd sources) in
(Row EmptyLine . fmap Pure <$> lines, (Range prevLeft prevLeft, range))
Pure (Delete term) -> let (lines, range) = splitTermByLines term (fst sources) in
(flip Row EmptyLine . fmap Pure <$> lines, (range, Range prevRight prevRight))
Pure (Replace leftTerm rightTerm) -> let (leftLines, leftRange) = splitTermByLines leftTerm (fst sources)
(rightLines, rightRange) = splitTermByLines rightTerm (snd sources) in
(zipWithDefaults Row EmptyLine EmptyLine (fmap Pure <$> leftLines) (fmap Pure <$> rightLines), (leftRange, rightRange))
where categories (Info _ left, Info _ right) = (left, right)
ranges (Info left _, Info right _) = (left, right)
instance ToMarkup Line where
toMarkup EmptyLine = codeTd False Nothing
toMarkup (Line changed html) = codeTd changed . Just . mconcat $ toMarkup <$> html
-- | Takes a term and a source and returns a list of lines and their range within source.
splitTermByLines :: Term a Info -> String -> ([Line (Term a Info)], Range)
splitTermByLines (Info range categories :< syntax) source = flip (,) range $ case syntax of
Leaf a -> contextLines (:< Leaf a) range categories source
Indexed children -> adjoinChildLines Indexed children
Fixed children -> adjoinChildLines Fixed children
Keyed children -> adjoinChildLines Keyed children
where adjoin = reverse . foldl (adjoinLinesBy $ openTerm source) []
adjoinChildLines constructor children = let (lines, previous) = foldl (childLines $ constructor mempty) ([], start range) children in
adjoin $ lines ++ contextLines (:< constructor mempty) (Range previous $ end range) categories source
data Line =
Line Bool [HTML]
| EmptyLine
deriving Eq
childLines constructor (lines, previous) child = let (childLines, childRange) = splitTermByLines child source in
(adjoin $ lines ++ contextLines (:< constructor) (Range previous $ start childRange) categories source ++ childLines, end childRange)
unLine :: Line -> [HTML]
unLine EmptyLine = []
unLine (Line _ htmls) = htmls
splitAnnotatedByLines :: (String, String) -> (Range, Range) -> (Set.Set Category, Set.Set Category) -> Syntax a (Diff a Info) -> [Row (SplitDiff a Info)]
splitAnnotatedByLines sources ranges categories syntax = case syntax of
Leaf a -> contextRows (Leaf a) ranges categories sources
Indexed children -> adjoinChildRows Indexed children
Fixed children -> adjoinChildRows Fixed children
Keyed children -> adjoinChildRows Keyed children
where contextRows constructor ranges categories sources = zipWithDefaults Row EmptyLine EmptyLine (contextLines (Free . (`Annotated` constructor)) (fst ranges) (fst categories) (fst sources)) (contextLines (Free . (`Annotated` constructor)) (snd ranges) (snd categories) (snd sources))
isChanged :: Line -> Bool
isChanged EmptyLine = False
isChanged (Line isChanged _) = isChanged
adjoin = reverse . foldl (adjoinRowsBy (openDiff $ fst sources) (openDiff $ snd sources)) []
adjoinChildRows constructor children = let (rows, previous) = foldl (childRows $ constructor mempty) ([], starts ranges) children in
adjoin $ rows ++ contextRows (constructor mempty) (makeRanges previous (ends ranges)) categories sources
instance Show Line where
show (Line change elements) = show change ++ " [" ++ (concat . intersperse ", " $ show <$> elements) ++ "]"
show EmptyLine = "EmptyLine"
childRows constructor (rows, previous) child = let (childRows, childRanges) = splitDiffByLines child previous sources in
(adjoin $ rows ++ contextRows constructor (makeRanges previous (starts childRanges)) categories sources ++ childRows, ends childRanges)
instance Monoid Line where
mempty = EmptyLine
mappend EmptyLine EmptyLine = EmptyLine
mappend EmptyLine (Line c ys) = Line c ys
mappend (Line c xs) EmptyLine = Line c xs
mappend (Line c1 xs) (Line c2 ys) = Line (c1 || c2) (xs <> ys)
starts (left, right) = (start left, start right)
ends (left, right) = (end left, end right)
makeRanges (leftStart, rightStart) (leftEnd, rightEnd) = (Range leftStart leftEnd, Range rightStart rightEnd)
instance Monoid Row where
mempty = Row EmptyLine EmptyLine
mappend (Row x1 y1) (Row x2 y2) = Row (x1 <> x2) (y1 <> y2)
contextLines :: (Info -> a) -> Range -> Set.Set Category -> String -> [Line a]
contextLines constructor range categories source = Line . (:[]) . constructor . (`Info` categories) <$> actualLineRanges range source
diffToRows :: Diff a Info -> (Int, Int) -> String -> String -> ([Row], (Range, Range))
diffToRows (Free annotated) _ before after = annotatedToRows annotated before after
diffToRows (Pure (Insert term)) (previousIndex, _) _ after = (rowWithInsertedLine <$> afterLines, (Range previousIndex previousIndex, range))
where
(afterLines, range) = termToLines term after
rowWithInsertedLine (Line _ elements) = Row EmptyLine $ Line True [ Div (Just "insert") elements ]
rowWithInsertedLine EmptyLine = mempty
diffToRows (Pure (Delete term)) (_, previousIndex) before _ = (rowWithDeletedLine <$> lines, (range, Range previousIndex previousIndex))
where
(lines, range) = termToLines term before
rowWithDeletedLine (Line _ elements) = Row (Line True [ Div (Just "delete") elements ]) EmptyLine
rowWithDeletedLine EmptyLine = mempty
diffToRows (Pure (Replace a b)) _ before after = (replacedRows, (leftRange, rightRange))
where
replacedRows = zipWithMaybe rowFromMaybeRows (replace <$> leftElements) (replace <$> rightElements)
replace = (:[]) . Div (Just "replace") . unLine
rowFromMaybeRows :: Maybe [HTML] -> Maybe [HTML] -> Row
rowFromMaybeRows a b = Row (maybe EmptyLine (Line True) a) (maybe EmptyLine (Line True) b)
(leftElements, leftRange) = termToLines a before
(rightElements, rightRange) = termToLines b after
openRange :: String -> Range -> Maybe Range
openRange source range = case (source !!) <$> maybeLastIndex range of
Just '\n' -> Nothing
_ -> Just range
-- | Takes a term and a `source` and returns a list of HTML lines
-- | and their range within `source`.
termToLines :: Term a Info -> String -> ([Line], Range)
termToLines (Info range categories :< syntax) source = (rows syntax, range)
where
rows (Leaf _) = reverse $ foldl adjoin2Lines [] $ Line True . (:[]) <$> elements
rows (Indexed i) = rewrapLineContentsIn Ul <$> childLines i
rows (Fixed f) = rewrapLineContentsIn Ul <$> childLines f
rows (Keyed k) = rewrapLineContentsIn Dl <$> childLines k
openTerm :: String -> Term a Info -> Maybe (Term a Info)
openTerm source term@(Info range _ :< _) = const term <$> openRange source range
rewrapLineContentsIn f (Line _ elements) = Line True [ f (classify categories) elements ]
rewrapLineContentsIn _ EmptyLine = EmptyLine
lineElements r s = Line True . (:[]) <$> textElements r s
childLines i = appendRemainder $ foldl sumLines ([], start range) i
appendRemainder (lines, previous) = reverse . foldl adjoin2Lines [] $ lines ++ lineElements (Range previous (end range)) source
sumLines (lines, previous) child = (allLines, end childRange)
where
separatorLines = lineElements (Range previous $ start childRange) source
unadjoinedLines = lines ++ separatorLines ++ childLines
allLines = reverse $ foldl adjoin2Lines [] unadjoinedLines
(childLines, childRange) = termToLines child source
elements = (elementAndBreak $ Span (classify categories)) =<< actualLines (substring range source)
openDiff :: String -> SplitDiff a Info -> Maybe (SplitDiff a Info)
openDiff source diff@(Free (Annotated (Info range _) _)) = const diff <$> openRange source range
openDiff source diff@(Pure term) = const diff <$> openTerm source term
-- | Given an Annotated and before/after strings, returns a list of `Row`s representing the newline-separated diff.
annotatedToRows :: Annotated a (Info, Info) (Diff a Info) -> String -> String -> ([Row], (Range, Range))
annotatedToRows (Annotated (Info left leftCategories, Info right rightCategories) syntax) before after = (rows syntax, ranges)
where
rows (Leaf _) = zipWithMaybe rowFromMaybeRows leftElements rightElements
rows (Indexed i) = rewrapRowContentsIn Ul <$> childRows i
rows (Fixed f) = rewrapRowContentsIn Ul <$> childRows f
rows (Keyed k) = rewrapRowContentsIn Dl <$> childRows (snd <$> Map.toList k)
leftElements = (elementAndBreak $ Span (classify leftCategories)) =<< actualLines (substring left before)
rightElements = (elementAndBreak $ Span (classify rightCategories)) =<< actualLines (substring right after)
wrap _ EmptyLine = EmptyLine
wrap f (Line c elements) = Line c [ f elements ]
rewrapRowContentsIn f (Row left right) = Row (wrap (f $ classify leftCategories) left) (wrap (f $ classify rightCategories) right)
ranges = (left, right)
sources = (before, after)
childRows = appendRemainder . foldl sumRows ([], starts ranges)
appendRemainder (rows, previousIndices) = reverse . foldl adjoin2 [] $ rows ++ (contextRows (ends ranges) previousIndices sources)
sumRows (rows, previousIndices) child = (allRows, ends childRanges)
where
separatorRows = contextRows (starts childRanges) previousIndices sources
allRows = rows ++ separatorRows ++ childRows
(childRows, childRanges) = diffToRows child previousIndices before after
contextRows :: (Int, Int) -> (Int, Int) -> (String, String) -> [Row]
contextRows childIndices previousIndices sources = zipWithMaybe rowFromMaybeRows leftElements rightElements
where
leftElements = textElements (Range (fst previousIndices) (fst childIndices)) (fst sources)
rightElements = textElements (Range (snd previousIndices) (snd childIndices)) (snd sources)
elementAndBreak :: (String -> HTML) -> String -> [HTML]
elementAndBreak _ "" = []
elementAndBreak _ "\n" = [ Break ]
elementAndBreak constructor x | '\n' <- last x = [ constructor $ init x, Break ]
elementAndBreak constructor x = [ constructor x ]
textElements :: Range -> String -> [HTML]
textElements range source = (elementAndBreak Text) =<< actualLines s
where s = substring range source
starts :: (Range , Range) -> (Int, Int)
starts (left, right) = (start left, start right)
ends :: (Range, Range) -> (Int, Int)
ends (left, right) = (end left, end right)
rowFromMaybeRows :: Maybe HTML -> Maybe HTML -> Row
rowFromMaybeRows a b = Row (maybe EmptyLine (Line False . (:[])) a) (maybe EmptyLine (Line False . (:[])) b)
maybeLast :: [a] -> Maybe a
maybeLast list = listToMaybe $ reverse list
adjoin2 :: [Row] -> Row -> [Row]
adjoin2 [] row = [row]
adjoin2 rows (Row left' right') | Just _ <- openLine $ leftLines rows, Just _ <- openLine $ rightLines rows = zipWith Row lefts rights
where lefts = adjoin2Lines (leftLines rows) left'
rights = adjoin2Lines (rightLines rows) right'
adjoin2 rows (Row left' right') | Just _ <- openLine $ leftLines rows = case right' of
EmptyLine -> rest
_ -> Row EmptyLine right' : rest
where rest = zipWith Row lefts rights
lefts = adjoin2Lines (leftLines rows) left'
rights = rightLines rows
adjoin2 rows (Row left' right') | Just _ <- openLine $ rightLines rows = case left' of
EmptyLine -> rest
_ -> Row left' EmptyLine : rest
where rest = zipWith Row lefts rights
lefts = leftLines rows
rights = adjoin2Lines (rightLines rows) right'
adjoin2 rows row = row : rows
leftLines :: [Row] -> [Line]
leftLines rows = left <$> rows
where
left (Row left _) = left
rightLines :: [Row] -> [Line]
rightLines rows = right <$> rows
where
right (Row _ right) = right
openElement :: HTML -> Maybe HTML
openElement Break = Nothing
openElement (Ul _ elements) = openElement =<< maybeLast elements
openElement (Dl _ elements) = openElement =<< maybeLast elements
openElement (Div _ elements) = openElement =<< maybeLast elements
openElement h = Just h
openLine :: [Line] -> Maybe Line
openLine [] = Nothing
openLine (EmptyLine : rest) = openLine rest
openLine (line : _) = const line <$> (openElement =<< (maybeLast $ unLine line))
adjoin2Lines :: [Line] -> Line -> [Line]
adjoin2Lines [] line = [line]
adjoin2Lines (EmptyLine : xs) line | Just _ <- openLine xs = EmptyLine : adjoin2Lines xs line
adjoin2Lines (prev:rest) line | Just _ <- openLine [ prev ] = (prev <> line) : rest
adjoin2Lines lines line = line : lines
zipWithMaybe :: (Maybe a -> Maybe b -> c) -> [a] -> [b] -> [c]
zipWithMaybe f la lb = take len $ zipWith f la' lb'
where
len = max (length la) (length lb)
la' = (Just <$> la) ++ (repeat Nothing)
lb' = (Just <$> lb) ++ (repeat Nothing)
classify :: Set.Set Category -> Maybe ClassName
classify = foldr (const . Just . ("category-" ++)) Nothing
zipWithDefaults :: (a -> b -> c) -> a -> b -> [a] -> [b] -> [c]
zipWithDefaults f da db a b = take (max (length a) (length b)) $ zipWith f (a ++ repeat da) (b ++ repeat db)
actualLines :: String -> [String]
actualLines "" = [""]
actualLines lines = case break (== '\n') lines of
(l, lines') -> (case lines' of
[] -> [ l ]
_:lines' -> (l ++ "\n") : actualLines lines')
(l, lines') -> case lines' of
[] -> [ l ]
_:lines' -> (l ++ "\n") : actualLines lines'
-- | Compute the line ranges within a given range of a string.
actualLineRanges :: Range -> String -> [Range]
actualLineRanges range = drop 1 . scanl toRange (Range (start range) (start range)) . actualLines . substring range
where toRange previous string = Range (end previous) $ end previous + length string

47
test/ArbitraryTerm.hs Normal file
View File

@ -0,0 +1,47 @@
module ArbitraryTerm where
import Categorizable
import Syntax
import Term
import Control.Comonad.Cofree
import Control.Monad
import qualified OrderedMap as Map
import qualified Data.List as List
import qualified Data.Set as Set
import GHC.Generics
import Test.QuickCheck hiding (Fixed)
newtype ArbitraryTerm a annotation = ArbitraryTerm (annotation, Syntax a (ArbitraryTerm a annotation))
deriving (Show, Eq, Generic)
unTerm :: ArbitraryTerm a annotation -> Term a annotation
unTerm = unfold unpack
where unpack (ArbitraryTerm (annotation, syntax)) = (annotation, syntax)
instance (Eq a, Eq annotation, Arbitrary a, Arbitrary annotation) => Arbitrary (ArbitraryTerm a annotation) where
arbitrary = sized (\ x -> boundedTerm x x) -- first indicates the cube of the max length of lists, second indicates the cube of the max depth of the tree
where boundedTerm maxLength maxDepth = ArbitraryTerm <$> ((,) <$> arbitrary <*> boundedSyntax maxLength maxDepth)
boundedSyntax _ maxDepth | maxDepth <= 0 = liftM Leaf arbitrary
boundedSyntax maxLength maxDepth = frequency
[ (12, liftM Leaf arbitrary),
(1, liftM Indexed $ take maxLength <$> listOf (smallerTerm maxLength maxDepth)),
(1, liftM Fixed $ take maxLength <$> listOf (smallerTerm maxLength maxDepth)),
(1, liftM (Keyed . Map.fromList) $ take maxLength <$> listOf (arbitrary >>= (\x -> (,) x <$> smallerTerm maxLength maxDepth))) ]
smallerTerm maxLength maxDepth = boundedTerm (div maxLength 3) (div maxDepth 3)
shrink term@(ArbitraryTerm (annotation, syntax)) = (++) (subterms term) $ filter (/= term) $
ArbitraryTerm <$> ((,) <$> shrink annotation <*> case syntax of
Leaf a -> Leaf <$> shrink a
Indexed i -> Indexed <$> (List.subsequences i >>= recursivelyShrink)
Fixed f -> Fixed <$> (List.subsequences f >>= recursivelyShrink)
Keyed k -> Keyed . Map.fromList <$> (List.subsequences (Map.toList k) >>= recursivelyShrink))
data CategorySet = A | B | C | D deriving (Eq, Show)
instance Categorizable CategorySet where
categories A = Set.fromList [ "a" ]
categories B = Set.fromList [ "b" ]
categories C = Set.fromList [ "c" ]
categories D = Set.fromList [ "d" ]
instance Arbitrary CategorySet where
arbitrary = elements [ A, B, C, D ]

View File

@ -9,164 +9,103 @@ import Test.Hspec.QuickCheck
import Test.QuickCheck hiding (Fixed)
import Control.Comonad.Cofree
import Control.Monad.Free hiding (unfold)
import qualified Data.Maybe as Maybe
import Line
import Row
import Patch
import Syntax
import ArbitraryTerm
instance Arbitrary Row where
instance Arbitrary a => Arbitrary (Row a) where
arbitrary = oneof [
Row <$> arbitrary <*> arbitrary ]
instance Arbitrary HTML where
instance Arbitrary a => Arbitrary (Line a) where
arbitrary = oneof [
Text <$> arbitrary,
Span <$> arbitrary <*> arbitrary,
const Break <$> (arbitrary :: Gen ()) ]
instance Arbitrary Line where
arbitrary = oneof [
Line <$> arbitrary <*> arbitrary,
Line <$> arbitrary,
const EmptyLine <$> (arbitrary :: Gen ()) ]
arbitraryLeaf :: Gen (String, Info, Syntax String f)
arbitraryLeaf = toTuple <$> arbitrary
where toTuple string = (string, Info (Range 0 $ length string) mempty, Leaf string)
spec :: Spec
spec = do
describe "annotatedToRows" $ do
it "outputs one row for single-line unchanged leaves" $
annotatedToRows (unchanged "a" "leaf" (Leaf "")) "a" "a" `shouldBe` ([ Row (Line False [ span "a" ]) (Line False [ span "a" ]) ], (Range 0 1, Range 0 1))
describe "splitAnnotatedByLines" $ do
prop "outputs one row for single-line unchanged leaves" $
forAll (arbitraryLeaf `suchThat` isOnSingleLine) $
\ (source, info@(Info range categories), syntax) -> splitAnnotatedByLines (source, source) (range, range) (categories, categories) syntax `shouldBe` [
Row (Line [ Free $ Annotated info $ Leaf source ]) (Line [ Free $ Annotated info $ Leaf source ]) ]
it "outputs one row for single-line empty unchanged indexed nodes" $
annotatedToRows (unchanged "[]" "branch" (Indexed [])) "[]" "[]" `shouldBe` ([ Row (Line False [ Ul (Just "category-branch") [ Text "[]" ] ]) (Line False [ Ul (Just "category-branch") [ Text "[]" ] ]) ], (Range 0 2, Range 0 2))
prop "outputs one row for single-line empty unchanged indexed nodes" $
forAll (arbitrary `suchThat` \ s -> filter (/= '\n') s == s) $
\ source -> splitAnnotatedByLines (source, source) (totalRange source, totalRange source) (mempty, mempty) (Indexed [] :: Syntax String (Diff String Info)) `shouldBe` [
Row (Line [ Free $ Annotated (Info (totalRange source) mempty) $ Indexed [] ]) (Line [ Free $ Annotated (Info (totalRange source) mempty) $ Indexed [] ]) ]
it "outputs one row for single-line non-empty unchanged indexed nodes" $
annotatedToRows (unchanged "[ a, b ]" "branch" (Indexed [
Free . offsetAnnotated 2 2 $ unchanged "a" "leaf" (Leaf ""),
Free . offsetAnnotated 5 5 $ unchanged "b" "leaf" (Leaf "")
])) "[ a, b ]" "[ a, b ]" `shouldBe` ([ Row (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ", ", span "b", Text " ]" ] ]) (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ", ", span "b", Text " ]" ] ]) ], (Range 0 8, Range 0 8))
prop "preserves line counts in equal sources" $
\ source ->
length (splitAnnotatedByLines (source, source) (totalRange source, totalRange source) (mempty, mempty) (Indexed . fst $ foldl combineIntoLeaves ([], 0) source)) `shouldBe` length (filter (== '\n') source) + 1
it "outputs one row for single-line non-empty formatted indexed nodes" $
annotatedToRows (formatted "[ a, b ]" "[ a, b ]" "branch" (Indexed [
Free . offsetAnnotated 2 2 $ unchanged "a" "leaf" (Leaf ""),
Free . offsetAnnotated 5 6 $ unchanged "b" "leaf" (Leaf "")
])) "[ a, b ]" "[ a, b ]" `shouldBe` ([ Row (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ", ", span "b", Text " ]" ] ]) (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ", ", span "b", Text " ]" ] ]) ], (Range 0 8, Range 0 9))
prop "produces the maximum line count in inequal sources" $
\ sourceA sourceB ->
length (splitAnnotatedByLines (sourceA, sourceB) (totalRange sourceA, totalRange sourceB) (mempty, mempty) (Indexed $ zipWith (leafWithRangesInSources sourceA sourceB) (actualLineRanges (totalRange sourceA) sourceA) (actualLineRanges (totalRange sourceB) sourceB))) `shouldBe` max (length (filter (== '\n') sourceA) + 1) (length (filter (== '\n') sourceB) + 1)
it "outputs two rows for two-line non-empty unchanged indexed nodes" $
annotatedToRows (unchanged "[ a,\nb ]" "branch" (Indexed [
Free . offsetAnnotated 2 2 $ unchanged "a" "leaf" (Leaf ""),
Free . offsetAnnotated 5 5 $ unchanged "b" "leaf" (Leaf "")
])) "[ a,\nb ]" "[ a,\nb ]" `shouldBe`
([
Row (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ",", Break ] ])
(Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ",", Break] ]),
Row (Line False [ Ul (Just "category-branch") [ span "b", Text " ]" ] ])
(Line False [ Ul (Just "category-branch") [ span "b", Text " ]" ] ])
], (Range 0 8, Range 0 8))
it "outputs two rows for two-line non-empty formatted indexed nodes" $
annotatedToRows (formatted "[ a,\nb ]" "[\na,\nb ]" "branch" (Indexed [
Free . offsetAnnotated 2 2 $ unchanged "a" "leaf" (Leaf ""),
Free . offsetAnnotated 5 5 $ unchanged "b" "leaf" (Leaf "")
])) "[ a,\nb ]" "[\na,\nb ]" `shouldBe`
([
Row (Line False [ Ul (Just "category-branch") [ Text "[ ", span "a", Text ",", Break ] ])
(Line False [ Ul (Just "category-branch") [ Text "[", Break ] ]),
Row EmptyLine
(Line False [ Ul (Just "category-branch") [ span "a", Text ",", Break ] ]),
Row (Line False [ Ul (Just "category-branch") [ span "b", Text " ]" ] ])
(Line False [ Ul (Just "category-branch") [ span "b", Text " ]" ] ])
], (Range 0 8, Range 0 8))
it "" $
let (sourceA, sourceB) = ("[\na\n,\nb]", "[a,b]") in
annotatedToRows (formatted sourceA sourceB "branch" (Indexed [
Free . offsetAnnotated 2 1 $ unchanged "a" "leaf" (Leaf ""),
Free . offsetAnnotated 6 3 $ unchanged "b" "leaf" (Leaf "")
])) sourceA sourceB `shouldBe`
([
Row (Line False [ Ul (Just "category-branch") [ Text "[", Break ] ])
(Line False [ Ul (Just "category-branch") [ Text "[", span "a", Text ",", span "b", Text "]" ] ]),
Row (Line False [ Ul (Just "category-branch") [ span "a", Break ] ])
EmptyLine,
Row (Line False [ Ul (Just "category-branch") [ Text ",", Break ] ])
EmptyLine,
Row (Line False [ Ul (Just "category-branch") [ span "b", Text "]" ] ])
EmptyLine
], (Range 0 8, Range 0 5))
it "should split multi-line deletions across multiple rows" $
let (sourceA, sourceB) = ("/*\n*/\na", "a") in
annotatedToRows (formatted sourceA sourceB "branch" (Indexed [
Pure . Delete $ (Info (Range 0 5) (Set.fromList ["leaf"]) :< (Leaf "")),
Free . offsetAnnotated 6 0 $ unchanged "a" "leaf" (Leaf "")
])) sourceA sourceB `shouldBe`
([
Row (Line True [ Ul (Just "category-branch") [ Div (Just "delete") [ span "/*", Break ] ] ]) EmptyLine,
Row (Line True [ Ul (Just "category-branch") [ Div (Just "delete") [ span "*/" ], Break ] ]) EmptyLine,
Row (Line False [ Ul (Just "category-branch") [ span "a" ] ]) (Line False [ Ul (Just "category-branch") [ span "a" ] ])
], (Range 0 7, Range 0 1))
describe "unicode" $
it "equivalent precomposed and decomposed characters are not equal" $
let (sourceA, sourceB) = ("t\776", "\7831")
syntax = Leaf . Pure $ Replace (info sourceA "leaf" :< (Leaf "")) (info sourceB "leaf" :< (Leaf ""))
in
annotatedToRows (formatted sourceA sourceB "leaf" syntax) sourceA sourceB `shouldBe`
([ Row (Line False [ span "t\776" ]) (Line False [ span "\7831"]) ], (Range 0 2, Range 0 1))
describe "adjoin2" $ do
describe "adjoinRowsBy" $ do
prop "is identity on top of no rows" $
\ a -> adjoin2 [] a == [ a ]
\ a -> adjoinRowsBy openMaybe openMaybe [] a == [ a ]
prop "appends onto open rows" $
forAll ((arbitrary `suchThat` isOpen) >>= \ a -> ((,) a) <$> (arbitrary `suchThat` isOpen)) $
\ (a@(Row (Line ac1 as1) (Line bc1 bs1)), b@(Row (Line ac2 as2) (Line bc2 bs2))) ->
adjoin2 [ a ] b `shouldBe` [ Row (Line (ac1 || ac2) $ as1 ++ as2) (Line (bc1 || bc2) $ bs1 ++ bs2) ]
forAll ((arbitrary `suchThat` isOpenBy openMaybe) >>= \ a -> (,) a <$> (arbitrary `suchThat` isOpenBy openMaybe)) $
\ (a@(Row (Line a1) (Line b1)), b@(Row (Line a2) (Line b2))) ->
adjoinRowsBy openMaybe openMaybe [ a ] b `shouldBe` [ Row (Line $ a1 ++ a2) (Line $ b1 ++ b2) ]
prop "does not append onto closed rows" $
forAll ((arbitrary `suchThat` isClosed) >>= \ a -> ((,) a) <$> (arbitrary `suchThat` isClosed)) $
\ (a, b) -> adjoin2 [ a ] b `shouldBe` [ b, a ]
forAll ((arbitrary `suchThat` isClosedBy openMaybe) >>= \ a -> (,) a <$> (arbitrary `suchThat` isClosedBy openMaybe)) $
\ (a, b) -> adjoinRowsBy openMaybe openMaybe [ a ] b `shouldBe` [ b, a ]
prop "does not promote elements through empty lines onto closed lines" $
forAll ((arbitrary `suchThat` isClosed) >>= \ a -> ((,) a) <$> (arbitrary `suchThat` isClosed)) $
\ (a, b) -> adjoin2 [ Row EmptyLine EmptyLine, a ] b `shouldBe` [ b, Row EmptyLine EmptyLine, a ]
forAll ((arbitrary `suchThat` isClosedBy openMaybe) >>= \ a -> (,) a <$> (arbitrary `suchThat` isClosedBy openMaybe)) $
\ (a, b) -> adjoinRowsBy openMaybe openMaybe [ Row EmptyLine EmptyLine, a ] b `shouldBe` [ b, Row EmptyLine EmptyLine, a ]
prop "promotes elements through empty lines onto open lines" $
forAll ((arbitrary `suchThat` isOpen) >>= \ a -> ((,) a) <$> (arbitrary `suchThat` isOpen)) $
\ (a, b) -> adjoin2 [ Row EmptyLine EmptyLine, a ] b `shouldBe` Row EmptyLine EmptyLine : adjoin2 [ a ] b
forAll ((arbitrary `suchThat` isOpenBy openMaybe) >>= \ a -> (,) a <$> (arbitrary `suchThat` isOpenBy openMaybe)) $
\ (a, b) -> adjoinRowsBy openMaybe openMaybe [ Row EmptyLine EmptyLine, a ] b `shouldBe` Row EmptyLine EmptyLine : adjoinRowsBy openMaybe openMaybe [ a ] b
describe "termToLines" $ do
it "splits multi-line terms into multiple lines" $
termToLines (Info (Range 0 5) (Set.singleton "leaf") :< (Leaf "")) "/*\n*/"
`shouldBe`
([
Line True [ span "/*", Break ],
Line True [ span "*/" ]
], Range 0 5)
describe "splitTermByLines" $ do
prop "preserves line count" $
\ source -> let range = totalRange source in
splitTermByLines (Info range mempty :< Leaf source) source `shouldBe` (Line . (:[]) . (:< Leaf source) . (`Info` mempty) <$> actualLineRanges range source, range)
describe "openLine" $ do
it "should produce the earliest non-empty line in a list, if open" $
openLine [
Line True [ Div (Just "delete") [ span "*/" ] ],
Line True [ Div (Just "delete") [ span " * Debugging", Break ] ],
Line True [ Div (Just "delete") [ span "/*", Break ] ]
] `shouldBe` (Just $ Line True [ Div (Just "delete") [ span "*/" ] ])
describe "openLineBy" $ do
it "produces the earliest non-empty line in a list, if open" $
openLineBy (openTerm "\n ") [
Line [ Info (Range 1 2) mempty :< Leaf "" ],
Line [ Info (Range 0 1) mempty :< Leaf "" ]
] `shouldBe` (Just $ Line [ Info (Range 1 2) mempty :< Leaf "" ])
it "should return Nothing if the earliest non-empty line is closed" $
openLine [
Line True [ Div (Just "delete") [ span " * Debugging", Break ] ]
it "returns Nothing if the earliest non-empty line is closed" $
openLineBy (openTerm "\n") [
Line [ Info (Range 0 1) mempty :< Leaf "" ]
] `shouldBe` Nothing
describe "openTerm" $ do
it "returns Just the term if its substring does not end with a newline" $
let term = Info (Range 0 2) mempty :< Leaf "" in openTerm " " term `shouldBe` Just term
it "returns Nothing for terms whose substring ends with a newline" $
openTerm " \n" (Info (Range 0 2) mempty :< Leaf "") `shouldBe` Nothing
where
rightRowText text = rightRow [ Text text ]
rightRow xs = Row EmptyLine (Line False xs)
leftRowText text = leftRow [ Text text ]
leftRow xs = Row (Line False xs) EmptyLine
rowText a b = Row (Line False [ Text a ]) (Line False [ Text b ])
info source category = Info (totalRange source) (Set.fromList [ category ])
unchanged source category = formatted source source category
formatted source1 source2 category = Annotated (info source1 category, info source2 category)
offsetInfo by (Info (Range start end) categories) = Info (Range (start + by) (end + by)) categories
offsetAnnotated by1 by2 (Annotated (left, right) syntax) = Annotated (offsetInfo by1 left, offsetInfo by2 right) syntax
span = Span (Just "category-leaf")
isOpen (Row a b) = (maybe False (const True) $ openLine [ a ]) && (maybe False (const True) $ openLine [ b ])
isClosed (Row a@(Line _ _) b@(Line _ _)) = (maybe True (const False) $ openLine [ a ]) && (maybe True (const False) $ openLine [ b ])
isClosed (Row _ _) = False
isOpenBy f (Row a b) = Maybe.isJust (openLineBy f [ a ]) && Maybe.isJust (openLineBy f [ b ])
isClosedBy f (Row a@(Line _) b@(Line _)) = Maybe.isNothing (openLineBy f [ a ]) && Maybe.isNothing (openLineBy f [ b ])
isClosedBy _ (Row _ _) = False
isOnSingleLine (a, _, _) = filter (/= '\n') a == a
combineIntoLeaves (leaves, start) char = (leaves ++ [ Free $ Annotated (Info (Range start $ start + 1) mempty, Info (Range start $ start + 1) mempty) (Leaf [ char ]) ], start + 1)
leafWithRangesInSources sourceA sourceB rangeA rangeB = Free $ Annotated (Info rangeA mempty, Info rangeB mempty) (Leaf $ substring rangeA sourceA ++ substring rangeB sourceB)
openMaybe :: Maybe Bool -> Maybe (Maybe Bool)
openMaybe (Just a) = Just (Just a)
openMaybe Nothing = Nothing

View File

@ -5,51 +5,11 @@ import Test.Hspec.QuickCheck
import Test.QuickCheck hiding (Fixed)
import Categorizable
import qualified OrderedMap as Map
import qualified Data.List as List
import qualified Data.Set as Set
import Interpreter
import Diff
import Control.Comonad.Cofree
import Control.Monad
import GHC.Generics
import Syntax
import Term
newtype ArbitraryTerm a annotation = ArbitraryTerm (annotation, (Syntax a (ArbitraryTerm a annotation)))
deriving (Show, Eq, Generic)
unTerm :: ArbitraryTerm a annotation -> Term a annotation
unTerm = unfold unpack
where unpack (ArbitraryTerm (annotation, syntax)) = (annotation, syntax)
instance (Eq a, Eq annotation, Arbitrary a, Arbitrary annotation) => Arbitrary (ArbitraryTerm a annotation) where
arbitrary = sized (\ x -> boundedTerm x x) -- first indicates the cube of the max length of lists, second indicates the cube of the max depth of the tree
where boundedTerm maxLength maxDepth = ArbitraryTerm <$> ((,) <$> arbitrary <*> boundedSyntax maxLength maxDepth)
boundedSyntax _ maxDepth | maxDepth <= 0 = liftM Leaf arbitrary
boundedSyntax maxLength maxDepth = frequency
[ (12, liftM Leaf arbitrary),
(1, liftM Indexed $ take maxLength <$> listOf (smallerTerm maxLength maxDepth)),
(1, liftM Fixed $ take maxLength <$> listOf (smallerTerm maxLength maxDepth)),
(1, liftM (Keyed . Map.fromList) $ take maxLength <$> listOf (arbitrary >>= (\x -> ((,) x) <$> smallerTerm maxLength maxDepth))) ]
smallerTerm maxLength maxDepth = boundedTerm (div maxLength 3) (div maxDepth 3)
shrink term@(ArbitraryTerm (annotation, syntax)) = (++) (subterms term) $ filter (/= term) $
ArbitraryTerm <$> ((,) <$> shrink annotation <*> case syntax of
Leaf a -> Leaf <$> shrink a
Indexed i -> Indexed <$> (List.subsequences i >>= recursivelyShrink)
Fixed f -> Fixed <$> (List.subsequences f >>= recursivelyShrink)
Keyed k -> Keyed . Map.fromList <$> (List.subsequences (Map.toList k) >>= recursivelyShrink))
data CategorySet = A | B | C | D deriving (Eq, Show)
instance Categorizable CategorySet where
categories A = Set.fromList [ "a" ]
categories B = Set.fromList [ "b" ]
categories C = Set.fromList [ "c" ]
categories D = Set.fromList [ "d" ]
instance Arbitrary CategorySet where
arbitrary = elements [ A, B, C, D ]
import ArbitraryTerm
main :: IO ()
main = hspec spec