/// module ts.SignatureHelp { // A partially written generic type expression is not guaranteed to have the correct syntax tree. the expression could be parsed as less than/greater than expression or a comma expression // or some other combination depending on what the user has typed so far. For the purposes of signature help we need to consider any location after "<" as a possible generic type reference. // To do this, the method will back parse the expression starting at the position required. it will try to parse the current expression as a generic type expression, if it did succeed it // will return the generic identifier that started the expression (e.g. "foo" in "foo; argumentIndex?: number; argumentCount: number; } export function getSignatureHelpItems(sourceFile: SourceFile, position: number, typeInfoResolver: TypeChecker, cancellationToken: CancellationTokenObject): SignatureHelpItems { // Decide whether to show signature help var startingToken = findTokenOnLeftOfPosition(sourceFile, position); if (!startingToken) { // We are at the beginning of the file return undefined; } var argumentInfo = getContainingArgumentInfo(startingToken); cancellationToken.throwIfCancellationRequested(); // Semantic filtering of signature help if (!argumentInfo) { return undefined; } var call = argumentInfo.invocation; var candidates = []; var resolvedSignature = typeInfoResolver.getResolvedSignature(call, candidates); cancellationToken.throwIfCancellationRequested(); if (!candidates.length) { return undefined; } return createSignatureHelpItems(candidates, resolvedSignature, argumentInfo); /** * If node is an argument, returns its index in the argument list. * If not, returns -1. */ function getImmediatelyContainingArgumentInfo(node: Node): ArgumentListInfo { var callLikeExpr: CallLikeExpression; if (node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.NewExpression) { var callExpression = node.parent; // There are 3 cases to handle: // 1. The token introduces a list, and should begin a sig 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 sig help // // The following are examples of each: // // Case 1: // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a sig help session // Case 2: // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end // Case 3: // foo(a#, #b#) -> The token is buried inside a list, and should give sig help // Find out if 'node' is an argument, a type argument, or neither if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { // Find the list that starts right *after* the < or ( token. // If the user has just opened a list, consider this item 0. var list = getChildListThatStartsWithOpenerToken(callExpression, node, sourceFile); var isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; Debug.assert(list !== undefined); return { kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, invocation: callExpression, arguments: list, argumentIndex: 0, argumentCount: getCommaBasedArgCount(list) }; } // 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 paren, 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 var listItemInfo = findListItemInfo(node); if (listItemInfo) { var list = listItemInfo.list; var isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; // The listItemIndex we got back includes commas. Our goal is to return the index of the proper // item (not including commas). Here are some examples: // 1. foo(a, b, c #) -> the listItemIndex is 4, we want to return 2 // 2. foo(a, b, # c) -> listItemIndex is 3, we want to return 2 // 3. foo(#a) -> listItemIndex is 0, we want to return 0 // // In general, we want to subtract the number of commas before the current index. // But if we are on a comma, we also want to pretend we are on the argument *following* // the comma. That amounts to taking the ceiling of half the index. var argumentIndex = (listItemInfo.listItemIndex + 1) >> 1; return { kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, invocation: callExpression, arguments: list, argumentIndex: argumentIndex, argumentCount: getCommaBasedArgCount(list) }; } } else if (node.parent.kind === SyntaxKind.TemplateExpression && node.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { // TODO (drosen): Can't get sig help to trigger within the template head itself; only when directly to the right. // Also, need to ensure that this works on NoSubstitutionTemplateExpressions when unterminated. Debug.assert(node.kind === SyntaxKind.TemplateHead, "Expected 'TemplateHead' as token."); var templateExpression = node.parent; var tagExpression = templateExpression.parent; // argumentIndex is 1 to adjust for the TemplateStringsArray return getArgumentListInfoForTemplate(tagExpression, templateExpression.templateSpans, /*argumentIndex*/ 1); } else if (node.parent.kind === SyntaxKind.TemplateSpan && node.parent.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { var templateSpan = node.parent; var templateExpression = templateSpan.parent; var tagExpression = templateExpression.parent; Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); // We need to account for the TemplateStringsArray, so we add at least 1. // Then, if we're on the template literal, we want to jump to the next argument, var spanIndex = templateExpression.templateSpans.indexOf(templateSpan); var adjustedIndex = isTemplateLiteralKind(node.kind) ? spanIndex + 2 : spanIndex + 1 return getArgumentListInfoForTemplate(tagExpression, templateExpression.templateSpans, adjustedIndex); } return undefined; } function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, spans: NodeArray, argumentIndex: number): ArgumentListInfo { var argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral ? 1 : (tagExpression.template).templateSpans.length + 1; return { kind: ArgumentListKind.TaggedTemplateArguments, invocation: tagExpression, arguments: spans, argumentIndex: argumentIndex, argumentCount: argumentCount }; } function getCommaBasedArgCount(argumentsList: Node) { // The number of arguments is the number of commas plus one, unless the list // is completely empty, in which case there are 0 arguments. return argumentsList.getChildCount() === 0 ? 0 : 1 + countWhere(argumentsList.getChildren(), arg => arg.kind === SyntaxKind.CommaToken); } function getContainingArgumentInfo(node: Node): ArgumentListInfo { for (var n = node; n.kind !== SyntaxKind.SourceFile; n = n.parent) { if (n.kind === SyntaxKind.FunctionBlock) { return undefined; } // 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. if (n.pos < n.parent.pos || n.end > n.parent.end) { Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind); } var argumentInfo = getImmediatelyContainingArgumentInfo(n); if (argumentInfo) { return argumentInfo; } // TODO: Handle generic call with incomplete syntax } return undefined; } function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { var children = parent.getChildren(sourceFile); var indexOfOpenerToken = children.indexOf(openerToken); Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); return children[indexOfOpenerToken + 1]; } /** * The selectedItemIndex could be negative for several reasons. * 1. There are too many arguments for all of the overloads * 2. None of the overloads were type compatible * The solution here is to try to pick the best overload by picking * either the first one that has an appropriate number of parameters, * or the one with the most parameters. */ function selectBestInvalidOverloadIndex(candidates: Signature[], argumentCount: number): number { var maxParamsSignatureIndex = -1; var maxParams = -1; for (var i = 0; i < candidates.length; i++) { var candidate = candidates[i]; if (candidate.hasRestParameter || candidate.parameters.length >= argumentCount) { return i; } if (candidate.parameters.length > maxParams) { maxParams = candidate.parameters.length; maxParamsSignatureIndex = i; } } return maxParamsSignatureIndex; } function createSignatureHelpItems(candidates: Signature[], bestSignature: Signature, argumentListInfo: ArgumentListInfo): SignatureHelpItems { var argumentsList = argumentListInfo.arguments; var isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments; var invocation = argumentListInfo.invocation; var invokerNode = getCallLikeInvoker(invocation) var invokerSymbol = typeInfoResolver.getSymbolInfo(invokerNode); var invokerDisplayParts = invokerSymbol && symbolToDisplayParts(typeInfoResolver, invokerSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined); var items: SignatureHelpItem[] = map(candidates, candidateSignature => { var signatureHelpParameters: SignatureHelpParameter[]; var prefixParts: SymbolDisplayPart[] = []; var suffixParts: SymbolDisplayPart[] = []; if (invokerDisplayParts) { prefixParts.push.apply(prefixParts, invokerDisplayParts); } if (isTypeParameterList) { prefixParts.push(punctuationPart(SyntaxKind.LessThanToken)); var typeParameters = candidateSignature.typeParameters; signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray; suffixParts.push(punctuationPart(SyntaxKind.GreaterThanToken)); var parameterParts = mapToDisplayParts(writer => typeInfoResolver.getSymbolDisplayBuilder().buildDisplayForParametersAndDelimiters(candidateSignature.parameters, writer, invocation)); suffixParts.push.apply(suffixParts, parameterParts); } else { var typeParameterParts = mapToDisplayParts(writer => typeInfoResolver.getSymbolDisplayBuilder().buildDisplayForTypeParametersAndDelimiters(candidateSignature.typeParameters, writer, invocation)); prefixParts.push.apply(prefixParts, typeParameterParts); prefixParts.push(punctuationPart(SyntaxKind.OpenParenToken)); var parameters = candidateSignature.parameters; signatureHelpParameters = parameters.length > 0 ? map(parameters, createSignatureHelpParameterForParameter) : emptyArray; suffixParts.push(punctuationPart(SyntaxKind.CloseParenToken)); } var returnTypeParts = mapToDisplayParts(writer => typeInfoResolver.getSymbolDisplayBuilder().buildReturnTypeDisplay(candidateSignature, writer, invocation)); suffixParts.push.apply(suffixParts, returnTypeParts); return { isVariadic: candidateSignature.hasRestParameter, prefixDisplayParts: prefixParts, suffixDisplayParts: suffixParts, separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()], parameters: signatureHelpParameters, documentation: candidateSignature.getDocumentationComment() }; }); // 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) var applicableSpanStart = argumentsList.pos; var applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.end, /*stopAfterLineBreak*/ false); var applicableSpan = new TypeScript.TextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); var argumentIndex = argumentListInfo.argumentIndex; // argumentCount is the *apparent* number of arguments. var argumentCount = argumentListInfo.argumentCount; var selectedItemIndex = candidates.indexOf(bestSignature); if (selectedItemIndex < 0) { selectedItemIndex = selectBestInvalidOverloadIndex(candidates, argumentCount); } return { items: items, applicableSpan: applicableSpan, selectedItemIndex: selectedItemIndex, argumentIndex: argumentIndex, argumentCount: argumentCount }; function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter { var displayParts = mapToDisplayParts(writer => typeInfoResolver.getSymbolDisplayBuilder().buildParameterDisplay(parameter, writer, invocation)); var isOptional = !!(parameter.valueDeclaration.flags & NodeFlags.QuestionMark); return { name: parameter.name, documentation: parameter.getDocumentationComment(), displayParts: displayParts, isOptional: isOptional }; } function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter { var displayParts = mapToDisplayParts(writer => typeInfoResolver.getSymbolDisplayBuilder().buildTypeParameterDisplay(typeParameter, writer, invocation)); return { name: typeParameter.symbol.name, documentation: emptyArray, displayParts: displayParts, isOptional: false }; } } } }