packages feed

haskeline-0.8.4.1: System/Console/Haskeline/Vi.hs

{-# LANGUAGE LambdaCase #-}
#if __GLASGOW_HASKELL__ < 802
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
#endif
module System.Console.Haskeline.Vi (emptyViState, viKeyCommands) where

import System.Console.Haskeline.Command
import System.Console.Haskeline.Monads
import System.Console.Haskeline.Key
import System.Console.Haskeline.Command.Completion
import System.Console.Haskeline.Command.History
import System.Console.Haskeline.Command.KillRing
import System.Console.Haskeline.Command.Undo
import System.Console.Haskeline.LineState
import System.Console.Haskeline.InputT

import Data.Char
import Control.Monad (liftM2, (>=>))
import Control.Monad.Catch (MonadMask)

type EitherMode = Either CommandMode InsertMode

type SavedCommand m = Command (ViT m) (ArgMode CommandMode) EitherMode

data InlineSearch = F -- f | F
                  | T -- t | T

data ViState m = ViState {
            lastCommand :: SavedCommand m,
            lastSearch :: [Grapheme],
            lastInlineSearch :: Maybe (Char, InlineSearch, Direction)
         }

emptyViState :: Monad m => ViState m
emptyViState = ViState {
            lastCommand = return . Left . argState,
            lastSearch = [],
            lastInlineSearch = Nothing
        }

type ViT m = StateT (ViState m) (InputCmdT m)

type InputCmd s t = forall m . (MonadIO m, MonadMask m) => Command (ViT m) s t
type InputKeyCmd s t = forall m . (MonadIO m, MonadMask m) => KeyCommand (ViT m) s t

viKeyCommands :: InputKeyCmd InsertMode (Maybe String)
viKeyCommands = choiceCmd
                [ simpleChar '\n' `useKey` finish
                , ctrlChar 'd' `useKey` eofIfEmpty
                , simpleInsertions >+> viCommands
                , simpleChar '\ESC' `useKey` (change enterCommandMode >=> viCommandActions)
                ]

viCommands :: InputCmd InsertMode (Maybe String)
viCommands = keyCommand viKeyCommands

simpleInsertions :: InputKeyCmd InsertMode InsertMode
simpleInsertions = choiceCmd
                [ simpleKey LeftKey `useKey` change goLeft
                , simpleKey RightKey `useKey` change goRight
                , simpleKey Backspace `useKey` change deletePrev
                , simpleKey Delete `useKey` change deleteNext
                , simpleKey Home `useKey` change moveToStart
                , simpleKey End `useKey` change moveToEnd
                , insertChars
                , ctrlChar 'l' `useKey` clearScreenCmd
                , simpleKey UpKey `useKey` historyBack
                , simpleKey DownKey `useKey` historyForward
                , simpleKey SearchReverse `useKey` searchForPrefix Reverse
                , simpleKey SearchForward `useKey` searchForPrefix Forward
                , searchHistory
                , simpleKey KillLine `useKey` killFromHelper (SimpleMove moveToStart)
                , ctrlChar 'w' `useKey` killFromHelper wordErase
                , completionCmd (simpleChar '\t')
                ]

insertChars :: InputKeyCmd InsertMode InsertMode
insertChars = useChar $ loop []
    where
        loop ds d = change (insertChar d)
                >=> keyChoiceCmd
                        [ useChar $ loop (d:ds)
                        , withoutConsuming (storeCharInsertion (reverse ds))
                        ]
        storeCharInsertion s = storeLastCmd
                             $ change (applyArg $ withCommandMode $ insertString s)
                                >=> return . Left

-- If we receive a ^D and the line is empty, return Nothing
-- otherwise, act like '\n' (mimicking how Readline behaves)
eofIfEmpty :: (Monad m, Save s, Result s) => Command m s (Maybe String)
eofIfEmpty s
    | save s == emptyIM = return Nothing
    | otherwise = finish s

viCommandActions :: InputCmd CommandMode (Maybe String)
viCommandActions = keyChoiceCmd [
                    simpleChar '\n' `useKey` finish
                    , ctrlChar 'd' `useKey` eofIfEmpty
                    , simpleCmdActions >+> viCommandActions
                    , exitingCommands >+> viCommands
                    , repeatedCommands >+> chooseEitherMode
                    ]
    where
        chooseEitherMode :: InputCmd EitherMode (Maybe String)
        chooseEitherMode (Left cm) = viCommandActions cm
        chooseEitherMode (Right im) = viCommands im

exitingCommands :: InputKeyCmd CommandMode InsertMode
exitingCommands = choiceCmd [
                      simpleChar 'i' `useKey` change insertFromCommandMode
                    , simpleChar 'I' `useKey` change (moveToStart . insertFromCommandMode)
                    , simpleKey Home `useKey` change (moveToStart . insertFromCommandMode)
                    , simpleChar 'a' `useKey` change appendFromCommandMode
                    , simpleChar 'A' `useKey` change (moveToEnd . appendFromCommandMode)
                    , simpleKey End `useKey` change (moveToStart . insertFromCommandMode)
                    , simpleChar 's' `useKey` change (insertFromCommandMode . deleteChar)
                    , simpleChar 'S' `useKey` (noArg >=> killAndStoreI killAll)
                    , simpleChar 'C' `useKey` (noArg >=> killAndStoreI (SimpleMove moveToEnd))
                    ]

simpleCmdActions :: InputKeyCmd CommandMode CommandMode
simpleCmdActions = choiceCmd [
                    simpleChar '\ESC' `useKey` change id -- helps break out of loops
                    , simpleChar 'r' `useKey` replaceOnce
                    , simpleChar 'R' `useKey` replaceLoop
                    , simpleChar 'D' `useKey` (noArg >=> killAndStoreC (SimpleMove moveToEnd))
                    , ctrlChar 'l' `useKey` clearScreenCmd
                    , simpleChar 'u' `useKey` commandUndo
                    , ctrlChar 'r' `useKey` commandRedo
                    -- vi-mode quirk: history is put at the start of the line.
                    , simpleChar 'j' `useKey` (historyForward >=> change moveToStart)
                    , simpleChar 'k' `useKey` (historyBack >=> change moveToStart)
                    , simpleKey DownKey `useKey` (historyForward >=> change moveToStart)
                    , simpleKey UpKey `useKey` (historyBack >=> change moveToStart)
                    , simpleChar '/' `useKey` viEnterSearch '/' Reverse
                    , simpleChar '?' `useKey` viEnterSearch '?' Forward
                    , simpleChar 'n' `useKey` viSearchHist Reverse []
                    , simpleChar 'N' `useKey` viSearchHist Forward []
                    , simpleKey KillLine `useKey` (noArg >=> killAndStoreC (SimpleMove moveToStart))
                    ]

inlineSearchActions :: InputKeyCmd (ArgMode CommandMode) CommandMode
inlineSearchActions = choiceCmd
                    [ simpleChar 'f' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch F Forward c >>=) . viInlineSearch id)
                    , simpleChar 'F' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch F Reverse c >>=) . viInlineSearch id)
                    , simpleChar 't' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch T Forward c >>=) . viInlineSearch id)
                    , simpleChar 'T' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch T Reverse c >>=) . viInlineSearch id)
                    , simpleChar ';' `useKey` ((getLastInlineSearch >>=) . viInlineSearch id)
                    , simpleChar ',' `useKey` ((getLastInlineSearch >>=) . viInlineSearch flipDir)
                    ]

viInlineSearch :: Monad m => (Direction -> Direction)
                          -> ArgMode CommandMode
                          -> (Maybe (Char, InlineSearch, Direction))
                          -> CmdM (ViT m) CommandMode
viInlineSearch flipdir = \s -> \case
    Nothing -> return $ argState s
    Just (g, fOrT, dir) -> setState $ (applyArg . withCommandMode . search fOrT (flipdir dir)) (== g) s
    where
        search :: InlineSearch -> Direction -> (Char -> Bool) -> InsertMode -> InsertMode
        search F Forward = goRightUntil . overChar
        search F Reverse = goLeftUntil . overChar
        search T Forward = goRightUntil . beforeChar
        search T Reverse = goLeftUntil . afterChar

getLastInlineSearch :: forall m. Monad m => CmdM (ViT m) (Maybe (Char, InlineSearch, Direction))
getLastInlineSearch = lastInlineSearch <$> (get :: CmdM (ViT m) (ViState m)) -- TODO: ideally this is a usage of `gets`

saveInlineSearch :: forall m. Monad m => InlineSearch -> Direction -> Char
                                      -> CmdM (ViT m) (Maybe (Char, InlineSearch, Direction))
saveInlineSearch fOrT dir char
  = do let ret = Just (char, fOrT, dir)
       modify $ \(vs :: ViState m) -> vs {lastInlineSearch = ret}
       return ret

replaceOnce :: InputCmd CommandMode CommandMode
replaceOnce = try $ changeFromChar replaceChar

repeatedCommands :: InputKeyCmd CommandMode EitherMode
repeatedCommands = choiceCmd [argumented, doBefore noArg repeatableCommands]
    where
        start = foreachDigit startArg ['1'..'9']
        addDigit = foreachDigit addNum ['0'..'9']
        argumented = start >+> loop
        loop = keyChoiceCmd [addDigit >+> loop
                            , repeatableCommands
                            -- if no match, bail out.
                            , withoutConsuming (change argState) >+> return . Left
                            ]

pureMovements :: InputKeyCmd (ArgMode CommandMode) CommandMode
pureMovements = choiceCmd $ map mkSimpleCommand movements
    where
        mkSimpleCommand (k, move) = k `useKey` change (applyCmdArg move)

useMovementsForKill :: (KillHelper -> Command m s t) -> KeyCommand m s t
useMovementsForKill useHelper = choiceCmd $
            specialCases
            ++ map (\(k,move) -> k `useKey` useHelper (SimpleMove move)) movements
    where
        specialCases = [ simpleChar 'e' `useKey` useHelper (SimpleMove goToWordDelEnd)
                       , simpleChar 'E' `useKey` useHelper (SimpleMove goToBigWordDelEnd)
                       , simpleChar '%' `useKey` useHelper (GenericKill deleteMatchingBrace)
                       ]

useInlineSearchForKill :: Monad m => Command (ViT m) s t -> (KillHelper -> Command (ViT m) s t) -> KeyMap (Command (ViT m) s t)
useInlineSearchForKill alternate killCmd = choiceCmd
        [ simpleChar 'f' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch F Forward c >>=) . getSearchAndKill)
        , simpleChar 'F' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch F Reverse c >>=) . getSearchAndKill)
        , simpleChar 't' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch T Forward c >>=) . getSearchAndKill)
        , simpleChar 'T' `useKey` keyCommand (useChar $ \c -> (saveInlineSearch T Reverse c >>=) . getSearchAndKill)
        , simpleChar ';' `useKey` ((getLastInlineSearch >>=) . getSearchAndKill)
        , simpleChar ',' `useKey` ((reverseDir <$> getLastInlineSearch >>=) . getSearchAndKill)
        ]
    where
        getSearchAndKill = \s
          -> \case (Just (g, fOrT, forOrRev)) -> killCmd (SimpleMove $ moveForKill fOrT forOrRev $ (== g)) s
                   Nothing -> alternate s

        moveForKill F Forward = goRightUntil . afterChar
        moveForKill F Reverse = goLeftUntil . overChar
        moveForKill T Forward = goRightUntil . overChar
        moveForKill T Reverse = goLeftUntil . afterChar

repeatableCommands :: InputKeyCmd (ArgMode CommandMode) EitherMode
repeatableCommands = choiceCmd
                        [ repeatableCmdToIMode
                        , repeatableCmdMode >+> return . Left
                        , simpleChar '.' `useKey` (saveForUndo >=> runLastCommand)
                        ]
    where
        runLastCommand s = fmap lastCommand get >>= ($ s)

repeatableCmdMode :: InputKeyCmd (ArgMode CommandMode) CommandMode
repeatableCmdMode = choiceCmd
                    [ simpleChar 'x' `useKey` repeatableChange deleteChar
                    , simpleChar 'X' `useKey` repeatableChange (withCommandMode deletePrev)
                    , simpleChar '~' `useKey` repeatableChange (goRight . flipCase)
                    , simpleChar 'p' `useKey` storedCmdAction (pasteCommand pasteGraphemesAfter)
                    , simpleChar 'P' `useKey` storedCmdAction (pasteCommand pasteGraphemesBefore)
                    , simpleChar 'd' `useKey` deletionCmd
                    , simpleChar 'y' `useKey` yankCommand
                    , ctrlChar 'w' `useKey` killAndStoreC wordErase
                    , inlineSearchActions
                    , pureMovements
                    ]
    where
        repeatableChange f = storedCmdAction (saveForUndo >=> change (applyArg f))

flipCase :: CommandMode -> CommandMode
flipCase CEmpty = CEmpty
flipCase (CMode xs y zs) = CMode xs (modifyBaseChar flipCaseG y) zs
    where
        flipCaseG c | isLower c = toUpper c
                    | otherwise = toLower c

repeatableCmdToIMode :: InputKeyCmd (ArgMode CommandMode) EitherMode
repeatableCmdToIMode = simpleChar 'c' `useKey` deletionToInsertCmd

deletionCmd :: InputCmd (ArgMode CommandMode) CommandMode
deletionCmd = keyChoiceCmd
        [ reinputArg >+> deletionCmd
        , simpleChar 'd' `useKey` killAndStoreC killAll
        , useMovementsForKill killAndStoreC
        , useInlineSearchForKill (change argState) killAndStoreC
        , withoutConsuming (change argState)
        ]

deletionToInsertCmd :: InputCmd (ArgMode CommandMode) EitherMode
deletionToInsertCmd = keyChoiceCmd
        [ reinputArg >+> deletionToInsertCmd
        , simpleChar 'c' `useKey` killAndStoreE killAll
        -- vim, for whatever reason, treats cw same as ce and cW same as cE.
        -- readline does this too, so we should also.
        , simpleChar 'w' `useKey` killAndStoreE (SimpleMove goToWordDelEnd)
        , simpleChar 'W' `useKey` killAndStoreE (SimpleMove goToBigWordDelEnd)
        , useMovementsForKill killAndStoreE
        , useInlineSearchForKill (fmap Left . change argState) killAndStoreE
        , withoutConsuming (return . Left . argState)
        ]


yankCommand :: InputCmd (ArgMode CommandMode) CommandMode
yankCommand = keyChoiceCmd
        [ reinputArg >+> yankCommand
        , simpleChar 'y' `useKey` copyAndStore killAll
        , useMovementsForKill copyAndStore
        , useInlineSearchForKill (change argState) copyAndStore
        , withoutConsuming (change argState)
        ]

reinputArg :: LineState s => InputKeyCmd (ArgMode s) (ArgMode s)
reinputArg = foreachDigit restartArg ['1'..'9'] >+> loop
  where
    restartArg n = startArg n . argState
    loop = keyChoiceCmd
            [ foreachDigit addNum ['0'..'9'] >+> loop
            , withoutConsuming return
            ]

goToWordDelEnd, goToBigWordDelEnd :: InsertMode -> InsertMode
goToWordDelEnd = goRightUntil $ atStart (not . isWordChar)
                                    .||. atStart (not . isOtherChar)
goToBigWordDelEnd = goRightUntil $ atStart (not . isBigWordChar)


movements :: [(Key, InsertMode -> InsertMode)]
movements = [ (simpleChar 'h', goLeft)
            , (simpleChar 'l', goRight)
            , (simpleChar ' ', goRight)
            , (simpleKey LeftKey, goLeft)
            , (simpleKey RightKey, goRight)
            , (simpleChar '0', moveToStart)
            , (simpleChar '$', moveToEnd)
            , (simpleChar '^', skipRight isSpace . moveToStart)
            , (simpleChar '%', findMatchingBrace)
            ------------------
            -- Word movements
            -- move to the start of the next word
            , (simpleChar 'w', goRightUntil $
                                atStart isWordChar .||. atStart isOtherChar)
            , (simpleChar 'W', goRightUntil (atStart isBigWordChar))
            -- move to the beginning of the previous word
            , (simpleChar 'b', goLeftUntil $
                                atStart isWordChar .||. atStart isOtherChar)
            , (simpleChar 'B', goLeftUntil (atStart isBigWordChar))
            -- move to the end of the current word
            , (simpleChar 'e', goRightUntil $
                                atEnd isWordChar .||. atEnd isOtherChar)
            , (simpleChar 'E', goRightUntil (atEnd isBigWordChar))
            ]

{-
From IEEE 1003.1:
A "bigword" consists of: a maximal sequence of non-blanks preceded and followed by blanks
A "word" consists of either:
 - a maximal sequence of wordChars, delimited at both ends by non-wordchars
 - a maximal sequence of non-blank non-wordchars, delimited at both ends by either blanks
   or a wordchar.
-}
isBigWordChar, isWordChar, isOtherChar :: Char -> Bool
isBigWordChar = not . isSpace
isWordChar = isAlphaNum .||. (=='_')
isOtherChar = not . (isSpace .||. isWordChar)

(.||.) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
(.||.) = liftM2 (||)

foreachDigit :: (Monad m, LineState t) => (Int -> s -> t) -> [Char]
                -> KeyCommand m s t
foreachDigit f ds = choiceCmd $ map digitCmd ds
    where digitCmd d = simpleChar d `useKey` change (f (toDigit d))
          toDigit d = fromEnum d - fromEnum '0'


-- This mimics the ctrl-w command in readline's vi mode, which corresponds to
-- the tty's werase character.
wordErase :: KillHelper
wordErase = SimpleMove $ goLeftUntil $ atStart isBigWordChar

------------------
-- Matching braces

findMatchingBrace :: InsertMode -> InsertMode
findMatchingBrace (IMode xs (y:ys))
    | Just b <- matchingRightBrace yc,
      Just ((b':bs),ys') <- scanBraces yc b ys = IMode (bs++[y]++xs) (b':ys')
    | Just b <- matchingLeftBrace yc,
      Just (bs,xs') <- scanBraces yc b xs = IMode xs' (bs ++ [y]++ys)
  where yc = baseChar y
findMatchingBrace im = im

deleteMatchingBrace :: InsertMode -> ([Grapheme],InsertMode)
deleteMatchingBrace (IMode xs (y:ys))
    | Just b <- matchingRightBrace yc,
      Just (bs,ys') <- scanBraces yc b ys = (y : reverse bs, IMode xs ys')
    | Just b <- matchingLeftBrace yc,
      Just (bs,xs') <- scanBraces yc b xs = (bs ++ [y], IMode xs' ys)
  where yc = baseChar y
deleteMatchingBrace im = ([],im)


scanBraces :: Char -> Char -> [Grapheme] -> Maybe ([Grapheme],[Grapheme])
scanBraces c d = scanBraces' (1::Int) []
    where
        scanBraces' 0 bs xs = Just (bs,xs)
        scanBraces' _ _ [] = Nothing
        scanBraces' n bs (x:xs) = scanBraces' m (x:bs) xs
            where m | baseChar x == c = n+1
                    | baseChar x == d = n-1
                    | otherwise = n

matchingRightBrace, matchingLeftBrace :: Char -> Maybe Char
matchingRightBrace = flip lookup braceList
matchingLeftBrace = flip lookup (map (\(c,d) -> (d,c)) braceList)

braceList :: [(Char,Char)]
braceList = [('(',')'), ('[',']'), ('{','}')]

---------------
-- Replace mode
replaceLoop :: InputCmd CommandMode CommandMode
replaceLoop = saveForUndo >=> change insertFromCommandMode >=> loop
                >=> change enterCommandModeRight
    where
        loop = try (oneReplaceCmd >+> loop)
        oneReplaceCmd = choiceCmd [
                simpleKey LeftKey `useKey` change goLeft
                , simpleKey RightKey `useKey` change goRight
                , changeFromChar replaceCharIM
                ]


---------------------------
-- Saving previous commands

storeLastCmd :: Monad m => SavedCommand m -> Command (ViT m) s s
storeLastCmd act = \s -> do
        modify $ \vs -> vs {lastCommand = act}
        return s

storedAction :: Monad m => SavedCommand m -> SavedCommand m
storedAction act = storeLastCmd act >=> act

storedCmdAction :: Monad m => Command (ViT m) (ArgMode CommandMode) CommandMode
                           -> Command (ViT m) (ArgMode CommandMode) CommandMode
storedCmdAction act = storeLastCmd (fmap Left . act) >=> act

storedIAction :: Monad m => Command (ViT m) (ArgMode CommandMode) InsertMode
                         -> Command (ViT m) (ArgMode CommandMode) InsertMode
storedIAction act = storeLastCmd (fmap Right . act) >=> act

killAndStoreC :: MonadIO m => KillHelper -> Command (ViT m) (ArgMode CommandMode) CommandMode
killAndStoreC = storedCmdAction . killFromArgHelper

killAndStoreI :: MonadIO m => KillHelper -> Command (ViT m) (ArgMode CommandMode) InsertMode
killAndStoreI = storedIAction . killFromArgHelper

killAndStoreE :: MonadIO m => KillHelper -> Command (ViT m) (ArgMode CommandMode) EitherMode
killAndStoreE helper = storedAction (killFromArgHelper helper >=> return . Right)

copyAndStore :: MonadIO m => KillHelper -> Command (ViT m) (ArgMode CommandMode) CommandMode
copyAndStore = storedCmdAction . copyFromArgHelper

noArg :: Monad m => Command m s (ArgMode s)
noArg = return . startArg 1

-------------------
-- Vi-style searching

data SearchEntry = SearchEntry {
                    entryState :: InsertMode,
                    searchChar :: Char
                    }

searchText :: SearchEntry -> [Grapheme]
searchText SearchEntry {entryState = IMode xs ys} = reverse xs ++ ys

instance LineState SearchEntry where
    beforeCursor prefix se = beforeCursor (prefix ++ stringToGraphemes [searchChar se])
                                (entryState se)
    afterCursor = afterCursor . entryState

viEnterSearch :: Monad m => Char -> Direction
                    -> Command (ViT m) CommandMode CommandMode
viEnterSearch c dir s = setState (SearchEntry emptyIM c) >>= loopEntry
    where
        modifySE f se = se {entryState = f (entryState se)}
        loopEntry = keyChoiceCmd
                        [ editEntry >+> loopEntry
                        , simpleChar '\n' `useKey` \se -> viSearchHist dir (searchText se) s
                        , withoutConsuming (change (const s))
                        ]
        editEntry = choiceCmd
                        [ useChar (change . modifySE . insertChar)
                        , simpleKey LeftKey `useKey` change (modifySE goLeft)
                        , simpleKey RightKey `useKey` change (modifySE goRight)
                        , simpleKey Backspace `useKey` change (modifySE deletePrev)
                        , simpleKey Delete `useKey` change (modifySE deleteNext)
                        ]

viSearchHist :: forall m . Monad m
    => Direction -> [Grapheme] -> Command (ViT m) CommandMode CommandMode
viSearchHist dir toSearch cm = do
    vstate :: ViState m <- get
    let toSearch' = if null toSearch
                        then lastSearch vstate
                        else toSearch
    result <- doSearch False SearchMode {
                                    searchTerm = toSearch',
                                    foundHistory = save cm, -- TODO: not needed
                                    direction = dir}
    case result of
        Left e -> effect e >> setState cm
        Right sm -> do
            put vstate {lastSearch = toSearch'}
            setState (restore (foundHistory sm))

reverseDir :: Maybe (a, b, Direction) -> Maybe (a, b, Direction)
reverseDir = (third3 flipDir <$>)
    where
        third3 f (a, b, c) = (a, b, f c)