diff --git a/src/services/completions.ts b/src/services/completions.ts index 667b1e573acc6..448d8f7c37b1c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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): 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 }; } @@ -291,7 +319,38 @@ namespace ts.Completions { } } - function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined { + function getLabelStatementCompletions(node: Node): CompletionEntry[] { + const entries: CompletionEntry[] = []; + const uniques = createMap(); + let current = node; + + while (current) { + if (isFunctionLike(current)) { + break; + } + if (isLabeledStatement(current)) { + const name = current.label.text; + if (!uniques.has(name)) { + uniques.set(name, true); + entries.push({ + name, + kindModifiers: ScriptElementKindModifier.none, + kind: ScriptElementKind.label, + sortText: "0" + }); + } + } + current = current.parent; + } + return entries; + } + + const enum StringLiteralCompletionKind { Paths, Properties, Types } + type StringLiteralCompletion = + | { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: ReadonlyArray } + | { readonly kind: StringLiteralCompletionKind.Properties, readonly symbols: ReadonlyArray } + | { readonly kind: StringLiteralCompletionKind.Types, readonly types: ReadonlyArray }; + 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) { @@ -305,15 +364,13 @@ namespace ts.Completions { // 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); + return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType).getApparentProperties() }; default: return undefined; } case SyntaxKind.PropertyAssignment: - if (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression && - (node.parent).name === node) { + if (isObjectLiteralExpression(node.parent.parent) && (node.parent).name === node) { // Get quoted name of properties of the object literal expression // i.e. interface ConfigFiles { // 'jspm:dev': string @@ -326,7 +383,8 @@ namespace ts.Completions { // foo({ // '/*completion position*/' // }); - return getStringLiteralCompletionEntriesFromPropertyAssignment(node.parent, sourceFile, typeChecker, compilerOptions.target, log); + const type = typeChecker.getContextualType(node.parent.parent); + return { kind: StringLiteralCompletionKind.Properties, symbols: type && type.getApparentProperties() }; } return fromContextualType(); @@ -339,10 +397,9 @@ namespace ts.Completions { // } // let a: A; // a['/*completion position*/'] - const type = typeChecker.getTypeAtLocation(expression); - return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log); + return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeAtLocation(expression).getApparentProperties() }; } - break; + return undefined; } case SyntaxKind.CallExpression: @@ -352,9 +409,15 @@ namespace ts.Completions { // 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(); + if (argumentInfo) { + const candidates: Signature[] = []; + typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); + const uniques = createMap(); + return { kind: StringLiteralCompletionKind.Types, types: flatMap(candidates, candidate => getStringLiteralTypes(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), typeChecker, uniques)) }; + } + return fromContextualType(); } - // falls through + // falls through (is `require("")` or `import("")`) case SyntaxKind.ImportDeclaration: case SyntaxKind.ExportDeclaration: @@ -365,132 +428,28 @@ namespace ts.Completions { // import x = require("/*completion position*/"); // var y = require("/*completion position*/"); // export * from "/*completion position*/"; - return pathCompletionsInfo(PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker)); + return { kind: StringLiteralCompletionKind.Paths, paths: PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; default: return fromContextualType(); } - function fromContextualType(): CompletionInfo { + function fromContextualType(): StringLiteralCompletion { // Get completion for string literal from string literal type // i.e. var x: "hi" | "hello" = "/*completion position*/" - return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker); + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(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((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(); - - 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(); - let current = node; - - while (current) { - if (isFunctionLike(current)) { - break; - } - if (isLabeledStatement(current)) { - const name = current.label.text; - if (!uniques.has(name)) { - uniques.set(name, true); - entries.push({ - name, - kindModifiers: ScriptElementKindModifier.none, - kind: ScriptElementKind.label, - sortText: "0" - }); - } - } - current = current.parent; - } - return entries; - } - - function addStringLiteralCompletionsFromType(type: Type, result: Push, typeChecker: TypeChecker, uniques = createMap()): void { + function getStringLiteralTypes(type: Type, typeChecker: TypeChecker, uniques = createMap()): ReadonlyArray { if (type && type.flags & TypeFlags.TypeParameter) { - type = typeChecker.getBaseConstraintOfType(type); - } - if (!type) { - return; - } - if (type.flags & TypeFlags.Union) { - for (const t of (type).types) { - addStringLiteralCompletionsFromType(t, result, typeChecker, uniques); - } - } - else if (type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral)) { - const name = (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((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 { diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index d27417ec521dd..0172d3bfce275 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -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, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] { + function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray, 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, includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] { + function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: ReadonlyArray, 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; @@ -282,7 +291,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); @@ -313,7 +322,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(); if (options.types) { @@ -361,7 +370,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); } } @@ -430,10 +439,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);