From 9106fdbc47f4b1e7ddb6458f62c2e00c775591d0 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 28 Aug 2018 14:21:09 -0700 Subject: [PATCH] Support signature help for type parameters of a type (#26702) --- src/compiler/checker.ts | 4 +- src/compiler/types.ts | 2 + src/services/signatureHelp.ts | 76 +++++++++++++++---- .../genericParameterHelpTypeReferences.ts | 50 +++++++----- 4 files changed, 99 insertions(+), 33 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 41573a7a8f6..cb3fc5614fb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -370,7 +370,9 @@ namespace ts { finally { cancellationToken = undefined; } - } + }, + + getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, }; function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, isForSignatureHelp: boolean): Signature | undefined { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e024885d6e8..10076014d96 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3118,6 +3118,8 @@ namespace ts { * and the operation is cancelled, then it should be discarded, otherwise it is safe to keep. */ runWithCancellationToken(token: CancellationToken, cb: (checker: TypeChecker) => T): T; + + /* @internal */ getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): ReadonlyArray | undefined; } /* @internal */ diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index d1705b51048..5211b244e21 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -2,7 +2,7 @@ namespace ts.SignatureHelp { const enum InvocationKind { Call, TypeArgs, Contextual } interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; } - interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Expression; } + interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; } interface ContextualInvocation { readonly kind: InvocationKind.Contextual; readonly signature: Signature; @@ -44,7 +44,7 @@ namespace ts.SignatureHelp { cancellationToken.throwIfCancellationRequested(); // Extra syntactic and semantic filtering of signature help - const candidateInfo = getCandidateInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); + const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); cancellationToken.throwIfCancellationRequested(); if (!candidateInfo) { @@ -53,11 +53,24 @@ namespace ts.SignatureHelp { return isSourceFileJavaScript(sourceFile) ? createJavaScriptSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; } - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)); + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => + candidateInfo.kind === CandidateOrTypeKind.Candidate + ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) + : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); } - interface CandidateInfo { readonly candidates: ReadonlyArray; readonly resolvedSignature: Signature; } - function getCandidateInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | undefined { + const enum CandidateOrTypeKind { Candidate, Type } + interface CandidateInfo { + readonly kind: CandidateOrTypeKind.Candidate; + readonly candidates: ReadonlyArray; + readonly resolvedSignature: Signature; + } + interface TypeInfo { + readonly kind: CandidateOrTypeKind.Type; + readonly symbol: Symbol; + } + + function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined { switch (invocation.kind) { case InvocationKind.Call: { if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) { @@ -65,17 +78,21 @@ namespace ts.SignatureHelp { } const candidates: Signature[] = []; const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217 - return candidates.length === 0 ? undefined : { candidates, resolvedSignature }; + return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature }; } case InvocationKind.TypeArgs: { - if (onlyUseSyntacticOwners && !lessThanFollowsCalledExpression(startingToken, sourceFile, invocation.called)) { + const { called } = invocation; + if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) { return undefined; } - const candidates = getPossibleGenericSignatures(invocation.called, argumentCount, checker); - return candidates.length === 0 ? undefined : { candidates, resolvedSignature: first(candidates) }; + const candidates = getPossibleGenericSignatures(called, argumentCount, checker); + if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) }; + + const symbol = checker.getSymbolAtLocation(called); + return symbol && { kind: CandidateOrTypeKind.Type, symbol }; } case InvocationKind.Contextual: - return { candidates: [invocation.signature], resolvedSignature: invocation.signature }; + return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature }; default: return Debug.assertNever(invocation); } @@ -92,7 +109,7 @@ namespace ts.SignatureHelp { return !!containingList && contains(invocationChildren, containingList); } case SyntaxKind.LessThanToken: - return lessThanFollowsCalledExpression(startingToken, sourceFile, node.expression); + return containsPrecedingToken(startingToken, sourceFile, node.expression); default: return false; } @@ -114,12 +131,12 @@ namespace ts.SignatureHelp { })); } - function lessThanFollowsCalledExpression(startingToken: Node, sourceFile: SourceFile, calledExpression: Expression) { + function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) { const precedingToken = Debug.assertDefined( findPrecedingToken(startingToken.getFullStart(), sourceFile, startingToken.parent, /*excludeJsdoc*/ true) ); - return rangeContainsRange(calledExpression, precedingToken); + return rangeContainsRange(container, precedingToken); } export interface ArgumentInfoForCompletions { @@ -457,6 +474,10 @@ namespace ts.SignatureHelp { return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called; } + function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node { + return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; + } + const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; function createSignatureHelpItems( candidates: ReadonlyArray, @@ -465,7 +486,7 @@ namespace ts.SignatureHelp { sourceFile: SourceFile, typeChecker: TypeChecker, ): SignatureHelpItems { - const enclosingDeclaration = invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; + const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)); const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray; const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); @@ -480,11 +501,36 @@ namespace ts.SignatureHelp { return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; } + function createTypeHelpItems( + symbol: Symbol, + { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, + sourceFile: SourceFile, + checker: TypeChecker + ): SignatureHelpItems | undefined { + const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + if (!typeParameters) return undefined; + const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)]; + return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount }; + } + + function getTypeHelpItem(symbol: Symbol, typeParameters: ReadonlyArray, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { + const typeSymbolDisplay = symbolToDisplayParts(checker, symbol); + + const printer = createPrinter({ removeComments: true }); + const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); + + const documentation = symbol.getDocumentationComment(checker); + const tags = symbol.getJsDocTags(); + const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)]; + return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; + } + + const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()]; + function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: ReadonlyArray, isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); const prefixDisplayParts = [...callTargetDisplayParts, ...prefix]; const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)]; - const separatorDisplayParts = [punctuationPart(SyntaxKind.CommaToken), spacePart()]; const documentation = candidateSignature.getDocumentationComment(checker); const tags = candidateSignature.getJsDocTags(); return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags }; diff --git a/tests/cases/fourslash/genericParameterHelpTypeReferences.ts b/tests/cases/fourslash/genericParameterHelpTypeReferences.ts index 5a477d23e56..a5dee833248 100644 --- a/tests/cases/fourslash/genericParameterHelpTypeReferences.ts +++ b/tests/cases/fourslash/genericParameterHelpTypeReferences.ts @@ -11,21 +11,37 @@ ////var x : testClass extends testClass; +//// +////interface I {} +////let i: I; +//// +////type Ty = T; +////let t: Ty; -// TODO: GH#26699 - -if (false) { - verify.signatureHelp( - { - marker: ["type1", "type2", "type3"], - text: "testClass", - parameterName: "T", - parameterSpan: "T extends IFoo", - }, - { - marker: "type4", - parameterName: "M", - parameterSpan: "M extends IFoo", - } - ); -} +verify.signatureHelp( + { + marker: ["type1", "type2", "type3"], + text: "testClass", + parameterName: "T", + parameterSpan: "T extends IFoo", + triggerReason: { kind: "characterTyped", triggerCharacter: "<" }, + }, + { + marker: "type4", + parameterName: "M", + parameterSpan: "M extends IFoo", + triggerReason: { kind: "characterTyped", triggerCharacter: "," }, + }, + { + marker: "interface", + text: "I", + parameterName: "T", + parameterSpan: "T", + }, + { + marker: "typeAlias", + text: "Ty", + parameterName: "T", + parameterSpan: "T", + }, +);