Skip to content

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

Draft
wants to merge 37 commits into
base: master
Choose a base branch
from
Draft

Conversation

jian-lin
Copy link
Contributor

@jian-lin jian-lin commented Jun 8, 2025

Closes #3598

I will update progress here.

This PR also relates to #2348 because we let mkDocMap expose the argument doc map.

2025-06-02 - 2025-06-08

  • Add basic boilerplate for signature help plugin
    • commit: 7a54a1d

    • click for screenshot

      image

2025-06-09 - 2025-07-13

  • Finish signature help plugin MVP: show function signature and highlight the current parameter
    • commit: 9168b74

    • click for video demo
      Screencast.From.2025-07-13.02-11-44.webm

2025-07-14 - 2025-07-20

  • Add basic tests. Some of them are not passed for now.

2025-07-21 - 2025-07-27

  • Change expected test results considering the cursor shape (a6635ca)
  • Replace maybe with case for better readability (bf0b4d5)
  • Call extractInfoFromSmallestContainingFunctionApplicationAst once (c95d6e4)

2025-07-28 - 2025-08-03

  • Show more types: each type as one signature help (d603ec4)
  • Add tests for kind signatures and higher-order function (dca1311) (471958f)

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 image

2025-08-18 - 2025-08-24

  • Upstream getSignatureHelp to lsp-test: Add signature help request to lsp-test lsp#621
  • Make signature helps reproducible in tests (eeeb283)
  • Do not show uris in the argument documentation (f1d19ba)
  • Add tests
    • imported function with argument doc (433a8ad)
    • imported function with no doc (d3d7d12)
  • Remember previous active signature (e6702b7)
  • Show signature help even when there are type applications (800f908)
  • Fix tests for ghc > 9.8
  • Add myself as codeowner of hls-signature-help-plugin (b6d4617)
  • Add hls-signature-help-plugin to some documentation files (9a1de4e)

@@ -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?
Copy link
Collaborator

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
@jian-lin jian-lin force-pushed the pr/signature-help branch from 7a02359 to 9168b74 Compare July 12, 2025 18:33
Copy link
Collaborator

@michaelpj michaelpj left a 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!

@jian-lin jian-lin force-pushed the pr/signature-help branch from 67f5cdb to 62fbccf Compare July 16, 2025 06:52
@jian-lin
Copy link
Contributor Author

jian-lin commented Aug 7, 2025

All tests on Linux and darwin passed! Not sure what happens on windows.


Added a few more tests and now 3 out of 87 tests failed.

Comment on lines +122 to +161
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)]
Copy link
Contributor Author

@jian-lin jian-lin Aug 7, 2025

Choose a reason for hiding this comment

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

Context:

@michaelpj

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.

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 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?

@michaelpj
Copy link
Collaborator

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).

Comment on lines 39 to +41
type DocMap = NameEnv SpanDoc
type TyThingMap = NameEnv TyThing
type ArgDocMap = NameEnv (IntMap SpanDoc)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of creating a new ArgDocMap, an alternative implementation is to extend DocMap to NameEnv (SpanDoc, IntMap SpanDoc).

Copy link
Collaborator

Choose a reason for hiding this comment

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

That sounds sensible to me? Any reason not to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do not have a strong opinion on this. I will change the code to type DocMap = NameEnv (SpanDoc, IntMap SpanDoc).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking at the code it seems like it would simplifiy things quite a lot.

Copy link
Contributor Author

@jian-lin jian-lin Aug 21, 2025

Choose a reason for hiding this comment

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

Now I have second thoughts about this because the alternative implementation type DocMap = NameEnv (SpanDoc, IntMap SpanDoc) is semantically incorrect. The semantically correct version should be type DocMap = NameEnv (Maybe SpanDoc, IntMap SpanDoc) or type DocMap = NameEnv (Maybe SpanDoc, Maybe (IntMap SpanDoc)) because a Name can have only function doc or arg doc.

I prefer to keep the old implementation, i.e., introducing a new ArgDocMap, since the semantically correct version of the alternative implementation seems more complicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am a bit confused about this failed test in CI which I cannot reproduce locally.

In CI, the function doc of pure is "\n\nLift a value.\n\n". Locally, it is "\n\nLift a value.\n\n\\[Documentation\\]\\(file://.*\\)\n\n\\[Source\\]\\(file://.*\\)\n\n". So somehow in CI HLS fails to find uris of the imported function pure.

type constraint with kind signatures
    type constraint with kind signatures 2:          FAIL (0.15s)
      src/Test/Hls.hs:400:
      Full Line:       "x = pure True"
      Cursor Column:   "         ^"
      Prefix Text:     ""
      
      expected: Just (SignatureHelp {_signatures = [SignatureInformation {_label = "pure :: forall (f :: Type -> Type) a. Applicative f => a -> f a", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n\\[Documentation\\]\\(file://.*\\)\n\n\\[Source\\]\\(file://.*\\)\n\n"})), _parameters = Just [ParameterInformation {_label = InR (55,56), _documentation = Nothing}], _activeParameter = Just (InL 0)},SignatureInformation {_label = "pure :: Bool -> IO Bool", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n\\[Documentation\\]\\(file://.*\\)\n\n\\[Source\\]\\(file://.*\\)\n\n"})), _parameters = Just [ParameterInformation {_label = InR (8,12), _documentation = Nothing}], _activeParameter = Just (InL 0)},SignatureInformation {_label = "pure :: forall a. a -> IO a", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n\\[Documentation\\]\\(file://.*\\)\n\n\\[Source\\]\\(file://.*\\)\n\n"})), _parameters = Just [ParameterInformation {_label = InR (18,19), _documentation = Nothing}], _activeParameter = Just (InL 0)}], _activeSignature = Just 0, _activeParameter = Just (InL 0)})
       but got: Just (SignatureHelp {_signatures = [SignatureInformation {_label = "pure :: forall (f :: Type -> Type) a. Applicative f => a -> f a", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n"})), _parameters = Just [ParameterInformation {_label = InR (55,56), _documentation = Nothing}], _activeParameter = Just (InL 0)},SignatureInformation {_label = "pure :: Bool -> IO Bool", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n"})), _parameters = Just [ParameterInformation {_label = InR (8,12), _documentation = Nothing}], _activeParameter = Just (InL 0)},SignatureInformation {_label = "pure :: forall a. a -> IO a", _documentation = Just (InR (MarkupContent {_kind = MarkupKind_Markdown, _value = "\n\nLift a value.\n\n"})), _parameters = Just [ParameterInformation {_label = InR (18,19), _documentation = Nothing}], _activeParameter = Just (InL 0)}], _activeSignature = Just 0, _activeParameter = Just (InL 0)})
      Use -p '/type constraint with kind signatures 2/' to rerun this test only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show function signature while providing the arguments
3 participants