packages feed

text-rope 0.1 → 0.2

raw patch · 12 files changed

+709/−155 lines, 12 files

Files

changelog.md view
@@ -1,3 +1,8 @@+# 0.2++* Share `TextLines` between `Char` and UTF-16 modules.+* Add `Data.Text.Utf16.Rope.Mixed`.+ # 0.1  * Initial release.
src/Data/Text/Lines/Internal.hs view
@@ -30,14 +30,17 @@   , intToWord   ) where -import Prelude ((+), (-), subtract, quot, fromIntegral, seq)+import Prelude ((+), (-), (*), subtract, quot, fromIntegral, seq, error) import Control.DeepSeq (NFData, rnf) import Data.Bits (toIntegralSized)-import Data.Bool (Bool, otherwise)+import Data.Bool (Bool, otherwise, not) import Data.Char (Char) import Data.Eq (Eq, (==))+import Data.Foldable (foldMap) import Data.Function (on, (.), ($)) import Data.Int (Int)+import Data.List (map, mapAccumL, filter)+import Data.List.NonEmpty (NonEmpty(..)) import Data.Maybe (Maybe(..)) import Data.Monoid (Monoid(..)) import Data.Ord (Ord, compare, (<=), (<), (>))@@ -46,6 +49,7 @@ import qualified Data.Text.Array as TA import Data.Text.Internal (Text(..)) import qualified Data.Text as T+import Data.Tuple (snd) import qualified Data.Vector.Unboxed as U import Data.Word (Word) import Foreign.C.Types (CSize(..))@@ -57,13 +61,11 @@  #if MIN_VERSION_text(2,0,0) #else-import Prelude ((*)) import Data.Bits (shiftR) #endif  #ifdef DEBUG-import Prelude (error)-import Data.Bool ((&&), not)+import Data.Bool ((&&)) import Data.Char (generalCategory, GeneralCategory(..)) import Data.Eq ((/=)) import Data.List ((++))@@ -86,32 +88,27 @@   }  instance NFData TextLines where-  rnf (TextLines a b) = rnf a `seq` rnf b+  rnf = (`seq` ())  instance Eq TextLines where   (==) = (==) `on` toText-  {-# INLINE (==) #-}  instance Ord TextLines where   compare = compare `on` toText-  {-# INLINE compare #-}  instance Show TextLines where #ifdef DEBUG   show (TextLines x y) = "TextLines { " ++ showText x ++ ", " ++ show y ++ " }" #else   show = show . toText-  {-# INLINE show #-} #endif  instance IsString TextLines where   fromString = fromText . fromString-  {-# INLINE fromString #-}  -- | Create from 'Text', linear time. fromText :: HasCallStack => Text -> TextLines fromText t = textLines t (U.fromList $ nlIndices t)-{-# INLINABLE fromText #-}  nlIndices :: Text -> [Int] #if MIN_VERSION_text(2,0,0)@@ -134,7 +131,6 @@         delta = fromIntegral (unsafeDupablePerformIO $           memchr (TA.aBA arr) (2 * fromIntegral n) (2 * fromIntegral (len + off - n))) `shiftR` 1 #endif-{-# INLINABLE nlIndices #-}  foreign import ccall unsafe "_hs_text_lines_memchr0A" memchr   :: ByteArray# -> CSize -> CSize -> IO CSsize@@ -142,8 +138,18 @@ -- | Check whether a text is empty, O(1). null :: TextLines -> Bool null = T.null . toText-{-# INLINE null #-} +concat :: [TextLines] -> TextLines+concat ts = case ts' of+  [] -> mempty+  [x] -> x+  _ -> textLines+    (T.concat (map toText ts'))+    (U.concat (snd (mapAccumL f 0 ts')))+  where+    ts' = filter (not . null) ts+    f l (TextLines (Text _ off len) nls) = (l + len, U.map (+ (l - off)) nls)+ instance Semigroup TextLines where   TextLines t1@(Text _ off1 len1) s1 <> TextLines t2@(Text _ off2 _) s2     | T.null t1 = textLines t2 s2@@ -152,15 +158,22 @@       (t1 <> t2)       (U.map (subtract off1) s1 <> U.map (+ (len1 - off2)) s2)       -- This relies on specific implementation of instance Semigroup Text!-  {-# INLINABLE (<>) #-}-  -- TODO implement sconcat via Builder +  sconcat (x :| xs) = concat (x : xs)++  stimes 1 tl = tl+  stimes n (TextLines t@(Text _ off len) nls)+    | n == fromIntegral n' = textLines t' nls'+    | otherwise = error "Data.Text.Lines: stimes argument is too large"+    where+      n' = fromIntegral n+      t' = T.replicate n' t+      nls' = foldMap (\i -> U.map (\j -> j - off + len * i) nls) [0..n'-1]+ instance Monoid TextLines where   mempty = textLines mempty mempty-  {-# INLINE mempty #-}   mappend = (<>)-  {-# INLINE mappend #-}-  -- TODO implement mconcat via Builder+  mconcat = concat  -- | Equivalent to 'Data.List.length' . 'lines', but in O(1). --@@ -202,7 +215,6 @@     arrLen = off + len     go i [] = [Text arr i (arrLen - i) | i < arrLen]     go i (x : xs) = Text arr i (x - i) : go (x + 1) xs-{-# INLINABLE lines #-}  -- | Split at given line, O(1). --@@ -212,7 +224,6 @@ -- splitAtLine :: HasCallStack => Word -> TextLines -> (TextLines, TextLines) splitAtLine k = splitAtPosition (Position k 0)-{-# INLINE splitAtLine #-}  ------------------------------------------------------------------------------- -- Unicode code points@@ -228,7 +239,6 @@ -- length :: TextLines -> Word length = intToWord . T.length . toText-{-# INLINE length #-}  -- | Represent a position in a text. data Position = Position@@ -237,7 +247,7 @@   } deriving (Eq, Ord, Show)  instance NFData Position where-  rnf (Position a b) = rnf a `seq` rnf b+  rnf = (`seq` ())  -- | Associativity does not hold when 'posLine' overflows. instance Semigroup Position where@@ -268,7 +278,6 @@   }   where     nl = if U.null nls then off else U.last nls + 1-{-# INLINABLE lengthAsPosition #-}  -- | Span by a predicate, similar to @Data.Text.@'Data.Text.span'. -- Takes linear (by length of the prefix satisfying the predicate) time.@@ -286,7 +295,6 @@     n = binarySearch nls (off' + len')     y = textLines (Text arr off (off' + len' - off)) (U.take n nls)     z = textLines tz (U.drop n nls)-{-# INLINABLE span #-}  -- | Combination of 'splitAtLine' and subsequent 'splitAt'. -- Time is linear in 'posColumn', but does not depend on 'posLine'.@@ -327,7 +335,6 @@     n = binarySearch nls (off' + len')     y = textLines (Text arr off (off' + len' - off)) (U.take n nls)     z = textLines tz (U.drop n nls)-{-# INLINABLE splitAtPosition #-}  -- | Split at given code point, similar to @Data.Text.@'Data.Text.splitAt'. -- Takes linear time.@@ -338,7 +345,6 @@ -- splitAt :: HasCallStack => Word -> TextLines -> (TextLines, TextLines) splitAt = splitAtPosition . Position 0-{-# INLINE splitAt #-}  ------------------------------------------------------------------------------- -- Utils@@ -361,15 +367,12 @@       where         k = (i + j) `quot` 2 {-# SPECIALIZE binarySearch :: U.Vector Int -> Int -> Int #-}-{-# INLINABLE binarySearch #-}  intToWord :: Int -> Word intToWord = fromIntegral-{-# INLINE intToWord #-}  wordToInt :: Word -> Int wordToInt = fromIntegral-{-# INLINE wordToInt #-}  ------------------------------------------------------------------------------- -- Debug@@ -399,6 +402,5 @@  textLines :: HasCallStack => Text -> U.Vector Int -> TextLines textLines = TextLines-{-# INLINE textLines #-}  #endif
src/Data/Text/Rope.hs view
@@ -41,7 +41,7 @@ import Data.Bool (Bool(..), otherwise) import Data.Char (Char) import Data.Eq (Eq, (==))-import Data.Function ((.), ($), on, id)+import Data.Function ((.), ($), on) import Data.Maybe (Maybe(..)) import Data.Monoid (Monoid(..)) import Data.Ord (Ord, compare, (<), (<=), Ordering(..))@@ -65,13 +65,23 @@ #endif  -- | Rope of 'Text' chunks with logarithmic concatenation.+-- This rope offers an interface, based on code points.+-- Use "Data.Text.Utf16.Rope", if you need UTF-16 code units,+-- or "Data.Text.Utf16.Rope.Mixed", if you need both interfaces. data Rope   = Empty-  | Node !Rope !TL.TextLines !Rope !Word !Position+  | Node+    { _ropeLeft         :: !Rope+    , _ropeMiddle       :: !TL.TextLines+    , _ropeRight        :: !Rope+    , _ropeCharLen      :: !Word+    , _ropeCharLenAsPos :: !Position+    }  instance NFData Rope where   rnf Empty = ()-  rnf (Node a b c d e) = rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e+  -- No need to deepseq strict fields, for which WHNF = NF+  rnf (Node l _ r _ _) = rnf l `seq` rnf r  instance Eq Rope where   (==) = (==) `on` toLazyText@@ -238,6 +248,9 @@ -- >>> lengthInLines "foo\nbar" -- 2 --+-- If you do not care about ignoring the last newline character,+-- you can use 'posLine' . 'lengthAsPosition' instead, which works in O(1).+-- lengthInLines :: Rope -> Word lengthInLines rp = case lastChar rp of   Nothing -> 0@@ -245,8 +258,7 @@  -- | Glue chunks into 'TL.TextLines', linear time. toTextLines :: Rope -> TL.TextLines-toTextLines = foldMapRope id--- TODO make it fast! implement via mconcat for TextLines+toTextLines = mconcat . foldMapRope (:[])  toLazyText :: Rope -> TextLazy.Text toLazyText = foldMapRope (TextLazy.fromStrict . TL.toText)
src/Data/Text/Utf16/Lines.hs view
@@ -10,14 +10,14 @@ {-# LANGUAGE UnliftedFFITypes #-}  module Data.Text.Utf16.Lines-  ( TextLines-  , fromText-  , toText-  , null+  ( I.TextLines+  , I.fromText+  , I.toText+  , I.null   -- * Lines-  , lines-  , lengthInLines-  , splitAtLine+  , I.lines+  , I.lengthInLines+  , I.splitAtLine   -- * UTF-16 code units   , length   , splitAt@@ -28,21 +28,19 @@  import Prelude ((+), (-), seq) import Control.DeepSeq (NFData, rnf)-import Data.Bool (Bool, otherwise)-import Data.Coerce (coerce)+import Data.Bool (otherwise) import Data.Eq (Eq, (==)) import Data.Function ((.), ($)) import Data.Maybe (Maybe(..)) import Data.Monoid (Monoid(..)) import Data.Ord (Ord, (<=), (>), (>=)) import Data.Semigroup (Semigroup(..))-import Data.String (IsString) import qualified Data.Text.Array as TA import Data.Text.Internal (Text(..)) import qualified Data.Text.Lines.Internal as I import qualified Data.Vector.Unboxed as U import Data.Word (Word)-import Text.Show (Show, show)+import Text.Show (Show)  #if MIN_VERSION_text(2,0,0) import Prelude (fromIntegral)@@ -62,79 +60,6 @@ #define HasCallStack () #endif --- | A wrapper around 'Text' for fast line/column navigation.--- Concatenation takes linear time.------ This is a building block for 'Data.Text.Utf16.Rope.Rope',--- which provides logarithmic concatenation.-newtype TextLines = TextLines I.TextLines-  deriving (Eq, Ord, IsString, Semigroup, Monoid, NFData)--instance Show TextLines where-  show (TextLines t) = show t---- | Create from 'Text', linear time.-fromText :: HasCallStack => Text -> TextLines-fromText = coerce I.fromText-{-# INLINE fromText #-}---- | Extract 'Text', O(1).-toText :: TextLines -> Text-toText = coerce I.toText-{-# INLINE toText #-}---- | Check whether a text is empty, O(1).-null :: TextLines -> Bool-null = coerce I.null-{-# INLINE null #-}---- | Split into lines by @\\n@, similar to @Data.Text.@'Data.Text.lines'.--- Each line is produced in O(1).------ >>> :set -XOverloadedStrings--- >>> lines ""--- []--- >>> lines "foo"--- ["foo"]--- >>> lines "foo\n"--- ["foo"]--- >>> lines "foo\n\n"--- ["foo",""]--- >>> lines "foo\nbar"--- ["foo","bar"]----lines :: TextLines -> [Text]-lines = coerce I.lines-{-# INLINE lines #-}---- | Equivalent to 'Data.List.length' . 'lines', but in O(1).------ >>> :set -XOverloadedStrings--- >>> lengthInLines ""--- 0--- >>> lengthInLines "foo"--- 1--- >>> lengthInLines "foo\n"--- 1--- >>> lengthInLines "foo\n\n"--- 2--- >>> lengthInLines "foo\nbar"--- 2----lengthInLines :: TextLines -> Word-lengthInLines = coerce I.lengthInLines-{-# INLINE lengthInLines #-}---- | Split at given line, O(1).------ >>> :set -XOverloadedStrings--- >>> map (\l -> splitAtLine l "foo\nbar") [0..3]--- [("","foo\nbar"),("foo\n","bar"),("foo\nbar",""),("foo\nbar","")]----splitAtLine :: HasCallStack => Word -> TextLines -> (TextLines, TextLines)-splitAtLine = coerce I.splitAtLine-{-# INLINE splitAtLine #-}- lengthTextUtf16 :: Text -> Word #if MIN_VERSION_text(2,0,0) lengthTextUtf16 (Text (TA.ByteArray arr) off len) = fromIntegral $ unsafeDupablePerformIO $@@ -145,7 +70,6 @@ #else lengthTextUtf16 (Text _ _ len) = I.intToWord len #endif-{-# INLINABLE lengthTextUtf16 #-}  -- | Length in UTF-16 code units. -- Takes linear time.@@ -156,9 +80,8 @@ -- >>> Data.Text.Lines.length "fя𐀀" -- 3 ---length :: TextLines -> Word-length = lengthTextUtf16 . toText-{-# INLINE length #-}+length :: I.TextLines -> Word+length = lengthTextUtf16 . I.toText  -- | Represent a position in a text. data Position = Position@@ -167,7 +90,7 @@   } deriving (Eq, Ord, Show)  instance NFData Position where-  rnf (Position a b) = rnf a `seq` rnf b+  rnf = (`seq` ())  -- | Associativity does not hold when 'posLine' overflows. instance Semigroup Position where@@ -190,15 +113,14 @@ -- Position {posLine = 2, posColumn = 0} -- lengthAsPosition-  :: TextLines+  :: I.TextLines   -> Position-lengthAsPosition (TextLines (I.TextLines (Text arr off len) nls)) = Position+lengthAsPosition (I.TextLines (Text arr off len) nls) = Position   { posLine = I.intToWord $ U.length nls   , posColumn = lengthTextUtf16 $ Text arr nl (off + len - nl)   }   where     nl = if U.null nls then off else U.last nls + 1-{-# INLINABLE lengthAsPosition #-}  splitTextAtUtf16Index :: Word -> Text -> Maybe (Text, Text) splitTextAtUtf16Index k t@(Text arr off len)@@ -222,9 +144,8 @@       k' = I.wordToInt k       c = TA.unsafeIndex arr (off + k') #endif-{-# INLINABLE splitTextAtUtf16Index #-} --- | Combination of 'splitAtLine' and subsequent 'splitAt'.+-- | Combination of 'I.splitAtLine' and subsequent 'splitAt'. -- If requested number of code units splits a code point in half, return 'Nothing'. -- Time is linear in 'posColumn', but does not depend on 'posLine'. --@@ -245,14 +166,14 @@ splitAtPosition   :: HasCallStack   => Position-  -> TextLines-  -> Maybe (TextLines, TextLines)-splitAtPosition (Position line column) (TextLines (I.TextLines (Text arr off len) nls)) =+  -> I.TextLines+  -> Maybe (I.TextLines, I.TextLines)+splitAtPosition (Position line column) (I.TextLines (Text arr off len) nls) =   case splitTextAtUtf16Index column tx of     Nothing -> Nothing     Just (Text _ off' len', tz) -> let n = I.binarySearch nls (off' + len') in Just-      ( TextLines $ I.textLines (Text arr off (off' + len' - off)) (U.take n nls)-      , TextLines $ I.textLines tz (U.drop n nls))+      ( I.textLines (Text arr off (off' + len' - off)) (U.take n nls)+      , I.textLines tz (U.drop n nls))   where     arrLen = off + len     nl@@ -260,7 +181,6 @@       | line > I.intToWord (U.length nls) = arrLen       | otherwise = nls U.! (I.wordToInt line - 1) + 1     tx = Text arr nl (arrLen - nl)-{-# INLINABLE splitAtPosition #-}  -- | Split at given UTF-16 code unit. -- If requested number of code units splits a code point in half, return 'Nothing'.@@ -270,6 +190,5 @@ -- >>> map (\c -> splitAt c "fя𐀀") [0..4] -- [Just ("","fя𐀀"),Just ("f","я𐀀"),Just ("fя","𐀀"),Nothing,Just ("fя𐀀","")] ---splitAt :: HasCallStack => Word -> TextLines -> Maybe (TextLines, TextLines)+splitAt :: HasCallStack => Word -> I.TextLines -> Maybe (I.TextLines, I.TextLines) splitAt = splitAtPosition . Position 0-{-# INLINE splitAt #-}
src/Data/Text/Utf16/Rope.hs view
@@ -41,7 +41,7 @@ import Data.Bool (Bool(..), otherwise) import Data.Char (Char) import Data.Eq (Eq, (==))-import Data.Function ((.), ($), on, id)+import Data.Function ((.), ($), on) import Data.Maybe (Maybe(..)) import Data.Monoid (Monoid(..)) import Data.Ord (Ord, compare, (<), (<=), Ordering(..))@@ -65,13 +65,23 @@ #endif  -- | Rope of 'Text' chunks with logarithmic concatenation.+-- This rope offers an interface, based on UTF-16 code units.+-- Use "Data.Text.Rope", if you need code points,+-- or "Data.Text.Utf16.Rope.Mixed", if you need both interfaces. data Rope   = Empty-  | Node !Rope !TL.TextLines !Rope !Word !Position+  | Node+    { _ropeLeft          :: !Rope+    , _ropeMiddle        :: !TL.TextLines+    , _ropeRight         :: !Rope+    , _ropeUtf16Len      :: !Word+    , _ropeUtf16LenAsPos :: !Position+    }  instance NFData Rope where   rnf Empty = ()-  rnf (Node a b c d e) = rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e+  -- No need to deepseq strict fields, for which WHNF = NF+  rnf (Node l _ r _ _) = rnf l `seq` rnf r  instance Eq Rope where   (==) = (==) `on` toLazyText@@ -237,6 +247,9 @@ -- >>> lengthInLines "foo\nbar" -- 2 --+-- If you do not care about ignoring the last newline character,+-- you can use 'posLine' . 'lengthAsPosition' instead, which works in O(1).+-- lengthInLines :: Rope -> Word lengthInLines rp = case lastChar rp of   Nothing -> 0@@ -244,8 +257,7 @@  -- | Glue chunks into 'TL.TextLines', linear time. toTextLines :: Rope -> TL.TextLines-toTextLines = foldMapRope id--- TODO make it fast! implement via mconcat for TextLines+toTextLines = mconcat . foldMapRope (:[])  toLazyText :: Rope -> TextLazy.Text toLazyText = foldMapRope (TextLazy.fromStrict . TL.toText)
+ src/Data/Text/Utf16/Rope/Mixed.hs view
@@ -0,0 +1,491 @@+-- |+-- Copyright:   (c) 2021-2022 Andrew Lelechenko+-- Licence:     BSD3+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TupleSections #-}++#ifdef DEBUG+#define DEFRAGMENTATION_THRESHOLD 4+#else+#define DEFRAGMENTATION_THRESHOLD 4096+#endif++module Data.Text.Utf16.Rope.Mixed+  ( Rope+  , fromText+  , fromTextLines+  , toText+  , toTextLines+  , null+  -- * Lines+  , lines+  , lengthInLines+  , splitAtLine+  -- * Code points+  , charLength+  , charSplitAt+  , charLengthAsPosition+  , charSplitAtPosition+  -- * UTF-16 code units+  , utf16Length+  , utf16SplitAt+  , utf16LengthAsPosition+  , utf16SplitAtPosition+  ) where++import Prelude ((-), (+), seq)+import Control.DeepSeq (NFData, rnf)+import Data.Bool (Bool(..), otherwise)+import Data.Char (Char)+import Data.Eq (Eq, (==))+import Data.Function ((.), ($), on)+import Data.Maybe (Maybe(..))+import Data.Monoid (Monoid(..))+import Data.Ord (Ord, compare, (<), (<=), Ordering(..))+import Data.Semigroup (Semigroup(..))+import Data.String (IsString(..))+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TextLazy+import qualified Data.Text.Lazy.Builder as Builder+import Data.Text.Lines.Internal (TextLines)+import qualified Data.Text.Lines.Internal as TL (null, fromText, toText, lines, splitAtLine)+import qualified Data.Text.Lines as Char+import qualified Data.Text.Utf16.Lines as Utf16+import Data.Word (Word)+import Text.Show (Show)++#ifdef DEBUG+import Prelude (error)+import GHC.Stack (HasCallStack)+#else+#define HasCallStack ()+import Text.Show (show)+#endif++-- | Rope of 'Text' chunks with logarithmic concatenation.+-- This rope offers two interfaces: one based on code points+-- and another one based on UTF-16 code units. This comes with a price+-- of double bookkeeping and is less performant than "Data.Text.Rope"+-- or "Data.Text.Utf16.Rope".+data Rope+  = Empty+  | Node+    { _ropeLeft          :: !Rope+    , _ropeMiddle        :: !TextLines+    , _ropeRight         :: !Rope+    , _ropeCharLen       :: !Word+    , _ropeCharLenAsPos  :: !Char.Position+    , _ropeUtf16Len      :: !Word+    , _ropeUtf16LenAsPos :: !Utf16.Position+    }++instance NFData Rope where+  rnf Empty = ()+  -- No need to deepseq strict fields, for which WHNF = NF+  rnf (Node l _ r _ _ _ _) = rnf l `seq` rnf r++instance Eq Rope where+  (==) = (==) `on` toLazyText++instance Ord Rope where+  compare = compare `on` toLazyText++#ifdef DEBUG+deriving instance Show Rope+#else+instance Show Rope where+  show = show . toLazyText+#endif++instance IsString Rope where+  fromString = fromTextLines . fromString++-- | Check whether a rope is empty, O(1).+null :: Rope -> Bool+null = \case+  Empty -> True+  Node{} -> False++-- | Length in code points, similar to @Data.Text.@'Data.Text.length', O(1).+--+-- >>> :set -XOverloadedStrings+-- >>> charLength "fя𐀀"+-- 3+--+charLength :: Rope -> Word+charLength = \case+  Empty -> 0+  Node _ _ _ w _ _ _ -> w++-- | Length in UTF-16 code units, O(1).+--+-- >>> :set -XOverloadedStrings+-- >>> utf16Length "fя𐀀"+-- 4+--+utf16Length :: Rope -> Word+utf16Length = \case+  Empty -> 0+  Node _ _ _ _ _ w _ -> w++-- | Measure text length as an amount of lines and columns, O(1).+--+-- >>> :set -XOverloadedStrings+-- >>> charLengthAsPosition "f𐀀"+-- Position {posLine = 0, posColumn = 2}+-- >>> charLengthAsPosition "f\n𐀀"+-- Position {posLine = 1, posColumn = 1}+-- >>> charLengthAsPosition "f\n𐀀\n"+-- Position {posLine = 2, posColumn = 0}+--+charLengthAsPosition :: Rope -> Char.Position+charLengthAsPosition = \case+  Empty -> mempty+  Node _ _ _ _ p _ _ -> p++-- | Measure text length as an amount of lines and columns, O(1).+--+-- >>> :set -XOverloadedStrings+-- >>> utf16LengthAsPosition "f𐀀"+-- Position {posLine = 0, posColumn = 3}+-- >>> utf16LengthAsPosition "f\n𐀀"+-- Position {posLine = 1, posColumn = 2}+-- >>> utf16LengthAsPosition "f\n𐀀\n"+-- Position {posLine = 2, posColumn = 0}+--+utf16LengthAsPosition :: Rope -> Utf16.Position+utf16LengthAsPosition = \case+  Empty -> mempty+  Node _ _ _ _ _ _ p -> p++instance Semigroup Rope where+  Empty <> t = t+  t <> Empty = t+  Node l1 c1 r1 u1 p1 u1' p1' <> Node l2 c2 r2 u2 p2 u2' p2' = defragment+    l1+    c1+    (Node (r1 <> l2) c2 r2 (charLength r1 + u2) (charLengthAsPosition r1 <> p2) (utf16Length r1 + u2') (utf16LengthAsPosition r1 <> p2'))+    (u1 + u2)+    (p1 <> p2)+    (u1' + u2')+    (p1' <> p2')++instance Monoid Rope where+  mempty = Empty+  mappend = (<>)++defragment :: HasCallStack => Rope -> TextLines -> Rope -> Word -> Char.Position -> Word -> Utf16.Position -> Rope+defragment !l !c !r !u !p !u' !p'+#ifdef DEBUG+  | TL.null c = error "Data.Text.Lines: violated internal invariant"+#endif+  | u < DEFRAGMENTATION_THRESHOLD+  = Node Empty (toTextLines rp) Empty u p u' p'+  | otherwise+  = rp+  where+    rp = Node l c r u p u' p'++-- | Create from 'TextLines', linear time.+fromTextLines :: TextLines -> Rope+fromTextLines tl+  | TL.null tl = Empty+  | otherwise = Node Empty tl Empty (Char.length tl) (Char.lengthAsPosition tl) (Utf16.length tl) (Utf16.lengthAsPosition tl)++node :: HasCallStack => Rope -> TextLines -> Rope -> Rope+node l c r = defragment l c r totalLength totalLengthAsPosition totalLength' totalLengthAsPosition'+  where+    totalLength = charLength l + Char.length c + charLength r+    totalLengthAsPosition = charLengthAsPosition l <> Char.lengthAsPosition c <> charLengthAsPosition r+    totalLength' = utf16Length l + Utf16.length c + utf16Length r+    totalLengthAsPosition' = utf16LengthAsPosition l <> Utf16.lengthAsPosition c <> utf16LengthAsPosition r++(|>) :: Rope -> TextLines -> Rope+tr |> tl+  | TL.null tl = tr+  | otherwise = node tr tl Empty++(<|) :: TextLines -> Rope -> Rope+tl <| tr+  | TL.null tl = tr+  | otherwise = node Empty tl tr++-- | Create from 'Text', linear time.+fromText :: Text -> Rope+fromText = fromTextLines . TL.fromText++foldMapRope :: Monoid a => (TextLines -> a) -> Rope -> a+foldMapRope f = go+  where+    go = \case+      Empty -> mempty+      Node l c r _ _ _ _ -> go l `mappend` f c `mappend` go r++data Lines = Lines ![Text] !Bool++instance Semigroup Lines where+  Lines [] _ <> ls = ls+  ls <> Lines [] _ = ls+  Lines xs x <> Lines ys y = Lines (if x then xs <> ys else go xs ys) y+    where+      go [] vs = vs+      go [u] (v : vs) = (u <> v) : vs+      go (u : us) vs = u : go us vs++instance Monoid Lines where+  mempty = Lines [] False+  mappend = (<>)++-- | Split into lines by @\\n@, similar to @Data.Text.@'Data.Text.lines'.+-- Each line is produced in O(1).+--+-- >>> :set -XOverloadedStrings+-- >>> lines ""+-- []+-- >>> lines "foo"+-- ["foo"]+-- >>> lines "foo\n"+-- ["foo"]+-- >>> lines "foo\n\n"+-- ["foo",""]+-- >>> lines "foo\nbar"+-- ["foo","bar"]+--+lines :: Rope -> [Text]+lines = (\(Lines ls _) -> ls) . foldMapRope+  -- This assumes that there are no empty chunks:+  (\tl -> Lines (TL.lines tl) (T.last (TL.toText tl) == '\n'))++lastChar :: Rope -> Maybe Char+lastChar = \case+  Empty -> Nothing+  -- This assumes that there are no empty chunks:+  Node _ c Empty _ _ _ _ -> Just $ T.last $ TL.toText c+  Node _ _ r _ _ _ _ -> lastChar r++-- | Equivalent to 'Data.List.length' . 'lines', but in logarithmic time.+--+-- >>> :set -XOverloadedStrings+-- >>> lengthInLines ""+-- 0+-- >>> lengthInLines "foo"+-- 1+-- >>> lengthInLines "foo\n"+-- 1+-- >>> lengthInLines "foo\n\n"+-- 2+-- >>> lengthInLines "foo\nbar"+-- 2+--+-- If you do not care about ignoring the last newline character,+-- you can use 'Char.posLine' . 'charLengthAsPosition' instead, which works in O(1).+--+lengthInLines :: Rope -> Word+lengthInLines rp = case lastChar rp of+  Nothing -> 0+  Just ch -> Char.posLine (charLengthAsPosition rp) + (if ch == '\n' then 0 else 1)++-- | Glue chunks into 'TextLines', linear time.+toTextLines :: Rope -> TextLines+toTextLines = mconcat . foldMapRope (:[])++toLazyText :: Rope -> TextLazy.Text+toLazyText = foldMapRope (TextLazy.fromStrict . TL.toText)++-- | Glue chunks into 'Text', linear time.+toText :: Rope -> Text+toText = TextLazy.toStrict . Builder.toLazyText . foldMapRope (Builder.fromText . TL.toText)++-- | Split at given code point, similar to @Data.Text.@'Data.Text.splitAt'.+-- Takes linear time.+--+-- >>> :set -XOverloadedStrings+-- >>> map (\c -> charSplitAt c "fя𐀀") [0..4]+-- [("","fя𐀀"),("f","я𐀀"),("fя","𐀀"),("fя𐀀",""),("fя𐀀","")]+--+charSplitAt :: HasCallStack => Word -> Rope -> (Rope, Rope)+charSplitAt !len = \case+  Empty -> (Empty, Empty)+  Node l c r _ _ _ _+    | len <= ll -> case charSplitAt len l of+        (before, after) -> (before, node after c r)+    | len <= llc -> case Char.splitAt (len - ll) c of+      (before, after) -> (l |> before, after <| r)+    | otherwise -> case charSplitAt (len - llc) r of+      (before, after) -> (node l c before, after)+    where+      ll = charLength l+      llc = ll + Char.length c++-- | Split at given UTF-16 code unit.+-- If requested number of code units splits a code point in half, return 'Nothing'.+-- Takes linear time.+--+-- >>> :set -XOverloadedStrings+-- >>> map (\c -> utf16SplitAt c "fя𐀀") [0..4]+-- [Just ("","fя𐀀"),Just ("f","я𐀀"),Just ("fя","𐀀"),Nothing,Just ("fя𐀀","")]+--+utf16SplitAt :: HasCallStack => Word -> Rope -> Maybe (Rope, Rope)+utf16SplitAt !len = \case+  Empty -> Just (Empty, Empty)+  Node l c r _ _ _ _+    | len <= ll -> case utf16SplitAt len l of+        Nothing -> Nothing+        Just (before, after) -> Just (before, node after c r)+    | len <= llc -> case Utf16.splitAt (len - ll) c of+      Nothing -> Nothing+      Just (before, after) -> Just (l |> before, after <| r)+    | otherwise -> case utf16SplitAt (len - llc) r of+      Nothing -> Nothing+      Just (before, after) -> Just (node l c before, after)+    where+      ll = utf16Length l+      llc = ll + Utf16.length c++-- | Split at given line, logarithmic time.+--+-- >>> :set -XOverloadedStrings+-- >>> map (\l -> splitAtLine l "foo\nbar") [0..3]+-- [("","foo\nbar"),("foo\n","bar"),("foo\nbar",""),("foo\nbar","")]+--+splitAtLine :: HasCallStack => Word -> Rope -> (Rope, Rope)+splitAtLine !len = \case+  Empty -> (Empty, Empty)+  Node l c r _ _ _ _+    | len <= ll -> case splitAtLine len l of+      (before, after) -> (before, node after c r)+    | len <= llc -> case TL.splitAtLine (len - ll) c of+      (before, after) -> (l |> before, after <| r)+    | otherwise -> case splitAtLine (len - llc) r of+      (before, after) -> (node l c before, after)+    where+      -- posLine is the same both in Char.lengthAsPosition and Utf16.lengthAsPosition+      ll = Char.posLine (charLengthAsPosition l)+      llc = ll + Char.posLine (Char.lengthAsPosition c)++charSubOnRope :: Rope -> Char.Position -> Char.Position -> Char.Position+charSubOnRope rp (Char.Position xl xc) (Char.Position yl yc) = case xl `compare` yl of+  GT -> Char.Position (xl - yl) xc+  EQ -> Char.Position 0 (xc - yc)+  LT -> Char.Position 0 (xc - charLength rp')+  where+    (_, rp') = splitAtLine xl rp++utf16SubOnRope :: Rope -> Utf16.Position -> Utf16.Position -> Utf16.Position+utf16SubOnRope rp (Utf16.Position xl xc) (Utf16.Position yl yc) = case xl `compare` yl of+  GT -> Utf16.Position (xl - yl) xc+  EQ -> Utf16.Position 0 (xc - yc)+  LT -> Utf16.Position 0 (xc - utf16Length rp')+  where+    (_, rp') = splitAtLine xl rp++charSubOnLines :: Char.TextLines -> Char.Position -> Char.Position -> Char.Position+charSubOnLines tl (Char.Position xl xc) (Char.Position yl yc) = case xl `compare` yl of+  GT -> Char.Position (xl - yl) xc+  EQ -> Char.Position 0 (xc - yc)+  LT -> Char.Position 0 (xc - Char.length tl')+  where+    (_, tl') = Char.splitAtLine xl tl++utf16SubOnLines :: Utf16.TextLines -> Utf16.Position -> Utf16.Position -> Utf16.Position+utf16SubOnLines tl (Utf16.Position xl xc) (Utf16.Position yl yc) = case xl `compare` yl of+  GT -> Utf16.Position (xl - yl) xc+  EQ -> Utf16.Position 0 (xc - yc)+  LT -> Utf16.Position 0 (xc - Utf16.length tl')+  where+    (_, tl') = Utf16.splitAtLine xl tl++-- | Combination of 'splitAtLine' and subsequent 'charSplitAt'.+-- Time is linear in 'Char.posColumn' and logarithmic in 'Char.posLine'.+--+-- >>> :set -XOverloadedStrings+-- >>> charSplitAtPosition (Position 1 0) "f\n𐀀я"+-- ("f\n","𐀀я")+-- >>> charSplitAtPosition (Position 1 1) "f\n𐀀я"+-- ("f\n𐀀","я")+-- >>> charSplitAtPosition (Position 1 2) "f\n𐀀я"+-- ("f\n𐀀я","")+-- >>> charSplitAtPosition (Position 0 2) "f\n𐀀я"+-- ("f\n","𐀀я")+-- >>> charSplitAtPosition (Position 0 3) "f\n𐀀я"+-- ("f\n𐀀","я")+-- >>> charSplitAtPosition (Position 0 4) "f\n𐀀я"+-- ("f\n𐀀я","")+--+charSplitAtPosition :: HasCallStack => Char.Position -> Rope -> (Rope, Rope)+charSplitAtPosition (Char.Position 0 0) = (mempty,)+charSplitAtPosition !len = \case+  Empty -> (Empty, Empty)+  Node l c r _ _ _ _+    | len <= ll -> case charSplitAtPosition len l of+      (before, after)+        | null after -> case charSplitAtPosition len' (c <| r) of+          (r', r'') -> (l <> r', r'')+        | otherwise -> (before, node after c r)+    | len <= llc -> case Char.splitAtPosition len' c of+      (before, after)+        | TL.null after -> case charSplitAtPosition len'' r of+          (r', r'') -> ((l |> c) <> r', r'')+        | otherwise -> (l |> before, after <| r)+    | otherwise -> case charSplitAtPosition len'' r of+      (before, after) -> (node l c before, after)+    where+      ll = charLengthAsPosition l+      lc = Char.lengthAsPosition c+      llc = ll <> lc+      len' = charSubOnRope l len ll+      len'' = charSubOnLines c len' lc++-- | Combination of 'splitAtLine' and subsequent 'utf16SplitAt'.+-- Time is linear in 'Utf16.posColumn' and logarithmic in 'Utf16.posLine'.+--+-- >>> :set -XOverloadedStrings+-- >>> utf16SplitAtPosition (Position 1 0) "f\n𐀀я"+-- Just ("f\n","𐀀я")+-- >>> utf16SplitAtPosition (Position 1 1) "f\n𐀀я"+-- Nothing+-- >>> utf16SplitAtPosition (Position 1 2) "f\n𐀀я"+-- Just ("f\n𐀀","я")+-- >>> utf16SplitAtPosition (Position 0 2) "f\n𐀀я"+-- Just ("f\n","𐀀я")+-- >>> utf16SplitAtPosition (Position 0 3) "f\n𐀀я"+-- Nothing+-- >>> utf16SplitAtPosition (Position 0 4) "f\n𐀀я"+-- Just ("f\n𐀀","я")+--+utf16SplitAtPosition :: HasCallStack => Utf16.Position -> Rope -> Maybe (Rope, Rope)+utf16SplitAtPosition (Utf16.Position 0 0) = Just . (mempty,)+utf16SplitAtPosition !len = \case+  Empty -> Just (Empty, Empty)+  Node l c r _ _ _ _+    | len <= ll -> case utf16SplitAtPosition len l of+      Nothing -> Nothing+      Just (before, after)+        | null after -> case utf16SplitAtPosition len' (c <| r) of+          Nothing -> Nothing+          Just (r', r'') -> Just (l <> r', r'')+        | otherwise -> Just (before, node after c r)+    | len <= llc -> case Utf16.splitAtPosition len' c of+      Nothing -> Nothing+      Just (before, after)+        | Utf16.null after -> case utf16SplitAtPosition len'' r of+          Nothing -> Nothing+          Just (r', r'') -> Just ((l |> c) <> r', r'')+        | otherwise -> Just (l |> before, after <| r)+    | otherwise -> case utf16SplitAtPosition len'' r of+      Nothing -> Nothing+      Just (before, after) -> Just (node l c before, after)+    where+      ll = utf16LengthAsPosition l+      lc = Utf16.lengthAsPosition c+      llc = ll <> lc+      len' = utf16SubOnRope l len ll+      len'' = utf16SubOnLines c len' lc
test/CharLines.hs view
@@ -11,14 +11,15 @@ import Data.Bool ((||), not, (&&)) import Data.Function (($)) import qualified Data.List as L-import Data.Monoid (mempty)+import Data.Monoid (mempty, mconcat) import Data.Ord (min, (>=), (<))-import Data.Semigroup ((<>))+import Data.Semigroup ((<>), stimes, stimesMonoid) import qualified Data.Text as T import Data.Text.Lines import Data.Tuple (snd)+import Data.Word (Word) import Test.Tasty (testGroup, TestTree)-import Test.Tasty.QuickCheck (testProperty, (===), applyFun, (.||.), (.&&.), (==>))+import Test.Tasty.QuickCheck (Small(..), testProperty, (===), applyFun, (.||.), (.&&.), (==>))  import Utils () @@ -36,6 +37,12 @@     \x -> mempty <> x === (x :: TextLines)   , testProperty "TextLines <> mempty" $     \x -> x <> mempty === (x :: TextLines)++  , testProperty "mconcat" $+    \xs -> L.foldr (<>) mempty xs === mconcat (xs :: [TextLines])++  , testProperty "stimes" $+    \(Small n) xs -> stimesMonoid n xs === stimes (n :: Word) (xs :: TextLines)    , testProperty "Position associativity" $     \x y z -> posLine x < maxBound - posLine y && posLine y < maxBound - posLine z ==>
test/Main.hs view
@@ -9,6 +9,7 @@  import qualified CharLines import qualified CharRope+import qualified MixedRope import qualified Utf16Lines import qualified Utf16Rope @@ -23,4 +24,5 @@   , CharRope.testSuite   , Utf16Lines.testSuite   , Utf16Rope.testSuite+  , MixedRope.testSuite   ]
+ test/MixedRope.hs view
@@ -0,0 +1,86 @@+-- |+-- Copyright:   (c) 2021-2022 Andrew Lelechenko+-- Licence:     BSD3+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>++module MixedRope+  ( testSuite+  ) where++import Prelude ((+), (-))+import Data.Bool (Bool(..), (&&))+import Data.Function (($))+import Data.Maybe (Maybe(..), isJust)+import Data.Semigroup ((<>))+import qualified Data.Text.Lines as Char+import qualified Data.Text.Utf16.Lines as Utf16+import qualified Data.Text.Utf16.Rope.Mixed as Mixed+import Test.Tasty (testGroup, TestTree)+import Test.Tasty.QuickCheck (testProperty, (===), property, (.&&.), counterexample)++import Utils ()++testSuite :: TestTree+testSuite = testGroup "Utf16 Mixed"+  [ testProperty "char null" $+    \x -> Mixed.null x === Char.null (Mixed.toTextLines x)++  , testProperty "charLength" $+    \x -> Mixed.charLength x === Char.length (Mixed.toTextLines x)+  , testProperty "utf16Length" $+    \x -> Mixed.utf16Length x === Utf16.length (Mixed.toTextLines x)++  , testProperty "lengthInLines" $+    \x -> Mixed.lengthInLines x === Char.lengthInLines (Mixed.toTextLines x)++  , testProperty "lines" $+    \x -> Mixed.lines x === Char.lines (Mixed.toTextLines x)++  , testProperty "splitAtLine" $+    \i x -> let (y, z) = Mixed.splitAtLine i x in+      (Mixed.toTextLines y, Mixed.toTextLines z) === Char.splitAtLine i (Mixed.toTextLines x)++  , testProperty "charSplitAt 1" $+    \i x -> case Mixed.charSplitAt i x of+      (y, z) -> x === y <> z+  , testProperty "charSplitAt 2" $+    \i x -> case (Mixed.charSplitAt i x, Char.splitAt i (Char.fromText $ Mixed.toText x)) of+      ((y, z), (y', z')) -> Char.fromText (Mixed.toText y) === y' .&&. Char.fromText (Mixed.toText z) === z'++  , testProperty "utf16SplitAt 1" $+    \i x -> case Mixed.utf16SplitAt i x of+      Nothing -> property True+      Just (y, z) -> x === y <> z+  , testProperty "utf16SplitAt 2" $+    \i x -> case (Mixed.utf16SplitAt i x, Utf16.splitAt i (Utf16.fromText $ Mixed.toText x)) of+      (Nothing, Nothing) -> property True+      (Nothing, Just{}) -> counterexample "can split TextLines, but not Mixed" False+      (Just{}, Nothing) -> counterexample "can split Mixed, but not TextLines" False+      (Just (y, z), Just (y', z')) -> Utf16.fromText (Mixed.toText y) === y' .&&. Utf16.fromText (Mixed.toText z) === z'+  , testProperty "splitAt 3" $+    \i x -> case Mixed.utf16SplitAt i x of+      Just{} -> True+      Nothing -> isJust (Mixed.utf16SplitAt (i - 1) x) && isJust (Mixed.utf16SplitAt (i + 1) x)++  , testProperty "charSplitAtPosition 1" $+    \i x -> case Mixed.charSplitAtPosition i x of+      (y, z) -> x === y <> z+  , testProperty "charSplitAtPosition 2" $+    \i x -> case (Mixed.charSplitAtPosition i x, Char.splitAtPosition i (Char.fromText $ Mixed.toText x)) of+      ((y, z), (y', z')) -> Char.fromText (Mixed.toText y) === y' .&&. Char.fromText (Mixed.toText z) === z'++  , testProperty "utf16SplitAtPosition 1" $+    \i x -> case Mixed.utf16SplitAtPosition i x of+      Nothing -> property True+      Just (y, z) -> x === y <> z+  , testProperty "utf16SplitAtPosition 2" $+    \i x -> case (Mixed.utf16SplitAtPosition i x, Utf16.splitAtPosition i (Utf16.fromText $ Mixed.toText x)) of+      (Nothing, Nothing) -> property True+      (Nothing, Just{}) -> counterexample "can split TextLines, but not Mixed" False+      (Just{}, Nothing) -> counterexample "can split Mixed, but not TextLines" False+      (Just (y, z), Just (y', z')) -> Utf16.fromText (Mixed.toText y) === y' .&&. Utf16.fromText (Mixed.toText z) === z'+  , testProperty "utf16SplitAtPosition 3" $+    \i x -> case Mixed.utf16SplitAtPosition i x of+      Just{} -> True+      Nothing -> isJust (Mixed.utf16SplitAtPosition (i <> Utf16.Position 0 1) x)+  ]
test/Utf16Lines.hs view
@@ -12,14 +12,15 @@ import Data.Function (($)) import qualified Data.List as L import Data.Maybe (Maybe(..), isJust)-import Data.Monoid (mempty)+import Data.Monoid (mempty, mconcat) import Data.Ord (min, (>=), (<))-import Data.Semigroup ((<>))+import Data.Semigroup ((<>), stimes, stimesMonoid) import qualified Data.Text as T import Data.Text.Utf16.Lines import Data.Tuple (snd)+import Data.Word (Word) import Test.Tasty (testGroup, TestTree)-import Test.Tasty.QuickCheck (testProperty, (===), property, (.||.), (.&&.), (==>))+import Test.Tasty.QuickCheck (Small(..), testProperty, (===), property, (.||.), (.&&.), (==>))  import Utils @@ -37,6 +38,12 @@     \x -> mempty <> x === (x :: TextLines)   , testProperty "TextLines <> mempty" $     \x -> x <> mempty === (x :: TextLines)++  , testProperty "mconcat" $+    \xs -> L.foldr (<>) mempty xs === mconcat (xs :: [TextLines])++  , testProperty "stimes" $+    \(Small n) xs -> stimesMonoid n xs === stimes (n :: Word) (xs :: TextLines)    , testProperty "Position associativity" $     \x y z -> posLine x < maxBound - posLine y && posLine y < maxBound - posLine z ==>
test/Utils.hs view
@@ -21,6 +21,7 @@ import qualified Data.Text.Rope as CharRope import qualified Data.Text.Utf16.Lines as Utf16 import qualified Data.Text.Utf16.Rope as Utf16Rope+import qualified Data.Text.Utf16.Rope.Mixed as MixedRope import Data.Word (Word) import Test.Tasty.QuickCheck (Gen, Arbitrary (arbitrary), arbitrary, shrink, frequency, arbitraryASCIIChar, arbitraryUnicodeChar, listOf, oneof) import Data.Monoid (mconcat, mappend)@@ -56,10 +57,6 @@   arbitrary = Char.fromText <$> arbitrary   shrink = L.map Char.fromText . shrink . Char.toText -instance Arbitrary Utf16.TextLines where-  arbitrary = Utf16.fromText <$> arbitrary-  shrink = L.map Utf16.fromText . shrink . Utf16.toText- instance Arbitrary Char.Position where   arbitrary = oneof     [ Char.Position <$> arbitrary <*> arbitrary@@ -99,3 +96,13 @@     | Utf16Rope.null rp = []     | otherwise = L.concatMap (\i -> maybe [] (\(x, y) -> [x, y]) (Utf16Rope.splitAt i rp))                   [1..Utf16Rope.length rp - 1]++instance Arbitrary MixedRope.Rope where+  arbitrary = frequency+    [ (9, mconcat . L.map MixedRope.fromText <$> arbitrary)+    , (1, mappend <$> arbitrary <*> arbitrary)+    ]+  shrink rp+    | MixedRope.null rp = []+    | otherwise = L.concatMap (\i -> (\(x, y) -> [x, y]) (MixedRope.charSplitAt i rp))+                  [1..MixedRope.charLength rp - 1]
text-rope.cabal view
@@ -1,5 +1,5 @@ name: text-rope-version: 0.1+version: 0.2 cabal-version: >=1.10 build-type: Simple license: BSD3@@ -18,7 +18,7 @@ data-files:   bench/bench.txt -tested-with: GHC == 9.2.1, GHC==9.0.2, GHC==8.10.7, GHC==8.8.4, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2+tested-with: GHC == 9.2.2, GHC==9.0.2, GHC==8.10.7, GHC==8.8.4, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2  source-repository head   type: git@@ -35,6 +35,7 @@     Data.Text.Rope     Data.Text.Utf16.Lines     Data.Text.Utf16.Rope+    Data.Text.Utf16.Rope.Mixed   other-modules:     Data.Text.Lines.Internal   build-depends:@@ -44,9 +45,11 @@     vector >=0.11   default-language: Haskell2010   hs-source-dirs: src-  ghc-options: -O2 -Wall -Wcompat+  ghc-options: -O2 -Wall -Wcompat -fexpose-all-unfoldings   if flag(debug)     cpp-options: -DDEBUG+    if impl(ghc >= 9.2.2)+      ghc-options: -fcheck-prim-bounds   c-sources:     cbits/utils.c @@ -56,6 +59,7 @@   other-modules:     CharLines     CharRope+    MixedRope     Utf16Lines     Utf16Rope     Utils