Skip to content

Commit a809980

Browse files
committed
Add class plugin rules keyed by Name
1 parent f158128 commit a809980

5 files changed

Lines changed: 105 additions & 122 deletions

File tree

ghcide/src/Development/IDE/GHC/Compat/Core.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ module Development.IDE.GHC.Compat.Core (
334334

335335
module GHC.Tc.Instance.Family,
336336
module GHC.Tc.Module,
337+
module GHC.Tc.TyCl.Class,
337338
module GHC.Tc.Types,
338339
module GHC.Tc.Types.Evidence,
339340
module GHC.Tc.Utils.Env,
@@ -445,6 +446,7 @@ import GHC.Rename.Splice
445446
import qualified GHC.Runtime.Interpreter as GHCi
446447
import GHC.Tc.Instance.Family
447448
import GHC.Tc.Module
449+
import GHC.Tc.TyCl.Class
448450
import GHC.Tc.Types
449451
import GHC.Tc.Types.Evidence hiding ((<.>))
450452
import GHC.Tc.Utils.Env

plugins/hls-class-plugin/src/Ide/Plugin/Class.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Language.LSP.Protocol.Message
1010
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
1111
descriptor recorder plId = (defaultPluginDescriptor plId "Provides code actions and lenses for working with typeclasses")
1212
{ pluginCommands = commands plId
13-
, pluginRules = getInstanceBindTypeSigsRule recorder >> getInstanceBindLensRule recorder
13+
, pluginRules = getClassInstancesRule recorder >> getInstanceBindLensRule recorder
1414
, pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeAction recorder)
1515
<> mkPluginHandler SMethod_TextDocumentCodeLens codeLens
1616
<> mkResolveHandler SMethod_CodeLensResolve codeLensResolve

plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeLens.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ codeLensResolve state plId cl uri uniqueID = do
5050
(hscEnv -> hsc, _) <- runActionE "classplugin.codeAction.GhcSession" state $ useWithStaleE GhcSession nfp
5151
(range, name, typ) <- handleMaybe PluginStaleResolve
5252
$ IntMap.lookup uniqueID lensDetails
53-
let title = prettyBindingNameString (printOutputable name) <> " :: " <> T.pack (showDoc hsc gblEnv typ)
53+
let title = toMethodName (printOutputable name) <> " :: " <> T.pack (showDoc hsc gblEnv typ)
5454
edit <- handleMaybe (PluginInvalidUserState "toCurrentRange") $ makeEdit range title pm
5555
let command = mkLspCommand plId typeLensCommandId title (Just [toJSON $ InstanceBindLensCommand uri edit])
5656
pure $ cl & L.command ?~ command

plugins/hls-class-plugin/src/Ide/Plugin/Class/Types.hs

Lines changed: 99 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
module Ide.Plugin.Class.Types where
99

1010
import Control.DeepSeq (rwhnf)
11-
import Control.Monad.Extra (mapMaybeM, whenMaybe)
11+
import Control.Monad.Extra (mapMaybeM)
1212
import Control.Monad.IO.Class (liftIO)
13-
import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT))
13+
import Control.Monad.Trans.Maybe (runMaybeT)
1414
import Data.Aeson
1515
import qualified Data.IntMap as IntMap
16-
import Data.List.Extra (firstJust)
17-
import Data.Maybe (catMaybes, mapMaybe,
18-
maybeToList)
16+
import Data.Maybe (fromMaybe, listToMaybe,
17+
mapMaybe, maybeToList)
1918
import qualified Data.Text as T
2019
import Data.Unique (hashUnique, newUnique)
2120
import Development.IDE
@@ -26,6 +25,7 @@ import Development.IDE.GHC.Compat.Util (bagToList)
2625
import Development.IDE.Graph.Classes
2726
import GHC.Generics
2827
import Ide.Plugin.Class.Utils
28+
import qualified Ide.Plugin.RangeMap as RangeMap
2929
import Ide.Types
3030
import Language.LSP.Protocol.Types (TextEdit,
3131
VersionedTextDocumentIdentifier)
@@ -49,27 +49,33 @@ data AddMinimalMethodsParams = AddMinimalMethodsParams
4949
}
5050
deriving (Show, Eq, Generic, ToJSON, FromJSON)
5151

52-
-- |The InstanceBindTypeSigs Rule collects the instance bindings type
53-
-- signatures (both name and type). It is used by both the code actions and the
54-
-- code lenses
55-
data GetInstanceBindTypeSigs = GetInstanceBindTypeSigs
52+
-- | Indexes the instances declared in a module by their source span, giving
53+
-- each instance's class and the full instantiated type of every class method.
54+
-- Both the placeholder code action (to enumerate missing methods) and the
55+
-- code lens (to display inferred signatures) consume this rule.
56+
data GetClassInstances = GetClassInstances
5657
deriving (Generic, Show, Eq, Ord, Hashable, NFData)
5758

58-
data InstanceBindTypeSig = InstanceBindTypeSig
59-
{ bindName :: Name
60-
, bindType :: Type
59+
data InstanceInfo = InstanceInfo
60+
{ instSpan :: SrcSpan
61+
-- ^ Source span of the instance declaration.
62+
, instClass :: Class
63+
, instMethods :: [(Name, Type)]
64+
-- ^ Each class method paired with its type instantiated for this
65+
-- instance, including any instance context (e.g. @Eq a@ for
66+
-- @instance Eq a => C [a]@).
6167
}
6268

63-
newtype InstanceBindTypeSigsResult =
64-
InstanceBindTypeSigsResult [InstanceBindTypeSig]
69+
newtype ClassInstancesResult =
70+
ClassInstancesResult (RangeMap.RangeMap InstanceInfo)
6571

66-
instance Show InstanceBindTypeSigsResult where
67-
show _ = "<InstanceBindTypeSigs>"
72+
instance Show ClassInstancesResult where
73+
show _ = "<ClassInstances>"
6874

69-
instance NFData InstanceBindTypeSigsResult where
75+
instance NFData ClassInstancesResult where
7076
rnf = rwhnf
7177

72-
type instance RuleResult GetInstanceBindTypeSigs = InstanceBindTypeSigsResult
78+
type instance RuleResult GetClassInstances = ClassInstancesResult
7379

7480
-- |The necessary data to execute our code lens
7581
data InstanceBindLensCommand = InstanceBindLensCommand
@@ -80,11 +86,10 @@ data InstanceBindLensCommand = InstanceBindLensCommand
8086
, commandEdit :: TextEdit }
8187
deriving (Generic, FromJSON, ToJSON)
8288

83-
-- | The InstanceBindLens rule is specifically for code lenses. It relies on
84-
-- the InstanceBindTypeSigs rule, filters out irrelevant matches and signatures
85-
-- that can't be matched to a source span. It provides all the signatures linked
86-
-- to a unique ID to aid in resolving. It also provides a list of enabled
87-
-- extensions.
89+
-- | The InstanceBindLens rule is specifically for code lenses. It correlates
90+
-- user-written instance method bindings (those without an explicit signature)
91+
-- to the instance's 'InstanceInfo', and emits range/name/type triples with
92+
-- unique IDs for resolve.
8893
data GetInstanceBindLens = GetInstanceBindLens
8994
deriving (Generic, Show, Eq, Ord, Hashable, NFData)
9095

@@ -123,11 +128,14 @@ instance Pretty Log where
123128
<+> pretty (showSDoc dflags $ ppr methods)
124129
LogShake log -> pretty log
125130

131+
-- | A user-written instance binding without an explicit signature. The 'Name'
132+
-- is the renamer-level method name (e.g. @(==)@), used to look up the
133+
-- instance-level type in 'instMethods'.
126134
data BindInfo = BindInfo
127-
{ bindSpan :: SrcSpan
135+
{ bindSpan :: SrcSpan
128136
-- ^ SrcSpan of the whole binding
129-
, bindNameSpan :: SrcSpan
130-
-- ^ SrcSpan of the binding name
137+
, bindFunName :: Name
138+
-- ^ The renamed method name of the binding.
131139
}
132140

133141
getInstanceBindLensRule :: Recorder (WithPriority Log) -> Rules ()
@@ -138,35 +146,34 @@ getInstanceBindLensRule recorder = do
138146
#else
139147
tmr@(tmrRenamed -> (hs_tyclds -> tycls, _, _, _)) <- useMT TypeCheck nfp
140148
#endif
141-
(InstanceBindTypeSigsResult allBinds) <- useMT GetInstanceBindTypeSigs nfp
142-
143-
let -- declared instance methods without signatures
144-
bindInfos = [ bind
145-
| instds <- map group_instds tycls -- class instance decls
146-
, instd <- instds
147-
, inst <- maybeToList $ getClsInstD (unLoc instd)
148-
, bind <- getBindSpanWithoutSig inst
149-
]
150-
targetSigs = matchBind bindInfos allBinds
151-
rangeIntNameType <- liftIO $ mapMaybeM getRangeWithSig targetSigs
149+
ClassInstancesResult instMap <- useMT GetClassInstances nfp
150+
151+
let -- Correlate renamed ClsInstDecls with their InstanceInfo by source
152+
-- span (the only link between the renamed tree and tcg_insts), then
153+
-- collect user-written bindings that lack an explicit signature.
154+
entries =
155+
[ (bind, ty)
156+
| instds <- map group_instds tycls
157+
, instd <- instds
158+
, inst <- maybeToList $ getClsInstD (unLoc instd)
159+
, info <- maybeToList $ do
160+
instdRange <- srcSpanToRange (getLocA instd)
161+
listToMaybe (RangeMap.elementsInRange instdRange instMap)
162+
, bind <- getBindSpanWithoutSig inst
163+
, ty <- maybeToList (lookup (bindFunName bind) (instMethods info))
164+
]
165+
rangeIntNameType <- liftIO $ mapMaybeM tagEntry entries
152166
let lensRange = (\(range, int, _, _) -> (range, int)) <$> rangeIntNameType
153-
lensDetails = IntMap.fromList $ (\(range, int, name, typ) -> (int, (range, name, typ))) <$> rangeIntNameType
167+
lensDetails = IntMap.fromList $
168+
(\(range, int, name, typ) -> (int, (range, name, typ))) <$> rangeIntNameType
154169
lensEnabledExtensions = getExtensions $ tmrParsed tmr
155170
pure $ InstanceBindLensResult $ InstanceBindLens{..}
156171
where
157-
-- Match Binds with their signatures
158-
-- We try to give every `InstanceBindTypeSig` a `SrcSpan`,
159-
-- hence we can display signatures for `InstanceBindTypeSig` with span later.
160-
matchBind :: [BindInfo] -> [InstanceBindTypeSig] -> [Maybe (InstanceBindTypeSig, SrcSpan)]
161-
matchBind existedBinds allBindWithSigs =
162-
[firstJust (go bindSig) existedBinds | bindSig <- allBindWithSigs]
163-
where
164-
go :: InstanceBindTypeSig -> BindInfo -> Maybe (InstanceBindTypeSig, SrcSpan)
165-
go bindSig bind = do
166-
range <- (srcSpanToRange . bindNameSpan) bind
167-
if inRange range (getSrcSpan $ bindName bindSig)
168-
then Just (bindSig, bindSpan bind)
169-
else Nothing
172+
tagEntry (bind, ty) = case srcSpanToRange (bindSpan bind) of
173+
Nothing -> pure Nothing
174+
Just r -> do
175+
uniqueID <- hashUnique <$> newUnique
176+
pure $ Just (r, uniqueID, bindFunName bind, ty)
170177

171178
getClsInstD (ClsInstD _ d) = Just d
172179
getClsInstD _ = Nothing
@@ -183,52 +190,54 @@ getInstanceBindLensRule recorder = do
183190
cid_binds
184191
go (L l bind) = case bind of
185192
FunBind{..}
186-
-- `Generated` tagged for Template Haskell,
187-
-- here we filter out nonsense generated bindings
188-
-- that are nonsense for displaying code lenses.
189-
--
190-
-- See https://github.com/haskell/haskell-language-server/issues/3319
191-
| not $ isGenerated (groupOrigin fun_matches)
192-
-> Just $ L l fun_id
193-
_ -> Nothing
193+
-- `Generated` tagged for Template Haskell,
194+
-- here we filter out nonsense generated bindings
195+
-- that are nonsense for displaying code lenses.
196+
--
197+
-- See https://github.com/haskell/haskell-language-server/issues/3319
198+
| not $ isGenerated (groupOrigin fun_matches)
199+
-> Just $ L l fun_id
200+
_ -> Nothing
194201
-- Existed signatures' name
195-
sigNames = concat $ mapMaybe (\(L _ r) -> getSigName r) cid_sigs
196-
toBindInfo (L l (L l' _)) = BindInfo
197-
(locA l) -- bindSpan
198-
(locA l') -- bindNameSpan
199-
in toBindInfo <$> filter (\(L _ name) -> unLoc name `notElem` sigNames) bindNames
200-
201-
-- Get bind definition range with its rendered signature text
202-
getRangeWithSig :: Maybe (InstanceBindTypeSig, SrcSpan) -> IO (Maybe (Range, Int, Name, Type))
203-
getRangeWithSig (Just (bind, span)) = runMaybeT $ do
204-
range <- MaybeT . pure $ srcSpanToRange span
205-
uniqueID <- liftIO $ hashUnique <$> newUnique
206-
pure (range, uniqueID, bindName bind, bindType bind)
207-
getRangeWithSig Nothing = pure Nothing
208-
209-
210-
getInstanceBindTypeSigsRule :: Recorder (WithPriority Log) -> Rules ()
211-
getInstanceBindTypeSigsRule recorder = do
212-
defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \GetInstanceBindTypeSigs nfp -> runMaybeT $ do
213-
(tmrTypechecked -> gblEnv ) <- useMT TypeCheck nfp
202+
existingSigNames = concat $ mapMaybe (\(L _ r) -> getSigName r) cid_sigs
203+
toBindInfo (L l (L _ n)) = BindInfo (locA l) n
204+
in toBindInfo <$> filter (\(L _ name) -> unLoc name `notElem` existingSigNames) bindNames
205+
206+
getClassInstancesRule :: Recorder (WithPriority Log) -> Rules ()
207+
getClassInstancesRule recorder = do
208+
defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \GetClassInstances nfp -> runMaybeT $ do
209+
(tmrTypechecked -> gblEnv) <- useMT TypeCheck nfp
214210
(hscEnv -> hsc) <- useMT GhcSession nfp
215-
let binds = collectHsBindsBinders $ tcg_binds gblEnv
216-
(_, maybe [] catMaybes -> instanceBinds) <- liftIO $
211+
(_, mInfos) <- liftIO $
217212
initTcWithGbl hsc gblEnv ghostSpan
218213
#if MIN_VERSION_ghc(9,7,0)
219214
$ liftZonkM
220215
#endif
221-
$ traverse bindToSig binds
222-
pure $ InstanceBindTypeSigsResult instanceBinds
216+
$ mkInfos gblEnv
217+
pure $ ClassInstancesResult $ RangeMap.fromList'
218+
$ mapMaybe (\i -> (,i) <$> srcSpanToRange (instSpan i)) (fromMaybe [] mInfos)
223219
where
224-
bindToSig id = do
225-
let name = idName id
226-
whenMaybe (isBindingName name) $ do
227-
env <- tcInitTidyEnv
220+
mkInfos gblEnv = do
221+
env <- tcInitTidyEnv
222+
pure $ map (mkInfo env) (tcg_insts gblEnv)
223+
224+
mkInfo env inst = do
225+
-- forall tvs. theta => cls tys
226+
let (_tvs, theta, cls, tys) = instanceSig inst
227+
-- Canonicalise internal GHC type-variable names (e.g. a_1 -> a).
228+
tidy ty =
228229
#if MIN_VERSION_ghc(9,11,0)
229-
let ty =
230+
tidyOpenType env ty
230231
#else
231-
let (_, ty) =
232+
snd (tidyOpenType env ty)
232233
#endif
233-
tidyOpenType env (idType id)
234-
pure $ InstanceBindTypeSig name ty
234+
-- `instantiateMethod` substitutes the instance head types into
235+
-- the method's type and drops the leading class predicate, but
236+
-- not the instance's own constraints (`theta`). Re-prepend them
237+
-- so we get every method's full instantiated type.
238+
mkMeth m = (idName m, tidy (mkInvisFunTys theta (instantiateMethod cls m tys)))
239+
InstanceInfo
240+
{ instSpan = getSrcSpan (is_dfun inst)
241+
, instClass = cls
242+
, instMethods = map mkMeth (classMethods cls)
243+
}

plugins/hls-class-plugin/src/Ide/Plugin/Class/Utils.hs

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,25 @@ module Ide.Plugin.Class.Utils where
44

55
import Control.Monad.IO.Class (MonadIO, liftIO)
66
import Control.Monad.Trans.Except
7-
import Data.Char (isAlpha, isDigit)
8-
import Data.List (isPrefixOf)
9-
import Data.String (IsString)
7+
import Data.Char (isAlpha)
108
import qualified Data.Text as T
119
import Development.IDE
1210
import Development.IDE.Core.PluginUtils
1311
import Development.IDE.GHC.Compat
14-
import Development.IDE.GHC.Compat.Util
12+
import Development.IDE.GHC.Compat.Util (fsLit)
1513
import Development.IDE.Spans.Pragmas (getNextPragmaInfo,
1614
insertNewPragma)
1715
import Ide.Plugin.Error
1816
import Ide.PluginUtils
1917
import Language.LSP.Protocol.Types
2018

21-
-- | All instance bindings are started with `$c`
22-
bindingPrefix :: IsString s => s
23-
bindingPrefix = "$c"
24-
25-
-- | Superclasses generate bindings in typeclasses as well.
26-
--
27-
-- When determining which bindings to create placeholders for, these
28-
-- superclass-generated names need to be excluded.
29-
-- TODO: This function should be replaced by an equivalent one from GHC:
30-
-- https://gitlab.haskell.org/ghc/ghc/-/issues/27195
31-
isSuperClassesBindingPrefix :: String -> Bool
32-
isSuperClassesBindingPrefix ('$' : 'c' : 'p' : n : _) | isDigit n = True
33-
isSuperClassesBindingPrefix _ = False
34-
35-
isBindingName :: Name -> Bool
36-
isBindingName name =
37-
let bindingName = occNameString $ nameOccName name
38-
in isPrefixOf bindingPrefix bindingName && not (isSuperClassesBindingPrefix bindingName)
39-
4019
-- | Check if some `HasSrcSpan` value in the given range
4120
inRange :: Range -> SrcSpan -> Bool
4221
inRange range s = maybe False (subRange range) (srcSpanToRange s)
4322

4423
ghostSpan :: RealSrcSpan
4524
ghostSpan = realSrcLocSpan $ mkRealSrcLoc (fsLit "<haskell-language-sever>") 1 1
4625

47-
-- | "$cname" ==> "name"
48-
prettyBindingNameString :: T.Text -> T.Text
49-
prettyBindingNameString name
50-
| T.isPrefixOf bindingPrefix name =
51-
toMethodName $ T.drop (T.length bindingPrefix) name
52-
| otherwise = name
53-
5426
showDoc :: HscEnv -> TcGblEnv -> Type -> String
5527
showDoc hsc gblEnv ty = showSDocForUser' hsc (mkPrintUnqualifiedDefault hsc (rdrEnv gblEnv)) (pprSigmaType ty)
5628
where rdrEnv gblEnv = tcg_rdr_env gblEnv

0 commit comments

Comments
 (0)