Skip to content

Commit 9106fdb

Browse files
author
Andy
authored
Support signature help for type parameters of a type (#26702)
1 parent 552bd1c commit 9106fdb

File tree

4 files changed

+99
-33
lines changed

4 files changed

+99
-33
lines changed

src/compiler/checker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,9 @@ namespace ts {
370370
finally {
371371
cancellationToken = undefined;
372372
}
373-
}
373+
},
374+
375+
getLocalTypeParametersOfClassOrInterfaceOrTypeAlias,
374376
};
375377

376378
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, isForSignatureHelp: boolean): Signature | undefined {

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3118,6 +3118,8 @@ namespace ts {
31183118
* and the operation is cancelled, then it should be discarded, otherwise it is safe to keep.
31193119
*/
31203120
runWithCancellationToken<T>(token: CancellationToken, cb: (checker: TypeChecker) => T): T;
3121+
3122+
/* @internal */ getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): ReadonlyArray<TypeParameter> | undefined;
31213123
}
31223124

31233125
/* @internal */

src/services/signatureHelp.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
namespace ts.SignatureHelp {
33
const enum InvocationKind { Call, TypeArgs, Contextual }
44
interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; }
5-
interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Expression; }
5+
interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; }
66
interface ContextualInvocation {
77
readonly kind: InvocationKind.Contextual;
88
readonly signature: Signature;
@@ -44,7 +44,7 @@ namespace ts.SignatureHelp {
4444
cancellationToken.throwIfCancellationRequested();
4545

4646
// Extra syntactic and semantic filtering of signature help
47-
const candidateInfo = getCandidateInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners);
47+
const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners);
4848
cancellationToken.throwIfCancellationRequested();
4949

5050
if (!candidateInfo) {
@@ -53,29 +53,46 @@ namespace ts.SignatureHelp {
5353
return isSourceFileJavaScript(sourceFile) ? createJavaScriptSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined;
5454
}
5555

56-
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker));
56+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
57+
candidateInfo.kind === CandidateOrTypeKind.Candidate
58+
? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)
59+
: createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker));
5760
}
5861

59-
interface CandidateInfo { readonly candidates: ReadonlyArray<Signature>; readonly resolvedSignature: Signature; }
60-
function getCandidateInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | undefined {
62+
const enum CandidateOrTypeKind { Candidate, Type }
63+
interface CandidateInfo {
64+
readonly kind: CandidateOrTypeKind.Candidate;
65+
readonly candidates: ReadonlyArray<Signature>;
66+
readonly resolvedSignature: Signature;
67+
}
68+
interface TypeInfo {
69+
readonly kind: CandidateOrTypeKind.Type;
70+
readonly symbol: Symbol;
71+
}
72+
73+
function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined {
6174
switch (invocation.kind) {
6275
case InvocationKind.Call: {
6376
if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) {
6477
return undefined;
6578
}
6679
const candidates: Signature[] = [];
6780
const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217
68-
return candidates.length === 0 ? undefined : { candidates, resolvedSignature };
81+
return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature };
6982
}
7083
case InvocationKind.TypeArgs: {
71-
if (onlyUseSyntacticOwners && !lessThanFollowsCalledExpression(startingToken, sourceFile, invocation.called)) {
84+
const { called } = invocation;
85+
if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) {
7286
return undefined;
7387
}
74-
const candidates = getPossibleGenericSignatures(invocation.called, argumentCount, checker);
75-
return candidates.length === 0 ? undefined : { candidates, resolvedSignature: first(candidates) };
88+
const candidates = getPossibleGenericSignatures(called, argumentCount, checker);
89+
if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) };
90+
91+
const symbol = checker.getSymbolAtLocation(called);
92+
return symbol && { kind: CandidateOrTypeKind.Type, symbol };
7693
}
7794
case InvocationKind.Contextual:
78-
return { candidates: [invocation.signature], resolvedSignature: invocation.signature };
95+
return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature };
7996
default:
8097
return Debug.assertNever(invocation);
8198
}
@@ -92,7 +109,7 @@ namespace ts.SignatureHelp {
92109
return !!containingList && contains(invocationChildren, containingList);
93110
}
94111
case SyntaxKind.LessThanToken:
95-
return lessThanFollowsCalledExpression(startingToken, sourceFile, node.expression);
112+
return containsPrecedingToken(startingToken, sourceFile, node.expression);
96113
default:
97114
return false;
98115
}
@@ -114,12 +131,12 @@ namespace ts.SignatureHelp {
114131
}));
115132
}
116133

117-
function lessThanFollowsCalledExpression(startingToken: Node, sourceFile: SourceFile, calledExpression: Expression) {
134+
function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) {
118135
const precedingToken = Debug.assertDefined(
119136
findPrecedingToken(startingToken.getFullStart(), sourceFile, startingToken.parent, /*excludeJsdoc*/ true)
120137
);
121138

122-
return rangeContainsRange(calledExpression, precedingToken);
139+
return rangeContainsRange(container, precedingToken);
123140
}
124141

125142
export interface ArgumentInfoForCompletions {
@@ -457,6 +474,10 @@ namespace ts.SignatureHelp {
457474
return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called;
458475
}
459476

477+
function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node {
478+
return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node;
479+
}
480+
460481
const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
461482
function createSignatureHelpItems(
462483
candidates: ReadonlyArray<Signature>,
@@ -465,7 +486,7 @@ namespace ts.SignatureHelp {
465486
sourceFile: SourceFile,
466487
typeChecker: TypeChecker,
467488
): SignatureHelpItems {
468-
const enclosingDeclaration = invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node;
489+
const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation);
469490
const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation));
470491
const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray;
471492
const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile));
@@ -480,11 +501,36 @@ namespace ts.SignatureHelp {
480501
return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
481502
}
482503

504+
function createTypeHelpItems(
505+
symbol: Symbol,
506+
{ argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo,
507+
sourceFile: SourceFile,
508+
checker: TypeChecker
509+
): SignatureHelpItems | undefined {
510+
const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol);
511+
if (!typeParameters) return undefined;
512+
const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)];
513+
return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount };
514+
}
515+
516+
function getTypeHelpItem(symbol: Symbol, typeParameters: ReadonlyArray<TypeParameter>, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
517+
const typeSymbolDisplay = symbolToDisplayParts(checker, symbol);
518+
519+
const printer = createPrinter({ removeComments: true });
520+
const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
521+
522+
const documentation = symbol.getDocumentationComment(checker);
523+
const tags = symbol.getJsDocTags();
524+
const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)];
525+
return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags };
526+
}
527+
528+
const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()];
529+
483530
function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: ReadonlyArray<SymbolDisplayPart>, isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
484531
const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile);
485532
const prefixDisplayParts = [...callTargetDisplayParts, ...prefix];
486533
const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)];
487-
const separatorDisplayParts = [punctuationPart(SyntaxKind.CommaToken), spacePart()];
488534
const documentation = candidateSignature.getDocumentationComment(checker);
489535
const tags = candidateSignature.getJsDocTags();
490536
return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags };

tests/cases/fourslash/genericParameterHelpTypeReferences.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,37 @@
1111
////var x : testClass</*type2*/
1212
////class Bar<T> extends testClass</*type3*/
1313
////var x : testClass<,, /*type4*/any>;
14+
////
15+
////interface I<T> {}
16+
////let i: I</*interface*/>;
17+
////
18+
////type Ty<T> = T;
19+
////let t: Ty</*typeAlias*/>;
1420

15-
// TODO: GH#26699
16-
17-
if (false) {
18-
verify.signatureHelp(
19-
{
20-
marker: ["type1", "type2", "type3"],
21-
text: "testClass<T extends IFoo, U, M extends IFoo>",
22-
parameterName: "T",
23-
parameterSpan: "T extends IFoo",
24-
},
25-
{
26-
marker: "type4",
27-
parameterName: "M",
28-
parameterSpan: "M extends IFoo",
29-
}
30-
);
31-
}
21+
verify.signatureHelp(
22+
{
23+
marker: ["type1", "type2", "type3"],
24+
text: "testClass<T extends IFoo, U, M extends IFoo>",
25+
parameterName: "T",
26+
parameterSpan: "T extends IFoo",
27+
triggerReason: { kind: "characterTyped", triggerCharacter: "<" },
28+
},
29+
{
30+
marker: "type4",
31+
parameterName: "M",
32+
parameterSpan: "M extends IFoo",
33+
triggerReason: { kind: "characterTyped", triggerCharacter: "," },
34+
},
35+
{
36+
marker: "interface",
37+
text: "I<T>",
38+
parameterName: "T",
39+
parameterSpan: "T",
40+
},
41+
{
42+
marker: "typeAlias",
43+
text: "Ty<T>",
44+
parameterName: "T",
45+
parameterSpan: "T",
46+
},
47+
);

0 commit comments

Comments
 (0)