module Data.Text.Zipper
( TextZipper
, mkZipper
, textZipper
, stringZipper
, clearZipper
, vectorZipper
, getText
, currentLine
, cursorPosition
, lineLengths
, getLineLimit
, moveCursor
, moveRight
, moveLeft
, moveUp
, moveDown
, gotoEOL
, gotoBOL
, currentChar
, nextChar
, previousChar
, insertChar
, insertMany
, deletePrevChar
, deleteChar
, breakLine
, killToEOL
, killToBOL
, transposeChars
)
where
import Control.Applicative ((<$>))
import Control.DeepSeq
import Data.Char (isPrint)
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Text.Zipper.Vector as V
data TextZipper a =
TZ { toLeft :: a
, toRight :: a
, above :: [a]
, below :: [a]
, fromChar :: Char -> a
, drop_ :: Int -> a -> a
, take_ :: Int -> a -> a
, length_ :: a -> Int
, last_ :: a -> Char
, init_ :: a -> a
, null_ :: a -> Bool
, lines_ :: a -> [a]
, toList_ :: a -> [Char]
, lineLimit :: Maybe Int
}
instance (NFData a) => NFData (TextZipper a) where
rnf z = (toLeft z) `deepseq`
(toRight z) `deepseq`
(above z) `deepseq`
(below z) `deepseq`
()
getLineLimit :: TextZipper a -> Maybe Int
getLineLimit = lineLimit
instance (Eq a) => Eq (TextZipper a) where
a == b = and [ toLeft a == toLeft b
, toRight a == toRight b
, above a == above b
, below a == below b
]
instance (Show a) => Show (TextZipper a) where
show tz = concat [ "TextZipper { "
, "above = "
, show $ above tz
, ", below = "
, show $ below tz
, ", toLeft = "
, show $ toLeft tz
, ", toRight = "
, show $ toRight tz
, " }"
]
mkZipper :: (Monoid a) =>
(Char -> a)
-> (Int -> a -> a)
-> (Int -> a -> a)
-> (a -> Int)
-> (a -> Char)
-> (a -> a)
-> (a -> Bool)
-> (a -> [a])
-> (a -> [Char])
-> [a]
-> Maybe Int
-> TextZipper a
mkZipper fromCh drp tk lngth lst int nl linesFunc toListF ls lmt =
let limitedLs = case lmt of
Nothing -> ls
Just n -> take n ls
(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
getText :: (Monoid a) => TextZipper a -> [a]
getText tz = concat [ above tz
, [currentLine tz]
, below tz
]
lineLengths :: (Monoid a) => TextZipper a -> [Int]
lineLengths tz = (length_ tz) <$> concat [ above tz
, [currentLine tz]
, below tz
]
cursorPosition :: TextZipper a -> (Int, Int)
cursorPosition tz = (length $ above tz, length_ tz $ toLeft tz)
moveCursor :: (Monoid a) => (Int, Int) -> TextZipper a -> TextZipper a
moveCursor (row, col) tz =
let t = getText tz
in if row < 0
|| row >= length t
|| col < 0
|| col > length_ tz (t !! row)
then tz
else tz { above = take row t
, below = drop (row + 1) t
, toLeft = take_ tz col (t !! row)
, toRight = drop_ tz col (t !! row)
}
isFirstLine :: TextZipper a -> Bool
isFirstLine = null . above
isLastLine :: TextZipper a -> Bool
isLastLine = (== 0) . length . below
nextLine :: TextZipper a -> a
nextLine = head . below
currentLine :: (Monoid a) => TextZipper a -> a
currentLine tz = (toLeft tz) `mappend` (toRight tz)
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
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
breakLine :: (Monoid a) => TextZipper a -> TextZipper a
breakLine tz = maybe tz id $ breakLine_ tz
breakLine_ :: (Monoid a) => TextZipper a -> Maybe (TextZipper a)
breakLine_ tz =
let modified = tz { above = above tz ++ [toLeft tz]
, toLeft = mempty
}
in case lineLimit tz of
Just lim -> if length (above tz) + length (below tz) + 2 > lim
then Nothing
else Just modified
Nothing -> Just modified
gotoEOL :: (Monoid a) => TextZipper a -> TextZipper a
gotoEOL tz = tz { toLeft = currentLine tz
, toRight = mempty
}
killToEOL :: (Monoid a) => TextZipper a -> TextZipper a
killToEOL tz
| (null_ tz $ toLeft tz) && (null_ tz $ toRight tz) &&
(not $ null $ below tz) =
tz { toRight = head $ below tz
, below = tail $ below tz
}
| otherwise = tz { toRight = mempty
}
killToBOL :: Monoid a => TextZipper a -> TextZipper a
killToBOL tz = tz { toLeft = mempty
}
deletePrevChar :: (Eq a, Monoid a) => TextZipper a -> TextZipper a
deletePrevChar tz
| moveLeft tz == tz = tz
| otherwise = deleteChar $ moveLeft tz
deleteChar :: (Monoid a) => TextZipper a -> TextZipper a
deleteChar tz
| (not $ null_ tz (toRight tz)) =
tz { toRight = drop_ tz 1 $ toRight tz
}
| null_ tz (toRight tz) && (not $ null $ below tz) =
tz { toRight = head $ below tz
, below = tail $ below tz
}
| otherwise = tz
currentChar :: TextZipper a -> Maybe Char
currentChar tz
| not (null_ tz (toRight tz)) =
Just (last_ tz (take_ tz 1 (toRight tz)))
| otherwise = Nothing
nextChar :: (Monoid a) => TextZipper a -> Maybe Char
nextChar tz = currentChar (moveRight tz)
previousChar :: (Monoid a) => TextZipper a -> Maybe Char
previousChar tz
| snd (cursorPosition tz) == 0 && all (null_ tz) (above tz) =
Nothing
| otherwise =
currentChar (moveLeft tz)
gotoBOL :: (Monoid a) => TextZipper a -> TextZipper a
gotoBOL tz = tz { toLeft = mempty
, toRight = currentLine tz
}
moveRight :: (Monoid a) => TextZipper a -> TextZipper a
moveRight tz
| not (null_ tz (toRight tz)) =
tz { toLeft = toLeft tz
`mappend` (take_ tz 1 $ toRight tz)
, toRight = drop_ tz 1 (toRight tz)
}
| not $ null (below tz) =
tz { above = above tz ++ [toLeft tz]
, below = tail $ below tz
, toLeft = mempty
, toRight = nextLine tz
}
| otherwise = tz
moveLeft :: (Monoid a) => TextZipper a -> TextZipper a
moveLeft tz
| not $ null_ tz (toLeft tz) =
tz { toLeft = init_ tz $ toLeft tz
, toRight = fromChar tz (last_ tz (toLeft tz))
`mappend` toRight tz
}
| not $ null (above tz) =
tz { above = init $ above tz
, below = currentLine tz : below tz
, toLeft = last $ above tz
, toRight = mempty
}
| otherwise = tz
moveUp :: (Monoid a) => TextZipper a -> TextZipper a
moveUp tz
| (not $ isFirstLine tz) &&
(length_ tz $ last $ above tz) >= length_ tz (toLeft tz) =
tz { below = currentLine tz : below tz
, above = init $ above tz
, toLeft = take_ tz (length_ tz $ toLeft tz) (last $ above tz)
, toRight = drop_ tz (length_ tz $ toLeft tz) (last $ above tz)
}
| (not $ isFirstLine tz) =
tz { above = init $ above tz
, below = currentLine tz : below tz
, toLeft = last $ above tz
, toRight = mempty
}
| otherwise = gotoBOL tz
moveDown :: (Monoid a) => TextZipper a -> TextZipper a
moveDown tz
| (not $ isLastLine tz) &&
(length_ tz $ nextLine tz) >= length_ tz (toLeft tz) =
tz { below = tail $ below tz
, above = above tz ++ [currentLine tz]
, toLeft = take_ tz (length_ tz $ toLeft tz) (nextLine tz)
, toRight = drop_ tz (length_ tz $ toLeft tz) (nextLine tz)
}
| (not $ isLastLine tz) =
tz { above = above tz ++ [currentLine tz]
, below = tail $ below tz
, toLeft = nextLine tz
, toRight = mempty
}
| otherwise = gotoEOL tz
transposeChars :: (Monoid a) => TextZipper a -> TextZipper a
transposeChars tz
| null_ tz (toLeft tz) = tz
| null_ tz (toRight tz) =
if length_ tz (toLeft tz) < 2
then tz
else let prefixLen = length_ tz (toLeft tz) 2
prefix = take_ tz prefixLen (toLeft tz)
lastTwo = drop_ tz prefixLen (toLeft tz)
a = take_ tz 1 lastTwo
b = drop_ tz 1 lastTwo
in tz { toLeft = prefix <> b <> a
}
| otherwise = tz { toLeft = (init_ tz $ toLeft tz) <>
(take_ tz 1 $ toRight tz) <>
(fromChar tz $ last_ tz $ toLeft tz)
, toRight = (drop_ tz 1 $ toRight tz)
}
stringZipper :: [String] -> Maybe Int -> TextZipper String
stringZipper =
mkZipper (:[]) drop take length last init null lines id
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
clearZipper :: (Monoid a) => TextZipper a -> TextZipper a
clearZipper tz =
tz { toLeft = mempty
, toRight = mempty
, above = []
, below = []
}
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