Skip to content

Commit dbf19f1

Browse files
committed
Adding import completions for typings
1 parent 0b16180 commit dbf19f1

7 files changed

+183
-45
lines changed

src/harness/fourslash.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ namespace FourSlash {
260260

261261
// Extend our existing compiler options so that we can also support tsconfig only options
262262
if (configJson.config.compilerOptions) {
263-
let baseDir = ts.normalizePath(ts.getDirectoryPath(file.fileName));
264-
let tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDir, file.fileName);
263+
const baseDirectory = ts.normalizePath(ts.getDirectoryPath(file.fileName));
264+
const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);
265265

266266
if (!tsConfig.errors || !tsConfig.errors.length) {
267267
compilationOptions = ts.extend(compilationOptions, tsConfig.options);

src/services/services.ts

+90-35
Original file line numberDiff line numberDiff line change
@@ -1968,7 +1968,7 @@ namespace ts {
19681968
* for completions.
19691969
* For example, this matches /// <reference path="fragment
19701970
*/
1971-
const tripleSlashDirectiveFragmentRegex = /^\/\/\/\s*<reference\s+path\s*=\s*(?:'|")([^'"]+)$/;
1971+
const tripleSlashDirectiveFragmentRegex = /^\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|")([^'"]+)$/;
19721972

19731973
let commandLineOptionsStringToEnum: CommandLineOptionOfCustomType[];
19741974

@@ -4501,7 +4501,7 @@ namespace ts {
45014501
result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false);
45024502

45034503
if (paths) {
4504-
for (var path in paths) {
4504+
for (const path in paths) {
45054505
if (paths.hasOwnProperty(path)) {
45064506
if (path === "*") {
45074507
if (paths[path]) {
@@ -4526,7 +4526,7 @@ namespace ts {
45264526
result = [];
45274527
}
45284528

4529-
4529+
getCompletionEntriesFromTypings(host, options, scriptPath, result);
45304530

45314531
forEach(enumeratePotentialNonRelativeModules(fragment, scriptPath), moduleName => {
45324532
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName));
@@ -4558,7 +4558,7 @@ namespace ts {
45584558
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
45594559
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
45604560
// doesn't support. For now, this is safer but slower
4561-
const includeGlob = normalizedSuffix ? "**/*" : "./*"
4561+
const includeGlob = normalizedSuffix ? "**/*" : "./*";
45624562

45634563
const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]);
45644564
const result: string[] = [];
@@ -4629,19 +4629,97 @@ namespace ts {
46294629
const text = sourceFile.text.substr(node.pos, position);
46304630
const match = tripleSlashDirectiveFragmentRegex.exec(text);
46314631
if (match) {
4632-
const fragment = match[1];
4633-
const scriptPath = getDirectoryPath(sourceFile.path);
4634-
return {
4635-
isMemberCompletion: false,
4636-
isNewIdentifierLocation: false,
4637-
entries: getCompletionEntriesForDirectoryFragment(fragment, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true)
4638-
};
4632+
const kind= match[1];
4633+
const fragment = match[2];
4634+
if (kind === "path") {
4635+
// Give completions for a relative path
4636+
const scriptPath = getDirectoryPath(sourceFile.path);
4637+
return {
4638+
isMemberCompletion: false,
4639+
isNewIdentifierLocation: false,
4640+
entries: getCompletionEntriesForDirectoryFragment(fragment, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true)
4641+
};
4642+
}
4643+
else {
4644+
// Give completions based on what is available in the types directory
4645+
}
46394646
}
46404647

46414648
return undefined;
46424649
}
46434650
}
46444651

4652+
function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, result: CompletionEntry[]): CompletionEntry[] {
4653+
// Check for typings specified in compiler options
4654+
if (options.types) {
4655+
forEach(options.types, moduleName => {
4656+
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName));
4657+
});
4658+
}
4659+
else if (options.typeRoots) {
4660+
const absoluteRoots = map(options.typeRoots, rootDirectory => getAbsoluteProjectPath(rootDirectory, host, options.project));
4661+
forEach(absoluteRoots, absoluteRoot => getCompletionEntriesFromDirectory(host, options, absoluteRoot, result));
4662+
}
4663+
4664+
// Also get all @types typings installed in visible node_modules directories
4665+
forEach(findPackageJsons(scriptPath), package => {
4666+
const typesDir = combinePaths(getDirectoryPath(package), "node_modules/@types");
4667+
getCompletionEntriesFromDirectory(host, options, typesDir, result);
4668+
});
4669+
4670+
return result;
4671+
}
4672+
4673+
function getAbsoluteProjectPath(path: string, host: LanguageServiceHost, projectDir?: string) {
4674+
if (isRootedDiskPath(path)) {
4675+
return normalizePath(path);
4676+
}
4677+
4678+
if (projectDir) {
4679+
return normalizePath(combinePaths(projectDir, path));
4680+
}
4681+
4682+
return normalizePath(host.resolvePath(path));
4683+
}
4684+
4685+
function getCompletionEntriesFromDirectory(host: LanguageServiceHost, options: CompilerOptions, directory: string, result: CompletionEntry[]) {
4686+
if (directoryProbablyExists(directory, host)) {
4687+
const typeDirectories = host.readDirectory(directory, getSupportedExtensions(options), /*exclude*/undefined, /*include*/["./*/*"]);
4688+
const seen: {[index: string]: boolean} = {};
4689+
forEach(typeDirectories, typeFile => {
4690+
const typeDirectory = getDirectoryPath(typeFile);
4691+
if (!hasProperty(seen, typeDirectory)) {
4692+
seen[typeDirectory] = true;
4693+
result.push(createCompletionEntryForModule(getBaseFileName(typeDirectory), ScriptElementKind.externalModuleName));
4694+
}
4695+
});
4696+
}
4697+
}
4698+
4699+
function findPackageJsons(currentDir: string): string[] {
4700+
const paths: string[] = [];
4701+
let currentConfigPath: string;
4702+
while (true) {
4703+
currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json");
4704+
if (currentConfigPath) {
4705+
paths.push(currentConfigPath);
4706+
4707+
currentDir = getDirectoryPath(currentConfigPath);
4708+
const parent = getDirectoryPath(currentDir);
4709+
if (currentDir === parent) {
4710+
break;
4711+
}
4712+
currentDir = parent;
4713+
}
4714+
else {
4715+
break;
4716+
}
4717+
}
4718+
4719+
return paths;
4720+
}
4721+
4722+
46454723
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) {
46464724
const result: VisibleModuleInfo[] = [];
46474725
findPackageJsons(scriptPath).forEach((packageJson) => {
@@ -4672,29 +4750,6 @@ namespace ts {
46724750

46734751
return result;
46744752

4675-
function findPackageJsons(currentDir: string): string[] {
4676-
const paths: string[] = [];
4677-
let currentConfigPath: string;
4678-
while (true) {
4679-
currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json");
4680-
if (currentConfigPath) {
4681-
paths.push(currentConfigPath);
4682-
4683-
currentDir = getDirectoryPath(currentConfigPath);
4684-
const parent = getDirectoryPath(currentDir);
4685-
if (currentDir === parent) {
4686-
break;
4687-
}
4688-
currentDir = parent;
4689-
}
4690-
else {
4691-
break;
4692-
}
4693-
}
4694-
4695-
return paths;
4696-
}
4697-
46984753
function tryReadingPackageJson(filePath: string) {
46994754
try {
47004755
const fileText = host.readFile(filePath);
@@ -4745,7 +4800,7 @@ namespace ts {
47454800
kind,
47464801
kindModifiers: ScriptElementKindModifier.none,
47474802
sortText: name
4748-
}
4803+
};
47494804
}
47504805

47514806
function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails {

tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@
2222
// @Filename: node_modules/unlisted-module/index.js
2323
//// /*unlisted-module*/
2424

25-
// @Filename: node_modules/@types/fake-module/other.d.ts
26-
//// declare module "fake-module/other" {}
27-
28-
// @Filename: node_modules/@types/unlisted-module/index.d.ts
29-
//// /*unlisted-types*/
25+
// @Filename: ambient.ts
26+
//// declare module "fake-module/other"
3027

3128
const kinds = ["import_as", "import_equals", "require"];
3229

tests/cases/fourslash/completionForStringLiteralNonrelativeImport4.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
// @Filename: dir1/package.json
2222
//// { "dependencies": { "fake-module2": "latest" } }
23-
// @Filename: dir1/node_modules/@types/fake-module2/js.d.ts
23+
// @Filename: dir1/node_modules/fake-module2/index.ts
2424
//// declare module "ambient-module-test" {}
2525

2626
// @Filename: dir1/dir2/dir3/package.json
@@ -34,7 +34,7 @@ for (const kind of kinds) {
3434
goTo.marker(kind + "0");
3535

3636
verify.completionListContains("fake-module/");
37-
verify.completionListContains("fake-module2/");
37+
verify.completionListContains("fake-module2");
3838
verify.completionListContains("fake-module3/");
3939
verify.not.completionListItemsCountIsGreaterThan(3);
4040

@@ -46,7 +46,7 @@ for (const kind of kinds) {
4646
goTo.marker(kind + "2");
4747

4848
verify.completionListContains("fake-module/");
49-
verify.completionListContains("fake-module2/");
49+
verify.completionListContains("fake-module2");
5050
verify.completionListContains("fake-module3/");
5151
verify.not.completionListItemsCountIsGreaterThan(3);
5252
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @typeRoots: my_typings,my_other_typings
4+
5+
6+
// @Filename: tests/test0.ts
7+
//// import * as foo1 from "m/*import_as0*/
8+
//// import foo2 = require("m/*import_equals0*/
9+
//// var foo3 = require("m/*require0*/
10+
11+
// @Filename: my_typings/module-x/index.d.ts
12+
//// export var x = 9;
13+
14+
// @Filename: my_typings/module-x/whatever.d.ts
15+
//// export var w = 9;
16+
17+
// @Filename: my_typings/module-y/index.d.ts
18+
//// export var y = 9;
19+
20+
// @Filename: my_other_typings/module-z/index.d.ts
21+
//// export var z = 9;
22+
23+
24+
const kinds = ["import_as", "import_equals", "require"];
25+
26+
for (const kind of kinds) {
27+
goTo.marker(kind + "0");
28+
verify.completionListContains("module-x");
29+
verify.completionListContains("module-y");
30+
verify.completionListContains("module-z");
31+
verify.not.completionListItemsCountIsGreaterThan(3);
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @typeRoots: my_typings,my_other_typings
4+
// @types: module-x,module-z
5+
6+
7+
// @Filename: tests/test0.ts
8+
//// import * as foo1 from "m/*import_as0*/
9+
//// import foo2 = require("m/*import_equals0*/
10+
//// var foo3 = require("m/*require0*/
11+
12+
// @Filename: my_typings/module-x/index.d.ts
13+
//// export var x = 9;
14+
15+
// @Filename: my_typings/module-y/index.d.ts
16+
//// export var y = 9;
17+
18+
// @Filename: my_other_typings/module-z/index.d.ts
19+
//// export var z = 9;
20+
21+
22+
const kinds = ["import_as", "import_equals", "require"];
23+
24+
for (const kind of kinds) {
25+
goTo.marker(kind + "0");
26+
verify.completionListContains("module-x");
27+
verify.completionListContains("module-z");
28+
verify.not.completionListItemsCountIsGreaterThan(2);
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: subdirectory/test0.ts
4+
//// import * as foo1 from "m/*import_as0*/
5+
//// import foo2 = require("m/*import_equals0*/
6+
//// var foo3 = require("m/*require0*/
7+
8+
// @Filename: subdirectory/node_modules/@types/module-x/index.d.ts
9+
//// export var x = 9;
10+
// @Filename: subdirectory/package.json
11+
//// { "dependencies": { "@types/module-x": "latest" } }
12+
13+
// @Filename: node_modules/@types/module-y/index.d.ts
14+
//// export var y = 9;
15+
// @Filename: package.json
16+
//// { "dependencies": { "@types/module-y": "latest" } }
17+
18+
const kinds = ["import_as", "import_equals", "require"];
19+
20+
for (const kind of kinds) {
21+
goTo.marker(kind + "0");
22+
verify.completionListContains("module-x");
23+
verify.completionListContains("module-y");
24+
verify.not.completionListItemsCountIsGreaterThan(2);
25+
}

0 commit comments

Comments
 (0)