packages feed

cursor-0.3.1.0: src/Cursor/Text.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeFamilies #-}

module Cursor.Text
  ( TextCursor (..),
    emptyTextCursor,
    makeTextCursor,
    makeTextCursorWithSelection,
    rebuildTextCursor,
    textCursorNull,
    textCursorLength,
    textCursorIndex,
    textCursorSelectPrev,
    textCursorSelectNext,
    textCursorSelectIndex,
    textCursorSelectStart,
    textCursorSelectEnd,
    textCursorPrevChar,
    textCursorNextChar,
    textCursorSelectBeginWord,
    textCursorSelectEndWord,
    textCursorSelectNextWord,
    textCursorSelectPrevWord,
    textCursorInsert,
    textCursorAppend,
    textCursorInsertString,
    textCursorAppendString,
    textCursorInsertText,
    textCursorAppendText,
    textCursorRemove,
    textCursorDelete,
    textCursorSplit,
    textCursorCombine,
  )
where

import Control.DeepSeq
import Cursor.List
import Cursor.Types
import Data.Char
import Data.Text (Text)
import qualified Data.Text as T
import Data.Validity
import GHC.Generics (Generic)
import Lens.Micro

-- | A cursor for single-line texts
newtype TextCursor = TextCursor
  { textCursorList :: ListCursor Char
  }
  deriving (Show, Eq, Generic)

instance Validity TextCursor where
  validate (TextCursor lc) =
    mconcat
      [ genericValidate lc,
        decorateList (rebuildListCursor lc) $ \c ->
          mconcat
            [ declare "The character is not a newline character" $ c /= '\n',
              declare "The character is a safe character" $ isSafeChar c
            ]
      ]

instance NFData TextCursor

emptyTextCursor :: TextCursor
emptyTextCursor = TextCursor emptyListCursor

makeTextCursor :: Text -> Maybe TextCursor
makeTextCursor t = makeTextCursorWithSelection (T.length t) t

makeTextCursorWithSelection :: Int -> Text -> Maybe TextCursor
makeTextCursorWithSelection i t =
  case T.split (== '\n') t of
    [l] -> TextCursor <$> makeListCursorWithSelection i (T.unpack l)
    _ -> Nothing

rebuildTextCursor :: TextCursor -> Text
rebuildTextCursor = T.pack . rebuildListCursor . textCursorList

textCursorListCursorL ::
  Functor f => (ListCursor Char -> f (ListCursor Char)) -> TextCursor -> f TextCursor
textCursorListCursorL = lens textCursorList (\tc lc -> tc {textCursorList = lc})

textCursorNull :: TextCursor -> Bool
textCursorNull = listCursorNull . textCursorList

textCursorLength :: TextCursor -> Int
textCursorLength = listCursorLength . textCursorList

textCursorIndex :: TextCursor -> Int
textCursorIndex = listCursorIndex . textCursorList

textCursorSelectPrev :: TextCursor -> Maybe TextCursor
textCursorSelectPrev = textCursorListCursorL listCursorSelectPrev

textCursorSelectNext :: TextCursor -> Maybe TextCursor
textCursorSelectNext = textCursorListCursorL listCursorSelectNext

textCursorSelectIndex :: Int -> TextCursor -> TextCursor
textCursorSelectIndex ix_ = textCursorListCursorL %~ listCursorSelectIndex ix_

textCursorSelectStart :: TextCursor -> TextCursor
textCursorSelectStart = textCursorListCursorL %~ listCursorSelectStart

textCursorSelectEnd :: TextCursor -> TextCursor
textCursorSelectEnd = textCursorListCursorL %~ listCursorSelectEnd

textCursorPrevChar :: TextCursor -> Maybe Char
textCursorPrevChar = listCursorPrevItem . textCursorList

textCursorNextChar :: TextCursor -> Maybe Char
textCursorNextChar = listCursorNextItem . textCursorList

-- | Move to the beginning of the word
--
-- * @"hell|o"@ -> @"|hello"@
-- * @"hello   | world"@ -> @"|hello    world"@
-- * @"hello |world"@ -> @"hello |world"@
-- * @"| hello"@ -> @"| hello"@
textCursorSelectBeginWord :: TextCursor -> TextCursor
textCursorSelectBeginWord tc =
  let goLeft = maybe tc textCursorSelectBeginWord (textCursorSelectPrev tc)
   in case textCursorPrevChar tc of
        Nothing -> tc
        Just p
          | isSpace p -> case textCursorNextChar tc of
            Nothing -> goLeft
            Just n
              | isSpace n -> goLeft
              | otherwise -> tc
          | otherwise -> goLeft

-- | Move to the end of the word
--
-- * @"hell|o"@ -> @"hello|"@
-- * @"hello   | world"@ -> @"hello    world|"@
-- * @"hello| world"@ -> @"hello| world"@
-- * @"hello |"@ -> @"hello |"@
textCursorSelectEndWord :: TextCursor -> TextCursor
textCursorSelectEndWord tc =
  let goRight = maybe tc textCursorSelectEndWord (textCursorSelectNext tc)
   in case textCursorNextChar tc of
        Nothing -> tc
        Just p
          | isSpace p -> case textCursorPrevChar tc of
            Nothing -> goRight
            Just n
              | isSpace n -> goRight
              | otherwise -> tc
          | otherwise -> goRight

-- | Move to the beginning of the next word
--
-- * @"|hello"@ -> @"hello|"@
-- * @"hell|o world"@ -> @"hello |world"@
-- * @"hello| world"@ -> @"hello |world"@
-- * @"hello |"@ -> @"hello |"@
textCursorSelectNextWord :: TextCursor -> TextCursor
textCursorSelectNextWord tc =
  case (textCursorPrevChar tc, textCursorNextChar tc) of
    (_, Nothing) -> tc
    (Just p, Just n) ->
      case (isSpace p, isSpace n) of
        (_, True) -> TextCursor $ listCursorNextUntil (not . isSpace) lc
        _ -> textCursorSelectNextWord . TextCursor $ listCursorNextUntil isSpace lc
    _ -> textCursorSelectNextWord $ TextCursor $ listCursorNextUntil isSpace lc
  where
    lc = textCursorList tc

-- | Move to the end of the previous word
--
-- * @"hello|"@ -> @"|hello"@
-- * @"hello w|orld"@ -> @"hello| world"@
-- * @"hello |world"@ -> @"hello| world"@
-- * @" h|ello"@ -> @"| hello"@
textCursorSelectPrevWord :: TextCursor -> TextCursor
textCursorSelectPrevWord tc =
  case (textCursorPrevChar tc, textCursorNextChar tc) of
    (Nothing, _) -> tc
    (Just p, Just n) ->
      case (isSpace p, isSpace n) of
        (True, _) -> TextCursor $ listCursorPrevUntil (not . isSpace) lc
        _ -> textCursorSelectPrevWord . TextCursor $ listCursorPrevUntil isSpace lc
    _ -> textCursorSelectPrevWord . TextCursor $ listCursorPrevUntil isSpace lc
  where
    lc = textCursorList tc

textCursorInsert :: Char -> TextCursor -> Maybe TextCursor
textCursorInsert '\n' _ = Nothing
textCursorInsert c tc =
  if isSafeChar c
    then Just (tc & textCursorListCursorL %~ listCursorInsert c)
    else Nothing

textCursorAppend :: Char -> TextCursor -> Maybe TextCursor
textCursorAppend '\n' _ = Nothing
textCursorAppend c tc =
  if isSafeChar c
    then Just (tc & textCursorListCursorL %~ listCursorAppend c)
    else Nothing

textCursorInsertString :: String -> TextCursor -> Maybe TextCursor
textCursorInsertString s tc =
  if any (\c -> c == '\n' || not (isSafeChar c)) s
    then Nothing
    else Just $ tc & textCursorListCursorL %~ listCursorInsertList s

textCursorAppendString :: String -> TextCursor -> Maybe TextCursor
textCursorAppendString s tc =
  if any (\c -> c == '\n' || not (isSafeChar c)) s
    then Nothing
    else Just $ tc & textCursorListCursorL %~ listCursorAppendList s

textCursorInsertText :: Text -> TextCursor -> Maybe TextCursor
textCursorInsertText = textCursorInsertString . T.unpack

textCursorAppendText :: Text -> TextCursor -> Maybe TextCursor
textCursorAppendText = textCursorAppendString . T.unpack

textCursorRemove :: TextCursor -> Maybe (DeleteOrUpdate TextCursor)
textCursorRemove = focusPossibleDeleteOrUpdate textCursorListCursorL listCursorRemove

textCursorDelete :: TextCursor -> Maybe (DeleteOrUpdate TextCursor)
textCursorDelete = focusPossibleDeleteOrUpdate textCursorListCursorL listCursorDelete

textCursorSplit :: TextCursor -> (TextCursor, TextCursor)
textCursorSplit tc =
  let (lc1, lc2) = listCursorSplit $ textCursorList tc
   in (TextCursor lc1, TextCursor lc2)

textCursorCombine :: TextCursor -> TextCursor -> TextCursor
textCursorCombine (TextCursor lc1) (TextCursor lc2) =
  TextCursor {textCursorList = listCursorCombine lc1 lc2}