packages feed

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 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