hsdev 0.1.0.0 → 0.1.0.1
raw patch · 10 files changed
+746/−506 lines, 10 filesdep ~ghc-mod
Dependency ranges changed: ghc-mod
Files
- hsdev.cabal +25/−10
- src/HsDev/Tools/GhcMod.hs +32/−32
- src/HsDev/Util.hs +9/−1
- tools/Commands.hs +453/−361
- tools/System/Command.hs +108/−49
- tools/Tool.hs +6/−6
- tools/Types.hs +68/−18
- tools/Update.hs +37/−21
- tools/hsclearimports.hs +7/−7
- tools/hsinspect.hs +1/−1
hsdev.cabal view
@@ -2,7 +2,7 @@ -- see http://haskell.org/cabal/users-guide/ name: hsdev -version: 0.1.0.0 +version: 0.1.0.1 synopsis: Haskell development library and tool with support of autocompletion, symbol info, go to declaration, find references etc. -- description: homepage: https://github.com/mvoidex/hsdev @@ -19,6 +19,8 @@ hs-source-dirs: src ghc-options: -threaded exposed-modules: + Data.Group + Data.Async HsDev HsDev.Cabal HsDev.Cache @@ -48,10 +50,6 @@ exposed-modules: System.Win32.PowerShell - other-modules: - Data.Group - Data.Async - build-depends: base >= 4.7 && < 5, aeson >= 0.7.0, @@ -63,7 +61,7 @@ directory >= 1.2.0, filepath >= 1.3.0, ghc >= 7.8.1, - ghc-mod >= 4.0.0, + ghc-mod >= 4.1.0 && < 4.2.0, ghc-paths >= 0.1.0, haddock >= 2.14.0, haskell-src-exts >= 1.14.0, @@ -113,6 +111,7 @@ mtl >= 2.1.0, network >= 2.4.0, process >= 1.2.0, + unordered-containers >= 0.2.0, text >= 1.1.0, transformers >= 0.3.0, unordered-containers >= 0.2.0, @@ -136,19 +135,29 @@ bytestring >= 0.10.0, containers >= 0.5.0, mtl >= 2.1.0, - transformers >= 0.3.0 + text >= 1.1.0, + transformers >= 0.3.0, + unordered-containers >= 0.2.0, + vector >= 0.10.0 executable hsclearimports main-is: hsclearimports.hs hs-source-dirs: tools + other-modules: + System.Command build-depends: base >= 4.7 && < 5, hsdev, + aeson >= 0.7.0, + aeson-pretty >= 0.7.0, directory >= 1.2.0, ghc >= 7.8.1, haskell-src-exts >= 1.14.0, containers >= 0.5.0, - mtl >= 2.1.0 + mtl >= 2.1.0, + text >= 1.1.0, + unordered-containers >= 0.2.0, + vector >= 0.10.0 executable hscabal main-is: hscabal.hs @@ -163,7 +172,10 @@ aeson-pretty >= 0.7.0, bytestring >= 0.10.0, containers >= 0.5.0, - mtl >= 2.1.0 + mtl >= 2.1.0, + text >= 1.1.0, + unordered-containers >= 0.2.0, + vector >= 0.10.0 executable hshayoo main-is: hshayoo.hs @@ -178,7 +190,10 @@ aeson-pretty >= 0.7.0, bytestring >= 0.10.0, containers >= 0.5.0, - mtl >= 2.1.0 + mtl >= 2.1.0, + text >= 1.1.0, + unordered-containers >= 0.2.0, + vector >= 0.10.0 test-suite test main-is: Test.hs
src/HsDev/Tools/GhcMod.hs view
@@ -31,18 +31,28 @@ import HsDev.Symbols import HsDev.Symbols.Location import HsDev.Tools.Base -import HsDev.Util ((.::)) +import HsDev.Util ((.::), liftException) list :: [String] -> Cabal -> ErrorT String IO [ModuleLocation] -list opts cabal = do - cradle <- liftIO $ cradle cabal Nothing - ms <- tryGhc $ GhcMod.listMods (GhcMod.defaultOptions { GhcMod.ghcOpts = opts }) cradle +list opts cabal = liftException $ do + cradle <- cradle cabal Nothing + ms <- (map splitPackage . lines) <$> GhcMod.listModules + (GhcMod.defaultOptions { GhcMod.ghcOpts = opts }) + cradle return [CabalModule cabal (readMaybe p) m | (m, p) <- ms] + where + splitPackage :: String -> (String, String) + splitPackage = second (drop 1) . break isSpace browse :: [String] -> Cabal -> String -> Maybe ModulePackage -> ErrorT String IO InspectedModule -browse opts cabal mname mpackage = inspect mloc (return $ browseInspection opts) $ do - cradle <- liftIO $ cradle cabal Nothing - ts <- tryGhc $ GhcMod.browse (GhcMod.defaultOptions { GhcMod.detailed = True, GhcMod.ghcOpts = packageOpt mpackage ++ opts, GhcMod.packageId = mpackagename }) cradle mname +browse opts cabal mname mpackage = inspect mloc (return $ browseInspection opts) $ liftException $ do + cradle <- cradle cabal Nothing + ts <- lines <$> GhcMod.browseModule + (GhcMod.defaultOptions { + GhcMod.detailed = True, + GhcMod.ghcOpts = packageOpt mpackage ++ opts }) + cradle + mpkgname return $ Module { moduleName = mname, moduleDocs = Nothing, @@ -51,7 +61,7 @@ moduleImports = [], moduleDeclarations = decls ts } where - mpackagename = fmap packageName mpackage + mpkgname = maybe mname (\p -> packageName p ++ ":" ++ mname) mpackage mloc = CabalModule cabal mpackage mname decls rs = M.fromList $ map (declarationName &&& id) $ mapMaybe parseDecl rs parseFunction s = do @@ -69,8 +79,9 @@ info :: [String] -> Cabal -> FilePath -> Maybe Project -> String -> String -> ErrorT String IO Declaration info opts cabal file mproj mname sname = do - cradle <- liftIO $ cradle cabal mproj - rs <- tryGhc $ GhcMod.info (GhcMod.defaultOptions { GhcMod.ghcOpts = cabalOpt cabal ++ opts }) cradle file mname sname + rs <- liftException $ do + cradle <- cradle cabal mproj + GhcMod.infoExpr (GhcMod.defaultOptions { GhcMod.ghcOpts = cabalOpt cabal ++ opts }) cradle file sname toDecl rs where toDecl s = maybe (throwError $ "Can't parse info: '" ++ s ++ "'") return $ parseData s `mplus` parseFunction s @@ -109,10 +120,15 @@ v .:: "type" typeOf :: [String] -> Cabal -> FilePath -> Maybe Project -> String -> Int -> Int -> ErrorT String IO [TypedRegion] -typeOf opts cabal file mproj mname line col = do - fileCts <- liftIO $ readFile file - cradle <- liftIO $ cradle cabal mproj - ts <- fmap lines $ tryGhc $ GhcMod.typeOf (GhcMod.defaultOptions { GhcMod.ghcOpts = cabalOpt cabal ++ opts }) cradle file mname line col +typeOf opts cabal file mproj mname line col = liftException $ do + fileCts <- readFile file + cradle <- cradle cabal mproj + ts <- lines <$> GhcMod.typeExpr + (GhcMod.defaultOptions { GhcMod.ghcOpts = cabalOpt cabal ++ opts }) + cradle + file + line + col return $ mapMaybe (toRegionType fileCts) ts where toRegionType :: String -> String -> Maybe TypedRegion @@ -124,30 +140,14 @@ parsePosition :: ReadM Position parsePosition = Position <$> readParse <*> readParse -tryGhc :: Ghc a -> ErrorT String IO a -tryGhc act = ErrorT $ ghandle rethrow $ liftM Right $ runGhc (Just libdir) $ do - dflags <- getSessionDynFlags - defaultCleanupHandler dflags act - where - rethrow :: SomeException -> IO (Either String a) - rethrow = return . Left . show - cradle :: Cabal -> Maybe Project -> IO GhcMod.Cradle cradle cabal Nothing = do dir <- getCurrentDirectory - return $ GhcMod.Cradle dir dir Nothing (sandbox cabal) [] + return $ GhcMod.Cradle dir dir Nothing [] cradle cabal (Just proj) = do dir <- getCurrentDirectory return $ GhcMod.Cradle dir (projectPath proj) (Just $ projectCabal proj) - (sandbox cabal) - (zip deps (repeat Nothing)) - where - deps = fromMaybe [] $ do - desc <- projectDescription proj - return $ nub $ sort $ concatMap infoDepends $ concat [ - map buildInfo (maybeToList (projectLibrary desc)), - map buildInfo (projectExecutables desc), - map buildInfo (projectTests desc)] + []
src/HsDev/Util.hs view
@@ -7,9 +7,10 @@ -- * String utils tab, tabs, trim, split, -- * Helper - (.::), + (.::), (.::?), -- * Exceptions liftException, liftExceptionM, liftIOErrors, + eitherT, -- * UTF-8 fromUtf8, toUtf8 ) where @@ -28,6 +29,7 @@ import Data.Text (Text) import qualified Data.Text.Lazy as T import qualified Data.Text.Lazy.Encoding as T +import Data.Traversable (traverse) import System.Directory import System.FilePath @@ -89,6 +91,9 @@ (.::) :: FromJSON a => HM.HashMap Text Value -> Text -> Parser a v .:: name = maybe (fail $ "key " ++ show name ++ " not present") parseJSON $ lookup name $ HM.toList v +(.::?) :: FromJSON a => HM.HashMap Text Value -> Text -> Parser (Maybe a) +v .::? name = traverse parseJSON $ lookup name $ HM.toList v + -- | Lift IO exception to ErrorT liftException :: C.MonadCatchIO m => m a -> ErrorT String m a liftException act = ErrorT $ C.catch (liftM Right act) onError where @@ -102,6 +107,9 @@ -- | Lift IO exceptions to ErrorT liftIOErrors :: C.MonadCatchIO m => ErrorT String m a -> ErrorT String m a liftIOErrors act = liftException (runErrorT act) >>= either throwError return + +eitherT :: (Monad m, Error e, MonadError e m) => Either String a -> m a +eitherT = either (throwError . strMsg) return fromUtf8 :: ByteString -> String fromUtf8 = T.unpack . T.decodeUtf8
tools/Commands.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings, CPP #-} +{-# LANGUAGE OverloadedStrings, CPP, TupleSections #-} module Commands ( mainCommands, commands @@ -13,6 +13,7 @@ import Control.Concurrent import Data.Aeson import Data.Aeson.Encode.Pretty +import Data.Aeson.Types import Data.Char import Data.Either import Data.List @@ -83,19 +84,13 @@ mainCommands :: [Command (IO ())] mainCommands = addHelp "hsdev" id $ srvCmds ++ map wrapCmd commands where wrapCmd :: Command CommandAction -> Command (IO ()) - wrapCmd = fmap sendCmd . addClientOpts . fmap withOptsArgs + wrapCmd = fmap sendCmd . addClientOpts . fmap withOptsCommand srvCmds = [ - cmd_ ["run"] [] "run interactive" runi', cmd ["server", "start"] [] "start remote server" serverOpts start', cmd ["server", "run"] [] "start server" serverOpts run', - cmd ["server", "stop"] [] "stop remote server" clientOpts stop'] + cmd ["server", "stop"] [] "stop remote server" clientOpts stop', + cmd ["connect"] [] "connect to send commands directly" clientOpts connect'] - runi' _ = do - dir <- getCurrentDirectory - db <- DB.newAsync - forever $ do - s <- getLine - processCmd (CommandOptions db (const $ return ()) (const $ return Nothing) dir putStrLn getLine (error "Not supported") exitSuccess) 1000 s (L.putStrLn . encode) start' sopts _ = do #if mingw32_HOST_OS let @@ -135,101 +130,37 @@ forkProcess proxy putStrLn $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts) #endif - run' sopts _ = do - msgs <- F.newChan - outputDone <- newEmptyMVar - forkIO $ finally - (F.readChan msgs >>= mapM_ (logMsg sopts)) - (putMVar outputDone ()) - - let - outputStr = F.putChan msgs - waitOutput = F.closeChan msgs >> takeMVar outputDone - withCache :: a -> (FilePath -> IO a) -> IO a - withCache v onCache = case getFirst (serverCache sopts) of - Nothing -> return v - Just cdir -> onCache cdir - writeCache :: Database -> IO () - writeCache d = withCache () $ \cdir -> do - outputStr "writing cache" - SC.dump cdir (structurize d) - readCache :: (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database) - readCache act = withCache Nothing $ join . liftM (either cacheErr cacheOk) . runErrorT . act where - cacheErr e = outputStr ("Unable read cache: " ++ e) >> return Nothing - cacheOk s = do - forM_ (M.keys (structuredCabals s)) $ \c -> outputStr ("cache read: cabal " ++ show c) - forM_ (M.keys (structuredProjects s)) $ \p -> outputStr ("cache read: project " ++ p) - case allModules (structuredFiles s) of - [] -> return () - ms -> outputStr $ "cache read: " ++ show (length ms) ++ " files" - return $ Just $ merge s - - outputStr $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts) - - logIO "server exception: " outputStr $ flip finally waitOutput $ do - db <- DB.newAsync - - when (getAny $ serverLoadCache sopts) $ withCache () $ \cdir -> do - outputStr $ "Loading cache from " ++ cdir - dbCache <- liftA merge <$> SC.load cdir - case dbCache of - Left err -> outputStr $ "Failed to load cache: " ++ err - Right dbCache' -> DB.update db (return dbCache') + run' sopts _ + | getAny (serverAsClient sopts) = runServer sopts $ \copts -> do + commandLog copts $ "Server started as client connecting at port " ++ show (fromJust $ getFirst $ serverPort sopts) + me <- myThreadId + s <- socket AF_INET Stream defaultProtocol + addr' <- inet_addr "127.0.0.1" + connect s $ SockAddrInet (fromIntegral $ fromJust $ getFirst $ serverPort sopts) addr' + bracket (socketToHandle s ReadWriteMode) hClose $ \h -> + processClient (show s) (hGetLine' h) (L.hPutStrLn h) sopts (copts { + commandExit = killThread me }) + | otherwise = runServer sopts $ \copts -> do + commandLog copts $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts) waitListen <- newEmptyMVar clientChan <- F.newChan - linkChan <- F.newChan - let - linkToSrv :: IO () - linkToSrv = do - v <- newEmptyMVar - F.putChan linkChan (putMVar v ()) - takeMVar v - -#if mingw32_HOST_OS - mmapPool <- createPool "hsdev" - let - -- | Send response as is or via memory mapped file - sendResponse :: Handle -> Response -> IO () - sendResponse h r@(ResponseMapFile _) = L.hPutStrLn h $ encode r - sendResponse h r - | L.length msg <= 1024 = L.hPutStrLn h msg - | otherwise = do - sync <- newEmptyMVar - forkIO $ void $ withName mmapPool $ \mmapName -> do - runErrorT $ flip catchError - (\e -> liftIO $ do - sendResponse h $ Response $ object ["error" .= e] - putMVar sync ()) - (withMapFile mmapName (L.toStrict msg) $ liftIO $ do - sendResponse h $ ResponseMapFile mmapName - putMVar sync () - -- Dirty: give 10 seconds for client to read it - threadDelay 10000000) - takeMVar sync - where - msg = encode r -#else - let - sendResponse h = L.hPutStrLn h . encode -#endif - forkIO $ do accepter <- myThreadId let + serverStop :: IO () serverStop = void $ forkIO $ do void $ tryPutMVar waitListen () killThread accepter s <- socket AF_INET Stream defaultProtocol - bind s (SockAddrInet (fromIntegral $ fromJust $ getFirst $ serverPort sopts) iNADDR_ANY) + bind s $ SockAddrInet (fromIntegral $ fromJust $ getFirst $ serverPort sopts) iNADDR_ANY listen s maxListenQueue - forever $ logIO "accept client exception: " outputStr $ do - s' <- fmap fst $ accept s - outputStr $ show s' ++ " connected" - void $ forkIO $ logIO (show s' ++ " exception: ") outputStr $ + forever $ logIO "accept client exception: " (commandLog copts) $ do + s' <- fst <$> accept s + void $ forkIO $ logIO (show s' ++ " exception: ") (commandLog copts) $ bracket (socketToHandle s' ReadWriteMode) hClose $ \h -> do bracket newEmptyMVar (`putMVar` ()) $ \done -> do me <- myThreadId @@ -238,79 +169,87 @@ notDone <- isEmptyMVar done when notDone $ do void $ forkIO $ do - threadDelay 10000000 + threadDelay 1000000 tryPutMVar done () killThread me - void $ takeMVar done + takeMVar done + waitForever = forever $ hGetLine' h F.putChan clientChan timeoutWait - req <- hGetLine' h - outputStr $ show s' ++ ": " ++ fromUtf8 req - case fmap extractCurrentDir $ eitherDecode req of - Left reqErr -> sendResponse h $ Response $ object [ - "error" .= ("Invalid request" :: String), - "request" .= fromUtf8 req, - "what" .= reqErr] - Right (clientDir, reqArgs) -> processCmdArgs - (CommandOptions - db - writeCache - readCache - clientDir - outputStr - (fromUtf8 <$> hGetLine' h) - linkToSrv - serverStop) - (fromJust $ getFirst $ serverTimeout sopts) reqArgs (sendResponse h) - -- Send 'end' message and wait client - L.hPutStrLn h L.empty - outputStr $ "waiting " ++ show s' - ignoreIO $ void $ timeout 10000000 $ hGetLine' h - outputStr $ show s' ++ " disconnected" + processClient (show s') (hGetLine' h) (L.hPutStrLn h) sopts (copts { + commandHold = waitForever, + commandExit = serverStop }) takeMVar waitListen - withCache () $ \cdir -> do - outputStr $ "saving cache to " ++ cdir - logIO "cache saving exception: " outputStr $ do - dbval <- DB.readAsync db - SC.dump cdir $ structurize dbval - outputStr "cache saved" - outputStr "closing links" - F.stopChan linkChan >>= sequence_ - outputStr "waiting for clients" + DB.readAsync (commandDatabase copts) >>= writeCache sopts (commandLog copts) F.stopChan clientChan >>= sequence_ - outputStr "server shutdown" + commandLog copts "server stopped" + stop' copts _ = run (map wrapCmd' commands) onDef onError ["exit"] where onDef = putStrLn "Command 'exit' not found" onError es = putStrLn $ "Failed to stop server: " ++ intercalate ", " es - wrapCmd' = fmap (sendCmd . fmap ((,) copts) . withOptsArgs) + wrapCmd' = fmap (sendCmd . (copts,) . withOptsCommand) - logIO :: String -> (String -> IO ()) -> IO () -> IO () - logIO pre out act = handle onIO act where - onIO :: IOException -> IO () - onIO e = out $ pre ++ show e + connect' copts _ = do + curDir <- getCurrentDirectory + s <- socket AF_INET Stream defaultProtocol + addr' <- inet_addr "127.0.0.1" + connect s (SockAddrInet (fromIntegral $ fromJust $ getFirst $ clientPort copts) addr') + bracket (socketToHandle s ReadWriteMode) hClose $ \h -> forever $ ignoreIO $ do + cmd <- hGetLine' stdin + case eitherDecode cmd of + Left e -> L.putStrLn $ encodeValue $ object ["error" .= ("invalid command" :: String)] + Right cmd' -> do + L.hPutStrLn h $ encode $ cmd' `addCallOpts` ["current-directory" %-- curDir] + waitResp h + where + pretty = getAny $ clientPretty copts + encodeValue :: ToJSON a => a -> L.ByteString + encodeValue + | pretty = encodePretty + | otherwise = encode - ignoreIO :: IO () -> IO () - ignoreIO = handle (const (return ()) :: IOException -> IO ()) + waitResp h = do + resp <- hGetLine' h + parseResp h resp - logMsg :: ServerOpts -> String -> IO () - logMsg sopts s = ignoreIO $ do - putStrLn s - case getFirst (serverLog sopts) of - Nothing -> return () - Just f -> withFile f AppendMode (`hPutStrLn` s) + parseResp h str = void $ runErrorT $ flip catchError (liftIO . putStrLn) $ do + v <- ErrorT (return $ eitherDecode str) `orFail` ("Can't decode response", ["response" .= fromUtf8 str]) + case v of + ResponseStatus s -> liftIO $ do + L.putStrLn $ encodeValue s + liftIO $ waitResp h +#if mingw32_HOST_OS + ResponseMapFile viewFile -> do + str <- fmap L.fromStrict (readMapFile viewFile) `orFail` + ("Can't read map view of file", ["file" .= viewFile]) + lift $ parseResp h str +#else + ResponseMapFile viewFile -> throwError $ fromUtf8 $ encodeValue $ + object ["error" .= ("Not supported" :: String)] +#endif + Response r -> liftIO $ L.putStrLn $ encodeValue r + where + orFail :: (Monad m, Functor m) => ErrorT String m a -> (String, [Pair]) -> ErrorT String m a + orFail act (msg, fs) = act <|> (throwError $ fromUtf8 $ encodeValue $ object ( + ("error" .= msg) : fs)) -- Send command to server - sendCmd :: IO (ClientOpts, [String]) -> IO () - sendCmd get' = do + sendCmd :: (ClientOpts, CommandCall) -> IO () + sendCmd (p, cmdCall) = do svar <- newEmptyMVar race [takeMVar svar, waitResponse >> putMVar svar ()] where + pretty = getAny $ clientPretty p + encodeValue :: ToJSON a => a -> L.ByteString + encodeValue + | pretty = encodePretty + | otherwise = encode + waitResponse = do curDir <- getCurrentDirectory - (p, as) <- get' stdinData <- if getAny (clientData p) then do - cdata <- liftM eitherDecode L.getContents + cdata <- liftM (eitherDecode :: L.ByteString -> Either String Value) L.getContents case cdata of Left cdataErr -> do putStrLn $ "Invalid data: " ++ cdataErr @@ -322,49 +261,169 @@ addr' <- inet_addr "127.0.0.1" connect s (SockAddrInet (fromIntegral $ fromJust $ getFirst $ clientPort p) addr') h <- socketToHandle s ReadWriteMode - L.hPutStrLn h $ encode $ ["--current-directory=" ++ curDir] ++ setData stdinData as - responses <- liftM (takeWhile (not . L.null) . L.lines) $ L.hGetContents h - forM_ responses $ - parseResponse >=> - (L.putStrLn . encodeValue (getAny $ clientPretty p)) + L.hPutStrLn h $ encode $ cmdCall `addCallOpts` [ + "current-directory" %-- curDir, + case stdinData of + Nothing -> mempty + Just d -> "data" %-- (fromUtf8 $ encode d)] + peekResponse h - setData :: Maybe ResultValue -> [String] -> [String] - setData Nothing = id - setData (Just d) = (++ ["--data=" ++ (fromUtf8 $ encode d)]) + peekResponse h = do + resp <- hGetLine' h + parseResponse h resp - parseResponse r = fmap (either err' id) $ runErrorT $ do - v <- errT (eitherDecode r) `orFail` (\e -> "Can't decode response") + parseResponse h str = void $ runErrorT $ flip catchError (liftIO . putStrLn) $ do + v <- ErrorT (return $ eitherDecode str) `orFail` ("Can't decode response", ["response" .= fromUtf8 str]) case v of - Response rv -> return rv - ResponseStatus sv -> return sv + ResponseStatus s -> liftIO $ do + L.putStrLn $ encodeValue s + peekResponse h #if mingw32_HOST_OS ResponseMapFile viewFile -> do str <- fmap L.fromStrict (readMapFile viewFile) `orFail` - (\e -> "Can't read map view of file") - lift $ parseResponse str + ("Can't read map view of file", ["file" .= viewFile]) + lift $ parseResponse h str #else - ResponseMapFile viewFile -> return $ err' ("Not supported" :: String) + ResponseMapFile viewFile -> throwError $ fromUtf8 $ encodeValue $ + object ["error" .= ("Not supported" :: String)] #endif + Response r -> liftIO $ L.putStrLn $ encodeValue r where - errT act = ErrorT $ return act - orFail act msg = act `catchError` (throwError . msg) - err' msg = object ["error" .= msg] - - encodeValue True = encodePretty - encodeValue False = encode + orFail :: (Monad m, Functor m) => ErrorT String m a -> (String, [Pair]) -> ErrorT String m a + orFail act (msg, fs) = act <|> (throwError $ fromUtf8 $ encodeValue $ object ( + ("error" .= msg) : fs)) -- Add parsing 'ClieptOpts' - addClientOpts :: Command (IO [String]) -> Command (IO (ClientOpts, [String])) + addClientOpts :: Command CommandCall -> Command (ClientOpts, CommandCall) addClientOpts c = c { commandRun = run' } where - run' args = fmap (fmap (fmap $ (,) p)) $ commandRun c args' where + run' args = fmap (fmap (p,)) $ commandRun c args' where (ps, args', _) = getOpt RequireOrder clientOpts args p = mconcat ps `mappend` defaultConfig - extractCurrentDir :: [String] -> (FilePath, [String]) - extractCurrentDir as = (head $ cur ++ ["."], as') where - (cur, as', _) = getOpt RequireOrder curDirOpts as - curDirOpts = [Option [] ["current-directory"] (ReqArg id "path") "current directory"] +-- | Inits log chan and returns functions (print message, wait channel) +initLog :: ServerOpts -> IO (String -> IO (), IO ()) +initLog sopts = do + msgs <- F.newChan + outputDone <- newEmptyMVar + forkIO $ finally + (F.readChan msgs >>= mapM_ (logMsg sopts)) + (putMVar outputDone ()) + return (F.putChan msgs, F.closeChan msgs >> takeMVar outputDone) +-- | Run server +runServer :: ServerOpts -> (CommandOptions -> IO ()) -> IO () +runServer sopts act = bracket (initLog sopts) snd $ \(outputStr, waitOutput) -> do + db <- DB.newAsync + when (getAny $ serverLoadCache sopts) $ withCache sopts () $ \cdir -> do + outputStr $ "Loading cache from " ++ cdir + dbCache <- liftA merge <$> SC.load cdir + case dbCache of + Left err -> outputStr $ "Failed to load cache: " ++ err + Right dbCache' -> DB.update db (return dbCache') +#if mingw32_HOST_OS + mmapPool <- Just <$> createPool "hsdev" +#endif + act $ CommandOptions + db + (writeCache sopts outputStr) + (readCache sopts outputStr) + "." + outputStr + waitOutput +#if mingw32_HOST_OS + mmapPool +#endif + (return ()) + (return ()) + (return ()) + +withCache :: ServerOpts -> a -> (FilePath -> IO a) -> IO a +withCache sopts v onCache = case getFirst (serverCache sopts) of + Nothing -> return v + Just cdir -> onCache cdir + +writeCache :: ServerOpts -> (String -> IO ()) -> Database -> IO () +writeCache sopts logMsg d = withCache sopts () $ \cdir -> do + logMsg $ "writing cache to " ++ cdir + logIO "cache writing exception: " logMsg $ do + SC.dump cdir $ structurize d + logMsg $ "cache saved to " ++ cdir + +readCache :: ServerOpts -> (String -> IO ()) -> (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database) +readCache sopts logMsg act = withCache sopts Nothing $ join . liftM (either cacheErr cacheOk) . runErrorT . act where + cacheErr e = logMsg ("Error reading cache: " ++ e) >> return Nothing + cacheOk s = do + forM_ (M.keys (structuredCabals s)) $ \c -> logMsg ("cache read: cabal " ++ show c) + forM_ (M.keys (structuredProjects s)) $ \p -> logMsg ("cache read: project " ++ p) + case allModules (structuredFiles s) of + [] -> return () + ms -> logMsg $ "cache read: " ++ show (length ms) ++ " files" + return $ Just $ merge s + +#if mingw32_HOST_OS +sendResponseMmap :: Pool -> (ByteString -> IO ()) -> Response -> IO () +sendResponseMmap mmapPool send r@(ResponseMapFile _) = send $ encode r +sendResponseMmap mmapPool send r + | L.length msg <= 1024 = send msg + | otherwise = do + sync <- newEmptyMVar + forkIO $ void $ withName mmapPool $ \mmapName -> do + runErrorT $ flip catchError + (\e -> liftIO $ do + sendResponseMmap mmapPool send $ Response $ object ["error" .= e] + putMVar sync ()) + (withMapFile mmapName (L.toStrict msg) $ liftIO $ do + sendResponseMmap mmapPool send $ ResponseMapFile mmapName + putMVar sync () + -- give 10 seconds for client to read data + threadDelay 10000000) + takeMVar sync + where + msg = encode r +#endif + +sendResponse :: (ByteString -> IO ()) -> Response -> IO () +sendResponse = (. encode) + +processClient :: String -> IO ByteString -> (ByteString -> IO ()) -> ServerOpts -> CommandOptions -> IO () +processClient name receive send sopts copts = do + commandLog copts $ name ++ " connected" + linkVar <- newMVar $ return () + flip finally (disconnected linkVar) $ forever $ do + req <- receive + commandLog copts $ name ++ " >> " ++ fromUtf8 req + case extractMeta <$> eitherDecode req of + Left err -> answer True $ Response $ object [ + "error" .= ("Invalid request" :: String), + "request" .= fromUtf8 req, + "what" .= err] + Right (cdir, noFile, reqArgs) -> processCmdArgs + (copts { commandLink = void (swapMVar linkVar $ commandExit copts), commandRoot = cdir }) + (fromJust $ getFirst $ serverTimeout sopts) + (callArgs reqArgs) + (answer noFile) + where + answer :: Bool -> Response -> IO () + answer noFile' r = do + commandLog copts $ name ++ " << " ++ fromUtf8 (encode r) +#if mingw32_HOST_OS + case noFile' of + True -> sendResponse send r + False -> maybe (sendResponse send) (`sendResponseMmap` send) (commandMmapPool copts) r +#else + sendResponse send r +#endif + + extractMeta :: CommandCall -> (FilePath, Bool, CommandCall) + extractMeta c = (fpath, noFile, c `removeCallOpts` ["current-directory", "no-file"]) where + fpath = fromMaybe (commandRoot copts) $ arg "current-directory" $ commandCallOpts c + noFile = flag "no-file" $ commandCallOpts c + + disconnected :: MVar (IO ()) -> IO () + disconnected var = do + commandLog copts $ name ++ " disconnected" + join $ takeMVar var + commands :: [Command CommandAction] commands = map wrapErrors $ map (fmap (fmap timeout')) cmds ++ map (fmap (fmap noTimeout)) linkCmd where timeout' :: (CommandOptions -> IO CommandResult) -> (Int -> CommandOptions -> IO CommandResult) @@ -385,9 +444,9 @@ cmd_' ["ping"] [] "ping server" ping', -- Database commands cmd' ["add"] [] "add info to database" [dataArg] add', - cmd' ["scan", "cabal"] [] "scan modules installed in cabal" [ - sandbox, ghcOpts, wait, status] scanCabal', - cmd' ["scan", "module"] ["module name"] "scan module in cabal" [sandbox, ghcOpts] scanModule', + cmd' ["scan", "cabal"] [] "scan modules installed in cabal" (sandboxes ++ [ + ghcOpts, wait, status]) scanCabal', + cmd' ["scan", "module"] ["module name"] "scan module in cabal" (sandboxes ++ [ghcOpts]) scanModule', cmd' ["scan"] [] "scan sources" [ projectArg "project path or .cabal", fileArg "source file", @@ -398,33 +457,32 @@ fileArg "source file", pathArg "path to rescan", ghcOpts, wait, status] rescan', - cmd' ["remove"] [] "remove modules info" [ - sandbox, + cmd' ["remove"] [] "remove modules info" (sandboxes ++ [ projectArg "module project", fileArg "module source file", moduleArg, packageArg, noLastArg, packageVersionArg, - allFlag] remove', + allFlag]) remove', -- | Context free commands - cmd' ["list", "modules"] [] "list modules" [ - projectArg "project to list modules from", + cmd' ["list", "modules"] [] "list modules" (sandboxes ++ [ + projectArg "projects to list modules from", noLastArg, packageArg, - sandbox, sourced, standaloned] listModules', + sourced, standaloned]) listModules', cmd_' ["list", "packages"] [] "list packages" listPackages', cmd_' ["list", "projects"] [] "list projects" listProjects', - cmd' ["symbol"] ["name"] "get symbol info" (matches ++ [ + cmd' ["symbol"] ["name"] "get symbol info" (matches ++ sandboxes ++ [ projectArg "related project", fileArg "source file", moduleArg, localsArg, packageArg, noLastArg, packageVersionArg, - sandbox, sourced, standaloned]) symbol', - cmd' ["module"] [] "get module info" [ + sourced, standaloned]) symbol', + cmd' ["module"] [] "get module info" (sandboxes ++ [ moduleArg, localsArg, packageArg, noLastArg, packageVersionArg, projectArg "module project", fileArg "module source file", - sandbox, sourced] modul', + sourced]) modul', cmd' ["project"] [] "get project info" [ projectArg "project path or name"] project', -- Context commands @@ -438,17 +496,17 @@ cmd' ["cabal", "list"] ["packages..."] "list cabal packages" [] cabalList', cmd' ["ghc-mod", "type"] ["line", "column"] "infer type with 'ghc-mod type'" ctx ghcmodType', -- Dump/load commands - cmd' ["dump", "cabal"] [] "dump cabal modules" [sandbox, cacheDir, cacheFile] dumpCabal', + cmd' ["dump", "cabal"] [] "dump cabal modules" (sandboxes ++ [cacheDir, cacheFile]) dumpCabal', cmd' ["dump", "projects"] [] "dump projects" [projectArg "project", cacheDir, cacheFile] dumpProjects', cmd' ["dump", "files"] [] "dump standalone files" [cacheDir, cacheFile] dumpFiles', cmd' ["dump"] [] "dump whole database" [cacheDir, cacheFile] dump', cmd' ["load"] [] "load data" [cacheDir, cacheFile, dataArg, wait] load', -- Exit cmd_' ["exit"] [] "exit" exit'] - linkCmd = [cmd' ["link"] [] "link to server" [] link'] + linkCmd = [cmd' ["link"] [] "link to server" [holdArg] link'] -- Command arguments and flags - allFlag = option_ ['a'] "all" flag "remove all" + allFlag = option_ ['a'] "all" no "remove all" cacheDir = pathArg "cache path" cacheFile = fileArg "cache file" ctx = [fileArg "source file", sandbox] @@ -456,9 +514,10 @@ fileArg = option_ ['f'] "file" (req "file") findArg = option_ [] "find" (req "find") "infix match" ghcOpts = option_ ['g'] "ghc" (req "ghc options") "options to pass to GHC" - globalArg = option_ [] "global" flag "scope of project" - localsArg = option_ ['l'] "locals" flag "look in local declarations" - noLastArg = option_ [] "no-last" flag "don't select last package version" + globalArg = option_ [] "global" no "scope of project" + holdArg = option_ ['h'] "hold" no "don't return any response" + localsArg = option_ ['l'] "locals" no "look in local declarations" + noLastArg = option_ [] "no-last" no "don't select last package version" matches = [prefixArg, findArg] moduleArg = option_ ['m'] "module" (req "module name") "module name" packageArg = option_ [] "package" (req "package") "module package" @@ -466,19 +525,22 @@ prefixArg = option_ [] "prefix" (req "prefix") "prefix match" projectArg = option [] "project" ["proj"] (req "project") packageVersionArg = option_ ['v'] "version" (req "version") "package version" - sandbox = option_ [] "sandbox" (noreq "path") "path to cabal sandbox" - sourced = option_ [] "src" flag "source files" - standaloned = option_ [] "stand" flag "standalone files" - status = option_ ['s'] "status" flag "show status of operation, works only with --wait" - wait = option_ ['w'] "wait" flag "wait for operation to complete" + sandbox = option_ [] "sandbox" (req "path") "path to cabal sandbox" + sandboxes = [ + option_ [] "cabal" no "cabal", + sandbox] + sourced = option_ [] "src" no "source files" + standaloned = option_ [] "stand" no "standalone files" + status = option_ ['s'] "status" no "show status of operation, works only with --wait" + wait = option_ ['w'] "wait" no "wait for operation to complete" -- ping server - ping' _ copts = return $ ResultOk $ ResultString "pong" + ping' _ copts = return $ ResultOk $ ResultMap $ M.singleton "message" (ResultString "pong") -- add data add' as _ copts = do dbval <- getDb copts res <- runErrorT $ do - jsonData <- maybe (throwError $ err "Specify --data") return $ askOpt "data" as + jsonData <- maybe (throwError $ err "Specify --data") return $ arg "data" as decodedData <- either (\err -> throwError (errArgs "Unable to decode data" [ ("why", ResultString err), @@ -512,20 +574,20 @@ return $ either id (const (ResultOk ResultNone)) res -- scan scan' as _ copts = updateProcess copts as $ - mapM_ (\(n, f) -> forM_ (askOpts n as) (canonicalizePath' copts >=> f (getGhcOpts as))) [ + mapM_ (\(n, f) -> forM_ (list n as) (findPath copts >=> f (list "ghc" as))) [ ("project", Update.scanProject), ("file", Update.scanFile), ("path", Update.scanDirectory)] -- scan cabal scanCabal' as _ copts = error_ $ do - cabal <- getCabal copts as - lift $ updateProcess copts as $ Update.scanCabal (getGhcOpts as) cabal + cabals <- getSandboxes copts as + lift $ updateProcess copts as $ mapM_ (Update.scanCabal $ list "ghc" as) cabals -- scan cabal module scanModule' as [] copts = return $ err "Module name not specified" scanModule' as ms copts = error_ $ do cabal <- getCabal copts as lift $ updateProcess copts as $ - forM_ ms (Update.scanModule (getGhcOpts as) . CabalModule cabal Nothing) + forM_ ms (Update.scanModule (list "ghc" as) . CabalModule cabal Nothing) -- rescan rescan' as _ copts = do dbval <- getDb copts @@ -534,21 +596,22 @@ selectModules (byFile . moduleId) dbval (errors, filteredMods) <- liftM partitionEithers $ mapM runErrorT $ concat [ - do - p <- askOpts "project" as - return $ do - p' <- getProject copts p - return $ M.fromList $ mapMaybe toPair $ - selectModules (inProject p' . moduleId) dbval, - do - f <- askOpts "file" as - return $ maybe - (throwError $ "Unknown file: " ++ f) - (return . M.singleton f) - (lookupFile f dbval), - do - d <- askOpts "path" as - return $ return $ M.filterWithKey (\f _ -> isParent d f) fileMap] + [do + p' <- findProject copts p + return $ M.fromList $ mapMaybe toPair $ + selectModules (inProject p' . moduleId) dbval | + p <- list "project" as], + [do + f' <- findPath copts f + maybe + (throwError $ "Unknown file: " ++ f') + (return . M.singleton f') + (lookupFile f' dbval) | + f <- list "file" as], + [do + d' <- findPath copts d + return $ M.filterWithKey (\f _ -> isParent d' f) fileMap | + d <- list "path" as]] let rescanMods = map (getInspected dbval) $ M.elems $ if null filteredMods then fileMap else M.unions filteredMods @@ -556,22 +619,22 @@ if not (null errors) then return $ err $ intercalate ", " errors else updateProcess copts as $ Update.runTask (toJSON $ ("rescanning modules" :: String)) $ do - needRescan <- Update.liftErrorT $ filterM (changedModule dbval (getGhcOpts as) . inspectedId) rescanMods - Update.scanModules (getGhcOpts as) (map (inspectedId &&& inspectionOpts . inspection) needRescan) + needRescan <- Update.liftErrorT $ filterM (changedModule dbval (list "ghc" as) . inspectedId) rescanMods + Update.scanModules (list "ghc" as) (map (inspectedId &&& inspectionOpts . inspection) needRescan) -- remove remove' as _ copts = errorT $ do - dbval <- liftIO $ getDb copts - cabal <- askCabal copts as - proj <- askProject copts as - file <- traverse (canonicalizePath' copts) $ askOpt "file" as + dbval <- getDb copts + cabal <- getCabal_ copts as + proj <- traverse (findProject copts) $ arg "project" as + file <- traverse (findPath copts) $ arg "file" as let - cleanAll = hasOpt "all" as + cleanAll = flag "all" as filters = catMaybes [ fmap inProject proj, fmap inFile file, - fmap inModule (askOpt "module" as), - fmap inPackage (askOpt "package" as), - fmap inVersion (askOpt "version" as), + fmap inModule (arg "module" as), + fmap inPackage (arg "package" as), + fmap inVersion (arg "version" as), fmap inCabal cabal] toClean = newest as $ filter (allOf filters . moduleId) (allModules dbval) action @@ -586,16 +649,20 @@ action -- list modules listModules' as _ copts = errorT $ do - dbval <- liftIO $ getDb copts - proj <- askProject copts as - cabal <- askCabal copts as + dbval <- getDb copts + projs <- traverse (findProject copts) $ list "project" as + cabals <- getSandboxes copts as let + packages = list "package" as + hasFilters = not $ null projs && null packages && null cabals filters = allOf $ catMaybes [ - fmap inProject proj, - fmap inPackage (askOpt "package" as), - fmap inCabal cabal, - if hasOpt "src" as then Just byFile else Nothing, - if hasOpt "stand" as then Just standalone else Nothing] + if hasFilters + then Just $ anyOf [ + \m -> any (`inProject` m) projs, + \m -> any (`inPackage` m) packages && any (`inCabal` m) cabals] + else Nothing, + if flag "src" as then Just byFile else Nothing, + if flag "stand" as then Just standalone else Nothing] return $ ResultList $ map (ResultModuleId . moduleId) $ newest as $ selectModules (filters . moduleId) dbval -- list packages listPackages' _ copts = do @@ -610,20 +677,20 @@ return $ ResultOk $ ResultList $ map ResultProject $ M.elems $ databaseProjects dbval -- get symbol info symbol' as ns copts = errorT $ do - dbval <- liftM (localsDatabase as) $ liftIO $ getDb copts - proj <- askProject copts as - file <- traverse (canonicalizePath' copts) $ askOpt "file" as - cabal <- askCabal copts as + dbval <- liftM (localsDatabase as) $ getDb copts + proj <- traverse (findProject copts) $ arg "project" as + file <- traverse (findPath copts) $ arg "file" as + cabal <- getCabal_ copts as let filters = checkModule $ allOf $ catMaybes [ fmap inProject proj, fmap inFile file, - fmap inModule (askOpt "module" as), - fmap inPackage (askOpt "package" as), - fmap inVersion (askOpt "version" as), + fmap inModule (arg "module" as), + fmap inPackage (arg "package" as), + fmap inVersion (arg "version" as), fmap inCabal cabal, - if hasOpt "src" as then Just byFile else Nothing, - if hasOpt "stand" as then Just standalone else Nothing] + if flag "src" as then Just byFile else Nothing, + if flag "stand" as then Just standalone else Nothing] toResult = ResultList . map ResultModuleDeclaration . newest as . filterMatch as . filter filters case ns of [] -> return $ toResult $ allDeclarations dbval @@ -632,62 +699,61 @@ _ -> throwError "Too much arguments" -- get module info modul' as _ copts = errorT' $ do - dbval <- liftM (localsDatabase as) $ liftIO $ getDb copts - proj <- mapErrorT (fmap $ strMsg +++ id) $ askProject copts as - cabal <- mapErrorT (fmap $ strMsg +++ id) $ askCabal copts as - file' <- traverse (canonicalizePath' copts) $ askOpt "file" as + dbval <- liftM (localsDatabase as) $ getDb copts + proj <- mapErrorT (fmap $ strMsg +++ id) $ traverse (findProject copts) $ arg "project" as + cabal <- mapErrorT (fmap $ strMsg +++ id) $ getCabal_ copts as + file' <- mapErrorT (fmap $ strMsg +++ id) $ traverse (findPath copts) $ arg "file" as let filters = allOf $ catMaybes [ fmap inProject proj, fmap inCabal cabal, fmap inFile file', - fmap inModule (askOpt "module" as), - fmap inPackage (askOpt "package" as), - fmap inVersion (askOpt "version" as), - if hasOpt "src" as then Just byFile else Nothing] + fmap inModule (arg "module" as), + fmap inPackage (arg "package" as), + fmap inVersion (arg "version" as), + if flag "src" as then Just byFile else Nothing] rs <- mapErrorT (fmap $ strMsg +++ id) $ (newest as . filter (filters . moduleId)) <$> maybe (return $ allModules dbval) (findModule dbval) - (askOpt "module" as) + (arg "module" as) case rs of [] -> throwError $ err "Module not found" [m] -> return $ ResultModule m ms' -> throwError $ errArgs "Ambiguous modules" [("modules", ResultList $ map (ResultModuleId . moduleId) ms')] -- get project info project' as _ copts = errorT $ do - proj <- askProject copts as - proj' <- maybe (throwError "Specify project name of .cabal file") return proj - return $ ResultProject proj' + proj <- maybe (throwError "Specify project name or .cabal file") (findProject copts) $ arg "project" as + return $ ResultProject proj -- lookup info about symbol lookup' as [nm] copts = errorT $ do - dbval <- liftIO $ getDb copts - (srcFile, cabal) <- askCtx copts as + dbval <- getDb copts + (srcFile, cabal) <- getCtx copts as liftM (ResultList . map ResultModuleDeclaration) $ lookupSymbol dbval cabal srcFile nm lookup' as _ copts = return $ err "Invalid arguments" -- get detailed info about symbol in source file whois' as [nm] copts = errorT $ do - dbval <- liftIO $ getDb copts - (srcFile, cabal) <- askCtx copts as + dbval <- getDb copts + (srcFile, cabal) <- getCtx copts as liftM (ResultList . map ResultModuleDeclaration) $ whois dbval cabal srcFile nm whois' as _ copts = return $ err "Invalid arguments" -- get modules accessible from module scopeModules' as [] copts = errorT $ do - dbval <- liftIO $ getDb copts - (srcFile, cabal) <- askCtx copts as + dbval <- getDb copts + (srcFile, cabal) <- getCtx copts as liftM (ResultList . map (ResultModuleId . moduleId)) $ scopeModules dbval cabal srcFile scopeModules' as _ copts = return $ err "Invalid arguments" -- get declarations accessible from module scope' as [] copts = errorT $ do - dbval <- liftIO $ getDb copts - (srcFile, cabal) <- askCtx copts as - liftM (ResultList . map ResultModuleDeclaration . filterMatch as) $ scope dbval cabal srcFile (hasOpt "global" as) + dbval <- getDb copts + (srcFile, cabal) <- getCtx copts as + liftM (ResultList . map ResultModuleDeclaration . filterMatch as) $ scope dbval cabal srcFile (flag "global" as) scope' as _ copts = return $ err "Invalid arguments" -- completion complete' as [] copts = complete' as [""] copts complete' as [input] copts = errorT $ do dbval <- getDb copts - (srcFile, cabal) <- askCtx copts as + (srcFile, cabal) <- getCtx copts as liftM (ResultList . map ResultModuleDeclaration) $ completions dbval cabal srcFile input complete' as _ copts = return $ err "Invalid arguments" -- hayoo @@ -706,82 +772,84 @@ ghcmodType' as [line, column] copts = errorT $ do line' <- maybe (throwError "line must be a number") return $ readMaybe line column' <- maybe (throwError "column must be a number") return $ readMaybe column - dbval <- liftIO $ getDb copts - (srcFile, cabal) <- askCtx copts as + dbval <- getDb copts + (srcFile, cabal) <- getCtx copts as (srcFile', m, mproj) <- fileCtx dbval srcFile - tr <- GhcMod.typeOf (getGhcOpts as) cabal srcFile' mproj (moduleName m) line' column' + tr <- GhcMod.typeOf (list "ghc" as) cabal srcFile' mproj (moduleName m) line' column' return $ ResultList $ map ResultTyped tr ghcmodType' as [] copts = return $ err "Specify line" ghcmodType' as _ copts = return $ err "Too much arguments" -- dump cabal modules dumpCabal' as _ copts = errorT $ do - dbval <- liftIO $ getDb copts - cabal <- getCabal copts as + dbval <- getDb copts + cabals <- getSandboxes copts as let - dat = cabalDB cabal dbval - liftM (fromMaybe (ResultDatabase dat)) $ runMaybeT $ msum [ - maybeOpt "path" as $ canonicalizePath' copts >=> \p -> - fork (dump (p </> cabalCache cabal) dat), - maybeOpt "file" as $ canonicalizePath' copts >=> \f -> - fork (dump f dat)] + dats = map (id &&& flip cabalDB dbval) cabals + liftM (fromMaybe (ResultList $ map (ResultDatabase . snd) dats)) $ + runMaybeT $ msum [ + maybeOpt "path" as $ (lift . findPath copts) >=> \p -> + fork (forM_ dats $ \(cabal, dat) -> (dump (p </> cabalCache cabal) dat)), + maybeOpt "file" as $ (lift . findPath copts) >=> \f -> + fork (dump f $ mconcat $ map snd dats)] -- dump projects dumpProjects' as [] copts = errorT $ do - dbval <- liftIO $ getDb copts - ps' <- traverse (getProject copts) $ askOpts "project" as + dbval <- getDb copts + ps' <- traverse (findProject copts) $ list "project" as let ps = if null ps' then M.elems (databaseProjects dbval) else ps' dats = map (id &&& flip projectDB dbval) ps liftM (fromMaybe (ResultList $ map (ResultDatabase . snd) dats)) $ runMaybeT $ msum [ - maybeOpt "path" as $ canonicalizePath' copts >=> \p -> + maybeOpt "path" as $ (lift . findPath copts) >=> \p -> fork (forM_ dats $ \(proj, dat) -> (dump (p </> projectCache proj) dat)), - maybeOpt "file" as $ canonicalizePath' copts >=> \f -> + maybeOpt "file" as $ (lift . findPath copts) >=> \f -> fork (dump f (mconcat $ map snd dats))] dumpProjects' as _ copts = return $ err "Invalid arguments" -- dump files - dumpFiles' as [] copts = do + dumpFiles' as [] copts = errorT $ do dbval <- getDb copts let dat = standaloneDB dbval - liftM (ResultOk . fromMaybe (ResultDatabase dat)) $ runMaybeT $ msum [ - maybeOpt "path" as $ canonicalizePath' copts >=> \p -> + liftM (fromMaybe $ ResultDatabase dat) $ runMaybeT $ msum [ + maybeOpt "path" as $ (lift . findPath copts) >=> \p -> fork (dump (p </> standaloneCache) dat), - maybeOpt "file" as $ canonicalizePath' copts >=> \f -> + maybeOpt "file" as $ (lift . findPath copts) >=> \f -> fork (dump f dat)] dumpFiles' as _ copts = return $ err "Invalid arguments" -- dump database - dump' as _ copts = do + dump' as _ copts = errorT $ do dbval <- getDb copts - liftM (fromMaybe (ResultOk $ ResultDatabase dbval)) $ runMaybeT $ msum [ + liftM (fromMaybe $ ResultDatabase dbval) $ runMaybeT $ msum [ do - p <- MaybeT $ traverse (canonicalizePath' copts) $ askOpt "path" as + p <- MaybeT $ traverse (findPath copts) $ arg "path" as fork $ SC.dump p $ structurize dbval - return ok, + return ResultNone, do - f <- MaybeT $ traverse (canonicalizePath' copts) $ askOpt "file" as + f <- MaybeT $ traverse (findPath copts) $ arg "file" as fork $ dump f dbval - return ok] + return ResultNone] -- load database load' as _ copts = do res <- liftM (fromMaybe (err "Specify one of: --path, --file or --data")) $ runMaybeT $ msum [ do - p <- MaybeT $ return $ askOpt "path" as + p <- MaybeT $ return $ arg "path" as forkOrWait as $ cacheLoad copts (liftA merge <$> SC.load p) return ok, do - f <- MaybeT $ return $ askOpt "file" as + f <- MaybeT $ return $ arg "file" as e <- liftIO $ doesFileExist f forkOrWait as $ when e $ cacheLoad copts (load f) return ok, do - dat <- MaybeT $ return $ askOpt "data" as + dat <- MaybeT $ return $ arg "data" as forkOrWait as $ cacheLoad copts (return $ eitherDecode (toUtf8 dat)) return ok] waitDb copts as return res -- link to server link' as _ copts = do - race [void (commandWaitInput copts) `finally` commandExit copts, commandLink copts] + commandLink copts + when (flag "hold" as) $ commandHold copts return ok -- exit exit' _ copts = do @@ -789,18 +857,63 @@ return ok -- Helper functions - cmd' :: [String] -> [String] -> String -> [OptDescr Opts] -> (Opts -> [String] -> a) -> Command (WithOpts a) + cmd' :: [String] -> [String] -> String -> [OptDescr (Opts String)] -> (Opts String -> [String] -> a) -> Command (WithOpts a) cmd' name posArgs descr as act = cmd name posArgs descr as act' where - act' os args = WithOpts (act os args) $ - return $ name ++ args ++ optsToArgs os + act' os args = WithOpts (act os args) $ CommandCall name args os cmd_' :: [String] -> [String] -> String -> ([String] -> a) -> Command (WithOpts a) cmd_' name posArgs descr act = cmd_ name posArgs descr act' where - act' args = WithOpts (act args) $ - return $ name ++ args ++ optsToArgs defaultConfig + act' args = WithOpts (act args) $ CommandCall name args defaultConfig - getGhcOpts = askOpts "ghc" + findSandbox :: MonadIO m => CommandOptions -> Maybe FilePath -> ErrorT String m Cabal + findSandbox copts = maybe + (return Cabal) + (findPath copts >=> mapErrorT liftIO . locateSandbox) + findPath :: MonadIO m => CommandOptions -> FilePath -> ErrorT String m FilePath + findPath copts f = liftIO $ canonicalizePath (normalise f') where + f' + | isRelative f = commandRoot copts </> f + | otherwise = f + + getCtx :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m (FilePath, Cabal) + getCtx copts as = liftM2 (,) + (forceJust "No file specified" $ traverse (findPath copts) $ arg "file" as) + (getCabal copts as) + + getCabal :: MonadIO m => CommandOptions -> Opts String -> ErrorT String m Cabal + getCabal copts as + | flag "cabal" as = findSandbox copts Nothing + | otherwise = findSandbox copts $ arg "sandbox" as + + getCabal_ :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m (Maybe Cabal) + getCabal_ copts as + | flag "cabal" as = Just <$> findSandbox copts Nothing + | otherwise = case arg "sandbox" as of + Just f -> Just <$> findSandbox copts (Just f) + Nothing -> return Nothing + + getSandboxes :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m [Cabal] + getSandboxes copts as = traverse (findSandbox copts) paths where + paths + | flag "cabal" as = Nothing : sboxes + | otherwise = sboxes + sboxes = map Just $ list "sandbox" as + + findProject :: MonadIO m => CommandOptions -> String -> ErrorT String m Project + findProject copts proj = do + db' <- getDb copts + proj' <- liftM addCabal $ findPath copts proj + let + result = + M.lookup proj' (databaseProjects db') <|> + find ((== proj) . projectName) (M.elems $ databaseProjects db') + maybe (throwError $ "Projects " ++ proj ++ " not found") return result + where + addCabal p + | takeExtension p == ".cabal" = p + | otherwise = p </> (takeBaseName p <.> "cabal") + toPair :: Module -> Maybe (FilePath, Module) toPair m = case moduleLocation m of FileModule f _ -> Just (f, m) @@ -811,13 +924,13 @@ CabalModule c _ _ -> Just c _ -> Nothing - waitDb copts as = when (hasOpt "wait" as) $ do + waitDb copts as = when (flag "wait" as) $ do commandLog copts "wait for db" DB.wait (dbVar copts) commandLog copts "db done" forkOrWait as act - | hasOpt "wait" as = liftIO act + | flag "wait" as = liftIO act | otherwise = liftIO $ void $ forkIO act cacheLoad copts act = do @@ -826,76 +939,33 @@ Left e -> commandLog copts e Right database -> DB.update (dbVar copts) (return database) - asCabal :: CommandOptions -> Maybe FilePath -> ErrorT String IO Cabal - asCabal copts = maybe - (return Cabal) - (canonicalizePath' copts >=> locateSandbox) - - askCabal :: CommandOptions -> Opts -> ErrorT String IO (Maybe Cabal) - askCabal copts as = traverse (asCabal copts) $ askOptDef "sandbox" as - - getCabal :: CommandOptions -> Opts -> ErrorT String IO Cabal - getCabal copts as = asCabal copts $ askOpt "sandbox" as - - getProject :: CommandOptions -> String -> ErrorT String IO Project - getProject copts proj = do - db' <- getDb copts - proj' <- liftM addCabal $ canonicalizePath' copts proj - let - result = - M.lookup proj' (databaseProjects db') <|> - find ((== proj) . projectName) (M.elems $ databaseProjects db') - maybe (throwError $ "Project " ++ proj ++ " not found") return result - where - addCabal p - | takeExtension p == ".cabal" = p - | otherwise = p </> (takeBaseName p <.> "cabal") - - localsDatabase :: Opts -> Database -> Database + localsDatabase :: Opts String -> Database -> Database localsDatabase as - | hasOpt "locals" as = databaseLocals + | flag "locals" as = databaseLocals | otherwise = id - newest :: Symbol a => Opts -> [a] -> [a] + newest :: Symbol a => Opts String -> [a] -> [a] newest as - | hasOpt "no-last" as = id + | flag "no-last" as = id | otherwise = newestPackage - askProject :: CommandOptions -> Opts -> ErrorT String IO (Maybe Project) - askProject copts = traverse (getProject copts) . askOpt "project" - - askFile :: CommandOptions -> Opts -> ErrorT String IO (Maybe FilePath) - askFile copts = traverse (canonicalizePath' copts) . askOpt "file" - - forceJust :: String -> ErrorT String IO (Maybe a) -> ErrorT String IO a + forceJust :: MonadIO m => String -> ErrorT String m (Maybe a) -> ErrorT String m a forceJust msg act = act >>= maybe (throwError msg) return - askCtx :: CommandOptions -> Opts -> ErrorT String IO (FilePath, Cabal) - askCtx copts as = liftM2 (,) - (forceJust "No file specified" $ askFile copts as) - (getCabal copts as) - getDb :: (MonadIO m) => CommandOptions -> m Database getDb = liftIO . DB.readAsync . commandDatabase dbVar :: CommandOptions -> DB.Async Database dbVar = commandDatabase - canonicalizePath' :: MonadIO m => CommandOptions -> FilePath -> m FilePath - canonicalizePath' copts f = liftIO $ canonicalizePath (normalise f') where - f' - | isRelative f = commandRoot copts </> f - | otherwise = f - - startProcess :: Opts -> ((Value -> IO ()) -> IO ()) -> IO CommandResult + startProcess :: Opts String -> ((Update.Status -> IO ()) -> IO ()) -> IO CommandResult startProcess as f - | hasOpt "wait" as = return $ ResultProcess (f . onMsg) + | flag "wait" as = return $ ResultProcess (f . onMsg) | otherwise = forkIO (f $ const $ return ()) >> return ok where onMsg showMsg - | hasOpt "status" as = showMsg + | flag "status" as = showMsg | otherwise = const $ return () - error_ :: ErrorT String IO CommandResult -> IO CommandResult error_ = liftM (either err id) . runErrorT @@ -905,8 +975,15 @@ errorT' :: ErrorT CommandResult IO ResultValue -> IO CommandResult errorT' = liftM (either id ResultOk) . runErrorT - updateProcess :: CommandOptions -> Opts -> ErrorT String (Update.UpdateDB IO) () -> IO CommandResult - updateProcess opts as act = startProcess as $ \onStatus -> Update.updateDB (Update.Settings (commandDatabase opts) (commandReadCache opts) onStatus (getGhcOpts as)) act + updateProcess :: CommandOptions -> Opts String -> ErrorT String (Update.UpdateDB IO) () -> IO CommandResult + updateProcess opts as act = startProcess as $ \onStatus -> + Update.updateDB + (Update.Settings + (commandDatabase opts) + (commandReadCache opts) + onStatus + (list "ghc" as)) + act fork :: MonadIO m => IO () -> m () fork = voidm . liftIO . forkIO @@ -914,24 +991,24 @@ voidm :: Monad m => m a -> m () voidm act = act >> return () - maybeOpt :: Monad m => String -> Opts -> (String -> MaybeT m a) -> MaybeT m ResultValue + maybeOpt :: Monad m => String -> Opts String -> (String -> MaybeT m a) -> MaybeT m ResultValue maybeOpt n as act = do - p <- MaybeT $ return $ askOpt n as + p <- MaybeT $ return $ arg n as act p return ResultNone - filterMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration] + filterMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] filterMatch as = findMatch as . prefMatch as - findMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration] - findMatch as = case askOpt "find" as of + findMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] + findMatch as = case arg "find" as of Nothing -> id Just str -> filter (match' str) where match' str m = str `isInfixOf` declarationName (moduleDeclaration m) - prefMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration] - prefMatch as = case fmap splitIdentifier (askOpt "prefix" as) of + prefMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] + prefMatch as = case fmap splitIdentifier (arg "prefix" as) of Nothing -> id Just (qname, pref) -> filter (match' qname pref) where @@ -979,3 +1056,18 @@ where ignoreError :: SomeException -> IO () ignoreError _ = return () + +logIO :: String -> (String -> IO ()) -> IO () -> IO () +logIO pre out act = handle onIO act where + onIO :: IOException -> IO () + onIO e = out $ pre ++ show e + +ignoreIO :: IO () -> IO () +ignoreIO = handle (const (return ()) :: IOException -> IO ()) + +logMsg :: ServerOpts -> String -> IO () +logMsg sopts s = ignoreIO $ do + putStrLn s + case getFirst (serverLog sopts) of + Nothing -> return () + Just f -> withFile f AppendMode (`hPutStrLn` s)
tools/System/Command.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, DefaultSignatures, FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, DefaultSignatures, FlexibleInstances, TupleSections #-} module System.Command ( Command(..), @@ -7,26 +7,38 @@ Help(..), addHelpCommand, addHelp, brief, help, run, runCmd, + OptionValue(..), Opts(..), - mapOpts, traverseOpts, - option, option_, req, noreq, flag, - opt, optMaybe, hasOpt, askOpt, askOptDef, askOpts, - optsToArgs, + (%--), hoist, + option, option_, + has, + req, noreq, no, + arg, opt, def, list, flag, + toArgs, splitArgs, unsplitArgs ) where import Control.Arrow import Control.Applicative -import Control.Monad (join) +import Control.Monad (join, (>=>)) +import Data.Aeson import Data.Char +import qualified Data.HashMap.Strict as HM (HashMap, toList) +import Data.String import Data.List (stripPrefix, unfoldr, isPrefixOf) -import Data.Maybe (fromMaybe, mapMaybe, listToMaybe, maybeToList) +import Data.Maybe (fromMaybe, mapMaybe, listToMaybe, maybeToList, isJust) import Data.Map (Map) import qualified Data.Map as M import Data.Monoid -import Data.Traversable (traverse) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Foldable (Foldable(foldMap)) +import Data.Traversable (Traversable(traverse)) +import qualified Data.Vector as V import System.Console.GetOpt +import Data.Group + -- | Command data Command a = Command { commandName :: [String], @@ -116,67 +128,114 @@ toMaybe :: Either b c -> Maybe c toMaybe = either (const Nothing) Just +-- | Convertible to option value +class OptionValue a where + toOption :: a -> String + default toOption :: Show a => a -> String + toOption = show + +instance OptionValue String where + toOption = id + +instance OptionValue Int +instance OptionValue Integer +instance OptionValue Float +instance OptionValue Double +instance OptionValue Bool + -- | Options holder -newtype Opts = Opts { getOpts :: Map String [String] } +newtype Opts a = Opts { getOpts :: Map String [a] } -instance Monoid Opts where +instance Eq a => Eq (Opts a) where + Opts l == Opts r = l == r + +instance Functor Opts where + fmap f (Opts opts) = Opts $ fmap (fmap f) opts + +instance Foldable Opts where + foldMap f (Opts opts) = foldMap (foldMap f) opts + +instance Traversable Opts where + traverse f (Opts opts) = Opts <$> traverse (traverse f) opts + +instance Monoid (Opts a) where mempty = Opts mempty - (Opts l) `mappend` (Opts r) = Opts $ M.unionWith (++) l r + (Opts l) `mappend` (Opts r) = Opts $ M.unionWith mappend l r -instance DefaultConfig Opts +instance Eq a => Group (Opts a) where + add = mappend + (Opts l) `sub` (Opts r) = Opts $ l `sub` r + zero = mempty --- | Map 'Opts' -mapOpts :: (String -> String) -> Opts -> Opts -mapOpts f = Opts . M.mapKeys f . getOpts +instance DefaultConfig (Opts a) --- | Traverse 'Opts' -traverseOpts :: Applicative f => (String -> String -> f String) -> Opts -> f Opts -traverseOpts f = fmap Opts . M.traverseWithKey (traverse . f) . getOpts +instance ToJSON a => ToJSON (Opts a) where + toJSON (Opts opts) = object $ map toPair $ M.toList opts where + toPair (n, []) = fromString n .= Null + toPair (n, [v]) = fromString n .= v + toPair (n, vs) = fromString n .= vs -option :: [Char] -> String -> [String] -> (String -> ArgDescr Opts) -> String -> OptDescr Opts -option fs name names onOption d = Option fs (name:names) (onOption name) d +instance FromJSON a => FromJSON (Opts a) where + parseJSON = withObject "options" $ fmap (Opts . M.fromList) . mapM fromPair . HM.toList where + fromPair (n, v) = (T.unpack n,) <$> case v of + Null -> return [] + _ -> (return <$> parseJSON v) <|> parseJSON v -option_ :: [Char] -> String -> (String -> ArgDescr Opts) -> String -> OptDescr Opts +-- | Make 'Opts' with one argument +(%--) :: OptionValue a => String -> a -> Opts String +n %-- v = Opts $ M.singleton n [toOption v] + +-- | Make 'Opts' with flag enabled +hoist :: String -> Opts a +hoist n = Opts $ M.singleton n [] + +option :: [Char] -> String -> [String] -> (String -> ArgDescr (Opts a)) -> String -> OptDescr (Opts a) +option fs name names onOpt d = Option fs (name:names) (onOpt name) d + +option_ :: [Char] -> String -> (String -> ArgDescr (Opts a)) -> String -> OptDescr (Opts a) option_ fs name = option fs name [] --- | Required option -req :: String -> String -> ArgDescr Opts -req nm n = ReqArg (opt n) nm +has :: String -> Opts a -> Bool +has n = M.member n . getOpts --- | Optional option -noreq :: String -> String -> ArgDescr Opts -noreq nm n = OptArg (optMaybe n) nm +-- | Required option +req :: String -> String -> ArgDescr (Opts String) +req nm n = ReqArg (n %--) nm --- | Flag option -flag :: String -> ArgDescr Opts -flag n = NoArg flag' where - flag' :: Opts - flag' = Opts $ M.singleton n [] +-- | Not required option +noreq :: String -> String -> ArgDescr (Opts String) +noreq nm n = OptArg (maybe (hoist n) (n %--)) nm -opt :: String -> String -> Opts -opt n v = Opts $ M.singleton n [v] +-- | No option +no :: String -> ArgDescr (Opts String) +no = NoArg . hoist -optMaybe :: String -> Maybe String -> Opts -optMaybe n = Opts . M.singleton n . maybeToList +-- | Get argument +arg :: String -> Opts a -> Maybe a +arg n = M.lookup n . getOpts >=> listToMaybe -hasOpt :: String -> Opts -> Bool -hasOpt n = M.member n . getOpts +-- | Get optional argument +opt :: String -> Opts a -> Maybe (Maybe a) +opt n = fmap listToMaybe . M.lookup n . getOpts -askOpt :: String -> Opts -> Maybe String -askOpt n = listToMaybe . askOpts n +-- | Get argument with default +def :: a -> String -> Opts a -> Maybe a +def d n = fmap (fromMaybe d . listToMaybe) . M.lookup n . getOpts -askOptDef :: String -> Opts -> Maybe (Maybe String) -askOptDef n = fmap listToMaybe . M.lookup n . getOpts +-- | Get list arguments +list :: String -> Opts a -> [a] +list n = fromMaybe [] . M.lookup n . getOpts -askOpts :: String -> Opts -> [String] -askOpts n = fromMaybe [] . M.lookup n . getOpts +-- | Get flag +flag :: String -> Opts a -> Bool +flag n = isJust . M.lookup n . getOpts -- | Print 'Opts' as args -optsToArgs :: Opts -> [String] -optsToArgs = concatMap optToArgs . M.toList . getOpts where - optToArgs :: (String, [String]) -> [String] - optToArgs (n, []) = ["--" ++ n] - optToArgs (n, vs) = ["--"++ n ++ "=" ++ v | v <- vs] +toArgs :: Opts String -> [String] +toArgs = concatMap toArgs' . M.toList . getOpts where + toArgs' :: (String, [String]) -> [String] + toArgs' (n, []) = ["--" ++ n] + toArgs' (n, vs) = [concat ["--", n, "=", v] | v <- vs] -- | Split string to words splitArgs :: String -> [String]
tools/Tool.hs view
@@ -48,12 +48,12 @@ usage toolName = mapM_ (putStrLn . ('\t':) . ((toolName ++ " ") ++) . brief) -- | Command with JSONable result -jsonCmd :: ToJSON a => [String] -> [String] -> String -> [OptDescr Opts] -> (Opts -> [String] -> ToolM a) -> Command (IO ()) +jsonCmd :: ToJSON a => [String] -> [String] -> String -> [OptDescr (Opts String)] -> (Opts String -> [String] -> ToolM a) -> Command (IO ()) jsonCmd name posArgs descr as act = cmd name posArgs descr (prettyOpt : as) $ \opts as -> do r <- runErrorT $ act opts as L.putStrLn $ either (toStr opts . errorStr) (toStr opts) r where - toStr :: ToJSON a => Opts -> a -> L.ByteString + toStr :: ToJSON a => Opts String -> a -> L.ByteString toStr opts | isPretty opts = encodePretty | otherwise = encode @@ -69,8 +69,8 @@ toolError :: String -> ToolM a toolError = throwError -prettyOpt :: OptDescr Opts -prettyOpt = option_ [] "pretty" flag "pretty JSON output" +prettyOpt :: OptDescr (Opts String) +prettyOpt = option_ [] "pretty" no "pretty JSON output" -isPretty :: Opts -> Bool -isPretty = hasOpt "pretty" +isPretty :: Opts String -> Bool +isPretty = flag "pretty"
tools/Types.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings, CPP #-} module Types ( -- * Server options @@ -8,7 +8,7 @@ -- * Messages and results ResultValue(..), Response(..), CommandResult(..), ok, err, errArgs, details, - WithOpts(..), + CommandCall(..), callArgs, addCallOpts, removeCallOpts, WithOpts(..), CommandOptions(..), CommandAction ) where @@ -16,6 +16,7 @@ import Control.Monad.Error import Data.Aeson import Data.Map (Map) +import Data.Maybe (fromMaybe) import Data.Monoid import qualified Data.HashMap.Strict as HM import qualified Data.Map as M @@ -27,16 +28,23 @@ import HsDev.Symbols import HsDev.Tools.GhcMod (TypedRegion) import qualified HsDev.Database.Async as DB +import HsDev.Util ((.::), (.::?)) import System.Command +import Update +#if mingw32_HOST_OS +import System.Win32.FileMapping.NamePool (Pool) +#endif + -- | Server options data ServerOpts = ServerOpts { serverPort :: First Int, serverTimeout :: First Int, serverLog :: First String, serverCache :: First FilePath, - serverLoadCache :: Any } + serverLoadCache :: Any, + serverAsClient :: Any } instance DefaultConfig ServerOpts where defaultConfig = ServerOpts @@ -45,15 +53,17 @@ (First Nothing) (First Nothing) mempty + mempty instance Monoid ServerOpts where - mempty = ServerOpts mempty mempty mempty mempty mempty + mempty = ServerOpts mempty mempty mempty mempty mempty mempty l `mappend` r = ServerOpts (serverPort l `mappend` serverPort r) (serverTimeout l `mappend` serverTimeout r) (serverLog l `mappend` serverLog r) (serverCache l `mappend` serverCache r) (serverLoadCache l `mappend` serverLoadCache r) + (serverAsClient l `mappend` serverAsClient r) -- | Server options command opts serverOpts :: [OptDescr ServerOpts] @@ -62,7 +72,8 @@ Option [] ["timeout"] (ReqArg (\t -> mempty { serverTimeout = First (readMaybe t) }) "msec") "query timeout", Option ['l'] ["log"] (ReqArg (\l -> mempty { serverLog = First (Just l) }) "file") "log file", Option [] ["cache"] (ReqArg (\p -> mempty { serverCache = First (Just p) }) "path") "cache directory", - Option [] ["load"] (NoArg (mempty { serverLoadCache = Any True })) "force load all data from cache on startup"] + Option [] ["load"] (NoArg (mempty { serverLoadCache = Any True })) "force load all data from cache on startup", + Option ['c'] ["as-client"] (NoArg (mempty { serverAsClient = Any True })) "make server be client and connect to port specified"] -- | Convert 'ServerOpts' to args serverOptsToArgs :: ServerOpts -> [String] @@ -71,7 +82,8 @@ arg' "timeout" show $ serverTimeout sopts, arg' "log" id $ serverLog sopts, arg' "cache" id $ serverCache sopts, - if getAny (serverLoadCache sopts) then ["--load"] else []] + if getAny (serverLoadCache sopts) then ["--load"] else [], + if getAny (serverAsClient sopts) then ["--as-client"] else []] where arg' :: String -> (a -> String) -> First a -> [String] arg' name str = maybe [] (\v -> ["--" ++ name, str v]) . getFirst @@ -151,25 +163,25 @@ ResultString <$> parseJSON v] data Response = - Response Value | - ResponseStatus Value | - ResponseMapFile String + ResponseStatus Status | + ResponseMapFile String | + Response Value instance ToJSON Response where - toJSON (Response v) = object ["result" .= v] - toJSON (ResponseStatus v) = object ["status" .= v] + toJSON (ResponseStatus s) = toJSON s toJSON (ResponseMapFile s) = object ["file" .= s] + toJSON (Response v) = v instance FromJSON Response where - parseJSON = withObject "response" (\v -> - (Response <$> (v .: "result")) <|> - (ResponseStatus <$> (v .: "status")) <|> - (ResponseMapFile <$> (v .: "file"))) + parseJSON v = foldr1 (<|>) [ + ResponseStatus <$> parseJSON v, + withObject "response" (\f -> (ResponseMapFile <$> (f .:: "file"))) v, + pure $ Response v] data CommandResult = ResultOk ResultValue | ResultError String (Map String ResultValue) | - ResultProcess ((Value -> IO ()) -> IO ()) + ResultProcess ((Status -> IO ()) -> IO ()) instance Error CommandResult where noMsg = ResultError noMsg mempty @@ -189,9 +201,43 @@ details as (ResultError s cs) = ResultError s (M.union (M.fromList as) cs) details _ r = r +data CommandCall = CommandCall { + commandCallName :: [String], + commandCallPosArgs :: [String], + commandCallOpts :: Opts String } + +instance ToJSON CommandCall where + toJSON (CommandCall n ps opts) = object [ + "command" .= n, + "args" .= ps, + "opts" .= opts] + +instance FromJSON CommandCall where + parseJSON = withObject "command call" $ \v -> CommandCall <$> + (v .:: "command") <*> + (fromMaybe [] <$> (v .::? "args")) <*> + (fromMaybe mempty <$> (v .::? "opts")) + +callArgs :: CommandCall -> [String] +callArgs (CommandCall n ps opts) = n ++ ps ++ toArgs opts + +-- | Add options +-- +-- >cmdCall `addCallOpts` ["foo" %-- 15] +addCallOpts :: CommandCall -> [Opts String] -> CommandCall +addCallOpts cmdCall os = cmdCall { + commandCallOpts = mconcat (commandCallOpts cmdCall : os) } + +-- | Remove specified call options +-- +-- >cmdCall `removeCallOpts` ["foo"] +removeCallOpts :: CommandCall -> [String] -> CommandCall +removeCallOpts cmdCall os = cmdCall { + commandCallOpts = Opts $ foldr (.) id (map M.delete os) $ getOpts (commandCallOpts cmdCall) } + data WithOpts a = WithOpts { withOptsAct :: a, - withOptsArgs :: IO [String] } + withOptsCommand :: CommandCall } instance Functor WithOpts where fmap f (WithOpts x as) = WithOpts (f x) as @@ -202,8 +248,12 @@ commandReadCache :: (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database), commandRoot :: FilePath, commandLog :: String -> IO (), - commandWaitInput :: IO String, + commandLogWait :: IO (), +#if mingw32_HOST_OS + commandMmapPool :: Maybe Pool, +#endif commandLink :: IO (), + commandHold :: IO (), commandExit :: IO () } type CommandAction = WithOpts (Int -> CommandOptions -> IO CommandResult)
tools/Update.hs view
@@ -1,13 +1,14 @@ {-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving, OverloadedStrings, MultiParamTypeClasses #-} module Update ( + Status(..), isStatus, Settings(..), UpdateDB, updateDB, - setStatus, waiter, updater, loadCache, runTask, runTasks, - status, readDB, + setStatus, postStatus, waiter, updater, loadCache, runTask, runTasks, + readDB, scanModule, scanModules, scanFile, scanCabal, scanProject, scanDirectory, @@ -15,15 +16,16 @@ liftErrorT ) where -import Control.Applicative (Applicative(..)) +import Control.Applicative import Control.Monad.CatchIO import Control.Monad.Error import Control.Monad.Reader import Data.Aeson +import Data.Aeson.Types import qualified Data.HashMap.Strict as HM import Data.Map (Map) import qualified Data.Map as M -import Data.Maybe (mapMaybe) +import Data.Maybe (mapMaybe, isJust) import Data.Monoid import Data.Traversable (traverse) import System.Directory (canonicalizePath) @@ -36,11 +38,25 @@ import HsDev.Tools.HDocs import qualified HsDev.Scan as S import HsDev.Scan.Browse +import HsDev.Util ((.::)) +data Status = Status { + status :: Value, + statusDetails :: [Pair] } + +instance ToJSON Status where + toJSON (Status s ds) = object $ ("status" .= s) : ds + +instance FromJSON Status where + parseJSON = withObject "status" $ \v -> Status <$> (v .:: "status") <*> pure (HM.toList $ HM.delete "status" v) + +isStatus :: Value -> Bool +isStatus = isJust . parseMaybe (parseJSON :: Value -> Parser Status) + data Settings = Settings { database :: Async Database, databaseCacheReader :: (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database), - onStatus :: Value -> IO (), + onStatus :: Status -> IO (), ghcOptions :: [String] } newtype UpdateDB m a = UpdateDB { runUpdateDB :: ReaderT Settings m a } @@ -50,12 +66,18 @@ updateDB :: Monad m => Settings -> ErrorT String (UpdateDB m) () -> m () updateDB sets act = runUpdateDB (runErrorT act >> return ()) `runReaderT` sets --- | Set partial status -setStatus :: MonadReader Settings m => Value -> m a -> m a -setStatus v act = local alter act where - alter s = s { - onStatus = \st -> onStatus s (v `union` st) } +-- | Set status details +setStatus :: MonadReader Settings m => [Pair] -> m a -> m a +setStatus ps act = local alter act where + alter st = st { + onStatus = \(Status s ds) -> onStatus st (Status s (ds ++ ps)) } +-- | Post status +postStatus :: (MonadIO m, MonadReader Settings m) => Value -> m () +postStatus s = do + on' <- asks onStatus + liftIO $ on' $ Status s [] + -- | Wait DB to complete actions waiter :: (MonadIO m, MonadReader Settings m) => m () -> m () waiter act = do @@ -80,15 +102,15 @@ -- | Run one task runTask :: MonadIO m => Value -> ErrorT String (UpdateDB m) a -> ErrorT String (UpdateDB m) a -runTask v act = object ["task" .= v] `setStatus` wrapAct act where +runTask v act = ["task" .= v] `setStatus` wrapAct act where wrapAct :: MonadIO m => ErrorT String (UpdateDB m) a -> ErrorT String (UpdateDB m) a wrapAct a = do - status $ object ["status" .= toJSON ("working" :: String)] + postStatus $ toJSON ("working" :: String) x <- a - status $ object ["status" .= taskOk] + postStatus taskOk return x `catchError` - (\e -> status (object ["status" .= taskErr e]) >> throwError e) + (\e -> postStatus (taskErr e) >> throwError e) taskOk = toJSON ("ok" :: String) taskErr e = object ["error" .= e] @@ -97,15 +119,9 @@ runTasks ts = zipWithM_ taskNum [1..] (map noErr ts) where total = length ts taskNum n t = progress `setStatus` t where - progress = object ["progress" .= object [ + progress = ["progress" .= object [ "current" .= (n :: Integer), "total" .= total]] noErr v = v `mplus` return () - --- | Post status -status :: (MonadIO m, MonadReader Settings m) => Value -> m () -status msg = do - on' <- asks onStatus - liftIO $ on' msg -- | Get database value readDB :: (MonadIO m, MonadReader Settings m) => m Database
tools/hsclearimports.hs view
@@ -15,17 +15,17 @@ import System.Command -opts :: [OptDescr Opts] +opts :: [OptDescr (Opts String)] opts = [ option_ ['g'] "ghc" (req "ghc options") "options for GHC", - option_ [] "hide-import-list" flag "hide import list", + option_ [] "hide-import-list" no "hide import list", option_ [] "max-import-list" (req "max import list length") "hide long import lists"] cmds :: [Command (IO ())] cmds = addHelp "hsclearimports" id [ cmd [] ["file"] "clear imports in haskell source" opts clear] where - clear :: Opts -> [String] -> IO () + clear :: Opts String -> [String] -> IO () clear as [f] = do file <- canonicalizePath f mroot <- locateSourceDir file @@ -33,14 +33,14 @@ flip finally (setCurrentDirectory cur) $ do maybe (return ()) setCurrentDirectory mroot void $ runErrorT $ catchError - (clearImports (askOpts "ghc" as) file >>= mapM_ (liftIO . putStrLn . format as)) + (clearImports (list "ghc" as) file >>= mapM_ (liftIO . putStrLn . format as)) (\e -> liftIO (putStrLn $ "Error: " ++ e)) clear _ _ = putStrLn "Invalid arguments" - format :: Opts -> (String, String) -> String + format :: Opts String -> (String, String) -> String format as (imp, lst) - | hasOpt "hide-import-list" as = imp - | maybe False (length lst >) (askOpt "max-import-list" as >>= readMaybe) = imp + | flag "hide-import-list" as = imp + | maybe False (length lst >) (arg "max-import-list" as >>= readMaybe) = imp | otherwise = imp ++ " (" ++ lst ++ ")" main :: IO ()
tools/hsinspect.hs view
@@ -17,7 +17,7 @@ jsonCmd_ ["cabal"] ["project file"] "inspect .cabal file" inspectCabal'] where ghcOpts = option_ ['g'] "ghc" (req "ghc options") "options to pass to GHC" - ghcs = askOpts "ghc" + ghcs = list "ghc" inspectModule' opts [mname] = scanModule (ghcs opts) (CabalModule Cabal Nothing mname) inspectModule' _ _ = toolError "Specify module name"