diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index a42abbbc609..87b537417ea 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -245,14 +245,7 @@ namespace FourSlash { constructor(private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) { // Create a new Services Adapter this.cancellationToken = new TestCancellationToken(); - const compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions); - if (compilationOptions.typeRoots) { - compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath)); - } - - const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions); - this.languageServiceAdapterHost = languageServiceAdapter.getHost(); - this.languageService = languageServiceAdapter.getLanguageService(); + let compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions); // Initialize the language service with all the scripts let startResolveFileRef: FourSlashFile; @@ -260,6 +253,22 @@ namespace FourSlash { ts.forEach(testData.files, file => { // Create map between fileName and its content for easily looking up when resolveReference flag is specified this.inputFiles[file.fileName] = file.content; + + if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") { + const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content); + assert.isTrue(configJson.config !== undefined); + + // Extend our existing compiler options so that we can also support tsconfig only options + if (configJson.config.compilerOptions) { + let baseDir = ts.normalizePath(ts.getDirectoryPath(file.fileName)); + let tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDir, file.fileName); + + if (!tsConfig.errors || !tsConfig.errors.length) { + compilationOptions = ts.extend(compilationOptions, tsConfig.options); + } + } + } + if (!startResolveFileRef && file.fileOptions[metadataOptionNames.resolveReference] === "true") { startResolveFileRef = file; } @@ -269,6 +278,15 @@ namespace FourSlash { } }); + + if (compilationOptions.typeRoots) { + compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath)); + } + + const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions); + this.languageServiceAdapterHost = languageServiceAdapter.getHost(); + this.languageService = languageServiceAdapter.getLanguageService(); + if (startResolveFileRef) { // Add the entry-point file itself into the languageServiceShimHost this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true); diff --git a/src/services/services.ts b/src/services/services.ts index 7024dc7211b..275c41732c1 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1757,6 +1757,12 @@ namespace ts { owners: string[]; } + interface VisibleModuleInfo { + moduleName: string; + moduleDir: string; + canBeImported: boolean; + } + export interface DisplayPartsSymbolWriter extends SymbolWriter { displayParts(): SymbolDisplayPart[]; } @@ -4438,18 +4444,100 @@ namespace ts { /** * Check all of the declared modules and those in node modules. Possible sources of modules: * Modules that are found by the type checker + * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) * Modules from node_modules (i.e. those listed in package.json) * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions */ function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string): CompletionEntry[] { - return ts.map(enumeratePotentialNonRelativeModules(fragment, scriptPath), (moduleName) => { - return { - name: moduleName, - kind: ScriptElementKind.externalModuleName, - kindModifiers: ScriptElementKindModifier.none, - sortText: moduleName - }; + const options = program.getCompilerOptions(); + const { baseUrl, paths } = options; + + let result: CompletionEntry[]; + + if (baseUrl) { + const fileExtensions = getSupportedExtensions(options); + const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(host.getCurrentDirectory(), baseUrl) + result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false); + + if (paths) { + for (var path in paths) { + if (paths.hasOwnProperty(path)) { + if (path === "*") { + if (paths[path]) { + forEach(paths[path], pattern => { + forEach(getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions), match => { + result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName)); + }); + }); + } + } + else if (startsWith(path, fragment)) { + const entry = paths[path] && paths[path].length === 1 && paths[path][0]; + if (entry) { + result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName)); + } + } + } + } + } + } + else { + result = []; + } + + + + forEach(enumeratePotentialNonRelativeModules(fragment, scriptPath), moduleName => { + result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName)); }); + + return result; + } + + function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: string[]): string[] { + const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; + if (parsed) { + const hasTrailingSlash = parsed.prefix.charAt(parsed.prefix.length - 1) === "/" || parsed.prefix.charAt(parsed.prefix.length - 1) === "\\"; + + // The prefix has two effective parts: the directory path and the base component after the filepath that is not a + // full directory component. For example: directory/path/of/prefix/base* + const normalizedPrefix = hasTrailingSlash ? ensureTrailingDirectorySeparator(normalizePath(parsed.prefix)) : normalizePath(parsed.prefix); + const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); + const normalizedPrefixBase = getBaseFileName(normalizedPrefix); + + const fragmentHasPath = fragment.indexOf(directorySeparator) !== -1; + + // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call + const expandedPrefixDir = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory; + + const normalizedSuffix = normalizePath(parsed.suffix); + const baseDirectory = combinePaths(baseUrl, expandedPrefixDir); + const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; + + // If we have a suffix, then we need to read the directory all the way down. We could create a glob + // that encodes the suffix, but we would have to escape the character "?" which readDirectory + // doesn't support. For now, this is safer but slower + const includeGlob = normalizedSuffix ? "**/*" : "./*" + + const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]); + const result: string[] = []; + + // Trim away prefix and suffix + forEach(matches, match => { + const normalizedMatch = normalizePath(match); + if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) { + return; + } + + const start = completePrefix.length; + const length = normalizedMatch.length - start - normalizedSuffix.length; + + result.push(removeFileExtension(normalizedMatch.substr(start, length))); + }); + return result; + } + + return undefined; } function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string): string[] { @@ -4513,6 +4601,112 @@ namespace ts { } } + function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) { + const result: VisibleModuleInfo[] = []; + findPackageJsons(scriptPath).forEach((packageJson) => { + const package = tryReadingPackageJson(packageJson); + if (!package) { + return; + } + + const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); + const foundModuleNames: string[] = []; + + if (package.dependencies) { + addPotentialPackageNames(package.dependencies, modulePrefix, foundModuleNames); + } + if (package.devDependencies) { + addPotentialPackageNames(package.devDependencies, modulePrefix, foundModuleNames); + } + + forEach(foundModuleNames, (moduleName) => { + const moduleDir = combinePaths(nodeModulesDir, moduleName); + result.push({ + moduleName, + moduleDir, + canBeImported: moduleCanBeImported(moduleDir) + }); + }); + }); + + return result; + + function findPackageJsons(currentDir: string): string[] { + const paths: string[] = []; + let currentConfigPath: string; + while (true) { + currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json"); + if (currentConfigPath) { + paths.push(currentConfigPath); + + currentDir = getDirectoryPath(currentConfigPath); + const parent = getDirectoryPath(currentDir); + if (currentDir === parent) { + break; + } + currentDir = parent; + } + else { + break; + } + } + + return paths; + } + + function tryReadingPackageJson(filePath: string) { + try { + const fileText = host.readFile(filePath); + return JSON.parse(fileText); + } + catch (e) { + return undefined; + } + } + + function addPotentialPackageNames(dependencies: any, prefix: string, result: string[]) { + for (const dep in dependencies) { + if (dependencies.hasOwnProperty(dep) && (!prefix || startsWith(dep, prefix))) { + result.push(dep); + } + } + } + + /* + * A module can be imported by name alone if one of the following is true: + * It defines the "typings" property in its package.json + * The module has a "main" export and an index.d.ts file + * The module has an index.ts + */ + function moduleCanBeImported(modulePath: string): boolean { + const packagePath = combinePaths(modulePath, "package.json"); + + let hasMainExport = false; + if (host.fileExists(packagePath)) { + const package = tryReadingPackageJson(packagePath); + if (package) { + if (package.typings) { + return true; + } + hasMainExport = !!package.main; + } + } + + hasMainExport = hasMainExport || host.fileExists(combinePaths(modulePath, "index.js")); + + return (hasMainExport && host.fileExists(combinePaths(modulePath, "index.d.ts"))) || host.fileExists(combinePaths(modulePath, "index.ts")); + } + } + + function createCompletionEntryForModule(name: string, kind: string): CompletionEntry { + return { + name, + kind, + kindModifiers: ScriptElementKindModifier.none, + sortText: name + } + } + function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { synchronizeHostData(); diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 44423860d4a..0c4c0fd1dea 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -6,12 +6,6 @@ namespace ts { list: Node; } - export interface VisibleModuleInfo { - moduleName: string; - moduleDir: string; - canBeImported: boolean; - } - export function getLineStartPositionForPosition(position: number, sourceFile: SourceFile): number { const lineStarts = sourceFile.getLineStarts(); const line = sourceFile.getLineAndCharacterOfPosition(position).line; @@ -933,101 +927,4 @@ namespace ts { } return ensureScriptKind(fileName, scriptKind); } - - export function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) { - const result: VisibleModuleInfo[] = []; - findPackageJsons(scriptPath).forEach((packageJson) => { - const package = tryReadingPackageJson(packageJson); - if (!package) { - return; - } - - const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); - const foundModuleNames: string[] = []; - - if (package.dependencies) { - addPotentialPackageNames(package.dependencies, modulePrefix, foundModuleNames); - } - if (package.devDependencies) { - addPotentialPackageNames(package.devDependencies, modulePrefix, foundModuleNames); - } - - forEach(foundModuleNames, (moduleName) => { - const moduleDir = combinePaths(nodeModulesDir, moduleName); - result.push({ - moduleName, - moduleDir, - canBeImported: moduleCanBeImported(moduleDir) - }); - }); - }); - - return result; - - function findPackageJsons(currentDir: string): string[] { - const paths: string[] = []; - let currentConfigPath: string; - while (true) { - currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json"); - if (currentConfigPath) { - paths.push(currentConfigPath); - - currentDir = getDirectoryPath(currentConfigPath); - const parent = getDirectoryPath(currentDir); - if (currentDir === parent) { - break; - } - currentDir = parent; - } - else { - break; - } - } - - return paths; - } - - function tryReadingPackageJson(filePath: string) { - try { - const fileText = host.readFile(filePath); - return JSON.parse(fileText); - } - catch (e) { - return undefined; - } - } - - function addPotentialPackageNames(dependencies: any, prefix: string, result: string[]) { - for (const dep in dependencies) { - if (dependencies.hasOwnProperty(dep) && (!prefix || startsWith(dep, prefix))) { - result.push(dep); - } - } - } - - /* - * A module can be imported by name alone if one of the following is true: - * It defines the "typings" property in its package.json - * The module has a "main" export and an index.d.ts file - * The module has an index.ts - */ - function moduleCanBeImported(modulePath: string): boolean { - const packagePath = combinePaths(modulePath, "package.json"); - - let hasMainExport = false; - if (host.fileExists(packagePath)) { - const package = tryReadingPackageJson(packagePath); - if (package) { - if (package.typings) { - return true; - } - hasMainExport = !!package.main; - } - } - - hasMainExport = hasMainExport || host.fileExists(combinePaths(modulePath, "index.js")); - - return (hasMainExport && host.fileExists(combinePaths(modulePath, "index.d.ts"))) || host.fileExists(combinePaths(modulePath, "index.ts")); - } - } } \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport7.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport7.ts new file mode 100644 index 00000000000..35e144c7184 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport7.ts @@ -0,0 +1,27 @@ +/// +// @baseUrl: tests/cases/fourslash/modules + +// @Filename: tests/test0.ts +//// import * as foo1 from "mod/*import_as0*/ +//// import foo2 = require("mod/*import_equals0*/ +//// var foo3 = require("mod/*require0*/ + +// @Filename: modules/module.ts +//// export var x = 5; + +// @Filename: package.json +//// { "dependencies": { "module-from-node": "latest" } } +// @Filename: node_modules/module-from-node/index.ts +//// /*module1*/ + + + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + + verify.completionListContains("module"); + verify.completionListContains("module-from-node"); + verify.not.completionListItemsCountIsGreaterThan(2); +} diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts new file mode 100644 index 00000000000..0e8da4b02bb --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts @@ -0,0 +1,53 @@ +/// + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": "./modules", +//// "paths": { +//// "*": [ +//// "prefix/0*/suffix.ts", +//// "prefix-only/*", +//// "*/suffix-only.ts" +//// ] +//// } +//// } +//// } + + +// @Filename: tests/test0.ts +//// import * as foo1 from "0/*import_as0*/ +//// import foo2 = require("0/*import_equals0*/ +//// var foo3 = require("0/*require0*/ + +//// import * as foo1 from "1/*import_as1*/ +//// import foo2 = require("1/*import_equals1*/ +//// var foo3 = require("1/*require1*/ + +//// import * as foo1 from "2/*import_as2*/ +//// import foo2 = require("2/*import_equals2*/ +//// var foo3 = require("2/*require2*/ + + +// @Filename: modules/prefix/00test/suffix.ts +//// export var x = 5; + +// @Filename: modules/prefix-only/1test.ts +//// export var y = 5; + +// @Filename: modules/2test/suffix-only.ts +//// export var z = 5; + + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListContains("0test"); + + goTo.marker(kind + "1"); + verify.completionListContains("1test"); + + goTo.marker(kind + "2"); + verify.completionListContains("2test"); +} diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport9.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport9.ts new file mode 100644 index 00000000000..3c32ec2670d --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport9.ts @@ -0,0 +1,34 @@ +/// + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": "./modules", +//// "paths": { +//// "module1": ["some/path/whatever.ts"], +//// "module2": ["some/other/path.ts"] +//// } +//// } +//// } + + +// @Filename: tests/test0.ts +//// import * as foo1 from "m/*import_as0*/ +//// import foo2 = require("m/*import_equals0*/ +//// var foo3 = require("m/*require0*/ + +// @Filename: some/path/whatever.ts +//// export var x = 9; + +// @Filename: some/other/path.ts +//// export var y = 10; + + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListContains("module1"); + verify.completionListContains("module2"); + verify.not.completionListItemsCountIsGreaterThan(2); +}