trifecta-2.1: src/Text/Trifecta/Rope.hs
{-# language BangPatterns #-}
{-# language CPP #-}
{-# language DeriveDataTypeable #-}
{-# language DeriveGeneric #-}
{-# language FlexibleInstances #-}
{-# language MultiParamTypeClasses #-}
{-# language TypeFamilies #-}
-----------------------------------------------------------------------------
-- |
-- Copyright : (C) 2011-2019 Edward Kmett
-- License : BSD-style (see the file LICENSE)
--
-- Maintainer : Edward Kmett <ekmett@gmail.com>
-- Stability : experimental
-- Portability : non-portable
--
-- A rope is a data strucure to efficiently store and manipulate long strings.
-- Wikipedia provides a nice overview:
-- <https://en.wikipedia.org/wiki/Rope_(data_structure)>
----------------------------------------------------------------------------
module Text.Trifecta.Rope
( Rope(..)
, rope
, ropeBS
, Strand(..)
, strand
, strands
, grabRest
, grabLine
) where
import Data.ByteString (ByteString)
import qualified Data.ByteString as Strict
import qualified Data.ByteString.Lazy as Lazy
import qualified Data.ByteString.UTF8 as UTF8
import Data.Data
import Data.FingerTree as FingerTree
import Data.Foldable (toList)
import Data.Hashable
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup
#endif
import Data.Semigroup.Reducer
import GHC.Generics
import Text.Trifecta.Delta
import Text.Trifecta.Util.Combinators as Util
-- $setup
--
-- >>> :set -XOverloadedStrings
-- >>> import Data.Monoid ((<>))
-- >>> import qualified Data.ByteString.UTF8 as Strict
-- >>> import qualified Data.ByteString.Lazy.UTF8 as Lazy
-- A 'Strand' is a chunk of data; many 'Strand's together make a 'Rope'.
data Strand
= Strand {-# UNPACK #-} !ByteString !Delta -- ^ Data of a certain length
| Skipping !Delta -- ^ Absence of data of a certain length
deriving (Show, Data, Typeable, Generic)
-- | Construct a single 'Strand' out of a 'ByteString'.
strand :: ByteString -> Strand
strand bs = Strand bs (delta bs)
instance Measured Delta Strand where
measure (Strand _ s) = delta s
measure (Skipping d) = d
instance Hashable Strand
instance HasDelta Strand where
delta = measure
instance HasBytes Strand where
bytes (Strand _ d) = bytes d
bytes _ = 0
data Rope = Rope !Delta !(FingerTree Delta Strand) deriving Show
rope :: FingerTree Delta Strand -> Rope
rope r = Rope (measure r) r
-- | Construct a 'Rope' out of a single 'ByteString' strand.
ropeBS :: ByteString -> Rope
ropeBS = rope . singleton . strand
strands :: Rope -> FingerTree Delta Strand
strands (Rope _ r) = r
-- | Grab the entire rest of the input 'Rope', starting at an initial offset, or
-- return a default if we’re already at or beyond the end. Also see 'grabLine'.
--
-- Extract a suffix of a certain length from the input:
--
-- >>> grabRest (delta ("Hello " :: ByteString)) (ropeBS "Hello World\nLorem") Nothing (\x y -> Just (x, Lazy.toString y))
-- Just (Columns 6 6,"World\nLorem")
--
-- Same deal, but over multiple strands:
--
-- >>> grabRest (delta ("Hel" :: ByteString)) (ropeBS "Hello" <> ropeBS "World") Nothing (\x y -> Just (x, Lazy.toString y))
-- Just (Columns 3 3,"loWorld")
--
-- When the offset is too long, fall back to a default:
--
-- >>> grabRest (delta ("OffetTooLong" :: ByteString)) (ropeBS "Hello") Nothing (\x y -> Just (x, Lazy.toString y))
-- Nothing
grabRest
:: Delta -- ^ Initial offset
-> Rope -- ^ Input
-> r -- ^ Default value if there is no input left
-> (Delta -> Lazy.ByteString -> r)
-- ^ If there is some input left, create an @r@ out of the data from the
-- initial offset until the end
-> r
grabRest offset input failure success = trim (delta l) (bytes offset - bytes l) (toList r) where
trim offset' 0 (Strand str _ : xs) = go offset' str xs
trim _ k (Strand str _ : xs) = go offset (Strict.drop (fromIntegral k) str) xs
trim offset' k (Skipping p : xs) = trim (offset' <> p) k xs
trim _ _ [] = failure
go offset' str strands'
= success offset' (Lazy.fromChunks (str : [ a | Strand a _ <- strands' ]))
(l, r) = splitRopeAt offset input
-- | Split the rope in two halves, given a 'Delta' offset from the beginning.
splitRopeAt :: Delta -> Rope -> (FingerTree Delta Strand, FingerTree Delta Strand)
splitRopeAt splitPos = FingerTree.split (\pos -> bytes pos > bytes splitPos) . strands
-- | Grab the rest of the line at a certain offset in the input 'Rope', or
-- return a default if there is no newline left in the input. Also see
-- 'grabRest'.
--
-- >>> grabLine (delta ("Hello " :: ByteString)) (ropeBS "Hello" <> ropeBS " World\nLorem") Nothing (\x y -> Just (x, Strict.toString y))
-- Just (Columns 6 6,"World\n")
grabLine
:: Delta -- ^ Initial offset
-> Rope -- ^ Input
-> r -- ^ Default value if there is no input left
-> (Delta -> Strict.ByteString -> r)
-- ^ If there is some input left, create an @r@ out of the data from the
-- initial offset until the end of the line
-> r
grabLine offset input failure success
= grabRest offset input failure (\d -> success d . Util.fromLazy . Util.takeLine)
instance HasBytes Rope where
bytes = bytes . measure
instance HasDelta Rope where
delta = measure
instance Measured Delta Rope where
measure (Rope s _) = s
instance Monoid Rope where
mempty = Rope mempty mempty
mappend = (<>)
instance Semigroup Rope where
Rope mx x <> Rope my y = Rope (mx <> my) (x `mappend` y)
instance Reducer Rope Rope where
unit = id
instance Reducer Strand Rope where
unit s = rope (FingerTree.singleton s)
cons s (Rope mt t) = Rope (delta s `mappend` mt) (s <| t)
snoc (Rope mt t) !s = Rope (mt `mappend` delta s) (t |> s)
instance Reducer Strict.ByteString Rope where
unit = unit . strand
cons = cons . strand
snoc r = snoc r . strand
instance Reducer [Char] Rope where
unit = unit . strand . UTF8.fromString
cons = cons . strand . UTF8.fromString
snoc r = snoc r . strand . UTF8.fromString