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