1-----------------------------------------------------------------------------
4-- Copyright : (c) Phil Freeman 2013
7-- Maintainer : Phil Freeman <paf31@cantab.net>
8-- Stability : experimental
12-- PureScript Compiler Interactive.
14-----------------------------------------------------------------------------
16{-# LANGUAGE DoAndIfThenElse, FlexibleContexts #-}
22import Control.Applicative
24import Control.Monad.Trans.Class
25import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
26import Control.Monad.Trans.State.Strict
28import Data.List (intercalate, isPrefixOf, nub, sortBy)
29import Data.Maybe (mapMaybe)
30import Data.Foldable (traverse_)
31import Data.Traversable (traverse)
32import Data.Version (showVersion)
36import System.Console.Haskeline
37import System.Directory (doesFileExist, findExecutable, getHomeDirectory, getCurrentDirectory)
39import System.Environment.XDG.BaseDir
40import System.FilePath ((</>), isPathSeparator)
41import qualified System.Console.CmdTheLine as Cmd
44import Text.Parsec (ParseError)
46import qualified Data.Map as M
47import qualified Language.PureScript as P
48import qualified Paths_purescript as Paths
49import qualified System.IO.UTF8 as U (print, readFile)
53-- Holds a list of imported modules, loaded files, and partial let bindings.
54-- The let bindings are partial,
55-- because it makes more sense to apply the binding to the final evaluated expression.
57data PSCiState = PSCiState
58 { psciImportedFilenames :: [FilePath]
59 , psciImportedModuleNames :: [P.ModuleName]
60 , psciLoadedModules :: [P.Module]
61 , psciLetBindings :: [P.Value -> P.Value]
67-- Updates the state to have more imported modules.
69updateImportedFiles :: FilePath -> PSCiState -> PSCiState
70updateImportedFiles filename st = st { psciImportedFilenames = filename : psciImportedFilenames st }
73-- Updates the state to have more imported modules.
75updateImports :: P.ModuleName -> PSCiState -> PSCiState
76updateImports name st = st { psciImportedModuleNames = name : psciImportedModuleNames st }
79-- Updates the state to have more loaded files.
81updateModules :: [P.Module] -> PSCiState -> PSCiState
82updateModules modules st = st { psciLoadedModules = psciLoadedModules st ++ modules }
85-- Updates the state to have more let bindings.
87updateLets :: (P.Value -> P.Value) -> PSCiState -> PSCiState
88updateLets name st = st { psciLetBindings = name : psciLetBindings st }
92-- Load the necessary modules.
94defaultImports :: [P.ModuleName]
95defaultImports = [P.ModuleName [P.ProperName "Prelude"]]
98-- Locates the node executable.
99-- Checks for either @nodejs@ or @node@.
101findNodeProcess :: IO (Maybe String)
102findNodeProcess = runMaybeT . msum $ map (MaybeT . findExecutable) names
103 where names = ["nodejs", "node"]
106-- Grabs the filename where the history is stored.
108getHistoryFilename :: IO FilePath
109getHistoryFilename = getUserConfigFile "purescript" "psci_history"
112-- Grabs the filename where prelude is.
114getPreludeFilename :: IO FilePath
115getPreludeFilename = Paths.getDataFileName "prelude/prelude.purs"
118-- Loads a file for use with imports.
120loadModule :: FilePath -> IO (Either String [P.Module])
121loadModule filename = either (Left . show) Right . P.runIndentParser filename P.parseModules <$> U.readFile filename
124-- Expands tilde in path.
126expandTilde :: FilePath -> IO FilePath
127expandTilde ('~':p:rest) | isPathSeparator p = (</> rest) <$> getHomeDirectory
128expandTilde p = return p
135helpMessage = "The following commands are available:\n\n " ++
136 intercalate "\n " (map (intercalate " ") help)
139-- The welcome prologue.
141prologueMessage :: String
142prologueMessage = intercalate "\n"
144 , "| _ \\ _ _ _ __ ___/ ___| ___ _ __(_)_ __ | |_ "
145 , "| |_) | | | | '__/ _ \\___ \\ / __| '__| | '_ \\| __|"
146 , "| __/| |_| | | | __/___) | (__| | | | |_) | |_ "
147 , "|_| \\__,_|_| \\___|____/ \\___|_| |_| .__/ \\__|"
152 , "Expressions are terminated using Ctrl+D"
159quitMessage = "See ya!"
161-- Haskeline completions
164-- Loads module, function, and file completions.
166completion :: CompletionFunc (StateT PSCiState IO)
167completion = completeWord Nothing " \t\n\r" findCompletions
169 findCompletions :: String -> StateT PSCiState IO [Completion]
170 findCompletions str = do
171 ms <- psciLoadedModules <$> get
172 files <- listFiles str
173 let matches = filter (isPrefixOf str) (names ms)
174 return $ sortBy sorter $ map simpleCompletion matches ++ files
175 getDeclName :: Maybe [P.DeclarationRef] -> P.Declaration -> Maybe P.Ident
176 getDeclName Nothing (P.ValueDeclaration ident _ _ _ _) = Just ident
177 getDeclName (Just exts) (P.ValueDeclaration ident _ _ _ _) | isExported = Just ident
179 isExported = any exports exts
180 exports (P.ValueRef ident') = ident == ident'
181 exports (P.PositionedDeclarationRef _ r) = exports r
183 getDeclName exts (P.PositionedDeclaration _ d) = getDeclName exts d
184 getDeclName _ _ = Nothing
185 names :: [P.Module] -> [String]
186 names ms = nub [ show qual
187 | P.Module moduleName ds exts <- ms
188 , ident <- mapMaybe (getDeclName exts) ds
189 , qual <- [ P.Qualified Nothing ident
190 , P.Qualified (Just moduleName) ident]
192 sorter :: Completion -> Completion -> Ordering
193 sorter (Completion _ d1 _) (Completion _ d2 _) = compare d1 d2
197-- | Compilation options.
200options = P.Options False True False True (Just "Main") True "PS" [] [] False
203-- Makes a volatile module to execute the current expression.
205createTemporaryModule :: Bool -> PSCiState -> P.Value -> P.Module
206createTemporaryModule exec PSCiState{psciImportedModuleNames = imports, psciLetBindings = lets} value =
208 moduleName = P.ModuleName [P.ProperName "Main"]
209 importDecl m = P.ImportDeclaration m Nothing Nothing
210 traceModule = P.ModuleName [P.ProperName "Debug", P.ProperName "Trace"]
211 trace = P.Var (P.Qualified (Just traceModule) (P.Ident "print"))
212 itValue = foldl (\x f -> f x) value lets
213 mainValue = P.App trace (P.Var (P.Qualified Nothing (P.Ident "it")))
214 itDecl = P.ValueDeclaration (P.Ident "it") P.Value [] Nothing itValue
215 mainDecl = P.ValueDeclaration (P.Ident "main") P.Value [] Nothing mainValue
216 decls = if exec then [itDecl, mainDecl] else [itDecl]
218 P.Module moduleName ((importDecl `map` imports) ++ decls) Nothing
221-- Takes a value declaration and evaluates it with the current state.
223handleDeclaration :: P.Value -> PSCiState -> InputT (StateT PSCiState IO) ()
224handleDeclaration value st = do
225 let m = createTemporaryModule True st value
226 case P.compile options (psciLoadedModules st ++ [m]) of
227 Left err -> outputStrLn err
228 Right (js, _, _) -> do
229 process <- lift . lift $ findNodeProcess
230 result <- lift . lift $ traverse (\node -> readProcessWithExitCode node [] js) process
232 Just (ExitSuccess, out, _) -> outputStrLn out
233 Just (ExitFailure _, _, err) -> outputStrLn err
234 Nothing -> outputStrLn "Couldn't find node.js"
237-- Takes a value and prints its type
239handleTypeOf :: P.Value -> PSCiState -> InputT (StateT PSCiState IO) ()
240handleTypeOf value st = do
241 let m = createTemporaryModule False st value
242 case P.compile options { P.optionsMain = Nothing } (psciLoadedModules st ++ [m]) of
243 Left err -> outputStrLn err
244 Right (_, _, env') ->
245 case M.lookup (P.ModuleName [P.ProperName "Main"], P.Ident "it") (P.names env') of
246 Just (ty, _) -> outputStrLn . P.prettyPrintType $ ty
247 Nothing -> outputStrLn "Could not find type"
252-- Parses the input and returns either a Metacommand or an expression.
254getCommand :: InputT (StateT PSCiState IO) (Either ParseError (Maybe Command))
256 firstLine <- getInputLine "> "
258 Nothing -> return (Right Nothing)
259 Just s@ (':' : _) -> return . either Left (Right . Just) $ parseCommand s -- The start of a command
260 Just s -> either Left (Right . Just) . parseCommand <$> go [s]
262 go :: [String] -> InputT (StateT PSCiState IO) String
263 go ls = maybe (return . unlines $ reverse ls) (go . (:ls)) =<< getInputLine " "
266-- Performs an action for each meta-command given, and also for expressions..
268handleCommand :: Command -> InputT (StateT PSCiState IO) ()
269handleCommand (Expression val) = lift get >>= handleDeclaration val
270handleCommand Help = outputStrLn helpMessage
271handleCommand (Import moduleName) = lift $ modify (updateImports moduleName)
272handleCommand (Let l) = lift $ modify (updateLets l)
273handleCommand (LoadFile filePath) = do
274 absPath <- lift . lift $ expandTilde filePath
275 exists <- lift . lift $ doesFileExist absPath
277 lift $ modify (updateImportedFiles absPath)
278 either outputStrLn (lift . modify . updateModules) =<< (lift . lift $ loadModule absPath)
280 outputStrLn $ "Couldn't locate: " ++ filePath
281handleCommand Reset = do
282 files <- psciImportedFilenames <$> lift get
283 modulesOrFirstError <- fmap concat . sequence <$> mapM (lift . lift . loadModule) files
284 case modulesOrFirstError of
285 Left err -> lift . lift $ putStrLn err >> exitFailure
286 Right modules -> lift $ put (PSCiState files defaultImports modules [])
287handleCommand (TypeOf val) = lift get >>= handleTypeOf val
288handleCommand _ = outputStrLn "Unknown command"
290inputFiles :: Cmd.Term [FilePath]
291inputFiles = Cmd.value $ Cmd.posAny [] $ Cmd.posInfo { Cmd.posName = "file(s)"
292 , Cmd.posDoc = "Optional .purs files to load on start" }
294loadUserConfig :: IO (Maybe [Command])
296 configFile <- (</> ".psci") <$> getCurrentDirectory
297 exists <- doesFileExist configFile
300 ls <- lines <$> U.readFile configFile
301 case mapM parseCommand ls of
302 Left err -> U.print err >> exitFailure
303 Right cs -> return $ Just cs
308-- The PSCI main loop.
310loop :: [FilePath] -> IO ()
312 config <- loadUserConfig
313 preludeFilename <- getPreludeFilename
314 modulesOrFirstError <- fmap concat . sequence <$> mapM loadModule (preludeFilename : files)
315 case modulesOrFirstError of
316 Left err -> putStrLn err >> exitFailure
318 historyFilename <- getHistoryFilename
319 let settings = defaultSettings {historyFile = Just historyFilename}
320 flip evalStateT (PSCiState (preludeFilename : files) defaultImports modules []) . runInputT (setComplete completion settings) $ do
321 outputStrLn prologueMessage
322 traverse_ (mapM_ handleCommand) config
325 go :: InputT (StateT PSCiState IO) ()
329 Left err -> outputStrLn (show err) >> go
331 Right (Just Quit) -> outputStrLn quitMessage
332 Right (Just c') -> handleCommand c' >> go
334term :: Cmd.Term (IO ())
335term = loop <$> inputFiles
337termInfo :: Cmd.TermInfo
339 { Cmd.termName = "psci"
340 , Cmd.version = showVersion Paths.version
341 , Cmd.termDoc = "Interactive mode for PureScript"
345main = Cmd.run (term, termInfo)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list