| 1 | module System.Console.Haskeline.Completion( |
|---|
| 2 | CompletionFunc, |
|---|
| 3 | Completion(..), |
|---|
| 4 | completeWord, |
|---|
| 5 | completeWordWithModifier, |
|---|
| 6 | completeQuotedWord, |
|---|
| 7 | |
|---|
| 8 | noCompletion, |
|---|
| 9 | simpleCompletion, |
|---|
| 10 | |
|---|
| 11 | completeFilename, |
|---|
| 12 | listFiles, |
|---|
| 13 | filenameWordBreakChars |
|---|
| 14 | ) where |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | import System.FilePath |
|---|
| 18 | import Data.List(isPrefixOf, find) |
|---|
| 19 | import Control.Monad(forM) |
|---|
| 20 | |
|---|
| 21 | import System.Console.Haskeline.Directory |
|---|
| 22 | import System.Console.Haskeline.Monads |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | type CompletionFunc m = (String,String) -> m (String, [Completion]) |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | data Completion = Completion {replacement :: String, -- ^ Text to insert in line. |
|---|
| 35 | display :: String, |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | isFinished :: Bool |
|---|
| 39 | |
|---|
| 40 | |
|---|
| 41 | } |
|---|
| 42 | deriving Show |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | noCompletion :: Monad m => CompletionFunc m |
|---|
| 46 | noCompletion (s,_) = return (s,[]) |
|---|
| 47 | |
|---|
| 48 | |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | completeWord :: Monad m => Maybe Char |
|---|
| 53 | |
|---|
| 54 | -> String |
|---|
| 55 | -> (String -> m [Completion]) |
|---|
| 56 | -> CompletionFunc m |
|---|
| 57 | completeWord esc ws f = completeWordWithModifier esc ws "" (const f) |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | completeWordWithModifier :: Monad m => Maybe Char |
|---|
| 61 | |
|---|
| 62 | -> String |
|---|
| 63 | -> String |
|---|
| 64 | -> (Maybe Char -> String -> m [Completion]) |
|---|
| 65 | -> CompletionFunc m |
|---|
| 66 | completeWordWithModifier esc ws ms f (line, _) = do |
|---|
| 67 | let (word,rest) = case esc of |
|---|
| 68 | Nothing -> break (`elem` ws) line |
|---|
| 69 | Just e -> escapedBreak e line |
|---|
| 70 | let modifier = find (`elem` ms) rest |
|---|
| 71 | completions <- f modifier (reverse word) |
|---|
| 72 | return (rest,map (escapeReplacement esc ws) completions) |
|---|
| 73 | where |
|---|
| 74 | escapedBreak e (c:d:cs) | d == e && c `elem` (e:ws) |
|---|
| 75 | = let (xs,ys) = escapedBreak e cs in (c:xs,ys) |
|---|
| 76 | escapedBreak e (c:cs) | notElem c ws |
|---|
| 77 | = let (xs,ys) = escapedBreak e cs in (c:xs,ys) |
|---|
| 78 | escapedBreak _ cs = ("",cs) |
|---|
| 79 | |
|---|
| 80 | |
|---|
| 81 | simpleCompletion :: String -> Completion |
|---|
| 82 | simpleCompletion = completion |
|---|
| 83 | |
|---|
| 84 | |
|---|
| 85 | |
|---|
| 86 | filenameWordBreakChars :: String |
|---|
| 87 | filenameWordBreakChars = " \t\n`@$><=;|&{(" |
|---|
| 88 | |
|---|
| 89 | |
|---|
| 90 | completeFilename :: MonadIO m => CompletionFunc m |
|---|
| 91 | completeFilename = completeQuotedWord (Just '\\') "\"'" listFiles |
|---|
| 92 | $ completeWord (Just '\\') ("\"\'" ++ filenameWordBreakChars) |
|---|
| 93 | listFiles |
|---|
| 94 | |
|---|
| 95 | completion :: String -> Completion |
|---|
| 96 | completion str = Completion str str True |
|---|
| 97 | |
|---|
| 98 | setReplacement :: (String -> String) -> Completion -> Completion |
|---|
| 99 | setReplacement f c = c {replacement = f $ replacement c} |
|---|
| 100 | |
|---|
| 101 | escapeReplacement :: Maybe Char -> String -> Completion -> Completion |
|---|
| 102 | escapeReplacement esc ws f = case esc of |
|---|
| 103 | Nothing -> f |
|---|
| 104 | Just e -> f {replacement = escape e (replacement f)} |
|---|
| 105 | where |
|---|
| 106 | escape e (c:cs) | c `elem` (e:ws) = e : c : escape e cs |
|---|
| 107 | | otherwise = c : escape e cs |
|---|
| 108 | escape _ "" = "" |
|---|
| 109 | |
|---|
| 110 | |
|---|
| 111 | |
|---|
| 112 | |
|---|
| 113 | completeQuotedWord :: Monad m => Maybe Char -- ^ An optional escape character |
|---|
| 114 | -> String |
|---|
| 115 | -> (String -> m [Completion]) |
|---|
| 116 | -> CompletionFunc m |
|---|
| 117 | |
|---|
| 118 | -> CompletionFunc m |
|---|
| 119 | completeQuotedWord esc qs completer alterative line@(left,_) |
|---|
| 120 | = case splitAtQuote esc qs left of |
|---|
| 121 | Just (w,rest) | isUnquoted esc qs rest -> do |
|---|
| 122 | cs <- completer (reverse w) |
|---|
| 123 | return (rest, map (addQuotes . escapeReplacement esc qs) cs) |
|---|
| 124 | _ -> alterative line |
|---|
| 125 | |
|---|
| 126 | addQuotes :: Completion -> Completion |
|---|
| 127 | addQuotes c = if isFinished c |
|---|
| 128 | then c {replacement = "\"" ++ replacement c ++ "\""} |
|---|
| 129 | else c {replacement = "\"" ++ replacement c} |
|---|
| 130 | |
|---|
| 131 | splitAtQuote :: Maybe Char -> String -> String -> Maybe (String,String) |
|---|
| 132 | splitAtQuote esc qs line = case line of |
|---|
| 133 | c:e:cs | isEscape e && isEscapable c |
|---|
| 134 | -> do |
|---|
| 135 | (w,rest) <- splitAtQuote esc qs cs |
|---|
| 136 | return (c:w,rest) |
|---|
| 137 | q:cs | isQuote q -> Just ("",cs) |
|---|
| 138 | c:cs -> do |
|---|
| 139 | (w,rest) <- splitAtQuote esc qs cs |
|---|
| 140 | return (c:w,rest) |
|---|
| 141 | "" -> Nothing |
|---|
| 142 | where |
|---|
| 143 | isQuote = (`elem` qs) |
|---|
| 144 | isEscape c = Just c == esc |
|---|
| 145 | isEscapable c = isEscape c || isQuote c |
|---|
| 146 | |
|---|
| 147 | isUnquoted :: Maybe Char -> String -> String -> Bool |
|---|
| 148 | isUnquoted esc qs s = case splitAtQuote esc qs s of |
|---|
| 149 | Just (_,s') -> not (isUnquoted esc qs s') |
|---|
| 150 | _ -> True |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | |
|---|
| 154 | listFiles :: MonadIO m => FilePath -> m [Completion] |
|---|
| 155 | listFiles path = liftIO $ do |
|---|
| 156 | fixedDir <- fixPath dir |
|---|
| 157 | dirExists <- doesDirectoryExist fixedDir |
|---|
| 158 | |
|---|
| 159 | allFiles <- if not dirExists |
|---|
| 160 | then return [] |
|---|
| 161 | else fmap (map completion . filterPrefix) |
|---|
| 162 | $ getDirectoryContents fixedDir |
|---|
| 163 | |
|---|
| 164 | |
|---|
| 165 | forM allFiles $ \c -> do |
|---|
| 166 | isDir <- doesDirectoryExist (fixedDir </> replacement c) |
|---|
| 167 | return $ setReplacement fullName $ alterIfDir isDir c |
|---|
| 168 | where |
|---|
| 169 | (dir, file) = splitFileName path |
|---|
| 170 | filterPrefix = filter (\f -> notElem f [".",".."] |
|---|
| 171 | && file `isPrefixOf` f) |
|---|
| 172 | alterIfDir False c = c |
|---|
| 173 | alterIfDir True c = c {replacement = addTrailingPathSeparator (replacement c), |
|---|
| 174 | isFinished = False} |
|---|
| 175 | fullName = replaceFileName path |
|---|
| 176 | |
|---|
| 177 | |
|---|
| 178 | fixPath :: String -> IO String |
|---|
| 179 | |
|---|
| 180 | fixPath "" = return "." |
|---|
| 181 | fixPath ('~':c:path) | isPathSeparator c = do |
|---|
| 182 | home <- getHomeDirectory |
|---|
| 183 | return (home </> path) |
|---|
| 184 | fixPath path = return path |
|---|