Skip to content

Commit 33d0893

Browse files
author
Andy
authored
Add completions from literal contextual types (microsoft#24674)
* Add completions from literal contextual types * Remove getTypesOfUnion * undo baseline changes
1 parent 604beba commit 33d0893

3 files changed

Lines changed: 60 additions & 23 deletions

File tree

src/services/completions.ts

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ namespace ts.Completions {
101101
}
102102

103103
function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined {
104-
const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData;
104+
const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData;
105105

106106
if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) {
107107
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
@@ -143,6 +143,10 @@ namespace ts.Completions {
143143
addRange(entries, getKeywordCompletions(keywordFilters));
144144
}
145145

146+
for (const literal of literals) {
147+
entries.push(createCompletionEntryForLiteral(literal));
148+
}
149+
146150
return { isGlobalCompletion: isInSnippetScope, isMemberCompletion, isNewIdentifierLocation, entries };
147151
}
148152

@@ -184,6 +188,11 @@ namespace ts.Completions {
184188
});
185189
}
186190

191+
const completionNameForLiteral = JSON.stringify;
192+
function createCompletionEntryForLiteral(literal: string | number): CompletionEntry {
193+
return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: "0" };
194+
}
195+
187196
function createCompletionEntry(
188197
symbol: Symbol,
189198
location: Node | undefined,
@@ -372,7 +381,7 @@ namespace ts.Completions {
372381
case SyntaxKind.LiteralType:
373382
switch (node.parent.parent.kind) {
374383
case SyntaxKind.TypeReference:
375-
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode), typeChecker), isNewIdentifier: false };
384+
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode)), isNewIdentifier: false };
376385
case SyntaxKind.IndexedAccessType:
377386
// Get all apparent property names
378387
// i.e. interface Foo {
@@ -448,7 +457,7 @@ namespace ts.Completions {
448457
function fromContextualType(): StringLiteralCompletion {
449458
// Get completion for string literal from string literal type
450459
// i.e. var x: "hi" | "hello" = "/*completion position*/"
451-
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker), isNewIdentifier: false };
460+
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false };
452461
}
453462
}
454463

@@ -462,7 +471,7 @@ namespace ts.Completions {
462471
if (!candidate.hasRestParameter && argumentInfo.argumentCount > candidate.parameters.length) return;
463472
const type = checker.getParameterType(candidate, argumentInfo.argumentIndex);
464473
isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String);
465-
return getStringLiteralTypes(type, checker, uniques);
474+
return getStringLiteralTypes(type, uniques);
466475
});
467476

468477
return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier };
@@ -472,11 +481,11 @@ namespace ts.Completions {
472481
return type && { kind: StringLiteralCompletionKind.Properties, symbols: type.getApparentProperties(), hasIndexSignature: hasIndexSignature(type) };
473482
}
474483

475-
function getStringLiteralTypes(type: Type | undefined, typeChecker: TypeChecker, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
484+
function getStringLiteralTypes(type: Type | undefined, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
476485
if (!type) return emptyArray;
477486
type = skipConstraint(type);
478487
return type.isUnion()
479-
? flatMap(type.types, t => getStringLiteralTypes(t, typeChecker, uniques))
488+
? flatMap(type.types, t => getStringLiteralTypes(t, uniques))
480489
: type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value)
481490
? [type]
482491
: emptyArray;
@@ -491,7 +500,7 @@ namespace ts.Completions {
491500
readonly isJsxInitializer: IsJsxInitializer;
492501
}
493502
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
494-
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
503+
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number } | { type: "none" } {
495504
const compilerOptions = program.getCompilerOptions();
496505
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
497506
if (!completionData) {
@@ -501,7 +510,10 @@ namespace ts.Completions {
501510
return { type: "request", request: completionData };
502511
}
503512

504-
const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
513+
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData;
514+
515+
const literal = find(literals, l => completionNameForLiteral(l) === entryId.name);
516+
if (literal !== undefined) return { type: "literal", literal };
505517

506518
// Find the symbol with the matching entry name.
507519
// We don't need to perform character checks here because we're only comparing the
@@ -574,12 +586,22 @@ namespace ts.Completions {
574586
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences);
575587
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
576588
}
589+
case "literal": {
590+
const { literal } = symbolCompletion;
591+
return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
592+
}
577593
case "none":
578594
// Didn't find a symbol with this name. See if we can find a keyword instead.
579-
return allKeywordsCompletions().some(c => c.name === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.keyword, [displayPart(name, SymbolDisplayPartKind.keyword)]) : undefined;
595+
return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;
596+
default:
597+
Debug.assertNever(symbolCompletion);
580598
}
581599
}
582600

601+
function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails {
602+
return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]);
603+
}
604+
583605
function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails {
584606
const { displayParts, documentation, symbolKind, tags } =
585607
checker.runWithCancellationToken(cancellationToken, checker =>
@@ -669,6 +691,7 @@ namespace ts.Completions {
669691
readonly isNewIdentifierLocation: boolean;
670692
readonly location: Node | undefined;
671693
readonly keywordFilters: KeywordCompletionFilters;
694+
readonly literals: ReadonlyArray<string | number>;
672695
readonly symbolToOriginInfoMap: SymbolOriginInfoMap;
673696
readonly recommendedCompletion: Symbol | undefined;
674697
readonly previousToken: Node | undefined;
@@ -685,23 +708,22 @@ namespace ts.Completions {
685708
None,
686709
}
687710

688-
function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined {
689-
const contextualType = getContextualType(currentToken, position, sourceFile, checker);
711+
function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined {
690712
// For a union, return the first one with a recommended completion.
691713
return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => {
692714
const symbol = type && type.symbol;
693715
// Don't include make a recommended completion for an abstract class
694716
return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol))
695-
? getFirstSymbolInChain(symbol, currentToken, checker)
717+
? getFirstSymbolInChain(symbol, previousToken, checker)
696718
: undefined;
697719
});
698720
}
699721

700-
function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
701-
const { parent } = currentToken;
702-
switch (currentToken.kind) {
722+
function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
723+
const { parent } = previousToken;
724+
switch (previousToken.kind) {
703725
case SyntaxKind.Identifier:
704-
return getContextualTypeFromParent(currentToken as Identifier, checker);
726+
return getContextualTypeFromParent(previousToken as Identifier, checker);
705727
case SyntaxKind.EqualsToken:
706728
switch (parent.kind) {
707729
case SyntaxKind.VariableDeclaration:
@@ -720,14 +742,14 @@ namespace ts.Completions {
720742
case SyntaxKind.OpenBraceToken:
721743
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
722744
default:
723-
const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile);
745+
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile);
724746
return argInfo
725747
// At `,`, treat this as the next argument after the comma.
726-
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0))
727-
: isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
748+
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0))
749+
: isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
728750
// completion at `x ===/**/` should be for the right side
729751
? checker.getTypeAtLocation(parent.left)
730-
: checker.getContextualType(currentToken as Expression);
752+
: checker.getContextualType(previousToken as Expression);
731753
}
732754
}
733755

@@ -1005,8 +1027,11 @@ namespace ts.Completions {
10051027

10061028
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
10071029

1008-
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker);
1009-
return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
1030+
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
1031+
const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined);
1032+
1033+
const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker);
1034+
return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
10101035

10111036
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
10121037

src/services/services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ namespace ts {
426426
return !!(this.flags & TypeFlags.UnionOrIntersection);
427427
}
428428
isLiteral(): this is LiteralType {
429-
return !!(this.flags & TypeFlags.Literal);
429+
return !!(this.flags & TypeFlags.StringOrNumberLiteral);
430430
}
431431
isStringLiteral(): this is StringLiteralType {
432432
return !!(this.flags & TypeFlags.StringLiteral);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////const x: 0 | "one" = /**/;
4+
5+
verify.completions({
6+
marker: "",
7+
includes: [
8+
{ name: "0", kind: "string", text: "0" },
9+
{ name: '"one"', kind: "string", text: '"one"' },
10+
],
11+
isNewIdentifierLocation: true,
12+
});

0 commit comments

Comments
 (0)