module System.Console.Haskeline.Completion(
CompletionFunc,
Completion(..),
noCompletion,
simpleCompletion,
fallbackCompletion,
completeWord,
completeWord',
completeWordWithPrev,
completeWordWithPrev',
completeQuotedWord,
completeFilename,
listFiles,
filenameWordBreakChars
) where
import System.FilePath
import Data.List(isPrefixOf)
import Control.Monad(forM)
import System.Console.Haskeline.Directory
import System.Console.Haskeline.Monads
type CompletionFunc m = (String,String) -> m (String, [Completion])
data Completion = Completion {replacement :: String,
display :: String,
isFinished :: Bool
}
deriving (Eq, Ord, Show)
noCompletion :: Monad m => CompletionFunc m
noCompletion (s,_) = return (s,[])
completeWord :: Monad m => Maybe Char
-> [Char]
-> (String -> m [Completion])
-> CompletionFunc m
completeWord esc ws = completeWordWithPrev esc ws . const
completeWord' :: Monad m => Maybe Char
-> (Char -> Bool)
-> (String -> m [Completion])
-> CompletionFunc m
completeWord' esc ws = completeWordWithPrev' esc ws . const
completeWordWithPrev :: Monad m => Maybe Char
-> [Char]
-> (String -> String -> m [Completion])
-> CompletionFunc m
completeWordWithPrev esc ws = completeWordWithPrev' esc (`elem` ws)
completeWordWithPrev' :: Monad m => Maybe Char
-> (Char -> Bool)
-> (String -> String -> m [Completion])
-> CompletionFunc m
completeWordWithPrev' esc wpred f (line, _) = do
let (word,rest) = case esc of
Nothing -> break wpred line
Just e -> escapedBreak e line
completions <- f rest (reverse word)
return (rest,map (escapeReplacement esc wpred) completions)
where
escapedBreak e (c:d:cs) | d == e && (c == e || wpred c)
= let (xs,ys) = escapedBreak e cs in (c:xs,ys)
escapedBreak e (c:cs) | not $ wpred c
= let (xs,ys) = escapedBreak e cs in (c:xs,ys)
escapedBreak _ cs = ("",cs)
simpleCompletion :: String -> Completion
simpleCompletion = completion
filenameWordBreakChars :: String
filenameWordBreakChars = " \t\n`@$><=;|&{("
completeFilename :: MonadIO m => CompletionFunc m
completeFilename = completeQuotedWord (Just '\\') "\"'" listFiles
$ completeWord (Just '\\') ("\"\'" ++ filenameWordBreakChars)
listFiles
completion :: String -> Completion
completion str = Completion str str True
setReplacement :: (String -> String) -> Completion -> Completion
setReplacement f c = c {replacement = f $ replacement c}
escapeReplacement :: Maybe Char -> (Char -> Bool) -> Completion -> Completion
escapeReplacement esc wpred f = case esc of
Nothing -> f
Just e -> f {replacement = escape e (replacement f)}
where
escape e (c:cs) | c == e || wpred c = e : c : escape e cs
| otherwise = c : escape e cs
escape _ "" = ""
completeQuotedWord :: Monad m => Maybe Char
-> [Char]
-> (String -> m [Completion])
-> CompletionFunc m
-> CompletionFunc m
completeQuotedWord esc qs completer alterative line@(left,_)
= case splitAtQuote esc qs left of
Just (w,rest) | isUnquoted esc qs rest -> do
cs <- completer (reverse w)
return (rest, map (addQuotes . escapeReplacement esc (`elem` qs)) cs)
_ -> alterative line
addQuotes :: Completion -> Completion
addQuotes c = if isFinished c
then c {replacement = "\"" ++ replacement c ++ "\""}
else c {replacement = "\"" ++ replacement c}
splitAtQuote :: Maybe Char -> String -> String -> Maybe (String,String)
splitAtQuote esc qs line = case line of
c:e:cs | isEscape e && isEscapable c
-> do
(w,rest) <- splitAtQuote esc qs cs
return (c:w,rest)
q:cs | isQuote q -> Just ("",cs)
c:cs -> do
(w,rest) <- splitAtQuote esc qs cs
return (c:w,rest)
"" -> Nothing
where
isQuote = (`elem` qs)
isEscape c = Just c == esc
isEscapable c = isEscape c || isQuote c
isUnquoted :: Maybe Char -> String -> String -> Bool
isUnquoted esc qs s = case splitAtQuote esc qs s of
Just (_,s') -> not (isUnquoted esc qs s')
_ -> True
listFiles :: MonadIO m => FilePath -> m [Completion]
listFiles path = liftIO $ do
fixedDir <- fixPath dir
dirExists <- doesDirectoryExist fixedDir
allFiles <- if not dirExists
then return []
else fmap (map completion . filterPrefix)
$ getDirectoryContents fixedDir
forM allFiles $ \c -> do
isDir <- doesDirectoryExist (fixedDir </> replacement c)
return $ setReplacement fullName $ alterIfDir isDir c
where
(dir, file) = splitFileName path
filterPrefix = filter (\f -> notElem f [".",".."]
&& file `isPrefixOf` f)
alterIfDir False c = c
alterIfDir True c = c {replacement = addTrailingPathSeparator (replacement c),
isFinished = False}
fullName = replaceFileName path
fixPath :: String -> IO String
fixPath "" = return "."
fixPath ('~':c:path) | isPathSeparator c = do
home <- getHomeDirectory
return (home </> path)
fixPath path = return path
fallbackCompletion :: Monad m => CompletionFunc m -> CompletionFunc m -> CompletionFunc m
fallbackCompletion a b input = do
aCompletions <- a input
if null (snd aCompletions)
then b input
else return aCompletions