diff --git a/src/services/completions.ts b/src/services/completions.ts index e70398067628a..56833298a644a 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -422,7 +422,7 @@ namespace ts.Completions { case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false) && !isImportCall(node.parent)) { - const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile); + const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile); // Get string literal completions from specialized signatures of the target // i.e. declare function f(a: 'A'); // f("/*completion position*/") @@ -452,7 +452,7 @@ namespace ts.Completions { } } - function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentListInfo, checker: TypeChecker): StringLiteralCompletionsFromTypes { + function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes { let isNewIdentifier = false; const uniques = createMap(); @@ -460,7 +460,7 @@ namespace ts.Completions { checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); const types = flatMap(candidates, candidate => { if (!candidate.hasRestParameter && argumentInfo.argumentCount > candidate.parameters.length) return; - const type = checker.getParameterType(candidate, argumentInfo.argumentIndex!); // TODO: GH#18217 + const type = checker.getParameterType(candidate, argumentInfo.argumentIndex); isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); return getStringLiteralTypes(type, checker, uniques); }); @@ -720,10 +720,10 @@ namespace ts.Completions { case SyntaxKind.OpenBraceToken: return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: - const argInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(currentToken, position, sourceFile); + const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile); return argInfo // At `,`, treat this as the next argument after the comma. - ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex! + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0)) // TODO: GH#18217 + ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0)) : isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) // completion at `x ===/**/` should be for the right side ? checker.getTypeAtLocation(parent.left) diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index 61330757f0836..5fcf3e4866bb0 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -1,17 +1,20 @@ /* @internal */ namespace ts.SignatureHelp { - export const enum ArgumentListKind { + const enum ArgumentListKind { TypeArguments, CallArguments, TaggedTemplateArguments, JSXAttributesArguments } - export interface ArgumentListInfo { + const enum InvocationKind { Call, TypeArgs } + type Invocation = { kind: InvocationKind.Call, node: CallLikeExpression } | { kind: InvocationKind.TypeArgs, called: Expression }; + + interface ArgumentListInfo { kind: ArgumentListKind; - invocation: CallLikeExpression; + invocation: Invocation; argumentsSpan: TextSpan; - argumentIndex?: number; + argumentIndex: number; /** argumentCount is the *apparent* number of arguments. */ argumentCount: number; } @@ -32,32 +35,39 @@ namespace ts.SignatureHelp { cancellationToken.throwIfCancellationRequested(); // Semantic filtering of signature help - const call = argumentInfo.invocation; - const candidates: Signature[] = []; - const resolvedSignature = typeChecker.getResolvedSignature(call, candidates, argumentInfo.argumentCount); + const candidateInfo = getCandidateInfo(argumentInfo, typeChecker); cancellationToken.throwIfCancellationRequested(); - if (!candidates.length) { + if (!candidateInfo) { // 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. if (isSourceFileJavaScript(sourceFile)) { return createJavaScriptSignatureHelpItems(argumentInfo, program, cancellationToken); } - return undefined; } - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidates, resolvedSignature!, argumentInfo, typeChecker)); + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)); } - function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined { - if (argumentInfo.invocation.kind !== SyntaxKind.CallExpression) { - return undefined; + function getCandidateInfo(argumentInfo: ArgumentListInfo, checker: TypeChecker): { readonly candidates: ReadonlyArray, readonly resolvedSignature: Signature } | undefined { + const { invocation } = argumentInfo; + if (invocation.kind === InvocationKind.Call) { + const candidates: Signature[] = []; + const resolvedSignature = checker.getResolvedSignature(invocation.node, candidates, argumentInfo.argumentCount)!; // TODO: GH#18217 + return candidates.length === 0 ? undefined : { candidates, resolvedSignature }; } + else { + const type = checker.getTypeAtLocation(invocation.called)!; // TODO: GH#18217 + const signatures = isNewExpression(invocation.called.parent) ? type.getConstructSignatures() : type.getCallSignatures(); + const candidates = signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= argumentInfo.argumentCount); + return candidates.length === 0 ? undefined : { candidates, resolvedSignature: first(candidates) }; + } + } + function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined { // See if we can find some symbol with the call expression name that has call signatures. - const callExpression = argumentInfo.invocation; - const expression = callExpression.expression; + const expression = getExpressionFromInvocation(argumentInfo.invocation); const name = isIdentifier(expression) ? expression : isPropertyAccessExpression(expression) ? expression.name : undefined; if (!name || !name.escapedText) { return undefined; @@ -76,7 +86,7 @@ namespace ts.SignatureHelp { if (type) { const callSignatures = type.getCallSignatures(); if (callSignatures && callSignatures.length) { - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker)); + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, sourceFile, typeChecker)); } } } @@ -85,13 +95,25 @@ namespace ts.SignatureHelp { } } + export interface ArgumentInfoForCompletions { + readonly invocation: CallLikeExpression; + readonly argumentIndex: number; + readonly argumentCount: number; + } + export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined { + const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile); + return !info || info.kind === ArgumentListKind.TypeArguments || info.invocation.kind === InvocationKind.TypeArgs ? undefined + : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex }; + } + /** * Returns relevant information for the argument list and the current argument if we are * in the argument of an invocation; returns undefined otherwise. */ - export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { + function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { const { parent } = node; if (isCallOrNewExpression(parent)) { + const invocation = parent; let list: Node | undefined; let argumentIndex: number; @@ -134,56 +156,63 @@ namespace ts.SignatureHelp { Debug.assertLessThan(argumentIndex, argumentCount); } const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); - return { kind, invocation: parent, argumentsSpan, argumentIndex, argumentCount }; + return { kind, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount }; } - else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && parent.kind === SyntaxKind.TaggedTemplateExpression) { + 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)) { - return getArgumentListInfoForTemplate(node.parent, /*argumentIndex*/ 0, sourceFile); + if (isInsideTemplateLiteral(node, position)) { + return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); } } - else if (node.kind === SyntaxKind.TemplateHead && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - const templateExpression = node.parent; + else if (isTemplateHead(node) && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { + const templateExpression = parent; const tagExpression = templateExpression.parent; Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - const argumentIndex = isInsideTemplateLiteral(node, position) ? 0 : 1; + const argumentIndex = isInsideTemplateLiteral(node, position) ? 0 : 1; return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); } - else if (parent.kind === SyntaxKind.TemplateSpan && parent.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - const templateSpan = node.parent; - const templateExpression = templateSpan.parent; - const tagExpression = templateExpression.parent; - Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); + else if (isTemplateSpan(parent) && isTaggedTemplateExpression(parent.parent.parent)) { + const templateSpan = parent; + const tagExpression = parent.parent.parent; // If we're just after a template tail, don't show signature help. if (node.kind === SyntaxKind.TemplateTail && !isInsideTemplateLiteral(node, position)) { return undefined; } - const spanIndex = templateExpression.templateSpans.indexOf(templateSpan); + const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan); const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position); return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); } - else if (node.parent && isJsxOpeningLikeElement(node.parent)) { + else if (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 { ... } // n.parent.end) { - Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind); - } - + Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.showSyntaxKind(n)}, parent: ${Debug.showSyntaxKind(n.parent)}`); const argumentInfo = getImmediatelyContainingArgumentInfo(n, position, sourceFile); if (argumentInfo) { return argumentInfo; } - - - // TODO: Handle generic call with incomplete syntax } return undefined; } @@ -343,16 +362,20 @@ namespace ts.SignatureHelp { return children[indexOfOpenerToken + 1]; } + function getExpressionFromInvocation(invocation: Invocation): Expression { + return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called; + } + const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors; - function createSignatureHelpItems(candidates: Signature[], resolvedSignature: Signature, argumentListInfo: ArgumentListInfo, typeChecker: TypeChecker): SignatureHelpItems { + function createSignatureHelpItems(candidates: ReadonlyArray, resolvedSignature: Signature, argumentListInfo: ArgumentListInfo, sourceFile: SourceFile, typeChecker: TypeChecker): SignatureHelpItems { const { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex } = argumentListInfo; const isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments; - const callTarget = getInvokedExpression(invocation); - const callTargetSymbol = typeChecker.getSymbolAtLocation(callTarget); + const enclosingDeclaration = invocation.kind === InvocationKind.Call ? invocation.node : invocation.called; + const callTargetSymbol = typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)); const callTargetDisplayParts = callTargetSymbol && symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined); const printer = createPrinter({ removeComments: true }); - const items: SignatureHelpItem[] = map(candidates, candidateSignature => { + const items = candidates.map(candidateSignature => { let signatureHelpParameters: SignatureHelpParameter[]; const prefixDisplayParts: SymbolDisplayPart[] = []; const suffixDisplayParts: SymbolDisplayPart[] = []; @@ -369,9 +392,9 @@ namespace ts.SignatureHelp { signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray; suffixDisplayParts.push(punctuationPart(SyntaxKind.GreaterThanToken)); const parameterParts = mapToDisplayParts(writer => { - const thisParameter = candidateSignature.thisParameter ? [typeChecker.symbolToParameterDeclaration(candidateSignature.thisParameter, invocation, signatureHelpNodeBuilderFlags)!] : []; - const params = createNodeArray([...thisParameter, ...map(candidateSignature.parameters, param => typeChecker.symbolToParameterDeclaration(param, invocation, signatureHelpNodeBuilderFlags)!)]); - printer.writeList(ListFormat.CallExpressionArguments, params, getSourceFileOfNode(getParseTreeNode(invocation)), writer); + const thisParameter = candidateSignature.thisParameter ? [typeChecker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; + const params = createNodeArray([...thisParameter, ...candidateSignature.parameters.map(param => typeChecker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); + printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer); }); addRange(suffixDisplayParts, parameterParts); } @@ -379,8 +402,8 @@ namespace ts.SignatureHelp { isVariadic = candidateSignature.hasRestParameter; const typeParameterParts = mapToDisplayParts(writer => { if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { - const args = createNodeArray(map(candidateSignature.typeParameters, p => typeChecker.typeParameterToDeclaration(p, invocation)!)); - printer.writeList(ListFormat.TypeParameters, args, getSourceFileOfNode(getParseTreeNode(invocation)), writer); + const args = createNodeArray(candidateSignature.typeParameters.map(p => typeChecker.typeParameterToDeclaration(p, enclosingDeclaration)!)); + printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer); } }); addRange(prefixDisplayParts, typeParameterParts); @@ -395,10 +418,10 @@ namespace ts.SignatureHelp { writer.writeSpace(" "); const predicate = typeChecker.getTypePredicateOfSignature(candidateSignature); if (predicate) { - typeChecker.writeTypePredicate(predicate, invocation, /*flags*/ undefined, writer); + typeChecker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer); } else { - typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), invocation, /*flags*/ undefined, writer); + typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer); } }); addRange(suffixDisplayParts, returnTypeParts); @@ -415,18 +438,18 @@ namespace ts.SignatureHelp { }); if (argumentIndex !== 0) { - Debug.assertLessThan(argumentIndex!, argumentCount); // TODO: GH#18217 + Debug.assertLessThan(argumentIndex, argumentCount); } const selectedItemIndex = candidates.indexOf(resolvedSignature); Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. - return { items, applicableSpan, selectedItemIndex, argumentIndex: argumentIndex!, argumentCount }; // TODO: GH#18217 + return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter { const displayParts = mapToDisplayParts(writer => { - const param = typeChecker.symbolToParameterDeclaration(parameter, invocation, signatureHelpNodeBuilderFlags)!; - printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer); + const param = typeChecker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; + printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); }); return { @@ -439,8 +462,8 @@ namespace ts.SignatureHelp { function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter { const displayParts = mapToDisplayParts(writer => { - const param = typeChecker.typeParameterToDeclaration(typeParameter, invocation)!; - printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer); + const param = typeChecker.typeParameterToDeclaration(typeParameter, enclosingDeclaration)!; + printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); }); return { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 54f88646071dc..e6ebdb7844768 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -920,7 +920,11 @@ namespace ts { } } - export function isPossiblyTypeArgumentPosition(tokenIn: Node, sourceFile: SourceFile): boolean { + export interface PossibleTypeArgumentInfo { + readonly called: Identifier; + readonly nTypeArguments: number; + } + export function isPossiblyTypeArgumentPosition(tokenIn: Node, sourceFile: SourceFile): PossibleTypeArgumentInfo | undefined { let token: Node | undefined = tokenIn; // This function determines if the node could be type argument position // Since during editing, when type argument list is not complete, @@ -928,15 +932,15 @@ namespace ts { // scanning of the previous identifier followed by "<" before current node would give us better result // Note that we also balance out the already provided type arguments, arrays, object literals while doing so let remainingLessThanTokens = 0; + let nTypeArguments = 0; while (token) { switch (token.kind) { case SyntaxKind.LessThanToken: // Found the beginning of the generic argument expression token = findPrecedingToken(token.getFullStart(), sourceFile); - if (!token) return false; - const tokenIsIdentifier = isIdentifier(token); - if (!remainingLessThanTokens || !tokenIsIdentifier) { - return tokenIsIdentifier; + if (!token || !isIdentifier(token)) return undefined; + if (!remainingLessThanTokens) { + return { called: token, nTypeArguments }; } remainingLessThanTokens--; break; @@ -957,25 +961,28 @@ namespace ts { // This can be object type, skip until we find the matching open brace token // Skip until the matching open brace token token = findPrecedingMatchingToken(token, SyntaxKind.OpenBraceToken, sourceFile); - if (!token) return false; + if (!token) return undefined; break; case SyntaxKind.CloseParenToken: // This can be object type, skip until we find the matching open brace token // Skip until the matching open brace token token = findPrecedingMatchingToken(token, SyntaxKind.OpenParenToken, sourceFile); - if (!token) return false; + if (!token) return undefined; break; case SyntaxKind.CloseBracketToken: // This can be object type, skip until we find the matching open brace token // Skip until the matching open brace token token = findPrecedingMatchingToken(token, SyntaxKind.OpenBracketToken, sourceFile); - if (!token) return false; + if (!token) return undefined; break; // Valid tokens in a type name. Skip. case SyntaxKind.CommaToken: + nTypeArguments++; + break; + case SyntaxKind.EqualsGreaterThanToken: case SyntaxKind.Identifier: @@ -999,13 +1006,13 @@ namespace ts { } // Invalid token in type - return false; + return undefined; } token = findPrecedingToken(token.getFullStart(), sourceFile); } - return false; + return undefined; } /** @@ -1086,7 +1093,7 @@ namespace ts { return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation; } - export function isInsideTemplateLiteral(node: LiteralExpression, position: number) { + export function isInsideTemplateLiteral(node: LiteralExpression | TemplateHead, position: number) { return isTemplateLiteralKind(node.kind) && (node.getStart() < position && position < node.getEnd()) || (!!node.isUnterminated && position === node.getEnd()); } diff --git a/tests/cases/fourslash/signatureHelpTypeArguments.ts b/tests/cases/fourslash/signatureHelpTypeArguments.ts new file mode 100644 index 0000000000000..d08e9fc5ec5dd --- /dev/null +++ b/tests/cases/fourslash/signatureHelpTypeArguments.ts @@ -0,0 +1,62 @@ +/// + +////declare function f(a: number, b: string, c: boolean): void; // ignored, not generic +////declare function f(): void; +////declare function f(): void; +////declare function f(): void; +////f(): void; +//// new(): void; +//// new(): void; +////}; +////new C(): void", + parameterName: "T", + parameterSpan: "T extends number", + }, + { + marker: "f1", + overloadsCount: 2, + text: "f(): void", + parameterName: "U", + parameterSpan: "U", + }, + { + marker: "f2", + text: "f(): void", + parameterName: "V", + parameterSpan: "V extends string", + }, + + { + marker: "C0", + overloadsCount: 3, + text: "C(): void", + parameterName: "T", + parameterSpan: "T extends number", + }, + { + marker: "C1", + overloadsCount: 2, + text: "C(): void", + parameterName: "U", + parameterSpan: "U", + }, + { + marker: "C2", + text: "C(): void", + parameterName: "V", + parameterSpan: "V extends string", + }, +);