Skip to content

Commit ad5441c

Browse files
author
Andy Hanson
committed
Support signature help for partially-filled-in type arguments f<
1 parent 7e515af commit ad5441c

File tree

3 files changed

+115
-51
lines changed

3 files changed

+115
-51
lines changed

src/services/completions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ namespace ts.Completions {
418418
case SyntaxKind.CallExpression:
419419
case SyntaxKind.NewExpression:
420420
if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false) && !isImportCall(node.parent)) {
421-
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
421+
const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile);
422422
// Get string literal completions from specialized signatures of the target
423423
// i.e. declare function f(a: 'A');
424424
// f("/*completion position*/")
@@ -448,7 +448,7 @@ namespace ts.Completions {
448448
}
449449
}
450450

451-
function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentListInfo, checker: TypeChecker): StringLiteralCompletionsFromTypes {
451+
function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes {
452452
let isNewIdentifier = false;
453453

454454
const uniques = createMap<true>();
@@ -712,7 +712,7 @@ namespace ts.Completions {
712712
case SyntaxKind.OpenBraceToken:
713713
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
714714
default:
715-
const argInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(currentToken, position, sourceFile);
715+
const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile);
716716
return argInfo
717717
// At `,`, treat this as the next argument after the comma.
718718
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0))

src/services/signatureHelp.ts

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
/* @internal */
22
namespace ts.SignatureHelp {
3-
export const enum ArgumentListKind {
3+
const enum ArgumentListKind {
44
TypeArguments,
55
CallArguments,
66
TaggedTemplateArguments,
77
JSXAttributesArguments
88
}
99

10-
export interface ArgumentListInfo {
10+
const enum InvocationKind { Call, TypeArgs }
11+
type Invocation = { kind: InvocationKind.Call, node: CallLikeExpression } | { kind: InvocationKind.TypeArgs, called: Expression };
12+
13+
interface ArgumentListInfo {
1114
kind: ArgumentListKind;
12-
invocation: CallLikeExpression;
15+
invocation: Invocation;
1316
argumentsSpan: TextSpan;
14-
argumentIndex?: number;
17+
argumentIndex: number;
1518
/** argumentCount is the *apparent* number of arguments. */
1619
argumentCount: number;
1720
}
@@ -32,12 +35,10 @@ namespace ts.SignatureHelp {
3235
cancellationToken.throwIfCancellationRequested();
3336

3437
// Semantic filtering of signature help
35-
const call = argumentInfo.invocation;
36-
const candidates: Signature[] = [];
37-
const resolvedSignature = typeChecker.getResolvedSignature(call, candidates, argumentInfo.argumentCount);
38+
const candidateInfo = getCandidateInfo(argumentInfo, typeChecker);
3839
cancellationToken.throwIfCancellationRequested();
3940

40-
if (!candidates.length) {
41+
if (!candidateInfo) {
4142
// We didn't have any sig help items produced by the TS compiler. If this is a JS
4243
// file, then see if we can figure out anything better.
4344
if (isSourceFileJavaScript(sourceFile)) {
@@ -47,17 +48,26 @@ namespace ts.SignatureHelp {
4748
return undefined;
4849
}
4950

50-
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidates, resolvedSignature, argumentInfo, typeChecker));
51+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker));
5152
}
5253

53-
function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems {
54-
if (argumentInfo.invocation.kind !== SyntaxKind.CallExpression) {
55-
return undefined;
54+
function getCandidateInfo(argumentInfo: ArgumentListInfo, checker: TypeChecker): { readonly candidates: ReadonlyArray<Signature>, readonly resolvedSignature: Signature } | undefined {
55+
const { invocation } = argumentInfo;
56+
if (invocation.kind === InvocationKind.Call) {
57+
const candidates: Signature[] = [];
58+
const resolvedSignature = checker.getResolvedSignature(invocation.node, candidates, argumentInfo.argumentCount);
59+
return candidates.length === 0 ? undefined : { candidates, resolvedSignature };
5660
}
61+
else {
62+
const type = checker.getTypeAtLocation(invocation.called);
63+
const candidates = type.getCallSignatures().filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= argumentInfo.argumentCount);
64+
return candidates.length === 0 ? undefined : { candidates, resolvedSignature: first(candidates) };
65+
}
66+
}
5767

68+
function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems {
5869
// See if we can find some symbol with the call expression name that has call signatures.
59-
const callExpression = argumentInfo.invocation;
60-
const expression = callExpression.expression;
70+
const expression = getExpressionFromInvocation(argumentInfo.invocation);
6171
const name = isIdentifier(expression) ? expression : isPropertyAccessExpression(expression) ? expression.name : undefined;
6272
if (!name || !name.escapedText) {
6373
return undefined;
@@ -76,7 +86,7 @@ namespace ts.SignatureHelp {
7686
if (type) {
7787
const callSignatures = type.getCallSignatures();
7888
if (callSignatures && callSignatures.length) {
79-
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker));
89+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, sourceFile, typeChecker));
8090
}
8191
}
8292
}
@@ -85,11 +95,22 @@ namespace ts.SignatureHelp {
8595
}
8696
}
8797

98+
export interface ArgumentInfoForCompletions {
99+
readonly invocation: CallLikeExpression;
100+
readonly argumentIndex: number;
101+
readonly argumentCount: number;
102+
}
103+
export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined {
104+
const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile);
105+
return !info || info.kind === ArgumentListKind.TypeArguments || info.invocation.kind === InvocationKind.TypeArgs ? undefined
106+
: { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex };
107+
}
108+
88109
/**
89110
* Returns relevant information for the argument list and the current argument if we are
90111
* in the argument of an invocation; returns undefined otherwise.
91112
*/
92-
export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
113+
function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
93114
if (isCallOrNewExpression(node.parent)) {
94115
const invocation = node.parent;
95116
let list: Node;
@@ -134,7 +155,7 @@ namespace ts.SignatureHelp {
134155
Debug.assertLessThan(argumentIndex, argumentCount);
135156
}
136157
const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
137-
return { kind, invocation, argumentsSpan, argumentIndex, argumentCount };
158+
return { kind, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount };
138159
}
139160
else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.parent.kind === SyntaxKind.TaggedTemplateExpression) {
140161
// Check if we're actually inside the template;
@@ -178,16 +199,33 @@ namespace ts.SignatureHelp {
178199
const attributeSpanEnd = skipTrivia(sourceFile.text, node.parent.attributes.getEnd(), /*stopAfterLineBreak*/ false);
179200
return {
180201
kind: ArgumentListKind.JSXAttributesArguments,
181-
invocation: node.parent,
202+
invocation: { kind: InvocationKind.Call, node: node.parent },
182203
argumentsSpan: createTextSpan(attributeSpanStart, attributeSpanEnd - attributeSpanStart),
183204
argumentIndex: 0,
184205
argumentCount: 1
185206
};
186207
}
187-
208+
else if (isBinaryExpression(node.parent)) {
209+
const { called, nTypeArguments } = getInfoFromBinaryExpressionResemblingTypeArguments(node.parent);
210+
const argumentsSpan = createTextSpanFromBounds(called.getStart(sourceFile), node.end);
211+
return { kind: ArgumentListKind.TypeArguments, invocation: { kind: InvocationKind.TypeArgs, called }, argumentsSpan, argumentIndex: nTypeArguments - 1, argumentCount: nTypeArguments };
212+
}
188213
return undefined;
189214
}
190215

216+
/** Code like `f<T, U,` will be parsed as a series of `BinaryExpression`s. */
217+
function getInfoFromBinaryExpressionResemblingTypeArguments(bin: BinaryExpression): { readonly called: Expression, readonly nTypeArguments: number } | undefined {
218+
switch (bin.operatorToken.kind) {
219+
case SyntaxKind.LessThanToken:
220+
return { called: bin.left, nTypeArguments: 1 };
221+
case SyntaxKind.CommaToken:
222+
const left = isBinaryExpression(bin.left) ? getInfoFromBinaryExpressionResemblingTypeArguments(bin.left) : undefined;
223+
return left && { called: left.called, nTypeArguments: left.nTypeArguments + 1 };
224+
default:
225+
return undefined;
226+
}
227+
}
228+
191229
function getArgumentIndex(argumentsList: Node, node: Node) {
192230
// The list we got back can include commas. In the presence of errors it may
193231
// also just have nodes without commas. For example "Foo(a b c)" will have 3
@@ -269,7 +307,7 @@ namespace ts.SignatureHelp {
269307
}
270308
return {
271309
kind: ArgumentListKind.TaggedTemplateArguments,
272-
invocation: tagExpression,
310+
invocation: { kind: InvocationKind.Call, node: tagExpression },
273311
argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile),
274312
argumentIndex,
275313
argumentCount
@@ -313,25 +351,15 @@ namespace ts.SignatureHelp {
313351
return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart);
314352
}
315353

316-
export function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo {
317-
for (let n = node; n.kind !== SyntaxKind.SourceFile; n = n.parent) {
318-
if (isFunctionBlock(n)) {
319-
return undefined;
320-
}
321-
354+
function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo {
355+
for (let n = node; !isBlock(n) && !isSourceFile(n); n = n.parent) {
322356
// If the node is not a subspan of its parent, this is a big problem.
323357
// There have been crashes that might be caused by this violation.
324-
if (n.pos < n.parent.pos || n.end > n.parent.end) {
325-
Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind);
326-
}
327-
358+
Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.showSyntaxKind(n)}, parent: ${Debug.showSyntaxKind(n.parent)}`);
328359
const argumentInfo = getImmediatelyContainingArgumentInfo(n, position, sourceFile);
329360
if (argumentInfo) {
330361
return argumentInfo;
331362
}
332-
333-
334-
// TODO: Handle generic call with incomplete syntax
335363
}
336364
return undefined;
337365
}
@@ -343,16 +371,20 @@ namespace ts.SignatureHelp {
343371
return children[indexOfOpenerToken + 1];
344372
}
345373

374+
function getExpressionFromInvocation(invocation: Invocation): Expression {
375+
return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called;
376+
}
377+
346378
const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors;
347-
function createSignatureHelpItems(candidates: Signature[], resolvedSignature: Signature, argumentListInfo: ArgumentListInfo, typeChecker: TypeChecker): SignatureHelpItems {
379+
function createSignatureHelpItems(candidates: ReadonlyArray<Signature>, resolvedSignature: Signature, argumentListInfo: ArgumentListInfo, sourceFile: SourceFile, typeChecker: TypeChecker): SignatureHelpItems {
348380
const { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex } = argumentListInfo;
349381
const isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments;
350382

351-
const callTarget = getInvokedExpression(invocation);
352-
const callTargetSymbol = typeChecker.getSymbolAtLocation(callTarget);
383+
const enclosingDeclaration = invocation.kind === InvocationKind.Call ? invocation.node : invocation.called;
384+
const callTargetSymbol = typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation));
353385
const callTargetDisplayParts = callTargetSymbol && symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined);
354386
const printer = createPrinter({ removeComments: true });
355-
const items: SignatureHelpItem[] = map(candidates, candidateSignature => {
387+
const items = candidates.map<SignatureHelpItem>(candidateSignature => {
356388
let signatureHelpParameters: SignatureHelpParameter[];
357389
const prefixDisplayParts: SymbolDisplayPart[] = [];
358390
const suffixDisplayParts: SymbolDisplayPart[] = [];
@@ -369,18 +401,18 @@ namespace ts.SignatureHelp {
369401
signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray;
370402
suffixDisplayParts.push(punctuationPart(SyntaxKind.GreaterThanToken));
371403
const parameterParts = mapToDisplayParts(writer => {
372-
const thisParameter = candidateSignature.thisParameter ? [typeChecker.symbolToParameterDeclaration(candidateSignature.thisParameter, invocation, signatureHelpNodeBuilderFlags)] : [];
373-
const params = createNodeArray([...thisParameter, ...map(candidateSignature.parameters, param => typeChecker.symbolToParameterDeclaration(param, invocation, signatureHelpNodeBuilderFlags))]);
374-
printer.writeList(ListFormat.CallExpressionArguments, params, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
404+
const thisParameter = candidateSignature.thisParameter ? [typeChecker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)] : [];
405+
const params = createNodeArray([...thisParameter, ...map(candidateSignature.parameters, param => typeChecker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags))]);
406+
printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
375407
});
376408
addRange(suffixDisplayParts, parameterParts);
377409
}
378410
else {
379411
isVariadic = candidateSignature.hasRestParameter;
380412
const typeParameterParts = mapToDisplayParts(writer => {
381413
if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) {
382-
const args = createNodeArray(map(candidateSignature.typeParameters, p => typeChecker.typeParameterToDeclaration(p, invocation)));
383-
printer.writeList(ListFormat.TypeParameters, args, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
414+
const args = createNodeArray(map(candidateSignature.typeParameters, p => typeChecker.typeParameterToDeclaration(p, enclosingDeclaration)));
415+
printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer);
384416
}
385417
});
386418
addRange(prefixDisplayParts, typeParameterParts);
@@ -395,10 +427,10 @@ namespace ts.SignatureHelp {
395427
writer.writeSpace(" ");
396428
const predicate = typeChecker.getTypePredicateOfSignature(candidateSignature);
397429
if (predicate) {
398-
typeChecker.writeTypePredicate(predicate, invocation, /*flags*/ undefined, writer);
430+
typeChecker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer);
399431
}
400432
else {
401-
typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), invocation, /*flags*/ undefined, writer);
433+
typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer);
402434
}
403435
});
404436
addRange(suffixDisplayParts, returnTypeParts);
@@ -425,8 +457,8 @@ namespace ts.SignatureHelp {
425457

426458
function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter {
427459
const displayParts = mapToDisplayParts(writer => {
428-
const param = typeChecker.symbolToParameterDeclaration(parameter, invocation, signatureHelpNodeBuilderFlags);
429-
printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
460+
const param = typeChecker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags);
461+
printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
430462
});
431463

432464
return {
@@ -439,8 +471,8 @@ namespace ts.SignatureHelp {
439471

440472
function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter {
441473
const displayParts = mapToDisplayParts(writer => {
442-
const param = typeChecker.typeParameterToDeclaration(typeParameter, invocation);
443-
printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
474+
const param = typeChecker.typeParameterToDeclaration(typeParameter, enclosingDeclaration);
475+
printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
444476
});
445477

446478
return {

0 commit comments

Comments
 (0)