{-# LANGUAGE RankNTypes, FlexibleContexts #-} module HsDev.Commands ( -- * Commands findDeclaration, findModule, fileModule, lookupSymbol, whois, scopeModules, scope, completions, moduleCompletions, -- * Filters checkModule, checkDeclaration, restrictPackageDb, restrictPackageDbStack, visibleFrom, splitIdentifier, -- * Helpers fileCtx, fileCtxMaybe, -- * Reexports module HsDev.Database, module HsDev.Symbols.Types, module Control.Monad.Except ) where import Control.Applicative import Control.Lens (view, set, each, toListOf) import Control.Monad.Except import Data.List (delete) import Data.Maybe 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 import HsDev.Symbols.Resolve import HsDev.Symbols.Types import HsDev.Symbols.Util import HsDev.Tools.Base (matchRx, at_) import HsDev.Util (liftE, ordNub) -- | Find declaration by name findDeclaration :: Database -> String -> ExceptT String IO [ModuleDeclaration] findDeclaration db ident = return $ selectDeclarations checkName db where checkName :: ModuleDeclaration -> Bool checkName m = (view (moduleDeclaration . declarationName) m == fromString iname) && (maybe True ((view (declarationModuleId . moduleIdName) m ==) . fromString) qname) (qname, iname) = splitIdentifier ident -- | Find module by name findModule :: Database -> String -> ExceptT String IO [Module] findModule db mname = return $ selectModules ((== fromString mname) . view moduleName) db -- | Find module in file fileModule :: Database -> FilePath -> ExceptT String IO Module fileModule db src = do src' <- liftE $ canonicalizePath src maybe (throwError $ "File '" ++ src' ++ "' not found") return $ lookupFile src' db -- | Find project of module getProject :: Database -> Project -> ExceptT String IO Project getProject db p = do p' <- liftE $ canonicalize p maybe (throwError $ "Project " ++ view projectCabal p' ++ " not found") return $ refineProject db p' -- | Lookup visible within project/cabal symbol lookupSymbol :: Database -> FilePath -> String -> ExceptT String IO [ModuleDeclaration] lookupSymbol db file ident = do (_, mthis, mproj) <- fileCtx db file liftM (filter $ checkModule $ allOf [ visibleFrom mproj mthis, maybe (const True) inModule qname]) (newestPackage <$> findDeclaration db iname) where (qname, iname) = splitIdentifier ident -- | Whois symbol in scope whois :: Database -> FilePath -> String -> ExceptT String IO [Declaration] whois db file ident = do (_, mthis, mproj) <- fileCtx db file return $ newestPackage $ filter checkDecl $ view moduleDeclarations $ scopeModule $ resolveOne (fileDeps file mproj db) $ moduleLocals mthis where (qname, iname) = splitIdentifier ident checkDecl d = fmap fromString qname `elem` scopes d && view declarationName d == fromString iname -- | Accessible modules scopeModules :: Database -> FilePath -> ExceptT String IO [Module] scopeModules db file = do (file', mthis, mproj) <- fileCtxMaybe db file newestPackage <$> case mproj of Nothing -> return $ maybe id (:) mthis $ selectModules (installed . view moduleId) db Just proj -> let deps' = deps file' proj in return $ concatMap (\p -> selectModules (p . view moduleId) db) [ inProject proj, \m -> any (`inPackage` m) deps'] where deps f p = delete (view projectName p) $ concatMap (view infoDepends) $ fileTargets p f -- | Symbols in scope scope :: Database -> FilePath -> Bool -> ExceptT String IO [Declaration] scope db file False = do (_, mthis, mproj) <- fileCtx db file return $ view moduleDeclarations $ scopeModule $ resolveOne (fileDeps file mproj db) mthis scope db file True = concatMap (view moduleDeclarations) <$> scopeModules db file -- | Completions completions :: Database -> FilePath -> String -> Bool -> ExceptT String IO [Declaration] completions db file prefix wide = do (_, mthis, mproj) <- fileCtx db file return $ toListOf (each . minimalDecl) $ newestPackage $ filter checkDecl $ view moduleDeclarations $ scopeModule $ resolveOne (fileDeps file mproj db) $ dropImportLists mthis where (qname, iname) = splitIdentifier prefix checkDecl d = fmap fromString qname `elem` scopes d && fromString iname `T.isPrefixOf` view declarationName d dropImportLists m | wide = set (moduleImports . each . importList) Nothing m | otherwise = m -- | Module completions moduleCompletions :: Database -> [Module] -> String -> ExceptT String IO [String] moduleCompletions _ ms prefix = return $ map T.unpack $ ordNub $ completions' $ map (view moduleName) ms where completions' = mapMaybe getNext where getNext m | fromString prefix `T.isPrefixOf` m = listToMaybe $ map snd $ dropWhile (uncurry (==)) $ zip (T.split (== '.') $ fromString prefix) (T.split (== '.') m) | otherwise = Nothing -- | Check module checkModule :: (ModuleId -> Bool) -> (ModuleDeclaration -> Bool) checkModule = (. view declarationModuleId) -- | Check declaration checkDeclaration :: (Declaration -> Bool) -> (ModuleDeclaration -> Bool) checkDeclaration = (. view moduleDeclaration) -- | Allow only selected cabal sandbox restrictPackageDb :: PackageDb -> ModuleId -> Bool restrictPackageDb pdb m = inPackageDb pdb m || not (installed m) -- | Allow only selected cabal sandboxes restrictPackageDbStack :: PackageDbStack -> ModuleId -> Bool restrictPackageDbStack pdbs m = any (`inPackageDb` m) (packageDbs pdbs) || not (installed m) -- | Check whether module is visible from source file visibleFrom :: Maybe Project -> Module -> ModuleId -> Bool visibleFrom (Just p) this m = visible p (view moduleId this) m visibleFrom Nothing this m = view moduleId this == m || installed m -- | Split identifier into module name and identifier itself splitIdentifier :: String -> (Maybe String, String) splitIdentifier name = fromMaybe (Nothing, name) $ do groups <- matchRx "(([A-Z][\\w']*\\.)*)(.*)" name return (dropDot <$> groups 1, groups `at_` 3) where dropDot :: String -> String dropDot "" = "" dropDot s = init s -- | Get context file and project fileCtx :: Database -> FilePath -> ExceptT String IO (FilePath, Module, Maybe Project) fileCtx db file = do file' <- liftE $ canonicalizePath file mthis <- fileModule db file' mproj <- traverse (getProject db) $ projectOf $ view moduleId mthis return (file', mthis, mproj) -- | Try get context file fileCtxMaybe :: Database -> FilePath -> ExceptT String IO (FilePath, Maybe Module, Maybe Project) fileCtxMaybe db file = ((\(f, m, p) -> (f, Just m, p)) <$> fileCtx db file) <|> onlyProj where onlyProj = do file' <- liftE $ canonicalizePath file mproj <- liftE $ locateProject file' mproj' <- traverse (getProject db) mproj return (file', Nothing, mproj') -- | Restrict only modules file depends on fileDeps :: FilePath -> Maybe Project -> Database -> Database fileDeps file mproj = filterDB fileDeps' (const True) where fileDeps' = liftM2 (||) (maybe (const True) inProject mproj) (\m -> any (`inDepsOfTarget` m) (fromMaybe [] $ fileTargets <$> mproj <*> pure file))