1/* @internal */ 2namespace ts.SignatureHelp { 3 const enum InvocationKind { Call, TypeArgs, Contextual } 4 interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; } 5 interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; } 6 interface ContextualInvocation { 7 readonly kind: InvocationKind.Contextual; 8 readonly signature: Signature; 9 readonly node: Node; // Just for enclosingDeclaration for printing types 10 readonly symbol: Symbol; 11 } 12 type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation; 13 14 interface ArgumentListInfo { 15 readonly isTypeParameterList: boolean; 16 readonly invocation: Invocation; 17 readonly argumentsSpan: TextSpan; 18 readonly argumentIndex: number; 19 /** argumentCount is the *apparent* number of arguments. */ 20 readonly argumentCount: number; 21 } 22 23 export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined { 24 const typeChecker = program.getTypeChecker(); 25 26 // Decide whether to show signature help 27 const startingToken = findTokenOnLeftOfPosition(sourceFile, position); 28 if (!startingToken) { 29 // We are at the beginning of the file 30 return undefined; 31 } 32 33 // Only need to be careful if the user typed a character and signature help wasn't showing. 34 const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped"; 35 36 // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. 37 if (onlyUseSyntacticOwners && (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position))) { 38 return undefined; 39 } 40 41 const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked"; 42 const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked); 43 if (!argumentInfo) return undefined; 44 45 cancellationToken.throwIfCancellationRequested(); 46 47 // Extra syntactic and semantic filtering of signature help 48 const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); 49 cancellationToken.throwIfCancellationRequested(); 50 51 if (!candidateInfo) { 52 // We didn't have any sig help items produced by the TS compiler. If this is a JS 53 // file, then see if we can figure out anything better. 54 return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; 55 } 56 57 return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => 58 candidateInfo.kind === CandidateOrTypeKind.Candidate 59 ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) 60 : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); 61 } 62 63 const enum CandidateOrTypeKind { Candidate, Type } 64 interface CandidateInfo { 65 readonly kind: CandidateOrTypeKind.Candidate; 66 readonly candidates: readonly Signature[]; 67 readonly resolvedSignature: Signature; 68 } 69 interface TypeInfo { 70 readonly kind: CandidateOrTypeKind.Type; 71 readonly symbol: Symbol; 72 } 73 74 function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined { 75 switch (invocation.kind) { 76 case InvocationKind.Call: { 77 if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) { 78 return undefined; 79 } 80 const candidates: Signature[] = []; 81 const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217 82 return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature }; 83 } 84 case InvocationKind.TypeArgs: { 85 const { called } = invocation; 86 if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) { 87 return undefined; 88 } 89 const candidates = getPossibleGenericSignatures(called, argumentCount, checker); 90 if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) }; 91 92 const symbol = checker.getSymbolAtLocation(called); 93 return symbol && { kind: CandidateOrTypeKind.Type, symbol }; 94 } 95 case InvocationKind.Contextual: 96 return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature }; 97 default: 98 return Debug.assertNever(invocation); 99 } 100 } 101 102 function isSyntacticOwner(startingToken: Node, node: CallLikeExpression, sourceFile: SourceFile): boolean { 103 if (!isCallOrNewExpression(node)) return false; 104 const invocationChildren = node.getChildren(sourceFile); 105 switch (startingToken.kind) { 106 case SyntaxKind.OpenParenToken: 107 return contains(invocationChildren, startingToken); 108 case SyntaxKind.CommaToken: { 109 const containingList = findContainingList(startingToken); 110 return !!containingList && contains(invocationChildren, containingList); 111 } 112 case SyntaxKind.LessThanToken: 113 return containsPrecedingToken(startingToken, sourceFile, node.expression); 114 default: 115 return false; 116 } 117 } 118 119 function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined { 120 if (argumentInfo.invocation.kind === InvocationKind.Contextual) return undefined; 121 // See if we can find some symbol with the call expression name that has call signatures. 122 const expression = getExpressionFromInvocation(argumentInfo.invocation); 123 const name = isPropertyAccessExpression(expression) ? expression.name.text : undefined; 124 const typeChecker = program.getTypeChecker(); 125 return name === undefined ? undefined : firstDefined(program.getSourceFiles(), sourceFile => 126 firstDefined(sourceFile.getNamedDeclarations().get(name), declaration => { 127 const type = declaration.symbol && typeChecker.getTypeOfSymbolAtLocation(declaration.symbol, declaration); 128 const callSignatures = type && type.getCallSignatures(); 129 if (callSignatures && callSignatures.length) { 130 return typeChecker.runWithCancellationToken( 131 cancellationToken, 132 typeChecker => createSignatureHelpItems( 133 callSignatures, 134 callSignatures[0], 135 argumentInfo, 136 sourceFile, 137 typeChecker, 138 /*useFullPrefix*/ true)); 139 } 140 })); 141 } 142 143 function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) { 144 const pos = startingToken.getFullStart(); 145 // There’s a possibility that `startingToken.parent` contains only `startingToken` and 146 // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that 147 // case, the preceding token we want is actually higher up the tree—almost definitely the 148 // next parent, but theoretically the situation with missing nodes might be happening on 149 // multiple nested levels. 150 let currentParent: Node | undefined = startingToken.parent; 151 while (currentParent) { 152 const precedingToken = findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true); 153 if (precedingToken) { 154 return rangeContainsRange(container, precedingToken); 155 } 156 currentParent = currentParent.parent; 157 } 158 return Debug.fail("Could not find preceding token"); 159 } 160 161 export interface ArgumentInfoForCompletions { 162 readonly invocation: CallLikeExpression; 163 readonly argumentIndex: number; 164 readonly argumentCount: number; 165 } 166 export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined { 167 const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile); 168 return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined 169 : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex }; 170 } 171 172 function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number, readonly argumentCount: number, readonly argumentsSpan: TextSpan } | undefined { 173 const info = getArgumentOrParameterListAndIndex(node, sourceFile); 174 if (!info) return undefined; 175 const { list, argumentIndex } = info; 176 177 const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node)); 178 if (argumentIndex !== 0) { 179 Debug.assertLessThan(argumentIndex, argumentCount); 180 } 181 const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); 182 return { list, argumentIndex, argumentCount, argumentsSpan }; 183 } 184 function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number } | undefined { 185 if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { 186 // Find the list that starts right *after* the < or ( token. 187 // If the user has just opened a list, consider this item 0. 188 return { list: getChildListThatStartsWithOpenerToken(node.parent, node, sourceFile), argumentIndex: 0 }; 189 } 190 else { 191 // findListItemInfo can return undefined if we are not in parent's argument list 192 // or type argument list. This includes cases where the cursor is: 193 // - To the right of the closing parenthesis, non-substitution template, or template tail. 194 // - Between the type arguments and the arguments (greater than token) 195 // - On the target of the call (parent.func) 196 // - On the 'new' keyword in a 'new' expression 197 const list = findContainingList(node); 198 return list && { list, argumentIndex: getArgumentIndex(list, node) }; 199 } 200 } 201 202 /** 203 * Returns relevant information for the argument list and the current argument if we are 204 * in the argument of an invocation; returns undefined otherwise. 205 */ 206 function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { 207 const { parent } = node; 208 if (isCallOrNewExpression(parent)) { 209 const invocation = parent; 210 211 // There are 3 cases to handle: 212 // 1. The token introduces a list, and should begin a signature help session 213 // 2. The token is either not associated with a list, or ends a list, so the session should end 214 // 3. The token is buried inside a list, and should give signature help 215 // 216 // The following are examples of each: 217 // 218 // Case 1: 219 // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session 220 // Case 2: 221 // fo#o<T, U>#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end 222 // Case 3: 223 // foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give signature help 224 // Find out if 'node' is an argument, a type argument, or neither 225 const info = getArgumentOrParameterListInfo(node, position, sourceFile); 226 if (!info) return undefined; 227 const { list, argumentIndex, argumentCount, argumentsSpan } = info; 228 const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos; 229 return { isTypeParameterList, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount }; 230 } 231 else if (isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent)) { 232 // Check if we're actually inside the template; 233 // otherwise we'll fall out and return undefined. 234 if (isInsideTemplateLiteral(node, position, sourceFile)) { 235 return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); 236 } 237 return undefined; 238 } 239 else if (isTemplateHead(node) && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { 240 const templateExpression = parent as TemplateExpression; 241 const tagExpression = templateExpression.parent as TaggedTemplateExpression; 242 Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); 243 244 const argumentIndex = isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1; 245 246 return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); 247 } 248 else if (isTemplateSpan(parent) && isTaggedTemplateExpression(parent.parent.parent)) { 249 const templateSpan = parent; 250 const tagExpression = parent.parent.parent; 251 252 // If we're just after a template tail, don't show signature help. 253 if (isTemplateTail(node) && !isInsideTemplateLiteral(node, position, sourceFile)) { 254 return undefined; 255 } 256 257 const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan); 258 const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position, sourceFile); 259 260 return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); 261 } 262 else if (isJsxOpeningLikeElement(parent)) { 263 // Provide a signature help for JSX opening element or JSX self-closing element. 264 // This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems") 265 // i.e 266 // export function MainButton(props: ButtonProps, context: any): JSX.Element { ... } 267 // <MainButton /*signatureHelp*/ 268 const attributeSpanStart = parent.attributes.pos; 269 const attributeSpanEnd = skipTrivia(sourceFile.text, parent.attributes.end, /*stopAfterLineBreak*/ false); 270 return { 271 isTypeParameterList: false, 272 invocation: { kind: InvocationKind.Call, node: parent }, 273 argumentsSpan: createTextSpan(attributeSpanStart, attributeSpanEnd - attributeSpanStart), 274 argumentIndex: 0, 275 argumentCount: 1 276 }; 277 } 278 else { 279 const typeArgInfo = getPossibleTypeArgumentsInfo(node, sourceFile); 280 if (typeArgInfo) { 281 const { called, nTypeArguments } = typeArgInfo; 282 const invocation: Invocation = { kind: InvocationKind.TypeArgs, called }; 283 const argumentsSpan = createTextSpanFromBounds(called.getStart(sourceFile), node.end); 284 return { isTypeParameterList: true, invocation, argumentsSpan, argumentIndex: nTypeArguments, argumentCount: nTypeArguments + 1 }; 285 } 286 return undefined; 287 } 288 } 289 290 function getImmediatelyContainingArgumentOrContextualParameterInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined { 291 return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile); 292 } 293 294 function getHighestBinary(b: BinaryExpression): BinaryExpression { 295 return isBinaryExpression(b.parent) ? getHighestBinary(b.parent) : b; 296 } 297 298 function countBinaryExpressionParameters(b: BinaryExpression): number { 299 return isBinaryExpression(b.left) ? countBinaryExpressionParameters(b.left) + 1 : 2; 300 } 301 302 function tryGetParameterInfo(startingToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined { 303 const info = getContextualSignatureLocationInfo(startingToken, sourceFile, position, checker); 304 if (!info) return undefined; 305 const { contextualType, argumentIndex, argumentCount, argumentsSpan } = info; 306 307 // for optional function condition. 308 const nonNullableContextualType = contextualType.getNonNullableType(); 309 310 const symbol = nonNullableContextualType.symbol; 311 if (symbol === undefined) return undefined; 312 313 const signature = lastOrUndefined(nonNullableContextualType.getCallSignatures()); 314 if (signature === undefined) return undefined; 315 316 const invocation: ContextualInvocation = { kind: InvocationKind.Contextual, signature, node: startingToken, symbol: chooseBetterSymbol(symbol) }; 317 return { isTypeParameterList: false, invocation, argumentsSpan, argumentIndex, argumentCount }; 318 } 319 320 interface ContextualSignatureLocationInfo { readonly contextualType: Type; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; } 321 function getContextualSignatureLocationInfo(startingToken: Node, sourceFile: SourceFile, position: number, checker: TypeChecker): ContextualSignatureLocationInfo | undefined { 322 if (startingToken.kind !== SyntaxKind.OpenParenToken && startingToken.kind !== SyntaxKind.CommaToken) return undefined; 323 const { parent } = startingToken; 324 switch (parent.kind) { 325 case SyntaxKind.ParenthesizedExpression: 326 case SyntaxKind.MethodDeclaration: 327 case SyntaxKind.FunctionExpression: 328 case SyntaxKind.ArrowFunction: 329 const info = getArgumentOrParameterListInfo(startingToken, position, sourceFile); 330 if (!info) return undefined; 331 const { argumentIndex, argumentCount, argumentsSpan } = info; 332 const contextualType = isMethodDeclaration(parent) ? checker.getContextualTypeForObjectLiteralElement(parent) : checker.getContextualType(parent as ParenthesizedExpression | FunctionExpression | ArrowFunction); 333 return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan }; 334 case SyntaxKind.BinaryExpression: { 335 const highestBinary = getHighestBinary(parent as BinaryExpression); 336 const contextualType = checker.getContextualType(highestBinary); 337 const argumentIndex = startingToken.kind === SyntaxKind.OpenParenToken ? 0 : countBinaryExpressionParameters(parent as BinaryExpression) - 1; 338 const argumentCount = countBinaryExpressionParameters(highestBinary); 339 return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan: createTextSpanFromNode(parent) }; 340 } 341 default: 342 return undefined; 343 } 344 } 345 346 // The type of a function type node has a symbol at that node, but it's better to use the symbol for a parameter or type alias. 347 function chooseBetterSymbol(s: Symbol): Symbol { 348 return s.name === InternalSymbolName.Type 349 ? firstDefined(s.declarations, d => isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s 350 : s; 351 } 352 353 function getArgumentIndex(argumentsList: Node, node: Node) { 354 // The list we got back can include commas. In the presence of errors it may 355 // also just have nodes without commas. For example "Foo(a b c)" will have 3 356 // args without commas. We want to find what index we're at. So we count 357 // forward until we hit ourselves, only incrementing the index if it isn't a 358 // comma. 359 // 360 // Note: the subtlety around trailing commas (in getArgumentCount) does not apply 361 // here. That's because we're only walking forward until we hit the node we're 362 // on. In that case, even if we're after the trailing comma, we'll still see 363 // that trailing comma in the list, and we'll have generated the appropriate 364 // arg index. 365 let argumentIndex = 0; 366 for (const child of argumentsList.getChildren()) { 367 if (child === node) { 368 break; 369 } 370 if (child.kind !== SyntaxKind.CommaToken) { 371 argumentIndex++; 372 } 373 } 374 375 return argumentIndex; 376 } 377 378 function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) { 379 // The argument count for a list is normally the number of non-comma children it has. 380 // For example, if you have "Foo(a,b)" then there will be three children of the arg 381 // list 'a' '<comma>' 'b'. So, in this case the arg count will be 2. However, there 382 // is a small subtlety. If you have "Foo(a,)", then the child list will just have 383 // 'a' '<comma>'. So, in the case where the last child is a comma, we increase the 384 // arg count by one to compensate. 385 // 386 // Note: this subtlety only applies to the last comma. If you had "Foo(a,," then 387 // we'll have: 'a' '<comma>' '<missing>' 388 // That will give us 2 non-commas. We then add one for the last comma, giving us an 389 // arg count of 3. 390 const listChildren = argumentsList.getChildren(); 391 392 let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); 393 if (!ignoreTrailingComma && listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) { 394 argumentCount++; 395 } 396 return argumentCount; 397 } 398 399 // spanIndex is either the index for a given template span. 400 // This does not give appropriate results for a NoSubstitutionTemplateLiteral 401 function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number, sourceFile: SourceFile): number { 402 // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1. 403 // There are three cases we can encounter: 404 // 1. We are precisely in the template literal (argIndex = 0). 405 // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1). 406 // 3. We are directly to the right of the template literal, but because we look for the token on the left, 407 // not enough to put us in the substitution expression; we should consider ourselves part of 408 // the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1). 409 // 410 /* eslint-disable local/no-double-space */ 411 // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` 412 // ^ ^ ^ ^ ^ ^ ^ ^ ^ 413 // Case: 1 1 3 2 1 3 2 2 1 414 /* eslint-enable local/no-double-space */ 415 Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); 416 if (isTemplateLiteralToken(node)) { 417 if (isInsideTemplateLiteral(node, position, sourceFile)) { 418 return 0; 419 } 420 return spanIndex + 2; 421 } 422 return spanIndex + 1; 423 } 424 425 function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo { 426 // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. 427 const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; 428 if (argumentIndex !== 0) { 429 Debug.assertLessThan(argumentIndex, argumentCount); 430 } 431 return { 432 isTypeParameterList: false, 433 invocation: { kind: InvocationKind.Call, node: tagExpression }, 434 argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), 435 argumentIndex, 436 argumentCount 437 }; 438 } 439 440 function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan { 441 // We use full start and skip trivia on the end because we want to include trivia on 442 // both sides. For example, 443 // 444 // foo( /*comment */ a, b, c /*comment*/ ) 445 // | | 446 // 447 // The applicable span is from the first bar to the second bar (inclusive, 448 // but not including parentheses) 449 const applicableSpanStart = argumentsList.getFullStart(); 450 const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); 451 return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); 452 } 453 454 function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression, sourceFile: SourceFile): TextSpan { 455 const template = taggedTemplate.template; 456 const applicableSpanStart = template.getStart(); 457 let applicableSpanEnd = template.getEnd(); 458 459 // We need to adjust the end position for the case where the template does not have a tail. 460 // Otherwise, we will not show signature help past the expression. 461 // For example, 462 // 463 // ` ${ 1 + 1 foo(10) 464 // | | 465 // This is because a Missing node has no width. However, what we actually want is to include trivia 466 // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. 467 if (template.kind === SyntaxKind.TemplateExpression) { 468 const lastSpan = last(template.templateSpans); 469 if (lastSpan.literal.getFullWidth() === 0) { 470 applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); 471 } 472 } 473 474 return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); 475 } 476 477 function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined { 478 for (let n = node; !isSourceFile(n) && (isManuallyInvoked || !isBlock(n)); n = n.parent) { 479 // If the node is not a subspan of its parent, this is a big problem. 480 // There have been crashes that might be caused by this violation. 481 Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.formatSyntaxKind(n.kind)}, parent: ${Debug.formatSyntaxKind(n.parent.kind)}`); 482 const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker); 483 if (argumentInfo) { 484 return argumentInfo; 485 } 486 } 487 return undefined; 488 } 489 490 function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { 491 const children = parent.getChildren(sourceFile); 492 const indexOfOpenerToken = children.indexOf(openerToken); 493 Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); 494 return children[indexOfOpenerToken + 1]; 495 } 496 497 function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): Expression { 498 return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called; 499 } 500 501 function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node { 502 return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; 503 } 504 505 const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; 506 function createSignatureHelpItems( 507 candidates: readonly Signature[], 508 resolvedSignature: Signature, 509 { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, 510 sourceFile: SourceFile, 511 typeChecker: TypeChecker, 512 useFullPrefix?: boolean, 513 ): SignatureHelpItems { 514 const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); 515 const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : (typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)) || useFullPrefix && resolvedSignature.declaration?.symbol); 516 const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, useFullPrefix ? sourceFile : undefined, /*meaning*/ undefined) : emptyArray; 517 const items = map(candidates, candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); 518 519 if (argumentIndex !== 0) { 520 Debug.assertLessThan(argumentIndex, argumentCount); 521 } 522 let selectedItemIndex = 0; 523 let itemsSeen = 0; 524 for (let i = 0; i < items.length; i++) { 525 const item = items[i]; 526 if (candidates[i] === resolvedSignature) { 527 selectedItemIndex = itemsSeen; 528 if (item.length > 1) { 529 // check to see if any items in the list better match than the first one, as the checker isn't filtering the nested lists 530 // (those come from tuple parameter expansion) 531 let count = 0; 532 for (const i of item) { 533 if (i.isVariadic || i.parameters.length >= argumentCount) { 534 selectedItemIndex = itemsSeen + count; 535 break; 536 } 537 count++; 538 } 539 } 540 } 541 itemsSeen += item.length; 542 } 543 544 Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. 545 const help = { items: flatMapToMutable(items, identity), applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; 546 const selected = help.items[selectedItemIndex]; 547 if (selected.isVariadic) { 548 const firstRest = findIndex(selected.parameters, p => !!p.isRest); 549 if (-1 < firstRest && firstRest < selected.parameters.length - 1) { 550 // We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL 551 help.argumentIndex = selected.parameters.length; 552 } 553 else { 554 help.argumentIndex = Math.min(help.argumentIndex, selected.parameters.length - 1); 555 } 556 } 557 return help; 558 } 559 560 function createTypeHelpItems( 561 symbol: Symbol, 562 { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, 563 sourceFile: SourceFile, 564 checker: TypeChecker 565 ): SignatureHelpItems | undefined { 566 const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); 567 if (!typeParameters) return undefined; 568 const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)]; 569 return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount }; 570 } 571 572 function getTypeHelpItem(symbol: Symbol, typeParameters: readonly TypeParameter[], checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { 573 const typeSymbolDisplay = symbolToDisplayParts(checker, symbol); 574 575 const printer = createPrinter({ removeComments: true }); 576 const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); 577 578 const documentation = symbol.getDocumentationComment(checker); 579 const tags = symbol.getJsDocTags(checker); 580 const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)]; 581 return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; 582 } 583 584 const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()]; 585 586 function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem[] { 587 const infos = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); 588 return map(infos, ({ isVariadic, parameters, prefix, suffix }) => { 589 const prefixDisplayParts = [...callTargetDisplayParts, ...prefix]; 590 const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)]; 591 const documentation = candidateSignature.getDocumentationComment(checker); 592 const tags = candidateSignature.getJsDocTags(); 593 return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags }; 594 }); 595 } 596 597 function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] { 598 return mapToDisplayParts(writer => { 599 writer.writePunctuation(":"); 600 writer.writeSpace(" "); 601 const predicate = checker.getTypePredicateOfSignature(candidateSignature); 602 if (predicate) { 603 checker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer); 604 } 605 else { 606 checker.writeType(checker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer); 607 } 608 }); 609 } 610 611 interface SignatureHelpItemInfo { readonly isVariadic: boolean; readonly parameters: SignatureHelpParameter[]; readonly prefix: readonly SymbolDisplayPart[]; readonly suffix: readonly SymbolDisplayPart[]; } 612 613 function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] { 614 const typeParameters = (candidateSignature.target || candidateSignature).typeParameters; 615 const printer = createPrinter({ removeComments: true }); 616 const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); 617 const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; 618 619 return checker.getExpandedParameters(candidateSignature).map(paramList => { 620 const params = factory.createNodeArray([...thisParameter, ...map(paramList, param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); 621 const parameterParts = mapToDisplayParts(writer => { 622 printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer); 623 }); 624 return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] }; 625 }); 626 } 627 628 function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] { 629 const printer = createPrinter({ removeComments: true }); 630 const typeParameterParts = mapToDisplayParts(writer => { 631 if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { 632 const args = factory.createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)); 633 printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer); 634 } 635 }); 636 const lists = checker.getExpandedParameters(candidateSignature); 637 const isVariadic: (parameterList: readonly Symbol[]) => boolean = 638 !checker.hasEffectiveRestParameter(candidateSignature) ? _ => false 639 : lists.length === 1 ? _ => true 640 : pList => !!(pList.length && (pList[pList.length - 1] as TransientSymbol).checkFlags & CheckFlags.RestParameter); 641 return lists.map(parameterList => ({ 642 isVariadic: isVariadic(parameterList), 643 parameters: parameterList.map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)), 644 prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)], 645 suffix: [punctuationPart(SyntaxKind.CloseParenToken)] 646 })); 647 } 648 649 function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { 650 const displayParts = mapToDisplayParts(writer => { 651 const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; 652 printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); 653 }); 654 const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ParameterDeclaration); 655 const isRest = !!((parameter as TransientSymbol).checkFlags & CheckFlags.RestParameter); 656 return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest }; 657 } 658 659 function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { 660 const displayParts = mapToDisplayParts(writer => { 661 const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; 662 printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); 663 }); 664 return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false }; 665 } 666} 667