packages feed

haskell-language-server-2.15.0.0: plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs

{-# LANGUAGE CPP             #-}
{-# LANGUAGE GADTs           #-}
{-# LANGUAGE LambdaCase      #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns    #-}

module Ide.Plugin.Class.CodeAction (
    addMethodPlaceholders,
    codeAction,
) where

import           Control.Arrow                    ((>>>))
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)
import           Control.Monad.Trans.Maybe
import           Data.Aeson                       hiding (Null)
import           Data.List
import           Data.List.Extra                  (nubOrdOn)
import           Data.Maybe                       (listToMaybe, mapMaybe)
import qualified Data.Text                        as T
import           Development.IDE
import           Development.IDE.Core.FileStore   (getVersionedTextDoc)
import           Development.IDE.Core.PluginUtils
import           Development.IDE.GHC.Compat
import           Development.IDE.GHC.Compat.Error (TcRnMessage (..),
                                                   _TcRnMessage,
                                                   msgEnvelopeErrorL,
                                                   stripTcRnMessageContext)
import           Development.IDE.GHC.Compat.Util
import           Ide.Plugin.Class.ExactPrint
import           Ide.Plugin.Class.Types
import           Ide.Plugin.Class.Utils
import qualified Ide.Plugin.Config
import           Ide.Plugin.Error
import qualified Ide.Plugin.RangeMap              as RangeMap
import           Ide.PluginUtils
import           Ide.Types
import qualified Language.LSP.Protocol.Lens       as L
import           Language.LSP.Protocol.Message
import           Language.LSP.Protocol.Types

addMethodPlaceholders :: PluginId -> CommandFunction IdeState AddMinimalMethodsParams
addMethodPlaceholders _ state _ param@AddMinimalMethodsParams{..} = do
    caps <- lift pluginGetClientCapabilities
    nfp <- getNormalizedFilePathE (verTxtDocId ^. L.uri)
    pm <- runActionE "classplugin.addMethodPlaceholders.GetParsedModule" state
        $ useE GetParsedModule nfp
    (hsc_dflags . hscEnv -> df) <- runActionE "classplugin.addMethodPlaceholders.GhcSessionDeps" state
        $ useE GhcSessionDeps nfp
    (old, new) <- handleMaybeM (PluginInternalError "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 $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ())

    pure $ InR Null
    where
        toTextDocumentEdit edit =
            TextDocumentEdit (verTxtDocId ^.re _versionedTextDocumentIdentifier) [InL edit]

        mergeEdit :: WorkspaceEdit -> [TextEdit] -> WorkspaceEdit
        mergeEdit WorkspaceEdit{..} edits = WorkspaceEdit
            { _documentChanges =
                (\x -> x ++ map (InL . toTextDocumentEdit) edits)
                    <$> _documentChanges
            , ..
            }

        workspaceEdit caps old new
            = diffText caps (verTxtDocId, 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 Method_TextDocumentCodeAction
codeAction recorder state plId (CodeActionParams _ _ docId caRange _) = do
    verTxtDocId <- liftIO $ runAction "classplugin.codeAction.getVersionedTextDoc" state $ getVersionedTextDoc docId
    nfp <- getNormalizedFilePathE (verTxtDocId ^. L.uri)
    activeDiagnosticsInRange (shakeExtras state) nfp caRange
        >>= \fileDiags -> do
            actions <- join <$> mapM (mkActions nfp verTxtDocId) (methodDiags fileDiags)
            pure $ InL actions
    where
        methodDiags fileDiags =
            mapMaybe (\d -> (d,) <$> isClassMethodWarning (d ^. fdStructuredMessageL)) fileDiags

        mkActions
            :: NormalizedFilePath
            -> VersionedTextDocumentIdentifier
            -> (FileDiagnostic, ClassMinimalDef)
            -> ExceptT PluginError (HandlerM Ide.Plugin.Config.Config) [Command |? CodeAction]
        mkActions docPath verTxtDocId (diag, classMinDef) = do
            ClassInstancesResult instMap <- runActionE "classplugin.codeAction.GetClassInstances" state
                $ useE GetClassInstances docPath
            inst <- handleMaybe (PluginInvalidUserState "no instance at diagnostic range")
                $ listToMaybe (RangeMap.filterByRange range instMap)
            (tmrTypechecked -> gblEnv) <- runActionE "classplugin.codeAction.TypeCheck" state $ useE TypeCheck docPath
            (hscEnv -> hsc) <- runActionE "classplugin.codeAction.GhcSession" state $ useE GhcSession docPath
            logWith recorder Debug (LogImplementedMethods (hsc_dflags hsc) (instClass inst) classMinDef)
            pure
                $ concatMap mkAction
                $ nubOrdOn snd
                $ filter ((/=) mempty . snd)
                $ mkMethodGroups hsc gblEnv inst classMinDef
            where
                range = diag ^. fdLspDiagnosticL . L.range

                mkMethodGroups :: HscEnv -> TcGblEnv -> InstanceInfo -> ClassMinimalDef -> [MethodGroup]
                mkMethodGroups hsc gblEnv inst classMinDef = minimalDef <> [allClassMethods]
                    where
                        methods    = instMethods inst
                        minimalDef = minDefToMethodGroups hsc gblEnv methods classMinDef
                        allClassMethods =
                            ("all missing methods", map (makeMethodDefinition hsc gblEnv) methods)

                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 :: [(T.Text, T.Text)] -> Bool -> [Value]
                mkCmdParams methodGroup withSig =
                    [toJSON (AddMinimalMethodsParams verTxtDocId range methodGroup withSig)]

                mkCodeAction title cmd
                    = InR
                    $ CodeAction
                        title
                        (Just CodeActionKind_QuickFix)
                        (Just [])
                        Nothing
                        Nothing
                        Nothing
                        (Just cmd)
                        Nothing

isClassMethodWarning :: StructuredMessage -> Maybe ClassMinimalDef
isClassMethodWarning message = case message ^? _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnMessage of
    Nothing          -> Nothing
    Just tcRnMessage -> isUnsatisfiedMinimalDefWarning tcRnMessage

isUnsatisfiedMinimalDefWarning :: TcRnMessage -> Maybe ClassMinimalDef
isUnsatisfiedMinimalDefWarning = stripTcRnMessageContext >>> \case
    TcRnUnsatisfiedMinimalDef classMinDef -> Just classMinDef
    _ -> Nothing

type MethodSignature = T.Text
type MethodName = T.Text
type MethodDefinition = (MethodName, MethodSignature)
type MethodGroup = (T.Text, [MethodDefinition])

makeMethodDefinition :: HscEnv -> TcGblEnv -> (Name, Type) -> MethodDefinition
makeMethodDefinition hsc gblEnv (name, ty) = (nameTxt, signature)
    where
        -- nameTxt is bare (no parens); ExactPrint.makeMethodDecl applies
        -- toMethodName to wrap operators when emitting the placeholder.
        nameTxt   = printOutputable name
        signature = toMethodName nameTxt <> " :: " <> T.pack (showDoc hsc gblEnv ty)

minDefToMethodGroups :: HscEnv -> TcGblEnv -> [(Name, Type)] -> ClassMinimalDef -> [MethodGroup]
minDefToMethodGroups hsc gblEnv methods minDef = makeMethodGroup <$> go minDef
    where
        makeMethodGroup methodDefinitions =
            let name = mconcat $ intersperse "," $ (\x -> "'" <> x <> "'") . fst <$> methodDefinitions
            in  (name, methodDefinitions)

        matchMethod n =
            map (makeMethodDefinition hsc gblEnv)
                $ filter ((== n) . fst) methods
#if __GLASGOW_HASKELL__ >= 913
        go (Var lmn)  = pure $ matchMethod (unLoc lmn)
#else
        go (Var mn)   = pure $ matchMethod mn
#endif
        go (Or ms)    = concatMap (go . unLoc) ms
        go (And ms)   = foldr (liftA2 (<>) . go . unLoc) [[]] ms
        go (Parens m) = go (unLoc m)