text-zipper 0.10.1 → 0.13
raw patch · 5 files changed
Files
- CHANGELOG +0/−109
- CHANGELOG.md +132/−0
- src/Data/Text/Zipper.hs +132/−72
- tests/WordsSpec.hs +20/−2
- text-zipper.cabal +2/−2
− CHANGELOG
@@ -1,109 +0,0 @@--0.10.1--------- * WordSpec: fix a test verification bug (fixed #11)- * WordSpec: generation of random text should never include newlines--0.10-------- Integrated word editing and navigation functions- courtesy of Hans-Peter Deifel's hledger-iadd project (see- Data.Text.Zipper.Generic.Words)-- Added currentChar, nextChar, and previousChar (thanks @kRITZCREEK)--0.9------- insertChar and insertMany now only insert printable characters and- newlines (subject to text zipper line limits)-- The GenericTextZipper class now requires a new method,- toList :: a -> [Char]--0.8.3--------- Fixed insertMany accidental addition of trailing newline--0.8.2--------- Fixed insertMany for zippers with no line limit--0.8.1--------- Added Github links and CHANGELOG to package--0.8------- Added 'transposeChars' function--0.7.1--------- Generic: import everything from Monoid for older GHCs--0.7------- API changes: Add Generic module to abstract over text container types--0.6.1--------- Make insertMany respect the zipper's line limit--0.6------- Add insertMany for faster bulk insertion--0.5------- Added killToBOL function (thanks Hans-Peter Deifel)-- Enabled -Wall-- Added dependency on deepseq-- Added NFData instance for TextZipper--0.4------- Added clearZipper-- Added isFirstLine (thanks Kwang Yul Seo)-- Renamed lastLine to isLastLine (thanks Kwang Yul Seo)--0.3.1--------- Fixed export of vectorZipper--0.3------- Added vectorZipper for zipping over vectors of characters--0.2.1--------- Exported getLineLimit to permit obtaining a zipper's line limit--0.2------- Added support for limiting the number of lines in the zipper-- insertChar "\n" is now equivalent to breakLine-- Improved Show instance for TextZipper--0.1.1--------- Updated package metadata--0.1------Initial release (originally split off from vty-ui)
+ CHANGELOG.md view
@@ -0,0 +1,132 @@++0.13+----++Bug fixes:+ * The zipper constructors now ignores non-printable characters (see+ also #13)+ * `insertMany` now no longer drops the input following a non-printable+ character (#13)++0.12+----++API changes:+ * Added `moveCursorClosest` to allow cursor placement as near as+ possible to a specified location.++0.11+----++API changes:+ * Added `gotoBOF`, `gotoEOF`, `killToBOF`, and `killToEOF` functions+ (thanks Itai Y. Efrat)++0.10.1+------++ * WordSpec: fix a test verification bug (fixed #11)+ * WordSpec: generation of random text should never include newlines++0.10+----++- Integrated word editing and navigation functions+ courtesy of Hans-Peter Deifel's hledger-iadd project (see+ Data.Text.Zipper.Generic.Words)+- Added currentChar, nextChar, and previousChar (thanks @kRITZCREEK)++0.9+---++- insertChar and insertMany now only insert printable characters and+ newlines (subject to text zipper line limits)+- The GenericTextZipper class now requires a new method,+ toList :: a -> [Char]++0.8.3+-----++- Fixed insertMany accidental addition of trailing newline++0.8.2+-----++- Fixed insertMany for zippers with no line limit++0.8.1+-----++- Added Github links and CHANGELOG to package++0.8+---++- Added 'transposeChars' function++0.7.1+-----++- Generic: import everything from Monoid for older GHCs++0.7+---++- API changes: Add Generic module to abstract over text container types++0.6.1+-----++- Make insertMany respect the zipper's line limit++0.6+---++- Add insertMany for faster bulk insertion++0.5+---++- Added killToBOL function (thanks Hans-Peter Deifel)+- Enabled -Wall+- Added dependency on deepseq+- Added NFData instance for TextZipper++0.4+---++- Added clearZipper+- Added isFirstLine (thanks Kwang Yul Seo)+- Renamed lastLine to isLastLine (thanks Kwang Yul Seo)++0.3.1+-----++- Fixed export of vectorZipper++0.3+---++- Added vectorZipper for zipping over vectors of characters++0.2.1+-----++- Exported getLineLimit to permit obtaining a zipper's line limit++0.2+---++- Added support for limiting the number of lines in the zipper+- insertChar "\n" is now equivalent to breakLine+- Improved Show instance for TextZipper++0.1.1+-----++- Updated package metadata++0.1+---++Initial release (originally split off from vty-ui)
src/Data/Text/Zipper.hs view
@@ -1,19 +1,19 @@--- |This module provides a two-dimensional text zipper data structure.+-- | This module provides a two-dimensional text zipper data structure. -- This structure represents a body of text and an editing cursor -- which can be moved throughout the text, along with a set of editing -- transformations. ----- Text zippers are generalized over the set of data types that might--- be used to store lists of characters (e.g., 'String', 'T.Text',--- etc.). As a result, the most general way to create a text zipper--- is to use 'mkZipper' and provide all of the functions required to--- manipulate the underlying text data.+-- Text zippers are generalized over the set of data types that might be+-- used to store lists of characters (e.g., 'String', 'T.Text', etc.).+-- As a result, the most general way to create a text zipper is to use+-- 'mkZipper' and provide all of the functions required to manipulate+-- the underlying text data. -- -- Implementations using 'T.Text' and 'String' are provided. module Data.Text.Zipper ( TextZipper - -- *Construction and extraction+ -- * Construction and extraction , mkZipper , textZipper , stringZipper@@ -25,21 +25,24 @@ , lineLengths , getLineLimit - -- *Navigation functions+ -- * Navigation functions , moveCursor+ , moveCursorClosest , moveRight , moveLeft , moveUp , moveDown , gotoEOL , gotoBOL+ , gotoEOF+ , gotoBOF - -- *Inspection functions+ -- * Inspection functions , currentChar , nextChar , previousChar - -- *Editing functions+ -- * Editing functions , insertChar , insertMany , deletePrevChar@@ -47,6 +50,8 @@ , breakLine , killToEOL , killToBOL+ , killToEOF+ , killToBOF , transposeChars ) where@@ -54,6 +59,7 @@ import Control.Applicative ((<$>)) import Control.DeepSeq import Data.Char (isPrint)+import Data.List (foldl') import Data.Monoid import qualified Data.Text as T import qualified Data.Vector as V@@ -107,9 +113,9 @@ , " }" ] --- |Create a zipper using a custom text storage type. Takes the--- initial text as well as all of the functions necessary to--- manipulate the underlying text values.+-- | Create a zipper using a custom text storage type. Takes the initial+-- text as well as all of the functions necessary to manipulate the+-- underlying text values. mkZipper :: (Monoid a) => (Char -> a) -- ^A singleton constructor.@@ -141,33 +147,35 @@ (first, rest) = if null limitedLs then (mempty, mempty) else (head limitedLs, tail limitedLs)- in TZ mempty first [] rest fromCh drp tk lngth lst int nl linesFunc toListF lmt+ numLines = length ls+ insertLine z (i, l) = (if i < numLines - 1 then breakLine else id) $ insertMany l z+ loadInitial z = foldl' insertLine z $ zip [0..] (first:rest)+ in loadInitial $ TZ mempty mempty mempty mempty fromCh drp tk lngth lst int nl linesFunc toListF lmt --- |Get the text contents of the zipper.+-- | Get the text contents of the zipper. getText :: (Monoid a) => TextZipper a -> [a] getText tz = concat [ above tz , [currentLine tz] , below tz ] --- |Return the lengths of the lines in the zipper.+-- | Return the lengths of the lines in the zipper. lineLengths :: (Monoid a) => TextZipper a -> [Int] lineLengths tz = (length_ tz) <$> concat [ above tz , [currentLine tz] , below tz ] --- |Get the cursor position of the zipper; returns @(row, col)@.+-- | Get the cursor position of the zipper; returns @(row, col)@. -- @row@ ranges from @[0..num_rows-1]@ inclusive; @col@ ranges from--- @[0..length of current line]@ inclusive. Column values equal to--- line width indicate a cursor that is just past the end of a line of--- text.+-- @[0..length of current line]@ inclusive. Column values equal to line+-- width indicate a cursor that is just past the end of a line of text. cursorPosition :: TextZipper a -> (Int, Int) cursorPosition tz = (length $ above tz, length_ tz $ toLeft tz) --- |Move the cursor to the specified row and column. Invalid cursor--- positions will be ignored. Valid cursor positions range as--- described for 'cursorPosition'.+-- | Move the cursor to the specified row and column. Invalid cursor+-- positions will be ignored. Valid cursor positions range as described+-- for 'cursorPosition'. moveCursor :: (Monoid a) => (Int, Int) -> TextZipper a -> TextZipper a moveCursor (row, col) tz = let t = getText tz@@ -182,6 +190,22 @@ , toRight = drop_ tz col (t !! row) } +-- | Move the cursor to the specified row and column. Invalid cursor+-- positions will be reinterpreted as the closest valid position. Valid+-- cursor positions range as described for 'cursorPosition'.+moveCursorClosest :: (Monoid a) => (Int, Int) -> TextZipper a -> TextZipper a+moveCursorClosest (row, col) tz =+ let t = getText tz+ bestRow = min (max 0 $ length t - 1) $ max 0 row+ bestCol = if bestRow < length t+ then min (length_ tz (t !! bestRow)) $ max 0 col+ else 0+ in tz { above = take bestRow t+ , below = drop (bestRow + 1) t+ , toLeft = take_ tz bestCol (t !! bestRow)+ , toRight = drop_ tz bestCol (t !! bestRow)+ }+ isFirstLine :: TextZipper a -> Bool isFirstLine = null . above @@ -191,11 +215,11 @@ nextLine :: TextZipper a -> a nextLine = head . below --- |The line of text on which the zipper's cursor currently resides.+-- | The line of text on which the zipper's cursor currently resides. currentLine :: (Monoid a) => TextZipper a -> a currentLine tz = (toLeft tz) `mappend` (toRight tz) --- |Insert a character at the current cursor position.+-- | Insert a character at the current cursor position. -- -- If the character is a newline, break the current line. --@@ -204,28 +228,19 @@ -- Otherwise insert the character and move the cursor one position to -- the right. insertChar :: (Monoid a) => Char -> TextZipper a -> TextZipper a-insertChar ch tz = maybe tz id $ insertChar_ ch tz--insertChar_ :: (Monoid a) => Char -> TextZipper a -> Maybe (TextZipper a)-insertChar_ ch tz- | ch == '\n' = breakLine_ tz- | isPrint ch = Just $ tz { toLeft = toLeft tz `mappend` (fromChar tz ch) }- | otherwise = Nothing+insertChar ch tz+ | ch == '\n' = breakLine tz+ | isPrint ch = tz { toLeft = toLeft tz `mappend` (fromChar tz ch) }+ | otherwise = tz --- |Insert many characters at the current cursor position. Move the+-- | Insert many characters at the current cursor position. Move the -- cursor to the end of the inserted text. insertMany :: (Monoid a) => a -> TextZipper a -> TextZipper a-insertMany str tz =- let go [] z = z- go (c:cs) z = maybe z (go cs) $ insertChar_ c z- in go (toList_ tz str) tz+insertMany str tz = foldl' (flip insertChar) tz $ toList_ tz str --- |Insert a line break at the current cursor position.+-- | Insert a line break at the current cursor position. breakLine :: (Monoid a) => TextZipper a -> TextZipper a-breakLine tz = maybe tz id $ breakLine_ tz--breakLine_ :: (Monoid a) => TextZipper a -> Maybe (TextZipper a)-breakLine_ tz =+breakLine tz = -- Plus two because we count the current line and the line we are -- about to create; if that number of lines exceeds the limit, -- ignore this operation.@@ -234,18 +249,32 @@ } in case lineLimit tz of Just lim -> if length (above tz) + length (below tz) + 2 > lim- then Nothing- else Just modified- Nothing -> Just modified+ then tz+ else modified+ Nothing -> modified --- |Move the cursor to the end of the current line.+-- | Move the cursor to the end of the current line. gotoEOL :: (Monoid a) => TextZipper a -> TextZipper a gotoEOL tz = tz { toLeft = currentLine tz , toRight = mempty } --- |Remove all text from the cursor position to the end of the current--- line. If the cursor is at the beginning of a line and the line is+-- | Move the cursor to the end of a text zipper.+gotoEOF :: (Monoid a) => TextZipper a -> TextZipper a+gotoEOF tz =+ tz { toLeft = end+ , toRight = mempty+ , above = top+ , below = mempty+ }+ where+ tx = getText tz+ (top, end) = if null tx+ then (mempty, mempty)+ else (init tx, last tx)++-- | Remove all text from the cursor position to the end of the current+-- line. If the cursor is at the beginning of a line and the line is -- empty, the entire line will be removed. killToEOL :: (Monoid a) => TextZipper a -> TextZipper a killToEOL tz@@ -257,21 +286,38 @@ | otherwise = tz { toRight = mempty } --- |Remove all text from the cursor position to the beginning of the+-- | Remove all text from the cursor position to the beginning of the -- current line. killToBOL :: Monoid a => TextZipper a -> TextZipper a killToBOL tz = tz { toLeft = mempty } --- |Delete the character preceding the cursor position, and move the+-- | Remove all text from the cursor position to the end of the text+-- zipper. If the cursor is at the beginning of a line and the line is+-- empty, the entire line will be removed.+killToEOF :: (Monoid a) => TextZipper a -> TextZipper a+killToEOF tz =+ tz { toRight = mempty+ , below = mempty+ }++-- | Remove all text from the cursor position to the beginning of the+-- text zipper.+killToBOF :: Monoid a => TextZipper a -> TextZipper a+killToBOF tz =+ tz { toLeft = mempty+ , above = mempty+ }++-- | Delete the character preceding the cursor position, and move the -- cursor backwards by one character. deletePrevChar :: (Eq a, Monoid a) => TextZipper a -> TextZipper a deletePrevChar tz | moveLeft tz == tz = tz | otherwise = deleteChar $ moveLeft tz --- |Delete the character at the cursor position. Leaves the cursor--- position unchanged. If the cursor is at the end of a line of text,+-- | Delete the character at the cursor position. Leaves the cursor+-- position unchanged. If the cursor is at the end of a line of text, -- this combines the line with the line below. deleteChar :: (Monoid a) => TextZipper a -> TextZipper a deleteChar tz@@ -286,21 +332,21 @@ } | otherwise = tz --- |Get the Char on which the cursor currently resides. If the cursor is--- at the end of the text or the text is empty return @Nothing@.+-- | Get the Char on which the cursor currently resides. If the cursor+-- is at the end of the text or the text is empty return @Nothing@. currentChar :: TextZipper a -> Maybe Char currentChar tz | not (null_ tz (toRight tz)) = Just (last_ tz (take_ tz 1 (toRight tz))) | otherwise = Nothing --- |Get the Char after the cursor position. If the cursor is at the end+-- | Get the Char after the cursor position. If the cursor is at the end -- of a line return the first character of the next line, or if that one -- is empty as well, return @Nothing@. nextChar :: (Monoid a) => TextZipper a -> Maybe Char nextChar tz = currentChar (moveRight tz) --- |Get the Char before the cursor position. If the cursor is at the+-- | Get the Char before the cursor position. If the cursor is at the -- beginning of the text, return @Nothing@ previousChar :: (Monoid a) => TextZipper a -> Maybe Char previousChar tz@@ -311,15 +357,29 @@ | otherwise = currentChar (moveLeft tz) --- |Move the cursor to the beginning of the current line.+-- | Move the cursor to the beginning of the current line. gotoBOL :: (Monoid a) => TextZipper a -> TextZipper a gotoBOL tz = tz { toLeft = mempty , toRight = currentLine tz } --- |Move the cursor right by one position. If the cursor is at the--- end of a line, the cursor is moved to the first position of the--- following line (if any).+-- | Move the cursor to the beginning of a text zipper.+gotoBOF :: (Monoid a) => TextZipper a -> TextZipper a+gotoBOF tz =+ tz { toLeft = mempty+ , toRight = first+ , above = mempty+ , below = rest+ }+ where+ tx = getText tz+ (first, rest) = if null tx+ then (mempty, mempty)+ else (head tx, tail tx)++-- | Move the cursor right by one position. If the cursor is at the end+-- of a line, the cursor is moved to the first position of the following+-- line (if any). moveRight :: (Monoid a) => TextZipper a -> TextZipper a moveRight tz -- Are we able to keep moving right on the current line?@@ -338,9 +398,9 @@ } | otherwise = tz --- |Move the cursor left by one position. If the cursor is at the--- beginning of a line, the cursor is moved to the last position of--- the preceding line (if any).+-- | Move the cursor left by one position. If the cursor is at the+-- beginning of a line, the cursor is moved to the last position of the+-- preceding line (if any). moveLeft :: (Monoid a) => TextZipper a -> TextZipper a moveLeft tz -- Are we able to keep moving left on the current line?@@ -359,8 +419,8 @@ } | otherwise = tz --- |Move the cursor up by one row. If there no are rows above the--- current one, move to the first position of the current row. If the+-- | Move the cursor up by one row. If there no are rows above the+-- current one, move to the first position of the current row. If the -- row above is shorter, move to the end of that row. moveUp :: (Monoid a) => TextZipper a -> TextZipper a moveUp tz@@ -382,9 +442,9 @@ -- If nothing else, go to the beginning of the current line | otherwise = gotoBOL tz --- |Move the cursor down by one row. If there are no rows below the--- current one, move to the last position of the current row. If the--- row below is shorter, move to the end of that row.+-- | Move the cursor down by one row. If there are no rows below the+-- current one, move to the last position of the current row. If the row+-- below is shorter, move to the end of that row. moveDown :: (Monoid a) => TextZipper a -> TextZipper a moveDown tz -- Is there a line below at least as long as the current one?@@ -428,17 +488,17 @@ , toRight = (drop_ tz 1 $ toRight tz) } --- |Construct a zipper from list values.+-- | Construct a zipper from list values. stringZipper :: [String] -> Maybe Int -> TextZipper String stringZipper = mkZipper (:[]) drop take length last init null lines id --- |Construct a zipper from vectors of characters.+-- | Construct a zipper from vectors of characters. vectorZipper :: [V.Vector Char] -> Maybe Int -> TextZipper (V.Vector Char) vectorZipper = mkZipper V.singleton V.drop V.take V.length V.last V.init V.null V.vecLines V.toList --- |Empty a zipper.+-- | Empty a zipper. clearZipper :: (Monoid a) => TextZipper a -> TextZipper a clearZipper tz = tz { toLeft = mempty@@ -447,7 +507,7 @@ , below = [] } --- |Construct a zipper from 'T.Text' values.+-- | Construct a zipper from 'T.Text' values. textZipper :: [T.Text] -> Maybe Int -> TextZipper T.Text textZipper = mkZipper T.singleton T.drop T.take T.length T.last T.init T.null T.lines T.unpack
tests/WordsSpec.hs view
@@ -3,8 +3,6 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} module WordsSpec (spec) where --- This test suite is Copyright (c) Hans-Peter Deifel- import Test.Hspec import Test.QuickCheck @@ -17,10 +15,30 @@ spec :: Spec spec = do+ constructorSpec+ insertCharSpec+ insertManySpec moveWordLeftSpec moveWordRightSpec deletePrevWordSpec deleteWordSpec++constructorSpec :: Spec+constructorSpec = describe "constructor" $ do+ it "inserts only printable characters at construction time" $+ (stringZipper ["abc\x1b def"] Nothing) `shouldBe` (stringZipper ["abc def"] Nothing)++insertCharSpec :: Spec+insertCharSpec = describe "insertChar" $ do+ it "ignores an insert of a non-printable character" $+ let z = stringZipper [] Nothing+ in (insertChar '\x1b' z) `shouldBe` z++insertManySpec :: Spec+insertManySpec = describe "insertMany" $ do+ it "ignores an insert of a non-printable character" $+ let z = stringZipper ["abc"] Nothing+ in (insertMany "ghi\x1bjkl" z) `shouldBe` (insertMany "ghijkl" z) moveWordLeftSpec :: Spec moveWordLeftSpec = describe "moveWordLeft" $ do
text-zipper.cabal view
@@ -1,5 +1,5 @@ name: text-zipper-version: 0.10.1+version: 0.13 synopsis: A text editor zipper library description: This library provides a zipper and API for editing text. license: BSD3@@ -10,7 +10,7 @@ category: Text build-type: Simple cabal-version: >=1.10-data-files: CHANGELOG+data-files: CHANGELOG.md homepage: https://github.com/jtdaugherty/text-zipper/ bug-reports: https://github.com/jtdaugherty/text-zipper/issues