{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} module Ide.Plugin.Class ( descriptor ) where import BooleanFormula import Class import ConLike import Control.Applicative import Control.Lens hiding (List, use) import Control.Monad import Control.Monad.Trans.Class import Control.Monad.Trans.Maybe import Data.Aeson import Data.Char import qualified Data.HashMap.Strict as H import Data.List import qualified Data.Map.Strict as Map import Data.Maybe import qualified Data.Text as T import Development.IDE import Development.IDE.Core.PositionMapping (fromCurrentRange, toCurrentRange) import Development.IDE.GHC.Compat hiding (getLoc) import Development.IDE.Spans.AtPoint import qualified GHC.Generics as Generics import GhcPlugins hiding (Var, getLoc, (<>)) import Ide.PluginUtils import Ide.Types import Language.Haskell.GHC.ExactPrint import Language.Haskell.GHC.ExactPrint.Parsers (parseDecl) import Language.Haskell.GHC.ExactPrint.Types hiding (GhcPs, Parens) import Language.Haskell.LSP.Core import Language.Haskell.LSP.Types import qualified Language.Haskell.LSP.Types.Lens as J import SrcLoc import TcEnv import TcRnMonad descriptor :: PluginId -> PluginDescriptor IdeState descriptor plId = (defaultPluginDescriptor plId) { pluginCommands = commands , pluginCodeActionProvider = Just codeAction } commands :: [PluginCommand IdeState] commands = [ PluginCommand "addMinimalMethodPlaceholders" "add placeholders for minimal methods" addMethodPlaceholders ] -- | Parameter for the addMethods PluginCommand. data AddMinimalMethodsParams = AddMinimalMethodsParams { uri :: Uri , range :: Range , methodGroup :: List T.Text } deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) addMethodPlaceholders :: CommandFunction IdeState AddMinimalMethodsParams addMethodPlaceholders lf state AddMinimalMethodsParams{..} = fmap (fromMaybe errorResult) . runMaybeT $ do docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri pm <- MaybeT . runAction "classplugin" state $ use GetParsedModule docPath let ps = pm_parsed_source pm anns = relativiseApiAnns ps (pm_annotations pm) old = T.pack $ exactPrint ps anns (hsc_dflags . hscEnv -> df) <- MaybeT . runAction "classplugin" state $ use GhcSessionDeps docPath List (unzip -> (mAnns, mDecls)) <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup let (ps', (anns', _), _) = runTransform (mergeAnns (mergeAnnList mAnns) anns) (addMethodDecls ps mDecls) new = T.pack $ exactPrint ps' anns' pure (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams (workspaceEdit caps old new))) where errorResult = (Right Null, Nothing) caps = clientCapabilities lf indent = 2 makeMethodDecl df mName = case parseDecl df (T.unpack mName) . T.unpack $ toMethodName mName <> " = _" of Right (ann, d) -> Just (setPrecedingLines d 1 indent ann, d) Left _ -> Nothing addMethodDecls :: ParsedSource -> [LHsDecl GhcPs] -> Transform (Located (HsModule GhcPs)) addMethodDecls ps mDecls = do d <- findInstDecl ps newSpan <- uniqueSrcSpanT let annKey = mkAnnKey d newAnnKey = AnnKey newSpan (CN "HsValBinds") addWhere mkds@(Map.lookup annKey -> Just ann) = Map.insert newAnnKey ann2 mkds2 where ann1 = ann { annsDP = annsDP ann ++ [(G AnnWhere, DP (0, 1))] , annCapturedSpan = Just newAnnKey , annSortKey = Just (fmap getLoc mDecls) } mkds2 = Map.insert annKey ann1 mkds ann2 = annNone { annEntryDelta = DP (1, indent) } addWhere _ = panic "Ide.Plugin.Class.addMethodPlaceholder" modifyAnnsT addWhere modifyAnnsT (captureOrderAnnKey newAnnKey mDecls) foldM (insertAfter d) ps (reverse mDecls) findInstDecl :: ParsedSource -> Transform (LHsDecl GhcPs) findInstDecl ps = head . filter (containRange range . getLoc) <$> hsDecls ps workspaceEdit caps old new = diffText caps (uri, old) new IncludeDeletions toMethodName n | Just (h, _) <- T.uncons n , not (isAlpha h || h == '_') = "(" <> n <> ")" | otherwise = n -- | -- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is -- sensitive to the format of diagnostic messages from GHC. codeAction :: CodeActionProvider IdeState codeAction _ state plId docId _ context = fmap (fromMaybe errorResult) . runMaybeT $ do docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri actions <- join <$> mapM (mkActions docPath) methodDiags pure . Right . List $ actions where errorResult = Right (List []) uri = docId ^. J.uri List diags = context ^. J.diagnostics ghcDiags = filter (\d -> d ^. J.source == Just "typecheck") diags methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message)) ghcDiags mkActions docPath diag = do ident <- findClassIdentifier docPath range cls <- findClassFromIdentifier docPath ident lift . traverse mkAction . minDefToMethodGroups . classMinimalDef $ cls where range = diag ^. J.range mkAction methodGroup = mkCodeAction title <$> mkLspCommand plId "addMinimalMethodPlaceholders" title (Just cmdParams) where title = mkTitle methodGroup cmdParams = mkCmdParams methodGroup mkTitle methodGroup = "Add placeholders for " <> mconcat (intersperse ", " (fmap (\m -> "'" <> m <> "'") methodGroup)) mkCmdParams methodGroup = [toJSON (AddMinimalMethodsParams uri range (List methodGroup))] mkCodeAction title = CACodeAction . CodeAction title (Just CodeActionQuickFix) (Just (List [])) Nothing . Just findClassIdentifier docPath range = do (hieAst -> hf, pmap) <- MaybeT . runAction "classplugin" state $ useWithStale GetHieAst docPath pure $ head . head $ pointCommand hf (fromJust (fromCurrentRange pmap range) ^. J.start & J.character -~ 1) ( (Map.keys . Map.filter isClassNodeIdentifier . nodeIdentifiers . nodeInfo) <=< nodeChildren ) findClassFromIdentifier docPath (Right name) = do (hscEnv -> hscenv, _) <- MaybeT . runAction "classplugin" state $ useWithStale GhcSessionDeps docPath (tmrTypechecked -> thisMod, _) <- MaybeT . runAction "classplugin" state $ useWithStale TypeCheck docPath MaybeT . fmap snd . initTcWithGbl hscenv thisMod ghostSpan $ do tcthing <- tcLookup name case tcthing of AGlobal (AConLike (RealDataCon con)) | Just cls <- tyConClass_maybe (dataConOrigTyCon con) -> pure cls _ -> panic "Ide.Plugin.Class.findClassFromIdentifier" findClassFromIdentifier _ (Left _) = panic "Ide.Plugin.Class.findClassIdentifier" ghostSpan :: RealSrcSpan ghostSpan = realSrcLocSpan $ mkRealSrcLoc (fsLit "") 1 1 containRange :: Range -> SrcSpan -> Bool containRange range x = isInsideSrcSpan (range ^. J.start) x || isInsideSrcSpan (range ^. J.end) x isClassNodeIdentifier :: IdentifierDetails a -> Bool isClassNodeIdentifier = isNothing . identType isClassMethodWarning :: T.Text -> Bool isClassMethodWarning = T.isPrefixOf "• No explicit implementation for" minDefToMethodGroups :: BooleanFormula Name -> [[T.Text]] minDefToMethodGroups = go where go (Var mn) = [[T.pack . occNameString . occName $ mn]] go (Or ms) = concatMap (go . unLoc) ms go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms) go (Parens m) = go (unLoc m)