-
Notifications
You must be signed in to change notification settings - Fork 636
Signature help implementation #861
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: main
Are you sure you want to change the base?
Signature help implementation #861
Conversation
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.
Pull Request Overview
This PR implements support for signature help in the language server and makes extensive naming refactors throughout the checker and utility modules. Key changes include adding a new handler for signature help in the server, updating configuration for the SignatureHelpProvider, and renaming many internal API functions (e.g. getSignaturesOfType → GetSignaturesOfType) to improve consistency and clarity.
Reviewed Changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
internal/lsp/server.go | Adds new case handling for SignatureHelpParams and configures the signature help provider. |
internal/ls/utilities.go | Updates comments and refactors type argument info functions without altering behavior. |
internal/checker/utilities.go | Renames internal helper functions (e.g. CanHaveSymbol, IsCallOrNewExpression) for consistency. |
internal/checker/types.go | Adds accessor methods on Signature and TupleType for better API exposure. |
internal/checker/services.go | Consistently replaces lower-case API functions with their exported versions. |
internal/checker/relater.go | Refactors signature comparison and tuple handling to use new exported functions. |
internal/checker/printer.go | Adjusts printer logic to use the new exported APIs and updates variadic parameter expansion. |
internal/checker/jsx.go | Updates JSX-related checks to use renamed functions. |
internal/checker/inference.go | Updates inference utilities with new function names. |
internal/checker/flow.go | Refactors flow analysis to call updated API names. |
internal/checker/checker.go | Widespread updates to use capitalized exported API functions for type retrievals and checks. |
internal/astnav/tokens.go | Adds a new helper function, HasQuestionToken, for token analysis. |
internal/ast/utilities.go | Adds helpers for handling template literal tokens and string text nodes. |
_submodules/TypeScript | Updates the submodule commit to a new revision. |
internal/checker/utilities.go
Outdated
@@ -2073,3 +2084,211 @@ func (c *Checker) checkNotCanceled() { | |||
panic("Checker was previously cancelled") | |||
} | |||
} | |||
|
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.
These are some functions I added temporarily to make Signature help work. They will be removed once the node builder pr is merged. Also, these do not give the best result for generics, and returns object
instead. This is why some of the tests have a comments //returns object
because this part returns object
.
internal/ls/signaturehelp.go
Outdated
// Converting signatureHelpParameter to *lsproto.ParameterInformation | ||
var parameters []*lsproto.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.
Some of these functions are converting from local types to lsproto type because they needed some extra parameters like isRest
, isOptional
or isVariadic
to be able to correctly calculate the parameter count and the parameter index that would be highlighted.
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 slice can be allocated with the correct length/capacity
internal/astnav/tokens.go
Outdated
@@ -602,3 +602,13 @@ func getNodeVisitor( | |||
}, | |||
}) | |||
} | |||
|
|||
// !!! | |||
func HasQuestionToken(node *ast.Node) bool { |
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.
Should this go into the ast package?
internal/checker/checker.go
Outdated
@@ -17392,7 +17392,7 @@ func (c *Checker) getTypeOfPropertyOfType(t *Type, name string) *Type { | |||
return nil | |||
} | |||
|
|||
func (c *Checker) getSignaturesOfType(t *Type, kind SignatureKind) []*Signature { | |||
func (c *Checker) GetSignaturesOfType(t *Type, kind SignatureKind) []*Signature { |
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.
Don't rename this method; just declare GetSignaturesOfType
in exports.go
and then forward to this unexported method.
I think the same should go for all of the other unexported functions; we're trying to consolidate these into a clearer API and also avoid huge diffs due to renames (which this PR does at the moment).
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.
Mostly stylistic feedback. I haven't fully reviewed but I haven't seen anything glaringly different from the original so far!
internal/checker/services.go
Outdated
ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool { | ||
return ast.IsCallLikeOrFunctionLikeExpression(n) | ||
}) |
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.
ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool { | |
return ast.IsCallLikeOrFunctionLikeExpression(n) | |
}) | |
ancestorNode := ast.FindAncestor(node, ast.IsCallLikeOrFunctionLikeExpression) |
internal/checker/services.go
Outdated
return ast.IsCallLikeOrFunctionLikeExpression(n) | ||
}) | ||
if ancestorNode != nil { | ||
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature) |
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 might just do two slices here instead of a map, but this might be cleaner.
Either way, I would just add a comment like
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature) | |
// We're going to temporarily reset resolved signatures for all surrounding calls. | |
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature) |
internal/checker/services.go
Outdated
result := fn() | ||
for signatureLinks, resolvedSignature := range cachedResolvedSignatures { |
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.
result := fn() | |
for signatureLinks, resolvedSignature := range cachedResolvedSignatures { | |
result := fn() | |
// Once our work is over, we'll restore any signatures we previously resolved. | |
for signatureLinks, resolvedSignature := range cachedResolvedSignatures { |
internal/ls/utilities.go
Outdated
// if tokenIn != nil { | ||
// searchPosition = scanner.GetTokenPosOfNode(tokenIn, sourceFile, false /*includeJSDoc*/) | ||
// } | ||
if strings.LastIndex(sourceFile.Text(), "<") == -1 { |
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.
if strings.LastIndex(sourceFile.Text(), "<") == -1 { | |
if strings.LastIndexByte(sourceFile.Text(), '<') == -1 { |
internal/ls/utilities.go
Outdated
// This function determines if the node could be type argument position | ||
// Since during editing, when type argument list is not complete, | ||
// the tree could be of any shape depending on the tokens parsed before current node, | ||
// scanning of the previous identifier followed by "<" before current node would give us better result | ||
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so |
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 function determines if the node could be type argument position | |
// Since during editing, when type argument list is not complete, | |
// the tree could be of any shape depending on the tokens parsed before current node, | |
// scanning of the previous identifier followed by "<" before current node would give us better result | |
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so | |
// This function determines if the node could be a type argument position | |
// When editing, it is common to have an incomplete type argument list (e.g. missing ">"), | |
// so the tree can have any shape depending on the tokens before the current node. | |
// Instead, scanning for an identifier followed by a "<" before current node | |
// will typically give us better results than inspecting the tree. | |
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so. |
internal/ls/utilities.go
Outdated
if token == nil || !ast.IsIdentifier(token) { | ||
return nil | ||
} | ||
if remainingLessThanTokens <= 0 { |
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.
No need to check less-than, right?
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.
Yeah, it can instead be changed to check if remainingLessThanTokens == 0
(in Strada it had (!remainingLessThanTokens)
) because this part is trying to check if we have found the start of the type argument.
internal/ls/utilities.go
Outdated
} | ||
tokenKind := token.Kind | ||
remainingMatchingTokens := 0 | ||
for true { |
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 the test of the codebases uses
for true { | |
for { |
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.
Some minor comments, but in general I think this is looking good.
internal/ls/utilities.go
Outdated
} | ||
// This is a rare case, but one that saves on a _lot_ of work if true - if the source file has _no_ `<` character, | ||
// then there obviously can't be any type arguments - no expensive brace-matching backwards scanning required | ||
// searchPosition := len(sourceFile.Text()) |
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.
Is this missing a // !!!
, or are we getting rid of this code?
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.
Yes, getting rid of it.
internal/ls/signaturehelp.go
Outdated
} | ||
|
||
// Only need to be careful if the user typed a character and signature help wasn't showing. | ||
onlyUseSyntacticOwners := context.TriggerKind == 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.
onlyUseSyntacticOwners := context.TriggerKind == 2 | |
onlyUseSyntacticOwners := context.TriggerKind == lsproto.SignatureHelpTriggerKindTriggerCharacter |
internal/ls/signaturehelp.go
Outdated
printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil) | ||
|
||
var getTypeParameters []signatureHelpParameter = []signatureHelpParameter{} | ||
if candidateSignature.TypeParameters() != nil && len(candidateSignature.TypeParameters()) != 0 { |
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 here is what I mentioned elsewhere that could become just len(candidateSignature.TypeParameters()) != 0
.
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 looking pretty good overall. Beyond the comments I left, I suggest you look at every declaration of a slice and ask these questions:
- is this a
displayParts
slice that should just be a string or astrings.Builder
? - if not, can I allocate it with the correct capacity up front?
internal/checker/checker.go
Outdated
result := [][]*ast.Symbol{} | ||
for _, t := range restType.AsUnionType().Types() { | ||
result = append(result, c.expandSignatureParametersWithTupleMembers(signature, t.AsTypeReference(), restIndex, restSymbol)) |
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 can allocate this array with the right size:
result := [][]*ast.Symbol{} | |
for _, t := range restType.AsUnionType().Types() { | |
result = append(result, c.expandSignatureParametersWithTupleMembers(signature, t.AsTypeReference(), restIndex, restSymbol)) | |
types := restType.AsUnionType().Types() | |
result := make([][]*ast.Symbol, len(types)) | |
for i, t := range types { | |
result[i] = c.expandSignatureParametersWithTupleMembers(signature, t.AsTypeReference(), restIndex, restSymbol) |
internal/checker/printer.go
Outdated
@@ -585,7 +585,7 @@ func (p *Printer) printSignature(sig *Signature, returnSeparator string) { | |||
p.printType(p.c.getTypeOfSymbol(sig.thisParameter)) | |||
tail = true | |||
} | |||
expandedParameters := p.c.GetExpandedParameters(sig) | |||
expandedParameters := p.c.getExpandedParameters(sig, true)[0] |
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.
expandedParameters := p.c.getExpandedParameters(sig, true)[0] | |
expandedParameters := p.c.getExpandedParameters(sig, true /*skipUnionExpanding*/)[0] |
internal/checker/services.go
Outdated
func (c *Checker) getResolvedSignatureWorker(node *ast.Node, candidatesOutArray *[]*Signature, checkMode CheckMode, argumentCount int) *Signature { | ||
parsedNode := printer.NewEmitContext().ParseNode(node) | ||
c.apparentArgumentCount = &argumentCount | ||
var res *Signature = nil |
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.
var res *Signature = nil | |
var res *Signature |
|
||
func (c *Checker) getResolvedSignatureWorker(node *ast.Node, candidatesOutArray *[]*Signature, checkMode CheckMode, argumentCount int) *Signature { | ||
parsedNode := printer.NewEmitContext().ParseNode(node) | ||
c.apparentArgumentCount = &argumentCount |
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 forgot to unset this at the end of the function.
internal/ls/signaturehelp.go
Outdated
// in the UI but can be omitted. | ||
Documentation *string | ||
// The Parameters of this signature. | ||
Parameters *[]signatureHelpParameter |
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.
Don't use a pointer to a slice if you can avoid it. You range over this without checking for nil, so it seems like you can avoid it.
internal/ls/signaturehelp.go
Outdated
signatureInformation := []*lsproto.SignatureInformation{} | ||
signatureInformation = append(signatureInformation, &lsproto.SignatureInformation{ | ||
Label: item.Label, | ||
Documentation: nil, | ||
Parameters: ¶meters, | ||
}) |
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 can be a single initialization
internal/ls/signaturehelp.go
Outdated
} | ||
|
||
// Creating display label | ||
typeParameterDisplayParts := []string{} |
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 would use a strings.Builder
here
internal/ls/signaturehelp.go
Outdated
callTargetDisplayParts := []string{} | ||
if callTargetSymbol != nil { | ||
callTargetDisplayParts = append(callTargetDisplayParts, c.SymbolToString(callTargetSymbol)) | ||
} | ||
var items [][]signatureInformation | ||
for _, candidateSignature := range *candidates { | ||
items = append(items, getSignatureHelpItem(candidateSignature, argumentInfo.isTypeParameterList, callTargetDisplayParts, enclosingDeclaration, sourceFile, c)) |
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 the only call to getSignatureHelpItem
, and callTargetDisplayParts
is always length 0 or 1. It could just be a string. We know we're not going to have display parts anymore, so it doesn't seem like there's a need to keep the slices around. A lot of things can probably just become strings.
internal/ls/signaturehelp.go
Outdated
if callTargetSymbol != nil { | ||
callTargetDisplayParts = append(callTargetDisplayParts, c.SymbolToString(callTargetSymbol)) | ||
} | ||
var items [][]signatureInformation |
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 can be allocated with the correct length/capacity
internal/ls/signaturehelp.go
Outdated
selectedItemIndex := 0 | ||
itemSeen := 0 | ||
for i := range items { | ||
item := items[i] | ||
if (*candidates)[i] == resolvedSignature { | ||
selectedItemIndex = itemSeen | ||
if len(item) > 1 { | ||
count := 0 | ||
for _, j := range item { | ||
if j.IsVariadic || len(*j.Parameters) >= argumentInfo.argumentCount { | ||
selectedItemIndex = itemSeen + count | ||
break | ||
} | ||
count++ | ||
} | ||
} | ||
} | ||
itemSeen = itemSeen + len(item) | ||
} |
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 looks terrifying but I see it's ported exactly 😅
This pr covers mostly everything that is needed for signature help except for signature help in .js files. Some tests that I haven't yet ported from Strada are tests for: