text-zipper-0.13: tests/WordsSpec.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module WordsSpec (spec) where
import Test.Hspec
import Test.QuickCheck
import Data.Char
import Data.Text.Zipper
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Zipper.Generic.Words
spec :: Spec
spec = do
constructorSpec
insertCharSpec
insertManySpec
moveWordLeftSpec
moveWordRightSpec
deletePrevWordSpec
deleteWordSpec
constructorSpec :: Spec
constructorSpec = describe "constructor" $ do
it "inserts only printable characters at construction time" $
(stringZipper ["abc\x1b def"] Nothing) `shouldBe` (stringZipper ["abc def"] Nothing)
insertCharSpec :: Spec
insertCharSpec = describe "insertChar" $ do
it "ignores an insert of a non-printable character" $
let z = stringZipper [] Nothing
in (insertChar '\x1b' z) `shouldBe` z
insertManySpec :: Spec
insertManySpec = describe "insertMany" $ do
it "ignores an insert of a non-printable character" $
let z = stringZipper ["abc"] Nothing
in (insertMany "ghi\x1bjkl" z) `shouldBe` (insertMany "ghijkl" z)
moveWordLeftSpec :: Spec
moveWordLeftSpec = describe "moveWordLeft" $ do
it "does nothing at the start of the text" $
moveWordLeft (zipLoc ["foo bar"] (0, 0)) `isAt` (0, 0)
it "moves from middle of the word to the start" $
moveWordLeft (zipLoc ["foo barfoo"] (0, 7)) `isAt` (0, 4)
it "moves from end to beginning" $
moveWordLeft (zipLoc ["barfoo"] (0, 6)) `isAt` (0, 0)
it "stops at beginning of line if word boundary" $
moveWordLeft (zipLoc ["foo", "bar"] (1, 2)) `isAt` (1, 0)
it "moves across lines from beginning of line" $
moveWordLeft (zipLoc ["foo", "bar"] (1, 0)) `isAt` (0, 0)
it "skips multiple space characters" $
moveWordLeft (zipLoc ["foo bar"] (0, 6)) `isAt` (0, 0)
it "skips multiple space characters across lines" $
moveWordLeft (zipLoc ["foo ", " bar"] (1, 1)) `isAt` (0, 0)
it "always lands on the start of a word" $ property $ \(textlist :: [Text]) cursor ->
isAtWordStart (moveWordLeft (zipLoc textlist cursor))
moveWordRightSpec :: Spec
moveWordRightSpec = describe "moveWordRight" $ do
it "does nothing at the end of the text" $
moveWordRight (zipLoc ["foo bar"] (0, 7)) `isAt` (0, 7)
it "moves from middle of the word to its end" $
moveWordRight (zipLoc ["barfoo foo"] (0, 2)) `isAt`(0, 6)
it "moves from beginning to end" $
moveWordRight (zipLoc ["barfoo"] (0, 0)) `isAt` (0, 6)
it "stops at end of line if word boundary" $
moveWordRight (zipLoc ["foo", "bar"] (0, 1)) `isAt` (0, 3)
it "moves across lines from end of line" $
moveWordRight (zipLoc ["foo", "bar"] (0, 3)) `isAt` (1, 3)
it "skips multiple space characters" $
moveWordRight (zipLoc ["foo bar"] (0, 4)) `isAt` (0, 10)
it "skips multiple space characters across lines" $
moveWordRight (zipLoc ["foo ", " bar"] (0, 4)) `isAt` (1, 5)
it "always lands at the end of a word" $ property $ \(textlist :: [Text]) cursor ->
isAtWordEnd (moveWordRight (zipLoc textlist cursor))
deletePrevWordSpec :: Spec
deletePrevWordSpec = describe "deletePrevWord" $ do
it "does the same cursor movement as moveWordLeft" $ property $ \(textlist :: [Text]) cursor ->
let zip = zipLoc textlist cursor
in deletePrevWord zip `isAt` (cursorPosition (moveWordLeft zip))
it "has the same prefix than moveWordLeft" $ property $ \textlist cursor ->
let zip = zipLoc textlist cursor
in deleteToEnd (deletePrevWord zip) === deleteToEnd (moveWordLeft zip)
it "has the same suffix than before" $ property $ \textlist cursor ->
let zip = zipLoc textlist cursor
in deleteToBeginning (deletePrevWord zip) === deleteToBeginning zip
deleteWordSpec :: Spec
deleteWordSpec = describe "deleteWord" $ do
it "does no cursor movement" $ property $ \textlist cursor ->
let zip = zipLoc textlist cursor
in deleteWord zip `isAt` cursorPosition zip
it "has the same prefix than before" $ property $ \textlist cursor ->
let zip = zipLoc textlist cursor
in deleteToEnd (deleteWord zip) === deleteToEnd zip
it "has the same suffix than moveWordRight" $ property $ \textlist cursor ->
let zip = zipLoc textlist cursor
in deleteToBeginning (deleteWord zip) === deleteToBeginning (moveWordRight zip)
-- Helpers
-- | Creates a zipper with initial content and cursor location
zipLoc :: [Text] -> (Int, Int) -> TextZipper Text
zipLoc content location = moveCursor location $ textZipper content Nothing
-- | Set the expectation that the given zipper is at the given cursor
-- location
isAt :: TextZipper a -> (Int, Int) -> Expectation
isAt zipper loc = cursorPosition zipper `shouldBe` loc
isAtWordEnd :: TextZipper Text -> Property
isAtWordEnd zipper = counterexample (show zipper) $
let
(row, col) = cursorPosition zipper
numLines = length (getText zipper)
curLine = currentLine zipper
in
(col == T.length curLine && row == numLines - 1)
|| ((col == T.length curLine || isSpace (T.index curLine col)) -- next is space
&& (col == 0 || not (isSpace (T.index curLine (col-1))))) -- previous is word
isAtWordStart :: TextZipper Text -> Property
isAtWordStart zipper = counterexample (show zipper) $
let
(row, col) = cursorPosition zipper
curLine = currentLine zipper
in
(row == 0 && col == 0)
|| ((col == 0 || isSpace (T.index curLine (col-1))) -- previous is space
&& (col == T.length curLine || not (isSpace (T.index curLine col)))) -- next is word
-- | Delete to the very end of a zipper
deleteToEnd :: TextZipper Text -> TextZipper Text
deleteToEnd zipper =
let
(row, _) = cursorPosition zipper
numLines = length (getText zipper)
in
if row == numLines-1 then
killToEOL zipper
else
deleteToEnd (deleteChar (killToEOL zipper))
deleteToBeginning :: TextZipper Text -> TextZipper Text
deleteToBeginning zipper = case cursorPosition zipper of
(0, _) -> killToBOL zipper
_ -> deleteToBeginning (deletePrevChar (killToBOL zipper))
instance Arbitrary Text where
arbitrary = do
ls <- lines <$> arbitrary
return $ T.pack $ case ls of
(l:_) -> l
_ -> mempty