diff --git a/src/Yi/Braid.hs b/src/Yi/Braid.hs new file mode 100644 index 0000000..822326f --- /dev/null +++ b/src/Yi/Braid.hs @@ -0,0 +1,580 @@ +{-# language BangPatterns #-} +{-# language DeriveDataTypeable #-} +{-# language LambdaCase #-} +{-# language MultiParamTypeClasses #-} +{-# language OverloadedStrings #-} +{-# language ScopedTypeVariables #-} +{-# language ViewPatterns #-} +{-# language FlexibleContexts #-} +{-# language UndecidableInstances #-} +{-# language ConstraintKinds #-} +{-# options_haddock show-extensions #-} + +-- | +-- Module : Yi.Braid +-- License : GPL-2 +-- Maintainer : yi-devel@googlegroups.com +-- Stability : experimental +-- Portability : portable +-- +-- This module defines a @Braid@ data structure. This is distinct from a +-- traditional @Rope@ in that it abstracts over the underlying segmentable +-- type. It uses FingerTree's for efficiency and depends on the underlying +-- type's own definitions of common segmentation operations. + +module Yi.Braid + ( Braid(..) + , HasSize(..) + , (Yi.Braid.-|) + , (Yi.Braid.|-) + , Yi.Braid.reverse + , Yi.Braid.toReversed + , Yi.Braid.toBraid + , Yi.Braid.toBraid' + , Yi.Braid.extractBraid + , Yi.Braid.null + , Yi.Braid.empty + , Yi.Braid.length + , Yi.Braid.append + , Yi.Braid.concat + , Yi.Braid.head + , Yi.Braid.last + , Yi.Braid.init + , Yi.Braid.tail + , Yi.Braid.splitAt + , Yi.Braid.take + , Yi.Braid.drop + , Yi.Braid.dropWhile + , Yi.Braid.dropWhileEnd + , Yi.Braid.takeWhile + , Yi.Braid.takeWhileEnd + , Yi.Braid.span + , Yi.Braid.break + , Yi.Braid.intercalate + , Yi.Braid.intersperse + , Yi.Braid.cons + , Yi.Braid.snoc + , Yi.Braid.singleton + , Yi.Braid.any + , Yi.Braid.all + , Yi.Braid.filter + , Yi.Braid.map + , Yi.Braid.split + , Yi.Braid.fmap' + , Yi.Braid.unsafeWithChunk + , Yi.Braid.foldl' + , Yi.Braid.replicate + , Yi.Braid.replicateSegment + ) where + +import Control.DeepSeq +import qualified Data.FingerTree as T +import Data.FingerTree hiding (null, empty, reverse, split) +import qualified Data.List as L (foldl') +import Data.Maybe +import Data.Monoid +import Data.Typeable + +import qualified Data.ListLike as LL +import qualified Yi.Rope.Internal.ListLikeHelpers as LL + +-- | A @'Braid' v a s@ is a 'FingerTree' over some underlying type @a@ with +-- a 'measure' of @v@. The underlying type @a@ is referred to a @chain@ which +-- can be split into smaller segments of type @s@. +newtype Braid v a = Braid { fromBraid :: T.FingerTree v a } + deriving (Show, Typeable) + +-- | @ValidBraid :: * -> * -> Constraint@ +-- +-- ValidBraid is a constraint which ensures that the values used in the Braid +-- are valid for use with FingerTrees and can be segmented. +type ValidBraid v s a = (T.Measured v a, HasSize v, LL.ListLike a s) + +instance (ValidBraid v s a) => Monoid (Braid v a) where + mempty = Yi.Braid.empty + mappend = Yi.Braid.append + mconcat = Yi.Braid.concat + +class HasSize a where + getSize :: a -> Int + + +-- | Two 'Braid's are equal if their underlying content is. +-- +-- Implementation note: This uses 'extractBraid' and relies on the chain's +-- underlying equality check, thus it's relatively inefficient and should +-- be avoided if possible. 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 (ValidBraid v s a, Eq a) => Eq (Braid v a) where + t == t' = Yi.Braid.length t == Yi.Braid.length t' && extractBraid t == extractBraid t' + +-- | Ord is checked by using 'extractBraid' and using the underlying chain's +-- Ord instance. +instance (Eq a, Ord a, ValidBraid v s a) => Ord (Braid v a) where + compare x y = extractBraid x `compare` extractBraid y + +instance (NFData a, ValidBraid v s a) => NFData (Braid v a) where + rnf = rnf . extractBraid + +-- | Prepend a chain onto a 'FingerTree' +{-# INLINABLE (-|) #-} +(-|) :: (ValidBraid v s a) => a -> FingerTree v a -> FingerTree v a +b -| t | LL.null b = t + | otherwise = b <| t + +-- | Append a chain onto a 'FingerTree' +{-# INLINABLE (|-) #-} +(|-) :: (ValidBraid v s a) => FingerTree v a -> a -> FingerTree v a +t |- b | LL.null b = t + | otherwise = t |> b + +-- | Default size chunk to use. Currently @1200@ as this is what +-- benchmarks suggest. +-- +-- For 'YiString' and similar chain types +-- this makes the biggest difference with 'lines'-like and +-- 'concat'-like functions. Bigger chunks make 'concat' (much) faster +-- but 'lines' slower. In general it seems that we benefit more from +-- larger chunks and 1200 seems to be the sweet spot. +defaultChunkSize :: Int +defaultChunkSize = 1200 + +-- | Reverse the whole underlying chain +-- +-- This involves reversing the order of the chunks as well as content +-- of the chunks. We use a little optimisation here that re-uses the +-- content of each chunk but this exposes a potential problem: after +-- many transformations, our chunks size might become quite varied +-- (but never more than the default size), perhaps we should +-- periodically rechunk the tree to recover nice sizes? +{-# INLINABLE reverse #-} +reverse :: (ValidBraid v s a) => Braid v a -> Braid v a +reverse = Braid . T.fmap' LL.reverse . T.reverse . fromBraid + +-- | This is like 'toBraid' but it allows the user to specify the +-- chunk size to be used. Uses 'defaultChunkSize' if the given +-- size is <= 0. +{-# INLINABLE toBraid' #-} +toBraid' :: forall v a s. (ValidBraid v s a) => Int -> a -> Braid v a +toBraid' n | n <= 0 = toBraid' defaultChunkSize + | otherwise = Braid . T.fromList . LL.chunksOf n + +-- | Converts a chain of arbitrary type into a 'Braid' using +-- 'defaultChunkSize'-sized chunks for the underlying tree. +{-# INLINABLE toBraid #-} +toBraid :: (ValidBraid v s a) => a -> Braid v a +toBraid = toBraid' defaultChunkSize + +-- | Extracts the entire underlying chain by deconstructing the tree. +-- Consider whether you really need to use this, it's very inefficient. +{-# INLINABLE extractBraid #-} +extractBraid :: forall v a s. (ValidBraid v s a) => Braid v a -> a +extractBraid = LL.concat . go . fromBraid + where + go :: FingerTree v a -> [a] + go t = case viewl t of + (!c :< cs) -> c : go cs + EmptyL -> [] + +-- | Spits out the underlying chain, reversed. +-- +-- Note that this is actually slightly faster than manually unrolling +-- the tree from the end, reversing each chunk and +-- concating, at least with -O2 which you really should be using anyway. +{-# INLINABLE toReversed #-} +toReversed :: (ValidBraid v s a) => Braid v a -> a +toReversed = LL.reverse . extractBraid + +-- | Checks if the given 'Braid' is empty. +{-# INLINABLE null #-} +null :: (ValidBraid v s a) => Braid v a -> Bool +null = T.null . fromBraid + +-- | Creates an empty 'Braid' +{-# INLINABLE empty #-} +empty :: (ValidBraid v s a) => Braid v a +empty = Braid T.empty + +-- | Length of the whole underlying chain. +-- +-- Amortized constant time. +{-# INLINABLE length #-} +length :: (ValidBraid v s a) => Braid v a -> Int +length = getSize . measure . fromBraid + +-- | Append two 'Braid's. +-- +-- We take the extra time to optimise this append for many small +-- insertions. With naive append of the inner fingertree with 'T.><', +-- it is often the case that we end up with a large collection of tiny +-- chunks. This function instead tries to join the underlying trees at +-- outermost chunks up to 'defaultChunkSize' which while slower, +-- should improve memory usage. +-- +-- I suspect that this pays for itself as we'd spend more time +-- computing over all the little chunks than few large ones anyway. +{-# INLINABLE append #-} +append :: (ValidBraid v s a) => Braid v a -> Braid v a -> Braid v a +append (Braid t) (Braid t') = case (viewr t, viewl t') of + (EmptyR, _) -> Braid t' + (_, EmptyL) -> Braid t + (ts :> x, x' :< ts') -> + let len = LL.length x + LL.length x' + in case compare len defaultChunkSize of + GT -> Braid (t <> t') + _ -> Braid (ts |- (x <> x') <> ts') + +-- | Concat a list of 'Braid's. +{-# INLINABLE concat #-} +concat :: (ValidBraid v s a) => [Braid v a] -> Braid v a +concat = L.foldl' append empty + +-- | Take the first segment of the underlying chain if possible. +{-# INLINABLE head #-} +head :: (ValidBraid v s a) => Braid v a -> Maybe s +head (Braid t) = case viewl t of + EmptyL -> Nothing + x :< _ -> if LL.null x then Nothing else Just (LL.head x) + +-- | Take the last segment of the underlying chain if possible. +{-# INLINABLE last #-} +last :: (ValidBraid v s a) => Braid v a -> Maybe s +last (Braid t) = case viewr t of + EmptyR -> Nothing + _ :> x -> if LL.null x then Nothing else Just (LL.last x) + +-- | Takes every segment but the last one: returns Nothing on empty +-- string. +{-# INLINABLE init #-} +init :: (ValidBraid v s a) => Braid v a -> Maybe (Braid v a) +init (Braid t) = case viewr t of + EmptyR -> Nothing + ts :> (LL.null -> True) -> Yi.Braid.init (Braid ts) + ts :> x -> Just . Braid $ ts |- (LL.init x) + +-- | Takes the tail of the underlying chain. If the string is empty +-- to begin with, returns Nothing. +{-# INLINABLE tail #-} +tail :: (ValidBraid v s a) => Braid v a -> Maybe (Braid v a) +tail (Braid t) = case viewl t of + EmptyL -> Nothing + (LL.null -> True) :< ts -> Yi.Braid.tail (Braid ts) + x :< ts -> Just . Braid $ (LL.tail x) -| ts + +-- | Splits the 'Braid' at given number of segments. +-- +-- If @position <= 0@ then the left 'Braid' is empty and the right string +-- contains everything else. +-- +-- If @position >= length of the 'Braid'@ then the left 'Braid' contains +-- everything and the right 'Braid' is empty. +-- +-- Implementation note: the way this works is by splitting the +-- underlying finger at a closest chunk that goes *over* the given +-- position (see 'T.split'). This either results in a perfect split at +-- which point we're done or more commonly, it leaves as few +-- segments short and we need to take few segments from the first +-- 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, the cost of one +-- cons and one cons of a chunk and lastly the cost of 'T.splitAt' of +-- the underlying chain type. It turns out to be fairly fast all +-- together. +{-# INLINABLE splitAt #-} +splitAt :: (ValidBraid v s a) => Int -> Braid v a -> (Braid v a, Braid v a) +splitAt n (Braid t) + | n <= 0 = (mempty, Braid t) + | otherwise = case viewl s of + x :< ts | n' /= 0 -> + let (lx, rx) = LL.splitAt n' x + in (Braid $ f |> lx, + Braid $ rx -| ts) + _ -> (Braid f, Braid s) + where + (f, s) = T.split ((> n) . getSize) t + n' = n - getSize (measure f) + +-- | Takes the first n given segments +{-# INLINABLE take #-} +take :: (ValidBraid v s a) => Int -> Braid v a -> Braid v a +take 1 = maybe mempty Yi.Braid.singleton . Yi.Braid.head +take n = fst . Yi.Braid.splitAt n + +-- | Drops the first n segments. +{-# INLINABLE drop #-} +drop :: (ValidBraid v s a) => Int -> Braid v a -> Braid v a +drop 1 = fromMaybe mempty . Yi.Braid.tail +drop n = snd . Yi.Braid.splitAt n + +-- | The usual 'Prelude.dropWhile' optimised for 'Braid's. +{-# INLINABLE dropWhile #-} +dropWhile :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Braid v a +dropWhile p = Braid . go . fromBraid + where + go t = case viewl t of + EmptyL -> T.empty + (LL.null -> True) :< ts -> go ts + x :< ts -> + let r = LL.dropWhile p x + l = LL.length x + l' = LL.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 | LL.null r -> go ts + -- It wasn't everything and we have left-overs, we must be done. + | otherwise -> 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. + _ -> r -| ts + +-- | As 'Yi.Braid.dropWhile' but drops from the end instead. +{-# INLINABLE dropWhileEnd #-} +dropWhileEnd :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Braid v a +dropWhileEnd p = Braid . go . fromBraid + where + go t = case viewr t of + EmptyR -> T.empty + ts :> (LL.null -> True) -> go ts + ts :> x -> + let r = LL.dropWhileEnd p x + l = LL.length x + l' = LL.length r + in case compare l' l of + EQ -> t + LT | LL.null r -> go ts + | otherwise -> ts |> r + _ -> ts |- r + +-- | The usual 'Prelude.takeWhile' optimised for 'Braid's. +{-# INLINABLE takeWhile #-} +takeWhile :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Braid v a +takeWhile p = Braid . go . fromBraid + where + go t = case viewl t of + EmptyL -> T.empty + (LL.null -> True) :< ts -> go ts + x :< ts -> + let r = LL.takeWhile p x + l = LL.length x + l' = LL.length r + in case compare l' l of + -- We took the whole chunk, keep taking more. + EQ -> 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. + _ -> T.singleton r + +-- | Like 'Yi.Braid.takeWhile' but takes from the end instead. +{-# INLINABLE takeWhileEnd #-} +takeWhileEnd :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Braid v a +takeWhileEnd p = Braid . go . fromBraid + where + go t = case viewr t of + EmptyR -> T.empty + ts :> (LL.null -> True) -> go ts + ts :> x -> case compare l' l of + EQ -> go ts |> x + _ -> T.singleton r + where + -- no TX.takeWhileEnd – https://github.com/bos/text/issues/89 + r = LL.reverse . LL.takeWhile p . LL.reverse $ x + l = LL.length x + l' = LL.length r + + +-- | Returns a pair whose first element is the longest prefix +-- (possibly empty) of t of elements that satisfy p, and whose second +-- is the remainder of the string. See also 'Data.Text.span'. +-- +-- This implementation uses 'Yi.Braid.splitAt' which actually is just +-- as fast as hand-unrolling the tree. GHC sure is great! +{-# INLINABLE span #-} +span :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> (Braid v a, Braid v a) +span p y = let x = Yi.Braid.takeWhile p y + in case Yi.Braid.splitAt (Yi.Braid.length x) y of + -- Re-using ‘x’ seems to gain us a minor performance + -- boost. + (_, y') -> (x, y') + +-- | Just like 'Yi.Braid.span' but with the predicate negated. +{-# INLINABLE break #-} +break :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> (Braid v a, Braid v a) +break p = Yi.Braid.span (not . p) + +-- | Concatenates the list of 'Braid's after inserting the +-- user-provided 'Braid' between the elements. +-- +-- Empty 'Braid's are not ignored and will end up as 'Braid's of +-- length 1. If you don't want this, it's up to you to pre-process the +-- list. Just as with 'Yi.Braid.intersperse', it is up to the user to +-- pre-process the list. +{-# INLINABLE intercalate #-} +intercalate :: (ValidBraid v s a) => Braid v a -> [Braid v a] -> Braid v a +intercalate _ [] = mempty +intercalate (Braid t') (Braid s:ss) = Braid $ go s ss + where + go !acc [] = acc + go acc (Braid t : ts') = go (acc >< t' >< t) ts' + +-- | Intersperses the given segment between the 'Braid's. This is +-- useful when you have a bunch of 'Braid's you just want to separate +-- with something. +-- +-- What's more, the result is a single 'Braid'. 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.Braid.intercalate'. Note that what this does __not__ do is +-- intersperse segments into the underlying chain, you should convert +-- and use your type's underlying intersperse for that instead. +{-# INLINABLE intersperse #-} +intersperse :: (ValidBraid v s a) => s -> [Braid v a] -> Braid v a +intersperse _ [] = mempty +intersperse c (t:ts) = go t ts + where + go !acc [] = acc + go acc (t':ts') = go (acc <> (c `cons` t')) ts' + +-- | Add a segment in front of a 'Braid'. +{-# INLINABLE cons #-} +cons :: (ValidBraid v s a) => s -> Braid v a -> Braid v a +cons c (Braid t) = case viewl t of + EmptyL -> Yi.Braid.singleton c + x :< ts | LL.length x < defaultChunkSize -> Braid $ (c `LL.cons` x) <| ts + _ -> Braid $ LL.singleton c <| t + +-- | Add a segment in the back of a 'Braid'. +{-# INLINABLE snoc #-} +snoc :: (ValidBraid v s a) => Braid v a -> s -> Braid v a +snoc (Braid t) c = case viewr t of + EmptyR -> Yi.Braid.singleton c + ts :> x | LL.length x < defaultChunkSize -> Braid $ ts |> (x `LL.snoc` c) + _ -> Braid $ t |> LL.singleton c + +-- | Turn a single segment into a 'Braid'. +-- Consider whether it's worth creating +-- this, maybe you can use 'cons' or 'snoc' instead? +{-# INLINABLE singleton #-} +singleton :: (ValidBraid v s a) => s -> Braid v a +singleton = Braid . T.singleton . LL.singleton + +-- | @any@ specialised to 'Braid' +-- +-- Implementation note: this currently just does any by doing 'S.any' +-- on underlying chunks. We should be able to speed it +-- up by running it in parallel over multiple chunks. +{-# INLINABLE any #-} +any :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Bool +any p = go . fromBraid + where + go x = case viewl x of + EmptyL -> False + t :< ts -> LL.any p t || go ts + +-- | @all@ specialised to 'Braid' +-- +-- See the implementation note for 'Yi.Braid.any'. +{-# INLINABLE all #-} +all :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Bool +all p = go . fromBraid + where + go x = case viewl x of + EmptyL -> True + t :< ts -> LL.all p t && go ts + +-- | Filters the segments from the underlying chain +-- +-- >>> filter (/= 'a') "bac" +-- "bc" +{-# INLINABLE filter #-} +filter :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> Braid v a +filter p = Braid . go . fromBraid + where + go t = case viewl t of + EmptyL -> T.empty + x :< ts -> LL.filter p x -| go ts + +-- | Maps the segments of the underlying chain. +{-# INLINABLE map #-} +map :: (ValidBraid v s a, ValidBraid q t b) => (s -> t) -> Braid v a -> Braid q b +map f = Braid . go . fromBraid + where + go t = case viewl t of + EmptyL -> T.empty + x :< ts -> LL.map f x <| go ts + +-- | Splits the 'Braid' on characters matching the predicate +-- +-- 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. +{-# INLINABLE split #-} +split :: (ValidBraid v s a) => (s -> Bool) -> Braid v a -> [Braid v a] +split p = fmap toBraid . LL.split p . extractBraid + +-- | Left fold. +-- +-- Benchmarks show that folding is actually Pretty Damn Slow™: consider +-- whether folding is really the best thing to use in your scenario. +{-# INLINABLE foldl' #-} +foldl' :: (ValidBraid v s a) => (b -> s -> b) -> b -> Braid v a -> b +foldl' f a = go a . fromBraid + where + go acc t = case viewl t of + EmptyL -> acc + x :< ts -> let r = LL.foldl f acc x + in r `seq` go r ts + +-- | Replicate the given 'Braid' a set number of times, concatenating +-- the results. Also see 'Yi.Braid.replicateChar'. +{-# INLINABLE replicate #-} +replicate :: (ValidBraid v s a) => Int -> Braid v a -> Braid v a +replicate n t | n <= 0 = mempty + | otherwise = t <> Yi.Braid.replicate (n - 1) t + +-- | Replicate the given segment a set number of times and pack the +-- result into a 'Braid'. +{-# INLINABLE replicateSegment #-} +replicateSegment :: (ValidBraid v s a) => Int -> s -> Braid v a +replicateSegment n = toBraid . LL.replicate n + +-- Please note that this maps over each __chunk__ so this can only be +-- used with layout-agnostic functions. For example +-- +-- >>> let t = 'toBraid' "abc" <> 'toBraid' "def" +-- >>> 'extractBraid' $ 'fmap'' 'Data.Text.reverse' t +-- "cbafed" +-- +-- If however your function is unaffected by this 'chunking' behaviour +-- you can tag or transform your underlying sequences or convert between +-- `Braid` types. +{-# INLINABLE fmap' #-} +fmap' :: (ValidBraid v s a, ValidBraid q t b) => (a -> b) -> Braid v a -> Braid q b +fmap' f = Braid . T.fmap' f . fromBraid + +-- | Maps over each __chunk__ which means this function is UNSAFE! If +-- you use this with functions which don't preserve a chain's measure +-- things will break really, really badly. You should not need to use this. +-- +-- Also see 'T.unsafeFmap' +{-# INLINABLE unsafeWithChunk #-} +unsafeWithChunk :: (ValidBraid v s a) => (a -> a) -> Braid v a -> Braid v a +unsafeWithChunk f = Braid . T.unsafeFmap f . fromBraid diff --git a/src/Yi/Rope.hs b/src/Yi/Rope.hs index a87ebbc..f42ca19 100644 --- a/src/Yi/Rope.hs +++ b/src/Yi/Rope.hs @@ -5,6 +5,7 @@ {-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-} {-# language ViewPatterns #-} +{-# language FlexibleInstances #-} {-# options_haddock show-extensions #-} -- | @@ -69,7 +70,7 @@ import qualified Data.ByteString.Lazy as BSL import Data.Char (isSpace) import qualified Data.FingerTree as T import Data.FingerTree hiding (null, empty, reverse, split) -import qualified Data.List as L (foldl') +import qualified Data.ListLike as LL import Data.Maybe import Data.Monoid import Data.String (IsString(..)) @@ -81,6 +82,31 @@ import qualified Data.Text.IO as TXIO (writeFile) import Data.Typeable import Prelude hiding (drop) +import qualified Yi.Braid as B + +-- | A chunk caches the length of the underlying Text since computing the +-- length of Text is O(n) +data YiChunk = Chunk { _chunkSize :: {-# UNPACK #-} !Int + , _fromChunk :: {-# UNPACK #-} !TX.Text + } deriving (Show, Eq, Typeable) + +instance NFData YiChunk where + rnf (Chunk !i !t) = i `seq` rnf t + +-- | Transform the chain inside the chunk. It's vital that the transformation +-- preserves the length of the content. +overChunk :: (TX.Text -> TX.Text) -- ^ Length-preserving content transformation. + -> YiChunk -> YiChunk +overChunk f (Chunk l t) = Chunk l (f t) + + +-- | A 'YiString' is a 'FingerTree' with cached char and line counts +-- over chunks of 'TX.Text'. +type YiString = B.Braid Size YiChunk + +fromRope :: YiString -> FingerTree Size YiChunk +fromRope = B.fromBraid + -- | Used to cache the size of the strings. data Size = Indices { charIndex :: {-# UNPACK #-} !Int -- ^ How many characters under here? @@ -88,11 +114,8 @@ data Size = Indices { charIndex :: {-# UNPACK #-} !Int -- ^ How many lines under here? } deriving (Eq, Show, Typeable) --- | A chunk storing the string of the type it is indexed by. It --- caches the length of stored string. -data YiChunk = Chunk { chunkSize :: {-# UNPACK #-} !Int - , _fromChunk :: {-# UNPACK #-} !TX.Text - } deriving (Show, Eq, Typeable) +instance B.HasSize Size where + getSize = charIndex -- | Makes a chunk from a given string. We allow for an arbitrary -- length function here to allow us to bypass the calculation with @@ -105,11 +128,6 @@ mkChunk :: (TX.Text -> Int) -- ^ The length function to use. -> YiChunk mkChunk l t = Chunk (l t) t --- | Transform the chunk content. It's vital that the transformation --- preserves the length of the content. -overChunk :: (TX.Text -> TX.Text) -- ^ Length-preserving content transformation. - -> YiChunk -> YiChunk -overChunk f (Chunk l t) = Chunk l (f t) -- | Counts number of newlines in the given 'TX.Text'. countNl :: TX.Text -> Int @@ -122,62 +140,17 @@ instance Monoid Size where instance Measured Size YiChunk where measure (Chunk l t) = Indices l (countNl t) --- | A 'YiString' is a 'FingerTree' with cached char and line counts --- over chunks of 'TX.Text'. -newtype YiString = YiString { fromRope :: FingerTree Size YiChunk } - deriving (Show, Typeable) - --- | 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` () -instance NFData YiChunk where - rnf (Chunk !i !t) = i `seq` rnf t - -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 +(-|) = (B.-|) (|-) :: FingerTree Size YiChunk -> YiChunk -> FingerTree Size YiChunk -t |- b | chunkSize b == 0 = t - | otherwise = t |> b - --- | Default size chunk to use. Currently @1200@ as this is what --- benchmarks suggest. --- --- This makes the biggest difference with 'lines'-like and --- 'concat'-like functions. Bigger chunks make 'concat' (much) faster --- but 'lines' slower. In general it seems that we benefit more from --- larger chunks and 1200 seems to be the sweet spot. -defaultChunkSize :: Int -defaultChunkSize = 1200 +(|-) = (B.|-) -- | Reverse the whole underlying string. -- @@ -188,7 +161,7 @@ defaultChunkSize = 1200 -- (but never more than the default size), perhaps we should -- periodically rechunk the tree to recover nice sizes? reverse :: YiString -> YiString -reverse = YiString . fmap' (overChunk TX.reverse) . T.reverse . fromRope +reverse = B.reverse -- | See 'fromText'. fromString :: String -> YiString @@ -214,38 +187,19 @@ toReverseString = TX.unpack . toReverseText -- chunk size to be used. Uses 'defaultChunkSize' if the given -- size is <= 0. fromText' :: Int -> TX.Text -> YiString -fromText' n | n <= 0 = fromText' defaultChunkSize - | otherwise = YiString . r T.empty . f - where - f = TX.chunksOf n - - -- Convert the given string into chunks in the tree. We have a - -- special case for a single element case: because we split on - -- predetermined chunk size, we know that all chunks but the last - -- one will be the specified size so we can optimise here instead - -- of having to recompute chunk size at creation. - r :: FingerTree Size YiChunk -> [TX.Text] -> FingerTree Size YiChunk - r !tr [] = tr - r !tr (t:[]) = tr |- mkChunk TX.length t - r !tr (t:ts) = let r' = tr |- mkChunk (const n) t - in r r' ts +fromText' n = B.toBraid' n . mkChunk TX.length -- | Converts a 'TX.Text' into a 'YiString' using -- 'defaultChunkSize'-sized chunks for the underlying tree. fromText :: TX.Text -> YiString -fromText = fromText' defaultChunkSize +fromText = B.toBraid . mkChunk TX.length fromLazyText :: TXL.Text -> YiString -fromLazyText = YiString . T.fromList . fmap (mkChunk TX.length) . TXL.toChunks +fromLazyText = B.Braid . T.fromList . fmap (mkChunk TX.length) . TXL.toChunks -- | Consider whether you really need to use this! toText :: YiString -> TX.Text -toText = TX.concat . go . fromRope - where - go :: FingerTree Size YiChunk -> [TX.Text] - go t = case viewl t of - Chunk _ !c :< cs -> c : go cs - EmptyL -> [] +toText = _fromChunk . B.extractBraid -- | Spits out the underlying string, reversed. -- @@ -258,23 +212,23 @@ toReverseText = TX.reverse . toText -- | Checks if the given 'YiString' is actually empty. null :: YiString -> Bool -null = T.null . fromRope +null = B.null -- | Creates an empty 'YiString'. empty :: YiString -empty = YiString T.empty +empty = B.empty -- | Length of the whole underlying string. -- -- Amortized constant time. length :: YiString -> Int -length = charIndex . measure . fromRope +length = B.length -- | Count the number of newlines in the underlying string. This is -- actually amortized constant time as we cache this information in -- the underlying tree. countNewLines :: YiString -> Int -countNewLines = lineIndex . measure . fromRope +countNewLines = lineIndex . measure . B.fromBraid -- | Append two 'YiString's. -- @@ -288,45 +242,29 @@ countNewLines = lineIndex . measure . fromRope -- I suspect that this pays for itself as we'd spend more time -- computing over all the little chunks than few large ones anyway. append :: YiString -> YiString -> YiString -append (YiString t) (YiString t') = case (viewr t, viewl t') of - (EmptyR, _) -> YiString t' - (_, EmptyL) -> YiString t - (ts :> Chunk l x, Chunk l' x' :< ts') -> - let len = l + l' in case compare len defaultChunkSize of - GT -> YiString (t <> t') - _ -> YiString (ts |- Chunk len (x <> x') <> ts') +append = B.append -- | Concat a list of 'YiString's. concat :: [YiString] -> YiString -concat = L.foldl' append empty +concat = B.concat -- | 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) +head = B.head -- | 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) +last = B.last -- | Takes every character but the last one: returns Nothing on empty -- string. init :: YiString -> Maybe YiString -init (YiString t) = case viewr t of - EmptyR -> Nothing - ts :> Chunk 0 _ -> Yi.Rope.init (YiString ts) - ts :> Chunk l x -> Just . YiString $ ts |- Chunk (l - 1) (TX.init x) +init = B.init -- | Takes the tail of the underlying string. If the string is empty -- to begin with, returns Nothing. tail :: YiString -> Maybe YiString -tail (YiString t) = case viewl t of - EmptyL -> Nothing - Chunk 0 _ :< ts -> Yi.Rope.tail (YiString ts) - Chunk l x :< ts -> Just . YiString $ Chunk (l - 1) (TX.tail x) -| ts +tail = B.tail -- | Splits the string at given character position. -- @@ -349,105 +287,31 @@ tail (YiString t) = case viewl t of -- the underlying 'TX.Text'. It turns out to be fairly fast all -- together. splitAt :: Int -> YiString -> (YiString, YiString) -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) +splitAt = B.splitAt -- | Takes the first n given characters. take :: Int -> YiString -> YiString -take 1 = maybe mempty Yi.Rope.singleton . Yi.Rope.head -take n = fst . Yi.Rope.splitAt n +take = B.take -- | Drops the first n characters. drop :: Int -> YiString -> YiString -drop 1 = fromMaybe mempty . Yi.Rope.tail -drop n = snd . Yi.Rope.splitAt n +drop = B.drop -- | 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 0 _ :< ts -> go ts - 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 +dropWhile = B.dropWhile -- | 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 0 _ -> go ts - 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 +dropWhileEnd = B.dropWhileEnd -- | 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 0 _ :< ts -> go ts - 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. - _ -> T.singleton $ Chunk l' r +takeWhile = B.takeWhile -- | 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 0 _ -> go ts - ts :> Chunk l x -> case compare l' l of - EQ -> go ts |> Chunk l x - _ -> T.singleton $ 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 - +takeWhileEnd = B.takeWhileEnd -- | Returns a pair whose first element is the longest prefix -- (possibly empty) of t of elements that satisfy p, and whose second @@ -456,15 +320,11 @@ takeWhileEnd p = YiString . go . fromRope -- This implementation uses 'Yi.Rope.splitAt' which actually is just -- as fast as hand-unrolling the tree. GHC sure is great! span :: (Char -> Bool) -> YiString -> (YiString, YiString) -span p y = let x = Yi.Rope.takeWhile p y - in case Yi.Rope.splitAt (Yi.Rope.length x) y of - -- Re-using ‘x’ seems to gain us a minor performance - -- boost. - (_, y') -> (x, y') +span = B.span -- | Just like 'Yi.Rope.span' but with the predicate negated. break :: (Char -> Bool) -> YiString -> (YiString, YiString) -break p = Yi.Rope.span (not . p) +break = B.break -- | Concatenates the list of 'YiString's after inserting the -- user-provided 'YiString' between the elements. @@ -474,11 +334,7 @@ break p = Yi.Rope.span (not . p) -- 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') (YiString s:ss) = YiString $ go s ss - where - go !acc [] = acc - go acc (YiString t : ts') = go (acc >< t' >< t) ts' +intercalate = B.intercalate -- | Intersperses the given character between the 'YiString's. This is -- useful when you have a bunch of strings you just want to separate @@ -494,30 +350,20 @@ intercalate (YiString t') (YiString s:ss) = YiString $ go s ss -- 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) = go t ts - where - go !acc [] = acc - go acc (t':ts') = go (acc <> (c `cons` t')) ts' +intersperse = B.intersperse -- | Add a 'Char' in front of a 'YiString'. cons :: Char -> YiString -> YiString -cons c (YiString t) = case viewl t of - EmptyL -> Yi.Rope.singleton c - Chunk l x :< ts | l < defaultChunkSize -> YiString $ Chunk (l + 1) (c `TX.cons` x) <| ts - _ -> YiString $ Chunk 1 (TX.singleton c) <| t +cons = B.cons -- | Add a 'Char' in the back of a 'YiString'. snoc :: YiString -> Char -> YiString -snoc (YiString t) c = case viewr t of - EmptyR -> Yi.Rope.singleton c - ts :> Chunk l x | l < defaultChunkSize -> YiString $ ts |> Chunk (l + 1) (x `TX.snoc` c) - _ -> YiString $ t |> Chunk 1 (TX.singleton c) +snoc = B.snoc -- | 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) +singleton = B.singleton -- | Splits the underlying string before the given line number. -- Zero-indexed lines. @@ -540,13 +386,13 @@ splitAtLine n r | n <= 0 = (empty, r) -- now looking for extra newlines in the next chunk rather than extra -- characters. splitAtLine' :: Int -> YiString -> (YiString, YiString) -splitAtLine' p (YiString tr) = case viewl s of +splitAtLine' p (B.Braid tr) = case viewl s of ch@(Chunk _ x) :< r -> let excess = lineIndex (measure f) + lineIndex (measure ch) - p - 1 (lx, rx) = cutExcess excess x - in (YiString $ f |- mkChunk TX.length lx, - YiString $ mkChunk TX.length rx -| r) - _ -> (YiString f, YiString s) + in (B.Braid $ f |- mkChunk TX.length lx, + B.Braid $ mkChunk TX.length rx -| r) + _ -> (B.Braid f, B.Braid s) where (f, s) = T.split ((p <) . lineIndex) tr @@ -575,10 +421,10 @@ splitAtLine' p (YiString tr) = case viewl s of lines :: YiString -> [YiString] lines = Prelude.map dropNl . lines' where - dropNl (YiString t) = case viewr t of + dropNl (B.Braid t) = case viewr t of EmptyR -> Yi.Rope.empty ts :> ch@(Chunk l tx) -> - YiString $ ts |- if TX.null tx + B.Braid $ ts |- if TX.null tx then ch else case TX.last tx of '\n' -> Chunk (l - 1) (TX.init tx) @@ -600,10 +446,10 @@ lines = Prelude.map dropNl . lines' -- but the underlying structure might change: notably, chunks will -- most likely change sizes. lines' :: YiString -> [YiString] -lines' t = let (YiString f, YiString s) = splitAtLine' 0 t +lines' t = let (B.Braid f, B.Braid s) = splitAtLine' 0 t in if T.null s - then if T.null f then [] else [YiString f] - else YiString f : lines' (YiString s) + then if T.null f then [] else [B.Braid f] + else B.Braid f : lines' (B.Braid s) -- | Joins up lines by a newline character. It does not leave a -- newline after the last line. If you want a more classical @@ -618,21 +464,13 @@ unlines = Yi.Rope.intersperse '\n' -- 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 +any = B.any -- | '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 -> True - Chunk _ t :< ts -> TX.all p t && go ts +all = B.all -- | To serialise a 'YiString', we turn it into a regular 'String' -- first. @@ -677,19 +515,11 @@ readFile fp = BSL.readFile fp >>= go decoders -- >>> 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 +filter = B.filter -- | 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 +map = B.map -- | Join given 'YiString's with a space. Empty lines will be filtered -- out first. @@ -713,25 +543,19 @@ words = Prelude.filter (not . Yi.Rope.null) . Yi.Rope.split isSpace -- 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 +split = B.split -- | Left fold. -- -- Benchmarks show that folding is actually Pretty Damn Slow™: consider -- whether folding is really the best thing to use in your scenario. foldl' :: (a -> Char -> a) -> a -> YiString -> a -foldl' f a = go a . fromRope - where - go acc t = case viewl t of - EmptyL -> acc - Chunk _ x :< ts -> let r = TX.foldl' f acc x - in r `seq` go r ts +foldl' = B.foldl' -- | Replicate the given YiString set number of times, concatenating --- the results. Also see 'Yi.Rope.replicateChar'. +-- the results. Also see 'Yi.Braid.replicateChar'. replicate :: Int -> YiString -> YiString -replicate n t | n <= 0 = mempty - | otherwise = t <> Yi.Rope.replicate (n - 1) t +replicate = B.replicate -- | Replicate the given character set number of times and pack the -- result into a 'YiString'. @@ -739,7 +563,7 @@ replicate n t | n <= 0 = mempty -- >>> replicateChar 4 ' ' -- " " replicateChar :: Int -> Char -> YiString -replicateChar n = fromText . TX.replicate n . TX.singleton +replicateChar = B.replicateSegment -- | Helper function doing conversions of to and from underlying -- 'TX.Text'. You should aim to implement everything in terms of @@ -760,7 +584,7 @@ replicateChar n = fromText . TX.replicate n . TX.singleton -- -- which should look very familiar. withText :: (TX.Text -> TX.Text) -> YiString -> YiString -withText f = YiString . T.fmap' (mkChunk TX.length . f . _fromChunk) . fromRope +withText = B.fmap' . overChunk -- | Maps over each __chunk__ which means this function is UNSAFE! If -- you use this with functions which don't preserve 'Size', that is @@ -769,6 +593,120 @@ withText f = YiString . T.fmap' (mkChunk TX.length . f . _fromChunk) . fromRope -- -- 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) +unsafeWithText = B.unsafeWithChunk . overChunk + +instance Monoid YiChunk where + mempty = Chunk 0 mempty + Chunk n txt `mappend` Chunk n' txt' = Chunk (n + n') (txt `mappend` txt') + +instance LL.FoldableLL YiChunk Char where + foldl f d (Chunk _ x) = LL.foldl f d x + foldl' f d (Chunk _ x)= LL.foldl' f d x + foldl1 f (Chunk _ x)= LL.foldl1 f x + foldr f d (Chunk _ x)= LL.foldr f d x + foldr' f d (Chunk _ x) = LL.foldr' f d x + foldr1 f (Chunk _ x) = LL.foldr1 f x + +instance LL.ListLike YiChunk Char where + empty = Chunk 0 TX.empty + singleton = Chunk 1 . TX.singleton + cons c (Chunk n x) = Chunk (n + 1) (TX.cons c x) + snoc (Chunk n x) c = Chunk (n + 1) (TX.snoc x c) + append (Chunk n x) (Chunk n' x') = Chunk (n + n') (x `TX.append` x') + head (Chunk _ x) = TX.head x + uncons (Chunk n x) = case TX.uncons x of + Just (c, rest) -> Just (c, Chunk (n - 1) rest) + Nothing -> Nothing + last (Chunk _ x) = TX.last x + tail (Chunk l x) = (Chunk (l - 1) (TX.tail x)) + init (Chunk l x) = (Chunk (l - 1) (TX.init x)) + + null (Chunk 0 _) = True + null _ = False + length (Chunk l _) = l + -- map f (Chunk n x) = Chunk n (TX.map f x) + -- rigidMap = LL.map + reverse (Chunk n x) = Chunk n (TX.reverse x) + intersperse _ (Chunk 0 x) = Chunk 0 x + intersperse _ (Chunk 1 x) = Chunk 1 x + intersperse c (Chunk n x) = Chunk ((2 * n) - 1) (TX.intersperse c x) + -- concat = fold + -- concatMap = foldMap + -- rigidConcatMap = concatMap + any p (Chunk _ x) = TX.any p x + all p (Chunk _ x) = TX.all p x + -- maximum = foldr1 max + -- minimum = foldr1 min + replicate n c = Chunk n (TX.replicate n (TX.singleton c)) + take n c@(Chunk n' x) | n >= n' = c + | otherwise = Chunk n (TX.take n x) + drop n (Chunk n' x) | n >= n' = LL.empty + | otherwise = Chunk (n' - n) (TX.drop n x) + + splitAt n c@(Chunk n' x) + | n <= 0 = (LL.empty, c) + | n >= n' = (c, LL.empty) + | otherwise = let (pre, post) = TX.splitAt n x + in (Chunk n pre, Chunk (n' - n) post) + + takeWhile p (Chunk _ x) = + let x' = TX.takeWhile p x + in (Chunk (TX.length x') x) + + dropWhile p (Chunk _ x) = + let x' = TX.dropWhile p x + in (Chunk (TX.length x') x) + + dropWhileEnd p (Chunk _ x) = + let x' = TX.dropWhileEnd p x + in (Chunk (TX.length x') x) + + span p (Chunk l x) = let (pre, post) = TX.span p x + preLen = TX.length pre + postLen = l - preLen + in (Chunk preLen pre, Chunk postLen post) + + -- break p = LL.span (not . p) + -- group :: (ListLike full' full, Eq item) => full -> full' Source # + -- inits :: ListLike full' full => full -> full' Source # + -- tails :: ListLike full' full => full -> full' Source # + -- isPrefixOf :: Eq item => full -> full -> Bool Source # + -- isSuffixOf :: Eq item => full -> full -> Bool Source # + -- isInfixOf :: Eq item => full -> full -> Bool Source # + -- stripPrefix :: Eq item => full -> full -> Maybe full Source # + -- stripSuffix :: Eq item => full -> full -> Maybe full Source # + -- elem :: Eq item => item -> full -> Bool Source # + -- notElem :: Eq item => item -> full -> Bool Source # + -- find :: (item -> Bool) -> full -> Maybe item Source # + filter p (Chunk _ x) = mkChunk TX.length $ TX.filter p x + -- index :: full -> Int -> item Source # + -- elemIndex :: Eq item => item -> full -> Maybe Int Source # + -- elemIndices :: (Eq item, ListLike result Int) => item -> full -> result Source # + -- findIndex :: (item -> Bool) -> full -> Maybe Int Source # + -- findIndices :: ListLike result Int => (item -> Bool) -> full -> result Source # + -- sequence :: (Monad m, ListLike fullinp (m item)) => fullinp -> m full Source # + -- mapM :: (Monad m, ListLike full' item') => (item -> m item') -> full -> m full' Source # + -- rigidMapM :: Monad m => (item -> m item) -> full -> m full Source # + -- nub :: Eq item => full -> full Source # + -- delete :: Eq item => item -> full -> full Source # + -- deleteFirsts :: Eq item => full -> full -> full Source # + -- union :: Eq item => full -> full -> full Source # + -- intersect :: Eq item => full -> full -> full Source # + -- sort :: Ord item => full -> full Source # + -- insert :: Ord item => item -> full -> full Source # + -- toList :: full -> [item] Source # + -- fromList :: [item] -> full Source # + -- fromListLike :: ListLike full' item => full -> full' Source # + -- nubBy :: (item -> item -> Bool) -> full -> full Source # + -- deleteBy :: (item -> item -> Bool) -> item -> full -> full Source # + -- deleteFirstsBy :: (item -> item -> Bool) -> full -> full -> full Source # + -- unionBy :: (item -> item -> Bool) -> full -> full -> full Source # + -- intersectBy :: (item -> item -> Bool) -> full -> full -> full Source # + -- groupBy :: (ListLike full' full, Eq item) => (item -> item -> Bool) -> full -> full' Source # + -- sortBy :: (item -> item -> Ordering) -> full -> full Source # + -- insertBy :: (item -> item -> Ordering) -> item -> full -> full Source # + -- genericLength :: Num a => full -> a Source # + -- genericTake :: Integral a => a -> full -> full Source # + -- genericDrop :: Integral a => a -> full -> full Source # + -- genericSplitAt :: Integral a => a -> full -> (full, full) Source # + -- genericReplicate :: Integral a => a -> item -> full diff --git a/src/Yi/Rope/Internal/ListLikeHelpers.hs b/src/Yi/Rope/Internal/ListLikeHelpers.hs new file mode 100644 index 0000000..6cf398e --- /dev/null +++ b/src/Yi/Rope/Internal/ListLikeHelpers.hs @@ -0,0 +1,19 @@ +module Yi.Rope.Internal.ListLikeHelpers + ( chunksOf + , split + ) where + +import qualified Data.ListLike as LL + +chunksOf :: (LL.ListLike full item) => Int -> full -> [full] +chunksOf k = go + where + go t = case LL.splitAt k t of + (a, b) | LL.null a -> [] + | otherwise -> a : go b + +split :: (LL.ListLike full item) => (item -> Bool) -> full -> [full] +split p t = loop t + where loop s | LL.null s' = [l] + | otherwise = l : loop (LL.tail s') + where (l, s') = LL.span (not . p) s diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..62645a0 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,11 @@ +resolver: lts-8.18 +# resolver: ghc-7.10 + +packages: +- '.' + +extra-deps: [] + +flags: {} + +extra-package-dbs: [] diff --git a/yi-rope.cabal b/yi-rope.cabal index 03e693c..20d3233 100644 --- a/yi-rope.cabal +++ b/yi-rope.cabal @@ -15,6 +15,8 @@ library exposed-modules: Yi.Rope + Yi.Braid + Yi.Rope.Internal.ListLikeHelpers build-depends: base >=4.8 && <5 @@ -23,6 +25,7 @@ library , deepseq , fingertree >= 0.1.1 , text >= 1.2 + , ListLike >= 4.5 hs-source-dirs: src default-language: Haskell2010