hls-class-plugin-1.1.0.0: 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.Either.Extra (rights)
import Data.List
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
toTextDocunemtEdit edit =
TextDocumentEdit (VersionedTextDocumentIdentifier uri (Just 0)) (List [InL edit])
mergeEdit :: WorkspaceEdit -> [TextEdit] -> WorkspaceEdit
mergeEdit WorkspaceEdit{..} edits = WorkspaceEdit
{ _documentChanges =
(\(List x) -> List $ x ++ map (InL . toTextDocunemtEdit) 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
$ fmap (filter (\(bind, _) -> bind `notElem` implemented))
$ minDefToMethodGroups range sigs
$ classMinimalDef cls
where
range = diag ^. J.range
mkAction :: [(T.Text, T.Text)] -> [Command |? CodeAction]
mkAction methodGroup
= [ mkCodeAction title
$ mkLspCommand plId codeActionCommandId title
(Just $ mkCmdParams methodGroup False)
, mkCodeAction titleWithSig
$ mkLspCommand plId codeActionCommandId titleWithSig
(Just $ mkCmdParams methodGroup True)
]
where
title = mkTitle $ fst <$> methodGroup
titleWithSig = mkTitleWithSig $ fst <$> methodGroup
mkTitle methodGroup
= "Add placeholders for "
<> mconcat (intersperse ", " (fmap (\m -> "'" <> m <> "'") methodGroup))
mkTitleWithSig methodGroup = mkTitle methodGroup <> " 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 "Error in 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
-- Return (name text, signature text)
minDefToMethodGroups :: Range -> [InstanceBindTypeSig] -> BooleanFormula Name -> [[(T.Text, T.Text)]]
minDefToMethodGroups range sigs = go
where
go (Var mn) = [[ (T.pack . occNameString . occName $ mn, bindRendered sig)
| sig <- sigs
, inRange range (getSrcSpan $ bindName sig)
, printOutputable mn == T.drop (T.length bindingPrefix) (printOutputable (bindName sig))
]]
go (Or ms) = concatMap (go . unLoc) ms
go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms)
go (Parens m) = go (unLoc m)