text-rope-zipper-0.1.0.0: test/Data/Text/Rope/ZipperSpec.hs
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Data.Text.Rope.ZipperSpec where
import Data.Text qualified as Text
import Data.Text.Lazy.Zipper qualified as TextZipper
import Data.Text.Rope qualified as Rope
import Data.Text.Rope.Zipper
import Data.Text.Rope.Zipper qualified as RopeZipper
import Util
rawOutput :: (HasCallStack) => Property -> IO ()
rawOutput =
quickCheckWithResult stdArgs >=> \case
Success{} -> pure ()
Failure{output} -> expectationFailure output
GaveUp{output} -> expectationFailure output
NoExpectedFailure{output} -> expectationFailure output
data Move
= Backward
| Forward
| Up
| Down
| LineStart
| LineEnd
| FirstLine
| LastLine
| Rel !(Int, Int)
| Abs !Position
deriving stock (Eq, Show)
instance Arbitrary Position where
arbitrary = do
posLine <- arbitrary
posColumn <- arbitrary
pure Position{..}
instance Arbitrary Move where
arbitrary =
oneof
[ pure Backward
, pure Forward
, pure LineStart
, pure LineEnd
, Rel <$> arbitrary
, Abs <$> arbitrary
]
newtype VerticalMove = VerticalMove Move
deriving newtype (Eq, Show)
instance Arbitrary VerticalMove where
arbitrary = VerticalMove <$> elements [Up, Down, FirstLine, LastLine]
moveZipper :: Move -> RopeZipper -> RopeZipper
moveZipper Forward = moveForward
moveZipper Backward = moveBackward
moveZipper Up = moveUp
moveZipper Down = moveDown
moveZipper LineStart = moveToLineStart
moveZipper LineEnd = moveToLineEnd
moveZipper FirstLine = moveToFirstLine
moveZipper LastLine = moveToLastLine
moveZipper (Rel (dy, dx)) = moveCursor $ \Position{..} ->
Position
{ posLine = boundedAdd dy posLine
, posColumn = boundedAdd dx posColumn
}
moveZipper (Abs c) = setCursor c
positionToPair :: Position -> (Int, Int)
positionToPair Position{..} = (fromIntegral posLine, fromIntegral posColumn)
pairToPosition :: (Int, Int) -> Position
pairToPosition (y, x) =
Position
{ posLine = fromIntegral $ max 0 y
, posColumn = fromIntegral $ max 0 x
}
spec :: Spec
spec = parallel $ modifyMaxSuccess (* 100) do
it "fromText is a right inverse of toText" $ property $ \t -> do
toText (fromText t) `shouldBe` t
it "fromRope is a right inverse of toRope" $ property $ \r -> do
toRope (fromRope r) `shouldBe` r
it "concatenates" $ property $ \r1 r2 -> do
fromRope r1 <> fromRope r2 `shouldBe` fromParts r1 r2
toRope (fromParts r1 r2) `shouldBe` r1 <> r2
it "concatenates newline" $ property $ \zipper -> do
splitFirstLine "\n" `shouldBe` ("\n", "")
toRope (zipper <> "\n") `shouldBe` toRope zipper <> "\n"
it "inserts" $ property $ \r1 r2 r3 -> do
insertRope r2 (fromParts r1 r3) `shouldBe` fromParts (r1 <> r2) r3
it "deletes before" $ property $ \r1 (Text.singleton -> Rope.fromText -> c) r2 ->
deleteBefore (fromParts (r1 <> c) r2) `shouldBe` fromParts r1 r2
it "deletes after" $ property $ \r1 (Text.singleton -> Rope.fromText -> c) r2 ->
deleteAfter (fromParts r1 (c <> r2)) `shouldBe` fromParts r1 r2
it "moves" $ property $ \t1 t2 move -> do
let zipper = fromParts (Rope.fromText t1) (Rope.fromText t2)
cursor =
Position
{ posLine = fromIntegral $ Text.count "\n" t1
, posColumn = fromIntegral . Text.length . snd $ Text.breakOnEnd "\n" t1
}
zipper.cursor `shouldBe` cursor
let t = t1 <> t2
numLines = Text.count "\n" t + 1
clampRow = clamp (0, numLines - 1)
lineLength y = maybe 0 Text.length $ Text.lines t !? y
clampCol (clampRow -> y, x) = (y, clamp (0, lineLength y) x)
moveCursor' (positionToPair -> (y, x)) = pairToPosition . clampCol $ case move of
Backward -> (y, x - 1)
Forward -> (y, x + 1)
Up -> (y - 1, x)
Down -> (y + 1, x)
LineStart -> (y, minBound)
LineEnd -> (y, maxBound)
FirstLine -> (minBound, x)
LastLine -> (maxBound, x)
Rel (dy, dx) -> (y + dy, x + dx)
Abs pos -> positionToPair pos
let zipper' = moveZipper move zipper
zipper'.cursor `shouldBe` moveCursor' zipper.cursor
toRope zipper `shouldBe` toRope zipper'
it "counts lines correctly" $ property $ \zipper -> do
lengthInLines zipper
`shouldBe` (fromIntegral . length . RopeZipper.lines) zipper
it "sticks the last requested column" $ property $ \zipper (VerticalMove move) -> do
let zipper' = moveZipper move zipper
let lineLen' =
fromIntegral
$ TextZipper.length zipper'.currentLine
- if TextZipper.hasTrailingNewline zipper'.currentLine then 1 else 0
let expectedColumn = min zipper.cursor.posColumn lineLen'
zipper'.cursor.posColumn `shouldBe` expectedColumn
moveCursor (\p -> p{posLine = zipper.cursor.posLine}) zipper' `shouldBe` zipper