module HsDev.Scan (
CompileFlag, ModuleToScan, ProjectToScan, PackageDbToScan, ScanContents(..),
EnumContents(..),
enumRescan, enumDependent, enumProject, enumSandbox, enumDirectory,
scanProjectFile,
scanModify,
upToDate, changedModules,
getFileContents,
module HsDev.Symbols.Types,
module Control.Monad.Except,
) where
import Control.DeepSeq
import Control.Lens
import Control.Monad.Except
import Data.Deps
import Data.Maybe (catMaybes, isJust, listToMaybe)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.List (intercalate)
import Data.Text (Text)
import Data.Text.Lens (unpacked)
import qualified Data.Text as T
import Data.Time.Clock.POSIX (POSIXTime)
import Data.Traversable (for)
import Data.String (IsString, fromString)
import qualified Data.Set as S
import System.Directory
import Text.Format
import qualified System.Log.Simple as Log
import HsDev.Error
import qualified HsDev.Database.SQLite as SQLite
import HsDev.Database.SQLite.Select
import HsDev.Scan.Browse (browsePackages)
import HsDev.Server.Types (FileSource(..), SessionMonad(..), CommandMonad(..), inSessionGhc, postSessionUpdater)
import HsDev.Sandbox
import HsDev.Symbols
import HsDev.Symbols.Types
import HsDev.Display
import HsDev.Inspect
import HsDev.Util
import System.Directory.Paths
type CompileFlag = String
type ModuleToScan = (ModuleLocation, [CompileFlag], Maybe Text)
type ProjectToScan = (Project, [ModuleToScan])
type PackageDbToScan = PackageDbStack
data ScanContents = ScanContents {
modulesToScan :: [ModuleToScan],
projectsToScan :: [ProjectToScan],
sandboxesToScan :: [PackageDbStack] }
instance NFData ScanContents where
rnf (ScanContents ms ps ss) = rnf ms `seq` rnf ps `seq` rnf ss
instance Monoid ScanContents where
mempty = ScanContents [] [] []
mappend (ScanContents lm lp ls) (ScanContents rm rp rs) = ScanContents
(uniqueBy (view _1) $ lm ++ rm)
(uniqueBy (view _1) $ lp ++ rp)
(ordNub $ ls ++ rs)
instance Formattable ScanContents where
formattable (ScanContents ms ps cs) = formattable str where
str :: String
str = format "modules: {}, projects: {}, package-dbs: {}"
~~ (T.intercalate comma $ ms ^.. each . _1 . moduleFile)
~~ (T.intercalate comma $ ps ^.. each . _1 . projectPath)
~~ (intercalate comma $ map (display . topPackageDb) $ cs ^.. each)
comma :: IsString s => s
comma = fromString ", "
class EnumContents a where
enumContents :: CommandMonad m => a -> m ScanContents
instance EnumContents ModuleLocation where
enumContents mloc = return $ ScanContents [(mloc, [], Nothing)] [] []
instance EnumContents (Extensions ModuleLocation) where
enumContents ex = return $ ScanContents [(view entity ex, extensionsOpts ex, Nothing)] [] []
instance EnumContents Project where
enumContents = enumProject
instance EnumContents PackageDbStack where
enumContents pdbs = return $ ScanContents [] [] (packageDbStacks pdbs)
instance EnumContents Sandbox where
enumContents = enumSandbox
instance EnumContents a => EnumContents [a] where
enumContents = liftM mconcat . tries . map enumContents
instance EnumContents FilePath where
enumContents f
| haskellSource f = hsdevLiftIO $ do
mproj <- liftIO $ locateProject f
case mproj of
Nothing -> enumContents $ FileModule (fromFilePath f) Nothing
Just proj -> do
ScanContents _ [(_, mods)] _ <- enumContents proj
return $ ScanContents (filter ((== Just f) . preview (_1 . moduleFile . path)) mods) [] []
| otherwise = enumDirectory f
instance EnumContents Path where
enumContents = enumContents . view path
instance EnumContents FileSource where
enumContents (FileSource f mcts)
| haskellSource (view path f) = do
ScanContents [(m, opts, _)] _ _ <- enumContents f
return $ ScanContents [(m, opts, mcts)] [] []
| otherwise = return mempty
enumRescan :: CommandMonad m => FilePath -> m ScanContents
enumRescan fpath = Log.scope "enum-rescan" $ do
ms <- SQLite.query @_ @(ModuleLocation SQLite.:. Inspection)
(toQuery $ mconcat [
qModuleLocation "ml",
select_ ["ml.inspection_time", "ml.inspection_opts"],
where_ ["ml.file == ?"]])
(SQLite.Only fpath)
case ms of
[] -> do
Log.sendLog Log.Warning $ "file {} not found" ~~ fpath
return mempty
((mloc SQLite.:. insp):_) -> do
when (length ms > 1) $ Log.sendLog Log.Warning $ "several modules with file == {} found, taking first one" ~~ fpath
return $ ScanContents [(mloc, insp ^.. inspectionOpts . each . unpacked, Nothing)] [] []
enumDependent :: CommandMonad m => FilePath -> m ScanContents
enumDependent fpath = Log.scope "enum-dependent" $ do
ms <- SQLite.query @_ @ModuleId
(toQuery $ qModuleId `mappend` where_ ["mu.file == ?"]) (SQLite.Only fpath)
case ms of
[] -> do
Log.sendLog Log.Warning $ "file {} not found" ~~ fpath
return mempty
(mid:_) -> do
when (length ms > 1) $ Log.sendLog Log.Warning $ "several modules with file == {} found, taking first one" ~~ fpath
let
mcabal = mid ^? moduleLocation . moduleProject . _Just . projectCabal
depList <- SQLite.query @_ @(Path, Path) "select d.module_file, d.depends_file from sources_depends as d, projects_modules_scope as ps where ps.cabal is ? and ps.module_id == d.module_id;"
(SQLite.Only mcabal)
let
rdeps = inverse . either (const mempty) id . flatten . mconcat . map (uncurry dep) $ depList
dependent = rdeps ^. ix (fromFilePath fpath)
liftM mconcat $ mapM (enumRescan . view path) dependent
enumProject :: CommandMonad m => Project -> m ScanContents
enumProject p = hsdevLiftIO $ do
p' <- liftIO $ loadProject p
pdbs <- inSessionGhc $ searchPackageDbStack (view projectPath p')
pkgs <- inSessionGhc $ liftM (S.fromList . map (view (package . packageName))) $ browsePackages [] pdbs
let
projOpts :: Path -> [Text]
projOpts f = map fromString $ concatMap makeOpts $ fileTargets p' f where
makeOpts :: Info -> [String]
makeOpts i = concat [
["-hide-all-packages"],
["-package " ++ view (projectName . path) p'],
["-package " ++ T.unpack dep' | dep' <- view infoDepends i, dep' `S.member` pkgs]]
srcs <- liftIO $ projectSources p'
let
mlocs = over each (\src -> over ghcOptions (++ projOpts (view entity src)) . over entity (\f -> FileModule f (Just p')) $ src) srcs
mods <- liftM modulesToScan $ enumContents mlocs
return $ ScanContents [] [(p', mods)] []
enumSandbox :: CommandMonad m => Sandbox -> m ScanContents
enumSandbox = (inSessionGhc . sandboxPackageDbStack) >=> enumContents
enumDirectory :: CommandMonad m => FilePath -> m ScanContents
enumDirectory dir = hsdevLiftIO $ do
cts <- liftIO $ traverseDirectory dir
let
projects = filter cabalFile cts
sources = filter haskellSource cts
dirs <- liftIO $ filterM doesDirectoryExist cts
sboxes <- liftM catMaybes $ triesMap (liftIO . findSandbox . fromFilePath) dirs
pdbs <- mapM enumSandbox sboxes
projs <- liftM mconcat $ triesMap (enumProject . project) projects
let
projPaths = map (view projectPath . fst) $ projectsToScan projs
standalone = map (`FileModule` Nothing) $ filter (\s -> not (any (`isParent` s) projPaths)) $ map fromFilePath sources
return $ mconcat [
ScanContents [(s, [], Nothing) | s <- standalone] [] [],
projs,
mconcat pdbs]
scanProjectFile :: CommandMonad m => [String] -> Path -> m Project
scanProjectFile _ f = hsdevLiftIO $ do
proj <- (liftIO $ locateProject (view path f)) >>= maybe (hsdevError $ FileNotFound f) return
liftIO $ loadProject proj
scanModify :: CommandMonad m => ([String] -> Module -> m Module) -> InspectedModule -> m InspectedModule
scanModify f im = traverse f' im where
f' m = f (toListOf (inspection . inspectionOpts . each . unpacked) im) m
upToDate :: SessionMonad m => ModuleLocation -> [String] -> Inspection -> m Bool
upToDate mloc opts insp = do
insp' <- liftIO $ moduleInspection mloc opts
mfinsp <- fmap join $ for (mloc ^? moduleFile) $ \fpath -> do
tm <- SQLite.query @_ @(SQLite.Only Double) "select mtime from file_contents where file = ?;" (SQLite.Only fpath)
return $ fmap (fileContentsInspection_ opts . fromRational . toRational . SQLite.fromOnly) (listToMaybe tm)
let
lastInsp = maybe insp' (max insp') mfinsp
return $ fresh insp lastInsp
changedModules :: SessionMonad m => Map ModuleLocation Inspection -> [String] -> [ModuleToScan] -> m [ModuleToScan]
changedModules inspMap opts = filterM $ \m -> if isJust (m ^. _3)
then return True
else maybe
(return True)
(liftM not . upToDate (m ^. _1) (opts ++ (m ^. _2)))
(M.lookup (m ^. _1) inspMap)
getFileContents :: SessionMonad m => Path -> m (Maybe (POSIXTime, Text))
getFileContents fpath = do
mcts <- SQLite.query @_ @(Double, Text) "select mtime, contents from file_contents where file = ?;" (SQLite.Only fpath)
insp <- liftIO $ fileInspection fpath []
case listToMaybe mcts of
Nothing -> return Nothing
Just (tm, cts) -> do
let
tm' = fromRational (toRational tm)
fmtime <- maybe (hsdevError $ OtherError "impossible: inspection time not set after call to `fileInspection`") return $ insp ^? inspectionAt
if fmtime < tm'
then return (Just (tm', cts))
else do
void $ postSessionUpdater $ SQLite.execute "delete from file_contents where file = ?;" (SQLite.Only fpath)
return Nothing