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