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

Jump to instance definition and explain typeclass evidence #4392

Open
wants to merge 4 commits 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
12 changes: 11 additions & 1 deletion ghcide/src/Development/IDE/Core/Actions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Development.IDE.Core.Actions
( getAtPoint
, getDefinition
, getTypeDefinition
, getImplementationDefinition
, highlightAtPoint
, refsAtPoint
, workspaceSymbols
Expand Down Expand Up @@ -98,7 +99,7 @@ getDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [(Location,
getDefinition file pos = runMaybeT $ do
ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask
opts <- liftIO $ getIdeOptionsIO ide
(HAR _ hf _ _ _, mapping) <- useWithStaleFastMT GetHieAst file
(hf, mapping) <- useWithStaleFastMT GetHieAst file
(ImportMap imports, _) <- useWithStaleFastMT GetImportMap file
!pos' <- MaybeT (pure $ fromCurrentPosition mapping pos)
locationsWithIdentifier <- AtPoint.gotoDefinition withHieDb (lookupMod hiedbWriter) opts imports hf pos'
Expand All @@ -120,6 +121,15 @@ getTypeDefinition file pos = runMaybeT $ do
pure $ Just (fixedLocation, identifier)
) locationsWithIdentifier

getImplementationDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location])
getImplementationDefinition file pos = runMaybeT $ do
ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask
opts <- liftIO $ getIdeOptionsIO ide
(hf, mapping) <- useWithStaleFastMT GetHieAst file
!pos' <- MaybeT (pure $ fromCurrentPosition mapping pos)
locs <- AtPoint.gotoImplementation withHieDb (lookupMod hiedbWriter) opts hf pos'
traverse (MaybeT . toCurrentLocation mapping file) locs

highlightAtPoint :: NormalizedFilePath -> Position -> IdeAction (Maybe [DocumentHighlight])
highlightAtPoint file pos = runMaybeT $ do
(HAR _ hf rf _ _,mapping) <- useWithStaleFastMT GetHieAst file
Expand Down
7 changes: 5 additions & 2 deletions ghcide/src/Development/IDE/LSP/HoverDefinition.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Development.IDE.LSP.HoverDefinition
, foundHover
, gotoDefinition
, gotoTypeDefinition
, gotoImplementation
, documentHighlight
, references
, wsSymbols
Expand Down Expand Up @@ -47,9 +48,11 @@ instance Pretty Log where
gotoDefinition :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentDefinition)
hover :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (Hover |? Null)
gotoTypeDefinition :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentTypeDefinition)
gotoImplementation :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentImplementation)
documentHighlight :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) ([DocumentHighlight] |? Null)
gotoDefinition = request "Definition" getDefinition (InR $ InR Null) (InL . Definition. InR . map fst)
gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InR Null) (InL . Definition. InR . map fst)
gotoDefinition = request "Definition" getDefinition (InR $ InR Null) (InL . Definition . InR . map fst)
gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InR Null) (InL . Definition . InR . map fst)
gotoImplementation = request "Implementation" getImplementationDefinition (InR $ InR Null) (InL . Definition . InR)
hover = request "Hover" getAtPoint (InR Null) foundHover
documentHighlight = request "DocumentHighlight" highlightAtPoint (InR Null) InL

Expand Down
2 changes: 2 additions & 0 deletions ghcide/src/Development/IDE/Plugin/HLS/GhcIde.hs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ descriptor recorder plId = (defaultPluginDescriptor plId desc)
Hover.gotoDefinition recorder ide TextDocumentPositionParams{..})
<> mkPluginHandler SMethod_TextDocumentTypeDefinition (\ide _ TypeDefinitionParams{..} ->
Hover.gotoTypeDefinition recorder ide TextDocumentPositionParams{..})
<> mkPluginHandler SMethod_TextDocumentImplementation (\ide _ ImplementationParams{..} ->
Hover.gotoImplementation recorder ide TextDocumentPositionParams{..})
<> mkPluginHandler SMethod_TextDocumentDocumentHighlight (\ide _ DocumentHighlightParams{..} ->
Hover.documentHighlight recorder ide TextDocumentPositionParams{..})
<> mkPluginHandler SMethod_TextDocumentReferences (Hover.references recorder)
Expand Down
140 changes: 108 additions & 32 deletions ghcide/src/Development/IDE/Spans/AtPoint.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Development.IDE.Spans.AtPoint (
atPoint
, gotoDefinition
, gotoTypeDefinition
, gotoImplementation
, documentHighlight
, pointCommand
, referencesAtPoint
Expand All @@ -23,6 +24,10 @@ module Development.IDE.Spans.AtPoint (
, LookupModule
) where


import GHC.Data.FastString (lengthFS)
import qualified GHC.Utils.Outputable as O

import Development.IDE.GHC.Error
import Development.IDE.GHC.Orphans ()
import Development.IDE.Types.Location
Expand Down Expand Up @@ -52,9 +57,13 @@ import qualified Data.Text as T

import qualified Data.Array as A
import Data.Either
import Data.List (isSuffixOf)
import Data.List.Extra (dropEnd1, nubOrd)


import Data.Either.Extra (eitherToMaybe)
import Data.List (isSuffixOf, sortOn)
import Data.Tree
import qualified Data.Tree as T
import Data.Version (showVersion)
import Development.IDE.Types.Shake (WithHieDb)
import HieDb hiding (pointCommand,
Expand Down Expand Up @@ -171,14 +180,18 @@ documentHighlight hf rf pos = pure highlights
highlights = do
n <- ns
ref <- fromMaybe [] (M.lookup (Right n) rf)
pure $ makeHighlight ref
makeHighlight (sp,dets) =
DocumentHighlight (realSrcSpanToRange sp) (Just $ highlightType $ identInfo dets)
maybeToList (makeHighlight n ref)
makeHighlight n (sp,dets)
| isTvNameSpace (nameNameSpace n) && isBadSpan n sp = Nothing
| otherwise = Just $ DocumentHighlight (realSrcSpanToRange sp) (Just $ highlightType $ identInfo dets)
highlightType s =
if any (isJust . getScopeFromContext) s
then DocumentHighlightKind_Write
else DocumentHighlightKind_Read

isBadSpan :: Name -> RealSrcSpan -> Bool
isBadSpan n sp = srcSpanStartLine sp /= srcSpanEndLine sp || (srcSpanEndCol sp - srcSpanStartCol sp > lengthFS (occNameFS $ nameOccName n))

-- | Locate the type definition of the name at a given position.
gotoTypeDefinition
:: MonadIO m
Expand All @@ -198,12 +211,25 @@ gotoDefinition
-> LookupModule m
-> IdeOptions
-> M.Map ModuleName NormalizedFilePath
-> HieASTs a
-> HieAstResult
-> Position
-> MaybeT m [(Location, Identifier)]
gotoDefinition withHieDb getHieFile ideOpts imports srcSpans pos
= lift $ locationsAtPoint withHieDb getHieFile ideOpts imports pos srcSpans

-- | Locate the implementation definition of the name at a given position.
-- Goto Implementation for an overloaded function.
gotoImplementation
:: MonadIO m
=> WithHieDb
-> LookupModule m
-> IdeOptions
-> HieAstResult
-> Position
-> MaybeT m [Location]
gotoImplementation withHieDb getHieFile ideOpts srcSpans pos
= lift $ instanceLocationsAtPoint withHieDb getHieFile ideOpts pos srcSpans

-- | Synopsis for the name at a given position.
atPoint
:: IdeOptions
Expand All @@ -212,13 +238,13 @@ atPoint
-> HscEnv
-> Position
-> IO (Maybe (Maybe Range, [T.Text]))
atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env pos =
atPoint IdeOptions{} (HAR _ (hf :: HieASTs a) rf _ (kind :: HieKind hietype)) (DKMap dm km) env pos =
listToMaybe <$> sequence (pointCommand hf pos hoverInfo)
where
-- Hover info for values/data
hoverInfo :: HieAST hietype -> IO (Maybe Range, [T.Text])
hoverInfo ast = do
prettyNames <- mapM prettyName filteredNames
prettyNames <- mapM prettyName names
pure (Just range, prettyNames ++ pTypes)
where
pTypes :: [T.Text]
Expand All @@ -235,24 +261,20 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env
info :: NodeInfo hietype
info = nodeInfoH kind ast

-- We want evidence variables to be displayed last.
-- Evidence trees contain information of secondary relevance.
names :: [(Identifier, IdentifierDetails hietype)]
names = M.assocs $ nodeIdentifiers info

-- Check for evidence bindings
isInternal :: (Identifier, IdentifierDetails a) -> Bool
isInternal (Right _, dets) =
any isEvidenceContext $ identInfo dets
isInternal (Left _, _) = False

filteredNames :: [(Identifier, IdentifierDetails hietype)]
filteredNames = filter (not . isInternal) names
names = sortOn (any isEvidenceUse . identInfo . snd) $ M.assocs $ nodeIdentifiers info

prettyName :: (Either ModuleName Name, IdentifierDetails hietype) -> IO T.Text
prettyName (Right n, dets) = pure $ T.unlines $
wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind))
: maybeToList (pretty (definedAt n) (prettyPackageName n))
++ catMaybes [ T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n
]
prettyName (Right n, dets)
-- We want to print evidence variable using a readable tree structure.
| any isEvidenceUse (identInfo dets) = pure $ maybe "" (printOutputable . renderEvidenceTree) (getEvidenceTree rf n) <> "\n"
Copy link
Collaborator

Choose a reason for hiding this comment

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

the maybe "" seems supicious. What case is that for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The getEvidenceTree may return Nothing for a variety of reasons. For example, when the name is unexpectedly not found in the reference map. I think in practice, it will always produce something.

We could log it if we failed to build the EvidenceTree for a given name.

| otherwise = pure $ T.unlines $
wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind))
: maybeToList (pretty (definedAt n) (prettyPackageName n))
++ catMaybes [ T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n
]
where maybeKind = fmap printOutputable $ safeTyThingType =<< lookupNameEnv km n
pretty Nothing Nothing = Nothing
pretty (Just define) Nothing = Just $ define <> "\n"
Expand Down Expand Up @@ -286,7 +308,7 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env
version = T.pack $ showVersion (unitPackageVersion conf)
pure $ pkgName <> "-" <> version

-- Type info for the current node, it may contains several symbols
-- Type info for the current node, it may contain several symbols
-- for one range, like wildcard
types :: [hietype]
types = nodeType info
Expand All @@ -295,9 +317,12 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env
prettyTypes = map (("_ :: "<>) . prettyType) types

prettyType :: hietype -> T.Text
prettyType t = case kind of
HieFresh -> printOutputable t
HieFromDisk full_file -> printOutputable $ hieTypeToIface $ recoverFullType t (hie_types full_file)
prettyType = printOutputable . expandType

expandType :: a -> SDoc
expandType t = case kind of
HieFresh -> ppr t
HieFromDisk full_file -> ppr $ hieTypeToIface $ recoverFullType t (hie_types full_file)

definedAt :: Name -> Maybe T.Text
definedAt name =
Expand All @@ -307,6 +332,39 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env
UnhelpfulLoc {} | isInternalName name || isSystemName name -> Nothing
_ -> Just $ "*Defined " <> printOutputable (pprNameDefnLoc name) <> "*"

-- We want to render the root constraint even if it is a let,
-- but we don't want to render any subsequent lets
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm assuming this makes sense to someone who knows how evidence is represented in GHC...

renderEvidenceTree :: Tree (EvidenceInfo a) -> SDoc
-- However, if the root constraint is simply an indirection (via let) to a single other constraint,
-- we can still skip rendering it
renderEvidenceTree (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_)}) [x])
= renderEvidenceTree x
renderEvidenceTree (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_), ..}) xs)
= hang (text "Evidence of constraint `" O.<> expandType evidenceType O.<> "`") 2 $
vcat $ text "constructed using:" : map renderEvidenceTree' xs
renderEvidenceTree (T.Node (EvidenceInfo{..}) _)
= hang (text "Evidence of constraint `" O.<> expandType evidenceType O.<> "`") 2 $
vcat $ printDets evidenceSpan evidenceDetails : map (text . T.unpack) (maybeToList $ definedAt evidenceVar)

-- renderEvidenceTree' skips let bound evidence variables and prints the children directly
renderEvidenceTree' (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_)}) xs)
= vcat (map renderEvidenceTree' xs)
renderEvidenceTree' (T.Node (EvidenceInfo{..}) _)
= hang (text "- `" O.<> expandType evidenceType O.<> "`") 2 $
vcat $ printDets evidenceSpan evidenceDetails : map (text . T.unpack) (maybeToList $ definedAt evidenceVar)

printDets :: RealSrcSpan -> Maybe (EvVarSource, Scope, Maybe Span) -> SDoc
printDets _ Nothing = text "using an external instance"
printDets ospn (Just (src,_,mspn)) = pprSrc
$$ text "at" <+> ppr spn
where
-- Use the bind span if we have one, else use the occurrence span
spn = fromMaybe ospn mspn
pprSrc = case src of
-- Users don't know what HsWrappers are
EvWrapperBind -> "bound by type signature or pattern"
_ -> ppr src

-- | Find 'Location's of type definition at a specific point and return them along with their 'Identifier's.
typeLocationsAtPoint
:: forall m
Expand All @@ -323,7 +381,7 @@ typeLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _ _ hieKi
let arr = hie_types hf
ts = concat $ pointCommand ast pos getts
unfold = map (arr A.!)
getts x = nodeType ni ++ (mapMaybe identType $ M.elems $ nodeIdentifiers ni)
getts x = nodeType ni ++ mapMaybe identType (M.elems $ nodeIdentifiers ni)
where ni = nodeInfo' x
getTypes' ts' = flip concatMap (unfold ts') $ \case
HTyVarTy n -> [n]
Expand All @@ -337,7 +395,7 @@ typeLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _ _ hieKi
in fmap nubOrd $ concatMapM (\n -> fmap (maybe [] (fmap (,Right n))) (nameToLocation withHieDb lookupModule n)) (getTypes' ts)
HieFresh ->
let ts = concat $ pointCommand ast pos getts
getts x = nodeType ni ++ (mapMaybe identType $ M.elems $ nodeIdentifiers ni)
getts x = nodeType ni ++ mapMaybe identType (M.elems $ nodeIdentifiers ni)
where ni = nodeInfo x
in fmap nubOrd $ concatMapM (\n -> fmap (maybe [] (fmap (,Right n))) (nameToLocation withHieDb lookupModule n)) (getTypes ts)

Expand All @@ -352,20 +410,20 @@ namesInType (LitTy _) = []
namesInType _ = []

getTypes :: [Type] -> [Name]
getTypes ts = concatMap namesInType ts
getTypes = concatMap namesInType

-- | Find 'Location's of definition at a specific point and return them along with their 'Identifier's.
locationsAtPoint
:: forall m a
:: forall m
. MonadIO m
=> WithHieDb
-> LookupModule m
-> IdeOptions
-> M.Map ModuleName NormalizedFilePath
-> Position
-> HieASTs a
-> HieAstResult
-> m [(Location, Identifier)]
locationsAtPoint withHieDb lookupModule _ideOptions imports pos ast =
locationsAtPoint withHieDb lookupModule _ideOptions imports pos (HAR _ ast _rm _ _) =
let ns = concat $ pointCommand ast pos (M.keys . getNodeIds)
zeroPos = Position 0 0
zeroRange = Range zeroPos zeroPos
Expand All @@ -375,6 +433,24 @@ locationsAtPoint withHieDb lookupModule _ideOptions imports pos ast =
(\n -> fmap (fmap $ fmap (,Right n)) (nameToLocation withHieDb lookupModule n)))
ns

-- | Find 'Location's of a implementation definition at a specific point.
instanceLocationsAtPoint
:: forall m
. MonadIO m
=> WithHieDb
-> LookupModule m
-> IdeOptions
-> Position
-> HieAstResult
-> m [Location]
instanceLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _rm _ _) =
let ns = concat $ pointCommand ast pos (M.keys . getNodeIds)
evTrees = mapMaybe (eitherToMaybe >=> getEvidenceTree _rm) ns
evNs = concatMap (map (evidenceVar) . T.flatten) evTrees
in fmap (nubOrd . concat) $ mapMaybeM
(nameToLocation withHieDb lookupModule)
evNs

-- | Given a 'Name' attempt to find the location where it is defined.
nameToLocation :: MonadIO m => WithHieDb -> LookupModule m -> Name -> m (Maybe [Location])
nameToLocation withHieDb lookupModule name = runMaybeT $
Expand Down
25 changes: 25 additions & 0 deletions ghcide/test/data/hover/GotoImplementation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{-# LANGUAGE GADTs #-}

module GotoImplementation where

data AAA = AAA
instance Num AAA where
aaa :: Num x => x
aaa = 1
aaa1 :: AAA = aaa

class BBB a where
bbb :: a -> a
instance BBB AAA where
bbb = const AAA
bbbb :: AAA
bbbb = bbb AAA

data DDD a where
DDD1 :: Int -> DDD Int
DDD2 :: String -> DDD String
ddd :: DDD a -> a
ddd d = case d of
DDD1 a -> a + a
DDD2 a -> a ++ a

2 changes: 1 addition & 1 deletion ghcide/test/data/hover/hie.yaml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cradle: {direct: {arguments: ["Foo", "Bar", "GotoHover", "RecordDotSyntax"]}}
cradle: {direct: {arguments: ["Foo", "Bar", "GotoHover", "RecordDotSyntax", "GotoImplementation"]}}
3 changes: 3 additions & 0 deletions ghcide/test/exe/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ data Expect
| ExpectHoverTextRegex T.Text -- the hover message must match this pattern
| ExpectExternFail -- definition lookup in other file expected to fail
| ExpectNoDefinitions
| ExpectNoImplementations
| ExpectNoHover
-- | ExpectExtern -- TODO: as above, but expected to succeed: need some more info in here, once we have some working examples
deriving Eq
Expand All @@ -134,6 +135,8 @@ checkDefs (defToLocation -> defs) mkExpectations = traverse_ check =<< mkExpecta
canonActualLoc <- canonicalizeLocation def
canonExpectedLoc <- canonicalizeLocation expectedLocation
canonActualLoc @?= canonExpectedLoc
check ExpectNoImplementations = do
liftIO $ assertBool "Expecting no implementations" $ null defs
check ExpectNoDefinitions = do
liftIO $ assertBool "Expecting no definitions" $ null defs
check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file"
Expand Down
Loading
Loading