module Language.PureScript.Ide.Imports
( addImplicitImport
, addQualifiedImport
, addImportForIdentifier
, answerRequest
, parseImportsFromFile
, parseImport
, prettyPrintImportSection
, addImplicitImport'
, addQualifiedImport'
, addExplicitImport'
, sliceImportSection
, prettyPrintImport'
, Import(Import)
)
where
import Protolude hiding (moduleName)
import Data.List (findIndex, nubBy, partition)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Text as T
import qualified Language.PureScript as P
import qualified Language.PureScript.Constants as C
import qualified Language.PureScript.CST as CST
import Language.PureScript.Ide.Completion
import Language.PureScript.Ide.Error
import Language.PureScript.Ide.Filter
import Language.PureScript.Ide.State
import Language.PureScript.Ide.Prim
import Language.PureScript.Ide.Types
import Language.PureScript.Ide.Util
import Lens.Micro.Platform ((^.), (%~), ix, has)
import System.IO.UTF8 (writeUTF8FileT)
data Import = Import P.ModuleName P.ImportDeclarationType (Maybe P.ModuleName)
deriving (Eq, Show)
parseImportsFromFile
:: (MonadIO m, MonadError IdeError m)
=> FilePath
-> m (P.ModuleName, [(P.ModuleName, P.ImportDeclarationType, Maybe P.ModuleName)])
parseImportsFromFile file = do
(mn, _, imports, _) <- parseImportsFromFile' file
pure (mn, unwrapImport <$> imports)
where
unwrapImport (Import a b c) = (a, b, c)
parseImportsFromFile'
:: (MonadIO m, MonadError IdeError m)
=> FilePath
-> m (P.ModuleName, [Text], [Import], [Text])
parseImportsFromFile' fp = do
(_, file) <- ideReadFile fp
case sliceImportSection (T.lines file) of
Right res -> pure res
Left err -> throwError (GeneralError err)
data ImportParse = ImportParse
{ ipModuleName :: P.ModuleName
, ipStart :: P.SourcePos
, ipEnd :: P.SourcePos
, ipImports :: [Import]
}
parseModuleHeader :: Text -> Either (NE.NonEmpty CST.ParserError) ImportParse
parseModuleHeader src = do
CST.PartialResult md _ <- CST.parseModule $ CST.lenient $ CST.lex src
let
mn = CST.nameValue $ CST.modNamespace md
decls = flip fmap (CST.modImports md) $ \decl -> do
let ((ss, _), mn', it, qual) = CST.convertImportDecl "<purs-ide>" decl
(ss, Import mn' it qual)
case (head decls, lastMay decls) of
(Just hd, Just ls) -> do
let
ipStart = P.spanStart $ fst hd
ipEnd = P.spanEnd $ fst ls
pure $ ImportParse mn ipStart ipEnd $ snd <$> decls
_ -> do
let pos = CST.sourcePos . CST.srcEnd . CST.tokRange . CST.tokAnn $ CST.modWhere md
pure $ ImportParse mn pos pos []
sliceImportSection :: [Text] -> Either Text (P.ModuleName, [Text], [Import], [Text])
sliceImportSection fileLines = first (toS . CST.prettyPrintError . NE.head) $ do
ImportParse{..} <- parseModuleHeader file
pure
( ipModuleName
, sliceFile (P.SourcePos 1 1) (prevPos ipStart)
, ipImports
, drop 1 (sliceFile (nextPos ipEnd) (P.SourcePos (length fileLines) (lineLength (length fileLines))))
)
where
prevPos (P.SourcePos l c)
| l == 1 && c == 1 = P.SourcePos l c
| c == 1 = P.SourcePos (l - 1) (lineLength (l - 1))
| otherwise = P.SourcePos l (c - 1)
nextPos (P.SourcePos l c)
| c == lineLength l = P.SourcePos (l + 1) 1
| otherwise = P.SourcePos l (c + 1)
file = T.unlines fileLines
lineLength l = T.length (fileLines ^. ix (l - 1))
sliceFile (P.SourcePos l1 c1) (P.SourcePos l2 c2) =
fileLines
& drop (l1 - 1)
& take (l2 - l1 + 1)
& ix 0 %~ T.drop (c1 - 1)
& ix (l2 - l1) %~ T.take c2
addImplicitImport
:: (MonadIO m, MonadError IdeError m)
=> FilePath
-> P.ModuleName
-> m [Text]
addImplicitImport fp mn = do
(_, pre, imports, post) <- parseImportsFromFile' fp
let newImportSection = addImplicitImport' imports mn
pure $ joinSections (pre, newImportSection, post)
addImplicitImport' :: [Import] -> P.ModuleName -> [Text]
addImplicitImport' imports mn =
prettyPrintImportSection (Import mn P.Implicit Nothing : imports)
addQualifiedImport
:: (MonadIO m, MonadError IdeError m)
=> FilePath
-> P.ModuleName
-> P.ModuleName
-> m [Text]
addQualifiedImport fp mn qualifier = do
(_, pre, imports, post) <- parseImportsFromFile' fp
let newImportSection = addQualifiedImport' imports mn qualifier
pure $ joinSections (pre, newImportSection, post)
addQualifiedImport' :: [Import] -> P.ModuleName -> P.ModuleName -> [Text]
addQualifiedImport' imports mn qualifier =
prettyPrintImportSection (Import mn P.Implicit (Just qualifier) : imports)
addExplicitImport :: (MonadIO m, MonadError IdeError m) =>
FilePath -> IdeDeclaration -> P.ModuleName -> Maybe P.ModuleName -> m [Text]
addExplicitImport fp decl moduleName qualifier = do
(mn, pre, imports, post) <- parseImportsFromFile' fp
let newImportSection =
if mn == moduleName
then imports
else addExplicitImport' decl moduleName qualifier imports
pure $ joinSections (pre, prettyPrintImportSection newImportSection, post)
addExplicitImport' :: IdeDeclaration -> P.ModuleName -> Maybe P.ModuleName -> [Import] -> [Import]
addExplicitImport' decl moduleName qualifier imports =
let
isImplicitlyImported =
any (\case
Import mn P.Implicit qualifier' -> mn == moduleName && qualifier == qualifier'
_ -> False) imports
isNotExplicitlyImportedFromPrim =
moduleName == C.Prim &&
not (any (\case
Import C.Prim (P.Explicit _) Nothing -> True
_ -> False) imports)
isModule = has _IdeDeclModule decl
matches (Import mn (P.Explicit _) qualifier') = mn == moduleName && qualifier == qualifier'
matches _ = False
freshImport = Import moduleName (P.Explicit [refFromDeclaration decl]) qualifier
in
if isImplicitlyImported || isNotExplicitlyImportedFromPrim || isModule
then imports
else updateAtFirstOrPrepend matches (insertDeclIntoImport decl) freshImport imports
where
refFromDeclaration (IdeDeclTypeClass tc) =
P.TypeClassRef ideSpan (tc ^. ideTCName)
refFromDeclaration (IdeDeclDataConstructor dtor) =
P.TypeRef ideSpan (dtor ^. ideDtorTypeName) Nothing
refFromDeclaration (IdeDeclType t) =
P.TypeRef ideSpan (t ^. ideTypeName) (Just [])
refFromDeclaration (IdeDeclValueOperator op) =
P.ValueOpRef ideSpan (op ^. ideValueOpName)
refFromDeclaration (IdeDeclTypeOperator op) =
P.TypeOpRef ideSpan (op ^. ideTypeOpName)
refFromDeclaration (IdeDeclKind kn) =
P.KindRef ideSpan kn
refFromDeclaration d =
P.ValueRef ideSpan (P.Ident (identifierFromIdeDeclaration d))
insertDeclIntoImport :: IdeDeclaration -> Import -> Import
insertDeclIntoImport decl' (Import mn (P.Explicit refs) qual) =
Import mn (P.Explicit (sortBy P.compDecRef (insertDeclIntoRefs decl' refs))) qual
insertDeclIntoImport _ is = is
insertDeclIntoRefs :: IdeDeclaration -> [P.DeclarationRef] -> [P.DeclarationRef]
insertDeclIntoRefs d@(IdeDeclDataConstructor dtor) refs =
updateAtFirstOrPrepend
(matchType (dtor ^. ideDtorTypeName))
(insertDtor (dtor ^. ideDtorName))
(refFromDeclaration d)
refs
insertDeclIntoRefs (IdeDeclType t) refs
| any matches refs = refs
where
matches (P.TypeRef _ typeName _) = _ideTypeName t == typeName
matches _ = False
insertDeclIntoRefs dr refs = nubBy ((==) `on` P.prettyPrintRef) (refFromDeclaration dr : refs)
insertDtor _ (P.TypeRef ss tn' _) = P.TypeRef ss tn' Nothing
insertDtor _ refs = refs
matchType :: P.ProperName 'P.TypeName -> P.DeclarationRef -> Bool
matchType tn (P.TypeRef _ n _) = tn == n
matchType _ _ = False
ideSpan :: P.SourceSpan
ideSpan = P.internalModuleSourceSpan "<psc-ide>"
updateAtFirstOrPrepend :: (a -> Bool) -> (a -> a) -> a -> [a] -> [a]
updateAtFirstOrPrepend p t d l =
case findIndex p l of
Nothing -> d : l
Just i ->
let (x, a : y) = splitAt i l
in x ++ [t a] ++ y
addImportForIdentifier
:: (Ide m, MonadError IdeError m)
=> FilePath
-> Text
-> Maybe P.ModuleName
-> [Filter]
-> m (Either [Match IdeDeclaration] [Text])
addImportForIdentifier fp ident qual filters = do
let addPrim = Map.union idePrimDeclarations
modules <- getAllModules Nothing
let
matches =
getExactMatches ident filters (addPrim modules)
& map (fmap discardAnn)
& filter (\(Match (_, d)) -> not (has _IdeDeclModule d))
case matches of
[] ->
throwError (NotFound "Couldn't find the given identifier. \
\Have you loaded the corresponding module?")
[Match (m, decl)] ->
Right <$> addExplicitImport fp decl m qual
ms@[Match (m1, d1), Match (m2, d2)] ->
if m1 /= m2
then pure (Left ms)
else case decideRedundantCase d1 d2 <|> decideRedundantCase d2 d1 of
Just decl ->
Right <$> addExplicitImport fp decl m1 qual
Nothing ->
throwError (GeneralError "Undecidable between type and dataconstructor")
xs ->
pure (Left xs)
where
decideRedundantCase d@(IdeDeclDataConstructor dtor) (IdeDeclType t) =
if dtor ^. ideDtorTypeName == t ^. ideTypeName then Just d else Nothing
decideRedundantCase IdeDeclType{} ts@IdeDeclTypeSynonym{} =
Just ts
decideRedundantCase _ _ = Nothing
prettyPrintImport' :: Import -> Text
prettyPrintImport' (Import mn idt qual) =
"import " <> P.prettyPrintImport mn idt qual
prettyPrintImportSection :: [Import] -> [Text]
prettyPrintImportSection imports =
let
(implicitImports, explicitImports) = partition isImplicitImport imports
in
sort (map prettyPrintImport' implicitImports)
<> (guard (not (null explicitImports || null implicitImports)) $> "")
<> sort (map prettyPrintImport' explicitImports)
where
isImplicitImport :: Import -> Bool
isImplicitImport i = case i of
Import _ P.Implicit Nothing -> True
Import _ (P.Hiding _) Nothing -> True
_ -> False
answerRequest :: (MonadIO m) => Maybe FilePath -> [Text] -> m Success
answerRequest outfp rs =
case outfp of
Nothing -> pure (MultilineTextResult rs)
Just outfp' -> do
liftIO (writeUTF8FileT outfp' (T.unlines rs))
pure (TextResult ("Written to " <> T.pack outfp'))
parseImport :: Text -> Maybe Import
parseImport t =
case fmap (CST.convertImportDecl "<purs-ide>")
$ CST.runTokenParser CST.parseImportDeclP
$ CST.lex t of
Right (_, mn, idt, mmn) ->
Just (Import mn idt mmn)
_ -> Nothing
joinSections :: ([Text], [Text], [Text]) -> [Text]
joinSections (pre, decls, post) = pre `joinLine` (decls `joinLine` post)
where
isBlank = T.all (== ' ')
joinLine as bs
| Just ln1 <- lastMay as
, Just ln2 <- head bs
, not (isBlank ln1) && not (isBlank ln2) =
as ++ [""] ++ bs
| otherwise =
as ++ bs