auto-import-0.1.0.0: src/AutoImport/Config.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE TypeFamilies #-}
module AutoImport.Config
( ConfigCache(..)
, Config(..)
, QualMods
, QualMod(..)
, UnqualIdentifiers
, UnqualIdentifier(..)
, IdInfo(..)
, Namespace(..)
, MonoidMap(..)
, resolveConfig
) where
import Control.Applicative ((<|>))
import Control.Monad (guard)
import Control.Exception (throw)
import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Char as Char
import Data.Functor (void)
import Data.IORef
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time (UTCTime)
import Data.Void
import GHC.Generics (Generic, Generically(..))
import qualified System.Directory as Dir
import System.IO.Unsafe (unsafePerformIO)
import qualified Text.Megaparsec as Parse
import qualified Text.Megaparsec.Char as Parse
import qualified Text.Megaparsec.Char.Lexer as Parse (skipLineComment)
import qualified AutoImport.GhcFacade as Ghc
-- | A global ref used to cache the config and the mod time of the config file
configCacheRef :: IORef (Maybe ConfigCache)
configCacheRef = unsafePerformIO $ newIORef Nothing
{-# NOINLINE configCacheRef #-}
data ConfigCache = ConfigCache
{ cachedConfig :: !Config
, localModTime :: !(Maybe UTCTime)
, homeModTime :: !(Maybe UTCTime)
}
data Config = Config
{ qualModules :: QualMods
, unqualIdentifiers :: UnqualIdentifiers
} deriving (Generic, Show)
deriving (Semigroup, Monoid) via Generically Config
newtype MonoidMap k a = MonoidMap {unMonoidMap :: M.Map k a}
deriving (Show)
instance (Ord k, Semigroup a) => Semigroup (MonoidMap k a) where
MonoidMap a <> MonoidMap b = MonoidMap $ M.unionWith (<>) a b
instance (Ord k, Monoid a) => Monoid (MonoidMap k a) where
mempty = MonoidMap mempty
type QualMods = MonoidMap T.Text [QualMod]
data QualMod = QualMod
{ modName :: T.Text
, modQual :: Maybe T.Text
} deriving (Show, Eq, Ord)
type UnqualIdentifiers = M.Map T.Text UnqualIdentifier
data UnqualIdentifier = UnqualIdentifier
{ importByMod :: T.Text
, identifier :: T.Text
, parentTy :: Maybe IdInfo
, isOperator :: Bool
, namespace :: Maybe Namespace
} deriving (Show, Eq, Ord)
data Namespace
= PatternNS
| TypeNS
deriving (Show, Eq, Ord)
data IdInfo = IdInfo
{ idLabel :: T.Text
, idIsOp :: Bool
, idNamespace :: Maybe Namespace
} deriving (Show, Eq, Ord)
localConfigFile, homeConfigFile :: FilePath
localConfigFile = "./.autoimport"
homeConfigFile = "~/.autoimport"
resolveConfig :: Maybe FilePath -> IO Config
resolveConfig mLocalConfigFileOverride = do
mCached <- readIORef configCacheRef
let localFile = fromMaybe localConfigFile mLocalConfigFileOverride
case mCached of
Just configCache -> do
mLocalModTime <-
Dir.doesFileExist localFile >>= \case
True -> Just <$> Dir.getModificationTime localFile
False -> pure Nothing
mHomeModTime <-
Dir.doesFileExist homeConfigFile >>= \case
True -> Just <$> Dir.getModificationTime homeConfigFile
False -> pure Nothing
if mLocalModTime /= localModTime configCache
|| mHomeModTime /= homeModTime configCache
then readAndCacheConfig localFile
else pure $ cachedConfig configCache
Nothing -> readAndCacheConfig localFile
readAndCacheConfig :: FilePath -> IO Config
readAndCacheConfig localFile = do
mHomeCfg <- readConfigFile homeConfigFile
mLocalCfg <- readConfigFile localFile
case (fst <$> mLocalCfg) <> (fst <$> mHomeCfg) of
Nothing -> do
BS8.putStrLn "'.autoimport' file not found by auto-import plugin"
writeIORef configCacheRef Nothing
pure mempty
Just cfg -> do
let cache = ConfigCache
{ cachedConfig = cfg
, localModTime = snd <$> mLocalCfg
, homeModTime = snd <$> mHomeCfg
}
writeIORef configCacheRef (Just cache)
pure cfg
readConfigFile :: FilePath -> IO (Maybe (Config, UTCTime))
readConfigFile file = do
exists <- Dir.doesFileExist file
if exists
then do
content <- T.readFile file
modTime <- Dir.getModificationTime file
case parseConfig file content of
Left err -> do
let diag = ConfigParseFailDiag err
msgEnv =
Ghc.mkPlainErrorMsgEnvelope
Ghc.noSrcSpan
(Ghc.ghcUnknownMessage diag)
throw $ Ghc.mkSrcErr (Ghc.mkMessages $ Ghc.unitBag msgEnv)
Right cfg -> pure $ Just (cfg, modTime)
else pure Nothing
parseConfig :: String -> T.Text -> Either String Config
parseConfig fileName content =
first Parse.errorBundlePretty $ Parse.runParser parser fileName content
where
parser = do
_ <- Parse.optional skipSpaceNoIndent
mconcat
<$> (Parse.many p <* Parse.space <* Parse.eof)
p = ((parseConfigEntry <* Parse.optional parseLineComment)
<|> (mempty <$ parseLineComment))
<* skipSpaceNoIndent
parseConfigEntry :: Parse.Parsec Void T.Text Config
parseConfigEntry = do
moName <- parseModName
let parseUnqualIdsEntry =
(\ids -> mempty
{unqualIdentifiers = M.fromList $ (\i -> (identifier i, i)) <$> ids}
) <$> parseUnqualIds moName
parseQualModEntry =
(\qualMod -> mempty
{qualModules = MonoidMap $
M.singleton (fromMaybe (modName qualMod) (modQual qualMod)) [qualMod]
}) <$> parseModQual moName
selfQualMod = pure mempty
{qualModules = MonoidMap $
M.singleton moName [QualMod moName Nothing]
}
( do
Parse.try hspaceOrIndent
parseUnqualIdsEntry <|> parseQualModEntry
) <|> selfQualMod
parseModQual :: T.Text -> Parse.Parsec Void T.Text QualMod
parseModQual moName = do
qual <- do
Parse.string "as" *> Parse.hspace1
parseModName
pure $ QualMod moName (Just qual)
parseModName :: Parse.Parsec Void T.Text T.Text
parseModName = T.intercalate "." <$> Parse.sepBy1 parseSegment (Parse.char '.')
where
parseSegment = do
h <- Parse.upperChar
rest <- Parse.many (Parse.satisfy (\c -> Char.isAlphaNum c || c == '_'))
Parse.<?> "module name chars"
pure . T.pack $ h : rest
parseUnqualIds :: T.Text -> Parse.Parsec Void T.Text [UnqualIdentifier]
parseUnqualIds moName = concat <$>
Parse.between (Parse.char '(' <* hspaceOrIndent) (Parse.char ')')
(Parse.sepBy1 (parseIdentifier moName <* hspaceOrIndent) (Parse.char ',' <* hspaceOrIndent))
parseIdentifier :: T.Text -> Parse.Parsec Void T.Text [UnqualIdentifier]
parseIdentifier moName = do
mNamespace <- Parse.try . Parse.optional $
(patternP <|> typeP) <* Parse.hspace1
(parent, parentIsOp) <- identP <|> operatorP
let parentId =
UnqualIdentifier
{ importByMod = moName
, identifier = parent
, parentTy = Nothing
, isOperator = parentIsOp
, namespace = mNamespace
}
if parentIsOp || T.all Char.isUpper (T.take 1 parent)
then do
hspaceOrIndent
mChildIds <- Parse.optional childIdsP
case mChildIds of
Nothing -> pure [ parentId ]
Just childIds -> pure . (parentId :) $
(\(cid, isOp) -> UnqualIdentifier
{ importByMod = moName
, identifier = cid
, parentTy = Just (IdInfo parent parentIsOp mNamespace)
, isOperator = isOp
, namespace = Nothing
}) <$> childIds
else pure [ parentId ]
where
identP = do
fc <- Parse.char '_' <|> Parse.letterChar
rest <- Parse.many (Parse.satisfy (\c -> Char.isAlphaNum c || c `elem` ['_', '\'', '#']))
Parse.<?> "identifier chars"
pure (T.pack $ fc : rest, False)
operatorP = (, True) . T.pack <$>
Parse.between (Parse.char '(' <* Parse.hspace) (Parse.hspace *> Parse.char ')')
(Parse.some (Parse.oneOf (":!#$%&*+./<=>?@\\^|-~" :: String) Parse.<?> "operator char"))
childIdsP =
Parse.between (Parse.char '(' <* hspaceOrIndent) (Parse.char ')') $
Parse.sepBy1 (identP <|> operatorP <* hspaceOrIndent)
(Parse.char ',' <* hspaceOrIndent)
patternP = PatternNS <$ Parse.string "pattern"
typeP = TypeNS <$ Parse.string "type"
parseLineComment :: Parse.Parsec Void T.Text ()
parseLineComment = do
Parse.hspace
Parse.skipLineComment "--"
-- Skip horizontal and vertical space until unindented text is found
skipSpaceNoIndent :: Parse.Parsec Void T.Text ()
skipSpaceNoIndent =
void (Parse.some (Parse.hspace *> Parse.eol))
<|> Parse.eof
-- | Parse any amount of whitespace not ending in a newline
hspaceOrIndent :: Parse.Parsec Void T.Text ()
hspaceOrIndent = do
Parse.hspace
(do void Parse.eol
s : _ <- reverse <$> Parse.some Parse.spaceChar
guard (s /= '\n')
) <|> pure ()
-- | Diagnostic thrown when config parsing fails
newtype ConfigParseFailDiag = ConfigParseFailDiag String
instance Ghc.Diagnostic ConfigParseFailDiag where
type DiagnosticOpts ConfigParseFailDiag = Ghc.NoDiagnosticOpts
diagnosticMessage _ (ConfigParseFailDiag err) = Ghc.mkSimpleDecorated $
Ghc.text err
diagnosticReason _ = Ghc.ErrorWithoutFlag
diagnosticHints _ = []
diagnosticCode _ = Nothing
#if !MIN_VERSION_ghc(9,8,0)
defaultDiagnosticOpts = Ghc.NoDiagnosticOpts
#endif