Simplify string literal completions (#21415)

This commit is contained in:
Andy
2018-02-06 12:31:34 -08:00
committed by GitHub
parent c4c9a00a9f
commit ba1833759f
2 changed files with 155 additions and 191 deletions
+133 -174
View File
@@ -36,7 +36,7 @@ namespace ts.Completions {
): CompletionInfo | undefined {
if (isInReferenceComment(sourceFile, position)) {
const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
return entries && pathCompletionsInfo(entries);
return entries && convertPathCompletions(entries);
}
const contextToken = findPrecedingToken(position, sourceFile);
@@ -44,7 +44,7 @@ namespace ts.Completions {
if (isInString(sourceFile, position, contextToken)) {
return !contextToken || !isStringLiteral(contextToken) && !isNoSubstitutionTemplateLiteral(contextToken)
? undefined
: getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host, log);
: convertStringLiteralCompletions(getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host), sourceFile, typeChecker, log);
}
if (contextToken && isBreakOrContinueStatement(contextToken.parent)
@@ -73,6 +73,34 @@ namespace ts.Completions {
}
}
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log): CompletionInfo | undefined {
if (completion === undefined) {
return undefined;
}
switch (completion.kind) {
case StringLiteralCompletionKind.Paths:
return convertPathCompletions(completion.paths);
case StringLiteralCompletionKind.Properties: {
const entries: CompletionEntry[] = [];
getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String); // Target will not be used, so arbitrary
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
}
case StringLiteralCompletionKind.Types: {
const entries = completion.types.map(type => ({ name: type.value, kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.variableElement, sortText: "0" }));
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries };
}
default:
return Debug.assertNever(completion);
}
}
function convertPathCompletions(pathCompletions: ReadonlyArray<PathCompletions.PathCompletion>): CompletionInfo {
const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of.
const entries = pathCompletions.map(({ name, kind, span }) => ({ name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: "0", replacementSpan: span }));
return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
}
function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
}
@@ -294,156 +322,6 @@ namespace ts.Completions {
}
}
function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined {
switch (node.parent.kind) {
case SyntaxKind.LiteralType:
switch (node.parent.parent.kind) {
case SyntaxKind.TypeReference:
// TODO: GH#21168
return undefined;
case SyntaxKind.IndexedAccessType:
// Get all apparent property names
// i.e. interface Foo {
// foo: string;
// bar: string;
// }
// let x: Foo["/*completion position*/"]
const type = typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType);
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
default:
return undefined;
}
case SyntaxKind.PropertyAssignment:
if (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression &&
(<PropertyAssignment>node.parent).name === node) {
// Get quoted name of properties of the object literal expression
// i.e. interface ConfigFiles {
// 'jspm:dev': string
// }
// let files: ConfigFiles = {
// '/*completion position*/'
// }
//
// function foo(c: ConfigFiles) {}
// foo({
// '/*completion position*/'
// });
return getStringLiteralCompletionEntriesFromPropertyAssignment(<PropertyAssignment>node.parent, sourceFile, typeChecker, compilerOptions.target, log);
}
return fromContextualType();
case SyntaxKind.ElementAccessExpression: {
const { expression, argumentExpression } = node.parent as ElementAccessExpression;
if (node === argumentExpression) {
// Get all names of properties on the expression
// i.e. interface A {
// 'prop1': string
// }
// let a: A;
// a['/*completion position*/']
const type = typeChecker.getTypeAtLocation(expression);
return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log);
}
break;
}
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteral*/ false) && !isImportCall(node.parent)) {
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
// Get string literal completions from specialized signatures of the target
// i.e. declare function f(a: 'A');
// f("/*completion position*/")
return argumentInfo ? getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, typeChecker) : fromContextualType();
}
// falls through
case SyntaxKind.ImportDeclaration:
case SyntaxKind.ExportDeclaration:
case SyntaxKind.ExternalModuleReference:
// Get all known external module names or complete a path to a module
// i.e. import * as ns from "/*completion position*/";
// var y = import("/*completion position*/");
// import x = require("/*completion position*/");
// var y = require("/*completion position*/");
// export * from "/*completion position*/";
return pathCompletionsInfo(PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker));
default:
return fromContextualType();
}
function fromContextualType(): CompletionInfo {
// Get completion for string literal from string literal type
// i.e. var x: "hi" | "hello" = "/*completion position*/"
return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker);
}
}
function pathCompletionsInfo(entries: CompletionEntry[]): CompletionInfo {
return {
// We don't want the editor to offer any other completions, such as snippets, inside a comment.
isGlobalCompletion: false,
isMemberCompletion: false,
// The user may type in a path that doesn't yet exist, creating a "new identifier"
// with respect to the collection of identifiers the server is aware of.
isNewIdentifierLocation: true,
entries,
};
}
function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
const type = typeChecker.getContextualType((<ObjectLiteralExpression>element.parent));
const entries: CompletionEntry[] = [];
if (type) {
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, sourceFile, typeChecker, target, log, CompletionKind.String);
if (entries.length) {
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
}
}
}
function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, typeChecker: TypeChecker): CompletionInfo | undefined {
const candidates: Signature[] = [];
const entries: CompletionEntry[] = [];
const uniques = createMap<true>();
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
for (const candidate of candidates) {
addStringLiteralCompletionsFromType(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), entries, typeChecker, uniques);
}
if (entries.length) {
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries };
}
return undefined;
}
function getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(stringLiteralNode: StringLiteral | NoSubstitutionTemplateLiteral, sourceFile: SourceFile, type: Type, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
const entries: CompletionEntry[] = [];
if (type) {
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, stringLiteralNode, sourceFile, typeChecker, target, log, CompletionKind.String);
if (entries.length) {
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
}
}
return undefined;
}
function getStringLiteralCompletionEntriesFromType(type: Type, typeChecker: TypeChecker): CompletionInfo | undefined {
if (type) {
const entries: CompletionEntry[] = [];
addStringLiteralCompletionsFromType(type, entries, typeChecker);
if (entries.length) {
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
}
}
return undefined;
}
function getLabelStatementCompletions(node: Node): CompletionEntry[] {
const entries: CompletionEntry[] = [];
const uniques = createMap<true>();
@@ -470,30 +348,111 @@ namespace ts.Completions {
return entries;
}
function addStringLiteralCompletionsFromType(type: Type, result: Push<CompletionEntry>, typeChecker: TypeChecker, uniques = createMap<true>()): void {
const enum StringLiteralCompletionKind { Paths, Properties, Types }
type StringLiteralCompletion =
| { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: ReadonlyArray<PathCompletions.PathCompletion> }
| { readonly kind: StringLiteralCompletionKind.Properties, readonly symbols: ReadonlyArray<Symbol> }
| { readonly kind: StringLiteralCompletionKind.Types, readonly types: ReadonlyArray<StringLiteralType> };
function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined {
switch (node.parent.kind) {
case SyntaxKind.LiteralType:
switch (node.parent.parent.kind) {
case SyntaxKind.TypeReference:
// TODO: GH#21168
return undefined;
case SyntaxKind.IndexedAccessType:
// Get all apparent property names
// i.e. interface Foo {
// foo: string;
// bar: string;
// }
// let x: Foo["/*completion position*/"]
return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType).getApparentProperties() };
default:
return undefined;
}
case SyntaxKind.PropertyAssignment:
if (isObjectLiteralExpression(node.parent.parent) && (<PropertyAssignment>node.parent).name === node) {
// Get quoted name of properties of the object literal expression
// i.e. interface ConfigFiles {
// 'jspm:dev': string
// }
// let files: ConfigFiles = {
// '/*completion position*/'
// }
//
// function foo(c: ConfigFiles) {}
// foo({
// '/*completion position*/'
// });
const type = typeChecker.getContextualType(node.parent.parent);
return { kind: StringLiteralCompletionKind.Properties, symbols: type && type.getApparentProperties() };
}
return fromContextualType();
case SyntaxKind.ElementAccessExpression: {
const { expression, argumentExpression } = node.parent as ElementAccessExpression;
if (node === argumentExpression) {
// Get all names of properties on the expression
// i.e. interface A {
// 'prop1': string
// }
// let a: A;
// a['/*completion position*/']
return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeAtLocation(expression).getApparentProperties() };
}
return undefined;
}
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteral*/ false) && !isImportCall(node.parent)) {
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
// Get string literal completions from specialized signatures of the target
// i.e. declare function f(a: 'A');
// f("/*completion position*/")
if (argumentInfo) {
const candidates: Signature[] = [];
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
const uniques = createMap<true>();
return { kind: StringLiteralCompletionKind.Types, types: flatMap(candidates, candidate => getStringLiteralTypes(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), typeChecker, uniques)) };
}
return fromContextualType();
}
// falls through (is `require("")` or `import("")`)
case SyntaxKind.ImportDeclaration:
case SyntaxKind.ExportDeclaration:
case SyntaxKind.ExternalModuleReference:
// Get all known external module names or complete a path to a module
// i.e. import * as ns from "/*completion position*/";
// var y = import("/*completion position*/");
// import x = require("/*completion position*/");
// var y = require("/*completion position*/");
// export * from "/*completion position*/";
return { kind: StringLiteralCompletionKind.Paths, paths: PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) };
default:
return fromContextualType();
}
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) };
}
}
function getStringLiteralTypes(type: Type, typeChecker: TypeChecker, uniques = createMap<true>()): ReadonlyArray<StringLiteralType> {
if (type && type.flags & TypeFlags.TypeParameter) {
type = typeChecker.getBaseConstraintOfType(type);
}
if (!type) {
return;
}
if (type.flags & TypeFlags.Union) {
for (const t of (<UnionType>type).types) {
addStringLiteralCompletionsFromType(t, result, typeChecker, uniques);
}
}
else if (type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral)) {
const name = (<StringLiteralType>type).value;
if (!uniques.has(name)) {
uniques.set(name, true);
result.push({
name,
kindModifiers: ScriptElementKindModifier.none,
kind: ScriptElementKind.variableElement,
sortText: "0"
});
}
type = type.getConstraint();
}
return type && type.flags & TypeFlags.Union
? flatMap((<UnionType>type).types, t => getStringLiteralTypes(t, typeChecker, uniques))
: type && type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, (type as StringLiteralType).value)
? [type as StringLiteralType]
: emptyArray;
}
interface SymbolCompletion {
+22 -17
View File
@@ -1,6 +1,15 @@
/* @internal */
namespace ts.Completions.PathCompletions {
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
export interface PathCompletion {
readonly name: string;
readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName;
readonly span: TextSpan;
}
function createPathCompletion(name: string, kind: PathCompletion["kind"], span: TextSpan): PathCompletion {
return { name, kind, span };
}
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): PathCompletion[] {
const literalValue = normalizeSlashes(node.text);
const scriptPath = node.getSourceFile().path;
@@ -43,12 +52,12 @@ namespace ts.Completions.PathCompletions {
compareStringsCaseSensitive);
}
function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray<string>, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] {
function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray<string>, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): PathCompletion[] {
const basePath = compilerOptions.project || host.getCurrentDirectory();
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptPath, ignoreCase);
const result: CompletionEntry[] = [];
const result: PathCompletion[] = [];
for (const baseDirectory of baseDirectories) {
getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensions, includeExtensions, span, host, exclude, result);
@@ -60,7 +69,7 @@ namespace ts.Completions.PathCompletions {
/**
* Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename.
*/
function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: ReadonlyArray<string>, includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] {
function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: ReadonlyArray<string>, includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: PathCompletion[] = []): PathCompletion[] {
if (fragment === undefined) {
fragment = "";
}
@@ -109,7 +118,7 @@ namespace ts.Completions.PathCompletions {
}
forEachKey(foundFiles, foundFile => {
result.push(createCompletionEntryForModule(foundFile, ScriptElementKind.scriptElement, span));
result.push(createPathCompletion(foundFile, ScriptElementKind.scriptElement, span));
});
}
@@ -120,7 +129,7 @@ namespace ts.Completions.PathCompletions {
for (const directory of directories) {
const directoryName = getBaseFileName(normalizePath(directory));
result.push(createCompletionEntryForModule(directoryName, ScriptElementKind.directory, span));
result.push(createPathCompletion(directoryName, ScriptElementKind.directory, span));
}
}
}
@@ -135,10 +144,10 @@ namespace ts.Completions.PathCompletions {
* Modules from node_modules (i.e. those listed in package.json)
* This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
*/
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): PathCompletion[] {
const { baseUrl, paths } = compilerOptions;
const result: CompletionEntry[] = [];
const result: PathCompletion[] = [];
const fileExtensions = getSupportedExtensions(compilerOptions);
if (baseUrl) {
@@ -152,7 +161,7 @@ namespace ts.Completions.PathCompletions {
for (const pathCompletion of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) {
// Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
if (!result.some(entry => entry.name === pathCompletion)) {
result.push(createCompletionEntryForModule(pathCompletion, ScriptElementKind.externalModuleName, span));
result.push(createPathCompletion(pathCompletion, ScriptElementKind.externalModuleName, span));
}
}
}
@@ -171,7 +180,7 @@ namespace ts.Completions.PathCompletions {
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span, result);
for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) {
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span));
result.push(createPathCompletion(moduleName, ScriptElementKind.externalModuleName, span));
}
return result;
@@ -285,7 +294,7 @@ namespace ts.Completions.PathCompletions {
return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive);
}
export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionEntry[] | undefined {
export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): PathCompletion[] | undefined {
const token = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end);
@@ -316,7 +325,7 @@ namespace ts.Completions.PathCompletions {
}
}
function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] {
function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: PathCompletion[] = []): PathCompletion[] {
// Check for typings specified in compiler options
const seen = createMap<true>();
if (options.types) {
@@ -364,7 +373,7 @@ namespace ts.Completions.PathCompletions {
function pushResult(moduleName: string) {
if (!seen.has(moduleName)) {
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span));
result.push(createPathCompletion(moduleName, ScriptElementKind.externalModuleName, span));
seen.set(moduleName, true);
}
}
@@ -433,10 +442,6 @@ namespace ts.Completions.PathCompletions {
}
}
function createCompletionEntryForModule(name: string, kind: ScriptElementKind, replacementSpan: TextSpan): CompletionEntry {
return { name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: name, replacementSpan };
}
// Replace everything after the last directory seperator that appears
function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan {
const index = text.lastIndexOf(directorySeparator);