Skip to content

Commit c6cc8e7

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

File tree

3 files changed

+117
-52
lines changed

3 files changed

+117
-52
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: 82 additions & 49 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,32 +35,38 @@ 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)) {
4445
return createJavaScriptSignatureHelpItems(argumentInfo, program, cancellationToken);
4546
}
46-
4747
return undefined;
4848
}
4949

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

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

67+
function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems {
5868
// 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;
69+
const expression = getExpressionFromInvocation(argumentInfo.invocation);
6170
const name = isIdentifier(expression) ? expression : isPropertyAccessExpression(expression) ? expression.name : undefined;
6271
if (!name || !name.escapedText) {
6372
return undefined;
@@ -76,7 +85,7 @@ namespace ts.SignatureHelp {
7685
if (type) {
7786
const callSignatures = type.getCallSignatures();
7887
if (callSignatures && callSignatures.length) {
79-
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker));
88+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, sourceFile, typeChecker));
8089
}
8190
}
8291
}
@@ -85,11 +94,22 @@ namespace ts.SignatureHelp {
8594
}
8695
}
8796

97+
export interface ArgumentInfoForCompletions {
98+
readonly invocation: CallLikeExpression;
99+
readonly argumentIndex: number;
100+
readonly argumentCount: number;
101+
}
102+
export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined {
103+
const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile);
104+
return !info || info.kind === ArgumentListKind.TypeArguments || info.invocation.kind === InvocationKind.TypeArgs ? undefined
105+
: { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex };
106+
}
107+
88108
/**
89109
* Returns relevant information for the argument list and the current argument if we are
90110
* in the argument of an invocation; returns undefined otherwise.
91111
*/
92-
export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
112+
function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
93113
if (isCallOrNewExpression(node.parent)) {
94114
const invocation = node.parent;
95115
let list: Node;
@@ -134,7 +154,7 @@ namespace ts.SignatureHelp {
134154
Debug.assertLessThan(argumentIndex, argumentCount);
135155
}
136156
const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
137-
return { kind, invocation, argumentsSpan, argumentIndex, argumentCount };
157+
return { kind, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount };
138158
}
139159
else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.parent.kind === SyntaxKind.TaggedTemplateExpression) {
140160
// Check if we're actually inside the template;
@@ -178,16 +198,35 @@ namespace ts.SignatureHelp {
178198
const attributeSpanEnd = skipTrivia(sourceFile.text, node.parent.attributes.getEnd(), /*stopAfterLineBreak*/ false);
179199
return {
180200
kind: ArgumentListKind.JSXAttributesArguments,
181-
invocation: node.parent,
201+
invocation: { kind: InvocationKind.Call, node: node.parent },
182202
argumentsSpan: createTextSpan(attributeSpanStart, attributeSpanEnd - attributeSpanStart),
183203
argumentIndex: 0,
184204
argumentCount: 1
185205
};
186206
}
187-
207+
else if (isBinaryExpression(node.parent)) {
208+
const info = getInfoForBinaryExpressionResemblingTypeArguments(node.parent);
209+
if (!info) return undefined;
210+
const { called, nTypeArguments } = info;
211+
const argumentsSpan = createTextSpanFromBounds(called.getStart(sourceFile), node.end);
212+
return { kind: ArgumentListKind.TypeArguments, invocation: { kind: InvocationKind.TypeArgs, called }, argumentsSpan, argumentIndex: nTypeArguments - 1, argumentCount: nTypeArguments };
213+
}
188214
return undefined;
189215
}
190216

217+
/** Code like `f<T, U,` will be parsed as a series of `BinaryExpression`s. */
218+
function getInfoForBinaryExpressionResemblingTypeArguments(bin: BinaryExpression): { readonly called: Expression, readonly nTypeArguments: number } | undefined {
219+
switch (bin.operatorToken.kind) {
220+
case SyntaxKind.LessThanToken:
221+
return { called: bin.left, nTypeArguments: 1 };
222+
case SyntaxKind.CommaToken:
223+
const left = isBinaryExpression(bin.left) ? getInfoForBinaryExpressionResemblingTypeArguments(bin.left) : undefined;
224+
return left && { called: left.called, nTypeArguments: left.nTypeArguments + 1 };
225+
default:
226+
return undefined;
227+
}
228+
}
229+
191230
function getArgumentIndex(argumentsList: Node, node: Node) {
192231
// The list we got back can include commas. In the presence of errors it may
193232
// also just have nodes without commas. For example "Foo(a b c)" will have 3
@@ -269,7 +308,7 @@ namespace ts.SignatureHelp {
269308
}
270309
return {
271310
kind: ArgumentListKind.TaggedTemplateArguments,
272-
invocation: tagExpression,
311+
invocation: { kind: InvocationKind.Call, node: tagExpression },
273312
argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile),
274313
argumentIndex,
275314
argumentCount
@@ -313,25 +352,15 @@ namespace ts.SignatureHelp {
313352
return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart);
314353
}
315354

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-
355+
function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo {
356+
for (let n = node; !isBlock(n) && !isSourceFile(n); n = n.parent) {
322357
// If the node is not a subspan of its parent, this is a big problem.
323358
// 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-
359+
Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.showSyntaxKind(n)}, parent: ${Debug.showSyntaxKind(n.parent)}`);
328360
const argumentInfo = getImmediatelyContainingArgumentInfo(n, position, sourceFile);
329361
if (argumentInfo) {
330362
return argumentInfo;
331363
}
332-
333-
334-
// TODO: Handle generic call with incomplete syntax
335364
}
336365
return undefined;
337366
}
@@ -343,16 +372,20 @@ namespace ts.SignatureHelp {
343372
return children[indexOfOpenerToken + 1];
344373
}
345374

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

351-
const callTarget = getInvokedExpression(invocation);
352-
const callTargetSymbol = typeChecker.getSymbolAtLocation(callTarget);
384+
const enclosingDeclaration = invocation.kind === InvocationKind.Call ? invocation.node : invocation.called;
385+
const callTargetSymbol = typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation));
353386
const callTargetDisplayParts = callTargetSymbol && symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined);
354387
const printer = createPrinter({ removeComments: true });
355-
const items: SignatureHelpItem[] = map(candidates, candidateSignature => {
388+
const items = candidates.map<SignatureHelpItem>(candidateSignature => {
356389
let signatureHelpParameters: SignatureHelpParameter[];
357390
const prefixDisplayParts: SymbolDisplayPart[] = [];
358391
const suffixDisplayParts: SymbolDisplayPart[] = [];
@@ -369,18 +402,18 @@ namespace ts.SignatureHelp {
369402
signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray;
370403
suffixDisplayParts.push(punctuationPart(SyntaxKind.GreaterThanToken));
371404
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);
405+
const thisParameter = candidateSignature.thisParameter ? [typeChecker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)] : [];
406+
const params = createNodeArray([...thisParameter, ...map(candidateSignature.parameters, param => typeChecker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags))]);
407+
printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
375408
});
376409
addRange(suffixDisplayParts, parameterParts);
377410
}
378411
else {
379412
isVariadic = candidateSignature.hasRestParameter;
380413
const typeParameterParts = mapToDisplayParts(writer => {
381414
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);
415+
const args = createNodeArray(map(candidateSignature.typeParameters, p => typeChecker.typeParameterToDeclaration(p, enclosingDeclaration)));
416+
printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer);
384417
}
385418
});
386419
addRange(prefixDisplayParts, typeParameterParts);
@@ -395,10 +428,10 @@ namespace ts.SignatureHelp {
395428
writer.writeSpace(" ");
396429
const predicate = typeChecker.getTypePredicateOfSignature(candidateSignature);
397430
if (predicate) {
398-
typeChecker.writeTypePredicate(predicate, invocation, /*flags*/ undefined, writer);
431+
typeChecker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer);
399432
}
400433
else {
401-
typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), invocation, /*flags*/ undefined, writer);
434+
typeChecker.writeType(typeChecker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer);
402435
}
403436
});
404437
addRange(suffixDisplayParts, returnTypeParts);
@@ -425,8 +458,8 @@ namespace ts.SignatureHelp {
425458

426459
function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter {
427460
const displayParts = mapToDisplayParts(writer => {
428-
const param = typeChecker.symbolToParameterDeclaration(parameter, invocation, signatureHelpNodeBuilderFlags);
429-
printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
461+
const param = typeChecker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags);
462+
printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
430463
});
431464

432465
return {
@@ -439,8 +472,8 @@ namespace ts.SignatureHelp {
439472

440473
function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter {
441474
const displayParts = mapToDisplayParts(writer => {
442-
const param = typeChecker.typeParameterToDeclaration(typeParameter, invocation);
443-
printer.writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(invocation)), writer);
475+
const param = typeChecker.typeParameterToDeclaration(typeParameter, enclosingDeclaration);
476+
printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
444477
});
445478

446479
return {

0 commit comments

Comments
 (0)