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