diff --git a/src/Data/Text/Lines/Internal.hs b/src/Data/Text/Lines/Internal.hs index f55e224..714a0ce 100644 --- a/src/Data/Text/Lines/Internal.hs +++ b/src/Data/Text/Lines/Internal.hs @@ -244,9 +244,9 @@ lines (TextLines (Text arr off len) nls) = go off (U.toList nls) splitAtLine :: HasCallStack => Word -> TextLines -> (TextLines, TextLines) splitAtLine k = splitAtPosition (Position k 0) --- | Get line with given index, O(1). +-- | Get line with given 0-based index, O(1). -- The resulting Text does not contain newlines. --- Returns "" if the line index is out of bounds +-- Returns "" if the line index is out of bounds. -- -- >>> :set -XOverloadedStrings -- >>> map (\l -> getLine l "fя𐀀\n☺bar\n\n") [0..3] diff --git a/src/Data/Text/Mixed/Rope.hs b/src/Data/Text/Mixed/Rope.hs index e1b5261..8788639 100644 --- a/src/Data/Text/Mixed/Rope.hs +++ b/src/Data/Text/Mixed/Rope.hs @@ -28,6 +28,7 @@ module Data.Text.Mixed.Rope , lines , lengthInLines , splitAtLine + , getLine -- * Code points , charLength , charSplitAt @@ -562,3 +563,21 @@ utf16SplitAtPosition (Utf16.Position l c) rp = do let (beforeLine, afterLine) = splitAtLine l rp (beforeColumn, afterColumn) <- utf16SplitAt c afterLine Just (beforeLine <> beforeColumn, afterColumn) + +-- | Get a line by its 0-based index. +-- Returns "" if the index is out of bounds. +-- The result doesn't contain newline characters. +-- +-- >>> :set -XOverloadedStrings +-- >>> map (\l -> getLine l "foo\nbar\n😊😊\n\n") [0..3] +-- ["foo","bar","😊😊",""] +-- +getLine :: Word -> Rope -> Text +getLine lineIdx rp = + case T.unsnoc firstLine of + Just (firstLineInit, '\n') -> firstLineInit + _ -> firstLine + where + (_, afterIndex) = splitAtLine lineIdx rp + (firstLineRope, _ ) = splitAtLine 1 afterIndex + firstLine = toText firstLineRope diff --git a/src/Data/Text/Rope.hs b/src/Data/Text/Rope.hs index f0a4fb4..f6fda7a 100644 --- a/src/Data/Text/Rope.hs +++ b/src/Data/Text/Rope.hs @@ -28,6 +28,7 @@ module Data.Text.Rope , lines , lengthInLines , splitAtLine + , getLine -- * Code points , length , splitAt @@ -388,3 +389,21 @@ splitAtPosition (Position l c) rp = (beforeLine <> beforeColumn, afterColumn) where (beforeLine, afterLine) = splitAtLine l rp (beforeColumn, afterColumn) = splitAt c afterLine + +-- | Get a line by its 0-based index. +-- Returns "" if the index is out of bounds. +-- The result doesn't contain newline characters. +-- +-- >>> :set -XOverloadedStrings +-- >>> map (\l -> getLine l "foo\nbar\n😊😊\n\n") [0..3] +-- ["foo","bar","😊😊",""] +-- +getLine :: Word -> Rope -> Text +getLine lineIdx rp = + case T.unsnoc firstLine of + Just (firstLineInit, '\n') -> firstLineInit + _ -> firstLine + where + (_, afterIndex) = splitAtLine lineIdx rp + (firstLineRope, _ ) = splitAtLine 1 afterIndex + firstLine = toText firstLineRope diff --git a/src/Data/Text/Utf16/Rope.hs b/src/Data/Text/Utf16/Rope.hs index d3e5f5f..4536149 100644 --- a/src/Data/Text/Utf16/Rope.hs +++ b/src/Data/Text/Utf16/Rope.hs @@ -28,6 +28,7 @@ module Data.Text.Utf16.Rope , lines , lengthInLines , splitAtLine + , getLine -- * UTF-16 code units , length , splitAt @@ -391,3 +392,21 @@ splitAtPosition (Position l c) rp = do let (beforeLine, afterLine) = splitAtLine l rp (beforeColumn, afterColumn) <- splitAt c afterLine Just (beforeLine <> beforeColumn, afterColumn) + +-- | Get a line by its 0-based index. +-- Returns "" if the index is out of bounds. +-- The result doesn't contain newline characters. +-- +-- >>> :set -XOverloadedStrings +-- >>> map (\l -> getLine l "foo\nbar\n😊😊\n\n") [0..3] +-- ["foo","bar","😊😊",""] +-- +getLine :: Word -> Rope -> Text +getLine lineIdx rp = + case T.unsnoc firstLine of + Just (firstLineInit, '\n') -> firstLineInit + _ -> firstLine + where + (_, afterIndex) = splitAtLine lineIdx rp + (firstLineRope, _ ) = splitAtLine 1 afterIndex + firstLine = toText firstLineRope \ No newline at end of file diff --git a/src/Data/Text/Utf8/Rope.hs b/src/Data/Text/Utf8/Rope.hs index 2243c36..d4fbe5e 100644 --- a/src/Data/Text/Utf8/Rope.hs +++ b/src/Data/Text/Utf8/Rope.hs @@ -23,6 +23,7 @@ module Data.Text.Utf8.Rope , lines , lengthInLines , splitAtLine + , getLine -- * UTF-8 code units , length , splitAt @@ -386,3 +387,21 @@ splitAtPosition (Position l c) rp = do let (beforeLine, afterLine) = splitAtLine l rp (beforeColumn, afterColumn) <- splitAt c afterLine Just (beforeLine <> beforeColumn, afterColumn) + +-- | Get a line by its 0-based index. +-- Returns "" if the index is out of bounds. +-- The result doesn't contain newline characters. +-- +-- >>> :set -XOverloadedStrings +-- >>> map (\l -> getLine l "foo\nbar\n😊😊\n\n") [0..3] +-- ["foo","bar","😊😊",""] +-- +getLine :: Word -> Rope -> Text +getLine lineIdx rp = + case T.unsnoc firstLine of + Just (firstLineInit, '\n') -> firstLineInit + _ -> firstLine + where + (_, afterIndex) = splitAtLine lineIdx rp + (firstLineRope, _ ) = splitAtLine 1 afterIndex + firstLine = toText firstLineRope diff --git a/test/CharRope.hs b/test/CharRope.hs index ebec181..1191179 100644 --- a/test/CharRope.hs +++ b/test/CharRope.hs @@ -7,13 +7,15 @@ module CharRope ( testSuite ) where -import Prelude () +import Prelude ((+), (-)) import Data.Function (($)) import Data.Semigroup ((<>)) +import Data.Monoid (mempty) +import qualified Data.List as L import qualified Data.Text.Lines as Lines import qualified Data.Text.Rope as Rope import Test.Tasty (testGroup, TestTree) -import Test.Tasty.QuickCheck (testProperty, (===), (.&&.)) +import Test.Tasty.QuickCheck (Positive(..), conjoin, testProperty, (===), (.&&.)) import Utils () @@ -48,4 +50,13 @@ testSuite = testGroup "Char Rope" , testProperty "splitAtPosition 2" $ \i x -> case (Rope.splitAtPosition i x, Lines.splitAtPosition i (Lines.fromText $ Rope.toText x)) of ((y, z), (y', z')) -> Lines.fromText (Rope.toText y) === y' .&&. Lines.fromText (Rope.toText z) === z' + + , testProperty "forall i in bounds: getLine i x == lines x !! i" $ + \x -> let lns = Rope.lines x in + conjoin $ L.zipWith (\idx ln -> Rope.getLine idx x === ln) [0..] lns + , testProperty "forall i out of bounds: getLine i x == mempty" $ + \x (Positive offset) -> + let maxIdx = L.genericLength (Rope.lines x) - 1 + outOfBoundsIdx = maxIdx + offset + in Rope.getLine outOfBoundsIdx x === mempty ] diff --git a/test/MixedRope.hs b/test/MixedRope.hs index fd5f678..c7f4cc1 100644 --- a/test/MixedRope.hs +++ b/test/MixedRope.hs @@ -11,13 +11,15 @@ import Prelude ((+), (-)) import Data.Bool (Bool(..), (&&)) import Data.Function (($)) import Data.Maybe (Maybe(..), isJust) +import Data.Monoid (mempty) import Data.Semigroup ((<>)) +import qualified Data.List as L import qualified Data.Text.Lines as Char import qualified Data.Text.Utf8.Lines as Utf8 import qualified Data.Text.Utf16.Lines as Utf16 import qualified Data.Text.Mixed.Rope as Mixed import Test.Tasty (testGroup, TestTree) -import Test.Tasty.QuickCheck (testProperty, (===), property, (.&&.), counterexample) +import Test.Tasty.QuickCheck (Positive(..), conjoin, counterexample, property, testProperty, (===), (.&&.)) import Utils () @@ -106,4 +108,13 @@ testSuite = testGroup "Utf16 Mixed" \i x -> case Mixed.utf16SplitAtPosition i x of Just{} -> True Nothing -> isJust (Mixed.utf16SplitAtPosition (i <> Utf16.Position 0 1) x) + + , testProperty "forall i in bounds: getLine i x == lines x !! i" $ + \x -> let lns = Mixed.lines x in + conjoin $ L.zipWith (\idx ln -> Mixed.getLine idx x === ln) [0..] lns + , testProperty "forall i out of bounds: getLine i x == mempty" $ + \x (Positive offset) -> + let maxIdx = L.genericLength (Mixed.lines x) - 1 + outOfBoundsIdx = maxIdx + offset + in Mixed.getLine outOfBoundsIdx x === mempty ] diff --git a/test/Utf16Rope.hs b/test/Utf16Rope.hs index 0338d4f..7009ff5 100644 --- a/test/Utf16Rope.hs +++ b/test/Utf16Rope.hs @@ -11,11 +11,13 @@ import Prelude ((+), (-)) import Data.Bool (Bool(..), (&&)) import Data.Function (($)) import Data.Maybe (Maybe(..), isJust) +import Data.Monoid (mempty) import Data.Semigroup ((<>)) -import qualified Data.Text.Utf16.Lines as Lines +import qualified Data.List as L +import qualified Data.Text.Utf16.Lines as Lines import qualified Data.Text.Utf16.Rope as Rope import Test.Tasty (testGroup, TestTree) -import Test.Tasty.QuickCheck (testProperty, (===), property, (.&&.), counterexample) +import Test.Tasty.QuickCheck (Positive(..), conjoin, counterexample, property, testProperty, (===), (.&&.)) import Utils () @@ -66,4 +68,13 @@ testSuite = testGroup "Utf16 Rope" \i x -> case Rope.splitAtPosition i x of Just{} -> True Nothing -> isJust (Rope.splitAtPosition (i <> Lines.Position 0 1) x) + + , testProperty "forall i in bounds: getLine i x == lines x !! i" $ + \x -> let lns = Rope.lines x in + conjoin $ L.zipWith (\idx ln -> Rope.getLine idx x === ln) [0..] lns + , testProperty "forall i out of bounds: getLine i x == mempty" $ + \x (Positive offset) -> + let maxIdx = L.genericLength (Rope.lines x) - 1 + outOfBoundsIdx = maxIdx + offset + in Rope.getLine outOfBoundsIdx x === mempty ] diff --git a/test/Utf8Rope.hs b/test/Utf8Rope.hs index 18a8f0c..89fa332 100644 --- a/test/Utf8Rope.hs +++ b/test/Utf8Rope.hs @@ -2,15 +2,17 @@ module Utf8Rope ( testSuite ) where -import Prelude () +import Prelude ((+), (-)) import Data.Bool (Bool(..)) import Data.Function (($)) import Data.Maybe (Maybe(..)) +import Data.Monoid (mempty) import Data.Semigroup ((<>)) +import qualified Data.List as L import qualified Data.Text.Utf8.Lines as Lines import qualified Data.Text.Utf8.Rope as Rope import Test.Tasty (testGroup, TestTree) -import Test.Tasty.QuickCheck (testProperty, (===), property, (.&&.), counterexample) +import Test.Tasty.QuickCheck (Positive(..), conjoin, counterexample, property, testProperty, (===), (.&&.)) import Utils () @@ -53,4 +55,13 @@ testSuite = testGroup "Utf8 Rope" (Nothing, Just{}) -> counterexample "can split TextLines, but not Rope" False (Just{}, Nothing) -> counterexample "can split Rope, but not TextLines" False (Just (y, z), Just (y', z')) -> Lines.fromText (Rope.toText y) === y' .&&. Lines.fromText (Rope.toText z) === z' + + , testProperty "forall i in bounds: getLine i x == lines x !! i" $ + \x -> let lns = Rope.lines x in + conjoin $ L.zipWith (\idx ln -> Rope.getLine idx x === ln) [0..] lns + , testProperty "forall i out of bounds: getLine i x == mempty" $ + \x (Positive offset) -> + let maxIdx = L.genericLength (Rope.lines x) - 1 + outOfBoundsIdx = maxIdx + offset + in Rope.getLine outOfBoundsIdx x === mempty ]