hls-class-plugin-1.1.1.1: src/Ide/Plugin/Class/CodeAction.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Ide.Plugin.Class.CodeAction where
import Control.Applicative (liftA2)
import Control.Lens hiding (List, use)
import Control.Monad.Extra
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT, throwE)
import Control.Monad.Trans.Maybe
import Data.Aeson
import Data.Bifunctor (second)
import Data.Either.Extra (rights)
import Data.List
import Data.List.Extra (nubOrdOn)
import qualified Data.Map.Strict as Map
import Data.Maybe (isNothing, listToMaybe,
mapMaybe)
import qualified Data.Set as Set
import qualified Data.Text as T
import Development.IDE
import Development.IDE.Core.PositionMapping (fromCurrentRange)
import Development.IDE.GHC.Compat
import Development.IDE.GHC.Compat.Util
import Development.IDE.Spans.AtPoint (pointCommand)
import GHC.LanguageExtensions.Type
import Ide.Plugin.Class.ExactPrint
import Ide.Plugin.Class.Types
import Ide.Plugin.Class.Utils
import qualified Ide.Plugin.Config
import Ide.PluginUtils
import Ide.Types
import Language.LSP.Server
import Language.LSP.Types
import qualified Language.LSP.Types.Lens as J
addMethodPlaceholders :: PluginId -> CommandFunction IdeState AddMinimalMethodsParams
addMethodPlaceholders _ state param@AddMinimalMethodsParams{..} = do
caps <- getClientCapabilities
pluginResponse $ do
nfp <- getNormalizedFilePath uri
pm <- handleMaybeM "Unable to GetParsedModule"
$ liftIO
$ runAction "classplugin.addMethodPlaceholders.GetParsedModule" state
$ use GetParsedModule nfp
(hsc_dflags . hscEnv -> df) <- handleMaybeM "Unable to GhcSessionDeps"
$ liftIO
$ runAction "classplugin.addMethodPlaceholders.GhcSessionDeps" state
$ use GhcSessionDeps nfp
(old, new) <- handleMaybeM "Unable to makeEditText"
$ liftIO $ runMaybeT
$ makeEditText pm df param
pragmaInsertion <- insertPragmaIfNotPresent state nfp InstanceSigs
let edit =
if withSig
then mergeEdit (workspaceEdit caps old new) pragmaInsertion
else workspaceEdit caps old new
void $ lift $ sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ())
pure Null
where
toTextDocumentEdit edit =
TextDocumentEdit (VersionedTextDocumentIdentifier uri (Just 0)) (List [InL edit])
mergeEdit :: WorkspaceEdit -> [TextEdit] -> WorkspaceEdit
mergeEdit WorkspaceEdit{..} edits = WorkspaceEdit
{ _documentChanges =
(\(List x) -> List $ x ++ map (InL . toTextDocumentEdit) edits)
<$> _documentChanges
, ..
}
workspaceEdit caps old new
= diffText caps (uri, old) new IncludeDeletions
-- |
-- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is
-- sensitive to the format of diagnostic messages from GHC.
codeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState TextDocumentCodeAction
codeAction recorder state plId (CodeActionParams _ _ docId _ context) = pluginResponse $ do
nfp <- getNormalizedFilePath uri
actions <- join <$> mapM (mkActions nfp) methodDiags
pure $ List actions
where
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
:: NormalizedFilePath
-> Diagnostic
-> ExceptT String (LspT Ide.Plugin.Config.Config IO) [Command |? CodeAction]
mkActions docPath diag = do
(HAR {hieAst = ast}, pmap) <- handleMaybeM "Unable to GetHieAst"
. liftIO
. runAction "classplugin.findClassIdentifier.GetHieAst" state
$ useWithStale GetHieAst docPath
instancePosition <- handleMaybe "No range" $
fromCurrentRange pmap range ^? _Just . J.start
& fmap (J.character -~ 1)
ident <- findClassIdentifier ast instancePosition
cls <- findClassFromIdentifier docPath ident
InstanceBindTypeSigsResult sigs <- handleMaybeM "Unable to GetInstanceBindTypeSigs"
$ liftIO
$ runAction "classplugin.codeAction.GetInstanceBindTypeSigs" state
$ use GetInstanceBindTypeSigs docPath
implemented <- findImplementedMethods ast instancePosition
logWith recorder Info (LogImplementedMethods cls implemented)
pure
$ concatMap mkAction
$ nubOrdOn snd
$ filter ((/=) mempty . snd)
$ fmap (second (filter (\(bind, _) -> bind `notElem` implemented)))
$ mkMethodGroups range sigs cls
where
range = diag ^. J.range
mkMethodGroups :: Range -> [InstanceBindTypeSig] -> Class -> [MethodGroup]
mkMethodGroups range sigs cls = minimalDef <> [allClassMethods]
where
minimalDef = minDefToMethodGroups range sigs $ classMinimalDef cls
allClassMethods = ("all missing methods", makeMethodDefinitions range sigs)
mkAction :: MethodGroup -> [Command |? CodeAction]
mkAction (name, methods)
= [ mkCodeAction title
$ mkLspCommand plId codeActionCommandId title
(Just $ mkCmdParams methods False)
, mkCodeAction titleWithSig
$ mkLspCommand plId codeActionCommandId titleWithSig
(Just $ mkCmdParams methods True)
]
where
title = "Add placeholders for " <> name
titleWithSig = title <> " with signature(s)"
mkCmdParams methodGroup withSig =
[toJSON (AddMinimalMethodsParams uri range (List methodGroup) withSig)]
mkCodeAction title cmd
= InR
$ CodeAction
title
(Just CodeActionQuickFix)
(Just (List []))
Nothing
Nothing
Nothing
(Just cmd)
Nothing
findClassIdentifier hf instancePosition =
handleMaybe "No Identifier found"
$ listToMaybe
$ mapMaybe listToMaybe
$ pointCommand hf instancePosition
( (Map.keys . Map.filter isClassNodeIdentifier . getNodeIds)
<=< nodeChildren
)
findImplementedMethods
:: HieASTs a
-> Position
-> ExceptT String (LspT Ide.Plugin.Config.Config IO) [T.Text]
findImplementedMethods asts instancePosition = do
pure
$ concat
$ pointCommand asts instancePosition
$ map (T.pack . getOccString) . rights . findInstanceValBindIdentifiers
-- | Recurses through the given AST to find identifiers which are
-- 'InstanceValBind's.
findInstanceValBindIdentifiers :: HieAST a -> [Identifier]
findInstanceValBindIdentifiers ast =
let valBindIds = Map.keys
. Map.filter (any isInstanceValBind . identInfo)
$ getNodeIds ast
in valBindIds <> concatMap findInstanceValBindIdentifiers (nodeChildren ast)
findClassFromIdentifier docPath (Right name) = do
(hscEnv -> hscenv, _) <- handleMaybeM "Unable to GhcSessionDeps"
. liftIO
. runAction "classplugin.findClassFromIdentifier.GhcSessionDeps" state
$ useWithStale GhcSessionDeps docPath
(tmrTypechecked -> thisMod, _) <- handleMaybeM "Unable to TypeCheck"
. liftIO
. runAction "classplugin.findClassFromIdentifier.TypeCheck" state
$ useWithStale TypeCheck docPath
handleMaybeM "TcEnv"
. liftIO
. 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
_ -> fail "Ide.Plugin.Class.findClassFromIdentifier"
findClassFromIdentifier _ (Left _) = throwE "Ide.Plugin.Class.findClassIdentifier"
isClassNodeIdentifier :: IdentifierDetails a -> Bool
isClassNodeIdentifier ident = (isNothing . identType) ident && Use `Set.member` identInfo ident
isClassMethodWarning :: T.Text -> Bool
isClassMethodWarning = T.isPrefixOf "• No explicit implementation for"
isInstanceValBind :: ContextInfo -> Bool
isInstanceValBind (ValBind InstanceBind _ _) = True
isInstanceValBind _ = False
type MethodSignature = T.Text
type MethodName = T.Text
type MethodDefinition = (MethodName, MethodSignature)
type MethodGroup = (T.Text, [MethodDefinition])
makeMethodDefinition :: InstanceBindTypeSig -> MethodDefinition
makeMethodDefinition sig = (name, signature)
where
name = T.drop (T.length bindingPrefix) (printOutputable (bindName sig))
signature = bindRendered sig
makeMethodDefinitions :: Range -> [InstanceBindTypeSig] -> [MethodDefinition]
makeMethodDefinitions range sigs =
[ makeMethodDefinition sig
| sig <- sigs
, inRange range (getSrcSpan $ bindName sig)
]
signatureToName :: InstanceBindTypeSig -> T.Text
signatureToName sig = T.drop (T.length bindingPrefix) (printOutputable (bindName sig))
-- Return [groupName text, [(methodName text, signature text)]]
minDefToMethodGroups :: Range -> [InstanceBindTypeSig] -> BooleanFormula Name -> [MethodGroup]
minDefToMethodGroups range sigs minDef = makeMethodGroup <$> go minDef
where
makeMethodGroup methodDefinitions =
let name = mconcat $ intersperse "," $ (\x -> "'" <> x <> "'") . fst <$> methodDefinitions
in (name, methodDefinitions)
go (Var mn) = pure $ makeMethodDefinitions range $ filter ((==) (printOutputable mn) . signatureToName) sigs
go (Or ms) = concatMap (go . unLoc) ms
go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms)
go (Parens m) = go (unLoc m)