hsdev 0.1.4.3 → 0.1.5.0
raw patch · 42 files changed
+1870/−3025 lines, 42 filesdep +asyncdep +cpphsdep +data-defaultdep ~ghc-mod
Dependencies added: async, cpphs, data-default, hformat, optparse-applicative, text-region
Dependency ranges changed: ghc-mod
Files
- hsdev.cabal +32/−9
- src/Control/Concurrent/Task.hs +0/−138
- src/Control/Concurrent/Worker.hs +20/−15
- src/Data/Mark.hs +0/−340
- src/HsDev/Client/Commands.hs +374/−966
- src/HsDev/Commands.hs +7/−5
- src/HsDev/Database.hs +36/−32
- src/HsDev/Database/Update.hs +52/−55
- src/HsDev/Database/Update/Types.hs +19/−18
- src/HsDev/Display.hs +6/−6
- src/HsDev/Inspect.hs +52/−22
- src/HsDev/Project.hs +3/−4
- src/HsDev/Scan.hs +13/−10
- src/HsDev/Server/Base.hs +157/−0
- src/HsDev/Server/Commands.hs +183/−407
- src/HsDev/Server/Message.hs +4/−40
- src/HsDev/Server/Types.hs +567/−64
- src/HsDev/Symbols.hs +11/−17
- src/HsDev/Symbols/Location.hs +6/−1
- src/HsDev/Symbols/Util.hs +1/−2
- src/HsDev/Tools/AutoFix.hs +27/−29
- src/HsDev/Tools/Cabal.hs +1/−1
- src/HsDev/Tools/Ghc/Check.hs +3/−8
- src/HsDev/Tools/Ghc/Types.hs +1/−0
- src/HsDev/Tools/Ghc/Worker.hs +1/−1
- src/HsDev/Tools/GhcMod.hs +42/−20
- src/HsDev/Tools/HLint.hs +1/−1
- src/HsDev/Tools/Hayoo.hs +1/−1
- src/HsDev/Tools/Types.hs +6/−3
- src/HsDev/Util.hs +72/−20
- src/System/Console/Args.hs +0/−295
- src/System/Console/Cmd.hs +0/−168
- src/System/Directory/Paths.hs +20/−0
- src/System/Win32/FileMapping/NamePool.hs +1/−1
- src/Text/Format.hs +0/−98
- tools/Tool.hs +17/−62
- tools/hsautofix.hs +45/−49
- tools/hscabal.hs +1/−2
- tools/hsclearimports.hs +26/−33
- tools/hsdev.hs +19/−50
- tools/hshayoo.hs +15/−11
- tools/hsinspect.hs +28/−21
hsdev.cabal view
@@ -2,7 +2,7 @@ -- see http://haskell.org/cabal/users-guide/ name: hsdev -version: 0.1.4.3 +version: 0.1.5.0 synopsis: Haskell development library and tool with support of autocompletion, symbol info, go to declaration, find references etc. description: Haskell development library and tool with support of autocompletion, symbol info, go to declaration, find references, hayoo search etc. @@ -22,14 +22,12 @@ exposed-modules: Control.Apply.Util Control.Concurrent.FiniteChan - Control.Concurrent.Task Control.Concurrent.Worker Control.Concurrent.Util Data.Async Data.Group Data.Help Data.Lisp - Data.Mark Data.Maybe.JustIf HsDev HsDev.Cabal @@ -46,6 +44,7 @@ HsDev.Project HsDev.Scan HsDev.Scan.Browse + HsDev.Server.Base HsDev.Server.Commands HsDev.Server.Message HsDev.Server.Types @@ -74,10 +73,8 @@ HsDev.Version HsDev.Watcher HsDev.Watcher.Types - System.Console.Args - System.Console.Cmd + System.Directory.Paths System.Directory.Watcher - Text.Format if os(windows) build-depends: @@ -95,23 +92,27 @@ aeson >= 0.7.0, aeson-pretty >= 0.7.0, array >= 0.5.0, + async >= 2.0, attoparsec >= 0.11.0, bin-package-db, bytestring >= 0.10.0, Cabal >= 1.22.0, containers >= 0.5.0, + cpphs >= 1.19.0, + data-default >= 0.5.0, deepseq >= 1.4.0, directory >= 1.2.0, exceptions >= 0.6.0, filepath >= 1.4.0, fsnotify >= 0.2.1, ghc >= 7.10.0 && < 7.11.0, - ghc-mod >= 5.3.0.0, + ghc-mod >= 5.4.0.0 && < 5.5.0.0, ghc-paths >= 0.1.0, ghc-syb-utils >= 0.2.3, haddock-api >= 2.16.0 && < 2.17.0, haskell-src-exts >= 1.16.0, hdocs >= 0.4.4, + hformat >= 0.1, hlint >= 1.9.0 && < 2.0.0, HTTP >= 4000.2.0, lens >= 4.8, @@ -121,6 +122,7 @@ mtl >= 2.2.0, MonadCatchIO-transformers >= 0.3.1, network >= 2.6.0, + optparse-applicative >= 0.11, process >= 1.2.0, regex-pcre-builtin >= 0.94, scientific >= 0.3, @@ -128,6 +130,7 @@ syb >= 0.5.1, template-haskell, text >= 1.2.0, + text-region >= 0.1, time >= 1.5.0, transformers >= 0.4.0, transformers-base >= 0.4.0, @@ -154,6 +157,7 @@ monad-loops >= 0.4.0, mtl >= 2.2.0, network >= 2.6.0, + optparse-applicative >= 0.11, process >= 1.2.0, text >= 1.2.0, transformers >= 0.4.0, @@ -175,6 +179,7 @@ directory >= 1.2.0, filepath >= 1.4.0, mtl >= 2.2.0, + optparse-applicative >= 0.11, text >= 1.2.0, transformers >= 0.4.0, unordered-containers >= 0.2.0, @@ -190,13 +195,16 @@ hsdev, aeson >= 0.7.0, aeson-pretty >= 0.7.0, + bytestring >= 0.10.0, containers >= 0.5.0, directory >= 1.2.0, ghc >= 7.10.0 && < 7.11.0, haskell-src-exts >= 1.16.0, lens >= 4.8, mtl >= 2.2.0, + optparse-applicative >= 0.11, text >= 1.2.0, + transformers >= 0.4.0, unordered-containers >= 0.2.0 executable hscabal @@ -213,7 +221,9 @@ bytestring >= 0.10.0, containers >= 0.5.0, mtl >= 2.2.0, + optparse-applicative >= 0.11, text >= 1.2.0, + transformers >= 0.4.0, unordered-containers >= 0.2.0 executable hshayoo @@ -230,7 +240,9 @@ bytestring >= 0.10.0, containers >= 0.5.0, mtl >= 2.2.0, + optparse-applicative >= 0.11, text >= 1.2.0, + transformers >= 0.4.0, unordered-containers >= 0.2.0 executable hsautofix @@ -247,7 +259,9 @@ bytestring >= 0.10.0, directory >= 1.2.0, lens >= 4.8, - mtl >= 2.2.0 + mtl >= 2.2.0, + optparse-applicative >= 0.11, + transformers >= 0.4.0 test-suite test main-is: Test.hs @@ -255,7 +269,16 @@ ghc-options: -Wall -fno-warn-tabs type: exitcode-stdio-1.0 build-depends: - base >= 4.7 && < 5 + base >= 4.7 && < 5, + hsdev, + async >= 2.0, + data-default >= 0.5.0, + containers >= 0.5.0, + deepseq >= 1.4.0, + hformat >= 0.1, + lens >= 4.8, + mtl >= 2.2.0, + text >= 1.2.0 source-repository head type: git
− src/Control/Concurrent/Task.hs
@@ -1,138 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-} - -module Control.Concurrent.Task ( - Task(..), TaskException(..), TaskResult(..), - taskStarted, taskRunning, taskStopped, taskDone, taskFailed, taskCancelled, - taskWaitStart, taskWait, taskJoinWith, taskJoin, taskJoin_, taskKill, taskCancel, taskStop, - runTask, runTask_, runTaskTry, runTaskError, forkTask, tryT, - - -- * Reexports - SomeException, - MonadIO, MonadCatch, MonadError - ) where - -import Control.Concurrent -import Control.Monad -import Control.Monad.Except -import Control.Monad.Catch -import Data.Either -import Data.Maybe -import Data.Typeable - --- | Task result -data Task a = Task { - taskStart :: MVar (Maybe (SomeException -> IO ())), - taskResult :: TaskResult a } - -data TaskException = TaskCancelled | TaskKilled deriving (Eq, Ord, Enum, Bounded, Read, Show, Typeable) - -instance Exception TaskException - -data TaskResult a = TaskResult { - taskResultEmpty :: IO Bool, - taskResultTryRead :: IO (Maybe (Either SomeException a)), - taskResultTake :: IO (Either SomeException a), - taskResultFail :: SomeException -> IO () } - -instance Functor TaskResult where - fmap f r = TaskResult { - taskResultEmpty = taskResultEmpty r, - taskResultTryRead = fmap (fmap f) <$> taskResultTryRead r, - taskResultTake = fmap f <$> taskResultTake r, - taskResultFail = taskResultFail r } - -instance Functor Task where - fmap f t = Task { - taskStart = taskStart t, - taskResult = fmap f (taskResult t) } - -taskStarted :: Task a -> IO Bool -taskStarted = fmap (maybe False isJust) . tryReadMVar . taskStart - -taskRunning :: Task a -> IO Bool -taskRunning t = (&&) <$> taskStarted t <*> (not <$> taskStopped t) - -taskStopped :: Task a -> IO Bool -taskStopped = fmap not . taskResultEmpty . taskResult - -taskDone :: Task a -> IO Bool -taskDone = fmap (maybe False isRight) . taskResultTryRead . taskResult - -taskFailed :: Task a -> IO Bool -taskFailed = fmap (maybe False isLeft) . taskResultTryRead . taskResult - -taskCancelled :: Task a -> IO Bool -taskCancelled = fmap (maybe False isNothing) . tryReadMVar . taskStart - --- | Wait until task starts or be cancelled, returns True if started -taskWaitStart :: Task a -> IO Bool -taskWaitStart = (`withMVar` (return . isJust)) . taskStart - --- | Wait for task -taskWait :: Task a -> IO (Either SomeException a) -taskWait = taskResultTake . taskResult - --- | Join task with -taskJoinWith :: MonadIO m => (SomeException -> m a) -> Task a -> m a -taskJoinWith err = liftIO . taskWait >=> either err return - --- | Join task, rethrowing its exceptions -taskJoin :: Task a -> IO a -taskJoin = taskJoinWith throwM - --- | Join task, returning exceptions as @EitherT@ -taskJoin_ :: Task a -> ExceptT SomeException IO a -taskJoin_ = taskJoinWith throwError - --- | Kill task -taskKill :: Task a -> IO () -taskKill = - tryTakeMVar . taskStart >=> - void . traverse ($ toException TaskKilled) . join - --- | Cancel task if it is not started yet -taskCancel :: Task a -> IO Bool -taskCancel t = do - aborted <- tryPutMVar (taskStart t) Nothing - when aborted $ void $ taskResultFail (taskResult t) (toException TaskCancelled) - return aborted - --- | Cancel or kill task, returns whether it was cancelled -taskStop :: Task a -> IO Bool -taskStop t = do - cancelled <- taskCancel t - unless cancelled $ taskKill t - return cancelled - -runTask :: (MonadCatch m, MonadIO m, MonadIO n) => (m () -> n ()) -> m a -> n (Task a) -runTask f = runTask_ (const f) - -runTask_ :: (MonadCatch m, MonadIO m, MonadIO n) => (Task a -> m () -> n ()) -> m a -> n (Task a) -runTask_ f = runTaskTry f . liftM Right - -runTaskTry :: (MonadCatch m, MonadIO m, MonadIO n) => (Task a -> m () -> n ()) -> m (Either SomeException a) -> n (Task a) -runTaskTry f act = do - throwVar <- liftIO newEmptyMVar - resultVar <- liftIO newEmptyMVar - f (Task throwVar $ toResult resultVar) $ handle (liftIO . putMVar resultVar . Left) $ do - th <- liftIO myThreadId - ok <- liftIO $ tryPutMVar throwVar (Just $ throwTo th) - when ok $ act >>= liftIO . putMVar resultVar - return $ Task throwVar $ toResult resultVar - where - toResult :: MVar (Either SomeException a) -> TaskResult a - toResult var = TaskResult { - taskResultEmpty = isEmptyMVar var, - taskResultTryRead = tryReadMVar var, - taskResultTake = takeMVar var, - taskResultFail = void . tryPutMVar var . Left } - -runTaskError :: (Show e, MonadError e m, MonadCatch m, MonadIO m, MonadIO n) => (Task a -> m () -> n ()) -> m a -> n (Task a) -runTaskError f = runTaskTry f . tryT - --- | Run task in separate thread -forkTask :: IO a -> IO (Task a) -forkTask = runTask (void . forkIO) - -tryT :: (Show e, MonadError e m) => m a -> m (Either SomeException a) -tryT act = catchError (liftM Right act) (return . Left . toException . userError . show)
src/Control/Concurrent/Worker.hs view
@@ -7,21 +7,22 @@ stopWorker, syncTask, inWorkerWith, inWorker, inWorker_, - module Control.Concurrent.Task + module Control.Concurrent.Async ) where import Control.Concurrent.MVar import Control.Monad.IO.Class import Control.Monad.Catch import Control.Monad.Except +import Data.Maybe (isJust) import Control.Concurrent.FiniteChan -import Control.Concurrent.Task +import Control.Concurrent.Async data Worker m = Worker { - workerChan :: Chan (Task (), m ()), + workerChan :: Chan (Async (), m ()), workerWrap :: forall a. m a -> m a, - workerTask :: MVar (Task ()), + workerTask :: MVar (Async ()), workerRestart :: IO Bool } -- | Create new worker @@ -30,27 +31,31 @@ ch <- newChan taskVar <- newEmptyMVar let - start = forkTask $ do + start = async $ do run $ initialize processWork processSkip processWork = whileJust (liftM (fmap snd) $ liftIO $ getChan ch) id - processSkip = whileJust (liftM (fmap fst) $ getChan ch) taskStop + processSkip = whileJust (liftM (fmap fst) $ getChan ch) cancel whileJust :: Monad m => m (Maybe a) -> (a -> m b) -> m () whileJust v act = v >>= maybe (return ()) (\x -> act x >> whileJust v act) start >>= putMVar taskVar let restart = do task <- readMVar taskVar - stopped <- taskStopped task + stopped <- liftM isJust $ poll task when stopped (start >>= void . swapMVar taskVar) return stopped return $ Worker ch wrap taskVar restart -sendTask :: (MonadCatch m, MonadIO m) => Worker m -> m a -> IO (Task a) -sendTask w = runTask_ putTask' . workerWrap w where - putTask' t act = putChan (workerChan w) (fmap (const ()) t, act) +sendTask :: (MonadCatch m, MonadIO m) => Worker m -> m a -> IO (Async a) +sendTask w act = mfix $ \async' -> do + var <- newEmptyMVar + let + act' = (workerWrap w act >>= liftIO . putMVar var . Right) `catch` (liftIO . putMVar var . Left) + f = putChan (workerChan w) (void async', void act') >> takeMVar var >>= either (throwM :: SomeException -> IO a) return + async f -pushTask :: (MonadCatch m, MonadIO m) => Worker m -> m a -> IO (Task a) +pushTask :: (MonadCatch m, MonadIO m) => Worker m -> m a -> IO (Async a) pushTask w act = workerRestart w >> sendTask w act stopWorker :: Worker m -> IO () @@ -58,16 +63,16 @@ -- | Send empty task and wait until worker run it syncTask :: (MonadCatch m, MonadIO m) => Worker m -> IO () -syncTask w = pushTask w (return ()) >>= void . taskWait +syncTask w = pushTask w (return ()) >>= void . wait -- | Run action in worker and wait for result inWorkerWith :: (MonadIO m, MonadCatch m, MonadIO n) => (SomeException -> n a) -> Worker m -> m a -> n a -inWorkerWith err w act = liftIO (pushTask w act) >>= taskJoinWith err +inWorkerWith err w act = liftIO (pushTask w act) >>= (liftIO . waitCatch >=> either err return) -- | Run action in worker and wait for result inWorker :: (MonadIO m, MonadCatch m) => Worker m -> m a -> IO a -inWorker w act = pushTask w act >>= taskJoin +inWorker w act = pushTask w act >>= liftIO . wait -- | Run action in worker and wait for result inWorker_ :: (MonadIO m, MonadCatch m) => Worker m -> m a -> ExceptT SomeException IO a -inWorker_ w act = liftIO (pushTask w act) >>= taskJoin_ +inWorker_ w act = liftIO (pushTask w act) >>= ExceptT . waitCatch
− src/Data/Mark.hs
@@ -1,340 +0,0 @@-{-# LANGUAGE ViewPatterns, OverloadedStrings, GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances, RankNTypes, MultiParamTypeClasses #-} - -module Data.Mark ( - Point(..), point, lineStart, (.-.), (.+.), till, Size, linesSize, stringSize, - Range(..), rangeLines, emptyRange, - line, - range, rangeSize, expandLines, at, - -- * Mappings - Map(..), apply, back, - cut, insert, - cutRange, insertRange, - -- * Edited data - Contents, Edit(..), - EditM(..), editRange, mapRange, runEdit, edit, editEval, - Prefix(..), prefix, Suffix(..), suffix, concatCts, splitCts, - -- * Editable class - Editable(..), measure, - -- * Actions - EditAction(..), - Replace(..), inverse, reverseEdits, run - ) where - -import Prelude hiding (splitAt, length, lines, unlines) - -import Control.Arrow ((&&&)) -import Control.Lens (view) -import Control.Lens.Iso -import Control.Monad.State -import Data.Aeson -import qualified Data.List as List (splitAt, length, break, intercalate) -import Data.Text (Text) -import qualified Data.Text as T (splitAt, length, split, intercalate) - -import HsDev.Util ((.::)) - --- | Point at text: line and column -data Point = Point { - pointLine :: Int, - pointColumn :: Int } - deriving (Eq, Ord, Read, Show) - -instance ToJSON Point where - toJSON (Point l c) = object ["line" .= l, "column" .= c] - -instance FromJSON Point where - parseJSON = withObject "point" $ \v -> Point <$> v .:: "line" <*> v .:: "column" - --- | Distance between points is measured in lines and columns. --- And it is defined, that distance between point at l:c and point (l + 1):0 is one line no matter c is --- because we need to go to new line to reach destination point --- Columns are taken into account only if points are on the same line --- @pt .-. base@ is distance from @base@ to @pt@ --- Distance can't be less then zero lines and columns -(.-.) :: Point -> Point -> Point -(Point l c) .-. (Point bl bc) - | bl < l = Point (l - bl) c - | bl == l = Point 0 (max 0 (c - bc)) - | otherwise = Point 0 0 - --- | Opposite to ".-.", @(pt .-. base) .+. base = pt@ -(.+.) :: Point -> Point -> Point -(Point l c) .+. (Point bl bc) - | l == 0 = Point bl (c + bc) - | otherwise = Point (l + bl) c - -point :: Int -> Int -> Point -point l c - | l < 0 = error "Line can't be less then zero" - | c < 0 = error "Column can't be less then zero" - | otherwise = Point l c - -lineStart :: Int -> Point -lineStart l = point l 0 - --- | Range from "Point" to "Point" -data Range = Range { - rangeFrom :: Point, - rangeTo :: Point } - deriving (Eq, Ord, Read, Show) - -type Size = Point - -instance Monoid Size where - mempty = Point 0 0 - l `mappend` r = r .+. l - -rangeLength :: Range -> Size -rangeLength (Range f t) = t .-. f - --- | Range from one @Point@ to another -till :: Point -> Point -> Range -l `till` r = Range (min l r) (max l r) - --- | Distance in @n@ lines -linesSize :: Int -> Point -linesSize n = Point n 0 - --- | Distance in @n@ chars within one line -stringSize :: Int -> Point -stringSize = Point 0 - -instance ToJSON Range where - toJSON (Range f t) = object ["from" .= f, "to" .= t] - -instance FromJSON Range where - parseJSON = withObject "range" $ \v -> Range <$> v .:: "from" <*> v .:: "to" - --- | "Range" height in lines. Any "Range" at least of one line height -rangeLines :: Range -> Int -rangeLines r = succ $ pointLine (rangeTo r) - pointLine (rangeFrom r) - --- | Is "Range" empty -emptyRange :: Range -> Bool -emptyRange r = rangeTo r == rangeFrom r - --- | n'th line range, starts at the beginning of line and ends on the next line -line :: Int -> Range -line l = range (Point l 0) (Point (succ l) 0) - --- | Make range -range :: Point -> Point -> Range -range f t = Range (min f t) (max f t) - --- | Make range from starting point and its size -rangeSize :: Point -> Size -> Range -rangeSize pt sz = range pt (sz .+. pt) - --- | Expand range to contain full lines -expandLines :: Range -> Range -expandLines (Range (Point sline _) (Point eline _)) = Range (Point sline 0) (Point (succ eline) 0) - --- | Get contents at specified range -at :: Editable a => Contents a -> Range -> Contents a -at cts r = - onHead (snd . splitAt (pointColumn $ rangeFrom r)) . - onLast (fst . splitAt (pointColumn $ rangeTo r)) . - take (rangeLines r) . - drop (pointLine (rangeFrom r)) $ - cts - where - onHead :: (a -> a) -> [a] -> [a] - onHead _ [] = [] - onHead f (x:xs) = f x : xs - onLast :: (a -> a) -> [a] -> [a] - onLast _ [] = [] - onLast f l@(last -> x) = init l ++ [f x] - --- | Main idea is that there are only two basic actions, that chances ranges: inserting and cutting --- When something is cutted out or inserted in, range positions must be updated --- All editings can be represented as many cuts and inserts, so we can combine them to get function --- which maps source ranges to ranges on updated data --- Because insert is dual to cut (and therefore composes iso), we can also get function to map ranges back --- Combining this functions while edit, we get function, that maps ranges from source data to edited one --- To get back function, we must also combine opposite actions, or we can represent actions as isomorphisms --- Same idea goes for modifying contents, represent each action as isomorphism and combine them together -newtype Map = Map { mapIso :: Iso' Range Range } - -instance Monoid Map where - mempty = Map $ iso id id - (Map l) `mappend` (Map r) = Map (l . r) - --- | Apply mapping -apply :: Map -> Range -> Range -apply = view . mapIso - --- | Back mapping -back :: Map -> Map -back (Map f) = Map (from f) - --- | Apply mapping to starting position only -applyStart :: Map -> Range -> Range -applyStart m r = rangeFrom (apply m r) `rangeSize` rangeLength r - --- | Cut range mapping -cut :: Range -> Map -cut rgn = Map $ iso (cutRange rgn) (insertRange rgn) - --- | Opposite to "cut" -insert :: Range -> Map -insert = back . cut - --- | Update second range position as if it was data cutted at first range -cutRange :: Range -> Range -> Range -cutRange (Range is ie) (Range s e) = Range - (if is < s then (s .-. ie) .+. is else s) - (if is < e then (e .-. ie) .+. is else e) - --- | Update second range position as if it was data inserted at first range -insertRange :: Range -> Range -> Range -insertRange (Range is ie) (Range s e) = Range - (if is < s then (s .-. is) .+. ie else s) - (if is < e then (e .-. is) .+. ie else e) - --- | Contents is list of lines -type Contents a = [a] - --- | Edit data -data Edit a = Edit { - editCts :: Contents a -> Contents a, -- ^ Edit contents splitted by lines - editMap :: Map } -- ^ Map range from source contents to edited - -instance Monoid (Edit a) where - mempty = Edit id mempty - (Edit fl ml) `mappend` (Edit fr mr) = Edit (fr . fl) (ml `mappend` mr) - --- | Edit monad is state on "Edit", it also collects range mappings -newtype EditM s a = EditM { runEditM :: State (Edit s) a } - deriving (Functor, Applicative, Monad, MonadState (Edit s)) - --- | Basic edit action in monad --- It takes range, range edit function and contents updater --- and passes mapped range to these functions to get new state -editRange :: Range -> (Range -> Edit a) -> EditM a () -editRange rgn edit' = do - rgn' <- mapRange rgn - modify (`mappend` edit' rgn') - --- | Get mapped range -mapRange :: Range -> EditM a Range -mapRange rgn = gets (($ rgn) . apply . editMap) - --- | Run edit monad -runEdit :: Editable s => EditM s a -> (a, Edit s) -runEdit act = runState (runEditM act) mempty - --- | Edit contents -edit :: Editable s => s -> EditM s a -> s -edit cts = snd . editEval cts - --- | Eval edit -editEval :: Editable s => s -> EditM s a -> (a, s) -editEval cts act = (v, unlines . editCts st . lines $ cts) where - (v, st) = runEdit act - --- | Prefix of contents cutted at some point -data Prefix a = Prefix { - prefixLines :: [a], - prefixLine :: a } - deriving (Eq, Ord, Read, Show) - -instance Functor Prefix where - fmap f (Prefix ls l) = Prefix (fmap f ls) (f l) - --- | Make prefix from full contents -prefix :: Contents a -> Prefix a -prefix cts = Prefix (init cts) (last cts) - --- | Suffix of contents -data Suffix a = Suffix { - suffixLine :: a, - suffixLines :: [a] } - deriving (Eq, Ord, Read, Show) - -instance Functor Suffix where - fmap f (Suffix l ls) = Suffix (f l) (fmap f ls) - -suffix :: Contents a -> Suffix a -suffix cts = Suffix (head cts) (tail cts) - --- | Concat prefix and suffix. First line of suffix is appended to last line of prefix -concatCts :: Monoid a => Prefix a -> Suffix a -> Contents a -concatCts (Prefix ps p) (Suffix s ss) = ps ++ [p `mappend` s] ++ ss - --- | Split contents at point. First argument is function to split one line at position. -splitCts :: Editable a => Point -> Contents a -> (Prefix a, Suffix a) -splitCts (Point l c) cts = (Prefix (take l cts) p, Suffix s (drop (succ l) cts)) where - (p, s) = splitAt c (cts !! l) - -class Monoid a => Editable a where - splitAt :: Int -> a -> (a, a) - length :: a -> Int - lines :: a -> [a] - unlines :: [a] -> a - -instance Editable String where - splitAt = List.splitAt - length = List.length - lines s = case List.break (== '\n') s of - (pre, "") -> [pre] - (pre, _:post) -> pre : lines post - unlines = List.intercalate "\n" - -instance Editable Text where - splitAt = T.splitAt - length = T.length - lines = T.split (== '\n') - unlines = T.intercalate "\n" - --- | Contents size -measure :: Editable s => Contents s -> Size -measure [] = error "Invalid argument" -measure cts = Point (pred $ List.length cts) (length $ last cts) - -class EditAction e where - erase :: Editable s => Range -> e s () - write :: Editable s => Point -> s -> e s () - replace :: Editable s => Range -> s -> e s () - -instance EditAction EditM where - erase rgn = editRange rgn (\r -> Edit (erase' r) (cut r)) where - erase' :: Editable a => Range -> Contents a -> Contents a - erase' rgn' cts = fst (splitCts (rangeFrom rgn') cts) `concatCts` snd (splitCts (rangeTo rgn') cts) - - write pt cts = editRange (pt `till` pt) (\r -> Edit (write' r) (insert $ rangeFrom r `rangeSize` measure cts')) where - cts' = lines cts - write' rgn' origin = prefix (before' `concatCts` suffix cts') `concatCts` after' where - (before', after') = splitCts (rangeFrom rgn') origin - - replace rgn cts = erase rgn >> write (rangeFrom rgn) cts - --- | Serializable replace action -data Replace s a = Replace { - replaceRange :: Range, - replaceWith :: s } - deriving (Eq, Read, Show) - -instance (Editable s, ToJSON s) => ToJSON (Replace s a) where - toJSON (Replace e c) = object ["range" .= e, "contents" .= c] - -instance (Editable s, FromJSON s) => FromJSON (Replace s a) where - parseJSON = withObject "edit" $ \v -> Replace <$> v .:: "range" <*> v .:: "contents" - -instance EditAction Replace where - erase rgn = Replace rgn mempty - write pt = Replace (range pt pt) - replace = Replace - --- | Inverted operation - restores previous state -inverse :: Editable s => Contents s -> Replace s a -> Replace s a -inverse cts (Replace r s) = Replace (rangeFrom r `rangeSize` measure (lines s)) (unlines $ cts `at` r) - --- | Reverse list of operations -reverseEdits :: Editable s => Contents s -> [Replace s ()] -> [Replace s ()] -reverseEdits cts rs = map (remap . inverse cts) $ reverse rs where - edit' = snd $ runEdit $ run rs - remap r = r { replaceRange = editMap edit' `applyStart` replaceRange r } - --- | Run replace actions to get monadic action -run :: Editable s => [Replace s ()] -> EditM s () -run = mapM_ (uncurry replace . (replaceRange &&& replaceWith))
src/HsDev/Client/Commands.hs view
@@ -1,969 +1,377 @@ {-# LANGUAGE OverloadedStrings #-} module HsDev.Client.Commands ( - commands - ) where - -import Control.Applicative -import Control.Arrow -import Control.Lens (view, over, preview, each, _Just) -import Control.Monad -import Control.Monad.Except -import Control.Monad.Catch (try, SomeException(..)) -import Control.Monad.Trans.Maybe -import Data.Aeson hiding (Result, Error) -import Data.List -import Data.Maybe -import qualified Data.Map as M -import Data.String (fromString) -import Data.Text (unpack) -import qualified Data.Text as T (isInfixOf, isPrefixOf) -import System.Directory -import System.FilePath -import qualified System.Log.Simple.Base as Log -import Text.Read (readMaybe) - -import HsDev.Cache -import qualified HsDev.Cache.Structured as SC -import HsDev.Commands -import qualified HsDev.Database.Async as DB -import HsDev.Server.Message as M -import HsDev.Server.Types -import HsDev.Symbols -import HsDev.Symbols.Resolve (resolveOne, scopeModule, exportsModule) -import HsDev.Symbols.Util -import qualified HsDev.Tools.AutoFix as AutoFix -import qualified HsDev.Tools.Cabal as Cabal -import HsDev.Tools.Ghc.Worker -import qualified HsDev.Tools.Ghc.Check as Check -import qualified HsDev.Tools.Ghc.Types as Types -import qualified HsDev.Tools.GhcMod as GhcMod -import qualified HsDev.Tools.Hayoo as Hayoo -import qualified HsDev.Tools.HLint as HLint -import qualified HsDev.Tools.Types as Tools -import HsDev.Util - -import Control.Concurrent.Util -import System.Console.Cmd - -import qualified HsDev.Database.Update as Update - --- | Client commands -commands :: [Cmd CommandAction] -commands = [ - -- Ping command - cmd' "ping" [] [] "ping server" ping', - cmd' "listen" [] [] "listen server log" listen', - -- Database commands - cmd' "add" [] [dataArg] "add info to database" add', - cmd' "scan" [] (sandboxes ++ [ - manyReq $ projectArg `desc` "project path or .cabal", - manyReq $ fileArg `desc` "source file", - manyReq $ pathArg `desc` "directory to scan for files and projects", - dataArg `desc` "files contents in format {<path>:<contents>, ...}", - ghcOpts, docsFlag, inferFlag]) - "scan sources" - scan', - cmd' "docs" [] [ - manyReq $ projectArg `desc` "project path or .cabal", - manyReq $ fileArg `desc` "source file", - manyReq $ moduleArg `desc` "module name"] - "scan docs" - docs', - cmd' "infer" [] ([ - manyReq $ projectArg `desc` "project path or .cabal", - manyReq $ fileArg `desc` "source file", - manyReq $ moduleArg `desc` "module name"] ++ autoScanOpts) - "infer types for specified modules" - infer', - cmdList' "remove" [] (sandboxes ++ [ - projectArg `desc` "module project", - fileArg `desc` "module source file", - moduleArg, - packageArg, noLastArg, packageVersionArg, - allFlag "remove all"]) - "remove modules info" - remove', - -- Context free commands - cmdList' "modules" [] (sandboxes ++ [ - manyReq $ projectArg `desc` "projects to list modules from", - moduleArg, - depsArg, - noLastArg, - manyReq packageArg, - sourced, standaloned]) - "list modules" - listModules', - cmdList' "packages" [] [] "list packages" listPackages', - cmdList' "projects" [] [] "list projects" listProjects', - cmdList' "sandboxes" [] [] "list sandboxes" listSandboxes', - cmdList' "symbol" ["name"] (matches ++ sandboxes ++ [ - projectArg `desc` "related project", - fileArg `desc` "source file", - moduleArg, localsArg, - packageArg, depsArg, noLastArg, packageVersionArg, - sourced, standaloned] ++ autoScanOpts) - "get symbol info" - symbol', - cmd' "module" [] (sandboxes ++ [ - moduleArg, localsArg, - packageArg, depsArg, noLastArg, packageVersionArg, - projectArg `desc` "module project", - fileArg `desc` "module source file", - sourced] ++ autoScanOpts) - "get module info" - modul', - cmd' "resolve" [] (sandboxes ++ [ - moduleArg, localsArg, - projectArg `desc` "module project", - fileArg `desc` "module source file", - exportsArg] ++ autoScanOpts) - "resolve module scope (or exports)" - resolve', - cmd' "project" [] ([ - projectArg `desc` "project path or name", - pathArg `desc` "locate project in parent of this path"] ++ autoScanOpts) - "get project info" - project', - cmd' "sandbox" [] [ - pathArg `desc` "locate sandbox in parent of this path"] - "get sandbox info" - sandbox', - -- Context commands - cmdList' "lookup" ["symbol"] ctx "lookup for symbol" lookup', - cmdList' "whois" ["symbol"] ctx "get info for symbol" whois', - cmdList' "scope modules" [] ctx "get modules accessible from module or within a project" scopeModules', - cmdList' "scope" [] (ctx ++ matches ++ [globalArg]) "get declarations accessible from module or within a project" scope', - cmdList' "complete" ["input"] (ctx ++ [wideArg]) "show completions for input" complete', - -- Tool commands - cmdList' "hayoo" ["query"] hayooArgs "find declarations online via Hayoo" hayoo', - cmdList' "cabal list" ["packages..."] [] "list cabal packages" cabalList', - cmdList' "lint" ["files..."] ([dataArg] ++ autoScanOpts) "lint source files or file contents" lint', - cmdList' "check" ["files..."] ([dataArg, sandboxArg, ghcOpts] ++ autoScanOpts) "check source files or file contents" check', - cmdList' "check-lint" ["files..."] ([dataArg, sandboxArg, ghcOpts] ++ autoScanOpts) "check and lint source files or file contents" checkLint', - cmdList' "types" ["file"] ([dataArg, sandboxArg, ghcOpts] ++ autoScanOpts) "get types for file expressions" types', - cmdList' "ghc-mod lang" [] [] "get LANGUAGE pragmas" ghcmodLang', - cmdList' "ghc-mod flags" [] [] "get OPTIONS_GHC pragmas" ghcmodFlags', - cmdList' "ghc-mod type" ["line", "column"] (ctx ++ [ghcOpts]) "infer type with 'ghc-mod type'" ghcmodType', - cmdList' "ghc-mod check" ["files..."] ([sandboxArg, ghcOpts] ++ autoScanOpts) "check source files" ghcmodCheck', - cmdList' "ghc-mod lint" ["files..."] ([hlintOpts] ++ autoScanOpts) "lint source files" ghcmodLint', - cmdList' "ghc-mod check-lint" ["files..."] ([sandboxArg, ghcOpts, hlintOpts] ++ autoScanOpts) "check & lint source files" ghcmodCheckLint', - -- Autofix - cmd' "autofix show" [] [dataArg] "generate corrections for check & lint messages" autofixShow', - cmd' "autofix fix" [] [dataArg, restMsgsArg, pureArg] "fix errors and return rest corrections with updated regions" autofixFix', - -- Ghc commands - cmdList' "ghc eval" ["expr..."] [] "evaluate expression" ghcEval', - -- Dump/load commands - cmd' "dump" [] (sandboxes ++ [ - cacheDir, cacheFile, - manyReq $ projectArg `desc` "project", - manyReq $ fileArg `desc` "file", - standaloned, - allFlag "dump all"]) - "dump database info" dump', - cmd' "load" [] [cacheDir, cacheFile, dataArg] "load data" load', - -- Link - cmd' "link" [] [holdArg] "link to server" link', - -- Exit - cmd' "exit" [] [] "exit" exit'] - where - cmd' :: ToJSON a => String -> [String] -> [Opt] -> String -> ([String] -> Opts String -> CommandActionT a) -> Cmd CommandAction - cmd' nm pos named descr act = checkPosArgs $ cmd nm pos named descr act' where - act' (Args args os) copts = Log.scopeLog (commandLogger copts) (fromString nm) $ do - r <- runExceptT (act args os copts) - case r of - Left (CommandError e ds) -> return $ Error e $ M.fromList $ map (first unpack) ds - Right r' -> return $ Result $ toJSON r' - - cmdList' :: ToJSON a => String -> [String] -> [Opt] -> String -> ([String] -> Opts String -> CommandActionT [a]) -> Cmd CommandAction - cmdList' nm pos named descr act = cmd' nm pos (named ++ [splitRes]) descr act' where - splitRes = flag "split-result" `desc` "split result list and return it with notifications" - act' args os copts = do - rs <- act args os' copts - if flagSet "split-result" isSplit - then do - liftIO $ mapM_ (commandNotify copts . resultPart) rs - return [] - else return rs - where - (isSplit, os') = splitOpts [splitRes] os - - -- Command arguments and flags - allFlag d = flag "all" `short` ['a'] `desc` d - autoScanFlag = flag "autoscan" `short` ['s'] `desc` "automatically scan related files/projects" - autoScanOpts = [autoScanFlag, ghcOpts] - cacheDir = req "cache-dir" "path" `desc` "cache path" - cacheFile = req "cache-file" "path" `desc` "cache file" - ctx = [fileArg `desc` "source file", sandboxArg] - dataArg = req "data" "contents" `desc` "data to pass to command" - depsArg = req "deps" "object" `desc` "filter to such that in dependency of specified object (file or project)" - docsFlag = flag "docs" `desc` "scan source file docs" - exportsArg = flag "exports" `short` ['e'] `desc` "resolve module exports" - fileArg = req "file" "path" `short` ['f'] - findArg = req "find" "query" `desc` "infix match" - ghcOpts = list "ghc" "option" `short` ['g'] `desc` "options to pass to GHC" - globalArg = flag "global" `desc` "scope of project" - hayooArgs = [ - req "page" "n" `short` ['p'] `desc` "page number (0 by default)", - req "pages" "count" `short` ['n'] `desc` "pages count (1 by default)"] - hlintOpts = list "hlint" "option" `short` ['h'] `desc` "options to pass to hlint" - holdArg = flag "hold" `short` ['h'] `desc` "don't return any response" - inferFlag = flag "infer" `desc` "infer types" - localsArg = flag "locals" `short` ['l'] `desc` "look in local declarations" - matches = [prefixArg, findArg] - moduleArg = req "module" "name" `short` ['m'] `desc` "module name" - noLastArg = flag "no-last" `desc` "select not only last version packages" - packageArg = req "package" "name" `desc` "module package" - pathArg = req "path" "path" `short` ['p'] - prefixArg = req "prefix" "prefix" `desc` "prefix match" - projectArg = req "project" "project" - packageVersionArg = req "version" "id" `short` ['v'] `desc` "package version" - pureArg = flag "pure" `desc` "don't modify actual file, just return result" - restMsgsArg = req "rest" "corrections" `short` ['r'] `desc` "corrections left unfixed to update locations" - sandboxArg = req "sandbox" "path" `desc` "path to cabal sandbox" - sandboxList = manyReq sandboxArg - sandboxes = [ - flag "cabal" `desc` "cabal", - sandboxList] - sourced = flag "src" `desc` "source files" - standaloned = flag "stand" `desc` "standalone files" - wideArg = flag "wide" `short` ['f'] `desc` "wide mode - complete as if there were no import lists" - - -- | Ping server - ping' :: [String] -> Opts String -> CommandActionT Value - ping' _ _ _ = return $ object ["message" .= ("pong" :: String)] - - -- | Listen server log - listen' :: [String] -> Opts String -> CommandActionT () - listen' _ _ copts = liftIO $ commandListenLog copts $ - mapM_ (\msg -> commandNotify copts (Notification $ object ["message" .= msg])) - - -- | Add data - add' :: [String] -> Opts String -> CommandActionT () - add' _ as copts = do - dbval <- getDb copts - jsonData <- maybe (commandError "Specify --data" []) return $ arg "data" as - decodedData <- either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= jsonData]) - return $ - eitherDecode $ toUtf8 jsonData - - let - updateData (ResultDatabase db) = DB.update (dbVar copts) $ return db - updateData (ResultDeclaration d) = commandError "Can't insert declaration" ["declaration" .= d] - updateData (ResultModuleDeclaration md) = do - let - ModuleId mname mloc = view declarationModuleId md - defMod = Module mname Nothing mloc mempty mempty mempty - defInspMod = Inspected InspectionNone mloc (Right defMod) - dbmod = maybe - defInspMod - (over inspectionResult (<|> Right defMod)) $ - M.lookup mloc (databaseModules dbval) - updatedMod = over inspectionResult (fmap $ addDeclaration (view moduleDeclaration md)) dbmod - DB.update (dbVar copts) $ return $ fromModule updatedMod - updateData (ResultModuleId (ModuleId mname mloc)) = when (M.notMember mloc $ databaseModules dbval) $ - DB.update (dbVar copts) $ return $ fromModule $ Inspected InspectionNone mloc (Right $ Module mname Nothing mloc mempty mempty mempty) - updateData (ResultModule m) = DB.update (dbVar copts) $ return $ fromModule $ Inspected InspectionNone (view moduleLocation m) (Right m) - updateData (ResultInspectedModule m) = DB.update (dbVar copts) $ return $ fromModule m - updateData (ResultProject p) = DB.update (dbVar copts) $ return $ fromProject p - updateData (ResultList l) = mapM_ updateData l - updateData (ResultMap m) = mapM_ updateData $ M.elems m - updateData ResultNone = return () - updateData _ = commandError "Invalid data" [] - updateData decodedData - - -- | Scan sources and installed packages - scan' :: [String] -> Opts String -> CommandActionT () - scan' _ as copts = do - cabals <- getSandboxes copts as - let - getData :: String -> CommandM [(FilePath, String)] - getData d = - either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= d]) - (return . M.toList) . - eitherDecode . toUtf8 $ d - ctsFiles <- traverse getData $ arg "data" as - updateProcess copts as $ concat [ - map (\(f, cts) -> Update.scanFileContents (listArg "ghc" as) f (Just cts)) (fromMaybe [] ctsFiles), - concatMap (\(n, f) -> [findPath copts v >>= f (listArg "ghc" as) | v <- listArg n as]) [ - ("project", Update.scanProject), - ("file", Update.scanFile), - ("path", Update.scanDirectory)], - map (Update.scanCabal (listArg "ghc" as)) cabals] - - -- | Scan docs - docs' :: [String] -> Opts String -> CommandActionT () - docs' _ as copts = do - files <- traverse (findPath copts) $ listArg "file" as - projects <- traverse (findProject copts) $ listArg "project" as - dbval <- getDb copts - let - filters = anyOf $ - map inProject projects ++ - map inFile files ++ - map inModule (listArg "module" as) - mods = selectModules (filters . view moduleId) dbval - updateProcess copts as [Update.scanDocs $ map (getInspected dbval) mods] - - -- | Infer types - infer' :: [String] -> Opts String -> CommandActionT () - infer' _ as copts = do - files <- traverse (findPath copts) $ listArg "file" as - projects <- traverse (findProject copts) $ listArg "project" as - autoScan as copts files projects - dbval <- getDb copts - let - filters = anyOf $ - map inProject projects ++ - map inFile files ++ - map inModule (listArg "module" as) - mods = selectModules (filters . view moduleId) dbval - updateProcess copts as [Update.inferModTypes $ map (getInspected dbval) mods] - - -- | Remove data - remove' :: [String] -> Opts String -> CommandActionT [ModuleId] - remove' _ as copts = do - dbval <- getDb copts - cabal <- getCabal_ copts as - proj <- traverse (findProject copts) $ arg "project" as - file <- traverse (findPath copts) $ arg "file" as - let - cleanAll = flagSet "all" as - filters = catMaybes [ - fmap inProject proj, - fmap inFile file, - fmap inModule (arg "module" as), - fmap inPackage (arg "package" as), - fmap inVersion (arg "version" as), - fmap inCabal cabal] - toClean = newest as $ filter (allOf filters . view moduleId) (allModules dbval) - action - | null filters && cleanAll = liftIO $ do - DB.modifyAsync (dbVar copts) DB.Clear - return [] - | null filters && not cleanAll = commandError "Specify filter or explicitely set flag --all" [] - | cleanAll = commandError "--all flag can't be set with filters" [] - | otherwise = liftIO $ do - DB.modifyAsync (dbVar copts) $ DB.Remove $ mconcat $ map (fromModule . getInspected dbval) toClean - return $ map (view moduleId) toClean - action - - -- | List modules - listModules' :: [String] -> Opts String -> CommandActionT [ModuleId] - listModules' _ as copts = do - dbval <- getDb copts - projs <- traverse (findProject copts) $ listArg "project" as - deps <- traverse (findDep copts) $ listArg "deps" as - cabals <- getSandboxes copts as - let - packages = listArg "package" as - hasFilters = not $ null projs && null packages && null cabals && null deps - filters = allOf $ catMaybes [ - if hasFilters - then Just $ anyOf $ catMaybes [ - if null projs then Nothing else Just (\m -> any (`inProject` m) projs), - if null deps then Nothing else Just (\m -> any (`inDeps` m) deps), - if null packages && null cabals then Nothing - else Just (\m -> (any (`inPackage` m) packages || null packages) && (any (`inCabal` m) cabals || null cabals))] - else Nothing, - fmap (\n m -> fromString n == view moduleIdName m) $ arg "module" as, - if flagSet "src" as then Just byFile else Nothing, - if flagSet "stand" as then Just standalone else Nothing] - return $ map (view moduleId) $ newest as $ selectModules (filters . view moduleId) dbval - - -- | List packages - listPackages' :: [String] -> Opts String -> CommandActionT [ModulePackage] - listPackages' _ _ copts = do - dbval <- getDb copts - return $ ordNub $ sort $ - mapMaybe (preview (moduleLocation . modulePackage . _Just)) $ - allModules dbval - - -- | List projects - listProjects' :: [String] -> Opts String -> CommandActionT [Project] - listProjects' _ _ copts = do - dbval <- getDb copts - return $ M.elems $ databaseProjects dbval - - -- | List sandboxes - listSandboxes' :: [String] -> Opts String -> CommandActionT [Cabal] - listSandboxes' _ _ copts = (ordNub . sort . mapMaybe (cabalOf . view moduleId) . allModules) <$> getDb copts - - -- | Get symbol info - symbol' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - symbol' ns as copts = do - dbval <- liftM (localsDatabase as) $ getDb copts - proj <- traverse (findProject copts) $ arg "project" as - file <- traverse (findPath copts) $ arg "file" as - autoScan as copts (maybeToList file) (maybeToList proj) - deps <- traverse (findDep copts) $ arg "deps" as - cabal <- getCabal_ copts as - let - filters = checkModule $ allOf $ catMaybes [ - fmap inProject proj, - fmap inFile file, - fmap inModule (arg "module" as), - fmap inPackage (arg "package" as), - fmap inDeps deps, - fmap inVersion (arg "version" as), - fmap inCabal cabal, - if flagSet "src" as then Just byFile else Nothing, - if flagSet "stand" as then Just standalone else Nothing] - toResult = newest as . filterMatch as . filter filters - case ns of - [] -> return $ toResult $ allDeclarations dbval - [nm] -> liftM toResult $ mapExceptT - (liftM $ left (\e -> CommandError ("Can't find symbol: " ++ e) [])) - (findDeclaration dbval nm) - _ -> commandError "Too much arguments" [] - - -- | Get module info - modul' :: [String] -> Opts String -> CommandActionT Module - modul' _ as copts = do - dbval <- liftM (localsDatabase as) $ getDb copts - proj <- traverse (findProject copts) $ arg "project" as - cabal <- getCabal_ copts as - file' <- mapExceptT (fmap $ left commandStrMsg) $ traverse (findPath copts) $ arg "file" as - autoScan as copts (maybeToList file') (maybeToList proj) - deps <- traverse (findDep copts) $ arg "deps" as - let - filters = allOf $ catMaybes [ - fmap inProject proj, - fmap inCabal cabal, - fmap inFile file', - fmap inModule (arg "module" as), - fmap inPackage (arg "package" as), - fmap inDeps deps, - fmap inVersion (arg "version" as), - if flagSet "src" as then Just byFile else Nothing] - rs <- (newest as . filter (filters . view moduleId)) <$> maybe - (return $ allModules dbval) - (mapCommandErrorStr . findModule dbval) - (arg "module" as) - case rs of - [] -> commandError "Module not found" [] - [m] -> return m - ms' -> commandError "Ambiguous modules" ["modules" .= map (view moduleId) ms'] - - -- | Resolve module scope - resolve' :: [String] -> Opts String -> CommandActionT Module - resolve' _ as copts = do - dbval <- liftM (localsDatabase as) $ getDb copts - proj <- traverse (findProject copts) $ arg "project" as - cabal <- getCabal copts as - file' <- mapExceptT (fmap $ left commandStrMsg) $ traverse (findPath copts) $ arg "file" as - autoScan as copts (maybeToList file') (maybeToList proj) - let - filters = allOf $ catMaybes [ - fmap inProject proj, - fmap inFile file', - fmap inModule (arg "module" as), - Just byFile] - rs <- (newest as . filter (filters . view moduleId)) <$> maybe - (return $ allModules dbval) - (mapCommandErrorStr . findModule dbval) - (arg "module" as) - let - cabaldb = filterDB (restrictCabal cabal) (const True) dbval - getScope = if flagSet "exports" as then exportsModule else scopeModule - case rs of - [] -> commandError "Module not found" [] - [m] -> return $ getScope $ resolveOne cabaldb m - ms' -> commandError "Ambiguous modules" ["modules" .= map (view moduleId) ms'] - - -- | Get project info - project' :: [String] -> Opts String -> CommandActionT Project - project' _ as copts = do - proj <- runMaybeT $ do - p <- msum $ map MaybeT [ - traverse (findProject copts) $ arg "project" as, - liftM join $ traverse (liftIO . searchProject) $ arg "path" as] - lift $ if flagSet "autoscan" as then mapCommandErrorStr (loadProject p) else return p - maybe (commandError "Specify project name, .cabal file or search directory" []) return proj - - -- | Locate sandbox - sandbox' :: [String] -> Opts String -> CommandActionT Cabal - sandbox' _ as _ = do - sbox <- traverse (liftIO . searchSandbox) (arg "path" as) - maybe (commandError "Specify search directory" []) return sbox - - -- | Lookup info about symbol - lookup' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - lookup' [nm] as copts = do - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - mapCommandErrorStr $ lookupSymbol dbval cabal srcFile nm - lookup' _ _ _ = commandError "Invalid arguments" [] - - -- | Get detailed info about symbol in source file - whois' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - whois' [nm] as copts = do - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - mapCommandErrorStr $ whois dbval cabal srcFile nm - whois' _ _ _ = commandError "Invalid arguments" [] - - -- | Get modules accessible from module or from directory - scopeModules' :: [String] -> Opts String -> CommandActionT [ModuleId] - scopeModules' [] as copts = do - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - liftM (map (view moduleId)) $ mapCommandErrorStr $ scopeModules dbval cabal srcFile - scopeModules' _ _ _ = commandError "Invalid arguments" [] - - -- | Get declarations accessible from module - scope' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - scope' [] as copts = do - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - liftM (filterMatch as) $ mapCommandErrorStr $ scope dbval cabal srcFile (flagSet "global" as) - scope' _ _ _ = commandError "Invalid arguments" [] - - -- | Completion - complete' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - complete' [] as copts = complete' [""] as copts - complete' [input] as copts = do - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - mapCommandErrorStr $ completions dbval cabal srcFile input (flagSet "wide" as) - complete' _ _ _ = commandError "Invalid arguments" [] - - -- | Hayoo - hayoo' :: [String] -> Opts String -> CommandActionT [ModuleDeclaration] - hayoo' [] _ _ = commandError "Query not specified" [] - hayoo' [query] opts _ = liftM concat $ forM [page .. page + pred pages] $ \i -> liftM - (mapMaybe Hayoo.hayooAsDeclaration . Hayoo.resultResult) $ - mapCommandErrorStr $ Hayoo.hayoo query (Just i) - where - page = fromMaybe 0 $ narg "page" opts - pages = fromMaybe 1 $ narg "pages" opts - hayoo' _ _ _ = commandError "Too much arguments" [] - - -- | Cabal list - cabalList' :: [String] -> Opts String -> CommandActionT [Cabal.CabalPackage] - cabalList' qs _ _ = mapCommandErrorStr $ Cabal.cabalList qs - - -- | HLint - lint' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - lint' files as copts = case arg "data" as of - Nothing -> do - files' <- mapM (findPath copts) files - autoScan as copts files' [] - mapCommandErrorStr $ liftM concat $ mapM HLint.hlintFile files' - Just src -> do - src' <- either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= src]) - return $ - eitherDecode (toUtf8 src) - when (length files > 1) $ commandError_ "Only one file permitted when passing source" - file' <- traverse (findPath copts) $ listToMaybe files - mapCommandErrorStr $ HLint.hlintSource (fromMaybe "<unnamed>" file') src' - - -- | Check - check' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - check' files as copts = case arg "data" as of - Nothing -> do - files' <- mapM (findPath copts) files - autoScan as copts files' [] - db <- getDb copts - cabal <- getCabal copts as - liftM concat $ forM files' $ \file' -> do - m <- maybe - (commandError_ $ "File '" ++ file' ++ "' not found, maybe you forgot to scan it?") - return $ - lookupFile file' db - notes <- inWorkerWith (commandError_ . show) (commandGhc copts) $ - (runExceptT $ Check.checkFile (listArg "ghc" as) cabal m) - either commandError_ return notes - Just src -> do - src' <- either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= src]) - return $ - eitherDecode (toUtf8 src) - when (length files > 1) $ commandError_ "Only one file permitted when passing source" - file' <- maybe (commandError_ "File must be specified") (findPath copts) $ listToMaybe files - db <- getDb copts - cabal <- getCabal copts as - m <- maybe - (commandError_ $ "File '" ++ file' ++ "' not found") - return $ - lookupFile file' db - notes <- inWorkerWith (commandError_ . show) (commandGhc copts) $ - (runExceptT $ Check.checkSource (listArg "ghc" as) cabal m src') - either commandError_ return notes - - -- | Check and lint - checkLint' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - checkLint' files as copts = liftM2 (++) (check' files as copts) (lint' files as copts) - - -- | Types - types' :: [String] -> Opts String -> CommandActionT [Tools.Note Types.TypedExpr] - types' [file] as copts = do - file' <- findPath copts file - autoScan as copts [file'] [] - db <- getDb copts - cabal <- getCabal copts as - let - decodeData src = either - (\err -> commandError "Unable to decode data" ["why" .= err, "data" .= src]) - return $ - eitherDecode (toUtf8 src) - msrc <- traverse decodeData $ arg "data" as - m <- maybe - (commandError_ $ "File '" ++ file' ++ "' not found") - return $ - lookupFile file' db - notes <- inWorkerWith (commandError_ . show) (commandGhc copts) $ - (runExceptT $ Types.fileTypes (listArg "ghc" as) cabal m msrc) - either commandError_ return notes - types' _ _ _ = commandError_ "One file must be specified" - - -- | Ghc-mod lang - ghcmodLang' :: [String] -> Opts String -> CommandActionT [String] - ghcmodLang' _ _ _ = mapCommandErrorStr GhcMod.langs - - -- | Ghc-mod flags - ghcmodFlags' :: [String] -> Opts String -> CommandActionT [String] - ghcmodFlags' _ _ _ = mapCommandErrorStr GhcMod.flags - - -- | Ghc-mod type - ghcmodType' :: [String] -> Opts String -> CommandActionT [GhcMod.TypedRegion] - ghcmodType' [line] as copts = ghcmodType' [line, "1"] as copts - ghcmodType' [line, column] as copts = do - line' <- maybe (commandError "line must be a number" []) return $ readMaybe line - column' <- maybe (commandError "column must be a number" []) return $ readMaybe column - dbval <- getDb copts - (srcFile, cabal) <- getCtx copts as - (srcFile', m', _) <- mapCommandErrorStr $ fileCtx dbval srcFile - mapCommandErrorStr $ GhcMod.waitMultiGhcMod (commandGhcMod copts) srcFile' $ - GhcMod.typeOf (listArg "ghc" as ++ moduleOpts (allPackages dbval) m') cabal srcFile' line' column' - ghcmodType' [] _ _ = commandError "Specify line" [] - ghcmodType' _ _ _ = commandError "Too much arguments" [] - - -- | Ghc-mod check - ghcmodCheck' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - ghcmodCheck' [] _ _ = commandError "Specify at least one file" [] - ghcmodCheck' files as copts = do - files' <- mapM (findPath copts) files - autoScan as copts files' [] - mproj <- (listToMaybe . catMaybes) <$> liftIO (mapM locateProject files') - cabal <- getCabal copts as - dbval <- getDb copts - mapCommandErrorStr $ liftM concat $ forM files' $ \file' -> do - (_, m', _) <- fileCtx dbval file' - GhcMod.waitMultiGhcMod (commandGhcMod copts) file' $ - GhcMod.check (listArg "ghc" as ++ moduleOpts (allPackages dbval) m') cabal [file'] mproj - - -- | Ghc-mod lint - ghcmodLint' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - ghcmodLint' [] _ _ = commandError "Specify at least one file to hlint" [] - ghcmodLint' files as copts = do - files' <- mapM (findPath copts) files - autoScan as copts files' [] - mapCommandErrorStr $ liftM concat $ forM files' $ \file' -> - GhcMod.waitMultiGhcMod (commandGhcMod copts) file' $ - GhcMod.lint (listArg "hlint" as) file' - - -- | Ghc-mod check & lint - ghcmodCheckLint' :: [String] -> Opts String -> CommandActionT [Tools.Note Tools.OutputMessage] - ghcmodCheckLint' [] _ _ = commandError "Specify at least one file" [] - ghcmodCheckLint' files as copts = do - files' <- mapM (findPath copts) files - autoScan as copts files' [] - mproj <- (listToMaybe . catMaybes) <$> liftIO (mapM locateProject files') - cabal <- getCabal copts as - dbval <- getDb copts - mapCommandErrorStr $ liftM concat $ forM files' $ \file' -> do - (_, m', _) <- fileCtx dbval file' - GhcMod.waitMultiGhcMod (commandGhcMod copts) file' $ do - checked <- GhcMod.check (listArg "ghc" as ++ moduleOpts (allPackages dbval) m') cabal [file'] mproj - linted <- GhcMod.lint (listArg "hlint" as) file' - return $ checked ++ linted - - -- | Autofix show - autofixShow' :: [String] -> Opts String -> CommandActionT [Tools.Note AutoFix.Correction] - autofixShow' _ as _ = do - jsonData <- maybe (commandError "Specify --data" []) return $ arg "data" as - msgs <- either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= jsonData]) - return $ - eitherDecode $ toUtf8 jsonData - return $ AutoFix.corrections msgs - - -- | Autofix fix - autofixFix' :: [String] -> Opts String -> CommandActionT [Tools.Note AutoFix.Correction] - autofixFix' _ as copts = do - let - readCorrs cts = either - (\err -> commandError "Unable to decode data" [ - "why" .= err, - "data" .= cts]) - return $ - eitherDecode $ toUtf8 cts - jsonData <- maybe (commandError "Specify --data" []) return $ arg "data" as - corrs <- readCorrs jsonData - upCorrs <- liftM (fromMaybe []) $ traverse readCorrs $ arg "rest" as - files <- liftM (ordNub . sort) $ mapM (findPath copts) $ mapMaybe (preview $ Tools.noteSource . moduleFile) corrs - let - doFix :: FilePath -> AutoFix.EditM String [Tools.Note AutoFix.Correction] - doFix file = do - AutoFix.autoFix_ fCorrs - (each . Tools.note) AutoFix.updateRange fUpCorrs - where - findCorrs :: FilePath -> [Tools.Note AutoFix.Correction] -> [Tools.Note AutoFix.Correction] - findCorrs f = filter ((== Just f) . preview (Tools.noteSource . moduleFile)) - fCorrs = map (view Tools.note) $ findCorrs file corrs - fUpCorrs = findCorrs file upCorrs - runFix file - | flagSet "pure" as = return $ fst $ AutoFix.runEdit $ doFix file - | otherwise = do - (corrs', cts') <- liftM (`AutoFix.editEval` doFix file) $ liftE $ readFileUtf8 file - liftE $ writeFileUtf8 file cts' - return corrs' - mapCommandErrorStr $ liftM concat $ mapM runFix files - - -- | Evaluate expression - ghcEval' :: [String] -> Opts String -> CommandActionT [Value] - ghcEval' exprs _ copts = mapCommandErrorStr $ liftM (map toValue) $ liftTask $ - pushTask (commandGhci copts) $ mapM (try . evaluate) exprs - where - toValue :: Either SomeException String -> Value - toValue (Left (SomeException e)) = object ["fail" .= show e] - toValue (Right s) = toJSON s - - -- | Dump database info - dump' :: [String] -> Opts String -> CommandActionT () - dump' [] as copts = do - dbval <- getDb copts - - cabals <- getSandboxes copts as - ps' <- traverse (findProject copts) $ listArg "project" as - fs' <- traverse (findPath copts) $ listArg "file" as - - let - dat = mconcat [ - if flagSet "all" as then dbval else mempty, - if flagSet "stand" as then standaloneDB dbval else mempty, - mconcat $ map (`cabalDB` dbval) cabals, - mconcat $ map (`projectDB` dbval) ps', - filterDB (\m -> any (`inFile` m) fs') (const False) dbval] - - void $ runMaybeT $ msum [ - do - p <- MaybeT $ traverse (findPath copts) $ arg "path" as - fork $ SC.dump p $ structurize dat, - do - f <- MaybeT $ traverse (findPath copts) $ arg "file" as - fork $ dump f dat] - dump' _ _ _ = commandError "Invalid arguments" [] - - -- | Load database - load' :: [String] -> Opts String -> CommandActionT () - load' _ as copts = do - void $ liftM (maybe (commandError "Specify one of: --path, --file or --data" []) return) $ runMaybeT $ msum [ - do - p <- MaybeT $ return $ arg "path" as - lift $ cacheLoad copts (liftA merge <$> SC.load p), - do - f <- MaybeT $ return $ arg "file" as - e <- liftIO $ doesFileExist f - when e $ lift $ cacheLoad copts (load f), - do - dat <- MaybeT $ return $ arg "data" as - lift $ cacheLoad copts (return $ eitherDecode (toUtf8 dat))] - waitDb copts as - - -- | Link to server - link' :: [String] -> Opts String -> CommandActionT () - link' _ as copts = liftIO $ do - commandLink copts - when (flagSet "hold" as) $ commandHold copts - - -- | Exit - exit' :: [String] -> Opts String -> CommandActionT () - exit' _ _ copts = liftIO $ commandExit copts - --- Helper functions - -commandStrMsg :: String -> CommandError -commandStrMsg m = CommandError m [] - --- | Automatically scan files and projects -autoScan :: Opts String -> CommandOptions -> [FilePath] -> [Project] -> CommandM () -autoScan as copts srcs projs - | flagSet "autoscan" as = updateProcess copts as $ - concatMap (\(n, f) -> [findPath copts v >>= f (listArg "ghc" as) | v <- n]) [ - (srcs, Update.scanFile), - (map (view projectCabal) projs, Update.scanProject)] - | otherwise = return () - --- | Check positional args count -checkPosArgs :: Cmd a -> Cmd a -checkPosArgs c = validateArgs pos' c where - pos' (Args args _) = case cmdArgs c of - [ellipsis] - | "..." `isSuffixOf` ellipsis -> return () - _ -> mplus - (guard (length args <= length (cmdArgs c))) - (failMatch ("unexpected positional arguments: " ++ unwords (drop (length $ cmdArgs c) args))) - --- | Find sandbox by path -findSandbox :: MonadIO m => CommandOptions -> Maybe FilePath -> ExceptT CommandError m Cabal -findSandbox copts = maybe - (return Cabal) - (findPath copts >=> mapCommandErrorStr . liftIO . getSandbox) - --- | Canonicalize path -findPath :: MonadIO m => CommandOptions -> FilePath -> ExceptT e m FilePath -findPath copts f = liftIO $ canonicalizePath (normalise f') where - f' - | isRelative f = commandRoot copts </> f - | otherwise = f - --- | Get context: file and sandbox -getCtx :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ExceptT CommandError m (FilePath, Cabal) -getCtx copts as = do - f <- forceJust "No file specified" $ traverse (findPath copts) $ arg "file" as - c <- getCabal_ copts as >>= maybe (liftIO $ getSandbox f) return - return (f, c) - --- | Get current sandbox set, user-db by default -getCabal :: MonadIO m => CommandOptions -> Opts String -> ExceptT CommandError m Cabal -getCabal copts as - | flagSet "cabal" as = findSandbox copts Nothing - | otherwise = findSandbox copts $ arg "sandbox" as - --- | Get current sandbox if set -getCabal_ :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ExceptT CommandError m (Maybe Cabal) -getCabal_ copts as - | flagSet "cabal" as = Just <$> findSandbox copts Nothing - | otherwise = case arg "sandbox" as of - Just f -> Just <$> findSandbox copts (Just f) - Nothing -> return Nothing - --- | Get list of enumerated sandboxes -getSandboxes :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ExceptT CommandError m [Cabal] -getSandboxes copts as = traverse (findSandbox copts) paths where - paths - | flagSet "cabal" as = Nothing : sboxes - | otherwise = sboxes - sboxes = map Just $ listArg "sandbox" as - --- | Find project by name of path -findProject :: MonadIO m => CommandOptions -> String -> ExceptT CommandError m Project -findProject copts proj = do - db' <- getDb copts - proj' <- liftM addCabal $ findPath copts proj - let - resultProj = - M.lookup proj' (databaseProjects db') <|> - find ((== proj) . view projectName) (M.elems $ databaseProjects db') - maybe (throwError $ commandStrMsg $ "Projects " ++ proj ++ " not found") return resultProj - where - addCabal p - | takeExtension p == ".cabal" = p - | otherwise = p </> (takeBaseName p <.> "cabal") - --- | Find dependency: it may be source, project file or project name, also returns sandbox found -findDep :: MonadIO m => CommandOptions -> String -> ExceptT CommandError m (Project, Maybe FilePath, Cabal) -findDep copts depName = do - depPath <- findPath copts depName - proj <- msum [ - mapCommandErrorStr $ do - p <- liftIO (locateProject depPath) - maybe (throwError $ "Project " ++ depName ++ " not found") (mapExceptT liftIO . loadProject) p, - findProject copts depName] - let - src - | takeExtension depPath == ".hs" = Just depPath - | otherwise = Nothing - sbox <- liftIO $ searchSandbox $ view projectPath proj - return (proj, src, sbox) - --- | Check if project or source depends from this module -inDeps :: (Project, Maybe FilePath, Cabal) -> ModuleId -> Bool -inDeps (proj, src, cabal) = liftM2 (&&) (restrictCabal cabal) deps' where - deps' = case src of - Nothing -> inDepsOfProject proj - Just src' -> inDepsOfFile proj src' - --- | Wait for DB to complete update -waitDb :: CommandOptions -> Opts String -> CommandM () -waitDb copts as = when (flagSet "wait" as) $ liftIO $ do - commandLog copts Log.Trace "wait for db" - DB.wait (dbVar copts) - commandLog copts Log.Trace "db done" - -cacheLoad :: CommandOptions -> IO (Either String Database) -> CommandM () -cacheLoad copts act = liftIO $ do - db' <- act - case db' of - Left e -> commandLog copts Log.Error e - Right database -> DB.update (dbVar copts) (return database) - --- | Bring locals to top scope to search within them if 'locals' flag set -localsDatabase :: Opts String -> Database -> Database -localsDatabase as - | flagSet "locals" as = databaseLocals - | otherwise = id - --- | Select newest packages if 'no-last' flag not set -newest :: Symbol a => Opts String -> [a] -> [a] -newest as - | flagSet "no-last" as = id - | otherwise = newestPackage - --- | Convert from just of throw -forceJust :: MonadIO m => String -> ExceptT CommandError m (Maybe a) -> ExceptT CommandError m a -forceJust msg act = act >>= maybe (throwError $ commandStrMsg msg) return - --- | Get actual DB state -getDb :: MonadIO m => CommandOptions -> m Database -getDb = liftIO . DB.readAsync . commandDatabase - --- | Get DB async var -dbVar :: CommandOptions -> DB.Async Database -dbVar = commandDatabase - -mapCommandErrorStr :: (Monad m) => ExceptT String m a -> ExceptT CommandError m a -mapCommandErrorStr = mapExceptT (liftM $ left commandStrMsg) - --- | Run DB update action -updateProcess :: CommandOptions -> Opts String -> [ExceptT String (Update.UpdateDB IO) ()] -> CommandM () -updateProcess copts as acts = lift $ Update.updateDB (Update.settings copts as) $ sequence_ [act `catchError` logErr | act <- acts] where - logErr :: String -> ExceptT String (Update.UpdateDB IO) () - logErr e = liftIO $ commandLog copts Log.Error e - --- | Filter declarations with prefix and infix -filterMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] -filterMatch as = findMatch as . prefMatch as - --- | Filter declarations with infix match -findMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] -findMatch as = case arg "find" as of - Nothing -> id - Just str -> filter (match' str) - where - match' str m = fromString str `T.isInfixOf` view (moduleDeclaration . declarationName) m - --- | Filter declarations with prefix match -prefMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration] -prefMatch as = case fmap splitIdentifier (arg "prefix" as) of - Nothing -> id - Just (qname, pref) -> filter (match' qname pref) - where - match' qname pref m = - fromString pref `T.isPrefixOf` view (moduleDeclaration . declarationName) m && - maybe True (view (declarationModuleId . moduleIdName) m ==) (fmap fromString qname) + runCommand + ) where + +import Control.Applicative +import Control.Arrow +import Control.Lens (view, preview, _Just, from, each) +import Control.Monad +import Control.Monad.Except +import Control.Monad.State (gets) +import Control.Monad.Catch (try, SomeException(..)) +import Data.Aeson hiding (Result, Error) +import Data.List +import Data.Foldable (toList) +import Data.Maybe +import qualified Data.Map as M +import Data.String (fromString) +import Data.Text (unpack) +import Data.Text.Lens (packed) +import qualified Data.Text as T (isInfixOf, isPrefixOf, isSuffixOf) +import System.Directory +import System.FilePath +import qualified System.Log.Simple.Base as Log +import Text.Regex.PCRE ((=~)) + +import System.Directory.Paths +import HsDev.Cache +import HsDev.Commands +import qualified HsDev.Database.Async as DB +import HsDev.Server.Message as M +import HsDev.Server.Types +import HsDev.Symbols +import HsDev.Symbols.Resolve (resolveOne, scopeModule, exportsModule) +import HsDev.Symbols.Util +import qualified HsDev.Tools.AutoFix as AutoFix +import qualified HsDev.Tools.Cabal as Cabal +import HsDev.Tools.Ghc.Worker +import qualified HsDev.Tools.Ghc.Check as Check +import qualified HsDev.Tools.Ghc.Types as Types +import qualified HsDev.Tools.GhcMod as GhcMod +import qualified HsDev.Tools.Hayoo as Hayoo +import qualified HsDev.Tools.HLint as HLint +import qualified HsDev.Tools.Types as Tools +import HsDev.Util + +import qualified HsDev.Database.Update as Update + +runCommandM :: ToJSON a => CommandM a -> IO Result +runCommandM = liftM toResult . runExceptT where + toResult (Left (CommandError e ds)) = Error e $ M.fromList $ map (first unpack) ds + toResult (Right r') = Result $ toJSON r' + +-- | Run command +runCommand :: CommandOptions -> Command -> IO Result +runCommand copts Ping = runCommandM $ return $ object ["message" .= ("pong" :: String)] +runCommand copts Listen = runCommandM $ liftIO $ commandListenLog copts $ + mapM_ (\msg -> commandNotify copts (Notification $ object ["message" .= msg])) +runCommand copts (AddData cts) = runCommandM $ mapM_ updateData cts where + updateData (AddedDatabase db) = DB.update (dbVar copts) $ return db + updateData (AddedModule m) = DB.update (dbVar copts) $ return $ fromModule m + updateData (AddedProject p) = DB.update (dbVar copts) $ return $ fromProject p +runCommand copts (Scan projs cabals fs paths fcts ghcs' docs' infer') = runCommandM $ do + sboxes <- getSandboxes copts cabals + updateProcess copts ghcs' docs' infer' $ concat [ + map (\(FileContents f cts) -> Update.scanFileContents ghcs' f (Just cts)) fcts, + map (Update.scanProject ghcs') projs, + map (Update.scanFile ghcs') fs, + map (Update.scanDirectory ghcs') paths, + map (Update.scanCabal ghcs') sboxes] +runCommand copts (RefineDocs projs fs ms) = runCommandM $ do + projects <- traverse (findProject copts) projs + dbval <- getDb copts + let + filters = anyOf $ concat [ + map inProject projects, + map inFile fs, + map inModule ms] + mods = selectModules (filters . view moduleId) dbval + updateProcess copts [] False False [Update.scanDocs $ map (getInspected dbval) mods] +runCommand copts (InferTypes projs fs ms) = runCommandM $ do + projects <- traverse (findProject copts) projs + dbval <- getDb copts + let + filters = anyOf $ concat [ + map inProject projects, + map inFile fs, + map inModule ms] + mods = selectModules (filters . view moduleId) dbval + updateProcess copts [] False False [Update.inferModTypes $ map (getInspected dbval) mods] +runCommand copts (Remove projs packages cabals fs) = undefined +runCommand copts (InfoModules f) = runCommandM $ do + dbval <- getDb copts + filter' <- targetFilter copts f + return $ map (view moduleId) $ newestPackage $ selectModules (filter' . view moduleId) dbval +runCommand copts InfoPackages = runCommandM $ + (ordNub . sort . mapMaybe (preview (moduleLocation . modulePackage . _Just)) . allModules) <$> getDb copts +runCommand copts InfoProjects = runCommandM $ (toList . databaseProjects) <$> getDb copts +runCommand copts InfoSandboxes = runCommandM $ + (ordNub . sort . mapMaybe (cabalOf . view moduleId) . allModules) <$> getDb copts +runCommand copts (InfoSymbol sq f) = runCommandM $ do + dbval <- liftM (localsDatabase False) $ getDb copts -- FIXME: Where is arg locals? + filter' <- targetFilter copts f + return $ newestPackage $ filterMatch sq $ filter (checkModule filter') $ allDeclarations dbval +runCommand copts (InfoModule sq f) = runCommandM $ do + dbval <- liftM (localsDatabase False) $ getDb copts -- FIXME: Where is arg locals? + filter' <- targetFilter copts f + return $ newestPackage $ filterMatch sq $ filter (filter' . view moduleId) $ allModules dbval +runCommand copts (InfoResolve fpath exports) = runCommandM $ do + dbval <- liftM (localsDatabase False) $ getDb copts -- FIXME: Where is arg locals? + cabal <- liftIO $ getSandbox fpath + let + cabaldb = filterDB (restrictCabal cabal) (const True) dbval + getScope + | exports = exportsModule + | otherwise = scopeModule + case lookupFile fpath dbval of + Nothing -> commandError "File not found" [] + Just m -> return $ getScope $ resolveOne cabaldb m +runCommand copts (InfoProject (Left projName)) = runCommandM $ findProject copts projName +runCommand copts (InfoProject (Right projPath)) = runCommandM $ liftIO $ searchProject projPath +runCommand copts (InfoSandbox sandbox) = runCommandM $ liftIO $ searchSandbox sandbox +runCommand copts (Lookup nm fpath) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + mapCommandErrorStr $ lookupSymbol dbval cabal fpath nm +runCommand copts (Whois nm fpath) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + mapCommandErrorStr $ whois dbval cabal fpath nm +runCommand copts (ResolveScopeModules fpath) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + liftM (map (view moduleId)) $ mapCommandErrorStr $ scopeModules dbval cabal fpath +runCommand copts (ResolveScope sq global fpath) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + liftM (filterMatch sq) $ mapCommandErrorStr $ scope dbval cabal fpath global +runCommand copts (Complete input wide fpath) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + mapCommandErrorStr $ completions dbval cabal fpath input wide +runCommand copts (Hayoo hq p ps) = runCommandM $ liftM concat $ forM [p .. p + pred ps] $ \i -> liftM + (mapMaybe Hayoo.hayooAsDeclaration . Hayoo.resultResult) $ + mapCommandErrorStr $ Hayoo.hayoo hq (Just i) +runCommand copts (CabalList packages) = runCommandM $ mapCommandErrorStr $ Cabal.cabalList packages +runCommand copts (Lint fs fcts) = runCommandM $ do + mapCommandErrorStr $ liftM2 (++) + (liftM concat $ mapM HLint.hlintFile fs) + (liftM concat $ mapM (\(FileContents f c) -> HLint.hlintSource f c) fcts) +runCommand copts (Check fs fcts ghcs') = runCommandM $ do + db <- getDb copts + let + checkSome file fn = do + cabal <- liftIO $ getSandbox file + m <- maybe + (commandError_ $ "File '" ++ file ++ "' not found") + return + (lookupFile file db) + notes <- inWorkerWith (commandError_ . show) (commandGhc copts) + (runExceptT $ fn cabal m) + either commandError_ return notes + liftM concat $ mapM (uncurry checkSome) $ + [(f, Check.checkFile ghcs') | f <- fs] ++ + [(f, \cabal m -> Check.checkSource ghcs' cabal m src) | FileContents f src <- fcts] +runCommand copts (CheckLint fs fcts ghcs') = runCommandM $ do + db <- getDb copts + let + checkSome file fn = do + cabal <- liftIO $ getSandbox file + m <- maybe + (commandError_ $ "File '" ++ file ++ "' not found") + return + (lookupFile file db) + notes <- inWorkerWith (commandError_ . show) (commandGhc copts) + (runExceptT $ fn cabal m) + either commandError_ return notes + checkMsgs <- liftM concat $ mapM (uncurry checkSome) $ + [(f, Check.checkFile ghcs') | f <- fs] ++ + [(f, \cabal m -> Check.checkSource ghcs' cabal m src) | FileContents f src <- fcts] + lintMsgs <- mapCommandErrorStr $ liftM2 (++) + (liftM concat $ mapM HLint.hlintFile fs) + (liftM concat $ mapM (\(FileContents f src) -> HLint.hlintSource f src) fcts) + return $ checkMsgs ++ lintMsgs +runCommand copts (Types fs fcts ghcs') = runCommandM $ do + db <- getDb copts + let + cts = [(f, Nothing) | f <- fs] ++ [(f, Just src) | FileContents f src <- fcts] + liftM concat $ forM cts $ \(file, msrc) -> do + cabal <- liftIO $ getSandbox file + m <- maybe + (commandError_ $ "File '" ++ file ++ "' not found") + return + (lookupFile file db) + notes <- inWorkerWith (commandError_ . show) (commandGhc copts) + (runExceptT $ Types.fileTypes ghcs' cabal m msrc) + either commandError_ return notes +runCommand copts (GhcMod GhcModLang) = runCommandM $ mapCommandErrorStr GhcMod.langs +runCommand copts (GhcMod GhcModFlags) = runCommandM $ mapCommandErrorStr GhcMod.flags +runCommand copts (GhcMod (GhcModType (Position line column) fpath ghcs')) = runCommandM $ do + dbval <- getDb copts + cabal <- liftIO $ getSandbox fpath + (fpath', m', _) <- mapCommandErrorStr $ fileCtx dbval fpath + mapCommandErrorStr $ GhcMod.waitMultiGhcMod (commandGhcMod copts) fpath' $ + GhcMod.typeOf (ghcs' ++ moduleOpts (allPackages dbval) m') cabal fpath' line column +runCommand copts (GhcMod (GhcModLint fs hlints')) = runCommandM $ do + mapCommandErrorStr $ liftM concat $ forM fs $ \file -> + GhcMod.waitMultiGhcMod (commandGhcMod copts) file $ + GhcMod.lint hlints' file +runCommand copts (GhcMod (GhcModCheck fs ghcs')) = runCommandM $ do + dbval <- getDb copts + mapCommandErrorStr $ liftM concat $ forM fs $ \file -> do + mproj <- liftIO $ locateProject file + cabal <- liftIO $ getSandbox file + (_, m', _) <- fileCtx dbval file + GhcMod.waitMultiGhcMod (commandGhcMod copts) file $ + GhcMod.check (ghcs' ++ moduleOpts (allPackages dbval) m') cabal [file] mproj +runCommand copts (GhcMod (GhcModCheckLint fs ghcs' hlints')) = runCommandM $ do + dbval <- getDb copts + mapCommandErrorStr $ liftM concat $ forM fs $ \file -> do + mproj <- liftIO $ locateProject file + cabal <- liftIO $ getSandbox file + (_, m', _) <- fileCtx dbval file + GhcMod.waitMultiGhcMod (commandGhcMod copts) file $ do + checked <- GhcMod.check (ghcs' ++ moduleOpts (allPackages dbval) m') cabal [file] mproj + linted <- GhcMod.lint hlints' file + return $ checked ++ linted +runCommand copts (AutoFix (AutoFixShow ns)) = runCommandM $ return $ AutoFix.corrections ns +runCommand copts (AutoFix (AutoFixFix ns rest isPure)) = runCommandM $ do + files <- liftM (ordNub . sort) $ mapM (findPath copts) $ mapMaybe (preview $ Tools.noteSource . moduleFile) ns + let + doFix :: FilePath -> String -> ([Tools.Note AutoFix.Correction], String) + doFix file cts = AutoFix.edit cts fUpCorrs $ do + AutoFix.autoFix fCorrs + gets (view AutoFix.regions) + where + findCorrs :: FilePath -> [Tools.Note AutoFix.Correction] -> [Tools.Note AutoFix.Correction] + findCorrs f = filter ((== Just f) . preview (Tools.noteSource . moduleFile)) + fCorrs = map (view Tools.note) $ findCorrs file ns + fUpCorrs = findCorrs file rest + runFix file + | isPure = return $ fst $ doFix file "" + | otherwise = do + (corrs', cts') <- liftM (doFix file) $ liftE $ readFileUtf8 file + liftE $ writeFileUtf8 file cts' + return corrs' + mapCommandErrorStr $ liftM concat $ mapM runFix files +runCommand copts (GhcEval exprs) = runCommandM $ mapCommandErrorStr $ liftM (map toValue) $ liftAsync $ + pushTask (commandGhci copts) $ mapM (try . evaluate) exprs + where + toValue :: Either SomeException String -> Value + toValue (Left (SomeException e)) = object ["fail" .= show e] + toValue (Right s) = toJSON s +runCommand copts (Link hold) = runCommandM $ liftIO $ commandLink copts >> when hold (commandHold copts) +runCommand copts Exit = runCommandM $ liftIO $ commandExit copts + +targetFilter :: MonadIO m => CommandOptions -> TargetFilter -> ExceptT CommandError m (ModuleId -> Bool) +targetFilter copts f = case f of + TargetProject proj -> liftM inProject $ findProject copts proj + TargetFile file -> return $ inFile file + TargetModule mname -> return $ inModule mname + TargetDepsOf dep -> liftM inDeps $ findDep copts dep + TargetCabal cabal -> liftM inCabal $ findSandbox copts cabal + TargetPackage pack -> return $ inPackage pack + TargetSourced -> return byFile + TargetStandalone -> return standalone + TargetAny -> return (const True) + +-- Helper functions + +commandStrMsg :: String -> CommandError +commandStrMsg m = CommandError m [] + +-- | Find sandbox by path +findSandbox :: MonadIO m => CommandOptions -> Cabal -> ExceptT CommandError m Cabal +findSandbox copts Cabal = return Cabal +findSandbox copts (Sandbox f) = (findPath copts >=> mapCommandErrorStr . liftIO . getSandbox) f + +-- | Canonicalize paths +findPath :: (MonadIO m, Paths a) => CommandOptions -> a -> ExceptT e m a +findPath copts = paths findPath' where + findPath' :: MonadIO m => FilePath -> ExceptT e m FilePath + findPath' f = liftIO $ canonicalizePath (normalise f') where + f' + | isRelative f = commandRoot copts </> f + | otherwise = f + +-- | Get list of enumerated sandboxes +getSandboxes :: (MonadIO m, Functor m) => CommandOptions -> [Cabal] -> ExceptT CommandError m [Cabal] +getSandboxes copts = traverse (findSandbox copts) + +-- | Find project by name of path +findProject :: MonadIO m => CommandOptions -> String -> ExceptT CommandError m Project +findProject copts proj = do + db' <- getDb copts + proj' <- liftM addCabal $ findPath copts proj + let + resultProj = + refineProject db' (project proj') <|> + find ((== proj) . view projectName) (databaseProjects db') + maybe (throwError $ commandStrMsg $ "Projects " ++ proj ++ " not found") return resultProj + where + addCabal p + | takeExtension p == ".cabal" = p + | otherwise = p </> (takeBaseName p <.> "cabal") + +-- | Find dependency: it may be source, project file or project name, also returns sandbox found +findDep :: MonadIO m => CommandOptions -> String -> ExceptT CommandError m (Project, Maybe FilePath, Cabal) +findDep copts depName = do + depPath <- findPath copts depName + proj <- msum [ + mapCommandErrorStr $ do + p <- liftIO (locateProject depPath) + maybe (throwError $ "Project " ++ depName ++ " not found") (mapExceptT liftIO . loadProject) p, + findProject copts depName] + let + src + | takeExtension depPath == ".hs" = Just depPath + | otherwise = Nothing + sbox <- liftIO $ searchSandbox $ view projectPath proj + return (proj, src, sbox) + +-- | Check if project or source depends from this module +inDeps :: (Project, Maybe FilePath, Cabal) -> ModuleId -> Bool +inDeps (proj, src, cabal) = liftM2 (&&) (restrictCabal cabal) deps' where + deps' = case src of + Nothing -> inDepsOfProject proj + Just src' -> inDepsOfFile proj src' + +cacheLoad :: CommandOptions -> IO (Either String Database) -> CommandM () +cacheLoad copts act = liftIO $ do + db' <- act + case db' of + Left e -> commandLog copts Log.Error e + Right database -> DB.update (dbVar copts) (return database) + +-- | Bring locals to top scope to search within them if 'locals' flag set +localsDatabase :: Bool -> Database -> Database +localsDatabase True = databaseLocals +localsDatabase False = id + +-- | Select newest packages if 'no-last' flag not set +newest :: Symbol a => Bool -> [a] -> [a] +newest True = newestPackage +newest False = id + +-- | Convert from just of throw +forceJust :: MonadIO m => String -> ExceptT CommandError m (Maybe a) -> ExceptT CommandError m a +forceJust msg act = act >>= maybe (throwError $ commandStrMsg msg) return + +-- | Get actual DB state +getDb :: MonadIO m => CommandOptions -> m Database +getDb = liftIO . DB.readAsync . commandDatabase + +-- | Get DB async var +dbVar :: CommandOptions -> DB.Async Database +dbVar = commandDatabase + +mapCommandErrorStr :: (Monad m) => ExceptT String m a -> ExceptT CommandError m a +mapCommandErrorStr = mapExceptT (liftM $ left commandStrMsg) + +-- | Run DB update action +updateProcess :: CommandOptions -> [String] -> Bool -> Bool -> [ExceptT String (Update.UpdateDB IO) ()] -> CommandM () +updateProcess copts ghcOpts' docs' infer' acts = lift $ Update.updateDB (Update.settings copts ghcOpts' docs' infer') $ sequence_ [act `catchError` logErr | act <- acts] where + logErr :: String -> ExceptT String (Update.UpdateDB IO) () + logErr e = liftIO $ commandLog copts Log.Error e + +-- | Filter declarations with prefix and infix +filterMatch :: Symbol a => SearchQuery -> [a] -> [a] +filterMatch (SearchQuery q st) = filter match' where + match' m = case st of + SearchExact -> fromString q == symbolName m + SearchPrefix -> fromString q `T.isPrefixOf` symbolName m + SearchInfix -> fromString q `T.isInfixOf` symbolName m + SearchSuffix -> fromString q `T.isSuffixOf` symbolName m + SearchRegex -> unpack (symbolName m) =~ q
src/HsDev/Commands.hs view
@@ -26,12 +26,14 @@ import Control.Applicative import Control.Lens (view, set, each) import Control.Monad.Except +import Data.List (delete) import Data.Maybe -import qualified Data.Map as M (lookup) import Data.String (fromString) import qualified Data.Text as T (isPrefixOf, split, unpack) import System.Directory (canonicalizePath) +import System.Directory.Paths + import HsDev.Database import HsDev.Project import HsDev.Symbols @@ -64,9 +66,9 @@ -- | Find project of module getProject :: Database -> Project -> ExceptT String IO Project getProject db p = do - p' <- liftE $ canonicalizePath $ view projectCabal p - maybe (throwError $ "Project " ++ p' ++ " not found") return $ - M.lookup p' $ databaseProjects db + p' <- liftE $ canonicalize p + maybe (throwError $ "Project " ++ view projectCabal p' ++ " not found") return $ + refineProject db p' -- | Lookup visible within project symbol lookupSymbol :: Database -> Cabal -> FilePath -> String -> ExceptT String IO [ModuleDeclaration] @@ -105,7 +107,7 @@ inProject proj, \m -> any (`inPackage` m) deps'] where - deps f p = concatMap (view infoDepends) $ fileTargets p f + deps f p = delete (view projectName p) $ concatMap (view infoDepends) $ fileTargets p f -- | Symbols in scope scope :: Database -> Cabal -> FilePath -> Bool -> ExceptT String IO [ModuleDeclaration]
src/HsDev/Database.hs view
@@ -6,7 +6,7 @@ fromModule, fromProject, filterDB, projectDB, cabalDB, standaloneDB, - selectModules, selectDeclarations, lookupModule, lookupFile, refineProject, + selectModules, selectDeclarations, lookupModule, lookupInspected, lookupFile, refineProject, getInspected, append, remove, @@ -17,11 +17,12 @@ Map ) where -import Control.Lens (set, view, preview, _Just) +import Control.Lens (set, view, preview, _Just, each, (^..)) import Control.Monad (msum, join) import Control.DeepSeq (NFData(..)) import Data.Aeson import Data.Either (rights) +import Data.Foldable (find) import Data.Function (on) import Data.Group (Group(..)) import Data.Map (Map) @@ -30,12 +31,12 @@ import HsDev.Symbols import HsDev.Symbols.Util -import HsDev.Util ((.::), ordNub) +import HsDev.Util ((.::), ordNub, mapBy) -- | HsDev database data Database = Database { - databaseModules :: Map ModuleLocation InspectedModule, - databaseProjects :: Map FilePath Project } + databaseModules :: [InspectedModule], + databaseProjects :: [Project] } deriving (Eq, Ord) instance NFData Database where @@ -43,14 +44,16 @@ instance Group Database where add old new = Database { - databaseModules = databaseModules new `M.union` databaseModules old, - databaseProjects = M.unionWith mergeProject (databaseProjects new) (databaseProjects old) } + databaseModules = M.elems $ mapBy (view inspectedId) (databaseModules new) `M.union` mapBy (view inspectedId) (databaseModules old), + databaseProjects = M.elems $ M.unionWith mergeProject + (mapBy (view projectCabal) (databaseProjects new)) + (mapBy (view projectCabal) (databaseProjects old)) } where mergeProject pl pr = set projectDescription (msum [view projectDescription pl, view projectDescription pr]) pl sub old new = Database { - databaseModules = databaseModules old `M.difference` databaseModules new, - databaseProjects = databaseProjects old `M.difference` databaseProjects new } - zero = Database M.empty M.empty + databaseModules = M.elems $ mapBy (view inspectedId) (databaseModules old) `M.difference` mapBy (view inspectedId) (databaseModules new), + databaseProjects = M.elems $ mapBy (view projectCabal) (databaseProjects old) `M.difference` mapBy (view projectCabal) (databaseProjects new) } + zero = Database [] [] instance Monoid Database where mempty = zero @@ -58,35 +61,32 @@ instance ToJSON Database where toJSON (Database ms ps) = object [ - "modules" .= M.elems ms, - "projects" .= M.elems ps] + "modules" .= ms, + "projects" .= ps] instance FromJSON Database where parseJSON = withObject "database" $ \v -> Database <$> - ((M.unions . map mkModule) <$> v .:: "modules") <*> - ((M.unions . map mkProject) <$> v .:: "projects") - where - mkModule m = M.singleton (view inspectedId m) m - mkProject p = M.singleton (view projectCabal p) p + (v .:: "modules") <*> + (v .:: "projects") -- | Database intersection, prefers first database data databaseIntersection :: Database -> Database -> Database databaseIntersection l r = mempty { - databaseModules = databaseModules l `M.intersection` databaseModules r, - databaseProjects = databaseProjects l `M.intersection` databaseProjects r } + databaseModules = M.elems $ mapBy (view inspectedId) (databaseModules l) `M.intersection` mapBy (view inspectedId) (databaseModules r), + databaseProjects = M.elems $ mapBy (view projectCabal) (databaseProjects l) `M.intersection` mapBy (view projectCabal) (databaseProjects r) } -- | Check if database is empty nullDatabase :: Database -> Bool -nullDatabase db = M.null (databaseModules db) && M.null (databaseProjects db) +nullDatabase db = null (databaseModules db) && null (databaseProjects db) -- | Bring all locals to scope databaseLocals :: Database -> Database databaseLocals db = db { - databaseModules = M.map (fmap moduleLocals) (databaseModules db) } + databaseModules = fmap (fmap moduleLocals) (databaseModules db) } -- | All modules allModules :: Database -> [Module] -allModules = rights . map (view inspectionResult) . M.elems . databaseModules +allModules = rights . fmap (view inspectionResult) . databaseModules -- | All declarations allDeclarations :: Database -> [ModuleDeclaration] @@ -101,18 +101,18 @@ -- | Make database from module fromModule :: InspectedModule -> Database fromModule m = zero { - databaseModules = M.singleton (view inspectedId m) m } + databaseModules = [m] } -- | Make database from project fromProject :: Project -> Database fromProject p = zero { - databaseProjects = M.singleton (view projectCabal p) p } + databaseProjects = [p] } -- | Filter database by predicate filterDB :: (ModuleId -> Bool) -> (Project -> Bool) -> Database -> Database filterDB m p db = mempty { - databaseModules = M.filter (either (const False) (m . view moduleId) . view inspectionResult) (databaseModules db), - databaseProjects = M.filter p (databaseProjects db) } + databaseModules = filter (either (const False) (m . view moduleId) . view inspectionResult) (databaseModules db), + databaseProjects = filter p (databaseProjects db) } -- | Project database projectDB :: Project -> Database -> Database @@ -124,7 +124,7 @@ -- | Standalone database standaloneDB :: Database -> Database -standaloneDB db = filterDB check' (const False) db where +standaloneDB = filterDB check' (const False) where check' m = standalone m && byFile m -- | Select module by predicate @@ -138,20 +138,24 @@ -- | Lookup module by its location and name lookupModule :: ModuleLocation -> Database -> Maybe Module lookupModule mloc db = do - m <- M.lookup mloc $ databaseModules db + m <- find ((== mloc) . view inspectedId) $ databaseModules db either (const Nothing) Just $ view inspectionResult m +-- | Lookup inspected module +lookupInspected :: ModuleLocation -> Database -> Maybe InspectedModule +lookupInspected mloc db = find ((== mloc) . view inspectedId) $ databaseModules db + -- | Lookup module by its source file lookupFile :: FilePath -> Database -> Maybe Module lookupFile f = listToMaybe . selectModules (inFile f . view moduleId) -- | Refine project refineProject :: Database -> Project -> Maybe Project -refineProject db proj = M.lookup (view projectCabal proj) $ databaseProjects db +refineProject db proj = find ((== view projectCabal proj) . view projectCabal) $ databaseProjects db -- | Get inspected module getInspected :: Database -> Module -> InspectedModule -getInspected db m = fromMaybe err $ M.lookup (view moduleLocation m) $ databaseModules db where +getInspected db m = fromMaybe err $ find ((== view moduleLocation m) . view inspectedId) $ databaseModules db where err = error "Impossible happened: getInspected" -- | Append database @@ -219,7 +223,7 @@ keyProj db = unique "No project" "Different module projects" - (return (M.keys (databaseProjects db))) + (return (databaseProjects db ^.. each . projectCabal)) -- Check that list results in one element unique :: (Eq a) => String -> String -> Either String [a] -> Either String a unique _ _ (Left e) = Left e @@ -230,7 +234,7 @@ structurize :: Database -> Structured structurize db = Structured cs ps fs where cs = M.fromList [(c, cabalDB c db) | c <- ordNub (mapMaybe modCabal (allModules db))] - ps = M.fromList [(pname, projectDB (project pname) db) | pname <- M.keys (databaseProjects db)] + ps = M.fromList [(pname, projectDB (project pname) db) | pname <- (databaseProjects db ^.. each . projectCabal)] fs = standaloneDB db merge :: Structured -> Database
src/HsDev/Database/Update.hs view
@@ -35,9 +35,9 @@ import Control.Monad.Trans.Control import Data.Aeson import Data.Aeson.Types -import qualified Data.HashMap.Strict as HM +import Data.Foldable (toList) import qualified Data.Map as M -import Data.Maybe (mapMaybe, isJust, fromMaybe, catMaybes) +import Data.Maybe (mapMaybe, isJust, fromMaybe) import qualified Data.Text as T (unpack) import System.Directory (canonicalizePath, doesFileExist) import qualified System.Log.Simple as Log @@ -48,7 +48,7 @@ import HsDev.Database import HsDev.Database.Async hiding (Event) import HsDev.Display -import HsDev.Inspect (inspectDocs, inspectDocsGhc) +import HsDev.Inspect (inspectDocs, inspectDocsGhc, getDefines) import HsDev.Project import HsDev.Symbols import HsDev.Tools.Ghc.Worker (ghcWorker) @@ -89,7 +89,7 @@ getMods :: (MonadIO m) => m [InspectedModule] getMods = do db' <- liftIO $ readAsync $ database sets - return $ catMaybes [M.lookup mloc' (databaseModules db') | mloc' <- mlocs'] + return $ filter ((`elem` mlocs') . view inspectedId) $ toList $ databaseModules db' when (updateDocs sets) $ do Log.log Log.Trace "forking inspecting source docs" void $ fork (getMods >>= waiter . mapM_ scanDocs_) @@ -115,7 +115,7 @@ postStatus :: (MonadIO m, MonadReader Settings m) => Task -> m () postStatus s = do on' <- asks onStatus - liftIO $ on' s + liftIO $ on' [s] -- | Wait DB to complete actions waiter :: (MonadIO m, MonadReader Settings m) => m () -> m () @@ -159,11 +159,11 @@ return dbval -- | Run one task -runTask :: MonadIO m => String -> [Pair] -> ExceptT String (UpdateDB m) a -> ExceptT String (UpdateDB m) a -runTask action params act = do +runTask :: (Display t, MonadIO m, NFData a, MonadCatchIO m) => String -> t -> ExceptT String (UpdateDB m) a -> ExceptT String (UpdateDB m) a +runTask action subj act = Log.scope "task" $ do postStatus $ task { taskStatus = StatusWorking } x <- local childTask act - postStatus $ task { taskStatus = StatusOk } + x `deepseq` postStatus (task { taskStatus = StatusOk }) return x `catchError` (\e -> postStatus (task { taskStatus = StatusError e }) >> throwError e) @@ -171,11 +171,11 @@ task = Task { taskName = action, taskStatus = StatusWorking, - taskParams = HM.fromList params, - taskProgress = Nothing, - taskChild = Nothing } + taskSubjectType = displayType subj, + taskSubjectName = display subj, + taskProgress = Nothing } childTask st = st { - onStatus = \t -> onStatus st (task { taskChild = Just t }) } + onStatus = \t -> onStatus st (task : t) } -- | Run many tasks with numeration runTasks :: Monad m => [ExceptT String (UpdateDB m) ()] -> ExceptT String (UpdateDB m) () @@ -183,7 +183,7 @@ total = length ts taskNum n = local setProgress where setProgress st = st { - onStatus = \t -> onStatus st (t { taskProgress = Just (Progress n total) }) } + onStatus = \(t:tl) -> onStatus st ((t { taskProgress = Just (Progress n total) }) : tl) } noErr v = v `mplus` return () -- | Get database value @@ -191,15 +191,16 @@ readDB = asks database >>= liftIO . readAsync -- | Scan module -scanModule :: (MonadIO m, MonadCatch m) => [String] -> ModuleLocation -> Maybe String -> ExceptT String (UpdateDB m) () -scanModule opts mloc mcts = runTask "scanning" (subject mloc ["module" .= mloc]) $ do - im <- liftExceptT $ S.scanModule opts mloc mcts +scanModule :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> ModuleLocation -> Maybe String -> ExceptT String (UpdateDB m) () +scanModule opts mloc mcts = runTask "scanning" mloc $ Log.scope "module" $ do + defs <- asks settingsDefines + im <- liftExceptT $ S.scanModule defs opts mloc mcts updater $ return $ fromModule im _ <- ExceptT $ return $ view inspectionResult im return () -- | Scan modules -scanModules :: (MonadIO m, MonadCatch m) => [String] -> [S.ModuleToScan] -> ExceptT String (UpdateDB m) () +scanModules :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> [S.ModuleToScan] -> ExceptT String (UpdateDB m) () scanModules opts ms = runTasks $ [scanProjectFile opts p >> return () | p <- ps] ++ [scanModule (opts ++ mopts) m mcts | (m, mopts, mcts) <- ms] @@ -209,12 +210,12 @@ toProj _ = Nothing -- | Scan source file -scanFile :: (MonadIO m, MonadCatch m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () +scanFile :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () scanFile opts fpath = scanFileContents opts fpath Nothing -- | Scan source file with contents -scanFileContents :: (MonadIO m, MonadCatch m) => [String] -> FilePath -> Maybe String -> ExceptT String (UpdateDB m) () -scanFileContents opts fpath mcts = do +scanFileContents :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> FilePath -> Maybe String -> ExceptT String (UpdateDB m) () +scanFileContents opts fpath mcts = Log.scope "file" $ do dbval <- readDB fpath' <- liftEIO $ canonicalizePath fpath ex <- liftEIO $ doesFileExist fpath' @@ -238,28 +239,25 @@ inFile f = maybe False (== f) . preview (moduleIdLocation . moduleFile) -- | Scan cabal modules -scanCabal :: (MonadIO m, MonadCatch m) => [String] -> Cabal -> ExceptT String (UpdateDB m) () -scanCabal opts cabalSandbox = runTask "scanning" (subject cabalSandbox ["sandbox" .= cabalSandbox]) $ do +scanCabal :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> Cabal -> ExceptT String (UpdateDB m) () +scanCabal opts cabalSandbox = runTask "scanning" cabalSandbox $ Log.scope "cabal" $ do watch (\w -> watchSandbox w cabalSandbox opts) - mlocs <- runTask "getting list of cabal modules" [] $ - liftExceptT $ listModules opts cabalSandbox + mlocs <- liftExceptT $ listModules opts cabalSandbox scan (Cache.loadCabal cabalSandbox) (cabalDB cabalSandbox) ((,,) <$> mlocs <*> pure [] <*> pure Nothing) opts $ \mlocs' -> do - ms <- runTask "loading modules" [] $ - liftExceptT $ browseModules opts cabalSandbox (mlocs' ^.. each . _1) - docs <- runTask "loading docs" [] $ - liftExceptT $ hdocsCabal cabalSandbox opts + ms <- liftExceptT $ browseModules opts cabalSandbox (mlocs' ^.. each . _1) + docs <- liftExceptT $ hdocsCabal cabalSandbox opts updater $ return $ mconcat $ map (fromModule . fmap (setDocs' docs)) ms where setDocs' :: Map String (Map String String) -> Module -> Module setDocs' docs m = maybe m (`setDocs` m) $ M.lookup (T.unpack $ view moduleName m) docs -- | Scan project file -scanProjectFile :: (MonadIO m, MonadCatch m) => [String] -> FilePath -> ExceptT String (UpdateDB m) Project -scanProjectFile opts cabal = runTask "scanning" (subject cabal ["file" .= cabal]) $ liftExceptT $ S.scanProjectFile opts cabal +scanProjectFile :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> FilePath -> ExceptT String (UpdateDB m) Project +scanProjectFile opts cabal = runTask "scanning" cabal $ liftExceptT $ S.scanProjectFile opts cabal -- | Scan project -scanProject :: (MonadIO m, MonadCatch m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () -scanProject opts cabal = runTask "scanning" (subject (project cabal) ["project" .= cabal]) $ do +scanProject :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () +scanProject opts cabal = runTask "scanning" (project cabal) $ Log.scope "project" $ do proj <- scanProjectFile opts cabal watch (\w -> watchProject w proj opts) (_, sources) <- liftExceptT $ S.enumProject proj @@ -268,10 +266,9 @@ updater $ return $ fromProject proj -- | Scan directory for source files and projects -scanDirectory :: (MonadIO m, MonadCatch m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () -scanDirectory opts dir = runTask "scanning" (subject dir ["path" .= dir]) $ do - S.ScanContents standSrcs projSrcs sboxes <- runTask "getting list of sources" [] $ - liftExceptT $ S.enumDirectory dir +scanDirectory :: (MonadIO m, MonadCatch m, MonadCatchIO m) => [String] -> FilePath -> ExceptT String (UpdateDB m) () +scanDirectory opts dir = runTask "scanning" dir $ Log.scope "directory" $ do + S.ScanContents standSrcs projSrcs sboxes <- liftExceptT $ S.enumDirectory dir runTasks [scanProject opts (view projectCabal p) | (p, _) <- projSrcs] runTasks $ map (scanCabal opts) sboxes mapMOf_ (each . _1) (watch . flip watchModule) standSrcs @@ -285,21 +282,21 @@ w <- liftIO $ ghcWorker ["-haddock"] (return ()) runTasks $ map (scanDocs' w) ims where - scanDocs' w im = runTask "scanning docs" (subject (view inspectedId im) []) $ do - Log.log Log.Trace $ "Scanning docs for $" ~~ view inspectedId im + scanDocs' w im = runTask "scanning docs" (view inspectedId im) $ Log.scope "docs" $ do + Log.log Log.Trace $ "Scanning docs for {}" ~~ view inspectedId im im' <- liftExceptT $ S.scanModify (\opts _ -> inWorkerT w . inspectDocsGhc opts) im - Log.log Log.Trace $ "Docs for $ updated" ~~ view inspectedId im + Log.log Log.Trace $ "Docs for {} updated" ~~ view inspectedId im updater $ return $ fromModule im' inWorkerT w = ExceptT . inWorker w . runExceptT inferModTypes :: (MonadIO m, MonadCatchIO m) => [InspectedModule] -> ExceptT String (UpdateDB m) () inferModTypes = runTasks . map inferModTypes' where - inferModTypes' im = runTask "inferring types" (subject (view inspectedId im) []) $ do + inferModTypes' im = runTask "inferring types" (view inspectedId im) $ Log.scope "infer" $ do -- TODO: locate sandbox sets <- ask - Log.log Log.Trace $ "Inferring types for $" ~~ view inspectedId im + Log.log Log.Trace $ "Inferring types for {}" ~~ view inspectedId im im' <- liftExceptT $ S.scanModify (infer' sets) im - Log.log Log.Trace $ "Types for $ inferred" ~~ view inspectedId im + Log.log Log.Trace $ "Types for {} inferred" ~~ view inspectedId im updater $ return $ fromModule im' infer' :: Settings -> [String] -> Cabal -> Module -> ExceptT String IO Module infer' sets opts cabal m = case preview (moduleLocation . moduleFile) m of @@ -308,7 +305,7 @@ inferTypes opts cabal m -- | Generic scan function. Reads cache only if data is not already loaded, removes obsolete modules and rescans changed modules. -scan :: (MonadIO m, MonadCatch m) +scan :: (MonadIO m, MonadCatch m, MonadCatchIO m) => (FilePath -> ExceptT String IO Structured) -- ^ Read data from cache -> (Database -> Database) @@ -320,20 +317,20 @@ -> ([S.ModuleToScan] -> ExceptT String (UpdateDB m) ()) -- ^ Function to update changed modules -> ExceptT String (UpdateDB m) () -scan cache' part' mlocs opts act = do +scan cache' part' mlocs opts act = Log.scope "scan" $ do dbval <- getCache cache' part' let obsolete = filterDB (\m -> view moduleIdLocation m `notElem` (mlocs ^.. each . _1)) (const False) dbval - changed <- runTask "getting list of changed modules" [] $ liftExceptT $ S.changedModules dbval opts mlocs - runTask "removing obsolete modules" ["modules" .= map (view moduleLocation) (allModules obsolete)] $ cleaner $ return obsolete + changed <- liftExceptT $ S.changedModules dbval opts mlocs + cleaner $ return obsolete act changed updateEvent :: (MonadIO m, MonadCatch m, MonadCatchIO m) => Watched -> Event -> ExceptT String (UpdateDB m) () updateEvent (WatchedProject proj projOpts) e | isSource e = do - Log.log Log.Info $ "File '${file}' in project ${proj} changed" ~~ - ("file" %= view eventPath e) % - ("proj" %= view projectName proj) + Log.log Log.Info $ "File '{file}' in project {proj} changed" + ~~ ("file" %= view eventPath e) + ~~ ("proj" %= view projectName proj) dbval <- readDB let opts = fromMaybe [] $ do @@ -341,17 +338,20 @@ preview (inspection . inspectionOpts) $ getInspected dbval m scanFile opts $ view eventPath e | isCabal e = do - Log.log Log.Info $ "Project ${proj} changed" ~~ ("proj" %= view projectName proj) + Log.log Log.Info $ "Project {proj} changed" + ~~ ("proj" %= view projectName proj) scanProject projOpts $ view projectCabal proj | otherwise = return () updateEvent (WatchedSandbox cabal cabalOpts) e | isConf e = do - Log.log Log.Info $ "Sandbox ${cabal} changed" ~~ ("cabal" %= cabal) + Log.log Log.Info $ "Sandbox {cabal} changed" + ~~ ("cabal" %= cabal) scanCabal cabalOpts cabal | otherwise = return () updateEvent WatchedModule e | isSource e = do - Log.log Log.Info $ "Module ${file} changed" ~~ ("file" %= view eventPath e) + Log.log Log.Info $ "Module {file} changed" + ~~ ("file" %= view eventPath e) dbval <- readDB let opts = fromMaybe [] $ do @@ -366,9 +366,6 @@ -- | Lift errors liftExceptT :: MonadIO m => ExceptT String IO a -> ExceptT String m a liftExceptT = mapExceptT liftIO - -subject :: Display a => a -> [Pair] -> [Pair] -subject x ps = ["name" .= display x, "type" .= displayType x] ++ ps watch :: (MonadIO m, MonadReader Settings m) => (Watcher -> IO ()) -> m () watch f = do
src/HsDev/Database/Update/Types.hs view
@@ -24,7 +24,6 @@ import HsDev.Symbols import HsDev.Util ((.::)) import HsDev.Watcher.Types -import System.Console.Args data Status = StatusWorking | StatusOk | StatusError String @@ -55,50 +54,52 @@ data Task = Task { taskName :: String, taskStatus :: Status, - taskParams :: Object, - taskProgress :: Maybe Progress, - taskChild :: Maybe Task } + taskSubjectType :: String, + taskSubjectName :: String, + taskProgress :: Maybe Progress } instance ToJSON Task where toJSON t = object [ - "status" .= taskStatus t, "task" .= taskName t, - "params" .= taskParams t, - "progress" .= taskProgress t, - "child" .= taskChild t] + "status" .= taskStatus t, + "type" .= taskSubjectType t, + "name" .= taskSubjectName t, + "progress" .= taskProgress t] instance FromJSON Task where parseJSON = withObject "task" $ \v -> Task <$> (v .:: "task") <*> (v .:: "status") <*> - (v .:: "params") <*> - (v .:: "progress") <*> - (v .:: "child") + (v .:: "type") <*> + (v .:: "name") <*> + (v .:: "progress") data Settings = Settings { database :: Async Database, databaseCacheReader :: (FilePath -> ExceptT String IO Structured) -> IO (Maybe Database), databaseCacheWriter :: Database -> IO (), - onStatus :: Task -> IO (), + onStatus :: [Task] -> IO (), ghcOptions :: [String], updateDocs :: Bool, runInferTypes :: Bool, settingsGhcModWorker :: Worker (ReaderT WorkerMap IO), settingsLogger :: Log.Log, - settingsWatcher :: Watcher } + settingsWatcher :: Watcher, + settingsDefines :: [(String, String)] } -settings :: CommandOptions -> Opts String -> Settings -settings copts as = Settings +settings :: CommandOptions -> [String] -> Bool -> Bool -> Settings +settings copts ghcOpts' docs' infer' = Settings (commandDatabase copts) (commandReadCache copts) (commandWriteCache copts) (commandNotify copts . Notification . toJSON) - (listArg "ghc" as) - (flagSet "docs" as) - (flagSet "infer" as) + ghcOpts' + docs' + infer' (commandGhcMod copts) (commandLogger copts) (commandWatcher copts) + (commandDefines copts) newtype UpdateDB m a = UpdateDB { runUpdateDB :: ReaderT Settings (WriterT [ModuleLocation] m) a } deriving (Applicative, Monad, MonadIO, MonadCatchIO, MonadThrow, MonadCatch, Functor, MonadReader Settings, MonadWriter [ModuleLocation])
src/HsDev/Display.hs view
@@ -37,11 +37,11 @@ display = id displayType _ = "path" -instance Format Cabal where - format = display +instance FormatBuild Cabal where + formatBuild = formatBuild . display -instance Format ModuleLocation where - format = display +instance FormatBuild ModuleLocation where + formatBuild = formatBuild . display -instance Format Project where - format = display +instance FormatBuild Project where + formatBuild = formatBuild . display
src/HsDev/Inspect.hs view
@@ -6,6 +6,8 @@ inspectFile, fileInspection, projectDirs, projectSources, inspectProject,+ getDefines,+ preprocess, preprocess_, module Control.Monad.Except ) where@@ -30,6 +32,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, getPOSIXTime) import qualified Data.Map as M import qualified Language.Haskell.Exts as H+import qualified Language.Preprocessor.Cpphs as Cpphs import qualified System.Directory as Dir import System.FilePath import Data.Generics.Uniplate.Data@@ -43,14 +46,14 @@ -- | Analize source contents analyzeModule :: [String] -> Maybe FilePath -> String -> Either String Module analyzeModule exts file source = case H.parseFileContentsWithMode (parseMode file exts) source' of- H.ParseFailed loc reason -> Left $ "Parse failed at " ++ show loc ++ ": " ++ reason- H.ParseOk (H.Module _ (H.ModuleName mname) _ _ mexports imports declarations) -> Right Module {- _moduleName = fromString mname,- _moduleDocs = Nothing,- _moduleLocation = ModuleSource Nothing,- _moduleExports = fmap (concatMap getExports) mexports,- _moduleImports = map getImport imports,- _moduleDeclarations = sortDeclarations $ getDecls declarations }+ H.ParseFailed loc reason -> Left $ "Parse failed at " ++ show loc ++ ": " ++ reason+ H.ParseOk (H.Module _ (H.ModuleName mname) _ _ mexports imports declarations) -> Right Module {+ _moduleName = fromString mname,+ _moduleDocs = Nothing,+ _moduleLocation = ModuleSource Nothing,+ _moduleExports = fmap (concatMap getExports) mexports,+ _moduleImports = map getImport imports,+ _moduleDeclarations = sortDeclarations $ getDecls declarations } where -- Replace all tabs to spaces to make SrcLoc valid, otherwise it treats tab as 8 spaces source' = map untab source@@ -106,8 +109,8 @@ -- | Get exports getExports :: H.ExportSpec -> [Export] getExports (H.EModuleContents (H.ModuleName m)) = [ExportModule $ fromString m]-getExports (H.EVar _ n) = [uncurry ExportName (identOfQName n) ExportNothing]-getExports (H.EAbs n) = [uncurry ExportName (identOfQName n) ExportNothing]+getExports (H.EVar n) = [uncurry ExportName (identOfQName n) ExportNothing]+getExports (H.EAbs _ n) = [uncurry ExportName (identOfQName n) ExportNothing] getExports (H.EThingAll n) = [uncurry ExportName (identOfQName n) ExportAll] getExports (H.EThingWith n ns) = [uncurry ExportName (identOfQName n) $ ExportWith (map toStr ns)] where toStr :: H.CName -> Text@@ -149,8 +152,8 @@ mergeInfos l _ = l -- | Get definitions-getBinds :: H.Binds -> [Declaration]-getBinds (H.BDecls decls) = getDecls decls+getBinds :: Maybe H.Binds -> [Declaration]+getBinds (Just (H.BDecls decls)) = getDecls decls getBinds _ = [] -- | Get declaration and child declarations@@ -197,7 +200,7 @@ -- | Get GADT constructor and record fields declarations getGConDecl :: H.GadtDecl -> [Declaration]-getGConDecl (H.GadtDecl loc n fields r) = mkFun loc n (Function (Just $ oneLinePrint $ map snd fields `tyFun` r) [] Nothing) : concatMap (uncurry (getRec loc r)) fields where+getGConDecl (H.GadtDecl loc n fields r) = mkFun loc n (Function (Just $ oneLinePrint $ map snd fields `tyFun` r) [] Nothing) : concatMap (uncurry (getRec loc r)) fields -- | Get record field declaration getRec :: H.SrcLoc -> H.Type -> [H.Name] -> H.Type -> [Declaration]@@ -319,9 +322,10 @@ _ -> throwError "Can inspect only source file docs" -- | Inspect contents-inspectContents :: String -> [String] -> String -> ExceptT String IO InspectedModule-inspectContents name opts cts = inspect (ModuleSource $ Just name) (contentsInspection cts opts) $ do- analyzed <- ExceptT $ return $ analyzeModule exts (Just name) cts <|> analyzeModule_ exts (Just name) cts+inspectContents :: String -> [(String, String)] -> [String] -> String -> ExceptT String IO InspectedModule+inspectContents name defines opts cts = inspect (ModuleSource $ Just name) (contentsInspection cts opts) $ do+ cts' <- lift $ preprocess_ defines exts name cts+ analyzed <- ExceptT $ return $ analyzeModule exts (Just name) cts' <|> analyzeModule_ exts (Just name) cts' return $ set moduleLocation (ModuleSource $ Just name) analyzed where exts = mapMaybe flagExtension opts@@ -330,8 +334,8 @@ contentsInspection _ _ = return InspectionNone -- crc or smth -- | Inspect file-inspectFile :: [String] -> FilePath -> Maybe String -> ExceptT String IO InspectedModule-inspectFile opts file mcts = do+inspectFile :: [(String, String)] -> [String] -> FilePath -> Maybe String -> ExceptT String IO InspectedModule+inspectFile defines opts file mcts = do proj <- liftE $ locateProject file absFilename <- liftE $ Dir.canonicalizePath file ex <- liftE $ Dir.doesFileExist absFilename@@ -342,7 +346,7 @@ -- else liftM Just $ hdocs (FileModule absFilename Nothing) opts forced <- ExceptT $ E.handle onError $ do analyzed <- liftM (\s -> analyzeModule exts (Just absFilename) s <|> analyzeModule_ exts (Just absFilename) s) $- maybe (readFileUtf8 absFilename) return mcts+ maybe (readFileUtf8 absFilename >>= preprocess_ defines exts file) return mcts force analyzed `deepseq` return analyzed -- return $ setLoc absFilename proj . maybe id addDocs docsMap $ forced return $ set moduleLocation (FileModule absFilename proj) forced@@ -385,11 +389,37 @@ liftM (ordNub . concat) $ triesMap (liftM sequenceA . traverse (liftE . enumHs)) dirs -- | Inspect project-inspectProject :: [String] -> Project -> ExceptT String IO (Project, [InspectedModule])-inspectProject opts p = do+inspectProject :: [(String, String)] -> [String] -> Project -> ExceptT String IO (Project, [InspectedModule])+inspectProject defines opts p = do p' <- loadProject p srcs <- projectSources p' modules <- mapM inspectFile' srcs return (p', catMaybes modules) where- inspectFile' exts = liftM return (inspectFile (opts ++ extensionsOpts exts) (view entity exts) Nothing) <|> return Nothing+ inspectFile' exts = liftM return (inspectFile defines (opts ++ extensionsOpts exts) (view entity exts) Nothing) <|> return Nothing++-- | Get actual defines+getDefines :: IO [(String, String)]+getDefines = do+ tmp <- Dir.getTemporaryDirectory+ writeFile (tmp </> "defines.hs") ""+ _ <- runWait "ghc.exe" ["-E", "-optP-dM", "-cpp", tmp </> "defines.hs"] ""+ cts <- readFileUtf8 (tmp </> "defines.hspp")+ Dir.removeFile (tmp </> "defines.hs")+ Dir.removeFile (tmp </> "defines.hspp")+ return $ mapMaybe (\g -> (,) <$> g 1 <*> g 2) $ mapMaybe (matchRx rx) $ lines cts+ where+ rx = "#define ([^\\s]+) (.*)"++preprocess :: [(String, String)] -> FilePath -> String -> ExceptT String IO String+preprocess defines fpath cts = do+ cts' <- liftE $ Cpphs.cppIfdef fpath defines [] Cpphs.defaultBoolOptions cts+ return $ unlines $ map snd cts'++preprocess_ :: [(String, String)] -> [String] -> FilePath -> String -> IO String+preprocess_ defines exts fpath cts+ | hasCPP = runExceptT (preprocess defines fpath cts) >>= either (const $ return cts) return+ | otherwise = return cts+ where+ exts' = map H.parseExtension exts ++ maybe [] snd (H.readExtensions cts)+ hasCPP = H.EnableExtension H.CPP `elem` exts'
src/HsDev/Project.hs view
@@ -209,12 +209,11 @@ lang = maybe [] (\l -> ["default-language: " ++ display l]) $ _infoLanguage i exts | null (_infoExtensions i) = [] - | otherwise = ["extensions:"] ++ map (tab 1 . display) (_infoExtensions i) + | otherwise = "extensions:" : map (tab 1 . display) (_infoExtensions i) opts | null (_infoGHCOptions i) = [] - | otherwise = ["ghc-options:"] ++ map (tab 1) (_infoGHCOptions i) - sources = ["source-dirs:"] ++ - (map (tab 1) $ _infoSourceDirs i) + | otherwise = "ghc-options:" : map (tab 1) (_infoGHCOptions i) + sources = "source-dirs:" : (map (tab 1) $ _infoSourceDirs i) instance ToJSON Info where toJSON i = object [
src/HsDev/Scan.hs view
@@ -14,9 +14,9 @@ ) where import Control.Applicative ((<|>)) +import Control.DeepSeq import Control.Lens (view, preview, set, _Right, _1, _2, _3, (^.)) import Control.Monad.Except -import qualified Data.Map as M import Data.Maybe (catMaybes, fromMaybe, isJust) import System.Directory @@ -47,6 +47,9 @@ projectsToScan :: [ProjectToScan], sandboxesToScan :: [SandboxToScan] } +instance NFData ScanContents where + rnf (ScanContents ms ps ss) = rnf ms `seq` rnf ps `seq` rnf ss + -- | Enum project sources enumProject :: Project -> ExceptT String IO ProjectToScan enumProject p = do @@ -76,7 +79,7 @@ projs <- triesMap (enumProject . project) projects let projPaths = map (view projectPath . fst) projs - standalone = map ((`FileModule` Nothing)) $ filter (\s -> not (any (`isParent` s) projPaths)) sources + standalone = map (`FileModule` Nothing) $ filter (\s -> not (any (`isParent` s) projPaths)) sources return ScanContents { modulesToScan = [(s, [], Nothing) | s <- standalone], projectsToScan = projs, @@ -89,8 +92,8 @@ loadProject proj -- | Scan module -scanModule :: [String] -> ModuleLocation -> Maybe String -> ExceptT String IO InspectedModule -scanModule opts (FileModule f p) mcts = liftM setProj $ inspectFile opts f mcts where +scanModule :: [(String, String)] -> [String] -> ModuleLocation -> Maybe String -> ExceptT String IO InspectedModule +scanModule defines opts (FileModule f p) mcts = liftM setProj $ inspectFile defines opts f mcts where setProj = set (inspectedId . moduleProject) p . set (inspectionResult . _Right . moduleLocation . moduleProject) p @@ -98,8 +101,8 @@ -- infer' m = tryInfer <|> return m where -- tryInfer = mapExceptT (withCurrentDirectory (sourceModuleRoot (moduleName m) f)) $ -- runGhcMod defaultOptions $ inferTypes opts Cabal m -scanModule opts (CabalModule c p n) _ = browse opts c n p -scanModule _ (ModuleSource _) _ = throwError "Can inspect only modules in file or cabal" +scanModule _ opts (CabalModule c p n) _ = browse opts c n p +scanModule _ _ (ModuleSource _) _ = throwError "Can inspect only modules in file or cabal" -- | Scan additional info and modify scanned module. Dones't fail on error, just left module unchanged scanModify :: ([String] -> Cabal -> Module -> ExceptT String IO Module) -> InspectedModule -> ExceptT String IO InspectedModule @@ -115,17 +118,17 @@ _ -> return False -- | Rescan inspected module -rescanModule :: [String] -> InspectedModule -> ExceptT String IO (Maybe InspectedModule) -rescanModule opts im = do +rescanModule :: [(String, String)] -> [String] -> InspectedModule -> ExceptT String IO (Maybe InspectedModule) +rescanModule defines opts im = do up <- upToDate opts im if up then return Nothing - else fmap Just $ scanModule opts (view inspectedId im) Nothing + else fmap Just $ scanModule defines opts (view inspectedId im) Nothing -- | Is module new or recently changed changedModule :: Database -> [String] -> ModuleLocation -> ExceptT String IO Bool changedModule db opts m = maybe (return True) (liftM not . upToDate opts) m' where - m' = M.lookup m (databaseModules db) + m' = lookupInspected m db -- | Returns new (to scan) and changed (to rescan) modules changedModules :: Database -> [String] -> [ModuleToScan] -> ExceptT String IO [ModuleToScan]
+ src/HsDev/Server/Base.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE CPP, OverloadedStrings #-} + +module HsDev.Server.Base ( + initLog, runServer, Server, startServer, inServer, + withCache, writeCache, readCache, + + module HsDev.Server.Types, + module HsDev.Server.Message + ) where + +import Control.Applicative +import Control.Concurrent +import Control.Exception +import Control.Monad +import Control.Monad.Except +import Control.Monad.Reader +import qualified Data.Map as M +import Data.Maybe +import Data.Text (Text) +import qualified Data.Text as T (pack, unpack) +import System.Log.Simple hiding (Level(..), Message(..), Command(..)) +import System.Log.Simple.Base (writeLog) +import qualified System.Log.Simple.Base as Log + +import qualified Control.Concurrent.FiniteChan as F +import qualified System.Directory.Watcher as Watcher +import Text.Format ((~~), FormatBuild(..)) + +import qualified HsDev.Cache.Structured as SC +import qualified HsDev.Client.Commands as Client +import HsDev.Database +import qualified HsDev.Database.Async as DB +import qualified HsDev.Database.Update as Update +import HsDev.Inspect (getDefines) +import HsDev.Tools.Ghc.Worker +import HsDev.Tools.GhcMod (ghcModMultiWorker) +import HsDev.Server.Types +import HsDev.Server.Message +import HsDev.Util + +#if mingw32_HOST_OS +import System.Win32.FileMapping.NamePool +#else +import System.Posix.Process +import System.Posix.IO +#endif + + +-- | Inits log chan and returns functions (print message, wait channel) +initLog :: ServerOpts -> IO (Log, Log.Level -> String -> IO (), ([String] -> IO ()) -> IO (), IO ()) +initLog sopts = do + msgs <- F.newChan + outputDone <- newEmptyMVar + void $ forkIO $ finally + (F.readChan msgs >>= mapM_ (const $ return ())) + (putMVar outputDone ()) + l <- newLog (constant [rule']) $ concat [ + [logger text console], + [logger text (chaner msgs)], + maybeToList $ (logger text . file) <$> serverLog sopts] + Log.writeLog l Log.Info ("Log politics: low = {}, high = {}" ~~ logLow ~~ logHigh) + let + listenLog f = logException "listen log" (F.putChan msgs) $ do + msgs' <- F.dupChan msgs + F.readChan msgs' >>= f + return (l, \lev -> writeLog l lev . T.pack, listenLog, F.closeChan msgs >> takeMVar outputDone) + where + rule' :: Log.Rule + rule' = parseRule_ $ T.pack ("/: " ++ serverLogConfig sopts) + (Log.Politics logLow logHigh) = Log.rulePolitics rule' Log.defaultPolitics + +instance FormatBuild Log.Level where + +-- | Run server +runServer :: ServerOpts -> (CommandOptions -> IO ()) -> IO () +runServer sopts act = bracket (initLog sopts) (\(_, _, _, x) -> x) $ \(logger', outputStr, listenLog, waitOutput) -> Log.scopeLog logger' (T.pack "hsdev") $ Watcher.withWatcher $ \watcher -> do + db <- DB.newAsync + when (serverLoad sopts) $ withCache sopts () $ \cdir -> do + outputStr Log.Info $ "Loading cache from " ++ cdir + dbCache <- liftA merge <$> SC.load cdir + case dbCache of + Left err -> outputStr Log.Error $ "Failed to load cache: " ++ err + Right dbCache' -> DB.update db (return dbCache') +#if mingw32_HOST_OS + mmapPool <- Just <$> createPool "hsdev" +#endif + ghcw <- ghcWorker [] (return ()) + ghciw <- ghciWorker + ghcmodw <- ghcModMultiWorker + defs <- getDefines + let + copts = CommandOptions + db + (writeCache sopts outputStr) + (readCache sopts outputStr) + "." + outputStr + logger' + listenLog + waitOutput + watcher +#if mingw32_HOST_OS + mmapPool +#endif + ghcw + ghciw + ghcmodw + (const $ return ()) + (return ()) + (return ()) + (return ()) + defs + _ <- forkIO $ Update.onEvent watcher (Update.processEvent $ Update.settings copts [] False False) + act copts + +type Server = Worker (ReaderT CommandOptions IO) + +startServer :: ServerOpts -> IO Server +startServer sopts = startWorker (runServer sopts . runReaderT) id id + +inServer :: Server -> Command -> IO Result +inServer srv c = inWorker srv (ReaderT (`Client.runCommand` c)) + +chaner :: F.Chan String -> Consumer Text +chaner ch = Consumer withChan where + withChan f = f (F.putChan ch . T.unpack) + +-- | Perform action on cache +withCache :: ServerOpts -> a -> (FilePath -> IO a) -> IO a +withCache sopts v onCache = case serverCache sopts of + Nothing -> return v + Just cdir -> onCache cdir + +writeCache :: ServerOpts -> (Log.Level -> String -> IO ()) -> Database -> IO () +writeCache sopts logMsg' d = withCache sopts () $ \cdir -> do + logMsg' Log.Info $ "writing cache to " ++ cdir + logIO "cache writing exception: " (logMsg' Log.Error) $ do + let + sd = structurize d + SC.dump cdir sd + forM_ (M.keys (structuredCabals sd)) $ \c -> logMsg' Log.Debug ("cache write: cabal " ++ show c) + forM_ (M.keys (structuredProjects sd)) $ \p -> logMsg' Log.Debug ("cache write: project " ++ p) + case allModules (structuredFiles sd) of + [] -> return () + ms -> logMsg' Log.Debug $ "cache write: " ++ show (length ms) ++ " files" + logMsg' Log.Info $ "cache saved to " ++ cdir + +readCache :: ServerOpts -> (Log.Level -> String -> IO ()) -> (FilePath -> ExceptT String IO Structured) -> IO (Maybe Database) +readCache sopts logMsg' act = withCache sopts Nothing $ join . liftM (either cacheErr cacheOk) . runExceptT . act where + cacheErr e = logMsg' Log.Error ("Error reading cache: " ++ e) >> return Nothing + cacheOk s = do + forM_ (M.keys (structuredCabals s)) $ \c -> logMsg' Log.Debug ("cache read: cabal " ++ show c) + forM_ (M.keys (structuredProjects s)) $ \p -> logMsg' Log.Debug ("cache read: project " ++ p) + case allModules (structuredFiles s) of + [] -> return () + ms -> logMsg' Log.Debug $ "cache read: " ++ show (length ms) ++ " files" + return $ Just $ merge s
src/HsDev/Server/Commands.hs view
@@ -1,22 +1,17 @@-{-# LANGUAGE OverloadedStrings, CPP, PatternGuards, LambdaCase #-} +{-# LANGUAGE OverloadedStrings, CPP, PatternGuards, LambdaCase, TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module HsDev.Server.Commands ( - commands, - serverOpts, serverDefCfg, - clientOpts, clientDefCfg, - - clientCmd, sendCmd, - - initLog, runServer, - processRequest, processClient, - - withCache, writeCache, readCache + ServerCommand(..), ServerOpts(..), ClientOpts(..), + Request(..), + sendCommand, runServerCommand, + findPath, + processRequest, processClient, processClientSocket, + module HsDev.Server.Types ) where -import Control.Applicative -import Control.Arrow (second) import Control.Concurrent +import Control.Concurrent.Async import Control.Exception import Control.Monad import Control.Monad.Except @@ -25,41 +20,30 @@ import qualified Data.ByteString.Char8 as BS import Data.ByteString.Lazy.Char8 (ByteString) import qualified Data.ByteString.Lazy.Char8 as L -import Data.Either (isLeft) import qualified Data.Map as M import Data.Maybe -import Data.Text (Text) -import qualified Data.Text as T (pack, unpack) +import qualified Data.Text as T (pack) import Network.Socket hiding (connect) import qualified Network.Socket as Net hiding (send) import qualified Network.Socket.ByteString as Net (send) import qualified Network.Socket.ByteString.Lazy as Net (getContents) import System.Directory import System.Exit +import System.FilePath import System.IO -import System.Log.Simple hiding (Level(..), Message(..)) -import System.Log.Simple.Base (writeLog) import qualified System.Log.Simple.Base as Log -import Text.Read (readMaybe) -import Control.Apply.Util import Control.Concurrent.Util import qualified Control.Concurrent.FiniteChan as F -import Data.Lisp -import qualified System.Directory.Watcher as Watcher -import System.Console.Cmd hiding (run) -import Text.Format ((~~), (%), Format(..)) +import Text.Format ((~~)) +import System.Directory.Paths -import qualified HsDev.Cache.Structured as SC import qualified HsDev.Client.Commands as Client -import HsDev.Database import qualified HsDev.Database.Async as DB -import qualified HsDev.Database.Update as Update -import HsDev.Tools.Ghc.Worker -import HsDev.Tools.GhcMod (ghcModMultiWorker) -import HsDev.Server.Message as M +import HsDev.Server.Base import HsDev.Server.Types import HsDev.Util +import HsDev.Version #if mingw32_HOST_OS import Data.Aeson.Types hiding (Result, Error) @@ -75,223 +59,23 @@ import System.Posix.IO #endif --- | Server commands -commands :: [Cmd (IO ())] -commands = [ - cmd' "start" server' "start remote server" start', - cmd' "run" server' "run server" run', - cmd' "stop" client' "stop remote server" stop', - cmd' "connect" client' "connect to send commands directly" connect'] - where - cmd' :: String -> ([Opt], Opts String) -> String -> (Args -> IO ()) -> Cmd (IO ()) - cmd' nm (opts', defOpts') desc' act' = - cmd nm [] opts' desc' act' `with` [defaultOpts defOpts'] - - server' = (serverOpts, serverDefCfg) - client' = (clientOpts, clientDefCfg) - - -- | Start remote server - start' :: Args -> IO () - start' (Args _ sopts) = do -#if mingw32_HOST_OS - let - args = ["run"] ++ toArgs (Args [] sopts) - myExe <- getExecutablePath - curDir <- getCurrentDirectory - let - -- one escape for start-process and other for callable process - -- seems, that start-process just concats arguments into one string - -- start-process foo 'bar baz' ⇒ foo bar baz -- not expected - -- start-process foo '"bar baz"' ⇒ foo "bar baz" -- ok - biescape = escape quote . escape quoteDouble - script = "try { start-process $ $ -WindowStyle Hidden -WorkingDirectory $ } catch { $$_.Exception, $$_.InvocationInfo.Line }" ~~ ( - escape quote myExe % - (intercalate ", " (map biescape args)) % - escape quote curDir) - r <- readProcess "powershell" [ - "-Command", - script] "" - if all isSpace r - then putStrLn $ "Server started at port " ++ (fromJust $ arg "port" sopts) - else mapM_ putStrLn [ - "Failed to start server", - "\tCommand: " ++ script, - "\tResult: " ++ r] -#else - let - forkError :: SomeException -> IO () - forkError e = putStrLn $ "Failed to start server: " ++ show e - - proxy :: IO () - proxy = do - _ <- createSession - _ <- forkProcess serverAction - exitImmediately ExitSuccess - - serverAction :: IO () - serverAction = do - mapM_ closeFd [stdInput, stdOutput, stdError] - nullFd <- openFd "/dev/null" ReadWrite Nothing defaultFileFlags - mapM_ (dupTo nullFd) [stdInput, stdOutput, stdError] - closeFd nullFd - run' (Args [] sopts) - - handle forkError $ do - _ <- forkProcess proxy - putStrLn $ "Server started at port " ++ fromJust (arg "port" sopts) -#endif - - -- | Run server - run' :: Args -> IO () - run' (Args _ sopts) - | flagSet "as-client" sopts = runServer sopts $ \copts -> do - commandLog copts Log.Info $ "Server started as client connecting at port " ++ fromJust (arg "port" sopts) - me <- myThreadId - s <- socket AF_INET Stream defaultProtocol - addr' <- inet_addr "127.0.0.1" - Net.connect s $ SockAddrInet (fromIntegral $ fromJust $ iarg "port" sopts) addr' - flip finally (close s) $ processClientSocket s (copts { - commandExit = killThread me }) - | otherwise = runServer sopts $ \copts -> do - commandLog copts Log.Info $ "Server started at port " ++ fromJust (arg "port" sopts) - - waitListen <- newEmptyMVar - clientChan <- F.newChan - - void $ 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 $ iarg "port" sopts) iNADDR_ANY - listen s maxListenQueue - forever $ logIO "accept client exception: " (commandLog copts Log.Error) $ do - s' <- fst <$> accept s - void $ forkIO $ logIO (show s' ++ " exception: ") (commandLog copts Log.Error) $ - flip finally (close s') $ - bracket newEmptyMVar (`putMVar` ()) $ \done -> do - me <- myThreadId - let - timeoutWait = do - notDone <- isEmptyMVar done - when notDone $ do - void $ forkIO $ do - threadDelay 1000000 - void $ tryPutMVar done () - killThread me - takeMVar done - -- waitForever = forever $ hGetLineBS h - F.putChan clientChan timeoutWait - processClientSocket s' (copts { - -- commandHold = waitForever, - commandExit = serverStop }) - - takeMVar waitListen - DB.readAsync (commandDatabase copts) >>= writeCache sopts (commandLog copts) - F.stopChan clientChan >>= sequence_ - commandLog copts Log.Info "server stopped" - - -- | Stop remote server - stop' :: Args -> IO () - stop' (Args _ copts) = runArgs (map clientCmd Client.commands) onDef onError (Args ["exit"] copts) where - onDef = putStrLn "Command 'exit' not found" - onError es = putStrLn $ "Failed to stop server: " ++ es - - -- | Connect to remote server - connect' :: Args -> IO () - connect' (Args _ copts) = do - curDir <- getCurrentDirectory - s <- socket AF_INET Stream defaultProtocol - addr' <- inet_addr "127.0.0.1" - Net.connect s (SockAddrInet (fromIntegral $ fromJust $ iarg "port" copts) addr') - bracket (socketToHandle s ReadWriteMode) hClose $ \h -> forM_ [(1 :: Integer)..] $ \i -> ignoreIO $ do - input' <- hGetLineBS stdin - case decodeLispOrJSON input' of - Left _ -> L.putStrLn $ encodeValue False $ object ["error" .= ("invalid command" :: String)] - Right (isLisp, req') -> do - L.hPutStrLn h $ encodeLispOrJSON isLisp $ Message (Just $ show i) $ - req' `M.withOpts` ["current-directory" %-- curDir] - waitResp h - where - pretty = flagSet "pretty" copts - - encodeValue :: ToJSON a => Bool -> a -> L.ByteString - encodeValue True = encodeLisp - encodeValue False - | pretty = encodePretty - | otherwise = encode - - waitResp h = do - resp <- hGetLineBS h - parseResp h resp - - parseResp h str = case decodeLispOrJSON str of - Left e -> putStrLn $ "Can't decode response: " ++ e - Right (isLisp, Message i r) -> do - r' <- unMmap r - putStrLn $ fromMaybe "_" i ++ ":" ++ fromUtf8 (encodeValue isLisp r') - case r of - Left _ -> waitResp h - _ -> return () - --- | Server options -serverOpts :: [Opt] -serverOpts = [ - req "port" "number" `desc` "listen port", - req "timeout" "msec" `desc` "query timeout", - req "log" "file" `short` ['l'] `desc` "log file", - req "log-config" "config" `desc` "config log", - req "cache" "path" `desc` "cache directory", - flag "load" `desc` "force load all data from cache on startup"] - --- | Client options -clientOpts :: [Opt] -clientOpts = [ - req "port" "number" `desc` "connection port", - flag "pretty" `desc` "pretty json output", - flag "stdin" `desc` "pass data to stdin", - req "timeout" "msec" `desc` "overwrite timeout duration", - flag "silent" `desc` "supress notifications"] - --- | Server default options -serverDefCfg :: Opts String -serverDefCfg = mconcat [ - "port" %-- (4567 :: Int), - "timeout" %-- (1000 :: Int)] - --- | Client default options -clientDefCfg :: Opts String -clientDefCfg = mconcat ["port" %-- (4567 :: Int)] - --- | Command to send to client -clientCmd :: Cmd CommandAction -> Cmd (IO ()) -clientCmd c = cmd (cmdName c) (cmdArgs c) (cmdOpts c ++ clientOpts) (cmdDesc c) (sendCmd (cmdName c)) - `with` [defaultOpts clientDefCfg] - --- | Send command to server -sendCmd :: String -> Args -> IO () -sendCmd name (Args args opts) = do - var <- newEmptyMVar - thId <- forkIO $ ignoreIO sendReceive >> putMVar var () - handle (\(SomeException _) -> killThread thId) $ takeMVar var +sendCommand :: ClientOpts -> Bool -> Command -> (Notification -> IO a) -> IO Result +sendCommand copts noFile c onNotification = do + asyncAct <- async sendReceive + res <- waitCatch asyncAct + case res of + Left e -> return $ Error (show e) $ M.fromList [] + Right r -> return r where - (copts, opts') = splitOpts clientOpts opts - reqCall = Request name args opts' - pretty = flagSet "pretty" copts encodeValue :: ToJSON a => a -> L.ByteString encodeValue - | pretty = encodePretty + | clientPretty copts = encodePretty | otherwise = encode sendReceive = do curDir <- getCurrentDirectory - input <- if flagSet "stdin" copts + input <- if clientStdin copts then liftM Just L.getContents - else return $ fmap toUtf8 $ arg "data" copts + else return $ fmap toUtf8 $ Nothing -- arg "data" copts let parseData :: L.ByteString -> IO Value parseData cts = case eitherDecode cts of @@ -301,13 +85,9 @@ s <- socket AF_INET Stream defaultProtocol addr' <- inet_addr "127.0.0.1" - Net.connect s (SockAddrInet (fromIntegral $ fromJust $ iarg "port" copts) addr') + Net.connect s (SockAddrInet (fromIntegral $ clientPort copts) addr') bracket (socketToHandle s ReadWriteMode) hClose $ \h -> do - L.hPutStrLn h $ encode $ Message Nothing $ reqCall `M.withOpts` [ - "current-directory" %-- curDir, - "data" %-? (fromUtf8 . encode <$> dat), - "timeout" %-? (iarg "timeout" copts :: Maybe Integer), - if flagSet "silent" copts then hoist "silent" else mempty] + L.hPutStrLn h $ encode $ Message Nothing $ Request c curDir noFile (clientTimeout copts) (clientSilent copts) hFlush h peekResponse h @@ -315,126 +95,172 @@ resp <- hGetLineBS h parseResponse h resp - parseResponse h str = case decodeLispOrJSON str of - Left e -> putStrLn $ "Can't decode response: " ++ e - Right (_, Message _ r) -> do + parseResponse h str = case eitherDecode str of + Left e -> return $ Error e $ M.fromList [("response", toJSON $ fromUtf8 str)] + Right (Message _ r) -> do r' <- unMmap r - L.putStrLn $ case r' of - Left n -> encodeValue n - Right (Result v) -> encodeValue v - Right e -> encodeValue e - when (isLeft r') $ peekResponse h + case r' of + Left n -> onNotification n >> peekResponse h + Right r -> return r -chaner :: F.Chan String -> Consumer Text -chaner ch = Consumer withChan where - withChan f = f (F.putChan ch . T.unpack) +runServerCommand :: ServerCommand -> IO () +runServerCommand Version = putStrLn $cabalVersion +runServerCommand (Start sopts) = do +#if mingw32_HOST_OS + let + args = "run" : serverOptsArgs sopts + myExe <- getExecutablePath + curDir <- getCurrentDirectory + let + -- one escape for start-process and other for callable process + -- seems, that start-process just concats arguments into one string + -- start-process foo 'bar baz' ⇒ foo bar baz -- not expected + -- start-process foo '"bar baz"' ⇒ foo "bar baz" -- ok + biescape = escape quote . escape quoteDouble + script = "try {{ start-process {} {} -WindowStyle Hidden -WorkingDirectory {} }} catch {{ $_.Exception, $_.InvocationInfo.Line }}" + ~~ escape quote myExe + ~~ intercalate ", " (map biescape args) + ~~ escape quote curDir + r <- readProcess "powershell" [ + "-Command", + script] "" + if all isSpace r + then putStrLn $ "Server started at port " ++ show (serverPort sopts) + else mapM_ putStrLn [ + "Failed to start server", + "\tCommand: " ++ script, + "\tResult: " ++ r] +#else + let + forkError :: SomeException -> IO () + forkError e = putStrLn $ "Failed to start server: " ++ show e -instance Format Log.Level where + proxy :: IO () + proxy = do + _ <- createSession + _ <- forkProcess serverAction + exitImmediately ExitSuccess --- | Inits log chan and returns functions (print message, wait channel) -initLog :: Opts String -> IO (Log, Log.Level -> String -> IO (), ([String] -> IO ()) -> IO (), IO ()) -initLog sopts = do - msgs <- F.newChan - outputDone <- newEmptyMVar - void $ forkIO $ finally - (F.readChan msgs >>= mapM_ (const $ return ())) - (putMVar outputDone ()) - l <- newLog (constant [rule']) $ concat [ - [logger text console], - [logger text (chaner msgs)], - maybeToList $ (logger text . file) <$> arg "log" sopts] - Log.writeLog l Log.Info ("Log politics: low = $, high = $" ~~ (logLow % logHigh)) - let - listenLog f = logException "listen log" (F.putChan msgs) $ do - msgs' <- F.dupChan msgs - F.readChan msgs' >>= f - return (l, \lev -> writeLog l lev . T.pack, listenLog, F.closeChan msgs >> takeMVar outputDone) - where - rule' :: Log.Rule - rule' = fromMaybe (Log.Rule null $ const Log.tracePolitics) $ - (parseRule_ . T.pack . ("/: " ++)) <$> - arg "log-config" sopts - (Log.Politics logLow logHigh) = Log.rulePolitics rule' Log.defaultPolitics + serverAction :: IO () + serverAction = do + mapM_ closeFd [stdInput, stdOutput, stdError] + nullFd <- openFd "/dev/null" ReadWrite Nothing defaultFileFlags + mapM_ (dupTo nullFd) [stdInput, stdOutput, stdError] + closeFd nullFd + runServerCommand (Run sopts) --- | Run server -runServer :: Opts String -> (CommandOptions -> IO ()) -> IO () -runServer sopts act = bracket (initLog sopts) (\(_, _, _, x) -> x) $ \(logger', outputStr, listenLog, waitOutput) -> Log.scopeLog logger' (T.pack "hsdev") $ Watcher.withWatcher $ \watcher -> do - db <- DB.newAsync - when (flagSet "load" sopts) $ withCache sopts () $ \cdir -> do - outputStr Log.Info $ "Loading cache from " ++ cdir - dbCache <- liftA merge <$> SC.load cdir - case dbCache of - Left err -> outputStr Log.Error $ "Failed to load cache: " ++ err - Right dbCache' -> DB.update db (return dbCache') -#if mingw32_HOST_OS - mmapPool <- Just <$> createPool "hsdev" -#endif - ghcw <- ghcWorker [] (return ()) - ghciw <- ghciWorker - ghcmodw <- ghcModMultiWorker - let - copts = CommandOptions - db - (writeCache sopts outputStr) - (readCache sopts outputStr) - "." - outputStr - logger' - listenLog - waitOutput - watcher -#if mingw32_HOST_OS - mmapPool + handle forkError $ do + _ <- forkProcess proxy + putStrLn $ "Server started at port " ++ show (serverPort sopts) #endif - ghcw - ghciw - ghcmodw - (const $ return ()) - (return ()) - (return ()) - (return ()) - _ <- forkIO $ Update.onEvent watcher (Update.processEvent $ Update.settings copts mempty) - act copts +runServerCommand (Run sopts) = runServer sopts $ \copts -> do + commandLog copts Log.Info $ "Server started at port " ++ show (serverPort sopts) -decodeLispOrJSON :: FromJSON a => ByteString -> Either String (Bool, a) -decodeLispOrJSON str = - ((,) <$> pure False <*> eitherDecode str) <|> - ((,) <$> pure True <*> decodeLisp str) + waitListen <- newEmptyMVar + clientChan <- F.newChan -encodeLispOrJSON :: ToJSON a => Bool -> a -> ByteString -encodeLispOrJSON True = encodeLisp -encodeLispOrJSON False = encode + void $ forkIO $ do + accepter <- myThreadId --- | Process request, notifications can be sent during processing -processRequest :: CommandOptions -> (Notification -> IO ()) -> Request -> IO Result -processRequest copts onNotify req' = - runArgs - Client.commands - unknownCommand - requestError - (requestToArgs req') - (copts { commandNotify = onNotify }) + let + serverStop :: IO () + serverStop = void $ forkIO $ do + void $ tryPutMVar waitListen () + killThread accepter + + s <- socket AF_INET Stream defaultProtocol + bind s $ SockAddrInet (fromIntegral $ serverPort sopts) iNADDR_ANY + listen s maxListenQueue + forever $ logAsync (commandLog copts Log.Fatal) $ logIO "accept client exception: " (commandLog copts Log.Error) $ do + s' <- fst <$> accept s + void $ forkIO $ logAsync (commandLog copts Log.Fatal) $ logIO (show s' ++ " exception: ") (commandLog copts Log.Error) $ + flip finally (close s') $ + bracket newEmptyMVar (`putMVar` ()) $ \done -> do + me <- myThreadId + let + timeoutWait = do + notDone <- isEmptyMVar done + when notDone $ do + void $ forkIO $ do + threadDelay 1000000 + void $ tryPutMVar done () + killThread me + takeMVar done + -- waitForever = forever $ hGetLineBS h + F.putChan clientChan timeoutWait + processClientSocket s' (copts { + -- commandHold = waitForever, + commandExit = serverStop }) + + takeMVar waitListen + DB.readAsync (commandDatabase copts) >>= writeCache sopts (commandLog copts) + F.stopChan clientChan >>= sequence_ + commandLog copts Log.Info "server stopped" +runServerCommand (Stop copts) = runServerCommand (Remote copts False Exit) +runServerCommand (Connect copts) = do + curDir <- getCurrentDirectory + s <- socket AF_INET Stream defaultProtocol + addr' <- inet_addr "127.0.0.1" + Net.connect s (SockAddrInet (fromIntegral $ clientPort copts) addr') + bracket (socketToHandle s ReadWriteMode) hClose $ \h -> forM_ [(1 :: Integer)..] $ \i -> ignoreIO $ do + input' <- hGetLineBS stdin + case eitherDecode input' of + Left _ -> L.putStrLn $ encodeValue $ object ["error" .= ("invalid command" :: String)] + Right req' -> do + L.hPutStrLn h $ encode $ Message (Just $ show i) $ Request req' curDir True (clientTimeout copts) False + waitResp h where - unknownCommand :: CommandAction - unknownCommand _ = return $ Error "Unknown command" M.empty + pretty = clientPretty copts - requestError :: String -> CommandAction - requestError errs _ = return $ Error "Command syntax error" $ M.fromList [ - ("what", toJSON $ lines errs)] + encodeValue :: ToJSON a => a -> L.ByteString + encodeValue + | pretty = encodePretty + | otherwise = encode + waitResp h = do + resp <- hGetLineBS h + parseResp h resp + + parseResp h str = case eitherDecode str of + Left e -> putStrLn $ "Can't decode response: " ++ e + Right (Message i r) -> do + r' <- unMmap r + putStrLn $ fromMaybe "_" i ++ ":" ++ fromUtf8 (encodeValue r') + case r of + Left _ -> waitResp h + _ -> return () +runServerCommand (Remote copts noFile c) = sendCommand copts noFile c printValue >>= printResult where + printValue :: ToJSON a => a -> IO () + printValue = L.putStrLn . encodeValue + printResult :: Result -> IO () + printResult (Result r) = printValue r + printResult e = printValue e + encodeValue :: ToJSON a => a -> L.ByteString + encodeValue = if clientPretty copts then encodePretty else encode + +findPath :: MonadIO m => CommandOptions -> FilePath -> m FilePath +findPath copts f = liftIO $ canonicalizePath (normalise f') where + f' + | isRelative f = commandRoot copts </> f + | otherwise = f + +-- | Process request, notifications can be sent during processing +processRequest :: CommandOptions -> (Notification -> IO ()) -> Command -> IO Result +processRequest copts onNotify c = paths (findPath copts) c >>= Client.runCommand (copts { commandNotify = onNotify }) + -- | Process client, listen for requests and process them processClient :: String -> IO ByteString -> (ByteString -> IO ()) -> CommandOptions -> IO () processClient name receive send' copts = do commandLog copts Log.Info $ name ++ " connected" respChan <- newChan - void $ forkIO $ getChanContents respChan >>= mapM_ (send' . uncurry encodeLispOrJSON) + void $ forkIO $ getChanContents respChan >>= mapM_ (send' . encode) linkVar <- newMVar $ return () let - answer :: Bool -> Message Response -> IO () - answer isLisp m@(Message _ r) = do - when (not $ isNotification r) $ + answer :: Message Response -> IO () + answer m@(Message _ r) = do + unless (isNotification r) $ commandLog copts Log.Trace $ " << " ++ ellipsis (fromUtf8 (encode r)) - writeChan respChan (isLisp, m) + writeChan respChan m where ellipsis :: String -> String ellipsis s @@ -443,44 +269,31 @@ flip finally (disconnected linkVar) $ forever $ Log.scopeLog (commandLogger copts) (T.pack name) $ do req' <- receive commandLog copts Log.Trace $ " => " ++ fromUtf8 req' - case second (fmap extractMeta) <$> decodeLispOrJSON req' of + case eitherDecode req' of Left _ -> do commandLog copts Log.Warning $ "Invalid request: " ++ fromUtf8 req' - answer False $ Message Nothing $ responseError "Invalid request" [ + answer $ Message Nothing $ responseError "Invalid request" [ "request" .= fromUtf8 req'] - Right (isLisp, m) -> Log.scopeLog (commandLogger copts) (T.pack $ fromMaybe "_" (messageId m)) $ do - resp' <- flip traverse m $ \(cdir, noFile, silent, tm, reqArgs) -> do + Right m -> Log.scopeLog (commandLogger copts) (T.pack $ fromMaybe "_" (messageId m)) $ do + resp' <- for m $ \(Request c cdir noFile tm silent) -> do let onNotify n | silent = return () - | otherwise = traverse (const $ mmap' noFile (Left n)) m >>= answer isLisp - commandLog copts Log.Trace $ name ++ " >> " ++ fromUtf8 (encode reqArgs) + | otherwise = traverse (const $ mmap' noFile (Left n)) m >>= answer + commandLog copts Log.Trace $ name ++ " >> " ++ fromUtf8 (encode c) resp <- fmap Right $ handleTimeout tm $ handleError $ processRequest (copts { commandRoot = cdir, commandLink = void (swapMVar linkVar $ commandExit copts) }) onNotify - reqArgs + c mmap' noFile resp - answer isLisp resp' + answer resp' where - extractMeta :: Request -> (FilePath, Bool, Bool, Maybe Int, Request) - extractMeta c = (cdir, noFile, silent, tm, c { requestOpts = opts' }) where - cdir = fromMaybe (commandRoot copts) $ arg "current-directory" metaOpts - noFile = flagSet "no-file" metaOpts - silent = flagSet "silent" metaOpts - tm = join $ fmap readMaybe $ arg "timeout" metaOpts - (metaOpts, opts') = splitOpts [ - req "current-directory" "path", - flag "no-file", - flag "silent", - req "timeout" "ms"] - (requestOpts c) - - handleTimeout :: Maybe Int -> IO Result -> IO Result - handleTimeout Nothing = id - handleTimeout (Just tm) = fmap (fromMaybe $ Error "Timeout" M.empty) . timeout tm + handleTimeout :: Int -> IO Result -> IO Result + handleTimeout 0 = id + handleTimeout tm = fmap (fromMaybe $ Error "Timeout" M.empty) . timeout tm handleError :: IO Result -> IO Result handleError = handle onErr where @@ -499,12 +312,6 @@ commandLog copts Log.Info $ name ++ " disconnected" join $ takeMVar var -{- --- | Process client by Handle -processClientHandle :: Show a => a -> Handle -> CommandOptions -> IO () -processClientHandle n h = processClient (show n) (hGetLineBS h) (\s -> L.hPutStrLn h s >> hFlush h) --} - -- | Process client by socket processClientSocket :: Socket -> CommandOptions -> IO () processClientSocket s copts = do @@ -530,37 +337,6 @@ | otherwise = do sent <- Net.send sock bs when (sent > 0) $ sendAll sock (BS.drop sent bs) - --- | Perform action on cache -withCache :: Opts String -> a -> (FilePath -> IO a) -> IO a -withCache sopts v onCache = case arg "cache" sopts of - Nothing -> return v - Just cdir -> onCache cdir - -writeCache :: Opts String -> (Log.Level -> String -> IO ()) -> Database -> IO () -writeCache sopts logMsg' d = withCache sopts () $ \cdir -> do - logMsg' Log.Info $ "writing cache to " ++ cdir - logIO "cache writing exception: " (logMsg' Log.Error) $ do - let - sd = structurize d - SC.dump cdir sd - forM_ (M.keys (structuredCabals sd)) $ \c -> logMsg' Log.Debug ("cache write: cabal " ++ show c) - forM_ (M.keys (structuredProjects sd)) $ \p -> logMsg' Log.Debug ("cache write: project " ++ p) - case allModules (structuredFiles sd) of - [] -> return () - ms -> logMsg' Log.Debug $ "cache write: " ++ show (length ms) ++ " files" - logMsg' Log.Info $ "cache saved to " ++ cdir - -readCache :: Opts String -> (Log.Level -> String -> IO ()) -> (FilePath -> ExceptT String IO Structured) -> IO (Maybe Database) -readCache sopts logMsg' act = withCache sopts Nothing $ join . liftM (either cacheErr cacheOk) . runExceptT . act where - cacheErr e = logMsg' Log.Error ("Error reading cache: " ++ e) >> return Nothing - cacheOk s = do - forM_ (M.keys (structuredCabals s)) $ \c -> logMsg' Log.Debug ("cache read: cabal " ++ show c) - forM_ (M.keys (structuredProjects s)) $ \p -> logMsg' Log.Debug ("cache read: project " ++ p) - case allModules (structuredFiles s) of - [] -> return () - ms -> logMsg' Log.Debug $ "cache read: " ++ show (length ms) ++ " files" - return $ Just $ merge s #if mingw32_HOST_OS data MmapFile = MmapFile String
src/HsDev/Server/Message.hs view
@@ -3,8 +3,6 @@ module HsDev.Server.Message ( Message(..), messagesById, - Request(..), requestToArgs, - withOpts, withoutOpts, Notification(..), Result(..), ResultPart(..), Response, isNotification, notification, result, responseError, resultPart, groupResponses, responsesById @@ -17,12 +15,10 @@ import Data.Aeson.Types (Pair) import Data.Either (lefts, isRight) import Data.List (unfoldr) -import Data.Maybe import Data.Map (Map) import qualified Data.Map as M import Data.Text (unpack) -import System.Console.Args hiding (withOpts) import HsDev.Util ((.::), (.::?), objectUnion) -- | Message with id to link request and response @@ -48,37 +44,6 @@ messagesById :: Maybe String -> [Message a] -> [a] messagesById i = map message . filter ((== i) . messageId) --- | Request from client -data Request = Request { - requestCommand :: String, - requestArgs :: [String], - requestOpts :: Opts String } - -requestToArgs :: Request -> Args -requestToArgs (Request c as opts) = Args (words c ++ as) opts - -instance ToJSON Request where - toJSON (Request c as os) = object [ - "command" .= c, - "args" .= as, - "opts" .= os] - -instance FromJSON Request where - parseJSON = withObject "request" $ \v -> Request <$> - v .:: "command" <*> - (fromMaybe [] <$> v .::? "args") <*> - (fromMaybe mempty <$> v .::? "opts") - --- | Add options to request -withOpts :: Request -> [Opts String] -> Request -withOpts r os = r { - requestOpts = mconcat (requestOpts r : os) } - --- | Remove options from request -withoutOpts :: Request -> [String] -> Request -withoutOpts r os = r { - requestOpts = Opts $ foldr (.) id (map M.delete os) $ getOpts (requestOpts r) } - -- | Notification from server data Notification = Notification Value @@ -96,16 +61,15 @@ -- ^ Error instance ToJSON Result where - toJSON (Result r) = object [ - "result" .= r] + toJSON (Result r) = object ["result" .= r] toJSON (Error msg rs) = object [ "error" .= msg, "details" .= toJSON rs] instance FromJSON Result where - parseJSON = withObject "result" $ \v -> foldr1 (<|>) [ - Result <$> v .:: "result", - Error <$> v .:: "error" <*> v .:: "details"] + parseJSON = withObject "result" $ \v -> + (Result <$> v .:: "result") <|> + (Error <$> v .:: "error" <*> v .:: "details") -- | Part of result list, returns via notification data ResultPart = ResultPart Value
src/HsDev/Server/Types.hs view
@@ -2,26 +2,41 @@ module HsDev.Server.Types ( CommandOptions(..), CommandError(..), commandError_, commandError, - CommandAction, CommandM, CommandActionT, - ResultValue(..) + CommandM, + ServerCommand(..), ServerOpts(..), ClientOpts(..), serverOptsArgs, Request(..), + + Command(..), AddedContents(..), + GhcModCommand(..), + AutoFixCommand(..), + FileContents(..), TargetFilter(..), SearchQuery(..), SearchType(..), + FromCmd(..), ) where import Control.Applicative +import Control.Lens (each) import Control.Monad.Except import Control.Monad.Reader -import Data.Aeson hiding (Result, Error) -import Data.Aeson.Types (Pair) -import qualified Data.HashMap.Strict as HM (null) -import System.Log.Simple +import Data.Aeson hiding (Result(..), Error) +import qualified Data.Aeson.Types as A +import qualified Data.ByteString.Lazy.Char8 as L +import Data.Default +import Data.Foldable (asum) +import Options.Applicative +import System.Log.Simple hiding (Command) +import System.Directory.Paths + import HsDev.Database import qualified HsDev.Database.Async as DB import HsDev.Project import HsDev.Symbols import HsDev.Server.Message import HsDev.Watcher.Types (Watcher) -import HsDev.Tools.GhcMod (OutputMessage, TypedRegion, WorkerMap) +import HsDev.Tools.GhcMod (OutputMessage, WorkerMap) import HsDev.Tools.Ghc.Worker (Worker, Ghc) +import HsDev.Tools.Types (Note) +import HsDev.Tools.AutoFix (Correction) +import HsDev.Util #if mingw32_HOST_OS import System.Win32.FileMapping.NamePool (Pool) @@ -46,9 +61,10 @@ commandNotify :: Notification -> IO (), commandLink :: IO (), commandHold :: IO (), - commandExit :: IO () } + commandExit :: IO (), + commandDefines :: [(String, String)] } -data CommandError = CommandError String [Pair] +data CommandError = CommandError String [A.Pair] instance Monoid CommandError where mempty = CommandError "" [] @@ -57,65 +73,552 @@ commandError_ :: String -> ExceptT CommandError IO a commandError_ m = commandError m [] -commandError :: String -> [Pair] -> ExceptT CommandError IO a +commandError :: String -> [A.Pair] -> ExceptT CommandError IO a commandError m ps = throwError $ CommandError m ps -type CommandAction = CommandOptions -> IO Result - type CommandM a = ExceptT CommandError IO a -type CommandActionT a = CommandOptions -> CommandM a +-- | Server control command +data ServerCommand = + Version | + Start ServerOpts | + Run ServerOpts | + Stop ClientOpts | + Connect ClientOpts | + Remote ClientOpts Bool Command + deriving (Show) -data ResultValue = - ResultDatabase Database | - ResultDeclaration Declaration | - ResultModuleDeclaration ModuleDeclaration | - ResultModuleId ModuleId | - ResultModule Module | - ResultInspectedModule InspectedModule | - ResultPackage ModulePackage | - ResultProject Project | - ResultTyped TypedRegion | - ResultOutputMessage OutputMessage | - ResultList [ResultValue] | - ResultMap (Map String ResultValue) | - ResultJSON Value | - ResultString String | - ResultNone +-- | Server options +data ServerOpts = ServerOpts { + serverPort :: Int, + serverTimeout :: Int, + serverLog :: Maybe FilePath, + serverLogConfig :: String, + serverCache :: Maybe FilePath, + serverLoad :: Bool } + deriving (Show) -instance ToJSON ResultValue where - toJSON (ResultDatabase db) = toJSON db - toJSON (ResultDeclaration d) = toJSON d - toJSON (ResultModuleDeclaration md) = toJSON md - toJSON (ResultModuleId mid) = toJSON mid - toJSON (ResultModule m) = toJSON m - toJSON (ResultInspectedModule m) = toJSON m - toJSON (ResultPackage p) = toJSON p - toJSON (ResultProject p) = toJSON p - toJSON (ResultTyped t) = toJSON t - toJSON (ResultOutputMessage e) = toJSON e - toJSON (ResultList l) = toJSON l - toJSON (ResultMap m) = toJSON m - toJSON (ResultJSON v) = toJSON v - toJSON (ResultString s) = toJSON s - toJSON ResultNone = toJSON $ object [] +instance Default ServerOpts where + def = ServerOpts 4567 0 Nothing "use default" Nothing False -instance FromJSON ResultValue where - parseJSON v = foldr1 (<|>) [ - do - (Object m) <- parseJSON v - if HM.null m then return ResultNone else mzero, - ResultDatabase <$> parseJSON v, - ResultDeclaration <$> parseJSON v, - ResultModuleDeclaration <$> parseJSON v, - ResultModuleId <$> parseJSON v, - ResultModule <$> parseJSON v, - ResultInspectedModule <$> parseJSON v, - ResultPackage <$> parseJSON v, - ResultProject <$> parseJSON v, - ResultTyped <$> parseJSON v, - ResultOutputMessage <$> parseJSON v, - ResultList <$> parseJSON v, - ResultMap <$> parseJSON v, - pure $ ResultJSON v, - ResultString <$> parseJSON v] +-- | Client options +data ClientOpts = ClientOpts { + clientPort :: Int, + clientPretty :: Bool, + clientStdin :: Bool, + clientTimeout :: Int, + clientSilent :: Bool } + deriving (Show) + +instance Default ClientOpts where + def = ClientOpts 4567 False False 0 False + +instance FromCmd ServerCommand where + cmdP = serv <|> remote where + serv = subparser $ mconcat [ + cmd "version" "hsdev version" (pure Version), + cmd "start" "start remote server" (Start <$> cmdP), + cmd "run" "run server" (Run <$> cmdP), + cmd "stop" "stop remote server" (Stop <$> cmdP), + cmd "connect" "connect to send commands directly" (Connect <$> cmdP)] + remote = Remote <$> cmdP <*> noFileFlag <*> cmdP + +instance FromCmd ServerOpts where + cmdP = ServerOpts <$> + (portArg <|> pure (serverPort def)) <*> + (timeoutArg <|> pure (serverTimeout def)) <*> + optional logArg <*> + (logConfigArg <|> pure (serverLogConfig def)) <*> + optional cacheArg <*> + loadFlag + +instance FromCmd ClientOpts where + cmdP = ClientOpts <$> + (portArg <|> pure (clientPort def)) <*> + prettyFlag <*> + stdinFlag <*> + (timeoutArg <|> pure (clientTimeout def)) <*> + silentFlag + +portArg = option auto (long "port" <> metavar "number" <> help "connection port") +timeoutArg = option auto (long "timeout" <> metavar "msec" <> help "query timeout") +logArg = strOption (long "log" <> short 'l' <> metavar "file" <> help "log file") +logConfigArg = strOption (long "log-config" <> metavar "rule" <> help "log config: low [low], high [high], set [low] [high], use [default/debug/trace/silent/supress]") +cacheArg = strOption (long "cache" <> metavar "path" <> help "cache directory") +noFileFlag = switch (long "no-file" <> help "don't use mmap files") +loadFlag = switch (long "load" <> help "force load all data from cache on startup") +prettyFlag = switch (long "pretty" <> help "pretty json output") +stdinFlag = switch (long "stdin" <> help "pass data to stdin") +silentFlag = switch (long "silent" <> help "supress notifications") + +serverOptsArgs :: ServerOpts -> [String] +serverOptsArgs sopts = concat [ + ["--port", show $ serverPort sopts], + ["--timeout", show $ serverTimeout sopts], + marg "--log" (serverLog sopts), + ["--log-config", serverLogConfig sopts], + marg "--cache" (serverCache sopts), + ["--load" | serverLoad sopts]] + where + marg :: String -> Maybe String -> [String] + marg n (Just v) = [n, v] + marg _ _ = [] + +data Request = Request { + requestCommand :: Command, + requestDirectory :: FilePath, + requestNoFile :: Bool, + requestTimeout :: Int, + requestSilent :: Bool } + +instance ToJSON Request where + toJSON (Request c dir f tm s) = object ["current-directory" .= dir, "no-file" .= f, "timeout" .= tm, "silent" .= s] `objectUnion` toJSON c + +instance FromJSON Request where + parseJSON = withObject "request" $ \v -> Request <$> + parseJSON (Object v) <*> + ((v .:: "current-directory") <|> pure ".") <*> + ((v .:: "no-file") <|> pure False) <*> + ((v .:: "timeout") <|> pure 0) <*> + ((v .:: "silent") <|> pure False) + +-- FIXME: Why so much commands in options? +-- | Command from client +data Command = + Ping | + Listen | + AddData { addedContents :: [AddedContents] } | + Scan { + scanProjects :: [FilePath], + scanSandboxes :: [Cabal], + scanFiles :: [FilePath], + scanPaths :: [FilePath], + scanContents :: [FileContents], + scanGhcOpts :: [String], + scanDocs :: Bool, + scanInferTypes :: Bool } | + RefineDocs { + docsProjects :: [FilePath], + docsFiles :: [FilePath], + docsModules :: [String] } | + InferTypes { + inferProjects :: [FilePath], + inferFiles :: [FilePath], + inferModules :: [String] } | + Remove { + removeProjects :: [FilePath], + removePackages :: [String], + removeSandboxes :: [Cabal], + removeFiles :: [FilePath] } | + InfoModules TargetFilter | + InfoPackages | + InfoProjects | + InfoSandboxes | + InfoSymbol SearchQuery TargetFilter | + InfoModule SearchQuery TargetFilter | + InfoResolve FilePath Bool | + InfoProject (Either String FilePath) | + InfoSandbox FilePath | + Lookup String FilePath | + Whois String FilePath | + ResolveScopeModules FilePath | + ResolveScope SearchQuery Bool FilePath | + Complete String Bool FilePath | + Hayoo { + hayooQuery :: String, + hayooPage :: Int, + hayooPages :: Int } | + CabalList { cabalListPackages :: [String] } | + Lint { + lintFiles :: [FilePath], + lintContents :: [FileContents] } | + Check { + checkFiles :: [FilePath], + checkContents :: [FileContents], + checkGhcOpts :: [String] } | + CheckLint { + checkLintFiles :: [FilePath], + checkLintContents :: [FileContents], + checkLintGhcOpts :: [String] } | + Types { + typesFiles :: [FilePath], + typesContents :: [FileContents], + typesGhcOpts :: [String] } | + GhcMod { ghcModCommand :: GhcModCommand } | + AutoFix { autoFixCommand :: AutoFixCommand } | + GhcEval { ghcEvalExpressions :: [String] } | + Link { linkHold :: Bool } | + Exit + deriving (Show) + +data AddedContents = + AddedDatabase Database | + AddedModule InspectedModule | + AddedProject Project + +instance Show AddedContents where + show = L.unpack . encode + +data GhcModCommand = + GhcModLang | + GhcModFlags | + GhcModType Position FilePath [String] | + GhcModLint [FilePath] [String] | + GhcModCheck [FilePath] [String] | + GhcModCheckLint [FilePath] [String] [String] + deriving (Show) + +data AutoFixCommand = + AutoFixShow [Note OutputMessage] | + AutoFixFix [Note Correction] [Note Correction] Bool + deriving (Show) + +data FileContents = FileContents FilePath String deriving (Show) +-- TODO: Why deps is just string? +data TargetFilter = + TargetProject String | + TargetFile FilePath | + TargetModule String | + TargetDepsOf String | + TargetCabal Cabal | + TargetPackage String | + TargetSourced | + TargetStandalone | + TargetAny + deriving (Eq, Show) +data SearchQuery = SearchQuery String SearchType deriving (Show) +data SearchType = SearchExact | SearchPrefix | SearchInfix | SearchSuffix | SearchRegex deriving (Show) + +instance Paths Command where + paths f (Scan projs cs fs ps fcts ghcs docs infer) = Scan <$> + each f projs <*> + (each . paths) f cs <*> + each f fs <*> + each f ps <*> + (each . paths) f fcts <*> + pure ghcs <*> + pure docs <*> + pure infer + paths f (RefineDocs projs fs ms) = RefineDocs <$> each f projs <*> each f fs <*> pure ms + paths f (InferTypes projs fs ms) = InferTypes <$> each f projs <*> each f fs <*> pure ms + paths f (Remove projs ps cs fs) = Remove <$> each f projs <*> pure ps <*> (each . paths) f cs <*> each f fs + paths f (InfoModules t) = InfoModules <$> paths f t + paths f (InfoSymbol q t) = InfoSymbol <$> pure q <*> paths f t + paths f (InfoModule q t) = InfoModule <$> pure q <*> paths f t + paths f (InfoResolve fpath es) = InfoResolve <$> f fpath <*> pure es + paths f (InfoProject (Right proj)) = InfoProject <$> (Right <$> f proj) + paths f (InfoSandbox fpath) = InfoSandbox <$> f fpath + paths f (Lookup n fpath) = Lookup <$> pure n <*> f fpath + paths f (Whois n fpath) = Whois <$> pure n <*> f fpath + paths f (ResolveScopeModules fpath) = ResolveScopeModules <$> f fpath + paths f (ResolveScope q g fpath) = ResolveScope q g <$> f fpath + paths f (Complete n g fpath) = Complete n g <$> f fpath + paths f (Lint fs fcts) = Lint <$> each f fs <*> (each . paths) f fcts + paths f (Check fs fcts ghcs) = Check <$> each f fs <*> (each . paths) f fcts <*> pure ghcs + paths f (CheckLint fs fcts ghcs) = CheckLint <$> each f fs <*> (each . paths) f fcts <*> pure ghcs + paths f (Types fs fcts ghcs) = Types <$> each f fs <*> (each . paths) f fcts <*> pure ghcs + paths f (GhcMod g) = GhcMod <$> paths f g + paths _ c = pure c + +instance Paths GhcModCommand where + paths f (GhcModType pos fpath opts) = GhcModType <$> pure pos <*> f fpath <*> pure opts + paths f (GhcModLint fs hlints) = GhcModLint <$> traverse f fs <*> pure hlints + paths f (GhcModCheck fs ghcs) = GhcModCheck <$> traverse f fs <*> pure ghcs + paths f (GhcModCheckLint fs ghcs hlints) = GhcModCheckLint <$> traverse f fs <*> pure ghcs <*> pure hlints + paths _ g = pure g + +instance Paths FileContents where + paths f (FileContents fpath cts) = FileContents <$> f fpath <*> pure cts + +instance Paths TargetFilter where + paths f (TargetFile fpath) = TargetFile <$> f fpath + paths f (TargetCabal c) = TargetCabal <$> paths f c + paths _ t = pure t + +instance FromCmd Command where + cmdP = subparser $ mconcat [ + cmd "ping" "ping server" (pure Ping), + cmd "listen" "listen server log" (pure Listen), + cmd "add" "add info to database" (AddData <$> option readJSON idm), + cmd "scan" "scan sources" $ Scan <$> + many projectArg <*> + many cabalArg <*> + many fileArg <*> + many (pathArg $ help "path") <*> + many cmdP <*> + ghcOpts <*> + docsFlag <*> + inferFlag, + cmd "docs" "scan docs" $ RefineDocs <$> many projectArg <*> many fileArg <*> many moduleArg, + cmd "infer" "infer types" $ InferTypes <$> many projectArg <*> many fileArg <*> many moduleArg, + cmd "remove" "remove modules info" $ Remove <$> + many projectArg <*> + many packageArg <*> + many cabalArg <*> + many fileArg, + cmd "modules" "list modules" (InfoModules <$> cmdP), + cmd "packages" "list packages" (pure InfoPackages), + cmd "projects" "list projects" (pure InfoProjects), + cmd "sandboxes" "list sandboxes" (pure InfoSandboxes), + cmd "symbol" "get symbol info" (InfoSymbol <$> cmdP <*> cmdP), + cmd "module" "get module info" (InfoModule <$> cmdP <*> cmdP), + cmd "resolve" "resolve module scope (or exports)" (InfoResolve <$> fileArg <*> exportsFlag), + cmd "project" "get project info" (InfoProject <$> ((Left <$> projectArg) <|> (Right <$> pathArg idm))), + cmd "sandbox" "get sandbox info" (InfoSandbox <$> (pathArg $ help "locate sandbox in parent of this path")), + cmd "lookup" "lookup for symbol" (Lookup <$> strArgument idm <*> ctx), + cmd "whois" "get info for symbol" (Whois <$> strArgument idm <*> ctx), + cmd "scope" "get declarations accessible from module or within a project" ( + subparser (cmd "modules" "get modules accessible from module or within a project" (ResolveScopeModules <$> ctx)) <|> + ResolveScope <$> cmdP <*> globalFlag <*> ctx), + cmd "complete" "show completions for input" (Complete <$> strArgument idm <*> wideFlag <*> ctx), + cmd "hayoo" "find declarations online via Hayoo" (Hayoo <$> strArgument idm <*> hayooPageArg <*> hayooPagesArg), + cmd "cabal" "cabal commands" (subparser $ cmd "list" "list cabal packages" (CabalList <$> many (strArgument idm))), + cmd "lint" "lint source files or file contents" (Lint <$> many fileArg <*> many cmdP), + cmd "check" "check source files or file contents" (Check <$> many fileArg <*> many cmdP <*> ghcOpts), + cmd "check-lint" "check and lint source files or file contents" (CheckLint <$> many fileArg <*> many cmdP <*> ghcOpts), + cmd "types" "get types for file expressions" (Types <$> many fileArg <*> many cmdP <*> ghcOpts), + cmd "ghc-mod" "ghc-mod commands" (GhcMod <$> cmdP), + cmd "autofix" "autofix commands" (AutoFix <$> cmdP), + cmd "ghc" "ghc commands" (subparser $ cmd "eval" "evaluate expression" (GhcEval <$> many (strArgument idm))), + cmd "link" "link to server" (Link <$> holdFlag), + cmd "exit" "exit" (pure Exit)] + +instance FromCmd GhcModCommand where + cmdP = subparser $ mconcat [ + cmd "lang" "get LANGUAGE pragmas" (pure GhcModLang), + cmd "flags" "get OPTIONS_GHC pragmas" (pure GhcModFlags), + cmd "type" "infer type with 'ghc-mod type'" (GhcModType <$> (Position <$> argument auto idm <*> argument auto idm) <*> fileArg <*> ghcOpts), + cmd "lint" "lint source files" (GhcModLint <$> many (strArgument idm) <*> hlintOpts), + cmd "check" "check source files" (GhcModCheck <$> many (strArgument idm) <*> ghcOpts), + cmd "check-lint" "check & lint source files" (GhcModCheckLint <$> many (strArgument idm) <*> ghcOpts <*> hlintOpts)] + +instance FromCmd AutoFixCommand where + cmdP = subparser $ mconcat [ + cmd "show" "generate corrections for check & lint messages" (AutoFixShow <$> option readJSON (long "data" <> metavar "message" <> help "messages to make fixes for")), + cmd "fix" "fix errors and return rest corrections with updated regions" (AutoFixFix <$> + option readJSON (long "data" <> metavar "message" <> help "messages to fix") <*> + option readJSON (long "rest" <> metavar "correction" <> short 'r' <> help "update corrections") <*> + pureFlag)] + +instance FromCmd FileContents where + cmdP = option readJSON (long "contents") + +instance FromCmd TargetFilter where + cmdP = asum [TargetProject <$> projectArg, TargetFile <$> fileArg, TargetModule <$> moduleArg, TargetDepsOf <$> depsArg, TargetCabal <$> cabalArg, TargetPackage <$> packageArg, flag' TargetSourced (long "src"), flag' TargetStandalone (long "stand"), pure TargetAny] + +instance FromCmd SearchQuery where + cmdP = SearchQuery <$> (strArgument idm <|> pure "") <*> (asum [ + flag' SearchExact (long "exact"), + flag' SearchRegex (long "regex"), + flag' SearchInfix (long "infix"), + flag' SearchSuffix (long "suffix"), + pure SearchPrefix <* switch (long "prefix")]) + +readJSON :: FromJSON a => ReadM a +readJSON = str >>= maybe (readerError "Can't parse JSON argument") return . decode . L.pack + +cabalArg = flag' Cabal (long "cabal") <|> (Sandbox <$> sandboxArg) +ctx = fileArg +depsArg = strOption (long "deps" <> metavar "object" <> help "filter to such that in dependency of specified object (file or project)") +docsFlag = switch (long "docs" <> help "scan source file docs") +exportsFlag = switch (long "exports" <> short 'e' <> help "resolve module exports") +fileArg = strOption (long "file" <> metavar "path" <> short 'f') +ghcOpts = many (strOption (long "ghc" <> metavar "option" <> short 'g' <> help "options to pass to GHC")) +globalFlag = switch (long "global" <> help "scope of project") +hayooPageArg = option auto (long "page" <> metavar "n" <> short 'p' <> help "page number (0 by default)" <> value 0) +hayooPagesArg = option auto (long "pages" <> metavar "count" <> short 'n' <> help "pages count (1 by default)" <> value 1) +hlintOpts = many (strOption (long "hlint" <> metavar "option" <> short 'h' <> help "options to pass to hlint")) +holdFlag = switch (long "hold" <> short 'h' <> help "don't return any response") +inferFlag = switch (long "infer" <> help "infer types") +-- localsArg = switch (long "locals" <> short 'l' <> help "look in local declarations") +moduleArg = strOption (long "module" <> metavar "name" <> short 'm' <> help "module name") +packageArg = strOption (long "package" <> metavar "name" <> help "module package") +pathArg f = strOption (long "path" <> metavar "path" <> short 'p' <> f) +projectArg = strOption (long "project" <> long "proj" <> metavar "project") +pureFlag = switch (long "pure" <> help "don't modify actual file, just return result") +sandboxArg = strOption (long "sandbox" <> metavar "path" <> help "path to cabal sandbox") +wideFlag = switch (long "wide" <> short 'w' <> help "wide mode - complete as if there were no import lists") + +instance ToJSON Command where + toJSON Ping = cmdJson "ping" [] + toJSON Listen = cmdJson "listen" [] + toJSON (AddData cts) = cmdJson "add" ["data" .= cts] + toJSON (Scan projs cabals fs ps contents ghcs docs' infer') = cmdJson "scan" [ + "projects" .= projs, + "sandboxes" .= cabals, + "files" .= fs, + "paths" .= ps, + "contents" .= contents, + "ghc-opts" .= ghcs, + "docs" .= docs', + "infer" .= infer'] + toJSON (RefineDocs projs fs ms) = cmdJson "docs" ["projects" .= projs, "files" .= fs, "modules" .= ms] + toJSON (InferTypes projs fs ms) = cmdJson "docs" ["projects" .= projs, "files" .= fs, "modules" .= ms] + toJSON (Remove projs packages cabals fs) = cmdJson "remove" ["projects" .= projs, "packages" .= packages, "sandboxes" .= cabals, "files" .= fs] + toJSON (InfoModules tf) = cmdJson "modules" ["filter" .= tf] + toJSON InfoPackages = cmdJson "packages" [] + toJSON InfoProjects = cmdJson "projects" [] + toJSON InfoSandboxes = cmdJson "sandboxes" [] + toJSON (InfoSymbol q tf) = cmdJson "symbol" ["query" .= q, "filter" .= tf] + toJSON (InfoModule q tf) = cmdJson "module" ["query" .= q, "filter" .= tf] + toJSON (InfoResolve f es) = cmdJson "resolve" ["file" .= f, "exports" .= es] + toJSON (InfoProject p) = cmdJson "project" $ either (\pname -> ["name" .= pname]) (\ppath -> ["path" .= ppath]) p + toJSON (InfoSandbox p) = cmdJson "sandbox" ["path" .= p] + toJSON (Lookup n f) = cmdJson "lookup" ["name" .= n, "file" .= f] + toJSON (Whois n f) = cmdJson "whois" ["name" .= n, "file" .= f] + toJSON (ResolveScopeModules f) = cmdJson "scope modules" ["file" .= f] + toJSON (ResolveScope q g f) = cmdJson "scope" ["query" .= q, "global" .= g, "file" .= f] + toJSON (Complete q w f) = cmdJson "complete" ["prefix" .= q, "wide" .= w, "file" .= f] + toJSON (Hayoo q p ps) = cmdJson "hayoo" ["query" .= q, "page" .= p, "pages" .= ps] + toJSON (CabalList ps) = cmdJson "cabal list" ["packages" .= ps] + toJSON (Lint fs cs) = cmdJson "lint" ["files" .= fs, "contents" .= cs] + toJSON (Check fs cs ghcs) = cmdJson "check" ["files" .= fs, "contents" .= cs, "ghc-opts" .= ghcs] + toJSON (CheckLint fs cs ghcs) = cmdJson "check-lint" ["files" .= fs, "contents" .= cs, "ghc-opts" .= ghcs] + toJSON (Types fs cs ghcs) = cmdJson "types" ["files" .= fs, "contents" .= cs, "ghc-opts" .= ghcs] + toJSON (GhcMod gcmd) = toJSON gcmd + toJSON (AutoFix acmd) = toJSON acmd + toJSON (GhcEval exprs) = cmdJson "ghc eval" ["exprs" .= exprs] + toJSON (Link h) = cmdJson "link" ["hold" .= h] + toJSON Exit = cmdJson "exit" [] + +instance FromJSON Command where + parseJSON = withObject "command" $ \v -> asum [ + guardCmd "ping" v *> pure Ping, + guardCmd "listen" v *> pure Listen, + guardCmd "add" v *> (AddData <$> v .:: "data"), + guardCmd "scan" v *> (Scan <$> + v .:: "projects" <*> + v .:: "sandboxes" <*> + v .:: "files" <*> + v .:: "paths" <*> + v .:: "contents" <*> + v .:: "ghc-opts" <*> + v .:: "docs" <*> + v .:: "infer"), + guardCmd "docs" v *> (RefineDocs <$> v .:: "projects" <*> v .:: "files" <*> v .:: "modules"), + guardCmd "infer" v *> (InferTypes <$> v .:: "projects" <*> v .:: "files" <*> v .:: "modules"), + guardCmd "remove" v *> (Remove <$> + v .:: "projects" <*> + v .:: "packages" <*> + v .:: "sandboxes" <*> + v .:: "files"), + guardCmd "modules" v *> (InfoModules <$> v .:: "filter"), + guardCmd "packages" v *> pure InfoPackages, + guardCmd "projects" v *> pure InfoProjects, + guardCmd "sandboxes" v *> pure InfoSandboxes, + guardCmd "symbol" v *> (InfoSymbol <$> v .:: "query" <*> v .:: "filter"), + guardCmd "module" v *> (InfoModule <$> v .:: "query" <*> v .:: "filter"), + guardCmd "resolve" v *> (InfoResolve <$> v .:: "file" <*> v .:: "exports"), + guardCmd "project" v *> (InfoProject <$> asum [Left <$> v .:: "name", Right <$> v .:: "path"]), + guardCmd "sandbox" v *> (InfoSandbox <$> v .:: "path"), + guardCmd "lookup" v *> (Lookup <$> v .:: "name" <*> v .:: "file"), + guardCmd "whois" v *> (Whois <$> v .:: "name" <*> v .:: "file"), + guardCmd "scope modules" v *> (ResolveScopeModules <$> v .:: "file"), + guardCmd "scope" v *> (ResolveScope <$> v .:: "query" <*> v .:: "global" <*> v .:: "file"), + guardCmd "complete" v *> (Complete <$> v .:: "prefix" <*> v .:: "wide" <*> v .:: "file"), + guardCmd "hayoo" v *> (Hayoo <$> v .:: "query" <*> v .:: "page" <*> v .:: "pages"), + guardCmd "cabal list" v *> (CabalList <$> v .:: "packages"), + guardCmd "lint" v *> (Lint <$> v .:: "files" <*> v .:: "contents"), + guardCmd "check" v *> (Check <$> v .:: "files" <*> v .:: "contents" <*> v .:: "ghc-opts"), + guardCmd "check-lint" v *> (CheckLint <$> v .:: "files" <*> v .:: "contents" <*> v .:: "ghc-opts"), + guardCmd "types" v *> (Types <$> v .:: "files" <*> v .:: "contents" <*> v .:: "ghc-opts"), + GhcMod <$> parseJSON (Object v), + AutoFix <$> parseJSON (Object v), + guardCmd "ghc eval" v *> (GhcEval <$> v .:: "exprs"), + guardCmd "link" v *> (Link <$> v .:: "hold"), + guardCmd "exit" v *> pure Exit] + +instance ToJSON AddedContents where + toJSON (AddedDatabase db) = object ["database" .= db] + toJSON (AddedModule im) = object ["module" .= im] + toJSON (AddedProject p) = object ["project" .= p] + +instance FromJSON AddedContents where + parseJSON = withObject "added-contents" $ \v -> asum [ + AddedDatabase <$> v .:: "database", + AddedModule <$> v .:: "module", + AddedProject <$> v .:: "project"] + +instance ToJSON GhcModCommand where + toJSON GhcModLang = cmdJson "ghc-mod lang" [] + toJSON GhcModFlags = cmdJson "ghc-mod flags" [] + toJSON (GhcModType pos f ghcs) = cmdJson "ghc-mod type" ["position" .= pos, "file" .= f, "ghc-opts" .= ghcs] + toJSON (GhcModLint fs lints) = cmdJson "ghc-mod lint" ["files" .= fs, "hlint-opts" .= lints] + toJSON (GhcModCheck fs ghcs) = cmdJson "ghc-mod check" ["files" .= fs, "ghc-opts" .= ghcs] + toJSON (GhcModCheckLint fs ghcs lints) = cmdJson "ghc-mod check-lint" ["files" .= fs, "ghc-opts" .= ghcs, "hlint-opts" .= lints] + +instance FromJSON GhcModCommand where + parseJSON = withObject "ghc-mod-command" $ \v -> asum [ + guardCmd "ghc-mod lang" v *> pure GhcModLang, + guardCmd "ghc-mod flags" v *> pure GhcModFlags, + guardCmd "ghc-mod type" v *> (GhcModType <$> v .:: "position" <*> v .:: "file" <*> v .:: "ghc-opts"), + guardCmd "ghc-mod lint" v *> (GhcModLint <$> v .:: "files" <*> v .:: "hlint-opts"), + guardCmd "ghc-mod check" v *> (GhcModCheck <$> v .:: "files" <*> v .:: "ghc-opts"), + guardCmd "ghc-mod check-lint" v *> (GhcModCheckLint <$> v .:: "files" <*> v .:: "ghc-opts" <*> v .:: "hlint-opts")] + +instance ToJSON AutoFixCommand where + toJSON (AutoFixShow ns) = cmdJson "autofix show" ["messages" .= ns] + toJSON (AutoFixFix ns rests pure') = cmdJson "autofix fix" ["messages" .= ns, "rest" .= rests, "pure" .= pure'] + +instance FromJSON AutoFixCommand where + parseJSON = withObject "auto-fix-command" $ \v -> asum [ + guardCmd "autofix show" v *> (AutoFixShow <$> v .:: "messages"), + guardCmd "autofix fix" v *> (AutoFixFix <$> v .:: "messages" <*> v .:: "rest" <*> v .:: "pure")] + +instance ToJSON FileContents where + toJSON (FileContents fpath cts) = object ["file" .= fpath, "contents" .= cts] + +instance FromJSON FileContents where + parseJSON = withObject "file-contents" $ \v -> FileContents <$> v .:: "file" <*> v .:: "contents" + +instance ToJSON TargetFilter where + toJSON (TargetProject pname) = object ["project" .= pname] + toJSON (TargetFile fpath) = object ["file" .= fpath] + toJSON (TargetModule mname) = object ["module" .= mname] + toJSON (TargetDepsOf dep) = object ["deps" .= dep] + toJSON (TargetCabal cabal) = object ["cabal" .= cabal] + toJSON (TargetPackage pname) = object ["package" .= pname] + toJSON TargetSourced = toJSON ("sourced" :: String) + toJSON TargetStandalone = toJSON ("standalone" :: String) + toJSON TargetAny = toJSON () + +instance FromJSON TargetFilter where + parseJSON j = obj j <|> str' <|> any' where + obj = withObject "target-filter" $ \v -> asum [ + TargetProject <$> v .:: "project", + TargetFile <$> v .:: "file", + TargetModule <$> v .:: "module", + TargetDepsOf <$> v .:: "deps", + TargetCabal <$> v .:: "cabal", + TargetPackage <$> v .:: "package"] + str' = do + s <- parseJSON j :: A.Parser String + case s of + "sourced" -> return TargetSourced + "standalone" -> return TargetStandalone + _ -> empty + any' = (\() -> TargetAny) <$> parseJSON j + +instance ToJSON SearchQuery where + toJSON (SearchQuery q st) = object ["input" .= q, "type" .= st] + +instance FromJSON SearchQuery where + parseJSON = withObject "search-query" $ \v -> SearchQuery <$> v .:: "input" <*> v .:: "type" + +instance ToJSON SearchType where + toJSON SearchExact = toJSON ("exact" :: String) + toJSON SearchPrefix = toJSON ("prefix" :: String) + toJSON SearchInfix = toJSON ("infix" :: String) + toJSON SearchSuffix = toJSON ("suffix" :: String) + toJSON SearchRegex = toJSON ("regex" :: String) + +instance FromJSON SearchType where + parseJSON v = do + str' <- parseJSON v :: A.Parser String + case str' of + "exact" -> return SearchExact + "prefix" -> return SearchPrefix + "infix" -> return SearchInfix + "suffix" -> return SearchInfix + "regex" -> return SearchRegex + _ -> empty
src/HsDev/Symbols.hs view
@@ -19,7 +19,6 @@ importQualifier, -- * Utility - Canonicalize(..), locateProject, searchProject, locateSourceDir, moduleOpts, @@ -50,6 +49,8 @@ import System.Directory import System.FilePath +import System.Directory.Paths + import HsDev.Symbols.Types import HsDev.Symbols.Class import HsDev.Symbols.Documented (Documented(..)) @@ -165,24 +166,17 @@ merge' [] = error "mergeExported: impossible" merge' ds@(d:_) = ExportedDeclaration (map (view declarationModuleId) ds) (view moduleDeclaration d) --- | Canonicalize all paths within something -class Canonicalize a where - canonicalize :: a -> IO a - -instance Canonicalize FilePath where - canonicalize = canonicalizePath - -instance Canonicalize Cabal where - canonicalize Cabal = return Cabal - canonicalize (Sandbox p) = fmap Sandbox $ canonicalizePath p +instance Paths Cabal where + paths _ Cabal = pure Cabal + paths f (Sandbox p) = Sandbox <$> f p -instance Canonicalize Project where - canonicalize (Project nm p c desc) = liftM3 (Project nm) (canonicalizePath p) (canonicalizePath c) (return desc) +instance Paths Project where + paths f (Project nm p c desc) = Project nm <$> f p <*> f c <*> pure desc -instance Canonicalize ModuleLocation where - canonicalize (FileModule f p) = liftM2 FileModule (canonicalizePath f) (traverse canonicalize p) - canonicalize (CabalModule c p n) = fmap (\c' -> CabalModule c' p n) $ canonicalize c - canonicalize (ModuleSource m) = return $ ModuleSource m +instance Paths ModuleLocation where + paths f (FileModule fpath p) = FileModule <$> f fpath <*> traverse (paths f) p + paths f (CabalModule c p n) = CabalModule <$> paths f c <*> pure p <*> pure n + paths _ (ModuleSource m) = pure $ ModuleSource m -- | Find project file is related to locateProject :: FilePath -> IO (Maybe Project)
src/HsDev/Symbols/Location.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings, TemplateHaskell #-} module HsDev.Symbols.Location ( - ModulePackage(..), ModuleLocation(..), moduleStandalone, noLocation, + ModulePackage(..), ModuleLocation(..), moduleStandalone, locationId, noLocation, Position(..), Region(..), region, regionAt, regionLines, regionStr, Location(..), @@ -82,6 +82,11 @@ moduleStandalone :: ModuleLocation -> Bool moduleStandalone = (== Just Nothing) . preview moduleProject + +locationId :: ModuleLocation -> String +locationId (FileModule fpath _) = fpath +locationId (CabalModule cabal mpack nm) = intercalate ":" [show cabal, maybe "" show mpack, nm] +locationId (ModuleSource msrc) = fromMaybe "" msrc instance NFData ModuleLocation where rnf (FileModule f p) = rnf f `seq` rnf p
src/HsDev/Symbols/Util.hs view
@@ -174,8 +174,7 @@ -- | Remove duplicate modules, leave only `preferredModule` uniqueModules :: Cabal -> Maybe Project -> [ModuleId] -> [ModuleId] uniqueModules cabal proj = - catMaybes . - map (preferredModule cabal proj) . + mapMaybe (preferredModule cabal proj) . groupBy ((==) `on` view moduleIdName) . sortBy (comparing (view moduleIdName))
src/HsDev/Tools/AutoFix.hs view
@@ -1,33 +1,35 @@ {-# LANGUAGE TemplateHaskell, OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module HsDev.Tools.AutoFix ( Correction(..), correctionMessage, corrector, correct, corrections, - autoFix_, autoFix, updateRange, + autoFix, CorrectorMatch, correctors, match, findCorrector, - module Data.Mark, + module Data.Text.Region, module HsDev.Tools.Types ) where import Control.Applicative -import Control.Lens (makeLenses, set, view) +import Control.Lens (makeLenses, set, view, (^.), over) import Data.Aeson import Data.Maybe (listToMaybe, mapMaybe) +import Data.Text.Region hiding (Region(..)) +import qualified Data.Text.Region as R -import Data.Mark hiding (at, length) -import HsDev.Symbols.Location +import HsDev.Symbols.Location (Position(..), Region(..)) import HsDev.Tools.Base import HsDev.Tools.Types import HsDev.Util ((.::)) data Correction = Correction { _correctionMessage :: String, - _corrector :: Replace String () } - deriving (Eq, Read, Show) + _corrector :: Replace String } + deriving (Eq, Show) instance ToJSON Correction where toJSON (Correction msg cor) = object [ @@ -39,10 +41,16 @@ v .:: "message" <*> v .:: "corrector" +instance ApplyMap Correction where + applyMap m (Correction msg c) = Correction msg (applyMap m c) + +instance ApplyMap a => ApplyMap (Note a) where + applyMap m = over note (applyMap m) + makeLenses ''Correction -correct :: Correction -> EditM String () -correct c = run [_corrector c] +correct :: Correction -> Edit String +correct c = Chain [_corrector c] corrections :: [Note OutputMessage] -> [Note Correction] corrections = mapMaybe toCorrection where @@ -56,21 +64,12 @@ note (Correction (view (note . message) n) - (replace (fromRegion $ view noteRegion n) sugg)) + (replace (fromRegion $ view noteRegion n) (by sugg))) n -- | Apply corrections -autoFix_ :: [Correction] -> EditM String () -autoFix_ = mapM_ correct - --- | Apply corrections and update rest correction positions -autoFix :: [Correction] -> [Correction] -> EditM String [Correction] -autoFix fix' up' = autoFix_ fix' >> mapM updateRange up' - -updateRange :: Correction -> EditM String Correction -updateRange corr = do - region' <- mapRange $ replaceRange (_corrector corr) - return $ corr { _corrector = (_corrector corr) { replaceRange = region' } } +autoFix :: ApplyMap r => [Correction] -> EditM String r () +autoFix = run . mconcat . map correct type CorrectorMatch = Note OutputMessage -> Maybe (Note Correction) @@ -78,16 +77,15 @@ correctors = [ match "^The (?:qualified )?import of `([\\w\\.]+)' is redundant" $ \_ rgn -> Correction "Redundant import" - (replace - (expandLines rgn) - ""), + (cut + (expandLines rgn)), match "^(.*?)\nFound:\n (.*?)\nWhy not:\n (.*?)$" $ \g rgn -> Correction (g `at` 1) (replace - (rangeFrom rgn `rangeSize` stringSize (length $ g `at` 2)) - (g `at` 3))] + ((rgn ^. regionFrom) `regionSize` pt 0 (length $ g `at` 2)) + (by $ g `at` 3))] -match :: String -> ((Int -> Maybe String) -> Range -> Correction) -> CorrectorMatch +match :: String -> ((Int -> Maybe String) -> R.Region -> Correction) -> CorrectorMatch match pat f n = do g <- matchRx pat (view (note . message) n) return $ set note (f g (fromRegion $ view noteRegion n)) n @@ -95,8 +93,8 @@ findCorrector :: Note OutputMessage -> Maybe (Note Correction) findCorrector n = listToMaybe $ mapMaybe ($ n) correctors -fromRegion :: Region -> Range +fromRegion :: Region -> R.Region fromRegion (Region f t) = fromPosition f `till` fromPosition t fromPosition :: Position -> Point -fromPosition (Position l c) = point (pred l) (pred c) +fromPosition (Position l c) = pt (pred l) (pred c)
src/HsDev/Tools/Cabal.hs view
@@ -43,7 +43,7 @@ (v .:: "name") <*> (v .:: "synopsis") <*> ((join . fmap simpleParse) <$> (v .:: "default-version")) <*> - ((mapMaybe simpleParse) <$> (v .:: "installed-versions")) <*> + (mapMaybe simpleParse <$> (v .:: "installed-versions")) <*> (v .:: "homepage") <*> ((join . fmap simpleParse) <$> (v .:: "license"))
src/HsDev/Tools/Ghc/Check.hs view
@@ -14,22 +14,18 @@ import Control.Lens (preview, view, each, _Just, (^..)) import Control.Monad.Except import Control.Concurrent.FiniteChan -import Data.Maybe (fromMaybe, mapMaybe) -import Data.Version (showVersion) -import Data.Time.Clock (getCurrentTime) +import Data.Maybe (fromMaybe) import HsDev.Tools.Ghc.Worker import System.FilePath (makeRelative) import System.Directory (doesDirectoryExist) -import Text.Read (readMaybe) import GHC hiding (Warning, Module, moduleName) import Outputable -import qualified Packages as GHC -import StringBuffer (stringToStringBuffer) import FastString (unpackFS) import qualified ErrUtils as E -import HsDev.Symbols (Canonicalize(..), moduleOpts) +import System.Directory.Paths +import HsDev.Symbols (moduleOpts) import HsDev.Symbols.Location import HsDev.Symbols.Types import HsDev.Tools.Base @@ -66,7 +62,6 @@ cabalOpt cabal, moduleOpts pkgs m, opts] - tm <- liftIO getCurrentTime clearTargets target <- makeTarget (makeRelative dir file) msrc loadTargets [target]
src/HsDev/Tools/Ghc/Types.hs view
@@ -25,6 +25,7 @@ import PprTyThing import Pretty +import System.Directory.Paths (canonicalize) import HsDev.Symbols import HsDev.Tools.Ghc.Worker import HsDev.Tools.Types
src/HsDev/Tools/Ghc/Worker.hs view
@@ -108,7 +108,7 @@ -- | Get list of installed packages listPackages :: Ghc [ModulePackage] -listPackages = getSessionDynFlags >>= return . mapMaybe readPackage . fromMaybe [] . pkgDatabase +listPackages = liftM (mapMaybe readPackage . fromMaybe [] . pkgDatabase) getSessionDynFlags readPackage :: PackageConfig -> Maybe ModulePackage readPackage pc = readMaybe $ packageNameString pc ++ "-" ++ showVersion (packageVersion pc)
src/HsDev/Tools/GhcMod.hs view
@@ -48,9 +48,10 @@ import System.FilePath (normalise) import Text.Read (readMaybe) -import Language.Haskell.GhcMod (GhcModT, runGhcModT, withOptions) +import Language.Haskell.GhcMod (GhcModT, withOptions) import qualified Language.Haskell.GhcMod as GhcMod -import qualified Language.Haskell.GhcMod.Internal as GhcMod +import qualified Language.Haskell.GhcMod.Monad as GhcMod +import qualified Language.Haskell.GhcMod.Types as GhcMod import Control.Concurrent.Worker import HsDev.Cabal @@ -58,10 +59,10 @@ import HsDev.Symbols import HsDev.Tools.Base import HsDev.Tools.Types -import HsDev.Util ((.::), liftIOErrors, liftThrow, withCurrentDirectory, readFileUtf8, ordNub) +import HsDev.Util ((.::), liftIOErrors, liftThrow, readFileUtf8, ordNub) list :: [String] -> Cabal -> ExceptT String IO [ModuleLocation] -list opts cabal = runGhcMod (GhcMod.defaultOptions { GhcMod.ghcUserOptions = opts }) $ do +list opts cabal = runGhcMod (GhcMod.defaultOptions { GhcMod.optGhcUserOptions = opts }) $ do ms <- (map splitPackage . lines) <$> GhcMod.modules return [CabalModule cabal (readMaybe p) m | (m, p) <- ms] where @@ -70,7 +71,7 @@ browse :: [String] -> Cabal -> String -> Maybe ModulePackage -> ExceptT String IO InspectedModule browse opts cabal mname mpackage = inspect thisLoc (return $ browseInspection opts) $ runGhcMod - (GhcMod.defaultOptions { GhcMod.detailed = True, GhcMod.qualified = True, GhcMod.ghcUserOptions = packageOpt mpackage ++ opts }) $ do + (GhcMod.defaultOptions { GhcMod.optDetailed = True, GhcMod.optQualified = True, GhcMod.optGhcUserOptions = packageOpt mpackage ++ opts }) $ do ds <- (mapMaybe parseDecl . lines) <$> GhcMod.browse mpkgname return Module { _moduleName = fromString mname, @@ -115,7 +116,7 @@ info :: [String] -> Cabal -> FilePath -> String -> GhcModT IO Declaration info opts cabal file sname = do fileCts <- liftIO $ readFileUtf8 file - rs <- withOptions (\o -> o { GhcMod.ghcUserOptions = cabalOpt cabal ++ opts }) $ + rs <- withOptions (\o -> o { GhcMod.optGhcUserOptions = cabalOpt cabal ++ opts }) $ liftM nullToNL $ GhcMod.info file (GhcMod.Expression sname) toDecl fileCts rs where @@ -173,7 +174,7 @@ v .:: "type" typeOf :: [String] -> Cabal -> FilePath -> Int -> Int -> GhcModT IO [TypedRegion] -typeOf opts cabal file line col = withOptions (\o -> o { GhcMod.ghcUserOptions = cabalOpt cabal ++ opts }) $ do +typeOf opts cabal file line col = withOptions (\o -> o { GhcMod.optGhcUserOptions = cabalOpt cabal ++ opts }) $ do fileCts <- liftIO $ readFileUtf8 file let Position line' col' = calcTabs fileCts 8 (Position line col) @@ -218,19 +219,35 @@ check :: [String] -> Cabal -> [FilePath] -> Maybe Project -> GhcModT IO [Note OutputMessage] check opts cabal files _ = do cts <- liftIO $ mapM readFileUtf8 files - withOptions (\o -> o { GhcMod.ghcUserOptions = cabalOpt cabal ++ opts }) $ do + withOptions (\o -> o { GhcMod.optGhcUserOptions = cabalOpt cabal ++ opts }) $ do res <- GhcMod.checkSyntax files return $ map (recalcOutputMessageTabs (zip files cts)) $ parseOutputMessages res lint :: [String] -> FilePath -> GhcModT IO [Note OutputMessage] lint opts file = do cts <- liftIO $ readFileUtf8 file - withOptions (\o -> o { GhcMod.hlintOpts = opts }) $ do + withOptions (\o -> o { GhcMod.optHlintOpts = opts }) $ do res <- GhcMod.lint file return $ map (recalcOutputMessageTabs [(file, cts)]) $ parseOutputMessages res +gmOut :: IO GhcMod.GhcModOut +gmOut = do + ch <- newChan + return GhcMod.GhcModOut { + GhcMod.gmoOptions = GhcMod.OutputOpts { + GhcMod.ooptLogLevel = GhcMod.GmSilent, + GhcMod.ooptStyle = GhcMod.PlainStyle, + GhcMod.ooptLineSeparator = GhcMod.LineSeparator "\0", + GhcMod.ooptLinePrefix = Nothing }, + GhcMod.gmoChan = ch } + runGhcMod :: (GhcMod.IOish m, MonadCatch m) => GhcMod.Options -> GhcModT m a -> ExceptT String m a -runGhcMod opts act = liftIOErrors $ ExceptT $ liftM (left show . fst) $ runGhcModT opts act +runGhcMod opts act = do + out <- liftIO gmOut + cur <- liftIO getCurrentDirectory + liftIOErrors $ ExceptT $ liftM (left show . right fst . fst) $ flip runReaderT out $ GhcMod.unGmOutT $ + GhcMod.withGhcModEnv cur opts $ \env -> + GhcMod.runGhcModT' env GhcMod.defaultGhcModState act locateGhcModEnv :: FilePath -> IO (Either Project Cabal) locateGhcModEnv f = do @@ -246,14 +263,13 @@ home <- getHomeDirectory startWorker (runGhcModT'' $ ghcModEnvPath home p) id liftThrow where - -- TODO: Uncomment comment below after ghc-mod exports neccessary functions - functionNotExported = False runGhcModT'' :: FilePath -> GhcModT IO () -> IO () - runGhcModT'' cur act - | functionNotExported = withCurrentDirectory cur $ - void $ runGhcModT GhcMod.defaultOptions $ act `catchError` (void . return) - | otherwise = do - void $ GhcMod.runGhcModT' cur GhcMod.defaultOptions $ act `catchError` (void . return) + runGhcModT'' cur act = void $ do + out <- gmOut + flip runReaderT out $ + GhcMod.unGmOutT $ + GhcMod.withGhcModEnv cur GhcMod.defaultOptions $ \env -> + GhcMod.runGhcModT' env GhcMod.defaultGhcModState (act `catchError` (void . return)) type WorkerMap = MVar (M.Map FilePath (Worker (GhcModT IO))) @@ -261,13 +277,19 @@ ghcModMultiWorker :: IO (Worker (ReaderT WorkerMap IO)) ghcModMultiWorker = newMVar M.empty >>= \m -> startWorker (`runReaderT` m) id id +instance MonadThrow (GhcMod.GmOutT IO) where + throwM = lift . throwM + +instance MonadCatch (GhcMod.GmOutT IO) where + catch = gcatch + instance MonadThrow (GhcModT IO) where throwM = lift . throwM instance MonadCatch (GhcModT IO) where catch = gcatch -dispatch :: FilePath -> GhcModT IO a -> ReaderT WorkerMap IO (Task a) +dispatch :: FilePath -> GhcModT IO a -> ReaderT WorkerMap IO (Async a) dispatch file act = do mvar <- ask home <- liftIO getHomeDirectory @@ -282,8 +304,8 @@ waitMultiGhcMod :: Worker (ReaderT WorkerMap IO) -> FilePath -> GhcModT IO a -> ExceptT String IO a waitMultiGhcMod w f = liftIO . pushTask w . dispatch f >=> - asExceptT . taskWait >=> - asExceptT . taskWait + asExceptT . waitCatch >=> + asExceptT . waitCatch where asExceptT :: Monad m => m (Either SomeException a) -> ExceptT String m a asExceptT = ExceptT . liftM (left (\(SomeException e) -> show e))
src/HsDev/Tools/HLint.hs view
@@ -15,7 +15,7 @@ import Language.Haskell.Exts.SrcLoc import qualified Language.Haskell.HLint3 as HL (Severity(..)) -import HsDev.Symbols (Canonicalize(..)) +import System.Directory.Paths (canonicalize) import HsDev.Symbols.Location import HsDev.Tools.Base import HsDev.Util (readFileUtf8, split)
src/HsDev/Tools/Hayoo.hs view
@@ -64,7 +64,7 @@ [] -> hayooName f (m:_) -> m ++ "." ++ hayooName f symbolDocs = Just . fromString . hayooDescription - symbolLocation r = Location (ModuleSource $ Just $ resultUri r) Nothing where + symbolLocation r = Location (ModuleSource $ Just $ resultUri r) Nothing instance Documented HayooSymbol where brief f
src/HsDev/Tools/Types.hs view
@@ -11,7 +11,8 @@ import Control.Monad import Data.Aeson hiding (Error) -import HsDev.Symbols (Canonicalize(..)) +import System.Directory.Paths +import HsDev.Symbols () import HsDev.Symbols.Location import HsDev.Util ((.::), (.::?)) @@ -43,6 +44,7 @@ _noteRegion :: Region, _noteLevel :: Maybe Severity, _note :: a } + deriving (Eq, Show) makeLenses ''Note @@ -70,13 +72,14 @@ recalcTabs cts n' (Note s r l n) = Note s (recalcTabs cts n' r) l n calcTabs cts n' (Note s r l n) = Note s (calcTabs cts n' r) l n -instance Canonicalize (Note a) where - canonicalize (Note s r l n) = Note <$> canonicalize s <*> pure r <*> pure l <*> pure n +instance Paths (Note a) where + paths f (Note s r l n) = Note <$> paths f s <*> pure r <*> pure l <*> pure n -- | Output message from some tool (ghc, ghc-mod, hlint) with optional suggestion data OutputMessage = OutputMessage { _message :: String, _messageSuggestion :: Maybe String } + deriving (Eq, Ord, Read, Show) instance NFData OutputMessage where rnf (OutputMessage m s) = rnf m `seq` rnf s
src/HsDev/Util.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleContexts, OverloadedStrings #-} module HsDev.Util ( withCurrentDirectory, @@ -10,9 +10,9 @@ -- * String utils tab, tabs, trim, split, -- * Other utils - ordNub, + ordNub, uniqueBy, mapBy, -- * Helper - (.::), (.::?), objectUnion, + (.::), (.::?), objectUnion, jsonUnion, -- * Exceptions liftException, liftE, liftEIO, tries, triesMap, liftExceptionM, liftIOErrors, eitherT, @@ -20,24 +20,29 @@ -- * UTF-8 fromUtf8, toUtf8, readFileUtf8, writeFileUtf8, -- * IO - hGetLineBS, logException, logIO, ignoreIO, - -- * Task - liftTask, + hGetLineBS, logException, logIO, ignoreIO, logAsync, + -- * Async + liftAsync, + -- * Command line + FromCmd(..), + cmdJson, withCmd, guardCmd, + withHelp, cmd, parseArgs, -- * Reexportss module Control.Monad.Except, MonadIO(..) ) where -import Control.Arrow (second, left) +import Control.Arrow (second, left, (&&&)) import Control.Exception import Control.Monad import Control.Monad.Except import qualified Control.Monad.Catch as C -import Data.Aeson -import Data.Aeson.Types (Parser) +import Data.Aeson hiding (Result(..), Error) +import qualified Data.Aeson.Types as A import Data.Char (isSpace) import Data.List (isPrefixOf, unfoldr) +import qualified Data.Map as M import Data.Maybe (catMaybes) import qualified Data.Set as Set import qualified Data.HashMap.Strict as HM (HashMap, toList, union) @@ -47,11 +52,12 @@ import Data.Text (Text) import qualified Data.Text.Lazy as T import qualified Data.Text.Lazy.Encoding as T +import Options.Applicative import System.Directory import System.FilePath import System.IO -import Control.Concurrent.Task +import Control.Concurrent.Async -- | Run action with current directory set withCurrentDirectory :: FilePath -> IO a -> IO a @@ -123,19 +129,25 @@ split :: (a -> Bool) -> [a] -> [[a]] split p = takeWhile (not . null) . unfoldr (Just . second (drop 1) . break p) --- | ordNub is quadratic, https://github.com/nh2/haskell-ordnub/#ordnub +-- | nub is quadratic, https://github.com/nh2/haskell-ordnub/#ordnub ordNub :: Ord a => [a] -> [a] -ordNub l = go Set.empty l where +ordNub = go Set.empty where go _ [] = [] go s (x:xs) | x `Set.member` s = go s xs | otherwise = x : go (Set.insert x s) xs +uniqueBy :: Ord b => (a -> b) -> [a] -> [a] +uniqueBy f = M.elems . mapBy f + +mapBy :: Ord b => (a -> b) -> [a] -> M.Map b a +mapBy f = M.fromList . map (f &&& id) + -- | Workaround, sometimes we get HM.lookup "foo" v == Nothing, but lookup "foo" (HM.toList v) == Just smth -(.::) :: FromJSON a => HM.HashMap Text Value -> Text -> Parser a +(.::) :: FromJSON a => HM.HashMap Text Value -> Text -> A.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) +(.::?) :: FromJSON a => HM.HashMap Text Value -> Text -> A.Parser (Maybe a) v .::? name = traverse parseJSON $ lookup name $ HM.toList v -- | Union two JSON objects @@ -145,9 +157,13 @@ objectUnion _ (Object r) = Object r objectUnion _ _ = Null +-- | Union two JSON objects +jsonUnion :: (ToJSON a, ToJSON b) => a -> b -> Value +jsonUnion x y = objectUnion (toJSON x) (toJSON y) + -- | Lift IO exception to ExceptT liftException :: C.MonadCatch m => m a -> ExceptT String m a -liftException = ExceptT . liftM (left $ \(SomeException e) -> show e) . C.try +liftException = ExceptT . liftM (left $ \(SomeException e) -> displayException e) . C.try -- | Same as @liftException@ liftE :: C.MonadCatch m => m a -> ExceptT String m a @@ -167,7 +183,7 @@ -- | Lift IO exception to MonadError liftExceptionM :: (C.MonadCatch m, MonadError String m) => m a -> m a liftExceptionM act = C.catch act onError where - onError = throwError . (\(SomeException e) -> show e) + onError = throwError . (\(SomeException e) -> displayException e) -- | Lift IO exceptions to ExceptT liftIOErrors :: C.MonadCatch m => ExceptT String m a -> ExceptT String m a @@ -204,15 +220,51 @@ logException :: String -> (String -> IO ()) -> IO () -> IO () logException pre out = handle onErr where onErr :: SomeException -> IO () - onErr e = out $ pre ++ show e + onErr e = out $ pre ++ displayException e logIO :: String -> (String -> IO ()) -> IO () -> IO () logIO pre out = handle onIO where onIO :: IOException -> IO () - onIO e = out $ pre ++ show e + onIO e = out $ pre ++ displayException e +logAsync :: (String -> IO ()) -> IO () -> IO () +logAsync out = handle onAsync where + onAsync :: AsyncException -> IO () + onAsync e = out (displayException e) >> throwIO e + ignoreIO :: IO () -> IO () ignoreIO = handle (const (return ()) :: IOException -> IO ()) -liftTask :: (C.MonadThrow m, C.MonadCatch m, MonadIO m) => IO (Task a) -> ExceptT String m a -liftTask = liftExceptionM . ExceptT . liftIO . liftM (left show) . join . liftM taskWait +liftAsync :: (C.MonadThrow m, C.MonadCatch m, MonadIO m) => IO (Async a) -> ExceptT String m a +liftAsync = liftExceptionM . ExceptT . liftIO . liftM (left displayException) . join . liftM waitCatch + +class FromCmd a where + cmdP :: Parser a + +cmdJson :: String -> [A.Pair] -> Value +cmdJson nm ps = object $ ("command" .= nm) : ps + +withCmd :: String -> (Object -> A.Parser a) -> Value -> A.Parser a +withCmd nm fn = withObject ("command " ++ nm) $ \v -> guardCmd nm v *> fn v + +guardCmd :: String -> Object -> A.Parser () +guardCmd nm obj = do + cmdName <- obj .:: "command" + guard (nm == cmdName) + +-- | Add help command to parser +withHelp :: Parser a -> Parser a +withHelp = (helper' <*>) where + helper' = abortOption ShowHelpText $ long "help" <> short '?' <> help "show help" <> hidden + +-- | Subcommand +cmd :: String -> String -> Parser a -> Mod CommandFields a +cmd n d p = command n (info (withHelp p) (progDesc d)) + +-- | Parse arguments or return help +parseArgs :: String -> ParserInfo a -> [String] -> Either String a +parseArgs nm p = handle' . execParserPure (prefs mempty) (p { infoParser = withHelp (infoParser p) }) where + handle' :: ParserResult a -> Either String a + handle' (Success r) = Right r + handle' (Failure f) = Left $ fst $ renderFailure f nm + handle' _ = Left "error: completion invoked result"
− src/System/Console/Args.hs
@@ -1,295 +0,0 @@-{-# LANGUAGE FlexibleInstances, TupleSections #-} - -module System.Console.Args ( - Args(..), Opts(..), Arg(..), Opt(..), - withOpts, defOpts, defArgs, selectOpts, splitOpts, - (%--), (%-?), hoist, has, arg, narg, iarg, listArg, flagSet, - flag, req, list, manyReq, - desc, alias, short, - parse, parse_, tryParse, toArgs, info, - - -- * Helpers - splitArgs, unsplitArgs, verify, - - module Data.Help - ) where - -import Control.Arrow -import Control.Applicative -import Control.Monad -import Control.Monad.Loops -import Data.Aeson -import Data.Char -import qualified Data.HashMap.Strict as HM -import Data.List -import Data.Map (Map) -import qualified Data.Map as M -import Data.Maybe -import Data.String (fromString) -import qualified Data.Text as T -import Text.Read (readMaybe) - -import Data.Help -import Text.Format - -data Args = Args { - posArgs :: [String], - namedArgs :: Opts String } - deriving (Eq, Show) - -instance Monoid Args where - mempty = Args [] mempty - (Args largs lopts) `mappend` (Args rargs ropts) = Args (largs ++ rargs) (lopts `mappend` ropts) - -newtype Opts a = Opts { getOpts :: Map String [a] } deriving (Eq, Show) - ---instance Eq a => Eq (Opts a) where --- Opts l == Opts r = l == r - -instance Functor Opts where - fmap f = Opts . fmap (fmap f) . getOpts - -instance Foldable Opts where - foldMap f = foldMap (foldMap f) . getOpts - -instance Traversable Opts where - traverse f = fmap Opts . traverse (traverse f) . getOpts - -instance Monoid (Opts a) where - mempty = Opts mempty - (Opts l) `mappend` (Opts r) = Opts $ M.unionWith mappend l r - -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 - -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 [] - Number n' -> return <$> parseJSON (toJSON $ show n') - _ -> (return <$> parseJSON v) <|> parseJSON v - -data Arg = Flag | Required String | List String deriving (Eq, Ord, Show) - -argName :: Arg -> Maybe String -argName Flag = Nothing -argName (Required n) = Just n -argName (List n) = Just $ n ++ "..." - -data Opt = Opt { - optName :: String, - optShort :: [Char], - optLong :: [String], - optDescription :: Maybe String, - optArg :: Arg } - deriving (Eq, Show) - -withOpts :: (Opts String -> Opts String) -> Args -> Args -withOpts f (Args a o) = Args a (f o) - --- | Set default values, if option doesn't present -defOpts :: Opts String -> Opts String -> Opts String -defOpts (Opts def) (Opts new) = Opts $ new `M.union` def - -defArgs :: Opts String -> Args -> Args -defArgs = withOpts . defOpts - -selectOpts :: [Opt] -> Opts a -> Opts a -selectOpts opts = Opts . M.filterWithKey (\n _ -> n `elem` optNames) . getOpts where - optNames = map optName opts - -splitOpts :: [Opt] -> Opts a -> (Opts a, Opts a) -splitOpts opts = (Opts *** Opts) . M.partitionWithKey (\n _ -> n `elem` optNames) . getOpts where - optNames = map optName opts - -(%--) :: Format a => String -> a -> Opts String -n %-- v = Opts $ M.singleton n [format v] - -(%-?) :: Format a => String -> Maybe a -> Opts String -n %-? v = maybe mempty (n %--) v - --- | Make 'Opts' with flag set -hoist :: String -> Opts a -hoist n = Opts $ M.singleton n [] - -has :: String -> Opts a -> Bool -has n = M.member n . getOpts - --- | Get argument value -arg :: String -> Opts a -> Maybe a -arg n = (M.lookup n . getOpts) >=> listToMaybe - --- | Get numeric value -narg :: (Read a, Num a) => String -> Opts String -> Maybe a -narg n = join . fmap readMaybe . arg n - --- | Get integer value -iarg :: String -> Opts String -> Maybe Integer -iarg = narg - --- | Get list argument -listArg :: String -> Opts a -> [a] -listArg n = fromMaybe [] . M.lookup n . getOpts - --- | Is flag set -flagSet :: String -> Opts a -> Bool -flagSet n = isJust . M.lookup n . getOpts - --- | Flag option -flag :: String -> Opt -flag n = Opt n [] [] Nothing Flag - --- | Required option -req :: String -> String -> Opt -req n v = Opt n [] [] Nothing (Required v) - --- | List option -list :: String -> String -> Opt -list n v = Opt n [] [] Nothing (List v) - --- | Convert req option to list -manyReq :: Opt -> Opt -manyReq o@(Opt { optArg = (Required n) }) = o { optArg = List n } -manyReq _ = error "manyReq: invalid argument" - --- | Set description --- --- >flag "quiet" `desc` "quiet mode" -desc :: Opt -> String -> Opt -desc o d = o { optDescription = Just d } - --- | Set aliases --- --- >fliag "quiet" `alias` -alias :: Opt -> [String] -> Opt -alias o ls = o { optLong = optLong o ++ ls } - --- | Shortcuts -short :: Opt -> [Char] -> Opt -short o ss = o { optShort = optShort o ++ ss } - -findOpt :: String -> [Opt] -> Maybe Opt -findOpt n = find opt' where - opt' :: Opt -> Bool - opt' (Opt n' s l _ _) = n `elem` (n' : (map return s ++ l)) - -parse :: [Opt] -> [String] -> Either String Args -parse os = unfoldrM parseCmd >=> (verify os . mconcat) where - parseCmd :: [String] -> Either String (Maybe (Args, [String])) - parseCmd [] = Right Nothing - parseCmd (cmd:cmds) - | isFlag cmd = do - opt' <- lookOpt cmd os - case optArg opt' of - Flag -> Right $ Just (Args [] $ Opts $ M.singleton (optName opt') [], cmds) - Required _ -> case cmds of - (value:cmds') - | not (isFlag value) -> Right $ Just (Args [] $ Opts $ M.singleton (optName opt') [value], cmds') - | otherwise -> Left $ "No value specified for option '$'" ~~ optName opt' - [] -> Left $ "No value specified for option '" ++ optName opt' ++ "'" - List _ -> case cmds of - (value:cmds') - | not (isFlag value) -> Right $ Just (Args [] $ Opts $ M.singleton (optName opt') [value], cmds') - | otherwise -> Left $ "No value specified for option '$'" ~~ optName opt' - [] -> Left $ "No value specified for option '$'" ~~ optName opt' - | otherwise = Right $ Just (Args [cmd] mempty, cmds) - - lookOpt :: String -> [Opt] -> Either String Opt - lookOpt n = maybe (Left $ "Invalid option '$'" ~~ n) Right . findOpt (dropWhile (== '-') n) - --- | Parse with no options declarations -parse_ :: [String] -> Args -parse_ = mconcat . unfoldr parseCmd where - parseCmd :: [String] -> Maybe (Args, [String]) - parseCmd [] = Nothing - parseCmd (cmd:cmds) - | isFlag cmd = case cmds of - (value:cmds') - | not (isFlag value) -> Just (Args [] $ Opts $ M.singleton cmd [value], cmds') - | otherwise -> Just (Args [] $ Opts $ M.singleton cmd [], cmds) - [] -> Just (Args [] $ Opts $ M.singleton cmd [], []) - | otherwise = Just (Args [cmd] mempty, cmds) - -tryParse :: [Opt] -> [String] -> Args -tryParse os s = either (const $ parse_ s) id $ parse os s - -toArgs :: Args -> [String] -toArgs (Args p o) = p ++ (concatMap toArgs' . M.toList . getOpts $ o) where - toArgs' :: (String, [String]) -> [String] - toArgs' (n, []) = ["--" ++ n] - toArgs' (n, vs) = concat [["--" ++ n, v] | v <- vs] - -instance Help Opt where - brief (Opt n _ _ _ arg') = concat [ - longOpt n, - maybe "" (" " ++) $ argName arg'] - help (Opt n ss ls desc' arg') = [concat [ - unwords (map shortOpt ss ++ map longOpt (n : ls)), - maybe "" (" " ++) $ argName arg', - maybe "" (" -- " ++) desc']] - -instance Help [Opt] where - brief = unwords . map ((\s -> "[" ++ s ++ "]") . brief) - help = concatMap help - -info :: [Opt] -> String -info = unlines . indented - -splitArgs :: String -> [String] -splitArgs "" = [] -splitArgs (c:cs) - | isSpace c = splitArgs cs - | c == '"' = let (w, cs') = readQuote cs in w : splitArgs cs' - | otherwise = let (ws, tl) = break isSpace cs in (c:ws) : splitArgs tl - where - readQuote :: String -> (String, String) - readQuote "" = ("", "") - readQuote ('\\':ss) - | null ss = ("\\", "") - | otherwise = first (head ss :) $ readQuote (tail ss) - readQuote ('"':ss) = ("", ss) - readQuote (s:ss) = first (s:) $ readQuote ss - -unsplitArgs :: [String] -> String -unsplitArgs = unwords . map escape where - escape :: String -> String - escape str - | any isSpace str || '"' `elem` str = "\"" ++ concat (unfoldr escape' str) ++ "\"" - | otherwise = str - escape' :: String -> Maybe (String, String) - escape' [] = Nothing - escape' (ch:tl) = Just (escaped, tl) where - escaped = case ch of - '"' -> "\\\"" - '\\' -> "\\\\" - _ -> [ch] - -verify :: [Opt] -> Args -> Either String Args -verify os = withOpts' $ fmap (Opts . M.fromList) . mapM (uncurry verify') . M.toList . getOpts where - withOpts' :: Functor f => (Opts String -> f (Opts String)) -> Args -> f Args - withOpts' f (Args a o) = Args a <$> f o - verify' :: String -> [String] -> Either String (String, [String]) - verify' n v = case findOpt n os of - Nothing -> Left $ "Invalid option '$'" ~~ n - Just opt -> maybe (Right (n, v)) Left $ case (optArg opt, v) of - (Flag, []) -> Nothing - (Flag, _) -> Just $ "Flag '$' has a value" ~~ n - (Required _, []) -> Just $ "No value for '$'" ~~ n - (Required _, [_]) -> Nothing - (Required _, _:_) -> Just $ "Too much values for '$'" ~~ n - (List _, []) -> Just $ "No values for '$'" ~~ n - (List _, _) -> Nothing - -isFlag :: String -> Bool -isFlag ('-':'-':s) = not $ null s -isFlag ('-':_:[]) = True -isFlag _ = False - -longOpt :: String -> String -longOpt = ("--" ++) - -shortOpt :: Char -> String -shortOpt = ('-':) . return
− src/System/Console/Cmd.hs
@@ -1,168 +0,0 @@-module System.Console.Cmd ( - CmdAction, notMatch, failMatch, runCmd, defaultOpts, validateArgs, alterArgs, - Cmd(..), cmdAct, cutName, cmda, cmda_, cmd, cmd_, defCmd, - CmdHelp(..), helpCommand, withHelp, printWith, - run, runArgs, runOn, - - module System.Console.Args - ) where - -import Control.Arrow (Arrow((&&&))) -import Control.Monad () -import Data.List (stripPrefix, isPrefixOf) -import Control.Monad.Except -import Data.Map () -import Data.Maybe -import qualified Data.Map as M (delete) - -import System.Console.Args -import Text.Format ((~~), (%)) - -type CmdAction a = ExceptT String Maybe a - --- | Arguments doesn't match command -notMatch :: CmdAction a -notMatch = lift Nothing - --- | Invalid command arguments -failMatch :: String -> CmdAction a -failMatch = throwError - -data Cmd a = Cmd { - cmdName :: String, - cmdArgs :: [String], - cmdOpts :: [Opt], - cmdDesc :: String, - cmdGetArgs :: Args -> CmdAction Args, - -- ^ Get command arguments from source arguments, by default it cuts command name - cmdAction :: Args -> CmdAction a } - -instance Functor Cmd where - fmap f cmd' = cmd' { - cmdAction = fmap f . cmdAction cmd' } - --- | Run cmd -runCmd :: Cmd a -> Args -> CmdAction a -runCmd c = cmdGetArgs c >=> cmdAction c - --- | Set default opts -defaultOpts :: Opts String -> Cmd a -> Cmd a -defaultOpts opts = alterArgs (cmdAct $ withOpts $ defOpts opts) - --- | Validate Args in command -validateArgs :: (Args -> CmdAction ()) -> Cmd a -> Cmd a -validateArgs p c = c { - cmdAction = \a -> p a >> cmdAction c a } - --- | Alter Args in command -alterArgs :: (Args -> CmdAction Args) -> Cmd a -> Cmd a -alterArgs f c = c { - cmdGetArgs = f >=> cmdGetArgs c } - --- | Make CmdAction function -cmdAct :: (b -> a) -> b -> CmdAction a -cmdAct f = return . f - --- | Cut name of command from arguments and checks if it matches --- --- > cutName >=> cmdAct act -cutName :: String -> Args -> CmdAction Args -cutName name (Args as os) = case stripPrefix (words name) as of - Just as' -> return (Args as' os) - Nothing -> notMatch - -verifyOpts :: [Opt] -> Args -> CmdAction Args -verifyOpts os = ExceptT . Just . verify os - -cmda :: String -> [String] -> [Opt] -> String -> (Args -> CmdAction a) -> Cmd a -cmda name as os cdesc act = Cmd { - cmdName = name, - cmdArgs = as, - cmdOpts = os, - cmdDesc = cdesc, - cmdGetArgs = cut', - cmdAction = verifyOpts os >=> act } - where - cut' - | null name = return - | otherwise = cutName name - -cmda_ :: String -> [Opt] -> String -> (Opts String -> CmdAction a) -> Cmd a -cmda_ name os cdesc act = validateArgs noPos $ cmda name [] os cdesc (act . namedArgs) where - noPos (Args [] _) = return () - noPos (Args _ _) = failMatch "No positional argument expected" - -cmd :: String -> [String] -> [Opt] -> String -> (Args -> a) -> Cmd a -cmd name as os cdesc act = cmda name as os cdesc (cmdAct act) - -cmd_ :: String -> [Opt] -> String -> (Opts String -> a) -> Cmd a -cmd_ name os cdesc act = cmda_ name os cdesc (cmdAct act) - --- | Unnamed command -defCmd :: [String] -> [Opt] -> String -> (Args -> a) -> Cmd a -defCmd as os cdesc act = cmda "" as os cdesc (cmdAct act) - -data CmdHelp = - HelpUsage [String] | - HelpCommands [(String, [String])] - deriving (Eq, Ord, Read, Show) - --- | Make help command, which will show help on for specified commands -helpCommand :: String -> (Either String CmdHelp -> a) -> [Cmd a] -> Cmd a -helpCommand tool toCmd cmds = helpcmd where - helpcmd = fmap toCmd $ alterArgs (cmdAct checkHelp) $ cmd - "help" - ["command"] - [flag "help" `short` ['?'] `desc` "show help (when using form 'command -?' or 'command --help')"] - ("help command, can be called in form '$ [command] -?' or '$ [command] --help" ~~ tool % tool) - onHelp - checkHelp :: Args -> Args - checkHelp a - | flagSet "help" (namedArgs a) = a { - posArgs = "help" : posArgs a, - namedArgs = Opts $ M.delete "help" $ getOpts $ namedArgs a } - | otherwise = a - onHelp (Args [] _) = Right $ HelpUsage [tool ++ " " ++ brief c | c <- (helpcmd:cmds)] - onHelp (Args cmdname _) = case filter ((cmdname `isPrefixOf`) . words . cmdName) (helpcmd:cmds) of - [] -> Left $ unlines $ ("Unknown command: " ++ unwords cmdname) : tryOut - helps -> Right $ HelpCommands $ map (cmdName &&& (addHeader . indented)) helps - where - pre = unwords cmdname - maybeCmds = filter (pre `isPrefixOf`) $ map cmdName (helpcmd:cmds) - tryOut = case maybeCmds of - [] -> [] - _ -> "\tMaybe you mean:" : map ("\t\t" ++) maybeCmds - addHeader [] = [] - addHeader (h:hs) = (tool ++ " " ++ h) : hs - --- | Add help command -withHelp :: String -> (Either String CmdHelp -> a) -> [Cmd a] -> [Cmd a] -withHelp tool toCmd cmds = helpCommand tool toCmd cmds : cmds - -printWith :: (String -> a) -> (Either String CmdHelp -> a) -printWith fn = fn . either id (unlines . print') where - print' :: CmdHelp -> [String] - print' (HelpUsage u) = map ('\t':) u - print' (HelpCommands cs) = map ('\t':) $ concatMap snd cs - -instance Help (Cmd a) where - brief c = unwords $ filter (not . null) $ [cmdName c, unwords (map angled (cmdArgs c)), brief (cmdOpts c)] ++ desc' where - angled s = "<" ++ s ++ ">" - desc' - | null (cmdDesc c) = [] - | otherwise = ["-- " ++ cmdDesc c] - help = help . cmdOpts - --- | Run commands -run :: [Cmd a] -> a -> (String -> a) -> [String] -> a -run cmds onDef onError = runOn cmds onDef onError (tryParse . cmdOpts) - --- | Run commands with parsed args -runArgs :: [Cmd a] -> a -> (String -> a) -> Args -> a -runArgs cmds onDef onError = runOn cmds onDef onError (const id) - --- | Run commands with -runOn :: [Cmd a] -> a -> (String -> a) -> (Cmd a -> c -> Args) -> c -> a -runOn cmds onDef onError f as = maybe onDef (either onError id) found where - found = listToMaybe $ mapMaybe (runExceptT . (`act` as)) cmds - act c = runCmd c . f c
+ src/System/Directory/Paths.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} + +module System.Directory.Paths ( + Paths(..), + canonicalize + ) where + +import Control.Lens +import System.Directory + +-- | Something with paths inside +class Paths a where + paths :: Traversal' a FilePath + +instance Paths FilePath where + paths = id + +-- | Canonicalize all paths +canonicalize :: Paths a => a -> IO a +canonicalize = paths canonicalizePath
src/System/Win32/FileMapping/NamePool.hs view
@@ -24,7 +24,7 @@ -- | Use free name from pool withName :: Pool -> (String -> IO a) -> IO a -withName p act = bracket getName freeName act where +withName p = bracket getName freeName where getName = modifyMVar (poolFreeNames p) $ \names -> case names of [] -> liftM ((,) []) $ poolNewName p (n:ns) -> return (ns, n)
− src/Text/Format.hs
@@ -1,98 +0,0 @@-{-# LANGUAGE FlexibleInstances, UndecidableInstances, DefaultSignatures #-} - --- | Format module --- --- >"My name is $, I am ${age} years old, I am from $" %~ ("Vasya" % ("age" %= 20) % "Moscow") --- >"My name is Vasya, I am 20 years old" -module Text.Format ( - Format(..), FormatArgs, Hole(..), - (%~), (~~), (%), (%=) - ) where - -import Control.Arrow (first) -import Data.List (delete, isPrefixOf) -import Data.String -import Text.Regex.PCRE - -class Format a where - format :: a -> String - default format :: Show a => a -> String - format = show - -instance Format String where - format = id -instance Format Int where - format = show -instance Format Integer where - format = show -instance Format Bool where - format = show - -type FormatArgs = [(Maybe String, String)] - -class Hole a where - hole :: a -> FormatArgs - -instance Hole FormatArgs where - hole = id - -instance {-# OVERLAPPABLE #-} Format a => Hole a where - hole v = [(Nothing, format v)] - -instance Format a => Hole (String, a) where - hole (n, v) = [(Just n, format v)] - -instance Hole [(String, String)] where - hole = map (first Just) - -infixr 1 %~ - -(%~) :: (Hole a, IsString s) => String -> a -> Either String s -fmt %~ hargs = fmap fromString $ case fmt =~ "\\$(\\{([a-zA-Z]+)\\})?" of - (pre, "", "", []) -> Right pre - (pre, _, post, []) -> Right $ pre ++ post - (pre, _, post, gs) -> do - let - name = case gs of - _:name':_ -> name' - _ -> "" - if null name && "$" `isPrefixOf` post - then do - post' <- tail post %~ hargs - return $ pre ++ "$" ++ post' - else do - (arg', args') <- split' name - post' <- post %~ args' - return $ pre ++ arg' ++ post' - where - args = hole hargs - - split' :: String -> Either String (String, FormatArgs) - split' n = maybe - (Left $ maybe - (concat [ - "Format: not enough arguments for format string '", - fmt, - "'"]) - ("Format argument '$' not found" ~~) n') - (\v -> Right (v, delete (n', v) args)) - (lookup n' args) - where - n' - | null n = Nothing - | otherwise = Just n - -infixr 1 ~~ - -(~~) :: (Hole a, IsString s) => String -> a -> s -fmt ~~ hargs = either error id $ fmt %~ hargs - -infixr 5 % - -(%) :: (Hole a, Hole b) => a -> b -> FormatArgs -x % y = hole x ++ hole y - -infixr 1 %= - -(%=) :: Format a => String -> a -> (String, String) -name %= value = (name, format value)
tools/Tool.hs view
@@ -2,80 +2,35 @@ module Tool ( -- * Tool - ToolM, toolMain, usage, - -- * Json command - jsonCmd, jsonCmd_, - -- * Errors - toolError, - -- * Options - prettyOpt, isPretty, lispOpt, isLisp, + ToolM, toolMain, printExceptT, printResult, - module System.Console.Cmd + module Options.Applicative, + module HsDev.Util ) where -import Control.Monad.Except (runExceptT, throwError) +import Control.Monad.Except (ExceptT, runExceptT) +import Control.Monad.IO.Class +import Control.Monad (liftM) import Data.Aeson -import Data.Aeson.Encode.Pretty (encodePretty) -import qualified Data.ByteString.Lazy.Char8 as L (ByteString, putStrLn) +import qualified Data.ByteString.Lazy.Char8 as L (putStrLn) +import Options.Applicative import System.Environment import System.IO -import Data.Lisp (encodeLisp) import HsDev.Tools.Base (ToolM) - -import System.Console.Cmd +import HsDev.Util (cmd, parseArgs) -- | Run tool with commands -toolMain :: String -> [Cmd (IO ())] -> IO () -toolMain name commands = do +toolMain :: String -> String -> Parser a -> (a -> IO ()) -> IO () +toolMain name d p act = do hSetBuffering stdout LineBuffering hSetEncoding stdout utf8 hSetEncoding stdin utf8 - getArgs >>= run toolCmds unknownCmd onError - where - onError :: String -> IO () - onError = putStrLn - - unknownCmd :: IO () - unknownCmd = putStrLn "Unknown command" >> usage name toolCmds - - toolCmds = withHelp name (printWith putStrLn) commands - --- | Print usage -usage :: String -> [Cmd (IO ())] -> IO () -usage toolName = mapM_ (putStrLn . ('\t':) . ((toolName ++ " ") ++) . brief) - --- | Command with JSONable result -jsonCmd :: ToJSON a => String -> [String] -> [Opt] -> String -> (Args -> ToolM a) -> Cmd (IO ()) -jsonCmd name pos os descr act = cmd name pos ([prettyOpt, lispOpt] ++ os) descr $ \(Args as opts) -> do - r <- runExceptT $ act (Args as opts) - L.putStrLn $ either (toStr opts . errorStr) (toStr opts) r - where - toStr :: ToJSON a => Opts String -> a -> L.ByteString - toStr opts - | isLisp opts = encodeLisp - | isPretty opts = encodePretty - | otherwise = encode - - errorStr :: String -> Value - errorStr s = object ["error" .= s] - --- | `jsonCmd` with the only '--pretty' option -jsonCmd_ :: ToJSON a => String -> [String] -> String -> ([String] -> ToolM a) -> Cmd (IO ()) -jsonCmd_ name pos descr act = jsonCmd name pos [] descr (act . posArgs) - --- | Fail with error -toolError :: String -> ToolM a -toolError = throwError - -prettyOpt :: Opt -prettyOpt = flag "pretty" `desc` "pretty JSON output" - -isPretty :: Opts String -> Bool -isPretty = flagSet "pretty" + res <- liftM (parseArgs name (info p (progDesc d))) getArgs + either putStrLn act res -lispOpt :: Opt -lispOpt = flag "lisp" `desc` "s-exp output" `short` ['l'] +printExceptT :: ExceptT String IO () -> IO () +printExceptT act = runExceptT act >>= either putStrLn return -isLisp :: Opts String -> Bool -isLisp = flagSet "lisp" +printResult :: (ToJSON a, MonadIO m) => m a -> m () +printResult act = act >>= liftIO . L.putStrLn . encode
tools/hsautofix.hs view
@@ -2,16 +2,18 @@ main ) where -import Control.Lens (each, view, preview) +import Control.Lens (view, preview) import Control.Arrow ((***)) import Control.Monad (liftM) +import Control.Monad.State (gets) +import Control.Monad.Except (throwError) import Data.Aeson import Data.List (partition, sort) import Data.Maybe (mapMaybe) import System.Directory (canonicalizePath) -import Text.Read (readMaybe) -import HsDev.Symbols (Canonicalize(..), moduleFile) +import System.Directory.Paths (canonicalize) +import HsDev.Symbols (moduleFile) import HsDev.Tools.Base import HsDev.Tools.AutoFix import HsDev.Tools.GhcMod (parseOutputMessages) @@ -19,51 +21,45 @@ import Tool -main :: IO () -main = toolMain "hsautofix" [ - jsonCmd "show" [] [jsonArg] "show what can be auto-fixed" show', - jsonCmd "fix" [] [nList, pureArg] "fix selected errors" fix'] - where - nList = list "num" "index" `short` ['n'] `desc` "corrrection indices to apply, if nothing specified - all corrections applies" - pureArg = flag "pure" `desc` "don't modify files, just return updated rest corrections" - jsonArg = flag "json" `desc` "output messages in JSON format" +data FixCmd = ShowCmd Bool | FixCmd [Int] Bool - show' :: Args -> ToolM [Note Correction] - show' (Args _ as) = do - input <- liftE getContents - msgs <- if flagSet "json" as - then maybe (toolError "Can't parse messages") return $ decode (toUtf8 input) - else return $ parseOutputMessages input - mapM (liftE . canonicalize) $ corrections msgs +fixP :: Parser FixCmd +fixP = subparser $ mconcat [ + cmd "show" "show what can be auto-fixed" (ShowCmd <$> switch (long "json" <> help "output messages in JSON format")), + cmd "fix" "fix selected errors" (FixCmd <$> + many (argument auto (metavar "N" <> help "indices of corrections to apply")) <*> + switch (long "pure" <> help "don't modify files, just return updated corrections"))] - fix' :: Args -> ToolM [Note Correction] - fix' (Args [] as) = do - input <- liftE getContents - corrs <- maybe (toolError "Can't parse messages") return $ decode (toUtf8 input) - let - nums :: [Int] - nums = mapMaybe readMaybe $ listArg "num" as - check i - | has "num" as = i `elem` nums - | otherwise = True - (fixCorrs, upCorrs) = (map snd *** map snd) $ - partition (check . fst) $ zip [1..] corrs - files <- liftE $ mapM canonicalizePath $ ordNub $ sort $ mapMaybe (preview $ noteSource . moduleFile) corrs - let - doFix :: FilePath -> EditM String [Note Correction] - doFix file = do - autoFix_ fixCorrs' - (each . note) updateRange upCorrs' - where - findCorrs :: FilePath -> [Note Correction] -> [Note Correction] - findCorrs f = filter ((== Just f) . preview (noteSource . moduleFile)) - fixCorrs' = map (view note) $ findCorrs file fixCorrs - upCorrs' = findCorrs file upCorrs - runFix file - | flagSet "pure" as = return $ fst $ runEdit $ doFix file - | otherwise = do - (corrs', cts') <- liftM (`editEval` doFix file) $ liftE $ readFileUtf8 file - liftE $ writeFileUtf8 file cts' - return corrs' - liftM concat $ mapM runFix files - fix' _ = toolError "Invalid arguments" +main :: IO () +main = toolMain "hsautofix" "automatically fix some errors" fixP (printExceptT . printResult . go) where + go (ShowCmd isJson) = do + input <- liftE getContents + msgs <- if isJson + then maybe (throwError "Can't parse messages") return $ decode (toUtf8 input) + else return $ parseOutputMessages input + mapM (liftE . canonicalize) $ corrections msgs + go (FixCmd ns pure') = do + input <- liftE getContents + corrs <- maybe (throwError "Can't parse messages") return $ decode (toUtf8 input) + let + check i = i `elem` ns || null ns + (fixCorrs, upCorrs) = (map snd *** map snd) $ + partition (check . fst) $ zip [1..] corrs + files <- liftE $ mapM canonicalizePath $ ordNub $ sort $ mapMaybe (preview $ noteSource . moduleFile) corrs + let + doFix :: FilePath -> String -> ([Note Correction], String) + doFix file cts = edit cts upCorrs' $ do + autoFix fixCorrs' + gets (view regions) + where + findCorrs :: FilePath -> [Note Correction] -> [Note Correction] + findCorrs f = filter ((== Just f) . preview (noteSource . moduleFile)) + fixCorrs' = map (view note) $ findCorrs file fixCorrs + upCorrs' = findCorrs file upCorrs + runFix file + | pure' = return $ fst $ doFix file "" + | otherwise = do + (corrs', cts') <- liftM (doFix file) $ liftE $ readFileUtf8 file + liftE $ writeFileUtf8 file cts' + return corrs' + liftM concat $ mapM runFix files
tools/hscabal.hs view
@@ -7,5 +7,4 @@ import Tool main :: IO () -main = toolMain "hscabal" [ - jsonCmd_ "list" ["packages..."] "list hackage packages" cabalList] +main = toolMain "hscabal" "cabal tool" (many (strArgument (metavar "package"))) $ printExceptT . printResult . cabalList
tools/hsclearimports.hs view
@@ -6,46 +6,39 @@ import Control.Exception (finally) import Control.Monad.Except import System.Directory -import System.Environment (getArgs) import HsDev.Tools.ClearImports (clearImports) import HsDev.Symbols (locateSourceDir) import HsDev.Project (entity) -import System.Console.Cmd - -opts :: [Opt] -opts = [ - list "ghc" "GHC_OPT" `short` "g" `desc` "options for GHC", - flag "hide-import-list" `desc` "hide import list", - req "max-import-list" "N" `desc` "hide long import lists"] +import Tool -cmds :: [Cmd (IO ())] -cmds = withHelp "hsclearimports" (printWith putStrLn) $ [ - cmd [] ["file"] opts "clear imports in haskell source" clear] - where - clear :: Args -> IO () - clear (Args [f] as) = do - file <- canonicalizePath f - mroot <- liftM (fmap $ view entity) $ locateSourceDir file - cur <- getCurrentDirectory - flip finally (setCurrentDirectory cur) $ do - maybe (return ()) setCurrentDirectory mroot - void $ runExceptT $ catchError - (clearImports (listArg "ghc" as) file >>= mapM_ (liftIO . putStrLn . format as)) - (\e -> liftIO (putStrLn $ "Error: " ++ e)) - clear _ = putStrLn "Invalid arguments" +data Opts = Opts { + optsFile :: String, + optsGHC :: [String], + optsHideImportList :: Bool, + optsMaxImportList :: Maybe Int } - format :: Opts String -> (String, String) -> String - format as (imp, lst) - | flagSet "hide-import-list" as = imp - | maybe False (length lst >) (narg "max-import-list" as) = imp - | otherwise = imp ++ " (" ++ lst ++ ")" +opts :: Parser Opts +opts = Opts <$> + strArgument (metavar "file" <> help "file to clear imports in") <*> + many (strOption (long "ghc" <> short 'g' <> metavar "GHC_OPT" <> help "options for GHC")) <*> + switch (long "hide-import-list" <> help "hide import list") <*> + optional (option auto (long "max-import-list" <> metavar "N" <> help "hide long import lists")) main :: IO () -main = do - args <- getArgs - run cmds noCmd onError args +main = toolMain "hsclearimports" "clears imports in haskell source" opts $ \opts' -> do + file <- canonicalizePath (optsFile opts') + mroot <- liftM (fmap $ view entity) $ locateSourceDir file + cur <- getCurrentDirectory + flip finally (setCurrentDirectory cur) $ do + maybe (return ()) setCurrentDirectory mroot + void $ runExceptT $ catchError + (clearImports (optsGHC opts') file >>= mapM_ (liftIO . putStrLn . format opts')) + (\e -> liftIO (putStrLn $ "Error: " ++ e)) where - noCmd = putStrLn "Invalid command" - onError = putStrLn + format :: Opts -> (String, String) -> String + format as (imp, lst) + | optsHideImportList as = imp + | maybe False (length lst >) (optsMaxImportList as) = imp + | otherwise = imp ++ " (" ++ lst ++ ")"
tools/hsdev.hs view
@@ -1,68 +1,37 @@-{-# LANGUAGE TemplateHaskell #-} - module Main ( main ) where import Control.Exception -import Control.Monad +import Data.List import Network.Socket (withSocketsDo) +import Options.Applicative import System.Environment (getArgs) import System.Exit import System.IO -import Text.Read (readMaybe) -import Control.Apply.Util (chain) -import System.Console.Cmd -import qualified System.Console.Cmd as C (brief) - -import qualified HsDev.Client.Commands as Client (commands) -import qualified HsDev.Server.Commands as Server -import HsDev.Version +import HsDev.Server.Commands (runServerCommand) +import HsDev.Server.Types +import HsDev.Util main :: IO () main = handle logErr' $ withSocketsDo $ do hSetBuffering stdout LineBuffering hSetEncoding stdout utf8 - as <- getArgs - when (null as) $ do - printUsage - exitSuccess - let - asr = if last as == "-?" then "help" : init as else as - run mainCommands (onError "Unknown command") onError asr + as <- prepareArgs + case parseArgs "hsdev" (info cmdP (progDesc "hsdev tool")) as of + Left e -> putStrLn e >> exitFailure + Right scmd -> runServerCommand scmd where - onError :: String -> IO () - onError errs = putStrLn errs >> exitFailure - logErr' (SomeException e) = putStrLn $ "exception " ++ show e -mainCommands :: [Cmd (IO ())] -mainCommands = withHelp "hsdev" (printWith putStrLn) $ concat [ - [cmd "version" [] [] "hsdev version" version'], - map (chain [validateOpts, noArgs]) Server.commands, - map Server.clientCmd Client.commands] - where - version' _ = putStrLn $cabalVersion - -printUsage :: IO () -printUsage = mapM_ (putStrLn . ('\t':) . ("hsdev " ++) . C.brief) mainCommands - --- | Check that specified options are numbers -validateNums :: [String] -> Cmd a -> Cmd a -validateNums ns = validateArgs (check . namedArgs) where - check os = forM_ ns $ \n -> case (readMaybe :: String -> Maybe Int) <$> arg n os of - Just Nothing -> failMatch "Must be a number" - _ -> return () - --- | Check, that 'port' and 'timeout' are numbers -validateOpts :: Cmd a -> Cmd a -validateOpts = validateNums ["port", "timeout"] - --- | Ensure no positional arguments provided -noArgs :: Cmd a -> Cmd a -noArgs = validateArgs (noPos . posArgs) where - noPos ps = - guard (null ps) - `mplus` - failMatch "positional arguments are not expected" +-- | Get args with '--stdin' flag replaced and 'help' command changed to flag +prepareArgs :: IO [String] +prepareArgs = getArgs >>= prepare where + prepare [] = return [] + prepare ("head" : as) = prepare $ as ++ ["-?"] + prepare as + | "--stdin" `elem` as = do + input <- getContents + return $ delete "--stdin" as ++ ["--data", input] + | otherwise = return as
tools/hshayoo.hs view
@@ -3,19 +3,23 @@ ) where import Data.Maybe + import HsDev.Tools.Hayoo import Tool -main :: IO () -main = toolMain "hshayoo" [ - jsonCmd "" ["query"] options "search in hayoo" hayoo'] - where - options = [ - req "page" "n" `short` ['p'] `desc` "page number (0 by default)", - req "pages" "count" `short` ['n'] `desc` "pages count (1 by default)"] +data HayooOpts = HayooOpts { + optsQuery :: String, + optsPage :: Int, + optsPages :: Int } - hayoo' (Args [q] opts) = liftM concat $ forM [page .. page + pred pages] $ \i -> liftM (mapMaybe hayooAsDeclaration . resultResult) $ hayoo q (Just i) where - page = fromMaybe 0 (narg "page" opts) - pages = fromMaybe 1 (narg "pages" opts) - hayoo' _ = toolError "Invalid arguments" +hayooOpts :: Parser HayooOpts +hayooOpts = HayooOpts <$> + strArgument (help "hayoo query") <*> + (option auto (long "page" <> short 'p' <> help "page number (0 by default") <|> pure 0) <*> + (option auto (long "pages" <> short 'n' <> help "pages count (1 by default") <|> pure 1) + +main :: IO () +main = toolMain "hshayoo" "hayoo search" hayooOpts $ \(HayooOpts q page pages) -> printExceptT $ printResult $ + liftM concat $ forM [page .. page + pred pages] $ \i -> + liftM (mapMaybe hayooAsDeclaration . resultResult) $ hayoo q (Just i)
tools/hsinspect.hs view
@@ -12,30 +12,37 @@ import HsDev.Project (readProject) import HsDev.Scan (scanModule, scanModify) -import HsDev.Inspect (inspectContents, inspectDocs) +import HsDev.Inspect (inspectContents, inspectDocs, getDefines) import HsDev.Symbols.Location (ModuleLocation(..), Cabal(..)) import HsDev.Tools.GhcMod.InferType (infer) import Tool -main :: IO () -main = toolMain "hsinspect" [ - jsonCmd "" ["what"] [ghcOpts] "depending of what <what> is, inspect installed module, source file (.hs), cabal file (.cabal) or contents, passes as input if no <what> specified" inspect'] - where - ghcOpts = list "ghc" "option" `short` ['g'] `desc` "options to pass to GHC" - ghcs = listArg "ghc" +data Opts = Opts { + optsWhat :: Maybe String, + optsGHC :: [String] } - inspect' (Args [] opts) = liftIO getContents >>= liftM toJSON . inspectContents "stdin" (ghcs opts) - inspect' (Args [fname@(takeExtension -> ".hs")] opts) = do - fname' <- liftIO $ canonicalizePath fname - im <- scanModule (ghcs opts) (FileModule fname' Nothing) Nothing - let - scanAdditional = - scanModify (\opts' _ -> inspectDocs opts') >=> - scanModify infer - toJSON <$> scanAdditional im - inspect' (Args [fcabal@(takeExtension -> ".cabal")] _) = do - fcabal' <- liftIO $ canonicalizePath fcabal - toJSON <$> readProject fcabal' - inspect' (Args [mname] opts) = toJSON <$> scanModule (ghcs opts) (CabalModule Cabal Nothing mname) Nothing - inspect' _ = toolError "Specify module name or file name (.hs or .cabal)" +opts :: Parser Opts +opts = Opts <$> + optional (strArgument (metavar "what" <> help "depending of what <what> is, inspect installed module, source file (.hs), cabal file (.cabal) or contents, passes as input if no <what> specified")) <*> + many (strOption (metavar "GHC_OPT" <> long "ghc" <> short 'g' <> help "options to pass to GHC")) + +main :: IO () +main = toolMain "hsinspect" "haskell inspect" opts (printExceptT . printResult . inspect') where + inspect' (Opts Nothing ghcs) = do + cts <- liftIO getContents + defs <- liftIO getDefines + liftM toJSON $ inspectContents "stdin" defs ghcs cts + inspect' (Opts (Just fname@(takeExtension -> ".hs")) ghcs) = do + fname' <- liftIO $ canonicalizePath fname + defs <- liftIO getDefines + im <- scanModule defs ghcs (FileModule fname' Nothing) Nothing + let + scanAdditional = + scanModify (\opts' _ -> inspectDocs opts') >=> + scanModify infer + toJSON <$> scanAdditional im + inspect' (Opts (Just fcabal@(takeExtension -> ".cabal")) _) = do + fcabal' <- liftIO $ canonicalizePath fcabal + toJSON <$> readProject fcabal' + inspect' (Opts (Just mname) ghcs) = toJSON <$> scanModule [] ghcs (CabalModule Cabal Nothing mname) Nothing