• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.Completions.StringCompletions {
3    export function getStringLiteralCompletions(sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, log: Log, preferences: UserPreferences): CompletionInfo | undefined {
4        if (isInReferenceComment(sourceFile, position)) {
5            const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host);
6            return entries && convertPathCompletions(entries);
7        }
8        if (isInString(sourceFile, position, contextToken)) {
9            if (!contextToken || !isStringLiteralLike(contextToken)) return undefined;
10            const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host);
11            return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, preferences);
12        }
13    }
14
15    function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined {
16        if (completion === undefined) {
17            return undefined;
18        }
19
20        const optionalReplacementSpan = createTextSpanFromStringLiteralLikeContent(contextToken);
21        switch (completion.kind) {
22            case StringLiteralCompletionKind.Paths:
23                return convertPathCompletions(completion.paths);
24            case StringLiteralCompletionKind.Properties: {
25                const entries: CompletionEntry[] = [];
26                getCompletionEntriesFromSymbols(
27                    completion.symbols,
28                    entries,
29                    contextToken,
30                    sourceFile,
31                    sourceFile,
32                    checker,
33                    ScriptTarget.ESNext,
34                    log,
35                    CompletionKind.String,
36                    preferences
37                ); // Target will not be used, so arbitrary
38                return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries };
39            }
40            case StringLiteralCompletionKind.Types: {
41                const entries = completion.types.map(type => ({
42                    name: type.value,
43                    kindModifiers: ScriptElementKindModifier.none,
44                    kind: ScriptElementKind.string,
45                    sortText: SortText.LocationPriority,
46                    replacementSpan: getReplacementSpanForContextToken(contextToken)
47                }));
48                return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries };
49            }
50            default:
51                return Debug.assertNever(completion);
52        }
53    }
54
55    export function getStringLiteralCompletionDetails(name: string, sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, cancellationToken: CancellationToken) {
56        if (!contextToken || !isStringLiteralLike(contextToken)) return undefined;
57        const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host);
58        return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken);
59    }
60
61    function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined {
62        switch (completion.kind) {
63            case StringLiteralCompletionKind.Paths: {
64                const match = find(completion.paths, p => p.name === name);
65                return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]);
66            }
67            case StringLiteralCompletionKind.Properties: {
68                const match = find(completion.symbols, s => s.name === name);
69                return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken);
70            }
71            case StringLiteralCompletionKind.Types:
72                return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.typeElement, [textPart(name)]) : undefined;
73            default:
74                return Debug.assertNever(completion);
75        }
76    }
77
78    function convertPathCompletions(pathCompletions: readonly PathCompletion[]): CompletionInfo {
79        const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
80        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.
81        const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry =>
82            ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: SortText.LocationPriority, replacementSpan: span }));
83        return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
84    }
85    function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier {
86        switch (extension) {
87            case Extension.Dts: return ScriptElementKindModifier.dtsModifier;
88            case Extension.Js: return ScriptElementKindModifier.jsModifier;
89            case Extension.Json: return ScriptElementKindModifier.jsonModifier;
90            case Extension.Jsx: return ScriptElementKindModifier.jsxModifier;
91            case Extension.Ts: return ScriptElementKindModifier.tsModifier;
92            case Extension.Tsx: return ScriptElementKindModifier.tsxModifier;
93            case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported.`);
94            case undefined: return ScriptElementKindModifier.none;
95            case Extension.Ets: return ScriptElementKindModifier.etsModifier;
96            default:
97                return Debug.assertNever(extension);
98        }
99    }
100
101    const enum StringLiteralCompletionKind { Paths, Properties, Types }
102    interface StringLiteralCompletionsFromProperties {
103        readonly kind: StringLiteralCompletionKind.Properties;
104        readonly symbols: readonly Symbol[];
105        readonly hasIndexSignature: boolean;
106    }
107    interface StringLiteralCompletionsFromTypes {
108        readonly kind: StringLiteralCompletionKind.Types;
109        readonly types: readonly StringLiteralType[];
110        readonly isNewIdentifier: boolean;
111    }
112    type StringLiteralCompletion = { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: readonly PathCompletion[] } | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes;
113    function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined {
114        const parent = walkUpParentheses(node.parent);
115        switch (parent.kind) {
116            case SyntaxKind.LiteralType: {
117                const grandParent = walkUpParentheses(parent.parent);
118                switch (grandParent.kind) {
119                    case SyntaxKind.TypeReference: {
120                        const typeReference = grandParent as TypeReferenceNode;
121                        const typeArgument = findAncestor(parent, n => n.parent === typeReference) as LiteralTypeNode;
122                        if (typeArgument) {
123                            return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false };
124                        }
125                        return undefined;
126                    }
127                    case SyntaxKind.IndexedAccessType:
128                        // Get all apparent property names
129                        // i.e. interface Foo {
130                        //          foo: string;
131                        //          bar: string;
132                        //      }
133                        //      let x: Foo["/*completion position*/"]
134                        const { indexType, objectType } = grandParent as IndexedAccessTypeNode;
135                        if (!rangeContainsPosition(indexType, position)) {
136                            return undefined;
137                        }
138                        return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
139                    case SyntaxKind.ImportType:
140                        return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) };
141                    case SyntaxKind.UnionType: {
142                        if (!isTypeReferenceNode(grandParent.parent)) {
143                            return undefined;
144                        }
145                        const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(grandParent as UnionTypeNode, parent as LiteralTypeNode);
146                        const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(grandParent as UnionTypeNode)).filter(t => !contains(alreadyUsedTypes, t.value));
147                        return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false };
148                    }
149                    default:
150                        return undefined;
151                }
152            }
153            case SyntaxKind.PropertyAssignment:
154                if (isObjectLiteralExpression(parent.parent) && (<PropertyAssignment>parent).name === node) {
155                    // Get quoted name of properties of the object literal expression
156                    // i.e. interface ConfigFiles {
157                    //          'jspm:dev': string
158                    //      }
159                    //      let files: ConfigFiles = {
160                    //          '/*completion position*/'
161                    //      }
162                    //
163                    //      function foo(c: ConfigFiles) {}
164                    //      foo({
165                    //          '/*completion position*/'
166                    //      });
167                    return stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent);
168                }
169                return fromContextualType();
170
171            case SyntaxKind.ElementAccessExpression: {
172                const { expression, argumentExpression } = parent as ElementAccessExpression;
173                if (node === skipParentheses(argumentExpression)) {
174                    // Get all names of properties on the expression
175                    // i.e. interface A {
176                    //      'prop1': string
177                    // }
178                    // let a: A;
179                    // a['/*completion position*/']
180                    return stringLiteralCompletionsFromProperties(typeChecker.getTypeAtLocation(expression));
181                }
182                return undefined;
183            }
184
185            case SyntaxKind.CallExpression:
186            case SyntaxKind.NewExpression:
187                if (!isRequireCall(parent, /*checkArgumentIsStringLiteralLike*/ false) && !isImportCall(parent)) {
188                    const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile);
189                    // Get string literal completions from specialized signatures of the target
190                    // i.e. declare function f(a: 'A');
191                    // f("/*completion position*/")
192                    return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo, typeChecker) : fromContextualType();
193                }
194                // falls through (is `require("")` or `import("")`)
195
196            case SyntaxKind.ImportDeclaration:
197            case SyntaxKind.ExportDeclaration:
198            case SyntaxKind.ExternalModuleReference:
199                // Get all known external module names or complete a path to a module
200                // i.e. import * as ns from "/*completion position*/";
201                //      var y = import("/*completion position*/");
202                //      import x = require("/*completion position*/");
203                //      var y = require("/*completion position*/");
204                //      export * from "/*completion position*/";
205                return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) };
206
207            default:
208                return fromContextualType();
209        }
210
211        function fromContextualType(): StringLiteralCompletion {
212            // Get completion for string literal from string literal type
213            // i.e. var x: "hi" | "hello" = "/*completion position*/"
214            return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false };
215        }
216    }
217
218    function walkUpParentheses(node: Node) {
219        switch (node.kind) {
220            case SyntaxKind.ParenthesizedType:
221                return walkUpParenthesizedTypes(node);
222            case SyntaxKind.ParenthesizedExpression:
223                return walkUpParenthesizedExpressions(node);
224            default:
225                return node;
226        }
227    }
228
229    function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current: LiteralTypeNode): readonly string[] {
230        return mapDefined(union.types, type =>
231            type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined);
232    }
233
234    function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes {
235        let isNewIdentifier = false;
236
237        const uniques = new Map<string, true>();
238        const candidates: Signature[] = [];
239        checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount);
240        const types = flatMap(candidates, candidate => {
241            if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return;
242            const type = checker.getParameterType(candidate, argumentInfo.argumentIndex);
243            isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String);
244            return getStringLiteralTypes(type, uniques);
245        });
246
247        return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier };
248    }
249
250    function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined {
251        return type && {
252            kind: StringLiteralCompletionKind.Properties,
253            symbols: filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && isPrivateIdentifierPropertyDeclaration(prop.valueDeclaration))),
254            hasIndexSignature: hasIndexSignature(type)
255        };
256    }
257
258    function stringLiteralCompletionsForObjectLiteral(checker: TypeChecker, objectLiteralExpression: ObjectLiteralExpression): StringLiteralCompletionsFromProperties | undefined {
259        const contextualType = checker.getContextualType(objectLiteralExpression);
260        if (!contextualType) return undefined;
261
262        const completionsType = checker.getContextualType(objectLiteralExpression, ContextFlags.Completions);
263        const symbols = getPropertiesForObjectExpression(
264            contextualType,
265            completionsType,
266            objectLiteralExpression,
267            checker
268        );
269
270        return {
271            kind: StringLiteralCompletionKind.Properties,
272            symbols,
273            hasIndexSignature: hasIndexSignature(contextualType)
274        };
275    }
276
277    function getStringLiteralTypes(type: Type | undefined, uniques = new Map<string, true>()): readonly StringLiteralType[] {
278        if (!type) return emptyArray;
279        type = skipConstraint(type);
280        return type.isUnion() ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) :
281            type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray;
282    }
283
284    interface NameAndKind {
285        readonly name: string;
286        readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName;
287        readonly extension: Extension | undefined;
288    }
289    interface PathCompletion extends NameAndKind {
290        readonly span: TextSpan | undefined;
291    }
292
293    function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind {
294        return { name, kind, extension };
295    }
296    function directoryResult(name: string): NameAndKind {
297        return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined);
298    }
299
300    function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] {
301        const span = getDirectoryFragmentTextSpan(text, textStart);
302        const wholeSpan = text.length === 0 ? undefined : createTextSpan(textStart, text.length);
303        return names.map(({ name, kind, extension }): PathCompletion =>
304            Math.max(name.indexOf(directorySeparator), name.indexOf(altDirectorySeparator)) !== -1 ? { name, kind, extension, span: wholeSpan } : { name, kind, extension, span });
305    }
306
307    function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly PathCompletion[] {
308        return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker));
309    }
310
311    function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] {
312        const literalValue = normalizeSlashes(node.text);
313
314        const scriptPath = sourceFile.path;
315        const scriptDirectory = getDirectoryPath(scriptPath);
316
317        return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue))
318            ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath)
319            : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker);
320    }
321
322    interface ExtensionOptions {
323        readonly extensions: readonly Extension[];
324        readonly includeExtensions: boolean;
325    }
326    function getExtensionOptions(compilerOptions: CompilerOptions, includeExtensions = false): ExtensionOptions {
327        return { extensions: getSupportedExtensionsForModuleResolution(compilerOptions), includeExtensions };
328    }
329    function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path) {
330        const extensionOptions = getExtensionOptions(compilerOptions);
331        if (compilerOptions.rootDirs) {
332            return getCompletionEntriesForDirectoryFragmentWithRootDirs(
333                compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath);
334        }
335        else {
336            return getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath);
337        }
338    }
339
340    function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[] {
341        const extensions = getSupportedExtensions(compilerOptions);
342        return compilerOptions.resolveJsonModule && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ?
343            extensions.concat(Extension.Json) :
344            extensions;
345    }
346
347    /**
348     * Takes a script path and returns paths for all potential folders that could be merged with its
349     * containing folder via the "rootDirs" compiler option
350     */
351    function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptDirectory: string, ignoreCase: boolean): readonly string[] {
352        // Make all paths absolute/normalized if they are not already
353        rootDirs = rootDirs.map(rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory)));
354
355        // Determine the path to the directory containing the script relative to the root directory it is contained within
356        const relativeDirectory = firstDefined(rootDirs, rootDirectory =>
357            containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!; // TODO: GH#18217
358
359        // Now find a path for each potential directory that is to be merged with the one containing the script
360        return deduplicate<string>(
361            [...rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory)), scriptDirectory],
362            equateStringsCaseSensitive,
363            compareStringsCaseSensitive);
364    }
365
366    function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude: string): readonly NameAndKind[] {
367        const basePath = compilerOptions.project || host.getCurrentDirectory();
368        const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
369        const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase);
370        return flatMap(baseDirectories, baseDirectory => getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude));
371    }
372
373    /**
374     * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename.
375     */
376    function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, { extensions, includeExtensions }: ExtensionOptions, host: LanguageServiceHost, exclude?: string, result: NameAndKind[] = []): NameAndKind[] {
377        if (fragment === undefined) {
378            fragment = "";
379        }
380
381        fragment = normalizeSlashes(fragment);
382
383        /**
384         * Remove the basename from the path. Note that we don't use the basename to filter completions;
385         * the client is responsible for refining completions.
386         */
387        if (!hasTrailingDirectorySeparator(fragment)) {
388            fragment = getDirectoryPath(fragment);
389        }
390
391        if (fragment === "") {
392            fragment = "." + directorySeparator;
393        }
394
395        fragment = ensureTrailingDirectorySeparator(fragment);
396
397        // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
398        const absolutePath = resolvePath(scriptPath, fragment);
399        const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
400
401        const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
402        if (!tryDirectoryExists(host, baseDirectory)) return result;
403
404        // Enumerate the available files if possible
405        const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/ undefined, /*include*/ ["./*"]);
406
407        if (files) {
408            /**
409             * Multiple file entries might map to the same truncated name once we remove extensions
410             * (happens iff includeExtensions === false)so we use a set-like data structure. Eg:
411             *
412             * both foo.ts and foo.tsx become foo
413             */
414            const foundFiles = new Map<string, Extension | undefined>(); // maps file to its extension
415            for (let filePath of files) {
416                filePath = normalizePath(filePath);
417                if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
418                    continue;
419                }
420
421                const foundFileName = includeExtensions || fileExtensionIs(filePath, Extension.Json) ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath));
422                foundFiles.set(foundFileName, tryGetExtensionFromPath(filePath));
423            }
424
425            foundFiles.forEach((ext, foundFile) => {
426                result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement, ext));
427            });
428        }
429
430        // If possible, get folder completion as well
431        const directories = tryGetDirectories(host, baseDirectory);
432
433        if (directories) {
434            for (const directory of directories) {
435                const directoryName = getBaseFileName(normalizePath(directory));
436                if (directoryName !== "@types") {
437                    result.push(directoryResult(directoryName));
438                }
439            }
440        }
441
442        // check for a version redirect
443        const packageJsonPath = findPackageJson(baseDirectory, host);
444        if (packageJsonPath) {
445            const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined });
446            const typesVersions = (packageJson as any).typesVersions;
447            if (typeof typesVersions === "object") {
448                const versionResult = getPackageJsonTypesVersionsPaths(typesVersions);
449                const versionPaths = versionResult && versionResult.paths;
450                const rest = absolutePath.slice(ensureTrailingDirectorySeparator(baseDirectory).length);
451                if (versionPaths) {
452                    addCompletionEntriesFromPaths(result, rest, baseDirectory, extensions, versionPaths, host);
453                }
454            }
455        }
456
457        return result;
458    }
459
460    function addCompletionEntriesFromPaths(result: NameAndKind[], fragment: string, baseDirectory: string, fileExtensions: readonly string[], paths: MapLike<string[]>, host: LanguageServiceHost) {
461        for (const path in paths) {
462            if (!hasProperty(paths, path)) continue;
463            const patterns = paths[path];
464            if (patterns) {
465                for (const { name, kind, extension } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) {
466                    // Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
467                    if (!result.some(entry => entry.name === name)) {
468                        result.push(nameAndKind(name, kind, extension));
469                    }
470                }
471            }
472        }
473    }
474
475    /**
476     * Check all of the declared modules and those in node modules. Possible sources of modules:
477     *      Modules that are found by the type checker
478     *      Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option)
479     *      Modules from node_modules (i.e. those listed in package.json)
480     *          This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
481     */
482    function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] {
483        const { baseUrl, paths } = compilerOptions;
484
485        const result: NameAndKind[] = [];
486
487        const extensionOptions = getExtensionOptions(compilerOptions);
488        if (baseUrl) {
489            const projectDir = compilerOptions.project || host.getCurrentDirectory();
490            const absolute = normalizePath(combinePaths(projectDir, baseUrl));
491            getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result);
492            if (paths) {
493                addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions.extensions, paths, host);
494            }
495        }
496
497        const fragmentDirectory = getFragmentDirectory(fragment);
498        for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
499            result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
500        }
501
502        getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
503
504        if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) {
505            // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
506            // (But do if we didn't find anything, e.g. 'package.json' missing.)
507            let foundGlobal = false;
508            if (fragmentDirectory === undefined) {
509                for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
510                    if (!result.some(entry => entry.name === moduleName)) {
511                        foundGlobal = true;
512                        result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
513                    }
514                }
515            }
516            if (!foundGlobal) {
517                forEachAncestorDirectory(scriptPath, ancestor => {
518                    const nodeModules = combinePaths(ancestor, "node_modules");
519                    if (tryDirectoryExists(host, nodeModules)) {
520                        getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result);
521                    }
522                });
523            }
524        }
525
526        return result;
527    }
528
529    function getFragmentDirectory(fragment: string): string | undefined {
530        return containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
531    }
532
533    function getCompletionsForPathMapping(
534        path: string, patterns: readonly string[], fragment: string, baseUrl: string, fileExtensions: readonly string[], host: LanguageServiceHost,
535    ): readonly NameAndKind[] {
536        if (!endsWith(path, "*")) {
537            // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion.
538            return !stringContains(path, "*") ? justPathMappingName(path) : emptyArray;
539        }
540
541        const pathPrefix = path.slice(0, path.length - 1);
542        const remainingFragment = tryRemovePrefix(fragment, pathPrefix);
543        return remainingFragment === undefined ? justPathMappingName(pathPrefix) : flatMap(patterns, pattern =>
544            getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host));
545
546        function justPathMappingName(name: string): readonly NameAndKind[] {
547            return startsWith(name, fragment) ? [directoryResult(name)] : emptyArray;
548        }
549    }
550
551    function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: readonly string[], host: LanguageServiceHost): readonly NameAndKind[] | undefined {
552        if (!host.readDirectory) {
553            return undefined;
554        }
555
556        const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
557        if (!parsed) {
558            return undefined;
559        }
560
561        // The prefix has two effective parts: the directory path and the base component after the filepath that is not a
562        // full directory component. For example: directory/path/of/prefix/base*
563        const normalizedPrefix = resolvePath(parsed.prefix);
564        const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix);
565        const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix);
566
567        const fragmentHasPath = containsSlash(fragment);
568        const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
569
570        // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
571        const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory;
572
573        const normalizedSuffix = normalizePath(parsed.suffix);
574        // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b".
575        const baseDirectory = normalizePath(combinePaths(baseUrl, expandedPrefixDirectory));
576        const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
577
578        // If we have a suffix, then we need to read the directory all the way down. We could create a glob
579        // that encodes the suffix, but we would have to escape the character "?" which readDirectory
580        // doesn't support. For now, this is safer but slower
581        const includeGlob = normalizedSuffix ? "**/*" : "./*";
582
583        const matches = mapDefined(tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => {
584            const extension = tryGetExtensionFromPath(match);
585            const name = trimPrefixAndSuffix(match);
586            return name === undefined ? undefined : nameAndKind(removeFileExtension(name), ScriptElementKind.scriptElement, extension);
587        });
588        const directories = mapDefined(tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)), dir => {
589            const name = trimPrefixAndSuffix(dir);
590            return name === undefined ? undefined : directoryResult(name);
591        });
592        return [...matches, ...directories];
593
594        function trimPrefixAndSuffix(path: string): string | undefined {
595            const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix);
596            return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner);
597        }
598    }
599
600    function withoutStartAndEnd(s: string, start: string, end: string): string | undefined {
601        return startsWith(s, start) && endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined;
602    }
603
604    function removeLeadingDirectorySeparator(path: string): string {
605        return path[0] === directorySeparator ? path.slice(1) : path;
606    }
607
608    function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): readonly string[] {
609        // Get modules that the type checker picked up
610        const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name));
611        const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment));
612
613        // Nested modules of the form "module-name/sub" need to be adjusted to only return the string
614        // after the last '/' that appears in the fragment because that's where the replacement span
615        // starts
616        if (fragmentDirectory !== undefined) {
617            const moduleNameWithSeparator = ensureTrailingDirectorySeparator(fragmentDirectory);
618            return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeparator));
619        }
620        return nonRelativeModuleNames;
621    }
622
623    function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): readonly PathCompletion[] | undefined {
624        const token = getTokenAtPosition(sourceFile, position);
625        const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
626        const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end);
627        if (!range) {
628            return undefined;
629        }
630        const text = sourceFile.text.slice(range.pos, position);
631        const match = tripleSlashDirectiveFragmentRegex.exec(text);
632        if (!match) {
633            return undefined;
634        }
635
636        const [, prefix, kind, toComplete] = match;
637        const scriptPath = getDirectoryPath(sourceFile.path);
638        const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, /*includeExtensions*/ true), host, sourceFile.path)
639            : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions))
640            : Debug.fail();
641        return addReplacementSpans(toComplete, range.pos + prefix.length, names);
642    }
643
644    function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result: NameAndKind[] = []): readonly NameAndKind[] {
645        // Check for typings specified in compiler options
646        const seen = new Map<string, true>();
647
648        const typeRoots = tryAndIgnoreErrors(() => getEffectiveTypeRoots(options, host)) || emptyArray;
649
650        for (const root of typeRoots) {
651            getCompletionEntriesFromDirectories(root);
652        }
653
654        // Also get all @types typings installed in visible node_modules directories
655        for (const packageJson of findPackageJsons(scriptPath, host)) {
656            const typesDir = combinePaths(getDirectoryPath(packageJson), "node_modules/@types");
657            getCompletionEntriesFromDirectories(typesDir);
658        }
659
660        return result;
661
662        function getCompletionEntriesFromDirectories(directory: string): void {
663            if (!tryDirectoryExists(host, directory)) return;
664
665            for (const typeDirectoryName of tryGetDirectories(host, directory)) {
666                const packageName = unmangleScopedPackageName(typeDirectoryName);
667                if (options.types && !contains(options.types, packageName)) continue;
668
669                if (fragmentDirectory === undefined) {
670                    if (!seen.has(packageName)) {
671                        result.push(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
672                        seen.set(packageName, true);
673                    }
674                }
675                else {
676                    const baseDirectory = combinePaths(directory, typeDirectoryName);
677                    const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host));
678                    if (remainingFragment !== undefined) {
679                        getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result);
680                    }
681                }
682            }
683        }
684    }
685
686    function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] {
687        if (!host.readFile || !host.fileExists) return emptyArray;
688
689        const result: string[] = [];
690        for (const packageJson of findPackageJsons(scriptPath, host)) {
691            const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined
692            // Provide completions for all non @types dependencies
693            for (const key of nodeModulesDependencyKeys) {
694                const dependencies: object | undefined = (contents as any)[key];
695                if (!dependencies) continue;
696                for (const dep in dependencies) {
697                    if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) {
698                        result.push(dep);
699                    }
700                }
701            }
702        }
703        return result;
704    }
705
706    // Replace everything after the last directory separator that appears
707    function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan | undefined {
708        const index = Math.max(text.lastIndexOf(directorySeparator), text.lastIndexOf(altDirectorySeparator));
709        const offset = index !== -1 ? index + 1 : 0;
710        // If the range is an identifier, span is unnecessary.
711        const length = text.length - offset;
712        return length === 0 || isIdentifierText(text.substr(offset, length), ScriptTarget.ESNext) ? undefined : createTextSpan(textStart + offset, length);
713    }
714
715    // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..)
716    function isPathRelativeToScript(path: string) {
717        if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) {
718            const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1;
719            const slashCharCode = path.charCodeAt(slashIndex);
720            return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash;
721        }
722        return false;
723    }
724
725    /**
726     * Matches a triple slash reference directive with an incomplete string literal for its path. Used
727     * to determine if the caret is currently within the string literal and capture the literal fragment
728     * for completions.
729     * For example, this matches
730     *
731     * /// <reference path="fragment
732     *
733     * but not
734     *
735     * /// <reference path="fragment"
736     */
737    const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
738
739    const nodeModulesDependencyKeys: readonly string[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
740
741    function containsSlash(fragment: string) {
742        return stringContains(fragment, directorySeparator);
743    }
744}
745