-
-
Notifications
You must be signed in to change notification settings - Fork 406
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
Implement signature help #4626
Conversation
TODO: - handle more cases - add successful and (currently failed) tests - show documentation
7a02359
to
9168b74
Compare
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 really worth trying to start getting some tests in place!
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
67f5cdb
to
62fbccf
Compare
This is discussed with @fendor.
This improves performance. It also improves correctness because functionName, functionType and argumentNumber are extracted from the same AST.
Vscode and Emacs(eglot) seems to treat newline as a normal char.
See comment for a comparison with alternative methods.
This comment was marked as outdated.
This comment was marked as outdated.
Test 5 does not pass.
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.
Context:
- Implement signature help #4626 (comment)
- Implement signature help #4626 (comment)
- Implement signature help #4626 (comment)
I implemented a mixed way which uses both structured type and type string. See the comment for the reason of doing it this way instead of using only structured type or only type string.
It seems to work pretty well. "3 out of 87 tests failed".
One failed test case is f :: Integer -> Num Integer => Integer -> Integer
. We matched Num Integer
as the first argument. This probably can be fixed by using regex.
Another similar failed test case is f :: forall l. l -> forall a. a -> a
. When we should highlight the argument l
, we highlight the l
for the second forall
.
Here is another failed test case.
f :: a -> forall a. a -> a f = _
The printed type string for f
is f :: forall a. a -> forall a1. a1 -> a1
. This seems tricky to fix in the current implementation because it needs us to duplicate the renaming logic (the second forall a
becomes forall a1
) of ppr
.
This case only happens when RankNTypes
is used so I do not think it is very common.
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'm wondering how bad it would be to just use the structured type and re-implement some pretty-printing logic. I think the only bit we'd need to duplicate would be the printing of ->
, =>
, and forall
? Maybe that's okay?
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
Another thing we should think about: type arguments.
id :: forall a . a -> a
id x = x
id @...
^^ --- we probably want to consider the "a" a parameter so we can highlight it here when the user is providing a type
-- with RequiredTypeArguments
id :: forall a -> a -> a
id _ x = x
id
^^ -- here again we need to highlight the forall
Type arguments are kind of annoying because they might be optional (with normal foralls), or they might be required (with required type arguments).
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.
TODO: different hightlighting strategies for "reading code" and "writing code".
By "writing code", I mean the cursor is at the end of the function application and we intend to write more args to that function application.
Other cases are "reading code".
Currently, the highlight strategy for "reading code" works well.
For example, f x | y
will highlight y
. Note that in this case the cursor |
is in the subtree of a HsApp
AST node.
Currently, the highlight strategy for "writing code" is problematic.
Case 1: only the function itself is written f |
. We want to highlight the first arg of f
. Note that in this case there is no HsApp
node and so the cursor |
is not in the subtree of any HsApp
AST node.
Case 2: the function itself and at least 1 arg have been written f x |
(f :: a -> b -> c
). We want to highlight the second arg of f
. Note that in this case there are HsApp
nodes but the cursor |
is not in the subtree of any HsApp
AST node.
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.
It sounds to me like your two strategies are the same? In both cases the strategy is "highlight the next argument after the cursor position (which may not exist)"?
Presumably in this situation we will highlight the second argument, though:
f one t|wo
Follows @fendor's suggestion to ignore some details in the doc string.
Newlines generated by neat-interpolation are sensitive[1] to platforms, which may make many tests of hls-cabal-plugin-tests fail on Windows. To keep this PR simple and focused, neat-interpolation usage outside of the signature help plugin is not changed. I plan to change that in a future PR. [1]: nikita-volkov/neat-interpolation#14
491eed2
to
9c97238
Compare
This basically changes indentation size from 4 to 2. Refs: haskell#4703
Follow LSP specification, we use parameter.
I think this PR is in a good shape now. All tests have passed. Please review.
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.
Looks good! Could maybe do with a few more comments but I think we should merge this and make some issues for later improvements.
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.
Let's add some haddock explaining what these things are, and that e.g. only functions (?) will have argument documentation.
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.
what these things are
You mean DocMap
, TyThingMap
and ArgDocMap
? Or just ArgDocMap
?
explaining what these things are
I am not sure what to explain here. For example, it is clear that DocMap
is a map from name to doc.
What about this doc: 'DocMap' stores docs for declarations: functions, data types, instances, methods etc. 'ArgDocMap' stores docs for arguments, e.g., function arguments, method arguments.
only functions (?) will have argument documentation
Probably. Here is its docstring from GHC:
Docs for arguments. E.g. function arguments, method arguments.
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.
We have a nice explanation of the approach for coming up with the parameter strings, but I think this part is also important and worth explaining? Especially since it's something we might want to try and improve later on.
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.
The core of this function is these two lines. I think the function name itself already explains the core pretty well. Not sure what to add.
smallestContainingSatisfying (positionToSpan hiePath position) (nodeHasAnnotation ("HsApp", "HsExpr")) hieAst
>>= extractInfo (positionToSpan hiePath position)
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.
Oh, I though you meant the extractInfoFromSmallestContainingFunctionApplicationAst
function. But maybe you mean the whole algorithm. I am not sure.
The algorithm is briefly described here. I can expand it a bit and add it to comments if that is what you mean.
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.
Have you tried using this from an editor? I wonder if we can actually get away with using a stale HIE AST here. Because we're likely to be looking at a bit of the AST that has just been edited, so if we get a stale one we could be missing e.g. the last argument that got typed. So maybe we actually want to insist on an up-to-date HIE AST?
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 have done many tests using Emacs eglot as the client. I also have done only a few tests in vscode. It seems to work well in both cases.
When experimenting the feature of automatically showing signature help when a space char in inserted in vscode, which is a future work and not implemented in this PR, I do notice that the stale value is not enough, I have to use useE
to get the up-to-date value.
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.
It's a little unclear to me at what point we exclude things that don't look like functions?
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.
This is by nature of our algorithm.
Input: cursorPosition
hsAppNode
= get the smallestHsApp
AST node which containscursorPostion
with the help of extractInfoFromSmallestContainingFunctionApplicationAstfunctionNode
= find the left-most node ofhsAppNode
- try to get
(functionName, functionTypes)
fromfunctionNode
.Nothing
means we fail to get the info. - count
parameterIndex
by traversing thehsAppNode
tree from root tocursorPosition
.Nothing
means eithercursorPosition
is atfunctionNode
or we encounter some AST nodes we do not yet know how to continue our traversal.
So step 1, 3 and 4 can exclude things that don't look like functions.
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 guess it's fine for us to ignore the active parameter. I don't know if this is something that the user can control? Maybe we should try it in VSCode? Perhaps the user can switch between the parameters, and if they've done that we should leave the active parameter as what they selected? Unclear.
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 what you refer to as active parameter
is activateSignature
or oldActivateSignature
.
Perhaps the user can switch between the parameters, and if they've done that we should leave the active parameter as what they selected?
Users cannot select active parameter. The active parameter is defined by the cursor position.
Users can select active signature and the feature of "remembering the previously selected signature when the cursor moves to another parameter" is implemented by reusing oldActivateSignature
.
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.
This isn't quite what I was suggesting, and I'm not sure it will work properly if we do have renaming inside types? e.g. does this work for forall a . a -> a
? I would expect that we would get something like
functionTypeString = "forall a1. a1 -> a1
parameterTypeStrings = ["a", "a"]
And then the parameter type strings won't appear in the full type string? But you have a test for this, so I guess it does work? 🤔
Anyway, perhaps worth explaining if there are cases where you know this won't work, as I'm a bit confused!
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.
You mean forall a . a -> a
is as part of a larger type, right? We have a test case for it. That test case is marked as mkTestExpectFail
and is linked to a future work.
If forall a . a -> a
is not part of a larger type, it works. Many test cases can verify this.
This isn't quite what I was suggesting, and I'm not sure it will work properly if we do have renaming inside types
Renaming currently does not work. We have a test case to track it. It is also listed in the future work.
As we discussed in the meeting, I was trying to work on fixing it this week but have not finished it. I plan to continue working on fixing it after the deadline.
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.
Why do we need to filter here? worth explaining!
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.
The last type of typesOfNode
may (always?) be the same as typeOfName
. To avoid generating two identical signature helps, we do a filtering here.
This filtering is similar to this dropEnd1, I think.
Do you think it is worth adding a comment for it? I personally think the code already says this clearly.
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.
What it does may be clear, but the reason is less clear. For example, in which cases are we generating the same types?
Since this filtering is similar to the one in atPoint
, could we perhaps extract a common function?
Otherwise, just adding what you are saying in #4691 sounds like a good thing to write down!
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.
in which cases are we generating the same types
I think we always generate the same types but I am not sure. I need to check how HIE files are generated to figure this out if it is necessary.
the reason is less clear
You mean why typesOfNode
may (always?) contain typeOfName
? Currently, I do not know. I need to check how HIE files are generated to figure this out if it is necessary.
Since this filtering is similar to the one in atPoint, could we perhaps extract a common function?
Sounds good.
I did not do so because I am not sure 1) if there always are duplicate types and 2) if the duplicate type is the last one of typesOfNode
. If the answers of both questions are true, then we can use a more efficient implementation, i.e., dropEnd1
, than filtering.
If you think filtering is efficient enough, I can extract a common function using filtering.
Otherwise, just adding what you are saying in #4691 sounds like a good thing to write down!
I think what I said in #4691 is two things.
One thing is that only doing dropEnd1
in some cases leads to a bug.
Another thing can be summarized well using my reply here:
The last type of typesOfNode may (always?) be the same as typeOfName. To avoid generating two identical signature helps, we do a filtering here.
So I guess you mean adding this sentence as a comment? I can do that if that is what you mean.
This improves readability.
Uh oh!
There was an error while loading. Please reload this page.
Introduction
Haskell language server (HLS) implements the server part of Language Server Protocol (LSP) to improve user experience of reading and writing Haskell. Signature help is a LSP language feature which shows function signatures and documentation when the cursor is inside a function application. It can also highlight part of the signature related to the current parameter the cursor is at. This is a useful feature and users want it.
This PR aims to implement the signature help feature for HLS.
My work
I have implemented most parts of the signature help feature for HLS in this PR. The last commit during GSoC is d0200a4.
The signature help feature works well in most cases, especially when a user is reading code.
Click for a video demo
hls-signature-help-gsoc-demo.webm
When I worked on this PR, a bug of other parts of HLS was found and I created a PR to fix it.
Besides HLS proper, I also created 2 PRs for
lsp-test
to add a needed function for tests.Click for more detailed work
2025年06月02日 - 2025年06月08日
commit: 7a54a1d
click for screenshot
image
2025年06月09日 - 2025年07月13日
commit: 9168b74
click for video demo
Screencast.From.2025年07月13日.02-11-44.webm
2025年07月14日 - 2025年07月20日
2025年07月21日 - 2025年07月27日
maybe
with case for better readability (bf0b4d5)2025年07月28日 - 2025年08月03日
2025年08月04日 - 2025年08月10日
2025年08月11日 - 2025年08月17日
Show function documentation in signature help (a522e88)
Show function argument documentation in signature help (d826d06)
Do not error if doc is not available (35399e7)
click for screenshots
image image2025年08月18日 - 2025年08月24日
getSignatureHelp
tolsp-test
: Add signature help request to lsp-test lsp#621Any
becomesZonkAny 0
(c7931a2)2025年08月25日 - 2025年08月31日
2025年09月01日 - 2025年09月02日
Future work
forall
when the cursor is at a type application such as@Int
.OpApp
(operator application) ofHsApp
such asf x $ y|
.Acknowledgements
This is a Google Summer of Code (GSoC) project. Thanks to all the people involved. Special thanks to my mentors, @michaelpj and @fendor, for their help.
Closes #3598
Related to #2348 since we make mkDocMap expose the argument doc map