-
-
Notifications
You must be signed in to change notification settings - Fork 401
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
Conversation
@@ -764,6 +770,10 @@ instance PluginRequestMethod Method_TextDocumentDocumentSymbol where | |||
si = SymbolInformation name' (ds ^. L.kind) Nothing parent (ds ^. L.deprecated) loc | |||
in [si] <> children' | |||
|
|||
-- TODO(@linj) is this correct? |
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!
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
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 comment
The 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 mkArguments
you can pattern match on the type itself, not on the string representation. Then you don't need to worry about e.g. whether it prints on multiple lines or whatever.
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.
See #4626 (comment)
(Just $ InL argumentNumber) | ||
|
||
-- 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 comment
The 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 comment
The 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 ParameterInformation
s and the overall label
, but that would probably still be nicer?
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.
See #4626 (comment)
plugins/hls-signature-help-plugin/src/Ide/Plugin/SignatureHelp.hs
Outdated
Show resolved
Hide resolved
nodeHasAnnotation :: Annotation -> HieAST a -> Bool | ||
nodeHasAnnotation annotation = sourceNodeInfo >>> maybe False (isAnnotationInNodeInfo annotation) | ||
|
||
-- 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 comment
The 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 comment
The 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.
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 one related test case for this now: the dynamic function
test.
This test is not a unit test for this getLeftMostNode
function. This test is an integration testing for the signature help plugin.
f :: Int -> Int -> Int
f = _
g :: Int -> Int -> Int
g = _
x = (if _ then f else g) 1 2
^^ ^^^ ^ ^^^ ^ ^^^^^^^^
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 comment
The 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
foldMap @[] f ^
then we might want to see both the generic Foldable
signature and the one for the list instance.
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 my general suggestion would be: let's treat possibly-multiple node annotations as producing possibly-multiple signatures.
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.
Implemented in d603ec4.
Related tests:
- type constraint
- type constraint with kind signatures
- multi-line type with type constraint
I'm not 100% sure when/if this can happen.
I do not have a principled conclusion. But from my experiments, the summary is that
- There are multiple types iif there are type variables.
- There seems to be only one
Name
.
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.
All tests on Linux and darwin passed! Not sure what happens on windows. Added a few more tests and now |
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.
a bit offtopic: Is 2-space indentation instead of 4 allowed for new code?
The .editorconfig says 4.
However, some newly added plugins use 2:
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.
Follow the .editorconfig
:)
hls-cabal-gild-plugin
was copy-pasted from the hls-cabal-fmt-plugin
findArgumentRanges :: Type -> [(UInt, UInt)] | ||
findArgumentRanges functionType = | ||
let functionTypeString = printOutputableOneLine functionType | ||
functionTypeStringLength = fromIntegral $ T.length functionTypeString | ||
splitFunctionTypes = filter notTypeConstraint $ splitFunTysIgnoringForAll functionType | ||
splitFunctionTypeStrings = printOutputableOneLine . fst <$> splitFunctionTypes | ||
-- reverse to avoid matching "a" of "forall a" in "forall a. a -> a" | ||
reversedRanges = | ||
drop 1 $ -- do not need the range of the result (last) type | ||
findArgumentStringRanges | ||
0 | ||
(T.reverse functionTypeString) | ||
(T.reverse <$> reverse splitFunctionTypeStrings) | ||
in reverse $ modifyRange functionTypeStringLength <$> reversedRanges | ||
where | ||
modifyRange functionTypeStringLength (start, end) = | ||
(functionTypeStringLength - end, functionTypeStringLength - start) | ||
|
||
{- | ||
The implemented method uses both structured type and unstructured type string. | ||
It provides good enough results and is easier to implement than alternative | ||
method 1 or 2. | ||
|
||
Alternative method 1: use only structured type | ||
This method is hard to implement because we need to duplicate some logic of 'ppr' for 'Type'. | ||
Some tricky cases are as follows: | ||
- 'Eq a => Num b -> c' is shown as '(Eq a, Numb) => c' | ||
- 'forall' can appear anywhere in a type when RankNTypes is enabled | ||
f :: forall a. Maybe a -> forall b. (a, b) -> b | ||
- '=>' can appear anywhere in a type | ||
g :: forall a b. Eq a => a -> Num b => b -> b | ||
- ppr the first argument type of '(a -> b) -> a -> b' is 'a -> b' (no parentheses) | ||
- 'forall' is not always shown | ||
|
||
Alternative method 2: use only unstructured type string | ||
This method is hard to implement because we need to parse the type string. | ||
Some tricky cases are as follows: | ||
- h :: forall a (m :: Type -> Type). Monad m => a -> m a | ||
-} | ||
findArgumentStringRanges :: UInt -> Text -> [Text] -> [(UInt, UInt)] |
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.
nodeHasAnnotation :: Annotation -> HieAST a -> Bool | ||
nodeHasAnnotation annotation = sourceNodeInfo >>> maybe False (isAnnotationInNodeInfo annotation) | ||
|
||
-- 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 comment
The reason will be displayed to describe this comment to others. Learn more.
We have one related test case for this now: the dynamic function
test.
This test is not a unit test for this getLeftMostNode
function. This test is an integration testing for the signature help plugin.
f :: Int -> Int -> Int
f = _
g :: Int -> Int -> Int
g = _
x = (if _ then f else g) 1 2
^^ ^^^ ^ ^^^ ^ ^^^^^^^^
Closes #3598
I will update progress here.
2025-06-02 - 2025-06-08
commit: 7a54a1d
click for screenshot
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