Visual studio has this ability to show information about symbols when you hover over them, this feature is called “QuickInfo”
This essentially means that you can hover over a symbol like “fmap” and it would tell you, fmap :: forall a b (f :: * -> *). (Functor f) => (a -> b) -> f a -> f b and that it’s defined in GHC.Base
in ghci this would be equivalent to typing :i fmap which would result in the following output
class Functor f where
fmap :: (a -> b) -> f a -> f b
…
— Defined in GHC.Base
Whenever the user hovers over a symbol in visual studio, the IDE will call a method
public void AugmentQuickInfoSession(IQuickInfoSession session, IList<object> qiContent, out ITrackingSpan applicableToSpan)
I use the information given to me to construct two things
- The word the user is hovering on
- The exact location within the source file of that word
This information is used to find the correct Name value in the Haskell Renamed AST. The problem is we can’t construct name values, so we have to look them up. This is provided with the help of a typeclass
class Finder a where
findName :: MonadPlus m => a -> FastString -> Maybe SrcSpan -> m Name
The monad used determines how many results you receive. Use a Maybe monad and you’ll get just 1. use a List monad and you’ll get more than one, but only if you don’t specify a specific source span to look for (wildcard match on name alone).
However we should never enter the PostTcType types inside the renamed AST. These are invalid at this stage. Unfortunately SYB’s listify does not provide a way to tell it not to enter a specific type.
So we create a modified version of those SYB calls:
data Guard where
Guard :: Typeable a => Maybe a -> Guard
type HList = [Guard]— | Summarise all nodes in top-down, left-to-right order
everythingBut :: (r -> r -> r) -> HList -> GenericQ r -> GenericQ r
everythingBut k q f x
= foldl k (f x) fsp
where fsp = case isPost x q of
True -> []
False -> gmapQ (everythingBut k q f) xisPost :: Typeable a => a -> HList -> Bool
isPost a = or . map check
where check :: Guard -> Bool
check x = case x of
Guard y -> isJust $ (cast a) `asTypeOf` y— | Get a list of all entities that meet a predicate
listifyBut :: Typeable r => (r -> Bool) -> HList -> GenericQ [r]
listifyBut p q
= everythingBut (++) q ([] `mkQ` (\x -> if p x then [x] else []))
Now listify takes a HList of types not to inspect. HList is a Heterogeneous list, so it’ll allow things of different types inside it. Finding the Name is now as simple as:
instance Finder (HsGroup Name) where
findName grp a b = findName (listifyBut (isName a b) [Guard (undefined :: Maybe PostTcType)] grp) a b
once we have the names, we can just call getInfo. Nothing else is needed because remember that all API calls have a Context as argument, for instance the full type of the tooltip function is:
— @@ Export
— | Gather information about the identifier you requested
– .
– Context: The session for this call, Serves as a cache
– .
– String : The name of the identifier to lookup
– .
– SrcSpan: The location of the identifier in the sourcefile
– .
– Bool : Whether to treat this call as a strict one. If it’s strict
– Then the name AND span must match. If it’s not, Any match will do
– .
getTooltip :: Context -> String -> SrcSpan -> Bool -> IO (Maybe String)
This produces the following result
There’s a problem however, if you hover over a variable name that’s defined in the body of the function it produces a runtime panic:
If you think about it, this kind of makes sense, GHCi also won’t produce anything on local variables. In fact you can’t even refer to them. But we would at the least we would like to prevent this crash, and in the best case scenario we would like *some* information on the symbol.
After poking around some I noticed that the type of the identifiers that produce the errors are “Internal Name” values. the function nameModule then fails on these types. The plan now is, whenever we find a Internal Name, we look into the TypecheckedModule to find the Id associated with the Name value we retrieved earlier. with SYB this is again easy. However there’s a catch (thanks to nominolo for pointing this out): we should not enter any PostTcKind nor NameSet because these are blank after type checking.
findID :: Data a => a -> Name -> [Id]
findID a n = listifyBut ((n==) . getName) [Guard (undefined :: Maybe NameSet)
,Guard (undefined :: Maybe PostTcKind)] a
and that’s all. The end result is that this now works on local variables as well. Hovering over for instance the variable file generates
The important thing to note here is the Context , it’ll contain a cache of information. So looking up any of this stuff will be instantaneous. You just hover and directly get back information.
A last cool but *I’m not so sure how useful* function is that if you select something, then hover over it, it will type check only that expression.
so if you have an expression "fmap foo” somewhere but don’t remember what type foo or fmap is, just select them and hover over the selection. (although this is somewhat limited, all identifiers have to be top-level. It can’t return anything for local variables. sorry )
And that’s it for this post, I’ll continue the work on Cabal now, or continue this track and fully finish intellisense.
No video for this in action, since I have a cold