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);
+}