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