{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS -Wno-orphans #-}
{-# LANGUAGE TupleSections #-}
module Ide.Plugin.Retrie (descriptor) where
import Control.Concurrent.STM (readTVarIO)
import Control.Exception.Safe (Exception (..),
SomeException, assert,
catch, throwIO, try)
import Control.Monad (forM, unless, when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans.Class (MonadTrans (lift))
import Control.Monad.Trans.Except (ExceptT (ExceptT),
runExceptT, throwE)
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Writer.Strict
import Data.Aeson (FromJSON (..),
ToJSON (..),
Value (Null))
import Data.Bifunctor (second)
import qualified Data.ByteString as BS
import Data.Coerce
import Data.Data
import Data.Either (partitionEithers)
import Data.Hashable (Hashable (hash),
unhashed)
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as Set
import Data.IORef.Extra (atomicModifyIORef'_,
newIORef, readIORef)
import Data.List.Extra (find, nubOrdOn)
import Data.Maybe (catMaybes, fromJust,
listToMaybe)
import Data.String (IsString)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Typeable (Typeable)
import Debug.Trace
import Development.IDE hiding (pluginHandlers)
import Development.IDE.Core.PositionMapping
import Development.IDE.Core.Shake (ShakeExtras (ShakeExtras, knownTargetsVar),
clientCapabilities,
getShakeExtras,
hiedbWriter,
toKnownFiles, withHieDb)
import Development.IDE.GHC.Compat (GRHSs (GRHSs),
GenLocated (L), GhcPs,
GhcRn, GhcTc,
HsBindLR (FunBind),
HsExpr (HsApp, OpApp),
HsGroup (..),
HsValBindsLR (..),
HscEnv, IdP,
ImportDecl (..), LHsExpr,
LRuleDecls, Match,
ModIface,
ModSummary (ModSummary, ms_hspp_buf, ms_mod),
Name, Outputable,
ParsedModule (..),
RealSrcLoc,
RuleDecl (HsRule),
RuleDecls (HsRules),
SourceText (..),
TyClDecl (SynDecl),
TyClGroup (..), fun_id,
hm_iface, isQual,
isQual_maybe, isVarOcc,
locA, mi_fixities,
moduleName,
moduleNameString,
ms_hspp_opts,
nameModule_maybe,
nameOccName, nameRdrName,
noLocA, occNameFS,
occNameString,
pattern IsBoot,
pattern NotBoot,
pattern RealSrcSpan,
pm_parsed_source,
printWithoutUniques,
rdrNameOcc, rds_rules,
srcSpanFile, topDir,
unLoc, unLocA)
import qualified Development.IDE.GHC.Compat as GHC
import Development.IDE.GHC.Compat.Util hiding (catch, try)
import Development.IDE.GHC.Dump (showAstDataHtml)
import Development.IDE.GHC.ExactPrint (ExceptStringT (ExceptStringT),
GetAnnotatedParsedSource (GetAnnotatedParsedSource),
TransformT,
graftExprWithM,
graftSmallestDeclsWithM,
hoistGraft, transformM)
import qualified GHC (Module, ParsedSource,
moduleName, parseModule)
import qualified GHC as GHCGHC
import GHC.Generics (Generic)
import GHC.Hs.Dump
import Ide.PluginUtils
import Ide.Types
import Language.LSP.Server (LspM,
ProgressCancellable (Cancellable),
sendNotification,
sendRequest,
withIndefiniteProgress)
import Language.LSP.Types as J hiding
(SemanticTokenAbsolute (length, line),
SemanticTokenRelative (length),
SemanticTokensEdit (_start))
import Retrie (Annotated (astA),
AnnotatedModule,
Fixity (Fixity),
FixityDirection (InfixL),
Options, Options_ (..),
RewriteSpec,
Verbosity (Loud),
addImports, apply,
applyWithUpdate)
import Retrie.Context
import Retrie.CPP (CPP (NoCPP), parseCPP)
import Retrie.ExactPrint (Annotated, fix,
transformA, unsafeMkA)
import Retrie.Expr (mkLocatedHsVar)
import Retrie.Fixity (FixityEnv, lookupOp,
mkFixityEnv)
import Retrie.Monad (getGroundTerms,
runRetrie)
import Retrie.Options (defaultOptions,
getTargetFiles)
import Retrie.Replace (Change (..),
Replacement (..))
import Retrie.Rewrites
import Retrie.Rewrites.Function (matchToRewrites)
import Retrie.SYB (everything, extQ,
listify, mkQ)
import Retrie.Types
import Retrie.Universe (Universe)
import System.Directory (makeAbsolute)
#if MIN_VERSION_ghc(9,3,0)
import GHC.Types.PkgQual
#endif
#if MIN_VERSION_ghc(9,2,0)
import Control.Exception (evaluate)
import Data.Monoid (First (First))
import Retrie.ExactPrint (makeDeltaAst)
import Retrie.GHC (ann)
#else
import Data.Monoid (First (..))
import qualified GHC.Exts as Ext
import Retrie.AlphaEnv (extendAlphaEnv)
import Retrie.ExactPrint (relativiseApiAnns)
#endif
import Control.Arrow ((&&&))
import Development.IDE.Core.Actions (lookupMod)
import Development.IDE.Spans.AtPoint (LookupModule,
getNamesAtPoint,
nameToLocation)
import Development.IDE.Types.Shake (WithHieDb)
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId =
(defaultPluginDescriptor plId)
{ pluginHandlers = mkPluginHandler STextDocumentCodeAction provider,
pluginCommands = [retrieCommand, retrieInlineThisCommand]
}
retrieCommandName :: T.Text
retrieCommandName = "retrieCommand"
retrieInlineThisCommandName :: T.Text
retrieInlineThisCommandName = "retrieInlineThisCommand"
retrieCommand :: PluginCommand IdeState
retrieCommand =
PluginCommand (coerce retrieCommandName) "run the refactoring" runRetrieCmd
retrieInlineThisCommand :: PluginCommand IdeState
retrieInlineThisCommand =
PluginCommand (coerce retrieInlineThisCommandName) "inline function call"
runRetrieInlineThisCmd
-- | Parameters for the runRetrie PluginCommand.
data RunRetrieParams = RunRetrieParams
{ description :: T.Text,
rewrites :: [RewriteSpec],
originatingFile :: Uri,
restrictToOriginatingFile :: Bool
}
deriving (Eq, Show, Generic, FromJSON, ToJSON)
runRetrieCmd ::
IdeState ->
RunRetrieParams ->
LspM c (Either ResponseError Value)
runRetrieCmd state RunRetrieParams{originatingFile = uri, ..} =
withIndefiniteProgress description Cancellable $ do
runMaybeT $ do
nfp <- MaybeT $ return $ uriToNormalizedFilePath $ toNormalizedUri uri
(session, _) <- MaybeT $ liftIO $
runAction "Retrie.GhcSessionDeps" state $
useWithStale GhcSessionDeps
nfp
(ms, binds, _, _, _) <- MaybeT $ liftIO $
runAction "Retrie.getBinds" state $ getBinds nfp
let importRewrites = concatMap (extractImports ms binds) rewrites
(errors, edits) <- liftIO $
callRetrie
state
(hscEnv session)
(map Right rewrites <> map Left importRewrites)
nfp
restrictToOriginatingFile
unless (null errors) $
lift $ sendNotification SWindowShowMessage $
ShowMessageParams MtWarning $
T.unlines $
"## Found errors during rewrite:" :
["-" <> T.pack (show e) | e <- errors]
lift $ sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edits) (\_ -> pure ())
return ()
return $ Right Null
data RunRetrieInlineThisParams = RunRetrieInlineThisParams
{ inlineIntoThisLocation :: !Location,
inlineFromThisLocation :: !Location,
inlineThisDefinition :: !T.Text
}
deriving (Eq, Show, Generic, FromJSON, ToJSON)
runRetrieInlineThisCmd :: IdeState
-> RunRetrieInlineThisParams -> LspM c (Either ResponseError Value)
runRetrieInlineThisCmd state RunRetrieInlineThisParams{..} = pluginResponse $ do
nfp <- handleMaybe "uri" $ uriToNormalizedFilePath $ toNormalizedUri $ getLocationUri inlineIntoThisLocation
nfpSource <- handleMaybe "sourceUri" $
uriToNormalizedFilePath $ toNormalizedUri $ getLocationUri inlineFromThisLocation
-- What we do here:
-- Find the identifier in the given position
-- Construct an inline rewrite for it
-- Run retrie to get a list of changes
-- Select the change that inlines the identifier in the given position
-- Apply the edit
ast <- handleMaybeM "ast" $ liftIO $ runAction "retrie" state $
use GetAnnotatedParsedSource nfp
astSrc <- handleMaybeM "ast" $ liftIO $ runAction "retrie" state $
use GetAnnotatedParsedSource nfpSource
msr <- handleMaybeM "modSummary" $ liftIO $ runAction "retrie" state $
use GetModSummaryWithoutTimestamps nfp
hiFileRes <- handleMaybeM "modIface" $ liftIO $ runAction "retrie" state $
use GetModIface nfpSource
let fixityEnv = fixityEnvFromModIface (hirModIface hiFileRes)
fromRange = rangeToRealSrcSpan nfpSource $ getLocationRange inlineFromThisLocation
intoRange = rangeToRealSrcSpan nfp $ getLocationRange inlineIntoThisLocation
inlineRewrite <- liftIO $ constructInlineFromIdentifer astSrc fromRange
when (null inlineRewrite) $ throwE "Empty rewrite"
let ShakeExtras{..}= shakeExtras state
(session, _) <- handleMaybeM "GHCSession" $ liftIO $ runAction "retrie" state $
useWithStale GhcSessionDeps nfp
(fixityEnv, cpp) <- liftIO $ getCPPmodule state (hscEnv session) $ fromNormalizedFilePath nfp
result <- liftIO $ try @_ @SomeException $
runRetrie fixityEnv (applyWithUpdate myContextUpdater inlineRewrite) cpp
case result of
Left err -> throwE $ "Retrie - crashed with: " <> show err
Right (_,_,NoChange) -> throwE "Retrie - inline produced no changes"
Right (_,_,Change replacements imports) -> do
let edits = asEditMap $ asTextEdits $ Change ourReplacement imports
wedit = WorkspaceEdit (Just edits) Nothing Nothing
ourReplacement = [ r
| r@Replacement{..} <- replacements
, RealSrcSpan intoRange Nothing `GHC.isSubspanOf` replLocation]
lift $ sendRequest SWorkspaceApplyEdit
(ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ())
return Null
-- Override to skip adding binders to the context, which prevents inlining
-- nested defined functions
myContextUpdater :: ContextUpdater
myContextUpdater c i =
updateContext c i
`extQ` (return . updExp)
`extQ` (skipUpdate @(GRHSs GhcPs (LHsExpr GhcPs)))
`extQ` (skipUpdate @(Match GhcPs (LHsExpr GhcPs)))
where
skipUpdate :: forall a m . Monad m => a -> TransformT m Context
skipUpdate _ = pure c
-- override to skip the HsLet case
updExp :: HsExpr GhcPs -> Context
updExp HsApp{} =
c { ctxtParentPrec = HasPrec $ Retrie.Fixity (SourceText "HsApp") (10 + i - firstChild) InfixL }
-- Reason for 10 + i: (i is index of child, 0 = left, 1 = right)
-- In left child, prec is 10, so HsApp child will NOT get paren'd
-- In right child, prec is 11, so every child gets paren'd (unless atomic)
updExp (OpApp _ _ op _) = c { ctxtParentPrec = HasPrec $ lookupOp op (ctxtFixityEnv c) }
updExp _ = c { ctxtParentPrec = NeverParen }
-- Deal with Trees-That-Grow adding extension points
-- as the first child everywhere.
firstChild :: Int
firstChild = 1
extractImports :: ModSummary -> [HsBindLR GhcRn GhcRn] -> RewriteSpec -> [ImportSpec]
extractImports ModSummary{ms_mod} topLevelBinds (Unfold thing)
| Just FunBind {fun_matches}
<- find (\case FunBind{fun_id = L _ n} -> T.unpack (printOutputable n) == thing ; _ -> False) topLevelBinds
, names <- listify p fun_matches
=
[ AddImport {..}
| let ideclSource = False,
name <- names,
let r = nameRdrName name,
let ideclQualifiedBool = isQual r,
let ideclAsString = moduleNameString . fst <$> isQual_maybe r,
let ideclThing = Just (IEVar $ occNameString $ rdrNameOcc r),
Just ideclNameString <-
[moduleNameString . GHC.moduleName <$> nameModule_maybe name]
]
where
p name = nameModule_maybe name /= Just ms_mod
-- TODO handle imports for all rewrites
extractImports _ _ _ = []
-------------------------------------------------------------------------------
provider :: PluginMethodHandler IdeState TextDocumentCodeAction
provider state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) range ca) = pluginResponse $ do
let (J.CodeActionContext _diags _monly) = ca
nuri = toNormalizedUri uri
nfp <- handleMaybe "uri" $ uriToNormalizedFilePath nuri
(ModSummary{ms_mod}, topLevelBinds, posMapping, hs_ruleds, hs_tyclds)
<- handleMaybeM "typecheck" $ liftIO $ runAction "retrie" state $
getBinds nfp
extras@ShakeExtras{ withHieDb, hiedbWriter } <- liftIO $ runAction "" state getShakeExtras
range <- handleMaybe "range" $ fromCurrentRange posMapping range
let pos = _start range
let rewrites =
concatMap (suggestBindRewrites uri pos ms_mod) topLevelBinds
++ concatMap (suggestRuleRewrites uri pos ms_mod) hs_ruleds
++ [ r
| TyClGroup {group_tyclds} <- hs_tyclds,
L (locA -> l) g <- group_tyclds,
pos `isInsideSrcSpan` l,
r <- suggestTypeRewrites uri ms_mod g
]
retrieCommands <- lift $
forM rewrites $ \(title, kind, params) -> liftIO $ do
let c = mkLspCommand plId (coerce retrieCommandName) title (Just [toJSON params])
return $ CodeAction title (Just kind) Nothing Nothing Nothing Nothing (Just c) Nothing
inlineSuggestions <- liftIO $ runIdeAction "" extras $
suggestBindInlines plId uri topLevelBinds range withHieDb (lookupMod hiedbWriter)
let inlineCommands =
[ Just $
CodeAction _title (Just CodeActionRefactorInline) Nothing Nothing Nothing Nothing (Just c) Nothing
| c@Command{..} <- inlineSuggestions
]
return $ J.List [InR c | c <- retrieCommands ++ catMaybes inlineCommands]
getLocationUri :: Location -> Uri
getLocationUri Location{_uri} = _uri
getLocationRange Location{_range} = _range
getBinds :: NormalizedFilePath -> Action (Maybe (ModSummary, [HsBindLR GhcRn GhcRn], PositionMapping, [LRuleDecls GhcRn], [TyClGroup GhcRn]))
getBinds nfp = runMaybeT $ do
(tm, posMapping) <- MaybeT $ useWithStale TypeCheck nfp
-- we use the typechecked source instead of the parsed source
-- to be able to extract module names from the Ids,
-- so that we can include adding the required imports in the retrie command
let rn = tmrRenamed tm
( HsGroup
{ hs_valds =
XValBindsLR
(GHC.NValBinds binds _sigs :: GHC.NHsValBindsLR GhcRn),
hs_ruleds,
hs_tyclds
},
_,
_,
_
) = rn
topLevelBinds =
[ decl
| (_, bagBinds) <- binds,
L _ decl <- bagToList bagBinds
]
return (tmrModSummary tm, topLevelBinds, posMapping, hs_ruleds, hs_tyclds)
suggestBindRewrites ::
Uri ->
Position ->
GHC.Module ->
HsBindLR GhcRn GhcRn ->
[(T.Text, CodeActionKind, RunRetrieParams)]
suggestBindRewrites originatingFile pos ms_mod FunBind {fun_id = L (locA -> l') rdrName}
| pos `isInsideSrcSpan` l' =
let pprNameText = printOutputable rdrName
pprName = T.unpack pprNameText
unfoldRewrite restrictToOriginatingFile =
let rewrites = [Unfold (qualify ms_mod pprName)]
description = "Unfold " <> pprNameText <> describeRestriction restrictToOriginatingFile
in (description, CodeActionRefactorInline, RunRetrieParams {..})
foldRewrite restrictToOriginatingFile =
let rewrites = [Fold (qualify ms_mod pprName)]
description = "Fold " <> pprNameText <> describeRestriction restrictToOriginatingFile
in (description, CodeActionRefactorExtract, RunRetrieParams {..})
in [unfoldRewrite False, unfoldRewrite True, foldRewrite False, foldRewrite True]
suggestBindRewrites _ _ _ _ = []
-- find all the identifiers in the AST for which have source definitions
suggestBindInlines :: PluginId -> Uri -> [HsBindLR GhcRn GhcRn] -> Range -> WithHieDb -> _ -> IdeAction [Command]
suggestBindInlines plId uri binds range hie lookupMod = do
identifiers <- definedIdentifiers
return $ map (\(name, siteLoc, srcLoc) ->
let
title = "Inline " <> printedName
printedName = printOutputable name
params = RunRetrieInlineThisParams
{ inlineIntoThisLocation = siteLoc
, inlineFromThisLocation = srcLoc
, inlineThisDefinition= printedName
}
in mkLspCommand plId (coerce retrieInlineThisCommandName) title (Just [toJSON params])
)
(Set.toList identifiers)
where
definedIdentifiers =
-- we search for candidates to inline in RHSs only, skipping LHSs
everything (<>) (pure mempty `mkQ` getGRHSIdentifierDetails hie lookupMod) binds
getGRHSIdentifierDetails :: WithHieDb -> _ -> GRHSs GhcRn (LHsExpr GhcRn) -> IdeAction (Set.HashSet (GHC.OccName, Location, Location))
getGRHSIdentifierDetails a b it@GRHSs{} =
-- we only select candidates for which we have source code
everything (<>) (pure mempty `mkQ` getDefinedIdentifierDetailsViaHieDb a b) it
getDefinedIdentifierDetailsViaHieDb :: WithHieDb -> LookupModule IdeAction -> GHC.LIdP GhcRn -> IdeAction (Set.HashSet (GHC.OccName, Location, Location))
getDefinedIdentifierDetailsViaHieDb withHieDb lookupModule lname | name <- unLoc lname =
case srcSpanToLocation (GHC.getLocA lname) of
Just siteLoc
| siteRange <- getLocationRange siteLoc
, range `isSubrangeOf` siteRange -> do
mbSrcLocation <- nameToLocation withHieDb lookupModule name
return $ maybe mempty (Set.fromList . map (nameOccName name, siteLoc,)) mbSrcLocation
_ -> pure mempty
describeRestriction :: IsString p => Bool -> p
describeRestriction restrictToOriginatingFile =
if restrictToOriginatingFile then " in current file" else ""
suggestTypeRewrites ::
(Outputable (IdP GhcRn)) =>
Uri ->
GHC.Module ->
TyClDecl GhcRn ->
[(T.Text, CodeActionKind, RunRetrieParams)]
suggestTypeRewrites originatingFile ms_mod SynDecl {tcdLName} =
let pprNameText = printOutputable (unLocA tcdLName)
pprName = T.unpack pprNameText
unfoldRewrite restrictToOriginatingFile =
let rewrites = [TypeForward (qualify ms_mod pprName)]
description = "Unfold " <> pprNameText <> describeRestriction restrictToOriginatingFile
in (description, CodeActionRefactorInline, RunRetrieParams {..})
foldRewrite restrictToOriginatingFile =
let rewrites = [TypeBackward (qualify ms_mod pprName)]
description = "Fold " <> pprNameText <> describeRestriction restrictToOriginatingFile
in (description, CodeActionRefactorExtract, RunRetrieParams {..})
in [unfoldRewrite False, unfoldRewrite True, foldRewrite False, foldRewrite True]
suggestTypeRewrites _ _ _ = []
suggestRuleRewrites ::
Uri ->
Position ->
GHC.Module ->
LRuleDecls GhcRn ->
[(T.Text, CodeActionKind, RunRetrieParams)]
suggestRuleRewrites originatingFile pos ms_mod (L _ HsRules {rds_rules}) =
concat
[ [ forwardRewrite ruleName True
, forwardRewrite ruleName False
, backwardsRewrite ruleName True
, backwardsRewrite ruleName False
]
| L (locA -> l) r <- rds_rules,
pos `isInsideSrcSpan` l,
#if MIN_VERSION_ghc(9,5,0)
let HsRule {rd_name = L _ rn} = r,
#else
let HsRule {rd_name = L _ (_, rn)} = r,
#endif
let ruleName = unpackFS rn
]
where
forwardRewrite ruleName restrictToOriginatingFile =
let rewrites = [RuleForward (qualify ms_mod ruleName)]
description = "Apply rule " <> T.pack ruleName <> " forward" <>
describeRestriction restrictToOriginatingFile
in ( description,
CodeActionRefactor,
RunRetrieParams {..}
)
backwardsRewrite ruleName restrictToOriginatingFile =
let rewrites = [RuleBackward (qualify ms_mod ruleName)]
description = "Apply rule " <> T.pack ruleName <> " backwards" <>
describeRestriction restrictToOriginatingFile
in ( description,
CodeActionRefactor,
RunRetrieParams {..}
)
qualify :: Outputable mod => mod -> String -> String
qualify ms_mod x = T.unpack (printOutputable ms_mod) <> "." <> x
-------------------------------------------------------------------------------
-- Retrie driving code
data CallRetrieError
= CallRetrieInternalError String NormalizedFilePath
| NoParse NormalizedFilePath
| GHCParseError NormalizedFilePath String
| NoTypeCheck NormalizedFilePath
deriving (Eq, Typeable)
instance Show CallRetrieError where
show (CallRetrieInternalError msg f) = msg <> " - " <> fromNormalizedFilePath f
show (NoParse f) = "Cannot parse: " <> fromNormalizedFilePath f
show (GHCParseError f m) = "Cannot parse " <> fromNormalizedFilePath f <> " : " <> m
show (NoTypeCheck f) = "File does not typecheck: " <> fromNormalizedFilePath f
instance Exception CallRetrieError
callRetrie ::
IdeState ->
HscEnv ->
[Either ImportSpec RewriteSpec] ->
NormalizedFilePath ->
Bool ->
IO ([CallRetrieError], WorkspaceEdit)
callRetrie state session rewrites origin restrictToOriginatingFile = do
knownFiles <- toKnownFiles . unhashed <$> readTVarIO (knownTargetsVar $ shakeExtras state)
let
-- TODO cover all workspaceFolders
target = "."
retrieOptions :: Retrie.Options
retrieOptions = (defaultOptions target)
{Retrie.verbosity = Loud
,Retrie.targetFiles = map fromNormalizedFilePath $
if restrictToOriginatingFile
then [origin]
else Set.toList knownFiles
}
(theImports, theRewrites) = partitionEithers rewrites
annotatedImports =
#if MIN_VERSION_ghc(9,2,0)
unsafeMkA (map (noLocA . toImportDecl) theImports) 0
#else
unsafeMkA (map (noLocA . toImportDecl) theImports) mempty 0
#endif
(originFixities, originParsedModule) <- reuseParsedModule state origin
retrie <-
(\specs -> apply specs >> addImports annotatedImports)
<$> parseSpecs state origin originParsedModule originFixities theRewrites
targets <- getTargetFiles retrieOptions (getGroundTerms retrie)
results <- forM targets $ \t -> runExceptT $ do
(fixityEnv, cpp) <- ExceptT $ try $ getCPPmodule state session t
-- TODO add the imports to the resulting edits
(_user, ast, change@(Change _replacements _imports)) <-
lift $ runRetrie fixityEnv retrie cpp
return $ asTextEdits change
let (errors :: [CallRetrieError], replacements) = partitionEithers results
editParams :: WorkspaceEdit
editParams =
WorkspaceEdit (Just $ asEditMap $ concat replacements) Nothing Nothing
return (errors, editParams)
useOrFail ::
IdeRule r v =>
IdeState ->
String ->
(NormalizedFilePath -> CallRetrieError) ->
r ->
NormalizedFilePath ->
IO (RuleResult r)
useOrFail state lbl mkException rule f =
useRule lbl state rule f >>= maybe (liftIO $ throwIO $ mkException f) return
fixityEnvFromModIface :: ModIface -> FixityEnv
fixityEnvFromModIface modIface =
mkFixityEnv
[ (fs, (fs, fixity))
| (n, fixity) <- mi_fixities modIface,
let fs = occNameFS n
]
fixFixities :: Data ast =>
IdeState
-> NormalizedFilePath
-> Annotated ast
-> IO (FixityEnv, Annotated ast)
fixFixities state f pm = do
HiFileResult {hirModIface} <-
useOrFail state "GetModIface" NoTypeCheck GetModIface f
let fixities = fixityEnvFromModIface hirModIface
res <- transformA pm (fix fixities)
return (fixities, res)
fixAnns :: ParsedModule -> Annotated GHC.ParsedSource
#if MIN_VERSION_ghc(9,2,0)
fixAnns GHC.ParsedModule{pm_parsed_source} = unsafeMkA (makeDeltaAst pm_parsed_source) 0
#else
fixAnns GHC.ParsedModule {..} =
let ranns = relativiseApiAnns pm_parsed_source pm_annotations
in unsafeMkA pm_parsed_source ranns 0
#endif
parseSpecs
:: IdeState
-> NormalizedFilePath
-> AnnotatedModule
-> FixityEnv
-> [RewriteSpec]
-> IO [Rewrite Universe]
parseSpecs state origin originParsedModule originFixities specs = do
#if MIN_VERSION_ghc(9,2,0)
-- retrie needs the libdir for `parseRewriteSpecs`
libdir <- topDir . ms_hspp_opts . msrModSummary <$> useOrFail state "Retrie.GetModSummary" (CallRetrieInternalError "file not found") GetModSummary origin
#endif
parseRewriteSpecs
#if MIN_VERSION_ghc(9,2,0)
libdir
#endif
(\_f -> return $ NoCPP originParsedModule)
originFixities
specs
constructfromFunMatches imps fun_id fun_matches = do
let fName = occNameFS (GHC.occName (unLoc fun_id))
fe <- mkLocatedHsVar fun_id
rewrites <- concat <$>
forM (unLoc $ GHC.mg_alts fun_matches) (matchToRewrites fe imps LeftToRight)
let urewrites = toURewrite <$> rewrites
-- traceShowM $ map showQuery urewrites
assert (not $ null urewrites) $
return urewrites
showQuery = ppRewrite
-- showQuery :: Rewrite (LHsExpr GhcPs) -> String
-- showQuery q = unlines
-- [ "template: " <> show (hash (printOutputable . showAstData NoBlankSrcSpan . astA . tTemplate . fst . qResult $ q))
-- , "quantifiers: " <> show (hash (T.pack (show(Ext.toList $ qQuantifiers q))))
-- , "matcher: " <> show (hash (printOutputable . showAstData NoBlankSrcSpan . astA . qPattern $ q))
-- ]
s :: Data a => a -> String
s = T.unpack . printOutputable . showAstData NoBlankSrcSpan
#if MIN_VERSION_ghc(9,2,0)
NoBlankEpAnnotations
#endif
constructInlineFromIdentifer originParsedModule originSpan = do
-- traceM $ s $ astA originParsedModule
fmap astA $ transformA originParsedModule $ \(L _ m) -> do
let ast = everything (<>) (First Nothing `mkQ` matcher) m
matcher :: HsBindLR GhcPs GhcPs -> First _
matcher FunBind{fun_id, fun_matches}
-- | trace (show (GHC.getLocA fun_id) <> ": " <> s fun_id) False = undefined
| RealSrcSpan sp _ <- GHC.getLocA fun_id
, sp == originSpan =
First $ Just (fun_id, fun_matches)
matcher _ = First Nothing
case ast of
First (Just (fun_id, fun_matches))
->
let imports = mempty in
constructfromFunMatches imports fun_id fun_matches
_ -> return $ error "cound not find source code to inline"
asEditMap :: [(Uri, TextEdit)] -> WorkspaceEditMap
asEditMap = coerce . HM.fromListWith (++) . map (second pure)
asTextEdits :: Change -> [(Uri, TextEdit)]
asTextEdits NoChange = []
asTextEdits (Change reps _imports) =
[ (filePathToUri spanLoc, edit)
| Replacement {..} <- nubOrdOn (realSpan . replLocation) reps,
(RealSrcSpan rspan _) <- [replLocation],
let spanLoc = unpackFS $ srcSpanFile rspan,
let edit = TextEdit (realSrcSpanToRange rspan) (T.pack replReplacement)
]
-------------------------------------------------------------------------------
-- Rule wrappers
_useRuleBlocking,
_useRuleStale,
useRule ::
(IdeRule k v) =>
String ->
IdeState ->
k ->
NormalizedFilePath ->
IO (Maybe (RuleResult k))
_useRuleBlocking label state rule f = runAction label state (use rule f)
_useRuleStale label state rule f =
fmap fst
<$> runIdeAction label (shakeExtras state) (useWithStaleFast rule f)
-- | Chosen approach for calling ghcide Shake rules
useRule label = _useRuleStale ("Retrie." <> label)
-------------------------------------------------------------------------------
-- Serialization wrappers and instances
deriving instance Eq RewriteSpec
deriving instance Show RewriteSpec
deriving instance Generic RewriteSpec
deriving instance FromJSON RewriteSpec
deriving instance ToJSON RewriteSpec
newtype IE name
= IEVar name
deriving (Eq, Show, Generic, FromJSON, ToJSON)
data ImportSpec = AddImport
{ ideclNameString :: String,
ideclSource :: Bool,
ideclQualifiedBool :: Bool,
ideclAsString :: Maybe String,
ideclThing :: Maybe (IE String)
}
deriving (Eq, Show, Generic, FromJSON, ToJSON)
toImportDecl :: ImportSpec -> GHC.ImportDecl GHC.GhcPs
toImportDecl AddImport {..} = GHC.ImportDecl {ideclSource = ideclSource', ..}
where
ideclSource' = if ideclSource then IsBoot else NotBoot
toMod = noLocA . GHC.mkModuleName
ideclName = toMod ideclNameString
#if MIN_VERSION_ghc(9,3,0)
ideclPkgQual = NoRawPkgQual
#else
ideclPkgQual = Nothing
#endif
ideclSafe = False
ideclImplicit = False
ideclHiding = Nothing
ideclSourceSrc = NoSourceText
#if MIN_VERSION_ghc(9,5,0)
ideclExt = GHCGHC.XImportDeclPass
{ ideclAnn = GHCGHC.EpAnnNotUsed
, ideclSourceText = ideclSourceSrc
, ideclImplicit = ideclImplicit
}
#elif MIN_VERSION_ghc(9,2,0)
ideclExt = GHCGHC.EpAnnNotUsed
#else
ideclExt = GHC.noExtField
#endif
ideclAs = toMod <$> ideclAsString
ideclQualified = if ideclQualifiedBool then GHC.QualifiedPre else GHC.NotQualified
reuseParsedModule state f = do
pm <- useOrFail state "Retrie.GetParsedModule" NoParse GetParsedModule f
(fixities, pm') <- fixFixities state f (fixAnns pm)
return (fixities, pm')
getCPPmodule state session t = do
nt <- toNormalizedFilePath' <$> makeAbsolute t
let getParsedModule f contents = do
modSummary <- msrModSummary <$>
useOrFail state "Retrie.GetModSummary" (CallRetrieInternalError "file not found") GetModSummary nt
let ms' =
modSummary
{ ms_hspp_buf =
Just (stringToStringBuffer contents)
}
logPriority (ideLogger state) Info $ T.pack $ "Parsing module: " <> t
parsed <- evalGhcEnv session (GHCGHC.parseModule ms')
`catch` \e -> throwIO (GHCParseError nt (show @SomeException e))
(fixities, parsed) <- fixFixities state f (fixAnns parsed)
return (fixities, parsed)
contents <- do
(_, mbContentsVFS) <-
runAction "Retrie.GetFileContents" state $ getFileContents nt
case mbContentsVFS of
Just contents -> return contents
Nothing -> T.decodeUtf8 <$> BS.readFile (fromNormalizedFilePath nt)
if any (T.isPrefixOf "#if" . T.toLower) (T.lines contents)
then do
fixitiesRef <- newIORef mempty
let parseModule x = do
(fix, res) <- getParsedModule nt x
atomicModifyIORef'_ fixitiesRef (fix <>)
return res
res <- parseCPP parseModule contents
fixities <- readIORef fixitiesRef
return (fixities, res)
else do
(fixities, pm) <- reuseParsedModule state nt
return (fixities, NoCPP pm)