diff options
author | alexwl <alexey.a.kiryushin@gmail.com> | 2018-10-02 13:17:04 +0300 |
---|---|---|
committer | alexwl <alexey.a.kiryushin@gmail.com> | 2018-10-02 13:17:04 +0300 |
commit | cf2c56c7061b7ed40fdd3b40a352ddb9c9b7371f (patch) | |
tree | b1de9ada0f1b1cb064e3a9e0d4042d1f519085bd /app |
Initial commit
Diffstat (limited to 'app')
-rw-r--r-- | app/Indexer.hs | 208 | ||||
-rw-r--r-- | app/Server.hs | 1014 |
2 files changed, 1222 insertions, 0 deletions
diff --git a/app/Indexer.hs b/app/Indexer.hs new file mode 100644 index 0000000..083c94d --- /dev/null +++ b/app/Indexer.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} + +module Main where + +import Codec.Compression.GZip(compress) +import Control.Exception (SomeException, handle) +import Control.Monad (when) +import Control.Monad.Logger (LogLevel(..), runLoggingT) +import qualified Data.Aeson as A +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy.Char8 as BSC +import qualified Data.HashMap.Strict as HM +import Data.Maybe (fromMaybe) +import Data.Semigroup ((<>)) +import qualified Data.Serialize as S +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Data.Time (getZonedTime) +import Data.Version (Version(..),showVersion) +import HaskellCodeExplorer.PackageInfo (createPackageInfo) +import qualified HaskellCodeExplorer.Types as HCE +import Network.URI.Encode (encode) +import Options.Applicative + ( Parser + , (<|>) + , execParser + , flag + , fullDesc + , help + , helper + , info + , long + , many + , metavar + , optional + , progDesc + , short + , strOption + ) +import Paths_haskell_code_explorer as HSE (version) +import System.Directory (createDirectoryIfMissing) +import System.Exit (ExitCode(..), exitWith) +import System.FilePath ((</>)) +import System.Log.FastLogger + ( LoggerSet + , ToLogStr(..) + , defaultBufSize + , fromLogStr + , newFileLoggerSet + , newStdoutLoggerSet + , pushLogStrLn + , rmLoggerSet + ) + +data IndexerConfig = IndexerConfig + { configPackageDirectoryPath :: FilePath + , configPackageDistDirRelativePath :: Maybe FilePath + , configOutputDirectoryName :: Maybe String + , configLog :: !HCE.Log + , configMinLogLevel :: !LogLevel + , configSourceCodePreprocessing :: !HCE.SourceCodePreprocessing + , configCompression :: !Compression + , configGhcOptions :: [String] + , configIgnoreDirectories :: [String] + } deriving (Show, Eq) + +data Compression + = Gzip + | NoCompression + deriving (Show, Eq) + +#if MIN_VERSION_GLASGOW_HASKELL(8,2,2,0) +ghcVersion :: Version +ghcVersion = Version {versionBranch = [8, 2, 2, 0], versionTags = []} +#else +ghcVersion :: Version +ghcVersion = Version {versionBranch = [8, 0, 2, 0], versionTags = []} +#endif + +versionInfo :: String +versionInfo = + "haskell-code-indexer version " ++ + showVersion version ++ ", GHC version " ++ showVersion ghcVersion + +main :: IO () +main = do + let description = + "haskell-code-indexer collects and saves information about the source code of a Cabal package. " ++ + versionInfo + config <- + execParser $ + info (helper <*> configParser) (fullDesc <> progDesc description) + loggerSet <- + case configLog config of + HCE.ToFile logfile -> newFileLoggerSet defaultBufSize logfile + HCE.StdOut -> newStdoutLoggerSet defaultBufSize + let minLogLevel = configMinLogLevel config + logger loggerSet minLogLevel LevelInfo versionInfo + logger loggerSet minLogLevel LevelDebug $ show config + handle + (\(e :: SomeException) -> do + logger loggerSet minLogLevel LevelError (show e) + rmLoggerSet loggerSet + exitWith (ExitFailure 1)) $ do + packageInfo <- + runLoggingT + (createPackageInfo + (configPackageDirectoryPath config) + (configPackageDistDirRelativePath config) + (configSourceCodePreprocessing config) + (configGhcOptions config) + (configIgnoreDirectories config)) + (\_loc _source level msg -> logger loggerSet minLogLevel level msg) + let outputDir = + configPackageDirectoryPath config </> + fromMaybe + HCE.defaultOutputDirectoryName + (configOutputDirectoryName config) + createDirectoryIfMissing False outputDir + logger loggerSet minLogLevel LevelDebug $ "Output directory : " ++ outputDir + BS.writeFile + (outputDir </> HCE.packageInfoBinaryFileName) + (S.encode $ HCE.toCompactPackageInfo packageInfo) + mapM_ + (\(HCE.HaskellModulePath path, modInfo) -> + let (compressFunction, compressExtension) = + case configCompression config of + Gzip -> (compress, ".gz") + NoCompression -> (id, "") + filePath = + outputDir </> + (encode (T.unpack path) ++ ".json" ++ compressExtension) + in BSL.writeFile filePath . compressFunction . A.encode $ modInfo) . + HM.toList $ + HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.ModuleInfo) + BSL.writeFile + (outputDir </> HCE.packageInfoJsonFileName) + (A.encode packageInfo) + BSL.writeFile (outputDir </> "version.txt") (BSC.pack $ showVersion version) + logger loggerSet minLogLevel LevelInfo ("Finished" :: T.Text) + rmLoggerSet loggerSet + +configParser :: Parser IndexerConfig +configParser = + IndexerConfig <$> + strOption + (long "package" <> short 'p' <> metavar "PATH" <> + help "Path to a Cabal package") <*> + optional + (strOption + (long "dist" <> metavar "RELATIVE_PATH" <> + help "Relative path to a dist directory")) <*> + optional + (strOption + (long "output" <> metavar "DIRECTORY_NAME" <> + help "Output directory (default is '.haskell-code-explorer')")) <*> + (pure HCE.StdOut <|> + (HCE.ToFile <$> + strOption + (long "logfile" <> metavar "PATH" <> + help "Path to a log file (by default log is written to stdout)"))) <*> + flag + LevelInfo + LevelDebug + (long "verbose" <> short 'v' <> help "Write debug information to a log") <*> + flag + HCE.AfterPreprocessing + HCE.BeforePreprocessing + (long "before-preprocessing" <> + help + "Index source code before preprocessor pass (by default source code after preprocessing is indexed)") <*> + flag + Gzip + NoCompression + (long "no-compression" <> + help + "Do not compress json files (by default json files are compressed using gzip)") <*> + many + (strOption + (long "ghc" <> metavar "OPTIONS" <> help "Command-line options for GHC")) <*> + many + (strOption + (long "ignore" <> metavar "DIRECTORY_NAME" <> + help "Directories to ignore (e.g. node_modules)")) + +logger :: ToLogStr msg => LoggerSet -> LogLevel -> LogLevel -> msg -> IO () +logger loggerSet minLogLevel logLevel msg = + when (logLevel >= minLogLevel) $ do + time <- getZonedTime + let showLogLevel :: LogLevel -> T.Text + showLogLevel LevelDebug = "[debug]" + showLogLevel LevelInfo = "[info]" + showLogLevel LevelWarn = "[warn]" + showLogLevel LevelError = "[error]" + showLogLevel (LevelOther t) = T.concat ["[",t,"]"] + text = + T.concat + [ T.pack $ show time + , " : " + , showLogLevel logLevel + , " " + , TE.decodeUtf8 . fromLogStr . toLogStr $ msg + ] + pushLogStrLn loggerSet $ toLogStr text diff --git a/app/Server.hs b/app/Server.hs new file mode 100644 index 0000000..39f550f --- /dev/null +++ b/app/Server.hs @@ -0,0 +1,1014 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Main where + +import Control.Exception + ( SomeAsyncException + , SomeException + , fromException + , handle + , throwIO + , throwIO + , try + ) +import Control.Monad (unless) +import Control.Monad.Except (ExceptT(..)) +import Control.Monad.Reader (MonadIO, MonadReader, ReaderT(..), asks, liftIO) +import qualified Data.Aeson as A +import qualified Data.ByteString as BS +import qualified Data.Vector as V +import qualified Data.ByteString.Lazy as BSL +import Data.Default (def) +import Data.Either (lefts, rights) +import qualified Data.HashMap.Strict as HM +import Data.Hashable (Hashable) +import qualified Data.IntervalMap.Strict as IVM +import qualified Data.List as L +import qualified Data.Map.Strict as M +import Data.Maybe(fromMaybe) +#if MIN_VERSION_GLASGOW_HASKELL(8,4,3,0) +import qualified GHC.Compact as C +import Data.Functor.Identity(Identity(..)) +#endif +import Data.Pagination + ( Paginated + , hasNextPage + , hasPrevPage + , mkPagination + , paginate + , paginatedItems + , paginatedPagesTotal + ) +import Data.Proxy (Proxy(..)) +import Data.Semigroup ((<>)) +import qualified Data.Serialize as Serialize +import qualified Data.Set as S +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Data.Text.Lazy (toStrict) +import Data.Text.Read(decimal) +import Data.Version (Version(..)) +import GHC.Exts (Down(..), groupWith) +import GHC.Generics (Generic) +import qualified HaskellCodeExplorer.Types as HCE +import Network.HTTP.Types + ( hContentEncoding + , hContentType + , status200 + , status404 + ) +import Network.Mime (defaultMimeLookup) +import Network.Wai + ( Application + , Middleware + , Response + , pathInfo + , responseFile + , responseLBS + , requestHeaders + ) +import Network.Wai.Handler.Warp (run) +import Network.Wai.Middleware.RequestLogger + ( Destination(..) + , OutputFormat(..) + , RequestLoggerSettings(..) + , mkRequestLogger + ) +import Numeric.Natural(Natural) +import Options.Applicative + ( Parser + , (<**>) + , (<|>) + , auto + , execParser + , fullDesc + , help + , helper + , info + , long + , some + , metavar + , option + , optional + , progDesc + , short + , strOption + , switch + ) +import Servant + ( (:<|>)(..) + , (:>) + , Capture + , FromHttpApiData(..) + , Get + , Header + , Headers + , QueryParam + , ServantErr + , ServerT + , ToHttpApiData(..) + , addHeader + , err404 + , errBody + , serve + ) +import Servant.API.ContentTypes (AllCTRender(..), JSON) +import Servant.Server (Handler(..), hoistServer) +import Servant.Utils.Links (safeLink) +import System.Directory (doesFileExist) +import System.FilePath.Find + ( FileType(..) + , (&&?) + , (/=?) + , (==?) + , (==?) + , depth + , filePath + , fileType + , find + ) +import System.FilePath.Posix ((</>),takeFileName) +import System.Log.FastLogger + ( LoggerSet + , defaultBufSize + , newFileLoggerSet + , newStdoutLoggerSet + ) +import Text.Blaze.Html.Renderer.Text (renderHtml) +import qualified Text.Blaze.Html5 as Html hiding (html, source) + +-------------------------------------------------------------------------------- +-- Server config +-------------------------------------------------------------------------------- + +data ServerConfig = ServerConfig + { configPackagesPath :: !PackagesPath + , configPort :: !Int + , configServeStaticFiles :: !Bool + , configEnableExpressionInfo :: !Bool + , configIndexDirectoryName :: !(Maybe FilePath) + , configLog :: !HCE.Log + , configStaticFilesUrlPrefix :: !String + , configJsDistDirectory :: !(Maybe String) + , configMaxPerPage :: !Int + } deriving (Show, Eq) + +data PackagesPath + = DirectoryWithPackages FilePath + | Directories [FilePath] + deriving (Show, Eq) + +configParser :: Parser ServerConfig +configParser = + ServerConfig <$> + ((DirectoryWithPackages <$> + strOption + (long "packages" <> metavar "PATH" <> + help "Path to a directory with Cabal packages")) <|> + Directories <$> + some + (strOption + (long "package" <> short 'p' <> metavar "PATH" <> + help "Path to a Cabal package"))) <*> + (pure 8080 <|> + option + auto + (long "port" <> help "Port to use (default is 8080)" <> metavar "PORT")) <*> + (not <$> switch (long "no-static" <> help "Do not serve static files")) <*> + (not <$> + switch + (long "no-expressions" <> + help "Disable queries that return expressions inside selected span")) <*> + optional + (strOption + (long "index-directory" <> + help + "Name of a directory with index (default is '.haskell-code-explorer')" <> + metavar "DIRECTORY_NAME")) <*> + (pure HCE.StdOut <|> + (HCE.ToFile <$> + strOption + (long "logfile" <> + help "Path to a log file (by default log is written to stdout)" <> + metavar "PATH"))) <*> + (pure "files" <|> + strOption + (long "static-url-prefix" <> metavar "STRING" <> + help "URL prefix for static files (default is 'files')")) <*> + optional + (strOption + (long "js-path" <> help "Path to a directory with javascript files" <> + metavar "PATH")) <*> + (pure 50 <|> + option + auto + (long "max-per-page" <> metavar "INTEGER" <> + help "Maximum number of items per page (default is 50)")) + +-------------------------------------------------------------------------------- +-- Loading packages +-------------------------------------------------------------------------------- + +data PackageVersions = PackageVersions + { name :: T.Text + , versions :: [Version] + } deriving (Show, Ord, Eq, Generic) + +type PackageMap + = HM.HashMap PackageName (M.Map Version (HCE.PackageInfo HCE.CompactModuleInfo)) + +type PackagePathMap = HM.HashMap PackageId FilePath + +newtype AllPackages = + AllPackages BSL.ByteString + +newtype PackageId = PackageId + { getPackageId :: T.Text + } deriving (Show, Eq, Hashable) + +newtype PackageName = PackageName + { getPackageName :: T.Text + } deriving (Show, Eq, Hashable) + +instance A.ToJSON PackageVersions + +loadPackages :: + ServerConfig -> IO (Maybe (PackageMap, PackagePathMap, [PackageVersions])) +loadPackages config = do + packageDirectories <- + case configPackagesPath config of + DirectoryWithPackages dir -> + find (depth ==? 0) (fileType ==? Directory &&? filePath /=? dir) dir + Directories dirs -> return dirs + result <- mapM (loadPackageInfo config) packageDirectories + let loadedPackages = rights result + packageLoadErrors = lefts result + packageInfos = map fst loadedPackages + packageIds = + map (HCE.id :: HCE.PackageInfo modInfo -> HCE.PackageId) packageInfos + unless (null packageInfos) $ do + putStrLn "Loaded packages : " + mapM_ (print . HCE.packageIdToText) packageIds + unless (null packageLoadErrors) $ do + putStrLn "Package loading errors : " + mapM_ (\(err, path) -> putStrLn $ path ++ " : " ++ err) packageLoadErrors + if not . null $ loadedPackages + then do + let packageVersions = + L.sortOn (T.toLower . (name :: PackageVersions -> T.Text)) . + map + (\(name, versions) -> + PackageVersions name (L.sortOn Down versions)) . + HM.toList . + HM.fromListWith (++) . + map (\HCE.PackageId {..} -> (name, [version])) $ + packageIds + packageMap = + L.foldl' + (\hMap packageInfo -> + let val = M.singleton (packageVersion packageInfo) packageInfo + in HM.insertWith M.union (packageName packageInfo) val hMap) + HM.empty + packageInfos + packagePathMap = + L.foldl' + (\hMap (packageInfo, path) -> + let key = + PackageId $ + HCE.packageIdToText + (HCE.id + (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo)) + in HM.insert key path hMap) + HM.empty + loadedPackages + packageMapCompacted <- ghcCompact packageMap + packagePathMapCompacted <- ghcCompact packagePathMap + packageVersionsCompacted <- ghcCompact packageVersions + return . Just $ + (packageMapCompacted, packagePathMapCompacted, packageVersionsCompacted) + else return Nothing + where + packageName :: HCE.PackageInfo HCE.CompactModuleInfo -> PackageName + packageName = + PackageName . + (HCE.name :: HCE.PackageId -> T.Text) . + (HCE.id :: HCE.PackageInfo modInfo -> HCE.PackageId) + packageVersion :: HCE.PackageInfo HCE.CompactModuleInfo -> Version + packageVersion = + HCE.version . (HCE.id :: HCE.PackageInfo modInfo -> HCE.PackageId) + +ghcCompact :: forall a. a -> IO a +ghcCompact = +#if MIN_VERSION_GLASGOW_HASKELL(8,4,3,0) + (fmap C.getCompact . C.compact) +#else + return +#endif + +loadPackageInfo :: + ServerConfig + -> String + -> IO (Either (String, FilePath) ( HCE.PackageInfo HCE.CompactModuleInfo + , FilePath)) +loadPackageInfo config path = + handleSync (\e -> return $ Left (show e, path)) $ do + let indexDirectory = + fromMaybe + HCE.defaultOutputDirectoryName + (configIndexDirectoryName config) + binaryContent <- + BS.readFile (path </> indexDirectory </> HCE.packageInfoBinaryFileName) + let eitherPackageInfo = Serialize.decode binaryContent + enableExpressionInfo = configEnableExpressionInfo config + case eitherPackageInfo of + Right packageInfo -> + return . Right $ + ( updateEachModuleInfo + packageInfo + (\modInfo -> + let source = + HCE.source :: HCE.CompactModuleInfo -> V.Vector T.Text + in if not enableExpressionInfo + then modInfo + { HCE.exprInfoMap = IVM.empty + } + else modInfo) + , path) + Left e -> return . Left $ (e, path) + +updateEachModuleInfo :: + HCE.PackageInfo HCE.CompactModuleInfo + -> (HCE.CompactModuleInfo -> HCE.CompactModuleInfo) + -> HCE.PackageInfo HCE.CompactModuleInfo +updateEachModuleInfo packageInfo update = + packageInfo {HCE.moduleMap = HM.map update $ HCE.moduleMap packageInfo} + +handleSync :: (SomeException -> IO a) -> IO a -> IO a +handleSync onError = + handle + (\ex -> + case fromException ex of + Just (asyncEx :: SomeAsyncException) -> throwIO asyncEx + _ -> onError ex) + +-------------------------------------------------------------------------------- +-- Servant API +-------------------------------------------------------------------------------- + +type API = GetAllPackages + :<|> GetDefinitionSite + :<|> GetExpressions + :<|> GetReferences + :<|> GetIdentifiers + +type GetAllPackages = "api" :> "packages" :> Get '[JSON] AllPackages + +type GetDefinitionSite = "api" :> "definitionSite" + :> Capture "packageId" PackageId + :> Capture "componentId" HCE.ComponentId + :> Capture "moduleName" HCE.HaskellModuleName + :> Capture "entity" HCE.LocatableEntity + :> Capture "name" T.Text + :> Get '[JSON] HCE.DefinitionSite + +type GetExpressions = "api" :> "expressions" + :> Capture "packageId" PackageId + :> Capture "modulePath" HCE.HaskellModulePath + :> Capture "lineStart" Int + :> Capture "columnStart" Int + :> Capture "lineEnd" Int + :> Capture "columnEnd" Int + :> Get '[JSON] [Expression] + +type GetReferences = "api" :> "references" + :> Capture "packageId" PackageId + :> Capture "name" HCE.ExternalId + :> QueryParam "page" Int + :> QueryParam "per_page" Int + :> Get '[JSON] (Headers '[Header "Link" T.Text,Header "X-Total-Count" Int] + [SourceFile]) + +type GetIdentifiers = "api" :> "identifiers" + :> Capture "packageId" PackageId + :> Capture "query" T.Text + :> QueryParam "page" Int + :> QueryParam "per_page" Int + :> Get '[JSON] (Headers '[Header "Link" T.Text,Header "X-Total-Count" Int] + [HCE.ExternalIdentifierInfo]) + +instance AllCTRender '[ JSON] AllPackages where + handleAcceptH _ _ (AllPackages bytestring) = + Just ("application/json", bytestring) + +instance FromHttpApiData HCE.LocatableEntity where + parseQueryParam "Val" = Right HCE.Val + parseQueryParam "Typ" = Right HCE.Typ + parseQueryParam "Inst" = Right HCE.Inst + parseQueryParam "Mod" = Right HCE.Mod + parseQueryParam val = Left $ T.append "Incorrect LocatableEntity : " val + +instance ToHttpApiData HCE.LocatableEntity where + toUrlPiece HCE.Val = "ValueEntity" + toUrlPiece HCE.Typ = "TypeEntity" + toUrlPiece HCE.Inst = "InstanceEntity" + toUrlPiece HCE.Mod = "ModuleEntity" + +instance ToHttpApiData HCE.ExternalId where + toUrlPiece (HCE.ExternalId i) = i + +instance ToHttpApiData PackageId where + toUrlPiece (PackageId p) = p + +instance FromHttpApiData HCE.HaskellModulePath where + parseQueryParam = Right . HCE.HaskellModulePath + +instance FromHttpApiData HCE.ComponentId where + parseQueryParam = Right . HCE.ComponentId + +instance FromHttpApiData HCE.HaskellModuleName where + parseQueryParam = Right . HCE.HaskellModuleName + +instance FromHttpApiData HCE.ExternalId where + parseQueryParam = Right . HCE.ExternalId + +instance FromHttpApiData PackageId where + parseQueryParam = Right . PackageId + +-------------------------------------------------------------------------------- +-- Request handlers +-------------------------------------------------------------------------------- + +data Environment = Environment + { envLogger :: !LoggerSet + , envPackageMap :: !PackageMap + , envPackageVersions :: !AllPackages + , envConfig :: !ServerConfig + } + +data Expression = Expression + { srcSpan :: !(IVM.Interval (Int, Int)) + , info :: !HCE.ExpressionInfo + } deriving (Show, Eq, Generic) + +instance A.ToJSON Expression + +data ReferenceWithSource = ReferenceWithSource + { sourceCodeHtml :: !T.Text + , idSrcSpan :: !HCE.IdentifierSrcSpan + } deriving (Show, Eq, Generic) + +data SourceFile = SourceFile + { name :: !T.Text + , references :: ![ReferenceWithSource] + } deriving (Show, Eq, Generic) + +instance A.ToJSON ReferenceWithSource +instance A.ToJSON SourceFile + +getAllPackages :: ReaderT Environment IO AllPackages +getAllPackages = asks envPackageVersions + +getPackageInfoAndModulePath :: + PackageId + -> HCE.ComponentId + -> HCE.HaskellModuleName + -> ReaderT Environment IO ( HCE.PackageInfo HCE.CompactModuleInfo + , HCE.HaskellModulePath) +getPackageInfoAndModulePath packageId componentId moduleName = + withPackageInfo packageId $ \packageInfo -> + case HM.lookup moduleName (HCE.moduleNameMap packageInfo) of + Just modulePathMap -> + case HM.lookup componentId modulePathMap of + Just path -> return (packageInfo, path) + Nothing -> + error404 $ + BSL.concat + [ "Module " + , toLazyBS $ HCE.getHaskellModuleName moduleName + , " not found in component " + , toLazyBS $ HCE.getComponentId componentId + ] + Nothing -> + error404 $ + BSL.concat + [ "Module " + , toLazyBS $ HCE.getHaskellModuleName moduleName + , " not found in package " + , toLazyBS $ getPackageId packageId + ] + +getExpressions :: + PackageId + -> HCE.HaskellModulePath + -> Int -- ^ Start line + -> Int -- ^ Start column + -> Int -- ^ End line + -> Int -- ^ End column + -> ReaderT Environment IO [Expression] +getExpressions packageId modulePath startLine startColumn endLine endColumn = do + enableExpressionInfo <- asks (configEnableExpressionInfo . envConfig) + if not enableExpressionInfo + then error404 "Expression queries are disabled" + else withPackageInfo packageId $ \packageInfo -> + withModuleInfo packageInfo modulePath $ \modInfo -> do + maxPerPage <- asks (configMaxPerPage . envConfig) + let exprInfoMap = + HCE.exprInfoMap (modInfo :: HCE.CompactModuleInfo) + requestedInterval = + IVM.ClosedInterval + (startLine, startColumn) + (endLine, endColumn) + return . + map (uncurry Expression) . + L.take maxPerPage . IVM.toList . IVM.within exprInfoMap $ + requestedInterval + +getDefinitionSite :: + PackageId + -> HCE.ComponentId + -> HCE.HaskellModuleName + -> HCE.LocatableEntity + -> T.Text + -> ReaderT Environment IO HCE.DefinitionSite +getDefinitionSite packageId componentId modName entity name = + withPackageInfo packageId $ \packageInfo -> + withModulePath packageInfo componentId modName $ \modPath -> + case entity of + HCE.Mod -> + return $ + HCE.DefinitionSite + (HCE.ExactLocation + (HCE.id (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo)) + modPath + modName + 1 + 1 + 1 + 1) + Nothing + _ -> + withModuleInfo packageInfo modPath $ \modInfo -> do + let defSites = + HCE.definitionSiteMap (modInfo :: HCE.CompactModuleInfo) + mbDefinitionSite = + case entity of + HCE.Typ -> + HM.lookup (HCE.OccName name) $ + HCE.types (defSites :: HCE.DefinitionSiteMap) + HCE.Val -> + HM.lookup (HCE.OccName name) $ + HCE.values (defSites :: HCE.DefinitionSiteMap) + HCE.Inst -> + HM.lookup name $ + HCE.instances (defSites :: HCE.DefinitionSiteMap) + _ -> Nothing + case mbDefinitionSite of + Just definitionSite -> return definitionSite + Nothing -> + error404 $ + BSL.concat + [ toLazyBS . T.pack $ show entity + , " " + , toLazyBS name + , " " + , " not found in a module " + , toLazyBS $ HCE.getHaskellModulePath modPath + ] + +buildLinkHeader :: T.Text -> Paginated a -> Natural -> Natural -> T.Text +buildLinkHeader url paginated currentPage perPage = + T.intercalate + "," + (let addFirst + | currentPage /= 1 = + (:) (link (T.append url $ params 1 perPage) "first") + | otherwise = id + addLast + | currentPage /= paginatedPagesTotal paginated = + (:) + (link + (T.append url $ params (paginatedPagesTotal paginated) perPage) + "last") + | otherwise = id + addNext + | hasNextPage paginated = + (:) (link (T.append url $ params (currentPage + 1) perPage) "next") + | otherwise = id + addPrev + | hasPrevPage paginated = + (:) (link (T.append url $ params (currentPage - 1) perPage) "prev") + | otherwise = id + in addFirst . addLast . addNext . addPrev $ []) + where + link :: T.Text -> T.Text -> T.Text + link u rel = T.concat ["<", u, ">; rel=\"", rel, "\""] + params :: Natural -> Natural -> T.Text + params p pp = + T.concat ["?page=", T.pack . show $ p, "&per_page=", T.pack . show $ pp] + +initializePagination :: + (MonadReader Environment m) + => Maybe Int + -> Maybe Int + -> m (Natural, Natural) +initializePagination mbPage mbPerPage = do + maxPerPage <- asks (configMaxPerPage . envConfig) + let page = + case mbPage of + Just p -> + if p > 0 + then p + else 1 + Nothing -> 1 + perPage = + case mbPerPage of + Just pp -> + if pp <= maxPerPage && pp > 0 + then pp + else maxPerPage + Nothing -> maxPerPage + return (fromIntegral page, fromIntegral perPage) + +getReferences :: + PackageId + -> HCE.ExternalId + -> Maybe Int -- ^ Page number + -> Maybe Int -- ^ Items per page + -> ReaderT Environment IO (Headers '[ Header "Link" T.Text, Header "X-Total-Count" Int] [SourceFile]) +getReferences packageId externalId mbPage mbPerPage = + withPackageInfo packageId $ \packageInfo -> + case S.toList <$> HM.lookup externalId (HCE.externalIdOccMap packageInfo) of + Just references -> do + (page, perPage) <- initializePagination mbPage mbPerPage + pagination <- mkPagination perPage page + let totalCount = L.length references + paginatedReferences <- + paginate + pagination + (fromIntegral totalCount) + (\offset limit -> return . L.take limit . L.drop offset $ references) + let url = + T.append "/" $ + toUrlPiece $ + safeLink + (Proxy :: Proxy API) + (Proxy :: Proxy GetReferences) + packageId + externalId + Nothing + Nothing + linkHeader = buildLinkHeader url paginatedReferences page perPage + addHeaders :: + forall a. + a + -> Headers '[ Header "Link" T.Text, Header "X-Total-Count" Int] a + addHeaders = addHeader linkHeader . addHeader totalCount + refModulePath :: ReferenceWithSource -> HCE.HaskellModulePath + refModulePath = + (HCE.modulePath :: HCE.IdentifierSrcSpan -> HCE.HaskellModulePath) . + idSrcSpan + return $ + addHeaders $ + concatMap + (\refs -> + case refs of + ref:_ -> + let path = + HCE.getHaskellModulePath . + (HCE.modulePath :: HCE.IdentifierSrcSpan -> HCE.HaskellModulePath) . + idSrcSpan $ + ref + in [SourceFile path refs] + _ -> []) $ + groupWith refModulePath $ + map + (mkReferenceWithSource packageInfo) + (paginatedItems paginatedReferences) + Nothing -> + error404 $ + BSL.concat + [ "Cannot find references to " + , toLazyBS $ HCE.getExternalId externalId + ] + +mkReferenceWithSource :: + HCE.PackageInfo HCE.CompactModuleInfo + -> HCE.IdentifierSrcSpan + -> ReferenceWithSource +mkReferenceWithSource packageInfo idSrcSpan = + let mbModule = + HM.lookup + (HCE.modulePath (idSrcSpan :: HCE.IdentifierSrcSpan)) + (HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo)) + in case mbModule of + Just modInfo -> + let sourceCodeHtml = + buildHtmlCodeSnippet + (HCE.source (modInfo :: HCE.CompactModuleInfo)) + (HCE.line (idSrcSpan :: HCE.IdentifierSrcSpan)) + (HCE.startColumn (idSrcSpan :: HCE.IdentifierSrcSpan)) + (HCE.endColumn (idSrcSpan :: HCE.IdentifierSrcSpan)) + in ReferenceWithSource sourceCodeHtml idSrcSpan + _ -> ReferenceWithSource "" idSrcSpan + +buildHtmlCodeSnippet :: V.Vector T.Text -> Int -> Int -> Int -> T.Text +buildHtmlCodeSnippet sourceLines lineNumber startColumn endColumn = + toStrict $ + renderHtml $ do + mkLineNumber (lineNumber - 1) >> + Html.toHtml + (T.append (fromMaybe "" $ (V.!?) sourceLines (lineNumber - 2)) "\n") + mkLineNumber lineNumber >> + highlightIdentifier + (T.append (fromMaybe "" $ (V.!?) sourceLines (lineNumber - 1)) "\n") + mkLineNumber (lineNumber + 1) >> + Html.toHtml (T.append (fromMaybe "" $ (V.!?) sourceLines lineNumber) "\n") + where + mkLineNumber :: Int -> Html.Html + mkLineNumber i = Html.toHtml (show i ++ " ") + highlightIdentifier :: T.Text -> Html.Html + highlightIdentifier line = + let (startLine, remaining) = T.splitAt (startColumn - 1) line + (identifier, endLine) = T.splitAt (endColumn - startColumn) remaining + in Html.toHtml startLine >> Html.b (Html.toHtml identifier) >> + Html.toHtml endLine + +findIdentifiers :: + PackageId + -> T.Text + -> Maybe Int + -> Maybe Int + -> ReaderT Environment IO (Headers '[ Header "Link" T.Text, Header "X-Total-Count" Int] + [HCE.ExternalIdentifierInfo]) +findIdentifiers packageId query mbPage mbPerPage = + withPackageInfo packageId $ \packageInfo -> do + let identifiers + | not $ T.null query = + S.toList $ + HCE.match (T.unpack query) (HCE.externalIdInfoMap packageInfo) + | otherwise = [] + (page, perPage) <- initializePagination mbPage mbPerPage + let totalCount = L.length identifiers + pagination <- mkPagination perPage page + paginatedIdentifiers <- + paginate + pagination + (fromIntegral totalCount) + (\offset limit -> return . L.take limit . L.drop offset $ identifiers) + let url = + T.append "/" $ + toUrlPiece $ + safeLink + (Proxy :: Proxy API) + (Proxy :: Proxy GetIdentifiers) + packageId + query + Nothing + Nothing + linkHeader = buildLinkHeader url paginatedIdentifiers page perPage + addHeaders :: + forall a. + a + -> Headers '[ Header "Link" T.Text, Header "X-Total-Count" Int] a + addHeaders = addHeader linkHeader . addHeader totalCount + return . addHeaders . paginatedItems $ paginatedIdentifiers + +error404 :: BSL.ByteString -> ReaderT Environment IO a +error404 body = throwServantError $ err404 {errBody = body} + +toLazyBS :: T.Text -> BSL.ByteString +toLazyBS = BSL.fromStrict . TE.encodeUtf8 + +withPackageInfo :: + PackageId + -> (HCE.PackageInfo HCE.CompactModuleInfo -> ReaderT Environment IO a) + -> ReaderT Environment IO a +withPackageInfo packageId action + | Just (packageName, mbVersion) <- parsePackageId packageId = do + packageMap <- asks envPackageMap + let mbPackageInfo = + HM.lookup packageName packageMap >>= + (\packages -> + let findLastVersion :: M.Map k v -> Maybe v + findLastVersion = fmap (snd . fst) . L.uncons . M.toDescList + in case mbVersion of + Just version -> + M.lookup version packages <|> findLastVersion packages + Nothing -> findLastVersion packages) + case mbPackageInfo of + Just p -> action p + Nothing -> packageNotFound packageId +withPackageInfo packageId _ = packageNotFound packageId + +packageNotFound :: PackageId -> ReaderT Environment IO a +packageNotFound packageId = + error404 $ + BSL.concat ["Package ", toLazyBS $ getPackageId packageId, " is not found."] + +withModuleInfo :: + HCE.PackageInfo HCE.CompactModuleInfo + -> HCE.HaskellModulePath + -> (HCE.CompactModuleInfo -> ReaderT Environment IO a) + -> ReaderT Environment IO a +withModuleInfo packageInfo path action = + case HM.lookup + path + (HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo)) of + Just modInfo -> action modInfo + Nothing -> + error404 $ + BSL.concat + [ "Module " + , toLazyBS $ HCE.getHaskellModulePath path + , " is not found in package " + , toLazyBS $ + HCE.packageIdToText $ + HCE.id (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo) + ] + +withModulePath :: + HCE.PackageInfo HCE.CompactModuleInfo + -> HCE.ComponentId + -> HCE.HaskellModuleName + -> (HCE.HaskellModulePath -> ReaderT Environment IO a) + -> ReaderT Environment IO a +withModulePath packageInfo componentId moduleName action = + case HM.lookup + (ghcPrimHack packageInfo moduleName) + (HCE.moduleNameMap packageInfo) of + Just modulePathMap -> + case HM.lookup componentId modulePathMap of + Just path -> action path + Nothing -> + case HM.lookup (HCE.ComponentId "lib") modulePathMap of + Just path -> action path + Nothing -> + error404 $ + BSL.concat + [ "Module " + , toLazyBS $ HCE.getHaskellModuleName moduleName + , " is not found in component " + , toLazyBS $ HCE.getComponentId componentId + ] + Nothing -> + error404 $ + BSL.concat + [ "Module " + , toLazyBS $ HCE.getHaskellModuleName moduleName + , " is not found in package " + , toLazyBS $ + HCE.packageIdToText $ + HCE.id (packageInfo :: HCE.PackageInfo HCE.CompactModuleInfo) + ] + +-- | Workaround for : +-- https://github.com/ghc/ghc/blob/ghc-8.2.2-release/compiler/main/Finder.hs#L310-L315 +ghcPrimHack :: + HCE.PackageInfo HCE.CompactModuleInfo + -> HCE.HaskellModuleName + -> HCE.HaskellModuleName +ghcPrimHack packageInfo (HCE.HaskellModuleName modName) + | HCE.packageName packageInfo == "ghc-prim" && modName == "GHC.Prim" = + HCE.HaskellModuleName "GHC.Prim_" + | otherwise = HCE.HaskellModuleName modName + +parsePackageId :: PackageId -> Maybe (PackageName, Maybe Version) +parsePackageId (PackageId text) = + case T.splitOn "-" text of + [name] -> Just (PackageName name, Nothing) + chunks@(_x:_xs) -> + case mapM decimal . T.splitOn "." . last $ chunks of + Right numbers -> + Just + ( PackageName $ T.intercalate "-" . init $ chunks + , Just $ Version (map fst numbers) []) + Left _ -> Just (PackageName text, Nothing) + _ -> Nothing + +staticMiddleware :: String -> PackagePathMap -> Maybe FilePath -> Middleware +staticMiddleware staticFilesPrefix packagePathMap _ app req callback + | ("api":_) <- pathInfo req = app req callback + | (prefix:packageId:rest) <- pathInfo req + , prefix == T.pack staticFilesPrefix = + case HM.lookup (PackageId packageId) packagePathMap of + Just basePath + | ".." `notElem` rest -> do + let clientAcceptsEncoding = + fromMaybe [] $ + map T.strip . T.splitOn "," . TE.decodeUtf8 <$> + lookup "Accept-Encoding" (requestHeaders req) + clientAcceptsGzip = "gzip" `elem` clientAcceptsEncoding + path = basePath </> T.unpack (T.intercalate "/" rest) + gzPath = path ++ ".gz" + sendGzipFile <- + if clientAcceptsGzip + then doesFileExist gzPath + else return False + if sendGzipFile + then callback $ + responseFile + status200 + [ (hContentEncoding, "gzip") + , ( hContentType + , defaultMimeLookup . T.pack . takeFileName $ path) + ] + gzPath + Nothing + else do + exists <- doesFileExist path + if exists + then callback $ sendFile path + else callback fileNotFound + _ -> callback fileNotFound +staticMiddleware _ _ mbJsDistPath _app req callback = + case mbJsDistPath of + Just jsDistPath -> do + let path = jsDistPath </> T.unpack (T.intercalate "/" $ pathInfo req) + exists <- doesFileExist path + if exists + then callback $ sendFile path + else callback $ sendFile (jsDistPath </> "index.html") + Nothing -> callback fileNotFound + +sendFile :: FilePath -> Response +sendFile path = + responseFile + status200 + [(hContentType, defaultMimeLookup $ T.pack $ takeFileName path)] + path + Nothing + +fileNotFound :: Response +fileNotFound = + responseLBS status404 [("Content-Type", "text/plain")] "Not found" + +throwServantError :: (MonadIO m) => ServantErr -> m a +throwServantError = liftIO . throwIO + +server :: Environment -> ServerT API Handler +server env = + hoistServer + (Proxy :: Proxy API) + toServantHandler + (getAllPackages :<|> + getDefinitionSite :<|> + getExpressions :<|> + getReferences :<|> + findIdentifiers) + where + toServantHandler :: ReaderT Environment IO a -> Handler a + toServantHandler ma = Handler . ExceptT . try . runReaderT ma $ env + +application :: Environment -> Application +application env = serve (Proxy :: Proxy API) (server env) + +main :: IO () +main = do + config <- + execParser + (Options.Applicative.info + (configParser <**> helper) + (fullDesc <> + progDesc + "haskell-code-server provides an HTTP API for Haskell code explorer")) + print config + packages <- loadPackages config + case packages of + Just (packageMap, packagePathMap, packageVersions) -> do + loggerSet <- + case configLog config of + HCE.ToFile logfile -> newFileLoggerSet defaultBufSize logfile + HCE.StdOut -> newStdoutLoggerSet defaultBufSize + loggerMiddleware <- + liftIO $ + mkRequestLogger + def {outputFormat = Detailed True, destination = Logger loggerSet} + let staticFilePrefix = configStaticFilesUrlPrefix config + mbJsDistPath = configJsDistDirectory config + environment = + Environment + loggerSet + packageMap + (AllPackages . A.encode $ packageVersions) + config + static = + if configServeStaticFiles config + then staticMiddleware staticFilePrefix packagePathMap mbJsDistPath + else id + run + (configPort config) + (loggerMiddleware . static $ application environment) + Nothing -> putStrLn "No packages found." |