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