From 33d08932596cf49f45958e370329d1852758298a Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 7 Jun 2018 15:03:19 -0700 Subject: [PATCH] Add completions from literal contextual types (#24674) * Add completions from literal contextual types * Remove getTypesOfUnion * undo baseline changes --- src/services/completions.ts | 69 +++++++++++++------- src/services/services.ts | 2 +- tests/cases/fourslash/completionsLiterals.ts | 12 ++++ 3 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 tests/cases/fourslash/completionsLiterals.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 707e5fc0b01..e31ac4e81f3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -101,7 +101,7 @@ namespace ts.Completions { } function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined { - const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; + const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) { // 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 { addRange(entries, getKeywordCompletions(keywordFilters)); } + for (const literal of literals) { + entries.push(createCompletionEntryForLiteral(literal)); + } + return { isGlobalCompletion: isInSnippetScope, isMemberCompletion, isNewIdentifierLocation, entries }; } @@ -184,6 +188,11 @@ namespace ts.Completions { }); } + const completionNameForLiteral = JSON.stringify; + function createCompletionEntryForLiteral(literal: string | number): CompletionEntry { + return { name: completionNameForLiteral(literal), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: "0" }; + } + function createCompletionEntry( symbol: Symbol, location: Node | undefined, @@ -372,7 +381,7 @@ namespace ts.Completions { case SyntaxKind.LiteralType: switch (node.parent.parent.kind) { case SyntaxKind.TypeReference: - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode), typeChecker), isNewIdentifier: false }; + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(node.parent as LiteralTypeNode)), isNewIdentifier: false }; case SyntaxKind.IndexedAccessType: // Get all apparent property names // i.e. interface Foo { @@ -448,7 +457,7 @@ namespace ts.Completions { function fromContextualType(): StringLiteralCompletion { // Get completion for string literal from string literal type // i.e. var x: "hi" | "hello" = "/*completion position*/" - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker), isNewIdentifier: false }; + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; } } @@ -462,7 +471,7 @@ namespace ts.Completions { if (!candidate.hasRestParameter && argumentInfo.argumentCount > candidate.parameters.length) return; const type = checker.getParameterType(candidate, argumentInfo.argumentIndex); isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); - return getStringLiteralTypes(type, checker, uniques); + return getStringLiteralTypes(type, uniques); }); return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; @@ -472,11 +481,11 @@ namespace ts.Completions { return type && { kind: StringLiteralCompletionKind.Properties, symbols: type.getApparentProperties(), hasIndexSignature: hasIndexSignature(type) }; } - function getStringLiteralTypes(type: Type | undefined, typeChecker: TypeChecker, uniques = createMap()): ReadonlyArray { + function getStringLiteralTypes(type: Type | undefined, uniques = createMap()): ReadonlyArray { if (!type) return emptyArray; type = skipConstraint(type); return type.isUnion() - ? flatMap(type.types, t => getStringLiteralTypes(t, typeChecker, uniques)) + ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray; @@ -491,7 +500,7 @@ namespace ts.Completions { readonly isJsxInitializer: IsJsxInitializer; } function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, - ): SymbolCompletion | { type: "request", request: Request } | { type: "none" } { + ): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number } | { type: "none" } { const compilerOptions = program.getCompilerOptions(); const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId); if (!completionData) { @@ -501,7 +510,10 @@ namespace ts.Completions { return { type: "request", request: completionData }; } - const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData; + const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData; + + const literal = find(literals, l => completionNameForLiteral(l) === entryId.name); + if (literal !== undefined) return { type: "literal", literal }; // Find the symbol with the matching entry name. // We don't need to perform character checks here because we're only comparing the @@ -574,12 +586,22 @@ namespace ts.Completions { const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences); return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } + case "literal": { + const { literal } = symbolCompletion; + return createSimpleDetails(completionNameForLiteral(literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral); + } case "none": // Didn't find a symbol with this name. See if we can find a keyword instead. - return allKeywordsCompletions().some(c => c.name === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.keyword, [displayPart(name, SymbolDisplayPartKind.keyword)]) : undefined; + return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined; + default: + Debug.assertNever(symbolCompletion); } } + function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails { + return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]); + } + function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails { const { displayParts, documentation, symbolKind, tags } = checker.runWithCancellationToken(cancellationToken, checker => @@ -669,6 +691,7 @@ namespace ts.Completions { readonly isNewIdentifierLocation: boolean; readonly location: Node | undefined; readonly keywordFilters: KeywordCompletionFilters; + readonly literals: ReadonlyArray; readonly symbolToOriginInfoMap: SymbolOriginInfoMap; readonly recommendedCompletion: Symbol | undefined; readonly previousToken: Node | undefined; @@ -685,23 +708,22 @@ namespace ts.Completions { None, } - function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined { - const contextualType = getContextualType(currentToken, position, sourceFile, checker); + function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined { // For a union, return the first one with a recommended completion. return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => { const symbol = type && type.symbol; // Don't include make a recommended completion for an abstract class return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol)) - ? getFirstSymbolInChain(symbol, currentToken, checker) + ? getFirstSymbolInChain(symbol, previousToken, checker) : undefined; }); } - function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { - const { parent } = currentToken; - switch (currentToken.kind) { + function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { + const { parent } = previousToken; + switch (previousToken.kind) { case SyntaxKind.Identifier: - return getContextualTypeFromParent(currentToken as Identifier, checker); + return getContextualTypeFromParent(previousToken as Identifier, checker); case SyntaxKind.EqualsToken: switch (parent.kind) { case SyntaxKind.VariableDeclaration: @@ -720,14 +742,14 @@ namespace ts.Completions { case SyntaxKind.OpenBraceToken: return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: - const argInfo = SignatureHelp.getArgumentInfoForCompletions(currentToken, position, sourceFile); + const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); return argInfo // At `,`, treat this as the next argument after the comma. - ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0)) - : isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) + ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) + : isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) // completion at `x ===/**/` should be for the right side ? checker.getTypeAtLocation(parent.left) - : checker.getContextualType(currentToken as Expression); + : checker.getContextualType(previousToken as Expression); } } @@ -1005,8 +1027,11 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker); - return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; + const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); + const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined); + + const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker); + return { kind: CompletionDataKind.Data, symbols, completionKind, isInSnippetScope, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, literals, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; diff --git a/src/services/services.ts b/src/services/services.ts index 0f05a542885..7d88d334b38 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -426,7 +426,7 @@ namespace ts { return !!(this.flags & TypeFlags.UnionOrIntersection); } isLiteral(): this is LiteralType { - return !!(this.flags & TypeFlags.Literal); + return !!(this.flags & TypeFlags.StringOrNumberLiteral); } isStringLiteral(): this is StringLiteralType { return !!(this.flags & TypeFlags.StringLiteral); diff --git a/tests/cases/fourslash/completionsLiterals.ts b/tests/cases/fourslash/completionsLiterals.ts new file mode 100644 index 00000000000..475d3dd247e --- /dev/null +++ b/tests/cases/fourslash/completionsLiterals.ts @@ -0,0 +1,12 @@ +/// + +////const x: 0 | "one" = /**/; + +verify.completions({ + marker: "", + includes: [ + { name: "0", kind: "string", text: "0" }, + { name: '"one"', kind: "string", text: '"one"' }, + ], + isNewIdentifierLocation: true, +});