-
-
Notifications
You must be signed in to change notification settings - Fork 398
Implement signature help #4626
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
base: master
Are you sure you want to change the base?
Implement signature help #4626
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
{-# LANGUAGE DataKinds #-} | ||
{-# LANGUAGE GADTs #-} | ||
|
||
module Ide.Plugin.SignatureHelp (descriptor) where | ||
|
||
import Control.Arrow ((>>>)) | ||
import Data.Bifunctor (bimap) | ||
import qualified Data.Map.Strict as M | ||
import Data.Maybe (mapMaybe) | ||
import qualified Data.Set as S | ||
import Data.Text (Text) | ||
import qualified Data.Text as T | ||
import Development.IDE (GetHieAst (GetHieAst), | ||
HieAstResult (HAR, hieAst, hieKind), | ||
HieKind (..), | ||
IdeState (shakeExtras), | ||
Pretty (pretty), | ||
Recorder, WithPriority, | ||
printOutputable) | ||
import Development.IDE.Core.PluginUtils (runIdeActionE, | ||
useWithStaleFastE) | ||
import Development.IDE.Core.PositionMapping (fromCurrentPosition) | ||
import Development.IDE.GHC.Compat (ContextInfo (Use), | ||
FastStringCompat, HieAST, | ||
HieASTs, | ||
IdentifierDetails, Name, | ||
RealSrcSpan, SDoc, | ||
getAsts, | ||
getSourceNodeIds, | ||
hieTypeToIface, | ||
hie_types, identInfo, | ||
identType, | ||
isAnnotationInNodeInfo, | ||
mkRealSrcLoc, | ||
mkRealSrcSpan, | ||
nodeChildren, nodeSpan, | ||
ppr, recoverFullType, | ||
smallestContainingSatisfying, | ||
sourceNodeInfo) | ||
import Development.IDE.GHC.Compat.Util (LexicalFastString (LexicalFastString)) | ||
import GHC.Data.Maybe (rightToMaybe) | ||
import GHC.Types.SrcLoc (isRealSubspanOf) | ||
import Ide.Plugin.Error (getNormalizedFilePathE) | ||
import Ide.Types (PluginDescriptor (pluginHandlers), | ||
PluginId, | ||
PluginMethodHandler, | ||
defaultPluginDescriptor, | ||
mkPluginHandler) | ||
import Language.LSP.Protocol.Message (Method (Method_TextDocumentSignatureHelp), | ||
SMethod (SMethod_TextDocumentSignatureHelp)) | ||
import Language.LSP.Protocol.Types (Null (Null), | ||
ParameterInformation (ParameterInformation), | ||
Position (Position), | ||
SignatureHelp (SignatureHelp), | ||
SignatureHelpParams (SignatureHelpParams), | ||
SignatureInformation (SignatureInformation), | ||
TextDocumentIdentifier (TextDocumentIdentifier), | ||
UInt, | ||
type (|?) (InL, InR)) | ||
|
||
data Log = LogDummy | ||
|
||
instance Pretty Log where | ||
pretty = \case | ||
LogDummy -> "TODO(@linj) remove this dummy log" | ||
|
||
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState | ||
descriptor _recorder pluginId = | ||
(defaultPluginDescriptor pluginId "Provides signature help of something callable") | ||
{ Ide.Types.pluginHandlers = mkPluginHandler SMethod_TextDocumentSignatureHelp signatureHelpProvider | ||
} | ||
|
||
-- TODO(@linj) get doc | ||
signatureHelpProvider :: PluginMethodHandler IdeState Method_TextDocumentSignatureHelp | ||
signatureHelpProvider ideState _pluginId (SignatureHelpParams (TextDocumentIdentifier uri) position _mProgreeToken _mContext) = do | ||
nfp <- getNormalizedFilePathE uri | ||
mResult <- runIdeActionE "signatureHelp" (shakeExtras ideState) $ do | ||
-- TODO(@linj) why HAR {hieAst} may have more than one AST? | ||
(HAR {hieAst, hieKind}, positionMapping) <- useWithStaleFastE GetHieAst nfp | ||
case fromCurrentPosition positionMapping position of | ||
Nothing -> pure Nothing | ||
Just oldPosition -> do | ||
let functionName = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you probably want to do all these in one go, this looks like it will traverse the AST three times! I think you can just do one call to |
||
extractInfoFromSmallestContainingFunctionApplicationAst | ||
oldPosition | ||
hieAst | ||
(\span -> getLeftMostNode >>> getNodeName span) | ||
functionType = | ||
extractInfoFromSmallestContainingFunctionApplicationAst | ||
oldPosition | ||
hieAst | ||
(\span -> getLeftMostNode >>> getNodeType hieKind span) | ||
argumentNumber = | ||
extractInfoFromSmallestContainingFunctionApplicationAst | ||
oldPosition | ||
hieAst | ||
getArgumentNumber | ||
pure $ Just (functionName, functionType, argumentNumber) | ||
case mResult of | ||
-- TODO(@linj) what do non-singleton lists mean? | ||
Just (functionName : _, functionType : _, argumentNumber : _) -> do | ||
pure $ InL $ mkSignatureHelp functionName functionType (fromIntegral argumentNumber - 1) | ||
_ -> pure $ InR Null | ||
|
||
mkSignatureHelp :: Name -> Text -> UInt -> SignatureHelp | ||
mkSignatureHelp functionName functionType argumentNumber = | ||
let functionNameLabelPrefix = printOutputable (ppr functionName) <> " :: " | ||
in SignatureHelp | ||
[ SignatureInformation | ||
(functionNameLabelPrefix <> functionType) | ||
Nothing | ||
(Just $ mkArguments (fromIntegral $ T.length functionNameLabelPrefix) functionType) | ||
(Just $ InL argumentNumber) | ||
] | ||
(Just 0) | ||
(Just $ InL argumentNumber) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we know if this is 0 or 1-indexed? specification doesn't say (of course 🙄 ) so I guess we'll have to test it in vscode to see |
||
|
||
-- TODO(@linj) can type string be a multi-line string? | ||
mkArguments :: UInt -> Text -> [ParameterInformation] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be a lot simpler if you were just looking at the type itself! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although hmm, the offsets are into the string 😱 So maybe you would need to incrementally build up the |
||
mkArguments offset functionType = | ||
let separator = " -> " | ||
separatorLength = fromIntegral $ T.length separator | ||
splits = T.breakOnAll separator functionType | ||
prefixes = fst <$> splits | ||
prefixLengths = fmap (T.length >>> fromIntegral) prefixes | ||
ranges = | ||
[ ( if previousPrefixLength == 0 then 0 else previousPrefixLength + separatorLength, | ||
currentPrefixLength | ||
) | ||
| (previousPrefixLength, currentPrefixLength) <- zip (0: prefixLengths) prefixLengths | ||
] | ||
in [ ParameterInformation (InR range) Nothing | ||
| range <- bimap (+offset) (+offset) <$> ranges | ||
] | ||
|
||
extractInfoFromSmallestContainingFunctionApplicationAst :: | ||
Position -> HieASTs a -> (RealSrcSpan -> HieAST a -> Maybe b) -> [b] | ||
extractInfoFromSmallestContainingFunctionApplicationAst position hieAsts extractInfo = | ||
M.elems $ flip M.mapMaybeWithKey (getAsts hieAsts) $ \hiePath hieAst -> | ||
smallestContainingSatisfying (positionToSpan hiePath position) (nodeHasAnnotation ("HsApp", "HsExpr")) hieAst | ||
>>= extractInfo (positionToSpan hiePath position) | ||
where | ||
positionToSpan hiePath position = | ||
let loc = mkLoc hiePath position in mkRealSrcSpan loc loc | ||
mkLoc (LexicalFastString hiePath) (Position line character) = | ||
mkRealSrcLoc hiePath (fromIntegral line + 1) (fromIntegral character + 1) | ||
|
||
type Annotation = (FastStringCompat, FastStringCompat) | ||
|
||
nodeHasAnnotation :: Annotation -> HieAST a -> Bool | ||
nodeHasAnnotation annotation = sourceNodeInfo >>> maybe False (isAnnotationInNodeInfo annotation) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think it tends to be easier to read a simple case expression than one of the pattern-matching functions like You can still avoid naming the |
||
|
||
-- TODO(@linj): the left most node may not be the function node. example: (if True then f else g) x | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. write a test! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a great example btw. In this case I think we would have to give no signature help, I think, since it's only dynamically known which function we're getting. |
||
getLeftMostNode :: HieAST a -> HieAST a | ||
getLeftMostNode thisNode = | ||
case nodeChildren thisNode of | ||
[] -> thisNode | ||
leftChild: _ -> getLeftMostNode leftChild | ||
|
||
getNodeName :: RealSrcSpan -> HieAST a -> Maybe Name | ||
getNodeName _span hieAst = | ||
if nodeHasAnnotation ("HsVar", "HsExpr") hieAst | ||
then | ||
case mapMaybe extractName $ M.keys $ M.filter isUse $ getSourceNodeIds hieAst of | ||
[name] -> Just name -- TODO(@linj) will there be more than one name? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not 100% sure when/if this can happen. I think it might happen if we're looking at a typeclass method, in which case we could get the generic typeclass method and the specific one perhaps? That would be a good use-case for multilple signatures, e.g. if we have
then we might want to see both the generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my general suggestion would be: let's treat possibly-multiple node annotations as producing possibly-multiple signatures. |
||
_ -> Nothing | ||
else Nothing -- TODO(@linj) must function node be HsVar? | ||
where | ||
extractName = rightToMaybe | ||
|
||
-- TODO(@linj) share code with getNodeName | ||
getNodeType :: HieKind a -> RealSrcSpan -> HieAST a -> Maybe Text | ||
getNodeType (hieKind :: HieKind a) _span hieAst = | ||
if nodeHasAnnotation ("HsVar", "HsExpr") hieAst | ||
then | ||
case M.elems $ M.filter isUse $ getSourceNodeIds hieAst of | ||
[identifierDetails] -> identType identifierDetails >>= (prettyType >>> Just) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest not printing the type here and instead returning the type itself. That would mean that when you're doing |
||
_ -> Nothing -- TODO(@linj) will there be more than one identifierDetails? | ||
else Nothing | ||
where | ||
-- modified from Development.IDE.Spans.AtPoint.atPoint | ||
prettyType :: a -> Text | ||
prettyType = expandType >>> printOutputable | ||
|
||
expandType :: a -> SDoc | ||
expandType t = case hieKind of | ||
HieFresh -> ppr t | ||
HieFromDisk hieFile -> ppr $ hieTypeToIface $ recoverFullType t (hie_types hieFile) | ||
|
||
isUse :: IdentifierDetails a -> Bool | ||
isUse = identInfo >>> S.member Use | ||
|
||
-- Just 1 means the first argument | ||
getArgumentNumber :: RealSrcSpan -> HieAST a -> Maybe Integer | ||
getArgumentNumber span hieAst = | ||
if nodeHasAnnotation ("HsApp", "HsExpr") hieAst | ||
then | ||
case nodeChildren hieAst of | ||
[leftChild, _] -> | ||
if span `isRealSubspanOf` nodeSpan leftChild | ||
then Nothing | ||
else getArgumentNumber span leftChild >>= \argumentNumber -> Just (argumentNumber + 1) | ||
_ -> Nothing -- impossible | ||
else | ||
case nodeChildren hieAst of | ||
[] -> Just 0 -- the function is found | ||
[child] -> getArgumentNumber span child -- ignore irrelevant nodes | ||
_ -> Nothing -- TODO(@linj) handle more cases such as `if` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is reasonable. Really
combineResponses
should have a way to return an error. There are a bunch of methods where it really only makes sense if we have one handler.We could try to combine responses: we would combine the signatures, and so long as only one of them indicated an active signature it would be okay. But that's a bit sketchy and I doubt we'll have several anyway!