yi-rope 0.2.0.0 → 0.2.1.11
raw patch · 4 files changed
+421/−35 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Yi.Rope: all :: (Char -> Bool) -> YiString -> Bool
+ Yi.Rope: any :: (Char -> Bool) -> YiString -> Bool
+ Yi.Rope: cons :: Char -> YiString -> YiString
+ Yi.Rope: dropWhile :: (Char -> Bool) -> YiString -> YiString
+ Yi.Rope: dropWhileEnd :: (Char -> Bool) -> YiString -> YiString
+ Yi.Rope: filter :: (Char -> Bool) -> YiString -> YiString
+ Yi.Rope: fromRope :: YiString -> FingerTree Size YiChunk
+ Yi.Rope: head :: YiString -> Maybe Char
+ Yi.Rope: instance Monoid YiString
+ Yi.Rope: instance Ord YiString
+ Yi.Rope: intercalate :: YiString -> [YiString] -> YiString
+ Yi.Rope: intersperse :: Char -> [YiString] -> YiString
+ Yi.Rope: last :: YiString -> Maybe Char
+ Yi.Rope: map :: (Char -> Char) -> YiString -> YiString
+ Yi.Rope: readFile' :: FilePath -> (Text -> Int) -> IO YiString
+ Yi.Rope: singleton :: Char -> YiString
+ Yi.Rope: snoc :: YiString -> Char -> YiString
+ Yi.Rope: split :: (Char -> Bool) -> YiString -> [YiString]
+ Yi.Rope: takeWhile :: (Char -> Bool) -> YiString -> YiString
+ Yi.Rope: takeWhileEnd :: (Char -> Bool) -> YiString -> YiString
+ Yi.Rope: unlines :: [YiString] -> YiString
+ Yi.Rope: unsafeWithText :: (Text -> Text) -> YiString -> YiString
+ Yi.Rope: unwords :: [YiString] -> YiString
+ Yi.Rope: withText :: (Text -> Text) -> YiString -> YiString
+ Yi.Rope: words :: YiString -> [YiString]
Files
- bench/MainBenchmarkSuite.hs +38/−5
- src/Yi/Rope.hs +342/−29
- test/Yi/RopeSpec.hs +39/−0
- yi-rope.cabal +2/−1
bench/MainBenchmarkSuite.hs view
@@ -5,7 +5,7 @@ import Control.DeepSeq import Criterion.Main import qualified Criterion.Main as C-import Data.Text (unlines, Text, replicate)+import Data.Text (unlines, Text, replicate, unpack) import Prelude hiding (unlines) import qualified Yi.Rope as F @@ -85,13 +85,18 @@ allTexts :: [(Int -> String, [(Int, F.YiString)])] allTexts = [longTexts {-, wideTexts, shortTexts, tinyTexts -}] +allChars :: [(Int -> String, [(Int, Char)])]+allChars = map mkChar "λa"+ where+ mkChar c = (\x -> unwords [ "char", [c], show x ], [(1, c)])+ -- | Sample usage: -- -- > mkGroup "drop" F.drop allTexts benchOnText mkGroup :: String -- ^ Group name -> f -- ^ Function being benchmarked- -> [(Int -> String, [(Int, F.YiString)])]- -> (F.YiString -> String -> f -> Benchmark)+ -> [(chsize -> String, [(chsize, input)])]+ -> (input -> String -> f -> Benchmark) -> Benchmark mkGroup n f subs r = bgroup n tests where@@ -101,6 +106,9 @@ onTextGroup :: NFData a => String -> (F.YiString -> a) -> Benchmark onTextGroup n f = mkGroup n f allTexts benchOnText +onCharGroup :: NFData a => String -> (Char -> a) -> Benchmark+onCharGroup n f = mkGroup n f allChars benchOnText+ onIntGroup :: String -> (Int -> F.YiString -> F.YiString) -> Benchmark onIntGroup n f = mkGroup n f allTexts benchTakeDrop @@ -109,14 +117,34 @@ -> Benchmark onSplitGroup n f = mkGroup n f allTexts benchSplitAt ++splitBench :: [Benchmark]+splitBench =+ [ onTextGroup "split none" (F.split (== '×'))+ , onTextGroup "split lots" (F.split (\x -> x == 'a' || x == 'o'))+ , onTextGroup "split all" (F.split (const True))+ ]++wordsBench :: [Benchmark]+wordsBench =+ -- The replicate here inflates the benchmark like mad, should be+ -- moved out.+ [ onTextGroup "unwords" (\x -> F.unwords (Prelude.replicate 100 x))+ , onTextGroup "words" F.words+ ]+ main :: IO ()-main = defaultMain+main = defaultMain $ [ onIntGroup "drop" F.drop , onIntGroup "take" F.take+ , onTextGroup "cons" (F.cons 'λ')+ , onTextGroup "snoc" (`F.snoc` 'λ')+ , onCharGroup "singleton" F.singleton , onTextGroup "countNewLines" F.countNewLines , onTextGroup "lines" F.lines , onSplitGroup "splitAt" F.splitAt , onSplitGroup "splitAtLine" F.splitAtLine+ , onTextGroup "toReverseString" F.toReverseString , onTextGroup "toReverseText" F.toReverseText , onTextGroup "toText" F.toText , onTextGroup "length" F.length@@ -125,4 +153,9 @@ , onTextGroup "empty" $ const F.empty , onTextGroup "append" (\x -> F.append x x) , onTextGroup "concat x100" $ F.concat . Prelude.replicate 100- ]+ , onTextGroup "any OK, (== '中')" $ F.any (== '中')+ , onTextGroup "any bad, (== '×')" $ F.any (== '×')+ , onTextGroup "all OK (/= '×')" $ F.all (== '×')+ , onTextGroup "all bad, (== '中')" $ F.all (== '中')+ ] ++ splitBench+ ++ wordsBench
src/Yi/Rope.hs view
@@ -1,7 +1,5 @@ {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_HADDOCK show-extensions #-} -- |@@ -32,35 +30,48 @@ Yi.Rope.toString, Yi.Rope.toReverseString, Yi.Rope.toText, Yi.Rope.toReverseText, - -- * List-like functions+ -- * Functions over content Yi.Rope.null, Yi.Rope.empty, Yi.Rope.take, Yi.Rope.drop, Yi.Rope.length, Yi.Rope.reverse, Yi.Rope.countNewLines,-- Yi.Rope.lines, Yi.Rope.lines',+ Yi.Rope.lines, Yi.Rope.lines', Yi.Rope.unlines, Yi.Rope.splitAt, Yi.Rope.splitAtLine,-+ Yi.Rope.cons, Yi.Rope.snoc, Yi.Rope.singleton,+ Yi.Rope.head, Yi.Rope.last, Yi.Rope.append, Yi.Rope.concat,+ Yi.Rope.any, Yi.Rope.all,+ Yi.Rope.dropWhile, Yi.Rope.takeWhile,+ Yi.Rope.dropWhileEnd, Yi.Rope.takeWhileEnd,+ Yi.Rope.intercalate, Yi.Rope.intersperse,+ Yi.Rope.filter, Yi.Rope.map,+ Yi.Rope.words, Yi.Rope.unwords,+ Yi.Rope.split, -- * IO- Yi.Rope.readFile, Yi.Rope.writeFile+ Yi.Rope.readFile, Yi.Rope.readFile', Yi.Rope.writeFile, + -- * Escape latches to underlying content. Note that these are safe+ -- to use but it does not mean they should.+ Yi.Rope.fromRope, Yi.Rope.withText, Yi.Rope.unsafeWithText+ ) where import Control.Applicative ((<$>)) import Control.DeepSeq import Data.Binary+import Data.Char (isSpace) import qualified Data.FingerTree as T import Data.FingerTree hiding (null, empty, reverse, split)-import qualified Data.List as L+import qualified Data.List as L (foldl') import Data.Monoid import Data.String (IsString(..)) import qualified Data.Text as TX import qualified Data.Text.IO as TF (writeFile, readFile)+import Prelude hiding (drop) -- | Used to cache the size of the strings. data Size = Indices { charIndex :: {-# UNPACK #-} !Int -- ^ How many characters under here?- , lineIndex :: {-# UNPACK #-} !Int+ , lineIndex :: Int -- ^ How many lines under here? } deriving (Eq, Show) @@ -87,18 +98,35 @@ -> YiChunk -> YiChunk overChunk f (Chunk l t) = Chunk l (f t) +countNl :: TX.Text -> Int+countNl = TX.count (TX.pack "\n")+ instance Monoid Size where mempty = Indices 0 0 Indices c l `mappend` Indices c' l' = Indices (c + c') (l + l') instance Measured Size YiChunk where- measure (Chunk l t) = Indices l (TX.count "\n" t)+ measure (Chunk l t) = Indices l (countNl t) -- | A 'YiString' is a 'FingerTree' with cached column and line counts -- over chunks of 'TX.Text'. newtype YiString = YiString { fromRope :: FingerTree Size YiChunk }- deriving (Show, Eq)+ deriving (Show) +-- | Two 'YiString's are equal if their underlying text is.+--+-- Implementation note: This just uses 'TX.Text' equality as there is+-- no real opportunity for optimisation here except for a cached+-- length check first. We could unroll the trees and mess around with+-- matching prefixes but the overhead would be higher than a simple+-- conversion and relying on GHC optimisation.+--+-- The derived Eq implementation for the underlying tree only passes+-- the equality check if the chunks are the same too which is not what+-- we want.+instance Eq YiString where+ t == t' = Yi.Rope.length t == Yi.Rope.length t' && toText t == toText t'+ instance NFData Size where rnf (Indices !c !l) = c `seq` l `seq` () @@ -108,6 +136,17 @@ instance NFData YiString where rnf = rnf . toText +instance IsString YiString where+ fromString = Yi.Rope.fromString++instance Monoid YiString where+ mempty = Yi.Rope.empty+ mappend = Yi.Rope.append+ mconcat = Yi.Rope.concat++instance Ord YiString where+ compare x y = toText x `compare` toText y+ (-|) :: YiChunk -> FingerTree Size YiChunk -> FingerTree Size YiChunk b -| t | chunkSize b == 0 = t | otherwise = b <| t@@ -150,8 +189,12 @@ toString = TX.unpack . toText -- | See 'toReverseText'.+--+-- Note that it is actually ~4.5 times faster to use 'toReverseText'+-- and unpack the result than to convert to 'String' and use+-- 'Prelude.reverse'. toReverseString :: YiString -> String-toReverseString = Prelude.reverse . toString+toReverseString = TX.unpack . toReverseText -- | This is like 'fromText' but it allows the user to specify the -- chunk size to be used. Uses 'defaultChunkSize' if the given@@ -196,9 +239,6 @@ toReverseText :: YiString -> TX.Text toReverseText = TX.reverse . toText -instance IsString YiString where- fromString = Yi.Rope.fromString- -- | Checks if the given 'YiString' is actually empty. null :: YiString -> Bool null = T.null . fromRope@@ -227,6 +267,18 @@ concat :: [YiString] -> YiString concat = L.foldl' append empty +-- | Take the first character of the underlying string if possible.+head :: YiString -> Maybe Char+head (YiString t) = case viewl t of+ EmptyL -> Nothing+ Chunk _ x :< _ -> if TX.null x then Nothing else Just (TX.head x)++-- | Take the last character of the underlying string if possible.+last :: YiString -> Maybe Char+last (YiString t) = case viewr t of+ EmptyR -> Nothing+ _ :> Chunk _ x -> if TX.null x then Nothing else Just (TX.last x)+ -- | Splits the string at given character position. -- -- If @position <= 0@ then the left string is empty and the right string@@ -243,17 +295,19 @@ -- chunk of the right side of the split. We do precisely that. -- -- All together, this split is only as expensive as underlying--- 'T.split', the cost of splitting a chunk into two and the cost--- consing and snocing one chunk to each string. As the chunks are--- short, the split fairly cheap and cons/snoc constant time, this--- turns out pretty fast all together.+-- 'T.split', the cost of splitting a chunk into two, the cost of one+-- cons and one cons of a chunk and lastly the cost of 'T.splitAt' of+-- the underlying 'TX.Text'. It turns out to be fairly fast all+-- together. splitAt :: Int -> YiString -> (YiString, YiString)-splitAt n (YiString t) = case viewl s of- Chunk _ x :< ts | n' /= 0 ->- let (lx, rx) = TX.splitAt n' x- in (YiString $ f |> mkChunk TX.length lx,- YiString $ mkChunk TX.length rx -| ts)- _ -> (YiString f, YiString s)+splitAt n (YiString t)+ | n <= 0 = (mempty, YiString t)+ | otherwise = case viewl s of+ Chunk l x :< ts | n' /= 0 ->+ let (lx, rx) = TX.splitAt n' x+ in (YiString $ f |> Chunk n' lx,+ YiString $ Chunk (l - n') rx -| ts)+ _ -> (YiString f, YiString s) where (f, s) = T.split ((> n) . charIndex) t n' = n - charIndex (measure f)@@ -266,6 +320,138 @@ drop :: Int -> YiString -> YiString drop n = snd . Yi.Rope.splitAt n +-- | The usual 'Prelude.dropWhile' optimised for 'YiString's.+dropWhile :: (Char -> Bool) -> YiString -> YiString+dropWhile p = YiString . go . fromRope+ where+ go t = case viewl t of+ EmptyL -> T.empty+ Chunk l x :< ts ->+ let r = TX.dropWhile p x+ l' = TX.length r+ in case compare l' l of+ -- We dropped nothing so we must be done.+ EQ -> t+ -- We dropped something, if it was everything then drop from+ -- next chunk.+ LT | TX.null r -> go ts+ -- It wasn't everything and we have left-overs, we must be done.+ | otherwise -> Chunk l' r <| ts+ -- We shouldn't really get here or it would mean that+ -- dropping stuff resulted in more content than we had. This+ -- can only happen if unsafe functions don't preserve the+ -- chunk size and it goes out of sync with the text length.+ -- Preserve this abomination, it may be useful for+ -- debugging.+ _ -> Chunk l' r -| ts++-- | As 'Yi.Rope.dropWhile' but drops from the end instead.+dropWhileEnd :: (Char -> Bool) -> YiString -> YiString+dropWhileEnd p = YiString . go . fromRope+ where+ go t = case viewr t of+ EmptyR -> T.empty+ ts :> Chunk l x ->+ let r = TX.dropWhileEnd p x+ l' = TX.length r+ in case compare l' l of+ EQ -> t+ LT | TX.null r -> go ts+ | otherwise -> ts |> Chunk l' r+ _ -> ts |- Chunk l' r++-- | The usual 'Prelude.takeWhile' optimised for 'YiString's.+takeWhile :: (Char -> Bool) -> YiString -> YiString+takeWhile p = YiString . go . fromRope+ where+ go t = case viewl t of+ EmptyL -> T.empty+ Chunk l x :< ts ->+ let r = TX.takeWhile p x+ l' = TX.length r+ in case compare l' l of+ -- We took the whole chunk, keep taking more.+ EQ -> Chunk l x <| go ts+ -- We took some stuff but not everything so we're done.+ -- Alternatively, we took more than the size chunk so+ -- preserve this wonder. This should only ever happen if you+ -- use unsafe functions and Chunk size goes out of sync with+ -- actual text length.+ _ -> Chunk l' r <| ts++-- | Like 'Yi.Rope.takeWhile' but takes from the end instead.+takeWhileEnd :: (Char -> Bool) -> YiString -> YiString+takeWhileEnd p = YiString . go . fromRope+ where+ go t = case viewr t of+ EmptyR -> T.empty+ ts :> Chunk l x -> case compare l' l of+ EQ -> go ts |> Chunk l x+ _ -> ts |> Chunk l' r+ where+ -- no TX.takeWhileEnd – https://github.com/bos/text/issues/89+ r = TX.reverse . TX.takeWhile p . TX.reverse $ x+ l' = TX.length r++-- | Concatenates the list of 'YiString's after inserting the+-- user-provided 'YiString' between the elements.+--+-- Empty 'YiString's are not ignored and will end up as strings of+-- length 1. If you don't want this, it's up to you to pre-process the+-- list. Just as with 'Yi.Rope.intersperse', it is up to the user to+-- pre-process the list.+intercalate :: YiString -> [YiString] -> YiString+intercalate _ [] = mempty+intercalate (YiString t') ts = YiString $ t' >< go ts+ where+ go [] = mempty+ go (YiString t : ts') = t >< t' >< go ts'++-- | Intersperses the given character between the 'YiString's. This is+-- useful when you have a bunch of strings you just want to separate+-- something with, comma or a dash. Note that it only inserts the+-- character between the elements.+--+-- What's more, the result is a single 'YiString'. You can easily+-- achieve a version that blindly inserts elements to the back by+-- mapping over the list instead of using this function.+--+-- You can think of it as a specialised version of+-- 'Yi.Rope.intercalate'. Note that what this does __not__ do is+-- intersperse characters into the underlying text, you should convert+-- and use 'TX.intersperse' for that instead.+intersperse :: Char -> [YiString] -> YiString+intersperse _ [] = mempty+intersperse c (t:ts) = t <> go ts+ where+ go [] = mempty+ go (t':ts') = (c `cons` t') <> go ts'++-- | Add a 'Char' in front of a 'YiString'.+--+-- We add the character to the front of the first chunk. This does+-- mean that a lot of 'cons' might result in an abnormally large first+-- chunk so if you have to do that, consider using 'append' instead..+cons :: Char -> YiString -> YiString+cons c (YiString t) = case viewl t of+ EmptyL -> Yi.Rope.singleton c+ Chunk !l x :< ts -> YiString $ Chunk (l + 1) (c `TX.cons` x) <| ts++-- | Add a 'Char' in the back of a 'YiString'.+--+-- We add the character to the end of the last chunk. This does mean+-- that a lot of 'snoc' might result in an abnormally large last chunk+-- so if you have to do that, consider using 'append' instead..+snoc :: YiString -> Char -> YiString+snoc (YiString t) c = case viewr t of+ EmptyR -> Yi.Rope.singleton c+ ts :> Chunk l x -> YiString $ ts |> Chunk (l + 1) (x `TX.snoc` c)++-- | Single character 'YiString'. Consider whether it's worth creating+-- this, maybe you can use 'cons' or 'snoc' instead?+singleton :: Char -> YiString+singleton c = YiString . T.singleton $ Chunk 1 (TX.singleton c)+ -- | Splits the underlying string before the given line number. -- Zero-indexed lines. --@@ -300,7 +486,7 @@ cutExcess :: Int -> TX.Text -> (TX.Text, TX.Text) cutExcess n t = case TX.length t of 0 -> (TX.empty, TX.empty)- _ -> let ns = TX.count "\n" t+ _ -> let ns = countNl t ls = TX.lines t front = TX.unlines $ Prelude.take (ns - n) ls back = TX.drop (TX.length front) t@@ -320,16 +506,16 @@ -- result back together which was inconsistent with the rest of the -- interface which worked with number of newlines. lines :: YiString -> [YiString]-lines = map dropNl . lines'+lines = Prelude.map dropNl . lines' where- dropNl (YiString t) = case viewr t of+ dropNl (YiString t) = case viewr t of+ EmptyR -> Yi.Rope.empty ts :> ch@(Chunk l tx) -> YiString $ ts |- if TX.null tx then ch else case TX.last tx of '\n' -> Chunk (l - 1) (TX.init tx) _ -> ch- EmptyR -> YiString T.empty -- | Splits the 'YiString' into a list of 'YiString' each containing a -- line.@@ -352,14 +538,141 @@ then if T.null f then [] else [YiString f] else YiString f : lines' (YiString s) +-- | Joins up lines by a newline character. It does not leave a+-- newline after the last line. If you want a more classical+-- 'Prelude.unlines' behaviour, use 'Yi.Rope.map' along with+-- 'Yi.Rope.snoc'.+unlines :: [YiString] -> YiString+unlines = Yi.Rope.intersperse '\n'++-- | 'YiString' specialised @any@.+--+-- Implementation note: this currently just does any by doing ‘TX.Text’+-- conversions upon consecutive chunks. We should be able to speed it+-- up by running it in parallel over multiple chunks.+any :: (Char -> Bool) -> YiString -> Bool+any p = go . fromRope+ where+ go x = case viewl x of+ EmptyL -> False+ Chunk _ t :< ts -> TX.any p t || go ts++-- | 'YiString' specialised @all@.+--+-- See the implementation note for 'Yi.Rope.any'.+all :: (Char -> Bool) -> YiString -> Bool+all p = go . fromRope+ where+ go x = case viewl x of+ EmptyL -> False+ Chunk _ t :< ts -> TX.all p t || go ts+ -- | To serialise a 'YiString', we turn it into a regular 'String' -- first. instance Binary YiString where put = put . toString get = Yi.Rope.fromString <$> get +-- | Write a 'YiString' into the given file. It's up to the user to+-- handle exceptions. writeFile :: FilePath -> YiString -> IO () writeFile f = TF.writeFile f . toText +-- | Reads file into the rope, using 'fromText'. It's up to the user+-- to handle exceptions. readFile :: FilePath -> IO YiString readFile f = fromText <$> TF.readFile f++-- | A version of 'readFile' which allows for arbitrary chunk size to+-- start with.+--+-- For example, @readFile' foo ((/ 2) . 'TX.length')@ would produce+-- chunks that are half the size of the read in text: whether that's a+-- good idea depends on situation.+--+-- Note that if this number ends up as @< 1@, 'defaultChunkSize' will+-- be used instead.+--+-- It's up to the user to handle exceptions.+readFile' :: FilePath -> (TX.Text -> Int) -> IO YiString+readFile' f l = do+ c <- TF.readFile f+ let l' = case l c of+ x | x < 1 -> defaultChunkSize+ | otherwise -> x+ return $ fromText' l' c++-- | Filters the characters from the underlying string.+--+-- >>> filter (/= 'a') "bac"+-- "bc"+filter :: (Char -> Bool) -> YiString -> YiString+filter p = YiString . go . fromRope+ where+ go t = case viewl t of+ EmptyL -> T.empty+ Chunk _ x :< ts -> mkChunk TX.length (TX.filter p x) -| go ts++-- | Maps the characters over the underlying string.+map :: (Char -> Char) -> YiString -> YiString+map f = YiString . go . fromRope+ where+ go t = case viewl t of+ EmptyL -> T.empty+ Chunk l x :< ts -> Chunk l (TX.map f x) <| go ts++-- | Join given 'YiString's with a space. Empty lines will be filtered+-- out first.+unwords :: [YiString] -> YiString+unwords = Yi.Rope.intersperse ' '++-- | Splits the given 'YiString' into a list of words, where spaces+-- are determined by 'isSpace'. No empty strings are in the result+-- list.+words :: YiString -> [YiString]+words = Prelude.filter (not . Yi.Rope.null) . Yi.Rope.split isSpace++-- | Splits the 'YiString' on characters matching the predicate, like+-- 'TX.split'.+--+-- For splitting on newlines use 'Yi.Rope.lines' or 'Yi.Rope.lines''+-- instead.+--+-- Implementation note: GHC actually makes this naive implementation+-- about as fast and in cases with lots of splits, faster, as a+-- hand-rolled version on chunks with appends which is quite amazing+-- in itself.+split :: (Char -> Bool) -> YiString -> [YiString]+split p = fmap fromText . TX.split p . toText++-- | Helper function doing conversions of to and from underlying+-- 'TX.Text'. You should aim to implement everything in terms of+-- 'YiString' instead.+--+-- Please note that this maps over each __chunk__ so this can only be+-- used with layout-agnostic functions. For example+--+-- >>> let t = 'fromString' "abc" <> 'fromString' "def"+-- >>> 'toString' $ 'withText' 'TX.reverse' t+-- "cbafed"+--+-- Probably doesn't do what you wanted, but 'TX.toUpper' would.+-- Specifically, for any @f : 'TX.Text' → 'TX.Text'@, 'withText' will+-- only do the ‘expected’ thing iff+--+-- @f x <> f y ≡ f (x <> y)@+--+-- which should look very familiar.+withText :: (TX.Text -> TX.Text) -> YiString -> YiString+withText f = YiString . T.fmap' (mkChunk TX.length . f . _fromChunk) . fromRope++-- | Maps over each __chunk__ which means this function is UNSAFE! If+-- you use this with functions which don't preserve 'Size', that is+-- the chunk length and number of newlines, things will break really,+-- really badly. You should not need to use this.+--+-- Also see 'T.unsafeFmap'+unsafeWithText :: (TX.Text -> TX.Text) -> YiString -> YiString+unsafeWithText f = YiString . T.unsafeFmap g . fromRope+ where+ g (Chunk l t) = Chunk l (f t)
test/Yi/RopeSpec.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} module Yi.RopeSpec (main, spec) where +import Control.Applicative+import Data.Char (isUpper, toUpper, isSpace) import qualified Data.Text as T import Test.Hspec import Test.Hspec.QuickCheck@@ -45,3 +47,40 @@ R.append (R.fromText t) `isLikeT` T.append t prop "R.concat ~ T.concat" $ \s -> (R.toText . R.concat . map R.fromText) s `shouldBe` T.concat s+ prop "R.head ~ T.head" $ R.head `isLike` (\x -> if T.null x+ then Nothing+ else Just (T.head x))+ prop "R.last ~ T.last" $ R.last `isLike` (\x -> if T.null x+ then Nothing+ else Just (T.last x))+ prop "R.cons ~ T.cons" $ \c -> R.cons c `isLikeT` T.cons c+ prop "R.snoc ~ T.snoc" $ \c -> R.cons c `isLikeT` T.cons c+ prop "R.singleton ~ T.singleton" $+ \c -> (R.toText . R.singleton) c `shouldBe` T.singleton c+ prop "\\p -> R.any p ~ T.any p $ const True" $ \t ->+ R.any (const True) (R.fromText t) `shouldBe` T.any (const True) t+ prop "\\p -> R.any p ~ T.any p $ const False" $ \t ->+ R.any (const False) (R.fromText t) `shouldBe` T.any (const False) t+ prop "\\f -> R.withText ~ f $ T.toTitle" $+ R.withText T.toTitle `isLikeT` T.toTitle+ prop "\\p -> R.dropWhile p ~ T.dropWhile p $ isUpper" $+ R.dropWhile isUpper `isLikeT` T.dropWhile isUpper+ prop "\\p -> R.takeWhile p ~ T.takeWhile p $ isUpper" $+ R.takeWhile isUpper `isLikeT` T.takeWhile isUpper+ prop "R.compare ~ T.compare" $ \t t' ->+ compare (R.fromText t) (R.fromText t') `shouldBe` compare t t'+ prop "\\p -> R.filter p ~ T.filter p $ isUpper" $+ R.filter isUpper `isLikeT` T.filter isUpper+ prop "\\f -> R.map f ~ T.map f $ toUpper" $+ R.map toUpper `isLikeT` T.map toUpper+ prop "\\p -> R.dropWhileEnd p ~ T.dropWhileEnd p $ isSpace" $+ R.dropWhileEnd isSpace `isLikeT` T.dropWhileEnd isSpace+ prop "\\p -> R.takeWhileEnd p ~ rev . T.takeWhile p . rev $ isSpace" $+ R.takeWhileEnd isSpace+ `isLikeT` T.reverse . T.takeWhile isSpace . T.reverse+ prop "R.words ~ T.words" $ \t ->+ R.toText <$> R.words (R.fromText t) `shouldBe` T.words t+ prop "R.unwords ~ T.unwords" $ \t ->+ R.toText (R.unwords (R.fromText <$> t)) `shouldBe` T.unwords t+ prop "\\p -> R.split p ~ T.split p $ isUpper" $ \t ->+ R.toText <$> R.split isUpper (R.fromText t) `shouldBe` T.split isUpper t
yi-rope.cabal view
@@ -1,5 +1,5 @@ name: yi-rope-version: 0.2.0.0+version: 0.2.1.11 synopsis: A rope data structure used by Yi description: A rope data structure used by Yi license: GPL-3@@ -11,6 +11,7 @@ cabal-version: >=1.10 library+ ghc-options: -fpedantic-bottoms -flate-dmd-anal -fexpose-all-unfoldings -Wall -O2 exposed-modules: Yi.Rope