• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.Completions.StringCompletions {
3    interface NameAndKindSet {
4        add(value: NameAndKind): void;
5        has(name: string): boolean;
6        values(): Iterator<NameAndKind>;
7    }
8    const kindPrecedence = {
9        [ScriptElementKind.directory]: 0,
10        [ScriptElementKind.scriptElement]: 1,
11        [ScriptElementKind.externalModuleName]: 2,
12    };
13    function createNameAndKindSet(): NameAndKindSet {
14        const map = new Map<string, NameAndKind>();
15        function add(value: NameAndKind) {
16            const existing = map.get(value.name);
17            if (!existing || kindPrecedence[existing.kind] < kindPrecedence[value.kind]) {
18                map.set(value.name, value);
19            }
20        }
21        return {
22            add,
23            has: map.has.bind(map),
24            values: map.values.bind(map),
25        };
26    }
27
28    export function getStringLiteralCompletions(
29        sourceFile: SourceFile,
30        position: number,
31        contextToken: Node | undefined,
32        options: CompilerOptions,
33        host: LanguageServiceHost,
34        program: Program,
35        log: Log,
36        preferences: UserPreferences): CompletionInfo | undefined {
37        if (isInReferenceComment(sourceFile, position)) {
38            const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host);
39            return entries && convertPathCompletions(entries);
40        }
41        if (isInString(sourceFile, position, contextToken)) {
42            if (!contextToken || !isStringLiteralLike(contextToken)) return undefined;
43            const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program.getTypeChecker(), options, host, preferences);
44            return convertStringLiteralCompletions(entries, contextToken, sourceFile, host, program, log, options, preferences);
45        }
46    }
47
48    function convertStringLiteralCompletions(
49        completion: StringLiteralCompletion | undefined,
50        contextToken: StringLiteralLike,
51        sourceFile: SourceFile,
52        host: LanguageServiceHost,
53        program: Program,
54        log: Log,
55        options: CompilerOptions,
56        preferences: UserPreferences,
57    ): CompletionInfo | undefined {
58        if (completion === undefined) {
59            return undefined;
60        }
61
62        const optionalReplacementSpan = createTextSpanFromStringLiteralLikeContent(contextToken);
63        switch (completion.kind) {
64            case StringLiteralCompletionKind.Paths:
65                return convertPathCompletions(completion.paths);
66            case StringLiteralCompletionKind.Properties: {
67                const entries = createSortedArray<CompletionEntry>();
68                getCompletionEntriesFromSymbols(
69                    completion.symbols,
70                    entries,
71                    contextToken,
72                    contextToken,
73                    sourceFile,
74                    sourceFile,
75                    host,
76                    program,
77                    ScriptTarget.ESNext,
78                    log,
79                    CompletionKind.String,
80                    preferences,
81                    options,
82                    /*formatContext*/ undefined,
83                ); // Target will not be used, so arbitrary
84                return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries };
85            }
86            case StringLiteralCompletionKind.Types: {
87                const entries = completion.types.map(type => ({
88                    name: type.value,
89                    kindModifiers: ScriptElementKindModifier.none,
90                    kind: ScriptElementKind.string,
91                    sortText: SortText.LocationPriority,
92                    replacementSpan: getReplacementSpanForContextToken(contextToken)
93                }));
94                return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries };
95            }
96            default:
97                return Debug.assertNever(completion);
98        }
99    }
100
101    export function getStringLiteralCompletionDetails(name: string, sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, cancellationToken: CancellationToken, preferences: UserPreferences) {
102        if (!contextToken || !isStringLiteralLike(contextToken)) return undefined;
103        const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host, preferences);
104        return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken);
105    }
106
107    function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined {
108        switch (completion.kind) {
109            case StringLiteralCompletionKind.Paths: {
110                const match = find(completion.paths, p => p.name === name);
111                return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]);
112            }
113            case StringLiteralCompletionKind.Properties: {
114                const match = find(completion.symbols, s => s.name === name);
115                return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken);
116            }
117            case StringLiteralCompletionKind.Types:
118                return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.typeElement, [textPart(name)]) : undefined;
119            default:
120                return Debug.assertNever(completion);
121        }
122    }
123
124    function convertPathCompletions(pathCompletions: readonly PathCompletion[]): CompletionInfo {
125        const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
126        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.
127        const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry =>
128            ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: SortText.LocationPriority, replacementSpan: span }));
129        return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
130    }
131    function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier {
132        switch (extension) {
133            case Extension.Dts: return ScriptElementKindModifier.dtsModifier;
134            case Extension.Js: return ScriptElementKindModifier.jsModifier;
135            case Extension.Json: return ScriptElementKindModifier.jsonModifier;
136            case Extension.Jsx: return ScriptElementKindModifier.jsxModifier;
137            case Extension.Ts: return ScriptElementKindModifier.tsModifier;
138            case Extension.Tsx: return ScriptElementKindModifier.tsxModifier;
139            case Extension.Dmts: return ScriptElementKindModifier.dmtsModifier;
140            case Extension.Mjs: return ScriptElementKindModifier.mjsModifier;
141            case Extension.Mts: return ScriptElementKindModifier.mtsModifier;
142            case Extension.Dcts: return ScriptElementKindModifier.dctsModifier;
143            case Extension.Cjs: return ScriptElementKindModifier.cjsModifier;
144            case Extension.Cts: return ScriptElementKindModifier.ctsModifier;
145            case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported.`);
146            case undefined: return ScriptElementKindModifier.none;
147            case Extension.Dets: return ScriptElementKindModifier.detsModifier;
148            case Extension.Ets: return ScriptElementKindModifier.etsModifier;
149            default:
150                return Debug.assertNever(extension);
151        }
152    }
153
154    const enum StringLiteralCompletionKind { Paths, Properties, Types }
155    interface StringLiteralCompletionsFromProperties {
156        readonly kind: StringLiteralCompletionKind.Properties;
157        readonly symbols: readonly Symbol[];
158        readonly hasIndexSignature: boolean;
159    }
160    interface StringLiteralCompletionsFromTypes {
161        readonly kind: StringLiteralCompletionKind.Types;
162        readonly types: readonly StringLiteralType[];
163        readonly isNewIdentifier: boolean;
164    }
165    type StringLiteralCompletion = { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: readonly PathCompletion[] } | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes;
166    function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, preferences: UserPreferences): StringLiteralCompletion | undefined {
167        const parent = walkUpParentheses(node.parent);
168        switch (parent.kind) {
169            case SyntaxKind.LiteralType: {
170                const grandParent = walkUpParentheses(parent.parent);
171                switch (grandParent.kind) {
172                    case SyntaxKind.ExpressionWithTypeArguments:
173                    case SyntaxKind.TypeReference: {
174                        const typeArgument = findAncestor(parent, n => n.parent === grandParent) as LiteralTypeNode;
175                        if (typeArgument) {
176                            return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false };
177                        }
178                        return undefined;
179                    }
180                    case SyntaxKind.IndexedAccessType:
181                        // Get all apparent property names
182                        // i.e. interface Foo {
183                        //          foo: string;
184                        //          bar: string;
185                        //      }
186                        //      let x: Foo["/*completion position*/"]
187                        const { indexType, objectType } = grandParent as IndexedAccessTypeNode;
188                        if (!rangeContainsPosition(indexType, position)) {
189                            return undefined;
190                        }
191                        return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
192                    case SyntaxKind.ImportType:
193                        return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker, preferences) };
194                    case SyntaxKind.UnionType: {
195                        if (!isTypeReferenceNode(grandParent.parent)) {
196                            return undefined;
197                        }
198                        const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(grandParent as UnionTypeNode, parent as LiteralTypeNode);
199                        const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(grandParent as UnionTypeNode)).filter(t => !contains(alreadyUsedTypes, t.value));
200                        return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false };
201                    }
202                    default:
203                        return undefined;
204                }
205            }
206            case SyntaxKind.PropertyAssignment:
207                if (isObjectLiteralExpression(parent.parent) && (parent as PropertyAssignment).name === node) {
208                    // Get quoted name of properties of the object literal expression
209                    // i.e. interface ConfigFiles {
210                    //          'jspm:dev': string
211                    //      }
212                    //      let files: ConfigFiles = {
213                    //          '/*completion position*/'
214                    //      }
215                    //
216                    //      function foo(c: ConfigFiles) {}
217                    //      foo({
218                    //          '/*completion position*/'
219                    //      });
220                    return stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent);
221                }
222                return fromContextualType();
223
224            case SyntaxKind.ElementAccessExpression: {
225                const { expression, argumentExpression } = parent as ElementAccessExpression;
226                if (node === skipParentheses(argumentExpression)) {
227                    // Get all names of properties on the expression
228                    // i.e. interface A {
229                    //      'prop1': string
230                    // }
231                    // let a: A;
232                    // a['/*completion position*/']
233                    return stringLiteralCompletionsFromProperties(typeChecker.getTypeAtLocation(expression));
234                }
235                return undefined;
236            }
237
238            case SyntaxKind.CallExpression:
239            case SyntaxKind.NewExpression:
240            case SyntaxKind.JsxAttribute:
241                if (!isRequireCallArgument(node) && !isImportCall(parent)) {
242                    const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile);
243                    // Get string literal completions from specialized signatures of the target
244                    // i.e. declare function f(a: 'A');
245                    // f("/*completion position*/")
246                    return argumentInfo && getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || fromContextualType();
247                }
248                // falls through (is `require("")` or `require(""` or `import("")`)
249
250            case SyntaxKind.ImportDeclaration:
251            case SyntaxKind.ExportDeclaration:
252            case SyntaxKind.ExternalModuleReference:
253                // Get all known external module names or complete a path to a module
254                // i.e. import * as ns from "/*completion position*/";
255                //      var y = import("/*completion position*/");
256                //      import x = require("/*completion position*/");
257                //      var y = require("/*completion position*/");
258                //      export * from "/*completion position*/";
259                return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker, preferences) };
260
261            default:
262                return fromContextualType();
263        }
264
265        function fromContextualType(): StringLiteralCompletion {
266            // Get completion for string literal from string literal type
267            // i.e. var x: "hi" | "hello" = "/*completion position*/"
268            return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false };
269        }
270    }
271
272    function walkUpParentheses(node: Node) {
273        switch (node.kind) {
274            case SyntaxKind.ParenthesizedType:
275                return walkUpParenthesizedTypes(node);
276            case SyntaxKind.ParenthesizedExpression:
277                return walkUpParenthesizedExpressions(node);
278            default:
279                return node;
280        }
281    }
282
283    function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current: LiteralTypeNode): readonly string[] {
284        return mapDefined(union.types, type =>
285            type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined);
286    }
287
288    function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes | undefined {
289        let isNewIdentifier = false;
290        const uniques = new Map<string, true>();
291        const candidates: Signature[] = [];
292        const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg;
293        checker.getResolvedSignatureForStringLiteralCompletions(call, editingArgument, candidates);
294        const types = flatMap(candidates, candidate => {
295            if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return;
296            let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex);
297            if (isJsxOpeningLikeElement(call)) {
298                const propType = checker.getTypeOfPropertyOfType(type, (editingArgument as JsxAttribute).name.text);
299                if (propType) {
300                    type = propType;
301                }
302            }
303            isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String);
304            return getStringLiteralTypes(type, uniques);
305        });
306        return length(types) ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier } : undefined;
307    }
308
309    function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined {
310        return type && {
311            kind: StringLiteralCompletionKind.Properties,
312            symbols: filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))),
313            hasIndexSignature: hasIndexSignature(type)
314        };
315    }
316
317    function stringLiteralCompletionsForObjectLiteral(checker: TypeChecker, objectLiteralExpression: ObjectLiteralExpression): StringLiteralCompletionsFromProperties | undefined {
318        const contextualType = checker.getContextualType(objectLiteralExpression);
319        if (!contextualType) return undefined;
320
321        const completionsType = checker.getContextualType(objectLiteralExpression, ContextFlags.Completions);
322        const symbols = getPropertiesForObjectExpression(
323            contextualType,
324            completionsType,
325            objectLiteralExpression,
326            checker
327        );
328
329        return {
330            kind: StringLiteralCompletionKind.Properties,
331            symbols,
332            hasIndexSignature: hasIndexSignature(contextualType)
333        };
334    }
335
336    function getStringLiteralTypes(type: Type | undefined, uniques = new Map<string, true>()): readonly StringLiteralType[] {
337        if (!type) return emptyArray;
338        type = skipConstraint(type);
339        return type.isUnion() ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) :
340            type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray;
341    }
342
343    interface NameAndKind {
344        readonly name: string;
345        readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName;
346        readonly extension: Extension | undefined;
347    }
348    interface PathCompletion extends NameAndKind {
349        readonly span: TextSpan | undefined;
350    }
351
352    function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind {
353        return { name, kind, extension };
354    }
355    function directoryResult(name: string): NameAndKind {
356        return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined);
357    }
358
359    function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] {
360        const span = getDirectoryFragmentTextSpan(text, textStart);
361        const wholeSpan = text.length === 0 ? undefined : createTextSpan(textStart, text.length);
362        return names.map(({ name, kind, extension }): PathCompletion =>
363            Math.max(name.indexOf(directorySeparator), name.indexOf(altDirectorySeparator)) !== -1 ? { name, kind, extension, span: wholeSpan } : { name, kind, extension, span });
364    }
365
366    function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker, preferences: UserPreferences): readonly PathCompletion[] {
367        return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker, preferences));
368    }
369
370    function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker, preferences: UserPreferences): readonly NameAndKind[] {
371        const literalValue = normalizeSlashes(node.text);
372        const mode = isStringLiteralLike(node) ? getModeForUsageLocation(sourceFile, node) : undefined;
373
374        const scriptPath = sourceFile.path;
375        const scriptDirectory = getDirectoryPath(scriptPath);
376
377        return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue))
378            ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath, getIncludeExtensionOption())
379            : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, mode, compilerOptions, host, getIncludeExtensionOption(), typeChecker);
380
381        function getIncludeExtensionOption() {
382            const mode = isStringLiteralLike(node) ? getModeForUsageLocation(sourceFile, node) : undefined;
383            return preferences.importModuleSpecifierEnding === "js" || mode === ModuleKind.ESNext ? IncludeExtensionsOption.ModuleSpecifierCompletion : IncludeExtensionsOption.Exclude;
384        }
385    }
386
387    interface ExtensionOptions {
388        readonly extensions: readonly Extension[];
389        readonly includeExtensionsOption: IncludeExtensionsOption;
390    }
391    function getExtensionOptions(compilerOptions: CompilerOptions, includeExtensionsOption = IncludeExtensionsOption.Exclude): ExtensionOptions {
392        return { extensions: flatten(getSupportedExtensionsForModuleResolution(compilerOptions)), includeExtensionsOption };
393    }
394    function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path, includeExtensions: IncludeExtensionsOption) {
395        const extensionOptions = getExtensionOptions(compilerOptions, includeExtensions);
396        if (compilerOptions.rootDirs) {
397            return getCompletionEntriesForDirectoryFragmentWithRootDirs(
398                compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath);
399        }
400        else {
401            return arrayFrom(getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath).values());
402        }
403    }
404
405    function isEmitResolutionKindUsingNodeModules(compilerOptions: CompilerOptions): boolean {
406        return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ||
407            getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 ||
408            getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
409    }
410
411    function isEmitModuleResolutionRespectingExportMaps(compilerOptions: CompilerOptions) {
412        return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 ||
413        getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
414    }
415
416    function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[][] {
417        const extensions = getSupportedExtensions(compilerOptions);
418        return isEmitResolutionKindUsingNodeModules(compilerOptions) ?
419            getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) :
420            extensions;
421    }
422
423    /**
424     * Takes a script path and returns paths for all potential folders that could be merged with its
425     * containing folder via the "rootDirs" compiler option
426     */
427    function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptDirectory: string, ignoreCase: boolean): readonly string[] {
428        // Make all paths absolute/normalized if they are not already
429        rootDirs = rootDirs.map(rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory)));
430
431        // Determine the path to the directory containing the script relative to the root directory it is contained within
432        const relativeDirectory = firstDefined(rootDirs, rootDirectory =>
433            containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!; // TODO: GH#18217
434
435        // Now find a path for each potential directory that is to be merged with the one containing the script
436        return deduplicate<string>(
437            [...rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory)), scriptDirectory],
438            equateStringsCaseSensitive,
439            compareStringsCaseSensitive);
440    }
441
442    function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude: string): readonly NameAndKind[] {
443        const basePath = compilerOptions.project || host.getCurrentDirectory();
444        const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
445        const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase);
446        return flatMap(baseDirectories, baseDirectory => arrayFrom(getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude).values()));
447    }
448
449    const enum IncludeExtensionsOption {
450        Exclude,
451        Include,
452        ModuleSpecifierCompletion,
453    }
454    /**
455     * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename.
456     */
457    function getCompletionEntriesForDirectoryFragment(
458        fragment: string,
459        scriptPath: string,
460        extensionOptions: ExtensionOptions,
461        host: LanguageServiceHost,
462        exclude?: string,
463        result = createNameAndKindSet()
464    ): NameAndKindSet {
465        if (fragment === undefined) {
466            fragment = "";
467        }
468
469        fragment = normalizeSlashes(fragment);
470
471        /**
472         * Remove the basename from the path. Note that we don't use the basename to filter completions;
473         * the client is responsible for refining completions.
474         */
475        if (!hasTrailingDirectorySeparator(fragment)) {
476            fragment = getDirectoryPath(fragment);
477        }
478
479        if (fragment === "") {
480            fragment = "." + directorySeparator;
481        }
482
483        fragment = ensureTrailingDirectorySeparator(fragment);
484
485        const absolutePath = resolvePath(scriptPath, fragment);
486        const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
487
488        // check for a version redirect
489        const packageJsonPath = findPackageJson(baseDirectory, host);
490        if (packageJsonPath) {
491            const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined });
492            const typesVersions = (packageJson as any).typesVersions;
493            if (typeof typesVersions === "object") {
494                const versionPaths = getPackageJsonTypesVersionsPaths(typesVersions)?.paths;
495                if (versionPaths) {
496                    const packageDirectory = getDirectoryPath(packageJsonPath);
497                    const pathInPackage = absolutePath.slice(ensureTrailingDirectorySeparator(packageDirectory).length);
498                    if (addCompletionEntriesFromPaths(result, pathInPackage, packageDirectory, extensionOptions, host, versionPaths)) {
499                        // A true result means one of the `versionPaths` was matched, which will block relative resolution
500                        // to files and folders from here. All reachable paths given the pattern match are already added.
501                        return result;
502                    }
503                }
504            }
505        }
506
507        const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
508        if (!tryDirectoryExists(host, baseDirectory)) return result;
509
510        // Enumerate the available files if possible
511        const files = tryReadDirectory(host, baseDirectory, extensionOptions.extensions, /*exclude*/ undefined, /*include*/ ["./*"]);
512
513        if (files) {
514            for (let filePath of files) {
515                filePath = normalizePath(filePath);
516                if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
517                    continue;
518                }
519
520                const { name, extension } = getFilenameWithExtensionOption(getBaseFileName(filePath), host.getCompilationSettings(), extensionOptions.includeExtensionsOption);
521                result.add(nameAndKind(name, ScriptElementKind.scriptElement, extension));
522            }
523        }
524
525        // If possible, get folder completion as well
526        const directories = tryGetDirectories(host, baseDirectory);
527
528        if (directories) {
529            for (const directory of directories) {
530                const directoryName = getBaseFileName(normalizePath(directory));
531                if (directoryName !== "@types") {
532                    result.add(directoryResult(directoryName));
533                }
534            }
535        }
536
537        return result;
538    }
539
540    function getFilenameWithExtensionOption(name: string, compilerOptions: CompilerOptions, includeExtensionsOption: IncludeExtensionsOption): { name: string, extension: Extension | undefined } {
541        const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, compilerOptions);
542        if (includeExtensionsOption === IncludeExtensionsOption.Exclude && !fileExtensionIsOneOf(name, [Extension.Json, Extension.Mts, Extension.Cts, Extension.Dmts, Extension.Dcts, Extension.Mjs, Extension.Cjs])) {
543            return { name: removeFileExtension(name), extension: tryGetExtensionFromPath(name) };
544        }
545        else if ((fileExtensionIsOneOf(name, [Extension.Mts, Extension.Cts, Extension.Dmts, Extension.Dcts, Extension.Mjs, Extension.Cjs]) || includeExtensionsOption === IncludeExtensionsOption.ModuleSpecifierCompletion) && outputExtension) {
546            return { name: changeExtension(name, outputExtension), extension: outputExtension };
547        }
548        else {
549            return { name, extension: tryGetExtensionFromPath(name) };
550        }
551    }
552
553    /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */
554    function addCompletionEntriesFromPaths(
555        result: NameAndKindSet,
556        fragment: string,
557        baseDirectory: string,
558        extensionOptions: ExtensionOptions,
559        host: LanguageServiceHost,
560        paths: MapLike<string[]>
561    ) {
562        const getPatternsForKey = (key: string) => paths[key];
563        const comparePaths = (a: string, b: string): Comparison => {
564            const patternA = tryParsePattern(a);
565            const patternB = tryParsePattern(b);
566            const lengthA = typeof patternA === "object" ? patternA.prefix.length : a.length;
567            const lengthB = typeof patternB === "object" ? patternB.prefix.length : b.length;
568            return compareValues(lengthB, lengthA);
569        };
570        return addCompletionEntriesFromPathsOrExports(result, fragment, baseDirectory, extensionOptions, host, getOwnKeys(paths), getPatternsForKey, comparePaths);
571    }
572
573    /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */
574    function addCompletionEntriesFromPathsOrExports(
575        result: NameAndKindSet,
576        fragment: string,
577        baseDirectory: string,
578        extensionOptions: ExtensionOptions,
579        host: LanguageServiceHost,
580        keys: readonly string[],
581        getPatternsForKey: (key: string) => string[] | undefined,
582        comparePaths: (a: string, b: string) => Comparison,
583    ) {
584        let pathResults: { results: NameAndKind[], matchedPattern: boolean }[] = [];
585        let matchedPath: string | undefined;
586        for (const key of keys) {
587            if (key === ".") continue;
588            const keyWithoutLeadingDotSlash = key.replace(/^\.\//, ""); // remove leading "./"
589            const patterns = getPatternsForKey(key);
590            if (patterns) {
591                const pathPattern = tryParsePattern(keyWithoutLeadingDotSlash);
592                if (!pathPattern) continue;
593                const isMatch = typeof pathPattern === "object" && isPatternMatch(pathPattern, fragment);
594                const isLongestMatch = isMatch && (matchedPath === undefined || comparePaths(key, matchedPath) === Comparison.LessThan);
595                if (isLongestMatch) {
596                    // If this is a higher priority match than anything we've seen so far, previous results from matches are invalid, e.g.
597                    // for `import {} from "some-package/|"` with a typesVersions:
598                    // {
599                    //   "bar/*": ["bar/*"], // <-- 1. We add 'bar', but 'bar/*' doesn't match yet.
600                    //   "*": ["dist/*"],    // <-- 2. We match here and add files from dist. 'bar' is still ok because it didn't come from a match.
601                    //   "foo/*": ["foo/*"]  // <-- 3. We matched '*' earlier and added results from dist, but if 'foo/*' also matched,
602                    // }                               results in dist would not be visible. 'bar' still stands because it didn't come from a match.
603                    //                                 This is especially important if `dist/foo` is a folder, because if we fail to clear results
604                    //                                 added by the '*' match, after typing `"some-package/foo/|"` we would get file results from both
605                    //                                 ./dist/foo and ./foo, when only the latter will actually be resolvable.
606                    //                                 See pathCompletionsTypesVersionsWildcard6.ts.
607                    matchedPath = key;
608                    pathResults = pathResults.filter(r => !r.matchedPattern);
609                }
610                if (typeof pathPattern === "string" || matchedPath === undefined || comparePaths(key, matchedPath) !== Comparison.GreaterThan) {
611                    pathResults.push({
612                        matchedPattern: isMatch,
613                        results: getCompletionsForPathMapping(keyWithoutLeadingDotSlash, patterns, fragment, baseDirectory, extensionOptions, host)
614                            .map(({ name, kind, extension }) => nameAndKind(name, kind, extension)),
615                    });
616                }
617            }
618        }
619
620        pathResults.forEach(pathResult => pathResult.results.forEach(r => result.add(r)));
621        return matchedPath !== undefined;
622    }
623
624    /**
625     * Check all of the declared modules and those in node modules. Possible sources of modules:
626     *      Modules that are found by the type checker
627     *      Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option)
628     *      Modules from node_modules (i.e. those listed in package.json)
629     *          This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
630     *      Modules from oh_modules (i.e. those listed in oh-package.json5)
631     *          This includes all files that are found in oh_modules/moduleName/ with acceptable file extensions
632     */
633    function getCompletionEntriesForNonRelativeModules(
634        fragment: string,
635        scriptPath: string,
636        mode: SourceFile["impliedNodeFormat"],
637        compilerOptions: CompilerOptions,
638        host: LanguageServiceHost,
639        includeExtensionsOption: IncludeExtensionsOption,
640        typeChecker: TypeChecker,
641    ): readonly NameAndKind[] {
642        const { baseUrl, paths } = compilerOptions;
643
644        const result = createNameAndKindSet();
645        const extensionOptions = getExtensionOptions(compilerOptions, includeExtensionsOption);
646        if (baseUrl) {
647            const projectDir = compilerOptions.project || host.getCurrentDirectory();
648            const absolute = normalizePath(combinePaths(projectDir, baseUrl));
649            getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result);
650            if (paths) {
651                addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions, host, paths);
652            }
653        }
654
655        const fragmentDirectory = getFragmentDirectory(fragment);
656        for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
657            result.add(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
658        }
659
660        getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
661
662        if (isEmitResolutionKindUsingNodeModules(compilerOptions)) {
663            // If looking for a global package name, don't just include everything in `node_modules` or `oh_modules` because that includes dependencies' own dependencies.
664            // (But do if we didn't find anything, e.g. 'package.json' missing.)
665            let foundGlobal = false;
666            if (fragmentDirectory === undefined) {
667                for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
668                    const moduleResult = nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined);
669                    if (!result.has(moduleResult.name)) {
670                        foundGlobal = true;
671                        result.add(moduleResult);
672                    }
673                }
674            }
675            if (!foundGlobal) {
676                let ancestorLookup: (directory: string) => void | undefined = ancestor => {
677                    const nodeModules = combinePaths(ancestor, getModuleByPMType(host.getCompilationSettings().packageManagerType));
678                    if (tryDirectoryExists(host, nodeModules)) {
679                        getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result);
680                    }
681                };
682                if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) {
683                    const nodeModulesDirectoryLookup = ancestorLookup;
684                    ancestorLookup = ancestor => {
685                        const components = getPathComponents(fragment);
686                        components.shift(); // shift off empty root
687                        let packagePath = components.shift();
688                        if (!packagePath) {
689                            return nodeModulesDirectoryLookup(ancestor);
690                        }
691                        if (startsWith(packagePath, "@")) {
692                            const subName = components.shift();
693                            if (!subName) {
694                                return nodeModulesDirectoryLookup(ancestor);
695                            }
696                            packagePath = combinePaths(packagePath, subName);
697                        }
698                        const packageDirectory = combinePaths(ancestor, "node_modules", packagePath);
699                        const packageFile = combinePaths(packageDirectory, "package.json");
700                        if (tryFileExists(host, packageFile)) {
701                            const packageJson = readJson(packageFile, host);
702                            const exports = (packageJson as any).exports;
703                            if (exports) {
704                                if (typeof exports !== "object" || exports === null) { // eslint-disable-line no-null/no-null
705                                    return; // null exports or entrypoint only, no sub-modules available
706                                }
707                                const keys = getOwnKeys(exports);
708                                const fragmentSubpath = components.join("/") + (components.length && hasTrailingDirectorySeparator(fragment) ? "/" : "");
709                                const conditions = mode === ModuleKind.ESNext ? ["node", "import", "types"] : ["node", "require", "types"];
710                                addCompletionEntriesFromPathsOrExports(
711                                    result,
712                                    fragmentSubpath,
713                                    packageDirectory,
714                                    extensionOptions,
715                                    host,
716                                    keys,
717                                    key => singleElementArray(getPatternFromFirstMatchingCondition(exports[key], conditions)),
718                                    comparePatternKeys);
719                                return;
720                            }
721                        }
722                        return nodeModulesDirectoryLookup(ancestor);
723                    };
724                }
725                forEachAncestorDirectory(scriptPath, ancestorLookup);
726            }
727        }
728
729        return arrayFrom(result.values());
730    }
731
732    function getPatternFromFirstMatchingCondition(target: unknown, conditions: readonly string[]): string | undefined {
733        if (typeof target === "string") {
734            return target;
735        }
736        if (target && typeof target === "object" && !isArray(target)) {
737            for (const condition in target) {
738                if (condition === "default" || conditions.indexOf(condition) > -1 || isApplicableVersionedTypesKey(conditions, condition)) {
739                    const pattern = (target as MapLike<unknown>)[condition];
740                    return getPatternFromFirstMatchingCondition(pattern, conditions);
741                }
742            }
743        }
744    }
745
746    function getFragmentDirectory(fragment: string): string | undefined {
747        return containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
748    }
749
750    function getCompletionsForPathMapping(
751        path: string,
752        patterns: readonly string[],
753        fragment: string,
754        packageDirectory: string,
755        extensionOptions: ExtensionOptions,
756        host: LanguageServiceHost,
757    ): readonly NameAndKind[] {
758        if (!endsWith(path, "*")) {
759            // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion.
760            return !stringContains(path, "*") ? justPathMappingName(path, ScriptElementKind.scriptElement) : emptyArray;
761        }
762
763        const pathPrefix = path.slice(0, path.length - 1);
764        const remainingFragment = tryRemovePrefix(fragment, pathPrefix);
765        if (remainingFragment === undefined) {
766            const starIsFullPathComponent = path[path.length - 2] === "/";
767            return starIsFullPathComponent ? justPathMappingName(pathPrefix, ScriptElementKind.directory) : flatMap(patterns, pattern =>
768                getModulesForPathsPattern("", packageDirectory, pattern, extensionOptions, host)?.map(({ name, ...rest }) => ({ name: pathPrefix + name, ...rest })));
769        }
770        return flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, packageDirectory, pattern, extensionOptions, host));
771
772        function justPathMappingName(name: string, kind: ScriptElementKind.directory | ScriptElementKind.scriptElement): readonly NameAndKind[] {
773            return startsWith(name, fragment) ? [{ name: removeTrailingDirectorySeparator(name), kind, extension: undefined }] : emptyArray;
774        }
775    }
776
777    function getModulesForPathsPattern(
778        fragment: string,
779        packageDirectory: string,
780        pattern: string,
781        extensionOptions: ExtensionOptions,
782        host: LanguageServiceHost,
783    ): readonly NameAndKind[] | undefined {
784        if (!host.readDirectory) {
785            return undefined;
786        }
787
788        const parsed = tryParsePattern(pattern);
789        if (parsed === undefined || isString(parsed)) {
790            return undefined;
791        }
792
793        // The prefix has two effective parts: the directory path and the base component after the filepath that is not a
794        // full directory component. For example: directory/path/of/prefix/base*
795        const normalizedPrefix = resolvePath(parsed.prefix);
796        const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix);
797        const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix);
798
799        const fragmentHasPath = containsSlash(fragment);
800        const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
801
802        // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
803        const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory;
804
805        const normalizedSuffix = normalizePath(parsed.suffix);
806        // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b".
807        const baseDirectory = normalizePath(combinePaths(packageDirectory, expandedPrefixDirectory));
808        const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
809
810        // If we have a suffix, then we read the directory all the way down to avoid returning completions for
811        // directories that don't contain files that would match the suffix. A previous comment here was concerned
812        // about the case where `normalizedSuffix` includes a `?` character, which should be interpreted literally,
813        // but will match any single character as part of the `include` pattern in `tryReadDirectory`. This is not
814        // a problem, because (in the extremely unusual circumstance where the suffix has a `?` in it) a `?`
815        // interpreted as "any character" can only return *too many* results as compared to the literal
816        // interpretation, so we can filter those superfluous results out via `trimPrefixAndSuffix` as we've always
817        // done.
818        const includeGlob = normalizedSuffix ? "**/*" + normalizedSuffix : "./*";
819
820        const matches = mapDefined(tryReadDirectory(host, baseDirectory, extensionOptions.extensions, /*exclude*/ undefined, [includeGlob]), match => {
821            const trimmedWithPattern = trimPrefixAndSuffix(match);
822            if (trimmedWithPattern) {
823                if (containsSlash(trimmedWithPattern)) {
824                    return directoryResult(getPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern))[1]);
825                }
826                const { name, extension } = getFilenameWithExtensionOption(trimmedWithPattern, host.getCompilationSettings(), extensionOptions.includeExtensionsOption);
827                return nameAndKind(name, ScriptElementKind.scriptElement, extension);
828            }
829        });
830
831        // If we had a suffix, we already recursively searched for all possible files that could match
832        // it and returned the directories leading to those files. Otherwise, assume any directory could
833        // have something valid to import.
834        const directories = normalizedSuffix
835            ? emptyArray
836            : mapDefined(tryGetDirectories(host, baseDirectory), dir => dir === "node_modules" ? undefined : directoryResult(dir));
837        return [...matches, ...directories];
838
839        function trimPrefixAndSuffix(path: string): string | undefined {
840            const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix);
841            return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner);
842        }
843    }
844
845    function withoutStartAndEnd(s: string, start: string, end: string): string | undefined {
846        return startsWith(s, start) && endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined;
847    }
848
849    function removeLeadingDirectorySeparator(path: string): string {
850        return path[0] === directorySeparator ? path.slice(1) : path;
851    }
852
853    function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): readonly string[] {
854        // Get modules that the type checker picked up
855        const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name));
856        const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment));
857
858        // Nested modules of the form "module-name/sub" need to be adjusted to only return the string
859        // after the last '/' that appears in the fragment because that's where the replacement span
860        // starts
861        if (fragmentDirectory !== undefined) {
862            const moduleNameWithSeparator = ensureTrailingDirectorySeparator(fragmentDirectory);
863            return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeparator));
864        }
865        return nonRelativeModuleNames;
866    }
867
868    function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): readonly PathCompletion[] | undefined {
869        const token = getTokenAtPosition(sourceFile, position);
870        const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
871        const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end);
872        if (!range) {
873            return undefined;
874        }
875        const text = sourceFile.text.slice(range.pos, position);
876        const match = tripleSlashDirectiveFragmentRegex.exec(text);
877        if (!match) {
878            return undefined;
879        }
880
881        const [, prefix, kind, toComplete] = match;
882        const scriptPath = getDirectoryPath(sourceFile.path);
883        const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, IncludeExtensionsOption.Include), host, sourceFile.path)
884            : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions))
885            : Debug.fail();
886        return addReplacementSpans(toComplete, range.pos + prefix.length, arrayFrom(names.values()));
887    }
888
889    function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result = createNameAndKindSet()): NameAndKindSet {
890        // Check for typings specified in compiler options
891        const seen = new Map<string, true>();
892
893        const typeRoots = tryAndIgnoreErrors(() => getEffectiveTypeRoots(options, host)) || emptyArray;
894
895        for (const root of typeRoots) {
896            getCompletionEntriesFromDirectories(root);
897        }
898
899        // Also get all @types typings installed in visible node_modules or oh_modules directories
900        for (const packageJson of findPackageJsons(scriptPath, host)) {
901            const typesDir = combinePaths(getDirectoryPath(packageJson), isOhpm(options.packageManagerType) ? "oh_modules/@types" : "node_modules/@types");
902            getCompletionEntriesFromDirectories(typesDir);
903        }
904
905        return result;
906
907        function getCompletionEntriesFromDirectories(directory: string): void {
908            if (!tryDirectoryExists(host, directory)) return;
909
910            for (const typeDirectoryName of tryGetDirectories(host, directory)) {
911                const packageName = unmangleScopedPackageName(typeDirectoryName);
912                if (options.types && !contains(options.types, packageName)) continue;
913
914                if (fragmentDirectory === undefined) {
915                    if (!seen.has(packageName)) {
916                        result.add(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
917                        seen.set(packageName, true);
918                    }
919                }
920                else {
921                    const baseDirectory = combinePaths(directory, typeDirectoryName);
922                    const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host));
923                    if (remainingFragment !== undefined) {
924                        getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result);
925                    }
926                }
927            }
928        }
929    }
930
931    function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] {
932        if (!host.readFile || !host.fileExists) return emptyArray;
933
934        const result: string[] = [];
935        for (const packageJson of findPackageJsons(scriptPath, host)) {
936            const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined
937            // Provide completions for all non @types dependencies
938            for (const key of nodeModulesDependencyKeys) {
939                const dependencies: object | undefined = (contents as any)[key];
940                if (!dependencies) continue;
941                for (const dep in dependencies) {
942                    if (hasProperty(dependencies, dep) && !startsWith(dep, "@types/")) {
943                        result.push(dep);
944                    }
945                }
946            }
947        }
948        return result;
949    }
950
951    // Replace everything after the last directory separator that appears
952    function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan | undefined {
953        const index = Math.max(text.lastIndexOf(directorySeparator), text.lastIndexOf(altDirectorySeparator));
954        const offset = index !== -1 ? index + 1 : 0;
955        // If the range is an identifier, span is unnecessary.
956        const length = text.length - offset;
957        return length === 0 || isIdentifierText(text.substr(offset, length), ScriptTarget.ESNext) ? undefined : createTextSpan(textStart + offset, length);
958    }
959
960    // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..)
961    function isPathRelativeToScript(path: string) {
962        if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) {
963            const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1;
964            const slashCharCode = path.charCodeAt(slashIndex);
965            return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash;
966        }
967        return false;
968    }
969
970    /**
971     * Matches a triple slash reference directive with an incomplete string literal for its path. Used
972     * to determine if the caret is currently within the string literal and capture the literal fragment
973     * for completions.
974     * For example, this matches
975     *
976     * /// <reference path="fragment
977     *
978     * but not
979     *
980     * /// <reference path="fragment"
981     */
982    const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
983
984    const nodeModulesDependencyKeys: readonly string[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
985
986    function containsSlash(fragment: string) {
987        return stringContains(fragment, directorySeparator);
988    }
989
990    /**
991     * Matches
992     *   require(""
993     *   require("")
994     */
995    function isRequireCallArgument(node: Node) {
996        return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node
997            && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require";
998    }
999}
1000