diff --git a/src/compiler/program.ts b/src/compiler/program.ts index e00e803b489..49f9cb98835 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -7,18 +7,10 @@ namespace ts { const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string { - while (true) { - const fileName = combinePaths(searchPath, configName); - if (fileExists(fileName)) { - return fileName; - } - const parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } - return undefined; + return forEachAncestorDirectory(searchPath, ancestor => { + const fileName = combinePaths(ancestor, configName); + return fileExists(fileName) ? fileName : undefined; + }); } export function resolveTripleslashReference(moduleName: string, containingFile: string): string { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e15b81db8e1..10b93ad59cf 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3592,7 +3592,7 @@ namespace ts { } /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ - export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T): T { + export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined { while (true) { const result = callback(directory); if (result !== undefined) { diff --git a/src/services/completions.ts b/src/services/completions.ts index f4feb8a1c10..6462788d5ae 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -30,7 +30,8 @@ namespace ts.Completions { allSourceFiles: ReadonlyArray, ): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { - return PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host); + const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host); + return entries && pathCompletionsInfo(entries); } if (isInString(sourceFile, position)) { @@ -250,7 +251,8 @@ namespace ts.Completions { // import x = require("/*completion position*/"); // var y = require("/*completion position*/"); // export * from "/*completion position*/"; - return PathCompletions.getStringLiteralCompletionEntriesFromModuleNames(node, compilerOptions, host, typeChecker); + const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(node, compilerOptions, host, typeChecker); + return pathCompletionsInfo(entries); } else if (isEqualityExpression(node.parent)) { // Get completions from the type of the other operand @@ -279,6 +281,18 @@ namespace ts.Completions { } } + function pathCompletionsInfo(entries: CompletionEntry[]): CompletionInfo { + return { + // We don't want the editor to offer any other completions, such as snippets, inside a comment. + isGlobalCompletion: false, + isMemberCompletion: false, + // 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. + isNewIdentifierLocation: true, + entries, + }; + } + function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined { const type = typeChecker.getContextualType((element.parent)); const entries: CompletionEntry[] = []; diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index 190e980c936..921c4a14d93 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -1,34 +1,27 @@ /* @internal */ namespace ts.Completions.PathCompletions { - export function getStringLiteralCompletionEntriesFromModuleNames(node: StringLiteral, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionInfo { + export function getStringLiteralCompletionsFromModuleNames(node: StringLiteral, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] { const literalValue = normalizeSlashes(node.text); const scriptPath = node.getSourceFile().path; const scriptDirectory = getDirectoryPath(scriptPath); const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); - let entries: CompletionEntry[]; if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { const extensions = getSupportedExtensions(compilerOptions); if (compilerOptions.rootDirs) { - entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( + return getCompletionEntriesForDirectoryFragmentWithRootDirs( compilerOptions.rootDirs, literalValue, scriptDirectory, extensions, /*includeExtensions*/ false, span, compilerOptions, host, scriptPath); } else { - entries = getCompletionEntriesForDirectoryFragment( + return getCompletionEntriesForDirectoryFragment( literalValue, scriptDirectory, extensions, /*includeExtensions*/ false, span, host, scriptPath); } } else { // Check for node modules - entries = getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, span, compilerOptions, host, typeChecker); + return getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, span, compilerOptions, host, typeChecker); } - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: true, - entries - }; } /** @@ -37,14 +30,14 @@ namespace ts.Completions.PathCompletions { */ function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptPath: string, ignoreCase: boolean): string[] { // Make all paths absolute/normalized if they are not already - rootDirs = map(rootDirs, rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); + rootDirs = rootDirs.map(rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); // Determine the path to the directory containing the script relative to the root directory it is contained within - const relativeDirectory = forEach(rootDirs, rootDirectory => + const relativeDirectory = firstDefined(rootDirs, rootDirectory => containsPath(rootDirectory, scriptPath, basePath, ignoreCase) ? scriptPath.substr(rootDirectory.length) : undefined); // Now find a path for each potential directory that is to be merged with the one containing the script - return deduplicate(map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory))); + return deduplicate(rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory))); } function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] { @@ -283,61 +276,35 @@ namespace ts.Completions.PathCompletions { return deduplicate(nonRelativeModuleNames); } - export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionInfo { + export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionEntry[] | undefined { const token = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); - if (!token) { - return undefined; - } - const commentRanges: CommentRange[] = getLeadingCommentRanges(sourceFile.text, token.pos); - - if (!commentRanges || !commentRanges.length) { - return undefined; - } - - const range = forEach(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end && commentRange); - + const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos); + const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end); if (!range) { return undefined; } - - const completionInfo: CompletionInfo = { - /** - * We don't want the editor to offer any other completions, such as snippets, inside a comment. - */ - isGlobalCompletion: false, - isMemberCompletion: false, - /** - * 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. - */ - isNewIdentifierLocation: true, - - entries: [] - }; - - const text = sourceFile.text.substr(range.pos, position - range.pos); - + const text = sourceFile.text.slice(range.pos, position); const match = tripleSlashDirectiveFragmentRegex.exec(text); - - if (match) { - const prefix = match[1]; - const kind = match[2]; - const toComplete = match[3]; - - const scriptPath = getDirectoryPath(sourceFile.path); - if (kind === "path") { - // Give completions for a relative path - const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/ true, span, host, sourceFile.path); - } - else { - // Give completions based on the typings available - const span: TextSpan = { start: range.pos + prefix.length, length: match[0].length - prefix.length }; - completionInfo.entries = getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span); - } + if (!match) { + return undefined; } - return completionInfo; + const [, prefix, kind, toComplete] = match; + const scriptPath = getDirectoryPath(sourceFile.path); + switch (kind) { + case "path": { + // Give completions for a relative path + const span = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); + return getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/ true, span, host, sourceFile.path); + } + case "types": { + // Give completions based on the typings available + const span = createTextSpan(range.pos + prefix.length, match[0].length - prefix.length); + return getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span); + } + default: + return undefined; + } } function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] { @@ -385,26 +352,15 @@ namespace ts.Completions.PathCompletions { } } - function findPackageJsons(currentDir: string, host: LanguageServiceHost): string[] { + function findPackageJsons(directory: string, host: LanguageServiceHost): string[] { const paths: string[] = []; - let currentConfigPath: string; - while (true) { - currentConfigPath = findConfigFile(currentDir, (f) => tryFileExists(host, f), "package.json"); - if (currentConfigPath) { - paths.push(currentConfigPath); - - currentDir = getDirectoryPath(currentConfigPath); - const parent = getDirectoryPath(currentDir); - if (currentDir === parent) { - break; - } - currentDir = parent; + forEachAncestorDirectory(directory, ancestor => { + const currentConfigPath = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json"); + if (!currentConfigPath) { + return true; // break out } - else { - break; - } - } - + paths.push(currentConfigPath); + }); return paths; }