kittenipc/kitcom/internal/tsgo/ls/signaturehelp.go
2025-10-15 10:12:44 +03:00

1158 lines
43 KiB
Go

package ls
import (
"context"
"fmt"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/nodebuilder"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
type callInvocation struct {
node *ast.Node
}
type typeArgsInvocation struct {
called *ast.Identifier
}
type contextualInvocation struct {
signature *checker.Signature
node *ast.Node // Just for enclosingDeclaration for printing types
symbol *ast.Symbol
}
type invocation struct {
callInvocation *callInvocation
typeArgsInvocation *typeArgsInvocation
contextualInvocation *contextualInvocation
}
func (l *LanguageService) ProvideSignatureHelp(
ctx context.Context,
documentURI lsproto.DocumentUri,
position lsproto.Position,
context *lsproto.SignatureHelpContext,
clientOptions *lsproto.SignatureHelpClientCapabilities,
preferences *UserPreferences,
) (lsproto.SignatureHelpResponse, error) {
program, sourceFile := l.getProgramAndFile(documentURI)
items := l.GetSignatureHelpItems(
ctx,
int(l.converters.LineAndCharacterToPosition(sourceFile, position)),
program,
sourceFile,
context,
clientOptions,
preferences)
return lsproto.SignatureHelpOrNull{SignatureHelp: items}, nil
}
func (l *LanguageService) GetSignatureHelpItems(
ctx context.Context,
position int,
program *compiler.Program,
sourceFile *ast.SourceFile,
context *lsproto.SignatureHelpContext,
clientOptions *lsproto.SignatureHelpClientCapabilities,
preferences *UserPreferences,
) *lsproto.SignatureHelp {
typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile)
defer done()
// Decide whether to show signature help
startingToken := astnav.FindPrecedingToken(sourceFile, position)
if startingToken == nil {
// We are at the beginning of the file
return nil
}
type signatureHelpTriggerReasonKind int32
const (
signatureHelpTriggerReasonKindNone signatureHelpTriggerReasonKind = 0 // was undefined
signatureHelpTriggerReasonKindInvoked signatureHelpTriggerReasonKind = iota // was "invoked"
signatureHelpTriggerReasonKindCharacterTyped // was "characterTyped"
signatureHelpTriggerReasonKindRetriggered // was "retrigger"
)
// Emulate VS Code's toTsTriggerReason.
triggerReasonKind := signatureHelpTriggerReasonKindNone
if context != nil {
switch context.TriggerKind {
case lsproto.SignatureHelpTriggerKindTriggerCharacter:
if context.TriggerCharacter != nil {
if context.IsRetrigger {
triggerReasonKind = signatureHelpTriggerReasonKindRetriggered
} else {
triggerReasonKind = signatureHelpTriggerReasonKindCharacterTyped
}
} else {
triggerReasonKind = signatureHelpTriggerReasonKindInvoked
}
case lsproto.SignatureHelpTriggerKindContentChange:
if context.IsRetrigger {
triggerReasonKind = signatureHelpTriggerReasonKindRetriggered
} else {
triggerReasonKind = signatureHelpTriggerReasonKindCharacterTyped
}
case lsproto.SignatureHelpTriggerKindInvoked:
triggerReasonKind = signatureHelpTriggerReasonKindInvoked
default:
triggerReasonKind = signatureHelpTriggerReasonKindInvoked
}
}
// Only need to be careful if the user typed a character and signature help wasn't showing.
onlyUseSyntacticOwners := triggerReasonKind == signatureHelpTriggerReasonKindCharacterTyped
// Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it.
if onlyUseSyntacticOwners && IsInString(sourceFile, position, startingToken) { // isInComment(sourceFile, position) needs formatting implemented
return nil
}
isManuallyInvoked := triggerReasonKind == signatureHelpTriggerReasonKindInvoked
argumentInfo := getContainingArgumentInfo(startingToken, sourceFile, typeChecker, isManuallyInvoked, position)
if argumentInfo == nil {
return nil
}
// cancellationToken.throwIfCancellationRequested();
// Extra syntactic and semantic filtering of signature help
candidateInfo := getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners)
// cancellationToken.throwIfCancellationRequested();
if candidateInfo == nil {
// !!!
// // We didn't have any sig help items produced by the TS compiler. If this is a JS
// // file, then see if we can figure out anything better.
// return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined;
return nil
}
// return typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
if candidateInfo.candidateInfo != nil {
return createSignatureHelpItems(candidateInfo.candidateInfo.candidates, candidateInfo.candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker, onlyUseSyntacticOwners, clientOptions)
}
return createTypeHelpItems(candidateInfo.typeInfo, argumentInfo, sourceFile, clientOptions, typeChecker)
}
func createTypeHelpItems(symbol *ast.Symbol, argumentInfo *argumentListInfo, sourceFile *ast.SourceFile, clientOptions *lsproto.SignatureHelpClientCapabilities, c *checker.Checker) *lsproto.SignatureHelp {
typeParameters := c.GetLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)
if typeParameters == nil {
return nil
}
item := getTypeHelpItem(symbol, typeParameters, getEnclosingDeclarationFromInvocation(argumentInfo.invocation), sourceFile, c)
// Converting signatureHelpParameter to *lsproto.ParameterInformation
parameters := make([]*lsproto.ParameterInformation, len(item.Parameters))
for i, param := range item.Parameters {
parameters[i] = param.parameterInfo
}
signatureInformation := []*lsproto.SignatureInformation{
{
Label: item.Label,
Documentation: nil,
Parameters: &parameters,
},
}
return &lsproto.SignatureHelp{
Signatures: signatureInformation,
ActiveSignature: ptrTo(uint32(0)),
ActiveParameter: &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(argumentInfo.argumentIndex))},
}
}
func getTypeHelpItem(symbol *ast.Symbol, typeParameter []*checker.Type, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) signatureInformation {
printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil)
parameters := make([]signatureHelpParameter, len(typeParameter))
for i, typeParam := range typeParameter {
parameters[i] = createSignatureHelpParameterForTypeParameter(typeParam, sourceFile, enclosingDeclaration, c, printer)
}
// Creating display label
var displayParts strings.Builder
displayParts.WriteString(c.SymbolToString(symbol))
if len(parameters) != 0 {
displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken))
for i, typeParameter := range parameters {
if i > 0 {
displayParts.WriteString(", ")
}
displayParts.WriteString(*typeParameter.parameterInfo.Label.String)
}
displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken))
}
return signatureInformation{
Label: displayParts.String(),
Documentation: nil,
Parameters: parameters,
IsVariadic: false,
}
}
func createSignatureHelpItems(candidates []*checker.Signature, resolvedSignature *checker.Signature, argumentInfo *argumentListInfo, sourceFile *ast.SourceFile, c *checker.Checker, useFullPrefix bool, clientOptions *lsproto.SignatureHelpClientCapabilities) *lsproto.SignatureHelp {
enclosingDeclaration := getEnclosingDeclarationFromInvocation(argumentInfo.invocation)
if enclosingDeclaration == nil {
return nil
}
var callTargetSymbol *ast.Symbol
if argumentInfo.invocation.contextualInvocation != nil {
callTargetSymbol = argumentInfo.invocation.contextualInvocation.symbol
} else {
callTargetSymbol = c.GetSymbolAtLocation(getExpressionFromInvocation(argumentInfo))
if callTargetSymbol == nil && useFullPrefix && resolvedSignature.Declaration() != nil {
callTargetSymbol = resolvedSignature.Declaration().Symbol()
}
}
var callTargetDisplayParts strings.Builder
if callTargetSymbol != nil {
callTargetDisplayParts.WriteString(c.SymbolToString(callTargetSymbol))
}
items := make([][]signatureInformation, len(candidates))
for i, candidateSignature := range candidates {
items[i] = getSignatureHelpItem(candidateSignature, argumentInfo.isTypeParameterList, callTargetDisplayParts.String(), enclosingDeclaration, sourceFile, c)
}
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)
}
debug.Assert(selectedItemIndex != -1)
flattenedSignatures := []signatureInformation{}
for _, item := range items {
flattenedSignatures = append(flattenedSignatures, item...)
}
if len(flattenedSignatures) == 0 {
return nil
}
// Converting []signatureInformation to []*lsproto.SignatureInformation
signatureInformation := make([]*lsproto.SignatureInformation, len(flattenedSignatures))
for i, item := range flattenedSignatures {
parameters := make([]*lsproto.ParameterInformation, len(item.Parameters))
for j, param := range item.Parameters {
parameters[j] = param.parameterInfo
}
signatureInformation[i] = &lsproto.SignatureInformation{
Label: item.Label,
Documentation: nil,
Parameters: &parameters,
}
}
help := &lsproto.SignatureHelp{
Signatures: signatureInformation,
ActiveSignature: ptrTo(uint32(selectedItemIndex)),
ActiveParameter: &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(argumentInfo.argumentIndex))},
}
activeSignature := flattenedSignatures[selectedItemIndex]
if activeSignature.IsVariadic {
firstRest := core.FindIndex(activeSignature.Parameters, func(p signatureHelpParameter) bool {
return p.isRest
})
if -1 < firstRest && firstRest < len(activeSignature.Parameters)-1 {
// We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL
help.ActiveParameter = &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(len(activeSignature.Parameters)))}
}
if help.ActiveParameter != nil && help.ActiveParameter.Uinteger != nil && *help.ActiveParameter.Uinteger > uint32(len(activeSignature.Parameters)-1) {
help.ActiveParameter = &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(len(activeSignature.Parameters) - 1))}
}
}
return help
}
func getSignatureHelpItem(candidate *checker.Signature, isTypeParameterList bool, callTargetSymbol string, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) []signatureInformation {
var infos []*signatureHelpItemInfo
if isTypeParameterList {
infos = itemInfoForTypeParameters(candidate, c, enclosingDeclaration, sourceFile)
} else {
infos = itemInfoForParameters(candidate, c, enclosingDeclaration, sourceFile)
}
suffixDisplayParts := returnTypeToDisplayParts(candidate, c)
result := make([]signatureInformation, len(infos))
for i, info := range infos {
var display strings.Builder
display.WriteString(callTargetSymbol)
display.WriteString(info.displayParts)
display.WriteString(suffixDisplayParts)
result[i] = signatureInformation{
Label: display.String(),
Documentation: nil,
Parameters: info.parameters,
IsVariadic: info.isVariadic,
}
}
return result
}
func returnTypeToDisplayParts(candidateSignature *checker.Signature, c *checker.Checker) string {
var returnType strings.Builder
returnType.WriteString(": ")
predicate := c.GetTypePredicateOfSignature(candidateSignature)
if predicate != nil {
returnType.WriteString(c.TypePredicateToString(predicate))
} else {
returnType.WriteString(c.TypeToString(c.GetReturnTypeOfSignature(candidateSignature)))
}
return returnType.String()
}
func itemInfoForTypeParameters(candidateSignature *checker.Signature, c *checker.Checker, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile) []*signatureHelpItemInfo {
printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil)
var typeParameters []*checker.Type
if candidateSignature.Target() != nil {
typeParameters = candidateSignature.Target().TypeParameters()
} else {
typeParameters = candidateSignature.TypeParameters()
}
signatureHelpTypeParameters := make([]signatureHelpParameter, len(typeParameters))
for i, typeParameter := range typeParameters {
signatureHelpTypeParameters[i] = createSignatureHelpParameterForTypeParameter(typeParameter, sourceFile, enclosingDeclaration, c, printer)
}
thisParameter := []signatureHelpParameter{}
if candidateSignature.ThisParameter() != nil {
thisParameter = []signatureHelpParameter{createSignatureHelpParameterForParameter(candidateSignature.ThisParameter(), enclosingDeclaration, printer, sourceFile, c)}
}
// Creating type parameter display label
var displayParts strings.Builder
displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken))
for i, typeParameter := range signatureHelpTypeParameters {
if i > 0 {
displayParts.WriteString(", ")
}
displayParts.WriteString(*typeParameter.parameterInfo.Label.String)
}
displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken))
// Creating display label for parameters like, (a: string, b: number)
lists := c.GetExpandedParameters(candidateSignature, false)
if len(lists) != 0 {
displayParts.WriteString(scanner.TokenToString(ast.KindOpenParenToken))
}
result := make([]*signatureHelpItemInfo, len(lists))
for i, parameterList := range lists {
var displayParameters strings.Builder
displayParameters.WriteString(displayParts.String())
parameters := thisParameter
for j, param := range parameterList {
parameter := createSignatureHelpParameterForParameter(param, enclosingDeclaration, printer, sourceFile, c)
parameters = append(parameters, parameter)
if j > 0 {
displayParameters.WriteString(", ")
}
displayParameters.WriteString(*parameter.parameterInfo.Label.String)
}
displayParameters.WriteString(scanner.TokenToString(ast.KindCloseParenToken))
result[i] = &signatureHelpItemInfo{
isVariadic: false,
parameters: signatureHelpTypeParameters,
displayParts: displayParameters.String(),
}
}
return result
}
func itemInfoForParameters(candidateSignature *checker.Signature, c *checker.Checker, enclosingDeclaratipn *ast.Node, sourceFile *ast.SourceFile) []*signatureHelpItemInfo {
printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil)
signatureHelpTypeParameters := make([]signatureHelpParameter, len(candidateSignature.TypeParameters()))
if len(candidateSignature.TypeParameters()) != 0 {
for i, typeParameter := range candidateSignature.TypeParameters() {
signatureHelpTypeParameters[i] = createSignatureHelpParameterForTypeParameter(typeParameter, sourceFile, enclosingDeclaratipn, c, printer)
}
}
// Creating display label for type parameters like, <T, U>
var displayParts strings.Builder
if len(signatureHelpTypeParameters) != 0 {
displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken))
for _, typeParameter := range signatureHelpTypeParameters {
displayParts.WriteString(*typeParameter.parameterInfo.Label.String)
}
displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken))
}
// Creating display parts for parameters. For example, (a: string, b: number)
lists := c.GetExpandedParameters(candidateSignature, false)
if len(lists) != 0 {
displayParts.WriteString(scanner.TokenToString(ast.KindOpenParenToken))
}
isVariadic := func(parameterList []*ast.Symbol) bool {
if !c.HasEffectiveRestParameter(candidateSignature) {
return false
}
if len(lists) == 1 {
return true
}
return len(parameterList) != 0 && parameterList[len(parameterList)-1] != nil && (parameterList[len(parameterList)-1].CheckFlags&ast.CheckFlagsRestParameter != 0)
}
result := make([]*signatureHelpItemInfo, len(lists))
for i, parameterList := range lists {
parameters := make([]signatureHelpParameter, len(parameterList))
var displayParameters strings.Builder
displayParameters.WriteString(displayParts.String())
for j, param := range parameterList {
parameter := createSignatureHelpParameterForParameter(param, enclosingDeclaratipn, printer, sourceFile, c)
parameters[j] = parameter
if j > 0 {
displayParameters.WriteString(", ")
}
displayParameters.WriteString(*parameter.parameterInfo.Label.String)
}
displayParameters.WriteString(scanner.TokenToString(ast.KindCloseParenToken))
result[i] = &signatureHelpItemInfo{
isVariadic: isVariadic(parameterList),
parameters: parameters,
displayParts: displayParameters.String(),
}
}
return result
}
const signatureHelpNodeBuilderFlags = nodebuilder.FlagsOmitParameterModifiers | nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
func createSignatureHelpParameterForParameter(parameter *ast.Symbol, enclosingDeclaratipn *ast.Node, p *printer.Printer, sourceFile *ast.SourceFile, c *checker.Checker) signatureHelpParameter {
display := p.Emit(checker.NewNodeBuilder(c, printer.NewEmitContext()).SymbolToParameterDeclaration(parameter, enclosingDeclaratipn, signatureHelpNodeBuilderFlags, nodebuilder.InternalFlagsNone, nil), sourceFile)
isOptional := parameter.CheckFlags&ast.CheckFlagsOptionalParameter != 0
isRest := parameter.CheckFlags&ast.CheckFlagsRestParameter != 0
return signatureHelpParameter{
parameterInfo: &lsproto.ParameterInformation{
Label: lsproto.StringOrTuple{String: &display},
Documentation: nil,
},
isRest: isRest,
isOptional: isOptional,
}
}
func createSignatureHelpParameterForTypeParameter(t *checker.Type, sourceFile *ast.SourceFile, enclosingDeclaration *ast.Node, c *checker.Checker, p *printer.Printer) signatureHelpParameter {
display := p.Emit(checker.NewNodeBuilder(c, printer.NewEmitContext()).TypeParameterToDeclaration(t, enclosingDeclaration, signatureHelpNodeBuilderFlags, nodebuilder.InternalFlagsNone, nil), sourceFile)
return signatureHelpParameter{
parameterInfo: &lsproto.ParameterInformation{
Label: lsproto.StringOrTuple{String: &display},
},
isRest: false,
isOptional: false,
}
}
// Represents the signature of something callable. A signature
// can have a label, like a function-name, a doc-comment, and
// a set of parameters.
type signatureInformation struct {
// The Label of this signature. Will be shown in
// the UI.
Label string
// The human-readable doc-comment of this signature. Will be shown
// in the UI but can be omitted.
Documentation *string
// The Parameters of this signature.
Parameters []signatureHelpParameter
// Needed only here, not in lsp
IsVariadic bool
}
type signatureHelpItemInfo struct {
isVariadic bool
parameters []signatureHelpParameter
displayParts string
}
type signatureHelpParameter struct {
parameterInfo *lsproto.ParameterInformation
isRest bool
isOptional bool
}
func getEnclosingDeclarationFromInvocation(invocation *invocation) *ast.Node {
if invocation.callInvocation != nil {
return invocation.callInvocation.node
} else if invocation.typeArgsInvocation != nil {
return invocation.typeArgsInvocation.called.AsNode()
} else {
return invocation.contextualInvocation.node
}
}
func getExpressionFromInvocation(argumentInfo *argumentListInfo) *ast.Node {
if argumentInfo.invocation.callInvocation != nil {
return ast.GetInvokedExpression(argumentInfo.invocation.callInvocation.node)
}
return argumentInfo.invocation.typeArgsInvocation.called.AsNode()
}
type candidateInfo struct {
candidates []*checker.Signature
resolvedSignature *checker.Signature
}
type CandidateOrTypeInfo struct {
candidateInfo *candidateInfo
typeInfo *ast.Symbol
}
func getCandidateOrTypeInfo(info *argumentListInfo, c *checker.Checker, sourceFile *ast.SourceFile, startingToken *ast.Node, onlyUseSyntacticOwners bool) *CandidateOrTypeInfo {
if info.invocation.callInvocation != nil {
if onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, info.invocation.callInvocation.node, sourceFile) {
return nil
}
resolvedSignature, candidates := checker.GetResolvedSignatureForSignatureHelp(info.invocation.callInvocation.node, info.argumentCount, c)
return &CandidateOrTypeInfo{
candidateInfo: &candidateInfo{
candidates: candidates,
resolvedSignature: resolvedSignature,
},
}
}
if info.invocation.typeArgsInvocation != nil {
called := info.invocation.typeArgsInvocation.called.AsNode()
container := called
if ast.IsIdentifier(called) {
container = called.Parent
}
if onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, container) {
return nil
}
candidates := getPossibleGenericSignatures(called, info.argumentCount, c)
if len(candidates) != 0 {
return &CandidateOrTypeInfo{
candidateInfo: &candidateInfo{
candidates: candidates,
resolvedSignature: candidates[0],
},
}
}
symbol := c.GetSymbolAtLocation(called)
return &CandidateOrTypeInfo{
typeInfo: symbol,
}
}
if info.invocation.contextualInvocation != nil {
return &CandidateOrTypeInfo{
candidateInfo: &candidateInfo{
candidates: []*checker.Signature{info.invocation.contextualInvocation.signature},
resolvedSignature: info.invocation.contextualInvocation.signature,
},
}
}
debug.AssertNever(info.invocation)
return nil
}
func isSyntacticOwner(startingToken *ast.Node, node *ast.CallLikeExpression, sourceFile *ast.SourceFile) bool { // !!! not tested
if !ast.IsCallOrNewExpression(node) {
return false
}
invocationChildren := getChildrenFromNonJSDocNode(node, sourceFile)
switch startingToken.Kind {
case ast.KindOpenParenToken, ast.KindCommaToken:
return containsNode(invocationChildren, startingToken)
case ast.KindLessThanToken:
return containsPrecedingToken(startingToken, sourceFile, node.AsCallExpression().Expression)
default:
return false
}
}
func containsPrecedingToken(startingToken *ast.Node, sourceFile *ast.SourceFile, container *ast.Node) bool {
pos := startingToken.Pos()
// There's a possibility that `startingToken.parent` contains only `startingToken` and
// missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that
// case, the preceding token we want is actually higher up the tree—almost definitely the
// next parent, but theoretically the situation with missing nodes might be happening on
// multiple nested levels.
currentParent := startingToken.Parent
for currentParent != nil {
precedingToken := astnav.FindPrecedingToken(sourceFile, pos)
if precedingToken != nil {
return RangeContainsRange(container.Loc, precedingToken.Loc)
}
currentParent = currentParent.Parent
}
// return Debug.fail("Could not find preceding token");
return false
}
func getContainingArgumentInfo(node *ast.Node, sourceFile *ast.SourceFile, checker *checker.Checker, isManuallyInvoked bool, position int) *argumentListInfo {
for n := node; !ast.IsSourceFile(n) && (isManuallyInvoked || !ast.IsBlock(n)); n = n.Parent {
// If the node is not a subspan of its parent, this is a big problem.
// There have been crashes that might be caused by this violation.
debug.Assert(RangeContainsRange(n.Parent.Loc, n.Loc), fmt.Sprintf("Not a subspan. Child: %s, parent: %s", n.KindString(), n.Parent.KindString()))
argumentInfo := getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker)
if argumentInfo != nil {
return argumentInfo
}
}
return nil
}
func getImmediatelyContainingArgumentOrContextualParameterInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, checker *checker.Checker) *argumentListInfo {
result := tryGetParameterInfo(node, sourceFile, checker)
if result == nil {
return getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker)
}
return result
}
type argumentListInfo struct {
isTypeParameterList bool
invocation *invocation
argumentsSpan core.TextRange
argumentIndex int
/** argumentCount is the *apparent* number of arguments. */
argumentCount int
}
// Returns relevant information for the argument list and the current argument if we are
// in the argument of an invocation; returns undefined otherwise.
func getImmediatelyContainingArgumentInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, c *checker.Checker) *argumentListInfo {
parent := node.Parent
if ast.IsCallOrNewExpression(parent) {
// There are 3 cases to handle:
// 1. The token introduces a list, and should begin a signature help session
// 2. The token is either not associated with a list, or ends a list, so the session should end
// 3. The token is buried inside a list, and should give signature help
//
// The following are examples of each:
//
// Case 1:
// foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session
// Case 2:
// fo#o<T, U>#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end
// Case 3:
// foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give signature help
// Find out if 'node' is an argument, a type argument, or neither
info := getArgumentOrParameterListInfo(node, sourceFile, c)
if info == nil {
return nil
}
list := info.list
argumentIndex := info.argumentIndex
argumentCount := info.argumentCount
argumentsSpan := info.argumentsSpan
isTypeParameterList := false
parentTypeArgumentList := parent.TypeArgumentList()
if parentTypeArgumentList != nil {
if parentTypeArgumentList.Pos() == list.Pos() {
isTypeParameterList = true
}
}
return &argumentListInfo{
isTypeParameterList: isTypeParameterList,
invocation: &invocation{callInvocation: &callInvocation{node: parent}},
argumentsSpan: argumentsSpan,
argumentIndex: argumentIndex,
argumentCount: argumentCount,
}
} else if isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent) {
// Check if we're actually inside the template;
// otherwise we'll fall out and return undefined.
if isInsideTemplateLiteral(node, position, sourceFile) {
return getArgumentListInfoForTemplate(parent.AsTaggedTemplateExpression(), 0, sourceFile)
}
return nil
} else if isTemplateHead(node) && parent.Parent.Kind == ast.KindTaggedTemplateExpression {
templateExpression := parent.AsTemplateExpression()
tagExpression := templateExpression.Parent.AsTaggedTemplateExpression()
argumentIndex := 1
if isInsideTemplateLiteral(node, position, sourceFile) {
argumentIndex = 0
}
return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile)
} else if ast.IsTemplateSpan(parent) && isTaggedTemplateExpression(parent.Parent.Parent) {
templateSpan := parent
tagExpression := parent.Parent.Parent
// If we're just after a template tail, don't show signature help.
if isTemplateTail(node) && !isInsideTemplateLiteral(node, position, sourceFile) {
return nil
}
spanIndex := ast.IndexOfNode(templateSpan.Parent.AsTemplateExpression().TemplateSpans.Nodes, templateSpan)
argumentIndex := getArgumentIndexForTemplatePiece(spanIndex, templateSpan, position, sourceFile)
return getArgumentListInfoForTemplate(tagExpression.AsTaggedTemplateExpression(), argumentIndex, sourceFile)
} else if ast.IsJsxOpeningLikeElement(parent) {
// Provide a signature help for JSX opening element or JSX self-closing element.
// This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems")
// i.e
// export function MainButton(props: ButtonProps, context: any): JSX.Element { ... }
// <MainButton /*signatureHelp*/
attributeSpanStart := parent.Attributes().Loc.Pos()
attributeSpanEnd := scanner.SkipTrivia(sourceFile.Text(), parent.Attributes().End())
return &argumentListInfo{
isTypeParameterList: false,
invocation: &invocation{callInvocation: &callInvocation{node: parent}},
argumentsSpan: core.NewTextRange(attributeSpanStart, attributeSpanEnd-attributeSpanStart),
argumentIndex: 0,
argumentCount: 1,
}
} else {
typeArgInfo := getPossibleTypeArgumentsInfo(node, sourceFile)
if typeArgInfo != nil {
called := typeArgInfo.called
nTypeArguments := typeArgInfo.nTypeArguments
invoc := &typeArgsInvocation{called: called.AsIdentifier()}
argumentRange := core.NewTextRange(called.Loc.Pos(), node.End())
return &argumentListInfo{
isTypeParameterList: true,
invocation: &invocation{
typeArgsInvocation: invoc,
},
argumentsSpan: argumentRange,
argumentIndex: nTypeArguments,
argumentCount: nTypeArguments + 1,
}
}
}
return nil
}
// spanIndex is either the index for a given template span.
// This does not give appropriate results for a NoSubstitutionTemplateLiteral
func getArgumentIndexForTemplatePiece(spanIndex int, node *ast.Node, position int, sourceFile *ast.SourceFile) int {
// Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1.
// There are three cases we can encounter:
// 1. We are precisely in the template literal (argIndex = 0).
// 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1).
// 3. We are directly to the right of the template literal, but because we look for the token on the left,
// not enough to put us in the substitution expression; we should consider ourselves part of
// the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1).
//
// Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # `
// ^ ^ ^ ^ ^ ^ ^ ^ ^
// Case: 1 1 3 2 1 3 2 2 1
debug.Assert(position >= node.Loc.Pos(), "Assumed 'position' could not occur before node.")
if ast.IsTemplateLiteralToken(node) {
if isInsideTemplateLiteral(node, position, sourceFile) {
return 0
}
return spanIndex + 2
}
return spanIndex + 1
}
func getAdjustedNode(node *ast.Node) *ast.Node {
switch node.Kind {
case ast.KindOpenParenToken, ast.KindCommaToken:
return node
default:
return ast.FindAncestor(node.Parent, func(n *ast.Node) bool {
if ast.IsParameter(n) {
return true
} else if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
return false
}
return false
})
}
}
type contextualSignatureLocationInfo struct {
contextualType *checker.Type
argumentIndex int
argumentCount int
argumentsSpan core.TextRange
}
func getSpreadElementCount(node *ast.SpreadElement, c *checker.Checker) int {
spreadType := c.GetTypeAtLocation(node.Expression)
if checker.IsTupleType(spreadType) {
tupleType := spreadType.Target().AsTupleType()
if tupleType == nil {
return 0
}
elementFlags := tupleType.ElementFlags()
fixedLength := tupleType.FixedLength()
if fixedLength == 0 {
return 0
}
firstOptionalIndex := core.FindIndex(elementFlags, func(f checker.ElementFlags) bool {
return (f&checker.ElementFlagsRequired == 0)
})
if firstOptionalIndex < 0 {
return fixedLength
}
return firstOptionalIndex
}
return 0
}
func getArgumentIndex(node *ast.Node, arguments *ast.NodeList, sourceFile *ast.SourceFile, c *checker.Checker) int {
return getArgumentIndexOrCount(getTokenFromNodeList(arguments, node.Parent, sourceFile), node, c)
}
func getArgumentCount(node *ast.Node, arguments *ast.NodeList, sourceFile *ast.SourceFile, c *checker.Checker) int {
return getArgumentIndexOrCount(getTokenFromNodeList(arguments, node.Parent, sourceFile), nil, c)
}
func getArgumentIndexOrCount(arguments []*ast.Node, node *ast.Node, c *checker.Checker) int {
argumentIndex := 0
skipComma := false
for _, arg := range arguments {
if node != nil && arg == node {
if !skipComma && arg.Kind == ast.KindCommaToken {
argumentIndex++
}
return argumentIndex
}
if ast.IsSpreadElement(arg) {
argumentIndex += getSpreadElementCount(arg.AsSpreadElement(), c)
skipComma = true
continue
}
if arg.Kind != ast.KindCommaToken {
argumentIndex++
skipComma = true
continue
}
if skipComma {
skipComma = false
continue
}
argumentIndex++
}
if node != nil {
return argumentIndex
}
// The argument count for a list is normally the number of non-comma children it has.
// For example, if you have "Foo(a,b)" then there will be three children of the arg
// list 'a' '<comma>' 'b'. So, in this case the arg count will be 2. However, there
// is a small subtlety. If you have "Foo(a,)", then the child list will just have
// 'a' '<comma>'. So, in the case where the last child is a comma, we increase the
// arg count by one to compensate.
argumentCount := argumentIndex
if len(arguments) > 0 && arguments[len(arguments)-1].Kind == ast.KindCommaToken {
argumentCount = argumentIndex + 1
}
return argumentCount
}
type argumentOrParameterListInfo struct {
list *ast.NodeList
argumentIndex int
argumentCount int
argumentsSpan core.TextRange
}
func getArgumentOrParameterListInfo(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentOrParameterListInfo {
info := getArgumentOrParameterListAndIndex(node, sourceFile, c)
if info == nil {
return nil
}
list := info.list
argumentIndex := info.argumentIndex
argumentCount := getArgumentCount(node, list, sourceFile, c)
argumentsSpan := getApplicableSpanForArguments(list, node, sourceFile)
return &argumentOrParameterListInfo{
list: list,
argumentIndex: argumentIndex,
argumentCount: argumentCount,
argumentsSpan: argumentsSpan,
}
}
func getApplicableSpanForArguments(argumentList *ast.NodeList, node *ast.Node, sourceFile *ast.SourceFile) core.TextRange {
// We use full start and skip trivia on the end because we want to include trivia on
// both sides. For example,
//
// foo( /*comment */ a, b, c /*comment*/ )
// | |
//
// The applicable span is from the first bar to the second bar (inclusive,
// but not including parentheses)
if argumentList == nil && node != nil {
// If the user has just opened a list, and there are no arguments.
// For example, foo( )
// | |
return core.NewTextRange(node.End(), scanner.SkipTrivia(sourceFile.Text(), node.End()))
}
applicableSpanStart := argumentList.Pos()
applicableSpanEnd := scanner.SkipTrivia(sourceFile.Text(), argumentList.End())
return core.NewTextRange(applicableSpanStart, applicableSpanEnd)
}
type argumentOrParameterListAndIndex struct {
list *ast.NodeList
argumentIndex int
}
func getArgumentOrParameterListAndIndex(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentOrParameterListAndIndex {
if node.Kind == ast.KindLessThanToken || node.Kind == ast.KindOpenParenToken {
// Find the list that starts right *after* the < or ( token.
// If the user has just opened a list, consider this item 0.
list := getChildListThatStartsWithOpenerToken(node.Parent, node)
return &argumentOrParameterListAndIndex{
list: list,
argumentIndex: 0,
}
} else {
// findListItemInfo can return undefined if we are not in parent's argument list
// or type argument list. This includes cases where the cursor is:
// - To the right of the closing parenthesis, non-substitution template, or template tail.
// - Between the type arguments and the arguments (greater than token)
// - On the target of the call (parent.func)
// - On the 'new' keyword in a 'new' expression
list := findContainingList(node, sourceFile)
if list == nil {
return nil
}
return &argumentOrParameterListAndIndex{
list: list,
// Find the index of the argument that contains the node.
argumentIndex: getArgumentIndex(node, list, sourceFile, c),
}
}
}
func getChildListThatStartsWithOpenerToken(parent *ast.Node, openerToken *ast.Node) *ast.NodeList { //!!!
if ast.IsCallExpression(parent) {
parentCallExpression := parent.AsCallExpression()
if openerToken.Kind == ast.KindLessThanToken {
return parentCallExpression.TypeArgumentList()
}
return parentCallExpression.Arguments
} else if ast.IsNewExpression(parent) {
parentNewExpression := parent.AsNewExpression()
if openerToken.Kind == ast.KindLessThanToken {
return parentNewExpression.TypeArgumentList()
}
return parentNewExpression.Arguments
}
return nil
}
func tryGetParameterInfo(startingToken *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentListInfo {
node := getAdjustedNode(startingToken)
if node == nil {
return nil
}
info := getContextualSignatureLocationInfo(node, sourceFile, c)
if info == nil {
return nil
}
// for optional function condition
nonNullableContextualType := c.GetNonNullableType(info.contextualType)
if nonNullableContextualType == nil {
return nil
}
symbol := nonNullableContextualType.Symbol()
if symbol == nil {
return nil
}
signatures := c.GetSignaturesOfType(nonNullableContextualType, checker.SignatureKindCall)
if signatures == nil || signatures[len(signatures)-1] == nil {
return nil
}
signature := signatures[len(signatures)-1]
contextualInvocation := &contextualInvocation{
signature: signature,
node: startingToken,
symbol: chooseBetterSymbol(symbol),
}
return &argumentListInfo{
isTypeParameterList: false,
invocation: &invocation{contextualInvocation: contextualInvocation},
argumentsSpan: info.argumentsSpan,
argumentIndex: info.argumentIndex,
argumentCount: info.argumentCount,
}
}
func chooseBetterSymbol(s *ast.Symbol) *ast.Symbol {
if s.Name == ast.InternalSymbolNameType {
for _, d := range s.Declarations {
if ast.IsFunctionTypeNode(d) && ast.CanHaveSymbol(d.Parent) {
return d.Parent.Symbol()
}
}
}
return s
}
func getContextualSignatureLocationInfo(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *contextualSignatureLocationInfo {
parent := node.Parent
switch parent.Kind {
case ast.KindParenthesizedExpression, ast.KindMethodDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction:
info := getArgumentOrParameterListInfo(node, sourceFile, c)
if info == nil {
return nil
}
argumentIndex := info.argumentIndex
argumentCount := info.argumentCount
argumentsSpan := info.argumentsSpan
var contextualType *checker.Type
if ast.IsMethodDeclaration(parent) {
contextualType = c.GetContextualTypeForObjectLiteralElement(parent, checker.ContextFlagsNone)
} else {
contextualType = c.GetContextualType(parent, checker.ContextFlagsNone)
}
if contextualType != nil {
return &contextualSignatureLocationInfo{
contextualType: contextualType,
argumentIndex: argumentIndex,
argumentCount: argumentCount,
argumentsSpan: argumentsSpan,
}
}
return nil
case ast.KindBinaryExpression:
highestBinary := getHighestBinary(parent.AsBinaryExpression())
contextualType := c.GetContextualType(highestBinary.AsNode(), checker.ContextFlagsNone)
argumentIndex := 0
if node.Kind != ast.KindOpenParenToken {
argumentIndex = countBinaryExpressionParameters(parent.AsBinaryExpression()) - 1
argumentCount := countBinaryExpressionParameters(highestBinary)
if contextualType != nil {
return &contextualSignatureLocationInfo{
contextualType: contextualType,
argumentIndex: argumentIndex,
argumentCount: argumentCount,
argumentsSpan: core.NewTextRange(parent.Pos(), parent.End()),
}
}
return nil
}
}
return nil
}
func getHighestBinary(b *ast.BinaryExpression) *ast.BinaryExpression {
if ast.IsBinaryExpression(b.Parent) {
return getHighestBinary(b.Parent.AsBinaryExpression())
}
return b
}
func countBinaryExpressionParameters(b *ast.BinaryExpression) int {
if ast.IsBinaryExpression(b.Left) {
return countBinaryExpressionParameters(b.Left.AsBinaryExpression()) + 1
}
return 2
}
func getTokenFromNodeList(nodeList *ast.NodeList, nodeListParent *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
if nodeList == nil || nodeListParent == nil {
return nil
}
left := nodeList.Pos()
nodeListIndex := 0
var tokens []*ast.Node
for left < nodeList.End() {
if len(nodeList.Nodes) > nodeListIndex && left == nodeList.Nodes[nodeListIndex].Pos() {
tokens = append(tokens, nodeList.Nodes[nodeListIndex])
left = nodeList.Nodes[nodeListIndex].End()
nodeListIndex++
} else {
scanner := scanner.GetScannerForSourceFile(sourceFile, left)
token := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, nodeListParent))
left = tokenEnd
}
}
return tokens
}
func containsNode(nodes []*ast.Node, node *ast.Node) bool {
for i := range nodes {
if nodes[i] == node {
return true
}
}
return false
}
func getArgumentListInfoForTemplate(tagExpression *ast.TaggedTemplateExpression, argumentIndex int, sourceFile *ast.SourceFile) *argumentListInfo {
// argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument.
argumentCount := 1
if !isNoSubstitutionTemplateLiteral(tagExpression.Template) {
argumentCount = len(tagExpression.Template.AsTemplateExpression().TemplateSpans.Nodes) + 1
}
if argumentIndex != 0 {
debug.AssertLessThan(argumentIndex, argumentCount)
}
return &argumentListInfo{
isTypeParameterList: false,
invocation: &invocation{callInvocation: &callInvocation{node: tagExpression.AsNode()}},
argumentIndex: argumentIndex,
argumentCount: argumentCount,
argumentsSpan: getApplicableRangeForTaggedTemplate(tagExpression, sourceFile),
}
}
func getApplicableRangeForTaggedTemplate(taggedTemplate *ast.TaggedTemplateExpression, sourceFile *ast.SourceFile) core.TextRange {
template := taggedTemplate.Template
applicableSpanStart := scanner.GetTokenPosOfNode(template, sourceFile, false)
applicableSpanEnd := template.End()
// We need to adjust the end position for the case where the template does not have a tail.
// Otherwise, we will not show signature help past the expression.
// For example,
//
// ` ${ 1 + 1 foo(10)
// | |
// This is because a Missing node has no width. However, what we actually want is to include trivia
// leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail.
if template.Kind == ast.KindTemplateExpression {
templateSpans := template.AsTemplateExpression().TemplateSpans
lastSpan := templateSpans.Nodes[len(templateSpans.Nodes)-1]
if lastSpan.AsTemplateSpan().Literal.End()-lastSpan.AsTemplateSpan().Literal.Pos() == 0 {
applicableSpanEnd = scanner.SkipTrivia(sourceFile.Text(), applicableSpanEnd)
}
}
return core.NewTextRange(applicableSpanStart, applicableSpanEnd-applicableSpanStart)
}