haskeline-0.2: System/Console/Haskeline/Completion.hs
module System.Console.Haskeline.Completion(
CompletionFunc,
Completion(..),
completeWord,
simpleCompletion,
noCompletion,
completeFilename,
filenameWordBreakChars
) where
import System.Directory
import System.FilePath
import Data.List(isPrefixOf)
import Control.Monad(forM)
import System.Console.Haskeline.Monads
-- | Performs completions from a reversed 'String'.
-- The output 'String' is also reversed.
-- Use 'completeWord' to build these functions.
type CompletionFunc m = String -> m (String, [Completion])
data Completion = Completion {replacement :: String, -- ^ Text to insert in line.
display :: String
-- ^ Text to display when listing
-- alternatives.
}
deriving Show
-- | Disable completion altogether.
noCompletion :: Monad m => CompletionFunc m
noCompletion s = return (s,[])
--------------
-- Word break functions
-- | The following function creates a custom 'CompletionFunc' for use in the 'Settings.'
completeWord :: Monad m => Maybe Char
-- ^ An optional escape character
-> String -- ^ List of characters which count as whitespace
-> (String -> m [Completion]) -- ^ Function to produce a list of possible completions
-> CompletionFunc m
completeWord esc ws f line = do
let (word,rest) = case esc of
Nothing -> break (`elem` ws) line
Just e -> escapedBreak e line
completions <- f (reverse word)
return (rest,completions)
where
escapedBreak e (c:d:cs) | d == e
= let (xs,ys) = escapedBreak e cs in (c:d:xs,ys)
escapedBreak e (c:cs) | not (elem c ws)
= let (xs,ys) = escapedBreak e cs in (c:xs,ys)
escapedBreak _ cs = ("",cs)
-- | Adds a space after the word when inserting it after expansion.
simpleCompletion :: String -> Completion
simpleCompletion = setReplacement (++ " ") . completion
-- NOTE: this is the same as for readline, except that I took out the '\\'
-- so they can be used as a path separator.
filenameWordBreakChars :: String
filenameWordBreakChars = " \t\n`@$><=;|&{("
-- A completion command for file and folder names.
completeFilename :: MonadIO m => CompletionFunc m
completeFilename = completeWord (Just '\\') filenameWordBreakChars $
(liftIO . quotedFilenames (`elem` "\"\'"))
completion :: String -> Completion
completion str = Completion str str
setReplacement :: (String -> String) -> Completion -> Completion
setReplacement f c = c {replacement = f $ replacement c}
--------
-- Helper funcs for file completion
quotedFilenames :: (Char -> Bool) -> String -> IO [Completion]
quotedFilenames isQuote (q:file) | isQuote q = do
files <- findFiles file
return $ map (setReplacement ((q:) . appendIfNotDir [q,' '])) files
quotedFilenames _ file = do
files <- findFiles file
return $ map (setReplacement (appendIfNotDir " ")) files
appendIfNotDir :: String -> FilePath -> FilePath
appendIfNotDir str file | null (takeFileName file) = file
| otherwise = file ++ str
findFiles :: FilePath -> IO [Completion]
-- NOTE: 'handle' catches exceptions from getDirectoryContents and getHomeDirectory.
findFiles path = handle (\_ -> return []) $ do
fixedDir <- fixPath dir
dirExists <- doesDirectoryExist fixedDir
-- get all of the files in that directory, as basenames
allFiles <- if not dirExists
then return []
else fmap (map completion . filterPrefix)
$ getDirectoryContents fixedDir
-- The replacement text should include the directory part, and also
-- have a trailing slash if it's itself a directory.
forM allFiles $ \c -> do
isDir <- doesDirectoryExist (fixedDir </> replacement c)
return $ setReplacement (fullName . maybeAddSlash isDir) c
where
(dir, file) = splitFileName path
filterPrefix = filter (\f -> not (f `elem` [".",".."])
&& file `isPrefixOf` f)
maybeAddSlash False = id
maybeAddSlash True = addTrailingPathSeparator
-- NOTE In order for completion to work properly, all of the alternatives
-- must have the exact same prefix. As a result, </> is a little too clever;
-- for example, it doesn't prepend the directory if the file looks like
-- an absolute path (strange, but it can happen).
-- The FilePath docs state that (++) is an exact inverse of splitFileName, so
-- that's the right function to user here.
fullName f = dir ++ f
-- turn a user-visible path into an internal version useable by System.FilePath.
fixPath :: String -> IO String
fixPath "" = return "."
fixPath ('~':c:path) | isPathSeparator c = do
home <- getHomeDirectory
return (home </> path)
fixPath path = return path