module System.Console.Haskeline.Command.History where
import System.Console.Haskeline.LineState
import System.Console.Haskeline.Command
import Control.Monad(liftM,mplus)
import System.Console.Haskeline.Monads
import Data.List
import Data.Maybe(fromMaybe)
import Control.Exception(evaluate)
import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as UTF8
import System.Directory(doesFileExist)
data History = History {historyLines :: [String]}
data HistLog = HistLog {pastHistory, futureHistory :: [String]}
deriving Show
prevHistoryM :: String -> HistLog -> Maybe (String,HistLog)
prevHistoryM _ HistLog {pastHistory = []} = Nothing
prevHistoryM s HistLog {pastHistory=ls:past, futureHistory=future}
= Just (ls,
HistLog {pastHistory=past, futureHistory= s:future})
prevHistories :: String -> HistLog -> [(String,HistLog)]
prevHistories s h = flip unfoldr (s,h) $ \(s',h') -> fmap (\r -> (r,r))
$ prevHistoryM s' h'
histLog :: History -> HistLog
histLog hist = HistLog {pastHistory = historyLines hist, futureHistory = []}
runHistoryFromFile :: MonadIO m => Maybe FilePath -> Maybe Int -> StateT History m a -> m a
runHistoryFromFile Nothing _ f = evalStateT' (History []) f
runHistoryFromFile (Just file) stifleAmt f = do
contents <- liftIO $ do
exists <- doesFileExist file
if exists
then fmap UTF8.toString (B.readFile file)
else return ""
liftIO $ evaluate (length contents)
let oldHistory = History (lines contents)
(x,newHistory) <- runStateT f oldHistory
let stifle = case stifleAmt of
Nothing -> id
Just m -> take m
liftIO $ B.writeFile file $ UTF8.fromString
$ unlines $ stifle $ historyLines newHistory
return x
addHistory :: MonadState History m => String -> m ()
addHistory l = modify $ \(History ls) -> History (l:ls)
runHistLog :: Monad m => StateT HistLog m a -> StateT History m a
runHistLog f = do
history <- get
lift (evalStateT' (histLog history) f)
prevHistory :: FromString s => s -> HistLog -> (s, HistLog)
prevHistory s h = let (s',h') = fromMaybe (toResult s,h) $ prevHistoryM (toResult s) h
in (fromString s',h')
historyBack, historyForward :: (FromString s, MonadState HistLog m) =>
Key -> Command m s s
historyBack = simpleCommand $ histUpdate prevHistory
historyForward = simpleCommand $ reverseHist $ histUpdate prevHistory
histUpdate :: MonadState HistLog m => (s -> HistLog -> (t,HistLog))
-> s -> m (Effect t)
histUpdate f = liftM Change . update . f
reverseHist :: MonadState HistLog m => (a -> m b) -> a -> m b
reverseHist f x = do
modify reverser
y <- f x
modify reverser
return y
where
reverser h = HistLog {futureHistory=pastHistory h,
pastHistory=futureHistory h}
data SearchMode = SearchMode {searchTerm :: String,
foundHistory :: InsertMode}
deriving Show
instance LineState SearchMode where
beforeCursor _ sm = beforeCursor prefix (foundHistory sm)
where prefix = "(reverse-i-search)`" ++ searchTerm sm ++ "': "
afterCursor = afterCursor . foundHistory
instance Result SearchMode where
toResult = toResult . foundHistory
startSearchMode :: InsertMode -> SearchMode
startSearchMode im = SearchMode {searchTerm = "",foundHistory=im}
addChar :: Char -> SearchMode -> SearchMode
addChar c s = s {searchTerm = searchTerm s ++ [c]}
searchHistories :: String -> [(String,HistLog)] -> Maybe (SearchMode,HistLog)
searchHistories text = foldr mplus Nothing . map findIt
where
findIt (l,h) = do
im <- findInLine text l
return (SearchMode text im,h)
findInLine :: String -> String -> Maybe InsertMode
findInLine text l = find' [] l
where
find' _ "" = Nothing
find' prev ccs@(c:cs)
| text `isPrefixOf` ccs = Just (IMode prev ccs)
| otherwise = find' (c:prev) cs
prepSearch :: SearchMode -> HistLog -> (String,[(String,HistLog)])
prepSearch sm h = let
text = searchTerm sm
l = toResult sm
in (text,prevHistories l h)
searchHistory :: MonadState HistLog m => Command m InsertMode InsertMode
searchHistory = controlKey 'r' +> change startSearchMode >|> backSearching
where
backKey = controlKey 'r'
backSearching = choiceCmd [
choiceCmd [
charCommand oneMoreChar
, backKey +> simpleCommand searchBackMore
, Backspace +> change delLastChar
, KeyChar '\b' +> change delLastChar
] >|> backSearching
, changeWithoutKey foundHistory
]
delLastChar s = s {searchTerm = minit (searchTerm s)}
minit xs = if null xs then "" else init xs
oneMoreChar c = histUpdate (\s h -> let
(text,hists) = prepSearch s h
in fromMaybe (s,h) $ searchHistories text ((toResult s,h):hists)
) . addChar c
searchBackMore = histUpdate $ \s h -> let
(text,hists) = prepSearch s h
in fromMaybe (s,h) $ searchHistories text hists