packages feed

ghc-mod-5.1.1.0: src/GHCModi.hs

{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}

-- Commands:
--  check <file>
--  find <symbol>
--  info <file> <expr>
--  type <file> <line> <column>
--  lint [hlint options] <file>
--     the format of hlint options is [String] because they may contain
--     spaces and also <file> may contain spaces.
--  boot
--  browse [<package>:]<module>
--  quit
--
-- Session separators:
--   OK -- success
--   NG -- failure

module Main where

import Config (cProjectVersion)
import Control.Applicative ((<$>))
import Control.Exception (SomeException(..))
import qualified Control.Exception as E
import Control.Monad (when)
import CoreMonad (liftIO)
import Data.List (intercalate)
import Data.List.Split (splitOn)
import Data.Version (showVersion)
import Language.Haskell.GhcMod
import Language.Haskell.GhcMod.Internal
import Paths_ghc_mod
import System.Console.GetOpt
import System.Directory (setCurrentDirectory)
import System.Environment (getArgs)
import System.Exit (ExitCode, exitFailure)
import System.IO (hFlush,stdout)

import Misc
import Utils

----------------------------------------------------------------

progVersion :: String
progVersion = "ghc-modi version " ++ showVersion version ++ " compiled by GHC " ++ cProjectVersion ++ "\n"

argspec :: [OptDescr (Options -> Options)]
argspec = [ Option "b" ["boundary"]
            (ReqArg (\s opts -> opts { lineSeparator = LineSeparator s }) "sep")
            "specify line separator (default is Nul string)"
          , Option "l" ["tolisp"]
            (NoArg (\opts -> opts { outputStyle = LispStyle }))
            "print as a list of Lisp"
          , Option "g" []
            (ReqArg (\s opts -> opts { ghcUserOptions = s : ghcUserOptions opts }) "flag") "specify a ghc flag"
          ]

usage :: String
usage =    progVersion
        ++ "Usage:\n"
        ++ "\t ghc-modi [-l] [-b sep] [-g flag]\n"
        ++ "\t ghc-modi version\n"
        ++ "\t ghc-modi help\n"

parseArgs :: [OptDescr (Options -> Options)] -> [String] -> (Options, [String])
parseArgs spec argv
    = case getOpt Permute spec argv of
        (o,n,[]  ) -> (foldr id defaultOptions o, n)
        (_,_,errs) -> E.throw (CmdArg errs)

----------------------------------------------------------------

-- Running two GHC monad threads disables the handling of
-- C-c since installSignalHandlers is called twice, sigh.

main :: IO ()
main = E.handle cmdHandler $
    go =<< parseArgs argspec <$> getArgs
  where
    cmdHandler (CmdArg _) = putStr $ usageInfo usage argspec
    go (_,"help":_) = putStr $ usageInfo usage argspec
    go (_,"version":_) = putStr progVersion
    go (opt,_) = emptyNewUnGetLine >>= run opt

run :: Options -> UnGetLine -> IO ()
run opt ref = flip E.catches handlers $ do
    cradle0 <- findCradle
    let rootdir = cradleRootDir cradle0
--        c = cradle0 { cradleCurrentDir = rootdir } TODO: ?????
    setCurrentDirectory rootdir
    prepareAutogen cradle0
    -- Asynchronous db loading starts here.
    symdbreq <- newSymDbReq opt
    (res, _) <- runGhcModT opt $ do
        crdl <- cradle
        world <- liftIO $ getCurrentWorld crdl
        loop symdbreq ref world
    case res of
        Right () -> return ()
        Left (GMECabalConfigure msg) -> do
            putStrLn $ notGood $ "cabal configure failed: " ++ show msg
            exitFailure
        Left e -> bug $ show e
  where
    -- this is just in case.
    -- If an error is caught here, it is a bug of GhcMod library.
    handlers = [ E.Handler (\(_ :: ExitCode) -> return ())
               , E.Handler (\(_ :: Restart) -> run opt ref)
               , E.Handler (\(SomeException e) -> bug $ show e) ]

bug :: String -> IO ()
bug msg = do
  putStrLn $ notGood $ "BUG: " ++ msg
  exitFailure

notGood :: String -> String
notGood msg = "NG " ++ escapeNewlines msg

escapeNewlines :: String -> String
escapeNewlines = replace "\n" "\\n" . replace "\\n" "\\\\n"

replace :: String -> String -> String -> String
replace needle replacement = intercalate replacement . splitOn needle

----------------------------------------------------------------

loop :: IOish m => SymDbReq -> UnGetLine -> World -> GhcModT m ()
loop symdbreq ref world = do
    -- blocking
    cmdArg <- liftIO $ getCommand ref
    -- after blocking, we need to see if the world has changed.
    crdl <- cradle
    changed <- liftIO $ isWorldChanged world crdl
    when changed $ do
        liftIO $ ungetCommand ref cmdArg
        E.throw Restart
    cradle >>= liftIO . prepareAutogen
    let (cmd,arg') = break (== ' ') cmdArg
        arg = dropWhile (== ' ') arg'
    (ret,ok) <- case cmd of
        "check"  -> checkStx arg
        "find"   -> findSym arg symdbreq
        "lint"   -> lintStx arg
        "info"   -> showInfo arg
        "type"   -> showType arg
        "split"  -> doSplit arg
        "sig"    -> doSig arg
        "refine" -> doRefine arg
        "auto"   -> doAuto arg
        "boot"   -> bootIt
        "browse" -> browseIt arg
        "quit"   -> return ("quit", False)
        ""       -> return ("quit", False)
        _        -> return ([], True)
    if ok then do
        liftIO $ putStr ret
        liftIO $ putStrLn "OK"
      else do
        liftIO $ putStrLn $ notGood ret
    liftIO $ hFlush stdout
    when ok $ loop symdbreq ref world

----------------------------------------------------------------

checkStx :: IOish m => FilePath -> GhcModT m (String, Bool)
checkStx file = do
    eret <- check [file]
    case eret of
        Right ret -> return (ret, True)
        Left ret  -> return (ret, True)

----------------------------------------------------------------

findSym :: IOish m => Symbol -> SymDbReq -> GhcModT m (String, Bool)
findSym sym symdbreq = do
    db <- getDb symdbreq >>= checkDb symdbreq
    ret <- lookupSymbol sym db
    return (ret, True)

lintStx :: IOish m => FilePath -> GhcModT m (String, Bool)
lintStx optFile = do
    ret <- withOptions changeOpt $ lint file
    return (ret, True)
  where
    (opts,file) = parseLintOptions optFile
    hopts = if opts == "" then [] else read opts
    changeOpt o = o { hlintOpts = hopts }

-- |
-- >>> parseLintOptions "[\"--ignore=Use camelCase\", \"--ignore=Eta reduce\"] file name"
-- (["--ignore=Use camelCase", "--ignore=Eta reduce"], "file name")
-- >>> parseLintOptions "file name"
-- ([], "file name")
parseLintOptions :: String -> (String, String)
parseLintOptions optFile = case brk (== ']') (dropWhile (/= '[') optFile) of
    ("","")      -> ([],   optFile)
    (opt',file') -> (opt', dropWhile (== ' ') file')
  where
    brk _ []         =  ([],[])
    brk p (x:xs')
        | p x        =  ([x],xs')
        | otherwise  =  let (ys,zs) = brk p xs' in (x:ys,zs)

----------------------------------------------------------------

showInfo :: IOish m => FilePath -> GhcModT m (String, Bool)
showInfo fileArg = do
    let [file, expr] = splitN 2 fileArg
    ret <- info file expr
    return (ret, True)

showType :: IOish m => FilePath -> GhcModT m (String, Bool)
showType fileArg  = do
    let [file, line, column] = splitN 3 fileArg
    ret <- types file (read line) (read column)
    return (ret, True)

doSplit :: IOish m => FilePath -> GhcModT m (String, Bool)
doSplit fileArg  = do
    let [file, line, column] = splitN 3 fileArg
    ret <- splits file (read line) (read column)
    return (ret, True)

doSig :: IOish m => FilePath -> GhcModT m (String, Bool)
doSig fileArg  = do
    let [file, line, column] = splitN 3 fileArg
    ret <- sig file (read line) (read column)
    return (ret, True)

doRefine :: IOish m => FilePath -> GhcModT m (String, Bool)
doRefine fileArg  = do
    let [file, line, column, expr] = splitN 4 fileArg
    ret <- refine file (read line) (read column) expr
    return (ret, True)

doAuto :: IOish m => FilePath -> GhcModT m (String, Bool)
doAuto fileArg  = do
    let [file, line, column] = splitN 3 fileArg
    ret <- auto file (read line) (read column)
    return (ret, True)

----------------------------------------------------------------

bootIt :: IOish m => GhcModT m (String, Bool)
bootIt = do
    ret <- boot
    return (ret, True)

browseIt :: IOish m => ModuleString -> GhcModT m (String, Bool)
browseIt mdl = do
    let (det,rest') = break (== ' ') mdl
        rest = dropWhile (== ' ') rest'
    ret <- if det == "-d"
               then withOptions setDetailed (browse rest)
               else browse mdl
    return (ret, True)
  where
    setDetailed opt = opt { detailed = True }