diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 87b537417ea..8a8cb348a73 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -260,8 +260,8 @@ namespace FourSlash { // 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); + const baseDirectory = ts.normalizePath(ts.getDirectoryPath(file.fileName)); + const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName); if (!tsConfig.errors || !tsConfig.errors.length) { compilationOptions = ts.extend(compilationOptions, tsConfig.options); diff --git a/src/services/services.ts b/src/services/services.ts index 43f9552730d..76aed2ffe01 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1968,7 +1968,7 @@ namespace ts { * for completions. * For example, this matches /// { result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName)); @@ -4558,7 +4558,7 @@ namespace ts { // 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 includeGlob = normalizedSuffix ? "**/*" : "./*"; const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]); const result: string[] = []; @@ -4629,19 +4629,97 @@ namespace ts { const text = sourceFile.text.substr(node.pos, position); const match = tripleSlashDirectiveFragmentRegex.exec(text); if (match) { - const fragment = match[1]; - const scriptPath = getDirectoryPath(sourceFile.path); - return { - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: getCompletionEntriesForDirectoryFragment(fragment, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true) - }; + const kind= match[1]; + const fragment = match[2]; + if (kind === "path") { + // Give completions for a relative path + const scriptPath = getDirectoryPath(sourceFile.path); + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: getCompletionEntriesForDirectoryFragment(fragment, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true) + }; + } + else { + // Give completions based on what is available in the types directory + } } return undefined; } } + function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, result: CompletionEntry[]): CompletionEntry[] { + // Check for typings specified in compiler options + if (options.types) { + forEach(options.types, moduleName => { + result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName)); + }); + } + else if (options.typeRoots) { + const absoluteRoots = map(options.typeRoots, rootDirectory => getAbsoluteProjectPath(rootDirectory, host, options.project)); + forEach(absoluteRoots, absoluteRoot => getCompletionEntriesFromDirectory(host, options, absoluteRoot, result)); + } + + // Also get all @types typings installed in visible node_modules directories + forEach(findPackageJsons(scriptPath), package => { + const typesDir = combinePaths(getDirectoryPath(package), "node_modules/@types"); + getCompletionEntriesFromDirectory(host, options, typesDir, result); + }); + + return result; + } + + function getAbsoluteProjectPath(path: string, host: LanguageServiceHost, projectDir?: string) { + if (isRootedDiskPath(path)) { + return normalizePath(path); + } + + if (projectDir) { + return normalizePath(combinePaths(projectDir, path)); + } + + return normalizePath(host.resolvePath(path)); + } + + function getCompletionEntriesFromDirectory(host: LanguageServiceHost, options: CompilerOptions, directory: string, result: CompletionEntry[]) { + if (directoryProbablyExists(directory, host)) { + const typeDirectories = host.readDirectory(directory, getSupportedExtensions(options), /*exclude*/undefined, /*include*/["./*/*"]); + const seen: {[index: string]: boolean} = {}; + forEach(typeDirectories, typeFile => { + const typeDirectory = getDirectoryPath(typeFile); + if (!hasProperty(seen, typeDirectory)) { + seen[typeDirectory] = true; + result.push(createCompletionEntryForModule(getBaseFileName(typeDirectory), ScriptElementKind.externalModuleName)); + } + }); + } + } + + 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 enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) { const result: VisibleModuleInfo[] = []; findPackageJsons(scriptPath).forEach((packageJson) => { @@ -4672,29 +4750,6 @@ namespace ts { 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); @@ -4745,7 +4800,7 @@ namespace ts { kind, kindModifiers: ScriptElementKindModifier.none, sortText: name - } + }; } function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts index 2b5f57412de..11420fe4f37 100644 --- a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts @@ -22,11 +22,8 @@ // @Filename: node_modules/unlisted-module/index.js //// /*unlisted-module*/ -// @Filename: node_modules/@types/fake-module/other.d.ts -//// declare module "fake-module/other" {} - -// @Filename: node_modules/@types/unlisted-module/index.d.ts -//// /*unlisted-types*/ +// @Filename: ambient.ts +//// declare module "fake-module/other" const kinds = ["import_as", "import_equals", "require"]; diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts index 64e3cbad48e..5a96e4ee63e 100644 --- a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts @@ -20,7 +20,7 @@ // @Filename: dir1/package.json //// { "dependencies": { "fake-module2": "latest" } } -// @Filename: dir1/node_modules/@types/fake-module2/js.d.ts +// @Filename: dir1/node_modules/fake-module2/index.ts //// declare module "ambient-module-test" {} // @Filename: dir1/dir2/dir3/package.json @@ -34,7 +34,7 @@ for (const kind of kinds) { goTo.marker(kind + "0"); verify.completionListContains("fake-module/"); - verify.completionListContains("fake-module2/"); + verify.completionListContains("fake-module2"); verify.completionListContains("fake-module3/"); verify.not.completionListItemsCountIsGreaterThan(3); @@ -46,7 +46,7 @@ for (const kind of kinds) { goTo.marker(kind + "2"); verify.completionListContains("fake-module/"); - verify.completionListContains("fake-module2/"); + verify.completionListContains("fake-module2"); verify.completionListContains("fake-module3/"); verify.not.completionListItemsCountIsGreaterThan(3); } \ No newline at end of file diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings1.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings1.ts new file mode 100644 index 00000000000..53dc12033c9 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings1.ts @@ -0,0 +1,32 @@ +/// + +// @typeRoots: my_typings,my_other_typings + + +// @Filename: tests/test0.ts +//// import * as foo1 from "m/*import_as0*/ +//// import foo2 = require("m/*import_equals0*/ +//// var foo3 = require("m/*require0*/ + +// @Filename: my_typings/module-x/index.d.ts +//// export var x = 9; + +// @Filename: my_typings/module-x/whatever.d.ts +//// export var w = 9; + +// @Filename: my_typings/module-y/index.d.ts +//// export var y = 9; + +// @Filename: my_other_typings/module-z/index.d.ts +//// export var z = 9; + + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListContains("module-x"); + verify.completionListContains("module-y"); + verify.completionListContains("module-z"); + verify.not.completionListItemsCountIsGreaterThan(3); +} diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings2.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings2.ts new file mode 100644 index 00000000000..01e6f0ed420 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings2.ts @@ -0,0 +1,29 @@ +/// + +// @typeRoots: my_typings,my_other_typings +// @types: module-x,module-z + + +// @Filename: tests/test0.ts +//// import * as foo1 from "m/*import_as0*/ +//// import foo2 = require("m/*import_equals0*/ +//// var foo3 = require("m/*require0*/ + +// @Filename: my_typings/module-x/index.d.ts +//// export var x = 9; + +// @Filename: my_typings/module-y/index.d.ts +//// export var y = 9; + +// @Filename: my_other_typings/module-z/index.d.ts +//// export var z = 9; + + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListContains("module-x"); + verify.completionListContains("module-z"); + verify.not.completionListItemsCountIsGreaterThan(2); +} diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings3.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings3.ts new file mode 100644 index 00000000000..9710ce05cd5 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings3.ts @@ -0,0 +1,25 @@ +/// + +// @Filename: subdirectory/test0.ts +//// import * as foo1 from "m/*import_as0*/ +//// import foo2 = require("m/*import_equals0*/ +//// var foo3 = require("m/*require0*/ + +// @Filename: subdirectory/node_modules/@types/module-x/index.d.ts +//// export var x = 9; +// @Filename: subdirectory/package.json +//// { "dependencies": { "@types/module-x": "latest" } } + +// @Filename: node_modules/@types/module-y/index.d.ts +//// export var y = 9; +// @Filename: package.json +//// { "dependencies": { "@types/module-y": "latest" } } + +const kinds = ["import_as", "import_equals", "require"]; + +for (const kind of kinds) { + goTo.marker(kind + "0"); + verify.completionListContains("module-x"); + verify.completionListContains("module-y"); + verify.not.completionListItemsCountIsGreaterThan(2); +}