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