ghcide-0.7.3.0: src/Development/IDE/Plugin/CodeAction/ExactPrint.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module Development.IDE.Plugin.CodeAction.ExactPrint
( Rewrite (..),
rewriteToEdit,
-- * Utilities
appendConstraint,
extendImport,
)
where
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Data.Data (Data)
import Data.Functor
import qualified Data.HashMap.Strict as HMap
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust)
import qualified Data.Text as T
import Development.IDE.GHC.Compat hiding (parseExpr)
import Development.IDE.GHC.ExactPrint
import Development.IDE.Types.Location
import GhcPlugins (realSrcSpanEnd, realSrcSpanStart, sigPrec)
import Language.Haskell.GHC.ExactPrint
import Language.Haskell.GHC.ExactPrint.Types (DeltaPos (DP), KeywordId (G), mkAnnKey)
import Language.Haskell.LSP.Types
import OccName
import Outputable (ppr, showSDocUnsafe)
------------------------------------------------------------------------------
-- | Construct a 'Rewrite', replacing the node at the given 'SrcSpan' with the
-- given 'ast'.
data Rewrite where
Rewrite ::
Annotate ast =>
-- | The 'SrcSpan' that we want to rewrite
SrcSpan ->
-- | The ast that we want to graft
(DynFlags -> TransformT (Either String) (Located ast)) ->
Rewrite
------------------------------------------------------------------------------
-- | Convert a 'Rewrite' into a 'WorkspaceEdit'.
rewriteToEdit ::
DynFlags ->
Uri ->
Anns ->
Rewrite ->
Either String WorkspaceEdit
rewriteToEdit dflags uri anns (Rewrite dst f) = do
(ast, (anns, _), _) <- runTransformT anns $ f dflags
let editMap =
HMap.fromList
[ ( uri,
List
[ TextEdit (fromJust $ srcSpanToRange dst) $
T.pack $ tail $ exactPrint ast anns
]
)
]
pure $ WorkspaceEdit (Just editMap) Nothing
srcSpanToRange :: SrcSpan -> Maybe Range
srcSpanToRange (UnhelpfulSpan _) = Nothing
srcSpanToRange (RealSrcSpan real) = Just $ realSrcSpanToRange real
realSrcSpanToRange :: RealSrcSpan -> Range
realSrcSpanToRange real =
Range
(realSrcLocToPosition $ realSrcSpanStart real)
(realSrcLocToPosition $ realSrcSpanEnd real)
realSrcLocToPosition :: RealSrcLoc -> Position
realSrcLocToPosition real =
Position (srcLocLine real - 1) (srcLocCol real - 1)
------------------------------------------------------------------------------
-- | Fix the parentheses around a type context
fixParens ::
(Monad m, Data (HsType pass)) =>
Maybe DeltaPos ->
Maybe DeltaPos ->
LHsContext pass ->
TransformT m [LHsType pass]
fixParens openDP closeDP ctxt@(L _ elems) = do
-- Paren annotation for type contexts are usually quite screwed up
-- we remove duplicates and fix negative DPs
modifyAnnsT $
Map.adjust
( \x ->
let annsMap = Map.fromList (annsDP x)
in x
{ annsDP =
Map.toList $
Map.alter (\_ -> openDP <|> Just dp00) (G AnnOpenP) $
Map.alter (\_ -> closeDP <|> Just dp00) (G AnnCloseP) $
annsMap <> parens
}
)
(mkAnnKey ctxt)
return $ map dropHsParTy elems
where
parens = Map.fromList [(G AnnOpenP, dp00), (G AnnCloseP, dp00)]
dropHsParTy :: LHsType pass -> LHsType pass
dropHsParTy (L _ (HsParTy _ ty)) = ty
dropHsParTy other = other
-- | Append a constraint at the end of a type context.
-- If no context is present, a new one will be created.
appendConstraint ::
-- | The new constraint to append
String ->
-- | The type signature where the constraint is to be inserted, also assuming annotated
LHsType GhcPs ->
Rewrite
appendConstraint constraintT = go
where
go (L l it@HsQualTy {hst_ctxt = L l' ctxt}) = Rewrite l $ \df -> do
constraint <- liftParseAST df constraintT
setEntryDPT constraint (DP (0, 1))
-- Paren annotations are usually attached to the first and last constraints,
-- rather than to the constraint list itself, so to preserve them we need to reposition them
closeParenDP <- lookupAnn (G AnnCloseP) `mapM` lastMaybe ctxt
openParenDP <- lookupAnn (G AnnOpenP) `mapM` headMaybe ctxt
ctxt' <- fixParens (join openParenDP) (join closeParenDP) (L l' ctxt)
addTrailingCommaT (last ctxt')
return $ L l $ it {hst_ctxt = L l' $ ctxt' ++ [constraint]}
go (L _ HsForAllTy {hst_body}) = go hst_body
go (L _ (HsParTy _ ty)) = go ty
go (L l other) = Rewrite l $ \df -> do
-- there isn't a context, so we must create one
constraint <- liftParseAST df constraintT
lContext <- uniqueSrcSpanT
lTop <- uniqueSrcSpanT
let context = L lContext [constraint]
addSimpleAnnT context (DP (0, 1)) $
[ (G AnnDarrow, DP (0, 1))
]
++ concat
[ [ (G AnnOpenP, dp00),
(G AnnCloseP, dp00)
]
| hsTypeNeedsParens sigPrec $ unLoc constraint
]
return $ L lTop $ HsQualTy noExtField context (L l other)
liftParseAST :: ASTElement ast => DynFlags -> String -> TransformT (Either String) (Located ast)
liftParseAST df s = case parseAST df "" s of
Right (anns, x) -> modifyAnnsT (anns <>) $> x
Left _ -> lift $ Left $ "No parse: " <> s
lookupAnn :: (Data a, Monad m) => KeywordId -> Located a -> TransformT m (Maybe DeltaPos)
lookupAnn comment la = do
anns <- getAnnsT
return $ Map.lookup (mkAnnKey la) anns >>= lookup comment . annsDP
dp00 :: DeltaPos
dp00 = DP (0, 0)
headMaybe :: [a] -> Maybe a
headMaybe [] = Nothing
headMaybe (a : _) = Just a
lastMaybe :: [a] -> Maybe a
lastMaybe [] = Nothing
lastMaybe other = Just $ last other
liftMaybe :: String -> Maybe a -> TransformT (Either String) a
liftMaybe _ (Just x) = return x
liftMaybe s _ = lift $ Left s
-- | Copy anns attached to a into b with modification, then delete anns of a
transferAnn :: (Data a, Data b) => Located a -> Located b -> (Annotation -> Annotation) -> TransformT (Either String) ()
transferAnn la lb f = do
anns <- getAnnsT
let oldKey = mkAnnKey la
newKey = mkAnnKey lb
oldValue <- liftMaybe "Unable to find ann" $ Map.lookup oldKey anns
putAnnsT $ Map.delete oldKey $ Map.insert newKey (f oldValue) anns
------------------------------------------------------------------------------
extendImport :: Maybe String -> String -> LImportDecl GhcPs -> Rewrite
extendImport mparent identifier lDecl@(L l _) =
Rewrite l $ \df -> do
case mparent of
Just parent -> extendImportViaParent df parent identifier lDecl
_ -> extendImportTopLevel df identifier lDecl
-- | Add an identifier to import list
--
-- extendImportTopLevel "foo" AST:
--
-- import A --> Error
-- import A (bar) --> import A (bar, foo)
extendImportTopLevel :: DynFlags -> String -> LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs)
extendImportTopLevel df idnetifier (L l it@ImportDecl {..})
| Just (hide, L l' lies) <- ideclHiding,
hasSibling <- not $ null lies = do
src <- uniqueSrcSpanT
top <- uniqueSrcSpanT
rdr <- liftParseAST df idnetifier
let lie = L src $ IEName rdr
x = L top $ IEVar noExtField lie
when hasSibling $
addTrailingCommaT (last lies)
addSimpleAnnT x (DP (0, if hasSibling then 1 else 0)) []
addSimpleAnnT rdr dp00 $ unqalDP $ hasParen idnetifier
-- Parens are attachted to `lies`, so if `lies` was empty previously,
-- we need change the ann key from `[]` to `:` to keep parens and other anns.
unless hasSibling $
transferAnn (L l' lies) (L l' [x]) id
return $ L l it {ideclHiding = Just (hide, L l' $ lies ++ [x])}
extendImportTopLevel _ _ _ = lift $ Left "Unable to extend the import list"
-- | Add an identifier with its parent to import list
--
-- extendImportViaParent "Bar" "Cons" AST:
--
-- import A --> Error
-- import A () --> import A (Bar(Cons))
-- import A (Foo, Bar) --> import A (Foo, Bar(Cons))
-- import A (Foo, Bar()) --> import A (Foo, Bar(Cons))
extendImportViaParent :: DynFlags -> String -> String -> LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs)
extendImportViaParent df parent child (L l it@ImportDecl {..})
| Just (hide, L l' lies) <- ideclHiding = go hide l' [] lies
where
go :: Bool -> SrcSpan -> [LIE GhcPs] -> [LIE GhcPs] -> TransformT (Either String) (LImportDecl GhcPs)
go hide l' pre (lAbs@(L ll' (IEThingAbs _ absIE@(L _ ie))) : xs)
-- ThingAbs ie => ThingWith ie child
| parent == unIEWrappedName ie = do
srcChild <- uniqueSrcSpanT
childRdr <- liftParseAST df child
let childLIE = L srcChild $ IEName childRdr
x :: LIE GhcPs = L ll' $ IEThingWith noExtField absIE NoIEWildcard [childLIE] []
-- take anns from ThingAbs, and attatch parens to it
transferAnn lAbs x $ \old -> old {annsDP = annsDP old ++ [(G AnnOpenP, DP (0, 1)), (G AnnCloseP, dp00)]}
addSimpleAnnT childRdr dp00 [(G AnnVal, dp00)]
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [x] ++ xs)}
go hide l' pre ((L l'' (IEThingWith _ twIE@(L _ ie) _ lies' _)) : xs)
-- ThingWith ie lies' => ThingWith ie (lies' ++ [child])
| parent == unIEWrappedName ie,
hasSibling <- not $ null lies' =
do
srcChild <- uniqueSrcSpanT
childRdr <- liftParseAST df child
when hasSibling $
addTrailingCommaT (last lies')
let childLIE = L srcChild $ IEName childRdr
addSimpleAnnT childRdr (DP (0, if hasSibling then 1 else 0)) $ unqalDP $ hasParen child
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [L l'' (IEThingWith noExtField twIE NoIEWildcard (lies' ++ [childLIE]) [])] ++ xs)}
go hide l' pre (x : xs) = go hide l' (x : pre) xs
go hide l' pre []
| hasSibling <- not $ null pre = do
-- [] => ThingWith parent [child]
l'' <- uniqueSrcSpanT
srcParent <- uniqueSrcSpanT
srcChild <- uniqueSrcSpanT
parentRdr <- liftParseAST df parent
childRdr <- liftParseAST df child
when hasSibling $
addTrailingCommaT (head pre)
let parentLIE = L srcParent $ IEName parentRdr
childLIE = L srcChild $ IEName childRdr
x :: LIE GhcPs = L l'' $ IEThingWith noExtField parentLIE NoIEWildcard [childLIE] []
addSimpleAnnT parentRdr (DP (0, if hasSibling then 1 else 0)) $ unqalDP $ hasParen parent
addSimpleAnnT childRdr (DP (0, 0)) $ unqalDP $ hasParen child
addSimpleAnnT x (DP (0, 0)) [(G AnnOpenP, DP (0, 1)), (G AnnCloseP, DP (0, 0))]
-- Parens are attachted to `pre`, so if `pre` was empty previously,
-- we need change the ann key from `[]` to `:` to keep parens and other anns.
unless hasSibling $
transferAnn (L l' $ reverse pre) (L l' [x]) id
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [x])}
extendImportViaParent _ _ _ _ = lift $ Left "Unable to extend the import list via parent"
unIEWrappedName :: IEWrappedName (IdP GhcPs) -> String
unIEWrappedName (occName -> occ) = showSDocUnsafe $ parenSymOcc occ (ppr occ)
hasParen :: String -> Bool
hasParen ('(' : _) = True
hasParen _ = False
unqalDP :: Bool -> [(KeywordId, DeltaPos)]
unqalDP paren =
( if paren
then \x -> (G AnnOpenP, dp00) : x : [(G AnnCloseP, dp00)]
else pure
)
(G AnnVal, dp00)