Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

class-plugin: Add code action for implementing instance method when default implementation doesn't work #3267

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

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)
Expand Down Expand Up @@ -89,7 +88,7 @@ codeAction recorder state plId (CodeActionParams _ _ docId _ context) = pluginRe
List diags = context ^. J.diagnostics

ghcDiags = filter (\d -> d ^. J.source == Just "typecheck") diags
methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message)) ghcDiags
methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message) || isInstanceMissingError (d ^. J.message)) ghcDiags

mkActions
:: NormalizedFilePath
Expand All @@ -111,11 +110,17 @@ codeAction recorder state plId (CodeActionParams _ _ docId _ context) = pluginRe
$ use GetInstanceBindTypeSigs docPath
implemented <- findImplementedMethods ast instancePosition
logWith recorder Info (LogImplementedMethods cls implemented)
let groups =
-- If the mininal def contains nothing, the plugin offers to
-- add stub implementations of all methods with default
-- implementations
case classMinimalDef cls of
And [] -> classOpItemsGroups range sigs cls
Or [] -> classOpItemsGroups range sigs cls
minDef -> minDefToMethodGroups range sigs minDef
pure
$ concatMap mkAction
$ fmap (filter (\(bind, _) -> bind `notElem` implemented))
$ minDefToMethodGroups range sigs
$ classMinimalDef cls
$ fmap (filter (\(bind, _) -> bind `notElem` implemented)) groups
where
range = diag ^. J.range

Expand Down Expand Up @@ -207,6 +212,11 @@ isClassNodeIdentifier ident = (isNothing . identType) ident && Use `Set.member`
isClassMethodWarning :: T.Text -> Bool
isClassMethodWarning = T.isPrefixOf "• No explicit implementation for"

isInstanceMissingError :: T.Text -> Bool
isInstanceMissingError err =
"• No instance for" `T.isPrefixOf` err
&& "In the instance declaration for" `T.isInfixOf` err

isInstanceValBind :: ContextInfo -> Bool
isInstanceValBind (ValBind InstanceBind _ _) = True
isInstanceValBind _ = False
Expand All @@ -215,11 +225,19 @@ isInstanceValBind _ = False
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 (Var mn) = mkGroups range sigs mn
go (Or ms) = concatMap (go . unLoc) ms
go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms)
go (Parens m) = go (unLoc m)

mkGroups :: Range -> [InstanceBindTypeSig] -> Name -> [[(T.Text, T.Text)]]
mkGroups range sigs name =
[[ (T.pack . occNameString . occName $ name, bindRendered sig)
| sig <- sigs
, inRange range (getSrcSpan $ bindName sig)
, printOutputable name == T.drop (T.length bindingPrefix) (printOutputable (bindName sig))
]]

classOpItemsGroups :: Range -> [InstanceBindTypeSig] -> Class -> [[(T.Text, T.Text)]]
classOpItemsGroups range sigs cls =
concatMap (mkGroups range sigs . getName . fst) $ classOpItems cls
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not just ones with default impl, but all methods, aren't they?

5 changes: 2 additions & 3 deletions plugins/hls-class-plugin/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ module Main
import Control.Lens (Prism', prism', (^.), (^..),
(^?))
import Control.Monad (void)
import Data.Aeson (toJSON, (.=))
import Data.Functor.Contravariant (contramap)
import Data.Maybe
import Development.IDE.Types.Logger
import qualified Ide.Plugin.Class as Class
import Ide.Plugin.Config (PluginConfig (plcConfig))
import qualified Ide.Plugin.Config as Plugin
import qualified Language.LSP.Types.Lens as J
import System.FilePath
import Test.Hls
Expand Down Expand Up @@ -78,6 +75,8 @@ codeActionTests recorder = testGroup
executeCodeAction eqWithSig
, goldenWithClass recorder "Only insert pragma once" "InsertPragmaOnce" "" $ \(_:multi:_) -> do
executeCodeAction multi
, goldenWithClass recorder "Creates a placeholder for a default method implementation" "DefaultImplementation" "" $ \(_:fAction:_) -> do
executeCodeAction fAction
]

codeLensTests :: Recorder (WithPriority Class.Log) -> TestTree
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE InstanceSigs #-}
module DefaultImplementation where

class Test1 a where

class Test a where
f :: a -> a
default f :: Test1 a => a -> a
f = id

data X = X | Y

instance Test X where
f :: X -> X
f = _
13 changes: 13 additions & 0 deletions plugins/hls-class-plugin/test/testdata/DefaultImplementation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{-# LANGUAGE DefaultSignatures #-}
module DefaultImplementation where

class Test1 a where

class Test a where
f :: a -> a
default f :: Test1 a => a -> a
f = id

data X = X | Y

instance Test X where