{-# LANGUAGE TemplateHaskell #-}
module Client.EditBox
( EditBox
, content
, pos
, tabSeed
, delete
, backspace
, home
, end
, killHome
, killEnd
, killWord
, paste
, left
, right
, leftWord
, rightWord
, insert
, insertString
, empty
, earlier
, later
, success
) where
import Control.Lens
import Data.Char
data EditBox = EditBox
{ _content :: !String
, _pos :: !Int
, _history :: ![String]
, _historyPos :: !Int
, _yankBuffer :: !(String)
, _tabSeed :: !(Maybe String)
}
deriving (Read, Show)
makeLenses ''EditBox
-- | Default 'EditBox value
empty :: EditBox
empty = EditBox
{ _content = ""
, _pos = 0
, _history = []
, _historyPos = -1
, _yankBuffer = ""
, _tabSeed = Nothing
}
-- | Sets the given string to the yank buffer unless the string is empty.
updateYankBuffer :: String -> EditBox -> EditBox
updateYankBuffer str
| null str = id
| otherwise = set yankBuffer str
-- | Indicate that the contents of the text box were successfully used
-- by the program. This clears the contents and cursor and updates the
-- history.
success :: EditBox -> EditBox
success e
= over history (cons (view content e))
$ set content ""
$ set tabSeed Nothing
$ set historyPos (-1)
$ set pos 0 e
-- | Update the editbox to reflect the earlier element in the history.
earlier :: EditBox -> Maybe EditBox
earlier e =
do let i = view historyPos e + 1
x <- preview (history . ix i) e
return $ set content x
$ set pos (length x)
$ set historyPos i e
-- | Update the editbox to reflect the later element in the history.
later :: EditBox -> Maybe EditBox
later e
| i < 0 = Nothing
| i == 0 = Just
$ set content ""
$ set pos 0
$ set historyPos (-1) e
| otherwise =
do x <- preview (history . ix (i-1)) e
return $ set content x
$ set pos (length x)
$ set historyPos (i-1) e
where
i = view historyPos e
-- Remove a character without the associated checks
-- internal helper for backspace and delete
removeImpl :: EditBox -> EditBox
removeImpl e
= set content (a++drop 1 b)
$ set tabSeed Nothing
$ over pos (min (views content length e - 1)) e
where
(a,b) = splitAt (view pos e) (view content e)
-- | Delete the character after the cursor.
delete :: EditBox -> EditBox
delete e
| view pos e < views content length e = removeImpl e
| otherwise = e
-- | Delete the character before the cursor.
backspace :: EditBox -> EditBox
backspace e
| view pos e > 0 = removeImpl (left e)
| otherwise = e
-- | Jump the cursor to the beginning of the input.
home :: EditBox -> EditBox
home
= set tabSeed Nothing
. set pos 0
-- | Jump the cursor to the end of the input.
end :: EditBox -> EditBox
end e
= set tabSeed Nothing
$ set pos (views content length e) e
-- | Delete all text from the cursor to the end and store it in
-- the yank buffer.
killEnd :: EditBox -> EditBox
killEnd e
= set content keep
$ updateYankBuffer kill e
where
(keep,kill) = splitAt (view pos e) (view content e)
-- | Delete all text from the cursor to the beginning and store it in
-- the yank buffer.
killHome :: EditBox -> EditBox
killHome e
= set content keep
$ set pos 0
$ set tabSeed Nothing
$ updateYankBuffer kill e
where
(kill,keep) = splitAt (view pos e) (view content e)
-- | Insert the yank buffer at the cursor.
paste :: EditBox -> EditBox
paste e = insertString (view yankBuffer e) e
-- | Kill the content from the cursor back to the previous word boundary.
-- When @yank@ is set the yank buffer will be updated.
killWord :: Bool {- ^ yank -} -> EditBox -> EditBox
killWord yank e
= set pos (length l')
$ sometimesUpdateYank
$ set content (l'++r) e
where
(l,r) = splitAt (view pos e) (view content e)
(sp,l1) = span isSpace (reverse l)
(wd,l2) = break isSpace l1
l' = reverse l2
yanked = reverse (sp++wd)
sometimesUpdateYank
| yank = updateYankBuffer yanked
| otherwise = id
-- | Insert a character at the cursor and advance the cursor.
insert :: Char -> EditBox -> EditBox
insert c
= set tabSeed Nothing
. insertString [c]
-- | Insert a string at the cursor and advance the cursor.
insertString :: String -> EditBox -> EditBox
insertString str e
= over pos (+length str)
$ set content (a ++ str ++ b) e
where
(a,b) = splitAt (view pos e) (view content e)
-- | Move the cursor left.
left :: EditBox -> EditBox
left = over pos (max 0 . subtract 1)
-- | Move the cursor right.
right :: EditBox -> EditBox
right e = over pos (min (views content length e) . (+1)) e
-- | Move the cursor left to the previous word boundary.
leftWord :: EditBox -> EditBox
leftWord e =
case search of
[] -> set pos 0 e
(i,_):_ -> set pos (i+1) e
where
search = dropWhile (isAlphaNum . snd)
$ dropWhile (not . isAlphaNum . snd)
$ reverse
$ take (view pos e)
$ zip [0..]
$ view content e
-- | Move the cursor right to the next word boundary.
rightWord :: EditBox -> EditBox
rightWord e =
case search of
[] -> set pos (views content length e) e
(i,_):_ -> set pos i e
where
search = dropWhile (isAlphaNum . snd)
$ dropWhile (not . isAlphaNum . snd)
$ drop (view pos e)
$ zip [0..]
$ view content e